feat: implement pan and zoom functionality with wheel gestures in Canvas component
This commit is contained in:
@@ -15,6 +15,7 @@ interface CanvasProps {
|
||||
const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: window.innerWidth, height: window.innerHeight - 120 });
|
||||
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
||||
const {
|
||||
snap,
|
||||
zoom,
|
||||
@@ -24,6 +25,7 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
selectElement,
|
||||
addElement,
|
||||
updateElement,
|
||||
setZoom,
|
||||
} = useCanvasStore();
|
||||
|
||||
const { width, height } = snap.meta;
|
||||
@@ -70,10 +72,15 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
const scaledWidth = width * zoom;
|
||||
const scaledHeight = height * zoom;
|
||||
return {
|
||||
x: Math.max(20, (dimensions.width - scaledWidth) / 2),
|
||||
y: Math.max(20, (dimensions.height - scaledHeight) / 2),
|
||||
x: Math.max(20, (dimensions.width - scaledWidth) / 2) + panOffset.x,
|
||||
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 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]);
|
||||
|
||||
// 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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex-1 bg-neutral-900 overflow-hidden relative"
|
||||
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
|
||||
ref={stageRef}
|
||||
|
||||
Reference in New Issue
Block a user