66 * @author HoodyHuo (https://github.com/HoodyHuo)
77 * @author Chris Morbitzer (https://github.com/cmorbitzer)
88 * @author Sam Hulick (https://github.com/ffxsam)
9- * @autor Gustav Sollenius (https://github.com/gustavsollenius)
9+ * @author Gustav Sollenius (https://github.com/gustavsollenius)
10+ * @author Viktor Jevdokimov (https://github.com/vitar)
1011 *
1112 * @example
1213 * // ... initialising wavesurfer with the plugin
@@ -65,12 +66,19 @@ class ZoomPlugin extends BasePlugin<ZoomPluginEvents, ZoomPluginOptions> {
6566 protected options : ZoomPluginOptions & typeof defaultOptions
6667 private wrapper : HTMLElement | undefined = undefined
6768 private container : HTMLElement | null = null
69+
70+ // State for wheel zoom
6871 private accumulatedDelta = 0
6972 private pointerTime : number = 0
7073 private oldX : number = 0
7174 private endZoom : number = 0
7275 private startZoom : number = 0
7376
77+ // State for proportional pinch-to-zoom
78+ private isPinching = false
79+ private initialPinchDistance = 0
80+ private initialZoom = 0
81+
7482 constructor ( options ?: ZoomPluginOptions ) {
7583 super ( options || { } )
7684 this . options = Object . assign ( { } , defaultOptions , options )
@@ -86,7 +94,12 @@ class ZoomPlugin extends BasePlugin<ZoomPluginEvents, ZoomPluginOptions> {
8694 return
8795 }
8896 this . container = this . wrapper . parentElement as HTMLElement
97+
8998 this . container . addEventListener ( 'wheel' , this . onWheel )
99+ this . container . addEventListener ( 'touchstart' , this . onTouchStart , { passive : false , capture : true } )
100+ this . container . addEventListener ( 'touchmove' , this . onTouchMove , { passive : false , capture : true } )
101+ this . container . addEventListener ( 'touchend' , this . onTouchEnd , { passive : false , capture : true } )
102+ this . container . addEventListener ( 'touchcancel' , this . onTouchEnd , { passive : false , capture : true } )
90103
91104 if ( typeof this . options . maxZoom === 'undefined' ) {
92105 this . options . maxZoom = this . container . clientWidth
@@ -156,9 +169,91 @@ class ZoomPlugin extends BasePlugin<ZoomPluginEvents, ZoomPluginOptions> {
156169 return Math . min ( newZoom , this . options . maxZoom ! )
157170 }
158171
172+ private getTouchDistance ( e : TouchEvent ) : number {
173+ const touch1 = e . touches [ 0 ]
174+ const touch2 = e . touches [ 1 ]
175+ return Math . sqrt ( Math . pow ( touch2 . clientX - touch1 . clientX , 2 ) + Math . pow ( touch2 . clientY - touch1 . clientY , 2 ) )
176+ }
177+
178+ private getTouchCenterX ( e : TouchEvent ) : number {
179+ const touch1 = e . touches [ 0 ]
180+ const touch2 = e . touches [ 1 ]
181+ return ( touch1 . clientX + touch2 . clientX ) / 2
182+ }
183+
184+ private onTouchStart = ( e : TouchEvent ) => {
185+ if ( ! this . wavesurfer || ! this . container ) return
186+ // Check if two fingers are used
187+ if ( e . touches . length === 2 ) {
188+ e . preventDefault ( )
189+ this . isPinching = true
190+
191+ // Store initial pinch distance
192+ this . initialPinchDistance = this . getTouchDistance ( e )
193+
194+ // Store initial zoom level
195+ const duration = this . wavesurfer . getDuration ( )
196+ this . initialZoom =
197+ this . wavesurfer . options . minPxPerSec === 0
198+ ? this . wavesurfer . getWrapper ( ) . scrollWidth / duration
199+ : this . wavesurfer . options . minPxPerSec
200+
201+ // Store anchor point for zooming
202+ const x = this . getTouchCenterX ( e ) - this . container . getBoundingClientRect ( ) . left
203+ const scrollX = this . wavesurfer . getScroll ( )
204+ this . pointerTime = ( scrollX + x ) / this . initialZoom
205+ this . oldX = x // Use oldX to store the anchor X position
206+ }
207+ }
208+
209+ private onTouchMove = ( e : TouchEvent ) => {
210+ if ( ! this . isPinching || e . touches . length !== 2 || ! this . wavesurfer || ! this . container ) {
211+ return
212+ }
213+ e . preventDefault ( )
214+
215+ // Calculate new zoom level
216+ const newDistance = this . getTouchDistance ( e )
217+ const scaleFactor = newDistance / this . initialPinchDistance
218+ let newMinPxPerSec = this . initialZoom * scaleFactor
219+
220+ // Constrain the zoom
221+ newMinPxPerSec = Math . min ( newMinPxPerSec , this . options . maxZoom ! )
222+
223+ // Calculate minimum zoom (fit to width)
224+ const duration = this . wavesurfer . getDuration ( )
225+ const width = this . container . clientWidth
226+ const minZoom = width / duration
227+ if ( newMinPxPerSec < minZoom ) {
228+ newMinPxPerSec = minZoom
229+ }
230+
231+ // Apply zoom and scroll
232+ const newLeftSec = ( width / newMinPxPerSec ) * ( this . oldX / width )
233+ if ( newMinPxPerSec === minZoom ) {
234+ this . wavesurfer . zoom ( minZoom )
235+ this . container . scrollLeft = 0
236+ } else {
237+ this . wavesurfer . zoom ( newMinPxPerSec )
238+ this . container . scrollLeft = ( this . pointerTime - newLeftSec ) * newMinPxPerSec
239+ }
240+ }
241+
242+ private onTouchEnd = ( e : TouchEvent ) => {
243+ if ( this . isPinching && e . touches . length < 2 ) {
244+ this . isPinching = false
245+ this . initialPinchDistance = 0
246+ this . initialZoom = 0
247+ }
248+ }
249+
159250 destroy ( ) {
160251 if ( this . container ) {
161252 this . container . removeEventListener ( 'wheel' , this . onWheel )
253+ this . container . removeEventListener ( 'touchstart' , this . onTouchStart )
254+ this . container . removeEventListener ( 'touchmove' , this . onTouchMove )
255+ this . container . removeEventListener ( 'touchend' , this . onTouchEnd )
256+ this . container . removeEventListener ( 'touchcancel' , this . onTouchEnd )
162257 }
163258 super . destroy ( )
164259 }
0 commit comments