如何使用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
相关产品推荐
相关产品推荐

