Home > Software engineering >  Prevent mouse position drifting when dragging a scaled image
Prevent mouse position drifting when dragging a scaled image

Time:12-02

I have a system to scale, rotate, and translate an image on a canvas. Everything is working as expected, except the translation of the image after scaling.

When the scale is not equal to 1, then the dragging starts to drift (see example illustration below):

enter image description here

The illustration above shows where I start dragging the image, and where my mouse position ends up.

Is there a way of maintaining the mouse position, so that when I'm dragging the image, the mouse stays in the same location of the image?

How can I achieve this, whilst still keeping the rest of the scaling and rotation (from the canvas centre) functionality?

Full CodeSandbox demo

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

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 ox = canvas.width / 2 - imgX;
  const oy = canvas.height / 2 - imgY;

  const { x: rx, y: ry } = rotate(imgX, imgY, rotation);

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

  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:

You need to divide mouse movement by the current scale

x = lastX   dX / imgWidth / scale;
y = lastY   dY / imgHeight / scale;
  • Related