使用 react-use-gesture 围绕原点缩放时计算 CSS 变换位移

Calculate CSS transform displacement while scaling around origin point using react-use-gesture

我正在使用来自 react-use-gesture 的 useWheel 创建可缩放和 "pannable" canvas。

到目前为止进展顺利,直到我尝试围绕原点(即鼠标位置)进行缩放。

我在计算位置位移以适应围绕不同原点缩放引起的变化时遇到问题。

这是一个代码沙箱(检查 App.js 大约第 60 行): https://codesandbox.io/s/usewheel-zoom-pan-Whosebug-27o0l

谢谢

已解决!

从这里借用解决方案并将其应用到我的反应应用程序

要点是我不再依赖 transform-origin,因为改变它是导致跳跃的原因。该解决方案现在计算新的原点并向其缩放。

供参考(由于模块的原因,此代码在此处的代码片段中不起作用,请在代码沙箱中尝试)

import React from "react";
import ReactDOM from "react-dom";
import { useWheel } from "react-use-gesture";
import clamp from "lodash/clamp";

import "./styles.css";

const STEP = 0.995;
const MAX_SCALE = 5;
const MIN_SCALE = 0.25;

export default function App() {
  const [isChecked, setIsChecked] = React.useState(true);

  const canvasRef = React.useRef();

  const [canvasTransform, setCanvasTransform] = React.useState({
    x: 0,
    y: 0,
    originCenterX: window.innerWidth / 2,
    originCenterY: window.innerHeight / 2,
    wheeling: false,
    scale: 1
  });

  // Set the drag hook and define component movement based on gesture data
  const bind = useWheel(
    ({ wheeling, metaKey, delta: [deltaX, deltaY], event }) => {
      if (metaKey && event) {
        const factor = deltaY;

        const { clientX, clientY } = event;

        const scaleChanged = Math.pow(STEP, factor);

        const newScale = clamp(
          scaleChanged * canvasTransform.scale,
          MIN_SCALE,
          MAX_SCALE
        );

        const rect = canvasRef.current.getBoundingClientRect();
        const currentCenterX = rect.x + rect.width / 2;
        const currentCenterY = rect.y + rect.height / 2;

        const mousePosToCurrentCenterDistanceX = clientX - currentCenterX;
        const mousePosToCurrentCenterDistanceY = clientY - currentCenterY;

        const newCenterX =
          currentCenterX +
          mousePosToCurrentCenterDistanceX * (1 - scaleChanged);
        const newCenterY =
          currentCenterY +
          mousePosToCurrentCenterDistanceY * (1 - scaleChanged);

        // All we are doing above is: getting the target center, then calculate the offset from origin center.
        const offsetX = newCenterX - canvasTransform.originCenterX;
        const offsetY = newCenterY - canvasTransform.originCenterY;

        if (newScale !== canvasTransform.scale) {
          setCanvasTransform({
            ...canvasTransform,
            scale: newScale,
            x: offsetX,
            y: offsetY,
            scaleChanged,
            currentCenterX,
            currentCenterY,
            wheeling
          });
        }
      } else {
        setCanvasTransform({
          ...canvasTransform,
          x: canvasTransform.x - deltaX,
          y: canvasTransform.y - deltaY,
          wheeling
        });
      }
    }
  );

  return (
    <div className="outer" {...bind()}>
      <div
        className="inner"
        style={{
          transform: `translateX(${canvasTransform.x}px) translateY(${
            canvasTransform.y
          }px) scale3d(${canvasTransform.scale}, ${canvasTransform.scale}, 1)`
        }}
        ref={canvasRef}
        id="canvas"
      >
        <h1>Hello CodeSandbox</h1>
        <h2>Start editing to see some magic happen!</h2>
      </div>
      <span>
        <input
          type="checkbox"
          checked={isChecked}
          onChange={() => setIsChecked(!isChecked)}
        />{" "}
        Around Mouse
      </span>
      <pre>{JSON.stringify(canvasTransform, null, 2)}</pre>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
body {
  margin: 0;
  font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
    "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  overscroll-behavior-x: none;
  overscroll-behavior-y: none;
  max-height: 100vh;
  overflow: hidden;
  background-color: #f0f4f7 !important;
}

div > div {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}

div > div > div {
  font-family: sans-serif;
  text-align: center;
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: white;
  background-image: linear-gradient(#eee 0.1em, transparent 0.1em),
    linear-gradient(90deg, #eee 0.1em, transparent 0.1em);
  background-size: 3em 3em;
}

span {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 1;
  display: flex;
  color: #fff;
  background-color: #333;
  padding: 10px;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
  justify-content: space-between;
  align-items: center;
}

span input {
  font-size: 16px;
}

pre {
  position: fixed;
  bottom: 0;
}
<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 <meta name="theme-color" content="#000000">
 <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
 <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
 <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
 <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
 <title>React App</title>
</head>

<body>
 <noscript>
  You need to enable JavaScript to run this app.
 </noscript>
 <div id="root"></div>
 <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
</body>

</html>