feat: implement pan and zoom functionality with wheel gestures in Canvas component

This commit is contained in:
2026-01-08 10:14:15 +02:00
parent 66d811aeaf
commit d95afbbc72
+65 -3
View File
@@ -15,6 +15,7 @@ interface CanvasProps {
const Canvas: React.FC<CanvasProps> = ({ stageRef }) => { const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: window.innerWidth, height: window.innerHeight - 120 }); const [dimensions, setDimensions] = useState({ width: window.innerWidth, height: window.innerHeight - 120 });
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
const { const {
snap, snap,
zoom, zoom,
@@ -24,6 +25,7 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
selectElement, selectElement,
addElement, addElement,
updateElement, updateElement,
setZoom,
} = useCanvasStore(); } = useCanvasStore();
const { width, height } = snap.meta; const { width, height } = snap.meta;
@@ -70,10 +72,15 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
const scaledWidth = width * zoom; const scaledWidth = width * zoom;
const scaledHeight = height * zoom; const scaledHeight = height * zoom;
return { return {
x: Math.max(20, (dimensions.width - scaledWidth) / 2), x: Math.max(20, (dimensions.width - scaledWidth) / 2) + panOffset.x,
y: Math.max(20, (dimensions.height - scaledHeight) / 2), y: Math.max(20, (dimensions.height - scaledHeight) / 2) + panOffset.y,
}; };
}, [width, height, zoom, dimensions]); }, [width, height, zoom, dimensions, panOffset]);
// Reset pan offset when zoom changes significantly or canvas is re-centered
const resetPan = useCallback(() => {
setPanOffset({ x: 0, y: 0 });
}, []);
const handleStageClick = (e: Konva.KonvaEventObject<MouseEvent>) => { const handleStageClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
const clickedOnEmpty = e.target === e.target.getStage() || e.target.name() === 'background'; const clickedOnEmpty = e.target === e.target.getStage() || e.target.name() === 'background';
@@ -416,11 +423,66 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
const stagePosition = useMemo(() => getStagePosition(), [getStagePosition]); const stagePosition = useMemo(() => getStagePosition(), [getStagePosition]);
// Handle wheel/pinch zoom and pan on canvas
const handleWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
// Check if it's a pinch zoom gesture (ctrlKey is true for pinch-to-zoom)
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
e.stopPropagation();
const scaleBy = 1.05;
const minZoom = 0.1;
const maxZoom = 3;
// Determine zoom direction
const direction = e.deltaY > 0 ? -1 : 1;
const newZoom = direction > 0 ? zoom * scaleBy : zoom / scaleBy;
// Clamp zoom between min and max
const clampedZoom = Math.min(Math.max(newZoom, minZoom), maxZoom);
setZoom(clampedZoom);
} else {
// Two-finger scroll for panning
e.preventDefault();
e.stopPropagation();
setPanOffset(prev => ({
x: prev.x - e.deltaX,
y: prev.y - e.deltaY,
}));
}
}, [zoom, setZoom]);
// Prevent default browser zoom behavior on the container
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const preventDefaultZoom = (e: WheelEvent) => {
// Prevent default for both zoom (ctrl/meta) and pan (regular scroll)
e.preventDefault();
};
// Use passive: false to allow preventDefault
container.addEventListener('wheel', preventDefaultZoom, { passive: false });
return () => {
container.removeEventListener('wheel', preventDefaultZoom);
};
}, []);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="flex-1 bg-neutral-900 overflow-hidden relative" className="flex-1 bg-neutral-900 overflow-hidden relative"
style={{ cursor: tool !== 'select' ? 'crosshair' : 'default' }} style={{ cursor: tool !== 'select' ? 'crosshair' : 'default' }}
onWheel={handleWheel}
onDoubleClick={(e) => {
// Double-click on empty area to reset pan
if (e.target === containerRef.current) {
resetPan();
}
}}
> >
<Stage <Stage
ref={stageRef} ref={stageRef}