@supunlakmal/hooks
Version:
A collection of reusable React hooks
119 lines • 5.48 kB
JavaScript
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