You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用DOMMatrix修正Canvas旋转后的平移问题

问题描述

我正在使用DOMMatrix为Canvas上下文设置变换,目前已实现围绕Canvas上指定点旋转图像的功能,但存在问题:旋转后平移操作是相对于旋转后的坐标系进行的,而非我期望的相对于Canvas本身。我曾尝试实现一个用于修正旋转后平移的rotate函数,但未达到预期效果。

尝试的rotate函数

function rotate(x, y, rotation) {
  const panXX = x * Math.cos((rotation * Math.PI) / 180);
  const panXY = y * Math.sin((rotation * Math.PI) / 180);
  const panYY = y * Math.cos((rotation * Math.PI) / 180);
  const panYX = x * Math.sin((rotation * Math.PI) / 180);
  const panX = panXX + panXY;
  const panY = panYY - panYX;
  return { x: panX, y: panY };
}

完整代码示例

JavaScript

let rotation = 0;
let scale = 1;
let x = 0;
let y = 0;
let startX = 0;
let startY = 0;
let lastX = 0;
let lastY = 0;
let pointerDown = false;

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");

const imgWidth = 480;
const imgHeight = 300;

function resizeCanvas() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}

resizeCanvas();
window.addEventListener("resize", resizeCanvas);

const img = new Image();
img.crossOrigin = "anonymous";
img.src = "https://i.imgur.com/3q3kNGh.png";

function onPointerDown(event) {
  pointerDown = true;
  startX = (event.clientX - canvas.offsetLeft) / imgWidth;
  startY = (event.clientY - canvas.offsetTop) / imgHeight;
}

function onPointerMove(event) {
  if (!pointerDown) return;
  x = lastX + ((event.clientX - canvas.offsetLeft) / imgWidth - startX);
  y = lastY + ((event.clientY - canvas.offsetTop) / imgHeight - startY);
}

function onPointerUp() {
  pointerDown = false;
  lastX = x;
  lastY = y;
}

window.addEventListener("pointerdown", onPointerDown);
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);

window.addEventListener("keydown", (event) => {
  const key = event.key.toLowerCase();
  switch (key) {
    case "r":
      rotation = (rotation + 5) % 360;
      break;
    case "-":
      scale = Math.max(0, scale - 0.1);
      break;
    case "=":
      scale = Math.min(2, scale + 0.1);
      break;
    default:
      break;
  }
});

function rotate(x, y, rotation) {
  const panXX = x * Math.cos((rotation * Math.PI) / 180);
  const panXY = y * Math.sin((rotation * Math.PI) / 180);
  const panYY = y * Math.cos((rotation * Math.PI) / 180);
  const panYX = x * Math.sin((rotation * Math.PI) / 180);
  const panX = panXX + panXY;
  const panY = panYY - panYX;
  return { x: panX, y: panY };
}

(function draw() {
  requestAnimationFrame(draw);

  const imgX = imgWidth * x;
  const imgY = imgHeight * y;

  const { x: tX, y: tY } = rotate(imgX, imgY, rotation);

  const ox = canvas.width / 2 - imgX;
  const oy = canvas.height / 2 - imgY;

  const matrix = new DOMMatrix()
    .translate(ox, oy)
    .rotate(rotation)
    .translate(-ox, -oy)
    .translate(tX, tY)
    .scale(scale);

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.setTransform(matrix);

  ctx.drawImage(img, 0, 0, imgWidth, imgHeight);

  ctx.resetTransform();

  ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
  ctx.fillRect(canvas.width / 2 - 5, canvas.height / 2 - 5, 10, 10);
})();

CSS

html,
body {
  margin: 0;
  padding: 0;
}
canvas {
  display: block;
}
pre {
  position: absolute;
  bottom: 0;
  left: 0;
  padding: 0.5em;
  pointer-events: none;
  user-select: none;
}

HTML

<canvas id="canvas"></canvas>
<pre>
  Hotkeys
  ---
  Rotate: r
  Zoom out: -
  Zoom in: =
</pre>
解决方案

核心问题在于变换顺序和坐标系转换:当你先旋转再平移时,平移会基于旋转后的坐标系。要让平移始终相对于Canvas原始坐标系,可通过以下两种方法解决:

方法一:调整DOMMatrix变换顺序

正确的变换流程应为:先应用平移(基于原始坐标系),再围绕Canvas中心旋转,最后缩放。同时确保旋转原点是Canvas中心而非图像自身原点。

修改draw函数中的矩阵构建逻辑:

(function draw() {
  requestAnimationFrame(draw);

  // 计算图像左上角相对于Canvas中心的偏移(基于原始坐标系)
  const offsetX = canvas.width / 2 + imgWidth * x;
  const offsetY = canvas.height / 2 + imgHeight * y;

  const matrix = new DOMMatrix()
    // 先平移到目标位置(基于Canvas原始坐标系)
    .translate(offsetX, offsetY)
    // 围绕当前位置旋转
    .rotate(rotation)
    // 缩放
    .scale(scale)
    // 回退到图像自身原点,保证drawImage绘制正确
    .translate(-imgWidth/2, -imgHeight/2);

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.setTransform(matrix);

  ctx.drawImage(img, 0, 0, imgWidth, imgHeight);

  ctx.resetTransform();

  ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
  ctx.fillRect(canvas.width / 2 - 5, canvas.height / 2 - 5, 10, 10);
})();

方法二:修正平移量的逆旋转变换

若想保留原有矩阵结构,可对平移量进行逆旋转,将原始坐标系的平移量转换为旋转后坐标系的等效值。修改rotate函数为逆旋转变换:

function inverseRotate(x, y, rotation) {
  const rad = (rotation * Math.PI) / 180;
  const cos = Math.cos(rad);
  const sin = Math.sin(rad);
  // 逆旋转公式:将原始坐标系的点转换为旋转后坐标系的点
  return {
    x: x * cos + y * sin,
    y: -x * sin + y * cos
  };
}

在draw函数中使用逆旋转后的平移量:

const imgX = imgWidth * x;
const imgY = imgHeight * y;
// 对平移量进行逆旋转
const { x: tX, y: tY } = inverseRotate(imgX, imgY, rotation);

const ox = canvas.width / 2;
const oy = canvas.height / 2;

const matrix = new DOMMatrix()
  .translate(ox, oy)
  .rotate(rotation)
  .translate(-ox, -oy)
  .translate(tX, tY)
  .scale(scale);

关键原理

  • Canvas变换采用右乘顺序:后调用的变换会先作用于坐标系。
  • 要让平移基于原始坐标系,需确保平移操作在旋转之前执行,或对平移量进行逆旋转变换,抵消旋转对坐标系的影响。

内容的提问来源于stack exchange,提问作者James Craig

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 05:15:49