From c04f62ac2c6d65923153961766d10b6ac5dd9bb4 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Wed, 12 Feb 2025 18:49:45 +0000 Subject: [PATCH] feat: pinch zooming --- package.json | 1 + pnpm-lock.yaml | 18 ++++++++++ src/components/canvas/Canvas.tsx | 56 ++++++++++++++++++++------------ src/hooks/tools/zoom.ts | 15 ++++++--- 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 4537045..703f5e2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@uiw/react-color": "^2.3.4", + "@use-gesture/react": "^10.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c5e265..738aa6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@uiw/react-color': specifier: ^2.3.4 version: 2.3.4(@babel/runtime@7.26.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@use-gesture/react': + specifier: ^10.3.1 + version: 10.3.1(react@18.3.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1873,6 +1876,14 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vitejs/plugin-react@4.3.4': resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4769,6 +4780,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@1.21.7)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.8 diff --git a/src/components/canvas/Canvas.tsx b/src/components/canvas/Canvas.tsx index c369bb2..d591307 100644 --- a/src/components/canvas/Canvas.tsx +++ b/src/components/canvas/Canvas.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useGesture } from "@use-gesture/react"; import * as PIXI from "pixi.js"; import { Container, Stage } from "@pixi/react"; @@ -68,17 +69,6 @@ function Canvas() { const clipboard = useClipboard(); - const zoom = useCallback( - (newScale: number) => { - setScale(newScale); - setCoords({ - x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale, - y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale, - }); - }, - [coords, mousePosition, scale, setCoords, setScale] - ); - const moveTool = useMoveTool(mouseMovementRef.current); const rectangleSelectTool = useRectangleSelectTool(mouseCoords, dragStartCoordsRef.current, holdingShiftRef.current); const lassoTool = useLassoTool(mouseCoords, holdingAltRef.current); @@ -88,7 +78,7 @@ function Canvas() { const paintBucketTool = usePaintBucketTool(mouseCoords); const shapeTool = useShapeTool(mouseCoords, dragStartCoordsRef.current, holdingShiftRef.current); const eyedropperTool = useEyedropperTool(mouseCoords); - const zoomTool = useZoomTool(zoom, holdingAltRef.current); + const zoomTool = useZoomTool(mousePosition, holdingAltRef.current); const visibleArea = useMemo(() => { const blockSize = 16 * scale; @@ -225,11 +215,16 @@ function Canvas() { const onWheel = useCallback( (e: React.WheelEvent) => { e.preventDefault(); - const scaleChange = e.deltaY > 0 ? -0.1 : 0.1; - const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32); - zoom(newScale); + const zoomFactor = e.deltaY > 0 ? -0.1 : 0.1; + const newScale = Math.min(Math.max(scale + zoomFactor * scale, 0.1), 32); + + setScale(newScale); + setCoords({ + x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale, + y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale, + }); }, - [scale, zoom] + [scale, setScale, setCoords, mousePosition, coords] ); const onClick = useCallback(() => { @@ -452,20 +447,39 @@ function Canvas() { }; }, [onKeyDown, onKeyUp]); + useGesture( + { + onPinch: ({ offset: [d] }) => { + setScale(d); + setCoords({ + x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * d, + y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * d, + }); + }, + }, + { + target: stageContainerRef, + pinch: { threshold: 0.1, scaleBounds: { min: 0.1, max: 32 }, pinchOnWheel: false }, + eventOptions: { passive: false }, + } + ); + return ( -
+
{/* Selection layer */} diff --git a/src/hooks/tools/zoom.ts b/src/hooks/tools/zoom.ts index 79ff11c..b577ce1 100644 --- a/src/hooks/tools/zoom.ts +++ b/src/hooks/tools/zoom.ts @@ -2,13 +2,18 @@ import { useContext } from "react"; import { CanvasContext } from "@/context/Canvas"; -export function useZoomTool(zoom: (newScale: number) => void, holdingAlt: boolean) { - const { scale } = useContext(CanvasContext); +export function useZoomTool(mousePosition: Position, holdingAlt: boolean) { + const { coords, scale, setScale, setCoords } = useContext(CanvasContext); const use = () => { - const scaleChange = holdingAlt ? -0.1 : 0.1; - const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32); - zoom(newScale); + const zoomFactor = holdingAlt ? -0.1 : 0.1; + const newScale = Math.min(Math.max(scale + zoomFactor * scale, 0.1), 32); + + setScale(newScale); + setCoords({ + x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale, + y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale, + }); }; return { use };