Skip to content

Commit fd5b8d7

Browse files
committed
add pinch-to-zoom and wheel zoom support
Support zooming via touch pinch gestures (mobile) and trackpad/mouse wheel (desktop) through the existing onRequestScaleChange callback. Closes #368
1 parent 9421c64 commit fd5b8d7

1 file changed

Lines changed: 68 additions & 7 deletions

File tree

packages/lib/src/index.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)