Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | 2x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x | import type { PointerEvent, WheelEvent } from "react";
import { useCallback, useRef, useState } from "react";
import { ZOOM_DEFAULT, ZOOM_MAX, ZOOM_MIN } from "@/lib/constants";
import { clamp } from "@/lib/geometry";
interface Point {
x: number;
y: number;
}
export interface UseCanvasZoomPanResult {
zoom: number;
pan: Point;
handleWheel: (event: WheelEvent<HTMLDivElement>) => void;
handlePointerDown: (event: PointerEvent<HTMLDivElement>) => void;
handlePointerMove: (event: PointerEvent<HTMLDivElement>) => void;
handlePointerUp: (event: PointerEvent<HTMLDivElement>) => void;
handleZoomIn: () => void;
handleZoomOut: () => void;
handleResetView: () => void;
}
const ZOOM_STEP = 0.1;
function roundZoom(value: number): number {
return Number(value.toFixed(2));
}
export function useCanvasZoomPan(): UseCanvasZoomPanResult {
const [zoom, setZoom] = useState<number>(ZOOM_DEFAULT);
const [pan, setPan] = useState<Point>({ x: 0, y: 0 });
const isPanningRef = useRef(false);
const lastPointRef = useRef<Point | null>(null);
const handleWheel = useCallback((event: WheelEvent<HTMLDivElement>) => {
event.preventDefault();
if (event.ctrlKey) {
setZoom((previousZoom) =>
clamp(
roundZoom(previousZoom + (event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP)),
ZOOM_MIN,
ZOOM_MAX,
),
);
return;
}
setPan((previousPan) => ({
x: previousPan.x - event.deltaX,
y: previousPan.y - event.deltaY,
}));
}, []);
const handlePointerDown = useCallback((event: PointerEvent<HTMLDivElement>) => {
if (event.button !== 1 && !event.altKey) {
return;
}
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
isPanningRef.current = true;
lastPointRef.current = { x: event.clientX, y: event.clientY };
}, []);
const handlePointerMove = useCallback((event: PointerEvent<HTMLDivElement>) => {
if (!isPanningRef.current || !lastPointRef.current) {
return;
}
const deltaX = event.clientX - lastPointRef.current.x;
const deltaY = event.clientY - lastPointRef.current.y;
lastPointRef.current = { x: event.clientX, y: event.clientY };
setPan((previousPan) => ({ x: previousPan.x + deltaX, y: previousPan.y + deltaY }));
}, []);
const handlePointerUp = useCallback((event: PointerEvent<HTMLDivElement>) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
isPanningRef.current = false;
lastPointRef.current = null;
}, []);
const handleZoomOut = useCallback(() => {
setZoom((previousZoom) => clamp(roundZoom(previousZoom - ZOOM_STEP), ZOOM_MIN, ZOOM_MAX));
}, []);
const handleZoomIn = useCallback(() => {
setZoom((previousZoom) => clamp(roundZoom(previousZoom + ZOOM_STEP), ZOOM_MIN, ZOOM_MAX));
}, []);
const handleResetView = useCallback(() => {
setZoom(ZOOM_DEFAULT);
setPan({ x: 0, y: 0 });
}, []);
return {
zoom,
pan,
handleWheel,
handlePointerDown,
handlePointerMove,
handlePointerUp,
handleZoomIn,
handleZoomOut,
handleResetView,
};
}
|