@@ -17,22 +17,40 @@ class InputHandler {
1717 this . _isRotating = false ;
1818 this . _isPanning = false ;
1919
20+ // Multi-touch gesture state
21+ this . _activeTouches = new Map ( ) ; // id -> {x, y}
22+ this . _prevTouchDist = null ;
23+ this . _prevTouchAngle = null ;
24+ this . _prevTouchCentroid = null ;
25+
26+ // Prevent browser scroll/zoom on the canvas
27+ canvas . style . touchAction = 'none' ;
28+
2029 // Bind handlers for clean removal
2130 this . _onPointerDown = this . _onPointerDown . bind ( this ) ;
2231 this . _onPointerUp = this . _onPointerUp . bind ( this ) ;
2332 this . _onPointerMove = this . _onPointerMove . bind ( this ) ;
2433 this . _onWheel = this . _onWheel . bind ( this ) ;
2534 this . _onDblClick = this . _onDblClick . bind ( this ) ;
2635 this . _onContextMenu = ( ev ) => ev . preventDefault ( ) ;
36+ this . _onTouchStart = this . _onTouchStart . bind ( this ) ;
37+ this . _onTouchMove = this . _onTouchMove . bind ( this ) ;
38+ this . _onTouchEnd = this . _onTouchEnd . bind ( this ) ;
2739
2840 canvas . addEventListener ( 'pointerdown' , this . _onPointerDown ) ;
2941 canvas . addEventListener ( 'pointerup' , this . _onPointerUp ) ;
3042 canvas . addEventListener ( 'pointermove' , this . _onPointerMove ) ;
3143 canvas . addEventListener ( 'wheel' , this . _onWheel , { passive : false } ) ;
3244 canvas . addEventListener ( 'dblclick' , this . _onDblClick ) ;
3345 canvas . addEventListener ( 'contextmenu' , this . _onContextMenu ) ;
46+ canvas . addEventListener ( 'touchstart' , this . _onTouchStart , { passive : false } ) ;
47+ canvas . addEventListener ( 'touchmove' , this . _onTouchMove , { passive : false } ) ;
48+ canvas . addEventListener ( 'touchend' , this . _onTouchEnd ) ;
49+ canvas . addEventListener ( 'touchcancel' , this . _onTouchEnd ) ;
3450 }
3551
52+ // --- Pointer events (mouse & single touch fallback) ---
53+
3654 _onPointerDown ( ev ) {
3755 ev . preventDefault ( ) ;
3856 if ( ev . button === 0 && ! ev . shiftKey && ! ev . ctrlKey && ! ev . altKey ) {
@@ -49,6 +67,9 @@ class InputHandler {
4967 }
5068
5169 _onPointerMove ( ev ) {
70+ // Suppress pointer-based rotation/pan while a multi-touch gesture is active
71+ if ( this . _activeTouches . size >= 2 ) return ;
72+
5273 const t = this . camera . transform ;
5374 if ( this . _isRotating ) {
5475 t . rotate ( 0.3 * ev . movementY , 0.3 * ev . movementX ) ;
@@ -82,6 +103,111 @@ class InputHandler {
82103 }
83104 }
84105
106+ // --- Multi-touch gesture handling ---
107+
108+ _onTouchStart ( ev ) {
109+ ev . preventDefault ( ) ;
110+ for ( const touch of ev . changedTouches ) {
111+ this . _activeTouches . set ( touch . identifier , { x : touch . clientX , y : touch . clientY } ) ;
112+ }
113+ if ( this . _activeTouches . size >= 2 ) {
114+ this . _initGestureState ( ) ;
115+ }
116+ }
117+
118+ _onTouchMove ( ev ) {
119+ ev . preventDefault ( ) ;
120+ for ( const touch of ev . changedTouches ) {
121+ if ( this . _activeTouches . has ( touch . identifier ) ) {
122+ this . _activeTouches . set ( touch . identifier , { x : touch . clientX , y : touch . clientY } ) ;
123+ }
124+ }
125+ if ( this . _activeTouches . size >= 2 ) {
126+ this . _handleMultiTouchGesture ( ) ;
127+ }
128+ }
129+
130+ _onTouchEnd ( ev ) {
131+ for ( const touch of ev . changedTouches ) {
132+ this . _activeTouches . delete ( touch . identifier ) ;
133+ }
134+ // Reset gesture state when fewer than 2 touches remain
135+ if ( this . _activeTouches . size < 2 ) {
136+ this . _prevTouchDist = null ;
137+ this . _prevTouchAngle = null ;
138+ this . _prevTouchCentroid = null ;
139+ }
140+ }
141+
142+ /** Compute initial distance, angle, and centroid for a two-finger gesture. */
143+ _initGestureState ( ) {
144+ const [ a , b ] = this . _getTwoTouches ( ) ;
145+ this . _prevTouchDist = this . _distance ( a , b ) ;
146+ this . _prevTouchAngle = this . _angle ( a , b ) ;
147+ this . _prevTouchCentroid = this . _centroid ( a , b ) ;
148+ }
149+
150+ /** Process ongoing two-finger gesture: pinch-zoom, rotation, and pan. */
151+ _handleMultiTouchGesture ( ) {
152+ const [ a , b ] = this . _getTwoTouches ( ) ;
153+ const dist = this . _distance ( a , b ) ;
154+ const angle = this . _angle ( a , b ) ;
155+ const centroid = this . _centroid ( a , b ) ;
156+ const t = this . camera . transform ;
157+
158+ // Pinch-to-zoom
159+ if ( this . _prevTouchDist != null && this . _prevTouchDist > 0 ) {
160+ const scaleFactor = dist / this . _prevTouchDist ;
161+ t . scale ( scaleFactor , t . _center ) ;
162+ }
163+
164+ // Two-finger rotation
165+ if ( this . _prevTouchAngle != null ) {
166+ let angleDelta = angle - this . _prevTouchAngle ;
167+ // Normalize to [-PI, PI]
168+ if ( angleDelta > Math . PI ) angleDelta -= 2 * Math . PI ;
169+ if ( angleDelta < - Math . PI ) angleDelta += 2 * Math . PI ;
170+ const degrees = angleDelta * ( 180 / Math . PI ) ;
171+ t . rotate ( 0 , degrees ) ;
172+ }
173+
174+ // Two-finger pan (centroid movement)
175+ if ( this . _prevTouchCentroid != null ) {
176+ const dx = centroid . x - this . _prevTouchCentroid . x ;
177+ const dy = centroid . y - this . _prevTouchCentroid . y ;
178+ t . translate ( 0.01 * dx , - 0.01 * dy ) ;
179+ }
180+
181+ this . _prevTouchDist = dist ;
182+ this . _prevTouchAngle = angle ;
183+ this . _prevTouchCentroid = centroid ;
184+
185+ this . camera . _notify ( ) ;
186+ this . onRender ( ) ;
187+ }
188+
189+ /** Return the first two active touch positions. */
190+ _getTwoTouches ( ) {
191+ const iter = this . _activeTouches . values ( ) ;
192+ return [ iter . next ( ) . value , iter . next ( ) . value ] ;
193+ }
194+
195+ _distance ( a , b ) {
196+ const dx = b . x - a . x ;
197+ const dy = b . y - a . y ;
198+ return Math . sqrt ( dx * dx + dy * dy ) ;
199+ }
200+
201+ _angle ( a , b ) {
202+ return Math . atan2 ( b . y - a . y , b . x - a . x ) ;
203+ }
204+
205+ _centroid ( a , b ) {
206+ return { x : ( a . x + b . x ) / 2 , y : ( a . y + b . y ) / 2 } ;
207+ }
208+
209+ // --- Cleanup ---
210+
85211 dispose ( ) {
86212 const c = this . canvas ;
87213 c . removeEventListener ( 'pointerdown' , this . _onPointerDown ) ;
@@ -90,6 +216,10 @@ class InputHandler {
90216 c . removeEventListener ( 'wheel' , this . _onWheel ) ;
91217 c . removeEventListener ( 'dblclick' , this . _onDblClick ) ;
92218 c . removeEventListener ( 'contextmenu' , this . _onContextMenu ) ;
219+ c . removeEventListener ( 'touchstart' , this . _onTouchStart ) ;
220+ c . removeEventListener ( 'touchmove' , this . _onTouchMove ) ;
221+ c . removeEventListener ( 'touchend' , this . _onTouchEnd ) ;
222+ c . removeEventListener ( 'touchcancel' , this . _onTouchEnd ) ;
93223 }
94224}
95225
0 commit comments