From 772da2533deb74139fd0fa0d7ffa98d5694b151d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:19:21 -0600 Subject: [PATCH] timeline zooming hook --- web/src/hooks/use-timeline-zoom.ts | 138 +++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 web/src/hooks/use-timeline-zoom.ts diff --git a/web/src/hooks/use-timeline-zoom.ts b/web/src/hooks/use-timeline-zoom.ts new file mode 100644 index 000000000..c8390bbe6 --- /dev/null +++ b/web/src/hooks/use-timeline-zoom.ts @@ -0,0 +1,138 @@ +import { useState, useEffect, useCallback, useRef } from "react"; + +type ZoomSettings = { + segmentDuration: number; + timestampSpread: number; +}; + +type UseTimelineZoomProps = { + zoomSettings: ZoomSettings; + zoomLevels: ZoomSettings[]; + onZoomChange: (newZoomLevel: number) => void; + pinchThresholdPercent?: number; +}; + +export function useTimelineZoom({ + zoomSettings, + zoomLevels, + onZoomChange, + pinchThresholdPercent = 20, +}: UseTimelineZoomProps) { + const [zoomLevel, setZoomLevel] = useState( + zoomLevels.findIndex( + (level) => + level.segmentDuration === zoomSettings.segmentDuration && + level.timestampSpread === zoomSettings.timestampSpread, + ), + ); + const touchStartDistanceRef = useRef(0); + + const getPinchThreshold = useCallback(() => { + return (window.innerHeight * pinchThresholdPercent) / 100; + }, [pinchThresholdPercent]); + + const wheelDeltaRef = useRef(0); + const isZoomingRef = useRef(false); + const debounceTimeoutRef = useRef(null); + + const handleZoom = useCallback( + (delta: number) => { + setZoomLevel((prevLevel) => { + const newLevel = Math.max( + 0, + Math.min(zoomLevels.length - 1, prevLevel - delta), + ); + if (newLevel !== prevLevel) { + onZoomChange(newLevel); + } + return newLevel; + }); + }, + [zoomLevels, onZoomChange], + ); + + const debouncedZoom = useCallback(() => { + if (Math.abs(wheelDeltaRef.current) >= 200) { + handleZoom(wheelDeltaRef.current > 0 ? 1 : -1); + wheelDeltaRef.current = 0; + isZoomingRef.current = false; + } else { + isZoomingRef.current = false; + } + }, [handleZoom]); + + const handleWheel = useCallback( + (event: WheelEvent) => { + if (event.ctrlKey) { + event.preventDefault(); + + if (!isZoomingRef.current) { + wheelDeltaRef.current += event.deltaY; + + if (Math.abs(wheelDeltaRef.current) >= 200) { + isZoomingRef.current = true; + + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + debounceTimeoutRef.current = setTimeout(() => { + debouncedZoom(); + }, 200); + } + } + } + }, + [debouncedZoom], + ); + + const handleTouchStart = useCallback((event: TouchEvent) => { + if (event.touches.length === 2) { + event.preventDefault(); + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + const distance = Math.hypot( + touch1.clientX - touch2.clientX, + touch1.clientY - touch2.clientY, + ); + touchStartDistanceRef.current = distance; + } + }, []); + + const handleTouchMove = useCallback( + (event: TouchEvent) => { + if (event.touches.length === 2) { + event.preventDefault(); + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + const currentDistance = Math.hypot( + touch1.clientX - touch2.clientX, + touch1.clientY - touch2.clientY, + ); + + const distanceDelta = currentDistance - touchStartDistanceRef.current; + const pinchThreshold = getPinchThreshold(); + + if (Math.abs(distanceDelta) > pinchThreshold) { + handleZoom(distanceDelta > 0 ? -1 : 1); + touchStartDistanceRef.current = currentDistance; + } + } + }, + [handleZoom, getPinchThreshold], + ); + + useEffect(() => { + window.addEventListener("wheel", handleWheel, { passive: false }); + window.addEventListener("touchstart", handleTouchStart, { passive: false }); + window.addEventListener("touchmove", handleTouchMove, { passive: false }); + + return () => { + window.removeEventListener("wheel", handleWheel); + window.removeEventListener("touchstart", handleTouchStart); + window.removeEventListener("touchmove", handleTouchMove); + }; + }, [handleWheel, handleTouchStart, handleTouchMove]); + + return { zoomLevel, handleZoom }; +}