@@ -33,6 +33,7 @@ export interface Props extends AvatarEditorConfig {
3333 onMouseMove ?: ( e : TouchEvent | MouseEvent ) => void
3434 onPositionChange ?: ( position : Position ) => void
3535 onRequestScaleChange ?: ( scale : number ) => void
36+ enableWheelZoom ?: boolean
3637}
3738
3839export type { Position , ImageState }
@@ -70,6 +71,7 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
7071 onMouseMove,
7172 onPositionChange,
7273 onRequestScaleChange,
74+ enableWheelZoom = false ,
7375 borderColor,
7476 style,
7577 keyboardStep = 1 ,
@@ -102,14 +104,23 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
102104 const mxRef = useRef < number | undefined > ( undefined )
103105 const myRef = useRef < number | undefined > ( undefined )
104106
107+ // Pinch-to-zoom state
108+ const pinchRef = useRef ( false )
109+ const pinchStartDistRef = useRef < number > ( 0 )
110+ const pinchStartScaleRef = useRef < number > ( 1 )
111+
105112 // Keep state for `drag` and `loading` to trigger re-renders
106113 const [ drag , setDrag ] = useState ( false )
107114 const [ loading , setLoading ] = useState ( false )
108115 const [ imageState , setImageState ] = useState < ImageState > (
109116 coreRef . current . getImageState ( ) ,
110117 )
111118
112- // Store latest callback props in refs so document handlers always call current versions
119+ // Store latest prop values in refs so document handlers always have current versions
120+ const scaleRef = useRef ( scale )
121+ scaleRef . current = scale
122+ const enableWheelZoomRef = useRef ( enableWheelZoom )
123+ enableWheelZoomRef . current = enableWheelZoom
113124 const onMouseUpRef = useRef ( onMouseUp )
114125 onMouseUpRef . current = onMouseUp
115126 const onMouseMoveRef = useRef ( onMouseMove )
@@ -243,13 +254,26 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
243254 [ ] ,
244255 )
245256
246- const handleTouchStart : TouchEventHandler < HTMLCanvasElement > =
247- useCallback ( ( ) => {
248- dragRef . current = true
249- mxRef . current = undefined
250- myRef . current = undefined
251- setDrag ( true )
252- } , [ ] )
257+ const handleTouchStart : TouchEventHandler < HTMLCanvasElement > = useCallback (
258+ ( e ) => {
259+ if ( e . touches . length === 2 ) {
260+ // Start pinch-to-zoom
261+ pinchRef . current = true
262+ dragRef . current = false
263+ setDrag ( false )
264+ const dx = e . touches [ 0 ] . pageX - e . touches [ 1 ] . pageX
265+ const dy = e . touches [ 0 ] . pageY - e . touches [ 1 ] . pageY
266+ pinchStartDistRef . current = Math . sqrt ( dx * dx + dy * dy )
267+ pinchStartScaleRef . current = scale
268+ } else {
269+ dragRef . current = true
270+ mxRef . current = undefined
271+ myRef . current = undefined
272+ setDrag ( true )
273+ }
274+ } ,
275+ [ scale ] ,
276+ )
253277
254278 const handleKeyDown : KeyboardEventHandler < HTMLCanvasElement > = useCallback (
255279 ( e ) => {
@@ -323,6 +347,29 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
323347 coreRef . current . paint ( context )
324348
325349 const handleDocumentMouseMove = ( e : MouseEvent | TouchEvent ) => {
350+ // Handle pinch-to-zoom (2 finger touch)
351+ if ( 'touches' in e && e . touches . length === 2 ) {
352+ if ( e . cancelable ) e . preventDefault ( )
353+ const dx = e . touches [ 0 ] . pageX - e . touches [ 1 ] . pageX
354+ const dy = e . touches [ 0 ] . pageY - e . touches [ 1 ] . pageY
355+ const dist = Math . sqrt ( dx * dx + dy * dy )
356+
357+ if ( ! pinchRef . current ) {
358+ // Start pinch (e.g. second finger added mid-drag)
359+ pinchRef . current = true
360+ dragRef . current = false
361+ setDrag ( false )
362+ pinchStartDistRef . current = dist
363+ pinchStartScaleRef . current = scaleRef . current
364+ return
365+ }
366+
367+ const ratio = dist / pinchStartDistRef . current
368+ const newScale = Math . max ( 0.1 , pinchStartScaleRef . current * ratio )
369+ onRequestScaleChangeRef . current ?.( newScale )
370+ return
371+ }
372+
326373 if ( ! dragRef . current ) {
327374 return
328375 }
@@ -362,24 +409,42 @@ const AvatarEditor = forwardRef<AvatarEditorRef, Props>((props, ref) => {
362409 }
363410
364411 const handleDocumentMouseUp = ( ) => {
412+ if ( pinchRef . current ) {
413+ pinchRef . current = false
414+ }
365415 if ( dragRef . current ) {
366416 dragRef . current = false
367417 setDrag ( false )
368418 onMouseUpRef . current ?.( )
369419 }
370420 }
371421
422+ const handleWheel = ( e : WheelEvent ) => {
423+ if ( ! enableWheelZoomRef . current || ! onRequestScaleChangeRef . current )
424+ return
425+ e . preventDefault ( )
426+
427+ // ctrlKey is set for trackpad pinch gestures; use finer sensitivity
428+ const sensitivity = e . ctrlKey ? 0.01 : 0.002
429+ const delta = - e . deltaY * sensitivity
430+ const currentScale = scaleRef . current
431+ onRequestScaleChangeRef . current ( Math . max ( 0.1 , currentScale + delta ) )
432+ }
433+
434+ const canvasEl = canvas . current
372435 const options = isPassiveSupported ( ) ? { passive : false } : false
373436 document . addEventListener ( 'mousemove' , handleDocumentMouseMove , options )
374437 document . addEventListener ( 'mouseup' , handleDocumentMouseUp , options )
375438 document . addEventListener ( 'touchmove' , handleDocumentMouseMove , options )
376439 document . addEventListener ( 'touchend' , handleDocumentMouseUp , options )
440+ canvasEl ?. addEventListener ( 'wheel' , handleWheel , { passive : false } )
377441
378442 return ( ) => {
379443 document . removeEventListener ( 'mousemove' , handleDocumentMouseMove , false )
380444 document . removeEventListener ( 'mouseup' , handleDocumentMouseUp , false )
381445 document . removeEventListener ( 'touchmove' , handleDocumentMouseMove , false )
382446 document . removeEventListener ( 'touchend' , handleDocumentMouseUp , false )
447+ canvasEl ?. removeEventListener ( 'wheel' , handleWheel )
383448 }
384449 } , [ ] )
385450
0 commit comments