|
| 1 | +import { RefObject, useEffect, useMemo, useState } from 'react'; |
| 2 | + |
| 3 | +export type CacheRef = { |
| 4 | + prevDiff: number; |
| 5 | + evCache: Array<PointerEvent>; |
| 6 | +}; |
| 7 | + |
| 8 | +export enum ZoomState { |
| 9 | + 'ZOOMING_IN' = 'ZOOMING_IN', |
| 10 | + 'ZOOMING_OUT' = 'ZOOMING_OUT', |
| 11 | +} |
| 12 | + |
| 13 | +export type ZoomStateType = ZoomState.ZOOMING_IN | ZoomState.ZOOMING_OUT; |
| 14 | + |
| 15 | +const usePinchZoom = (ref: RefObject<HTMLElement>) => { |
| 16 | + const cacheRef = useMemo<CacheRef>( |
| 17 | + () => ({ |
| 18 | + evCache: [], |
| 19 | + prevDiff: -1, |
| 20 | + }), |
| 21 | + [ref.current] |
| 22 | + ); |
| 23 | + |
| 24 | + const [zoomingState, setZoomingState] = useState<[ZoomStateType, number]>(); |
| 25 | + |
| 26 | + const pointermove_handler = (ev: PointerEvent) => { |
| 27 | + // This function implements a 2-pointer horizontal pinch/zoom gesture. |
| 28 | + // |
| 29 | + // If the distance between the two pointers has increased (zoom in), |
| 30 | + // the target element's background is changed to 'pink' and if the |
| 31 | + // distance is decreasing (zoom out), the color is changed to 'lightblue'. |
| 32 | + // |
| 33 | + // This function sets the target element's border to 'dashed' to visually |
| 34 | + // indicate the pointer's target received a move event. |
| 35 | + // Find this event in the cache and update its record with this event |
| 36 | + for (let i = 0; i < cacheRef.evCache.length; i++) { |
| 37 | + if (ev.pointerId == cacheRef.evCache[i].pointerId) { |
| 38 | + cacheRef.evCache[i] = ev; |
| 39 | + break; |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + // If two pointers are down, check for pinch gestures |
| 44 | + if (cacheRef.evCache.length == 2) { |
| 45 | + // console.log(prevDiff) |
| 46 | + // Calculate the distance between the two pointers |
| 47 | + const curDiff = Math.abs(cacheRef.evCache[0].clientX - cacheRef.evCache[1].clientX); |
| 48 | + |
| 49 | + if (cacheRef.prevDiff > 0) { |
| 50 | + if (curDiff > cacheRef.prevDiff) { |
| 51 | + // The distance between the two pointers has increased |
| 52 | + setZoomingState([ZoomState.ZOOMING_IN, curDiff]); |
| 53 | + } |
| 54 | + if (curDiff < cacheRef.prevDiff) { |
| 55 | + // The distance between the two pointers has decreased |
| 56 | + setZoomingState([ZoomState.ZOOMING_OUT, curDiff]); |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + // Cache the distance for the next move event |
| 61 | + cacheRef.prevDiff = curDiff; |
| 62 | + } |
| 63 | + }; |
| 64 | + |
| 65 | + const pointerdown_handler = (ev: PointerEvent) => { |
| 66 | + // The pointerdown event signals the start of a touch interaction. |
| 67 | + // This event is cached to support 2-finger gestures |
| 68 | + cacheRef.evCache.push(ev); |
| 69 | + // console.log('pointerDown', ev); |
| 70 | + }; |
| 71 | + |
| 72 | + const pointerup_handler = (ev: PointerEvent) => { |
| 73 | + // Remove this pointer from the cache and reset the target's |
| 74 | + // background and border |
| 75 | + remove_event(ev); |
| 76 | + |
| 77 | + // If the number of pointers down is less than two then reset diff tracker |
| 78 | + if (cacheRef.evCache.length < 2) { |
| 79 | + cacheRef.prevDiff = -1; |
| 80 | + } |
| 81 | + }; |
| 82 | + |
| 83 | + const remove_event = (ev: PointerEvent) => { |
| 84 | + // Remove this event from the target's cache |
| 85 | + for (let i = 0; i < cacheRef.evCache.length; i++) { |
| 86 | + if (cacheRef.evCache[i].pointerId == ev.pointerId) { |
| 87 | + cacheRef.evCache.splice(i, 1); |
| 88 | + break; |
| 89 | + } |
| 90 | + } |
| 91 | + }; |
| 92 | + |
| 93 | + useEffect(() => { |
| 94 | + if (ref?.current) { |
| 95 | + ref.current.onpointerdown = pointerdown_handler; |
| 96 | + ref.current.onpointermove = pointermove_handler; |
| 97 | + ref.current.onpointerup = pointerup_handler; |
| 98 | + ref.current.onpointercancel = pointerup_handler; |
| 99 | + ref.current.onpointerout = pointerup_handler; |
| 100 | + ref.current.onpointerleave = pointerup_handler; |
| 101 | + } |
| 102 | + }, [ref?.current]); |
| 103 | + |
| 104 | + return zoomingState |
| 105 | + ? { zoomingState: zoomingState[0], pinchState: zoomingState[1] } |
| 106 | + : { zoomingState: null, pinchState: 0 }; |
| 107 | +}; |
| 108 | + |
| 109 | +export default usePinchZoom; |
0 commit comments