UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

119 lines 5.48 kB
import { useRef, useEffect } from 'react'; import { useEventCallback } from '../event-handling/useEventCallback'; // Assuming useEventCallback exists const isBrowser = typeof window !== 'undefined'; // Helper function to calculate distance between two touches const getDistance = (touches) => { const touch1 = touches[0]; const touch2 = touches[1]; return Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) + Math.pow(touch2.clientY - touch1.clientY, 2)); }; // Helper function to calculate the center point between two touches const getCenter = (touches) => { const touch1 = touches[0]; const touch2 = touches[1]; return { x: (touch1.clientX + touch2.clientX) / 2, y: (touch1.clientY + touch2.clientY) / 2, }; }; /** * Hook to detect pinch-to-zoom gestures on a target element. * * @param {React.RefObject<HTMLElement>} targetRef Ref to the target HTML element. * @param {PinchZoomOptions} options Configuration options and callbacks. */ export function usePinchZoom(targetRef, options) { const { onPinchStart, onPinchMove, onPinchEnd, minScale = 0.5, maxScale = 4, } = options; const pinchStateRef = useRef({ isPinching: false, initialDistance: 0, currentScale: 1, lastScale: 1, origin: { x: 0, y: 0 }, }); const stableOnPinchStart = useEventCallback((state, event) => onPinchStart === null || onPinchStart === void 0 ? void 0 : onPinchStart(state, event)); const stableOnPinchMove = useEventCallback((state, event) => onPinchMove === null || onPinchMove === void 0 ? void 0 : onPinchMove(state, event)); const stableOnPinchEnd = useEventCallback((state, event) => onPinchEnd === null || onPinchEnd === void 0 ? void 0 : onPinchEnd(state, event)); useEffect(() => { const element = targetRef.current; if (!isBrowser || !element) { return; } const handleTouchStart = (event) => { if (event.touches.length === 2) { event.preventDefault(); // Prevent default scroll/zoom behavior const initialDistance = getDistance(event.touches); const origin = getCenter(event.touches); pinchStateRef.current = { isPinching: true, initialDistance, currentScale: pinchStateRef.current.lastScale, // Start from last scale lastScale: pinchStateRef.current.lastScale, origin, }; const state = { scale: pinchStateRef.current.currentScale, delta: 0, origin: pinchStateRef.current.origin, }; stableOnPinchStart(state, event); } }; const handleTouchMove = (event) => { if (!pinchStateRef.current.isPinching || event.touches.length !== 2) { return; } event.preventDefault(); const currentDistance = getDistance(event.touches); const scaleDelta = currentDistance / pinchStateRef.current.initialDistance; let newScale = pinchStateRef.current.lastScale * scaleDelta; // Clamp scale within bounds newScale = Math.max(minScale, Math.min(maxScale, newScale)); const scaleChange = newScale - pinchStateRef.current.currentScale; pinchStateRef.current.currentScale = newScale; // Update origin if needed (can make movement feel more natural) pinchStateRef.current.origin = getCenter(event.touches); const state = { scale: pinchStateRef.current.currentScale, delta: scaleChange, origin: pinchStateRef.current.origin, }; stableOnPinchMove(state, event); }; const handleTouchEnd = (event) => { if (pinchStateRef.current.isPinching) { const state = { scale: pinchStateRef.current.currentScale, delta: pinchStateRef.current.currentScale - pinchStateRef.current.lastScale, origin: pinchStateRef.current.origin, }; pinchStateRef.current.isPinching = false; pinchStateRef.current.lastScale = pinchStateRef.current.currentScale; // Save the final scale stableOnPinchEnd(state, event); } }; // Add passive: false for touchstart/touchmove to allow preventDefault element.addEventListener('touchstart', handleTouchStart, { passive: false, }); element.addEventListener('touchmove', handleTouchMove, { passive: false }); element.addEventListener('touchend', handleTouchEnd, { passive: true }); element.addEventListener('touchcancel', handleTouchEnd, { passive: true }); // Also handle cancel return () => { element.removeEventListener('touchstart', handleTouchStart); element.removeEventListener('touchmove', handleTouchMove); element.removeEventListener('touchend', handleTouchEnd); element.removeEventListener('touchcancel', handleTouchEnd); }; }, [ targetRef, minScale, maxScale, stableOnPinchStart, stableOnPinchMove, stableOnPinchEnd, ]); } //# sourceMappingURL=usePinchZoom.js.map