Home > OS >  Fix canvas translation after rotate with DOMMatrix
Fix canvas translation after rotate with DOMMatrix

Time:11-30

I'm using DOMMatrix to set transform on a canvas context, but I am not sure how to "fix" the translation after the rotate. Currently the code I've written rotates an image around a given point on the canvas, which works fine. The issue is after the rotation, the translate moves relative to the rotation, which is not what I want. I want the translation to be relative to the canvas.

CodeSandbox demo

I implemented this function which I read "fixes" the translation after rotation, but it doesn't seem to do what I need:

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 };
}

Is there a way of doing this, either by modifying the rotate function above, or the DOMMatrix, or a different way entirely?

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);
})();
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;
}
<canvas id="canvas"></canvas>
<pre>
  Hotkeys
  ---
  Rotate: r
  Zoom out: -
  Zoom in: =
</pre>

CodePudding user response:

Applying rotate() to mouse-movements, it might work as expected.

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

    let deltaX = (event.clientX - canvas.offsetLeft) / imgWidth - startX;
    let deltaY = (event.clientY - canvas.offsetTop) / imgHeight - startY;
    const { x: dX, y: dY } = rotate(deltaX * imgWidth, deltaY * imgHeight, rotation);
    x = lastX   dX / imgWidth;
    y = lastY   dY / imgHeight;
}

CodePudding user response:

You are close. Your order of transformations were wrong. Here is the correct one:

const matrix = new DOMMatrix()
.translate(imgWidth/2, imgHeight/2)
.rotate(rotation)
.scale(scale)
.translate(-imgWidth/2, -imgHeight/2)
.translate(tX / scale, tY / scale);

You are forgetting that css transformations and canvas/svg transformations are a little bit different. In CSS the origin is the center of the element. In SVG/canvas, the origin is the upper left corner (0,0). IN the latter case, center the object to the origin, apply scale/rotate and THEN do the transformation.

  • Related