@@ -102,14 +102,21 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
102102 const mxRef = useRef < number | undefined > ( undefined )
103103 const myRef = useRef < number | undefined > ( undefined )
104104
105+ // Pinch-to-zoom state
106+ const pinchRef = useRef ( false )
107+ const pinchStartDistRef = useRef < number > ( 0 )
108+ const pinchStartScaleRef = useRef < number > ( 1 )
109+
105110 // Keep state for `drag` and `loading` to trigger re-renders
106111 const [ drag , setDrag ] = useState ( false )
107112 const [ loading , setLoading ] = useState ( false )
108113 const [ imageState , setImageState ] = useState < ImageState > (
109114 coreRef . current . getImageState ( ) ,
110115 )
111116
112- // Store latest callback props in refs so document handlers always call current versions
117+ // Store latest prop values in refs so document handlers always have current versions
118+ const scaleRef = useRef ( scale )
119+ scaleRef . current = scale
113120 const onMouseUpRef = useRef ( onMouseUp )
114121 onMouseUpRef . current = onMouseUp
115122 const onMouseMoveRef = useRef ( onMouseMove )
@@ -244,12 +251,26 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
244251 )
245252
246253 const handleTouchStart : TouchEventHandler < HTMLCanvasElement > =
247- useCallback ( ( ) => {
248- dragRef . current = true
249- mxRef . current = undefined
250- myRef . current = undefined
251- setDrag ( true )
252- } , [ ] )
254+ useCallback (
255+ ( e ) => {
256+ if ( e . touches . length === 2 ) {
257+ // Start pinch-to-zoom
258+ pinchRef . current = true
259+ dragRef . current = false
260+ setDrag ( false )
261+ const dx = e . touches [ 0 ] . pageX - e . touches [ 1 ] . pageX
262+ const dy = e . touches [ 0 ] . pageY - e . touches [ 1 ] . pageY
263+ pinchStartDistRef . current = Math . sqrt ( dx * dx + dy * dy )
264+ pinchStartScaleRef . current = scale
265+ } else {
266+ dragRef . current = true
267+ mxRef . current = undefined
268+ myRef . current = undefined
269+ setDrag ( true )
270+ }
271+ } ,
272+ [ scale ] ,
273+ )
253274
254275 const handleKeyDown : KeyboardEventHandler < HTMLCanvasElement > = useCallback (
255276 ( e ) => {
@@ -323,6 +344,29 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
323344 coreRef . current . paint ( context )
324345
325346 const handleDocumentMouseMove = ( e : MouseEvent | TouchEvent ) => {
347+ // Handle pinch-to-zoom (2 finger touch)
348+ if ( 'touches' in e && e . touches . length === 2 ) {
349+ if ( e . cancelable ) e . preventDefault ( )
350+ const dx = e . touches [ 0 ] . pageX - e . touches [ 1 ] . pageX
351+ const dy = e . touches [ 0 ] . pageY - e . touches [ 1 ] . pageY
352+ const dist = Math . sqrt ( dx * dx + dy * dy )
353+
354+ if ( ! pinchRef . current ) {
355+ // Start pinch (e.g. second finger added mid-drag)
356+ pinchRef . current = true
357+ dragRef . current = false
358+ setDrag ( false )
359+ pinchStartDistRef . current = dist
360+ pinchStartScaleRef . current = scaleRef . current
361+ return
362+ }
363+
364+ const ratio = dist / pinchStartDistRef . current
365+ const newScale = Math . max ( 0.1 , pinchStartScaleRef . current * ratio )
366+ onRequestScaleChangeRef . current ?.( newScale )
367+ return
368+ }
369+
326370 if ( ! dragRef . current ) {
327371 return
328372 }
@@ -362,24 +406,41 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
362406 }
363407
364408 const handleDocumentMouseUp = ( ) => {
409+ if ( pinchRef . current ) {
410+ pinchRef . current = false
411+ }
365412 if ( dragRef . current ) {
366413 dragRef . current = false
367414 setDrag ( false )
368415 onMouseUpRef . current ?.( )
369416 }
370417 }
371418
419+ const handleWheel = ( e : WheelEvent ) => {
420+ if ( ! onRequestScaleChangeRef . current ) return
421+ e . preventDefault ( )
422+
423+ // ctrlKey is set for trackpad pinch gestures; use finer sensitivity
424+ const sensitivity = e . ctrlKey ? 0.01 : 0.002
425+ const delta = - e . deltaY * sensitivity
426+ const currentScale = scaleRef . current
427+ onRequestScaleChangeRef . current ( Math . max ( 0.1 , currentScale + delta ) )
428+ }
429+
430+ const canvasEl = canvas . current
372431 const options = isPassiveSupported ( ) ? { passive : false } : false
373432 document . addEventListener ( 'mousemove' , handleDocumentMouseMove , options )
374433 document . addEventListener ( 'mouseup' , handleDocumentMouseUp , options )
375434 document . addEventListener ( 'touchmove' , handleDocumentMouseMove , options )
376435 document . addEventListener ( 'touchend' , handleDocumentMouseUp , options )
436+ canvasEl ?. addEventListener ( 'wheel' , handleWheel , { passive : false } )
377437
378438 return ( ) => {
379439 document . removeEventListener ( 'mousemove' , handleDocumentMouseMove , false )
380440 document . removeEventListener ( 'mouseup' , handleDocumentMouseUp , false )
381441 document . removeEventListener ( 'touchmove' , handleDocumentMouseMove , false )
382442 document . removeEventListener ( 'touchend' , handleDocumentMouseUp , false )
443+ canvasEl ?. removeEventListener ( 'wheel' , handleWheel )
383444 }
384445 } , [ ] )
385446
0 commit comments