From 50e779b6c481b6a38a295510a2da4ae1ceb68d7c Mon Sep 17 00:00:00 2001 From: Gustav Sollenius <94722719+gustavsollenius@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:28:04 +0100 Subject: [PATCH] Feat(Zoom): Add exponential zooming and a few bugfixes (#3934) * Added exponential zooming and a few bugfixes * Adressed review comments --------- Co-authored-by: gustavso --- src/plugins/zoom.ts | 63 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/plugins/zoom.ts b/src/plugins/zoom.ts index 077f585e2..c3fb61c28 100644 --- a/src/plugins/zoom.ts +++ b/src/plugins/zoom.ts @@ -6,6 +6,7 @@ * @author HoodyHuo (https://github.com/HoodyHuo) * @author Chris Morbitzer (https://github.com/cmorbitzer) * @author Sam Hulick (https://github.com/ffxsam) + * @autor Gustav Sollenius (https://github.com/gustavsollenius) * * @example * // ... initialising wavesurfer with the plugin @@ -36,10 +37,26 @@ export type ZoomPluginOptions = { * @default 5 */ deltaThreshold?: number + /** + * Whether to zoom into the waveform using a consistent exponential factor instead of a linear scale. + * Exponential zooming ensures the zoom steps feel uniform regardless of scale. + * When disabled, the zooming is linear and influenced by the `scale` parameter. + * + * @default false + */ + exponentialZooming?: boolean + /** + * Number of steps required to zoom from the initial zoom level to `maxZoom`. + * + * @default 20 + */ + iterations?: number } const defaultOptions = { scale: 0.5, deltaThreshold: 5, + exponentialZooming: false, + iterations: 20, } export type ZoomPluginEvents = BasePluginEvents @@ -49,6 +66,10 @@ class ZoomPlugin extends BasePlugin { private wrapper: HTMLElement | undefined = undefined private container: HTMLElement | null = null private accumulatedDelta = 0 + private pointerTime: number = 0 + private oldX: number = 0 + private endZoom: number = 0 + private startZoom: number = 0 constructor(options?: ZoomPluginOptions) { super(options || {}) @@ -65,7 +86,12 @@ class ZoomPlugin extends BasePlugin { return } this.container = this.wrapper.parentElement as HTMLElement - this.wrapper.addEventListener('wheel', this.onWheel) + this.container.addEventListener('wheel', this.onWheel) + + if (typeof this.options.maxZoom === 'undefined') { + this.options.maxZoom = this.container.clientWidth + } + this.endZoom = this.options.maxZoom } private onWheel = (e: WheelEvent) => { @@ -78,14 +104,27 @@ class ZoomPlugin extends BasePlugin { // Update the accumulated delta... this.accumulatedDelta += -e.deltaY + if (this.startZoom === 0 && this.options.exponentialZooming) { + this.startZoom = this.wavesurfer.getWrapper().clientWidth / this.wavesurfer.getDuration() + } + // ...and only scroll once we've hit our threshold if (this.options.deltaThreshold === 0 || Math.abs(this.accumulatedDelta) >= this.options.deltaThreshold) { const duration = this.wavesurfer.getDuration() - const oldMinPxPerSec = this.wavesurfer.options.minPxPerSec - const x = e.clientX + const oldMinPxPerSec = + this.wavesurfer.options.minPxPerSec === 0 + ? this.wavesurfer.getWrapper().scrollWidth / duration + : this.wavesurfer.options.minPxPerSec + const x = e.clientX - this.container.getBoundingClientRect().left const width = this.container.clientWidth const scrollX = this.wavesurfer.getScroll() - const pointerTime = (scrollX + x) / oldMinPxPerSec + + // Update pointerTime only if the pointer position has changed. This prevents the waveform from drifting during fixed zooming. + if (x !== this.oldX || this.oldX === 0) { + this.pointerTime = (scrollX + x) / oldMinPxPerSec + } + this.oldX = x + const newMinPxPerSec = this.calculateNewZoom(oldMinPxPerSec, this.accumulatedDelta) const newLeftSec = (width / newMinPxPerSec) * (x / width) @@ -94,7 +133,7 @@ class ZoomPlugin extends BasePlugin { this.container.scrollLeft = 0 } else { this.wavesurfer.zoom(newMinPxPerSec) - this.container.scrollLeft = (pointerTime - newLeftSec) * newMinPxPerSec + this.container.scrollLeft = (this.pointerTime - newLeftSec) * newMinPxPerSec } // Reset the accumulated delta @@ -103,8 +142,18 @@ class ZoomPlugin extends BasePlugin { } private calculateNewZoom = (oldZoom: number, delta: number) => { - const newZoom = Math.max(0, oldZoom + delta * this.options.scale) - return typeof this.options.maxZoom === 'undefined' ? newZoom : Math.min(newZoom, this.options.maxZoom) + let newZoom + if (this.options.exponentialZooming) { + const zoomFactor = + delta > 0 + ? Math.pow(this.endZoom / this.startZoom, 1 / (this.options.iterations - 1)) + : Math.pow(this.startZoom / this.endZoom, 1 / (this.options.iterations - 1)) + newZoom = Math.max(0, oldZoom * zoomFactor) + } else { + // Default linear zooming + newZoom = Math.max(0, oldZoom + delta * this.options.scale) + } + return Math.min(newZoom, this.options.maxZoom!) } destroy() {