season's quarterly

数学/物理/プログラミング

マウス操作によるCanvasの平行移動と拡大縮小

コード

function Canvas() {
  const [mousePressed, setMousePressed] = useState(false);
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  const [origin, setOrigin] = useState({ x: 0, y: 0 });
  const [logScale, setLogScale] = useState(0);

  function translateCanvas(x: number, y: number) {
    if (mousePressed) {
      setOrigin({ x: origin.x + x - mousePos.x, y: origin.y + y - mousePos.y });
    }
    setMousePos({ x, y });
  }

  function scaleCanvas(delta: number) {
    let nextX = mousePos.x + Math.pow(1.1, delta/100) * (origin.x - mousePos.x);
    let nextY = mousePos.y + Math.pow(1.1, delta/100) * (origin.y - mousePos.y);
    let nextLogScale = logScale + delta / 100;
    if (-50 < nextLogScale && nextLogScale < 50) {
      setOrigin({ x: nextX, y: nextY });
      setLogScale(nextLogScale);
    }
  }

  useEffect(
    () => {
      const canvas = document.querySelector('canvas') as HTMLCanvasElement;
      const context = canvas.getContext('2d') as CanvasRenderingContext2D;
      
      context.resetTransform();
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.translate(origin.x, origin.y);
      context.scale(Math.pow(1.1, logScale), Math.pow(1.1, logScale));

      // 描画
    },
    [origin, logScale]
  );

  return (
    <canvas
      onMouseDown={(e) => setMousePressed(true)}
      onMouseUp={(e) => setMousePressed(false)}
      onMouseMove={
        (e) => {
          let target = e.currentTarget.getBoundingClientRect();
          translateCanvas(e.clientX - target.left, e.clientY - target.top);
        }
      }
      onWheel={(e) => scaleCanvas(-e.deltaY)}
    ></canvas>
  )
}

export default Canvas;

方針

Canvasの原点の位置originとlogスケールlogScaleを持って置く。拡大率はホイールの回転数について指数関数にしておくと滑らかになる気がするのでlogスケールで管理する。useEffectを使って、originlogScaleが変更されたら再描画させる。

  const [origin, setOrigin] = useState({ x: 0, y: 0 });
  const [logScale, setLogScale] = useState(0);

変換の順番に注意する。原点を移動した上でスケールを変更する。

  useEffect(
    () => {
      const canvas = document.querySelector('canvas') as HTMLCanvasElement;
      const context = canvas.getContext('2d') as CanvasRenderingContext2D;
      
      context.resetTransform();
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.translate(origin.x, origin.y);
      context.scale(Math.pow(1.1, logScale), Math.pow(1.1, logScale));

      // 描画
    },
    [origin, logScale]
  );

ドラッグで平行移動

現在マウスが押されているかをmousePressedで管理。マウスが押されている状態でMouseMoveが発生したら、Canvasの原点に現在のマウスの位置と直前の位置の差分を足し込む。

  function translateCanvas(x: number, y: number) {
    if (mousePressed) {
      setOrigin({ x: origin.x + x - mousePos.x, y: origin.y + y - mousePos.y });
    }
    setMousePos({ x, y });
  }

ホイールで拡大縮小

マウスの位置を中心に拡大縮小する。Canvasの原点は、現在のマウスの位置と原点の差を拡大縮小した上で元の座標に足す。logスケールは単純に足す。

  function scaleCanvas(delta: number) {
    let nextX = mousePos.x + Math.pow(1.1, delta/100) * (origin.x - mousePos.x);
    let nextY = mousePos.y + Math.pow(1.1, delta/100) * (origin.y - mousePos.y);
    let nextLogScale = logScale + delta / 100;
    if (-50 < nextLogScale && nextLogScale < 50) {
      setOrigin({ x: nextX, y: nextY });
      setLogScale(nextLogScale);
    }
  }