2

Problem: I'm working with an HTML canvas. My canvas has a background image that multiple people can draw over in real-time (via socket.io), but drawing breaks if you've zoomed in.

Cause: To calculate where to start and end a line, I normalize input upon capture to be between 0 and 1 inclusive, like so:

// Pseudocode
line.x = mousePosition.x / canvas.width;    
line.y = mousePosition.y / canvas.height;

Because of this, the canvas can be of any size and in any position.

To implement a zoom-on-scroll functionality, I simply translate based on the current mouse position, scale the canvas by a factor of 2, then translate back the negative value of the current mouse position (as recommended here).

Here's where the problem lies

When I zoom, the canvas doesn't seem to have a notion of it's original size.

For instance, let's say I have a 1000px square canvas. Using my normalized x and y above, the upper left corner is 0, 0 and the lower right is 1, 1.

I then zoom into the center through scaling by a factor of 2. I would expect that my new upper left would be 0.5, 0.5 and my lower right would be 0.75, 0.75, but it isn't. Even when I zoom in, the upper left is still 0, 0 and the lower right is 1, 1.

The result is that when I zoom in and draw, the lines appear where they would as if I were not zoomed at all. If I zoomed into the center and "drew" in the upper left, I'd see nothing until I scrolled out to see that the line was actually getting drawn on the original upper left.

What I need to know: When zoomed, is there a way to get a read on what your new origin is relative to the un-zoomed canvas, or what amount of the canvas is hidden? Either of these would let me zoom in and draw and have it track correctly.


If I'm totally off base here and there's a better way to approach this, I'm all ears. If you need additional information, I'll provide what I can.

Kieran E
  • 3,616
  • 2
  • 18
  • 41

2 Answers2

3

It's not clear to me what you mean by "zoomed".

Zoomed =

  • made the canvas a different size?

  • changed the transform on the canvas

  • used CSS transform?

  • used CSS zoom?

I'm going to assume it's transform on the canvas in which case it's something like

function getElementRelativeMousePosition(e) {
  return [e.offsetX, e.offsetY];
}

function getCanvasRelativeMousePosition(e) {
  const pos = getElementRelativeMousePosition(e);
  pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
  pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
  return pos;
}

function getComputedMousePosition(e) {
  const pos = getCanvasRelativeMousePosition(e);
  const p = new DOMPoint(...pos);
  const point = inverseOriginTransform.transformPoint(p);
  return [point.x, point.y];
}

Where inverseOriginTransform is the inverse of whatever transform you're using to zoom and scroll the contents of the canvas.

const settings = {
  zoom: 1,
  xoffset: 0,
  yoffset: 0,
};

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const lines = [
   [[100, 10], [200, 30]],
   [[50, 50], [100, 30]],
];
let newStart;
let newEnd;
let originTransform = new DOMMatrix();
let inverseOriginTransform = new DOMMatrix();

function setZoomAndOffsetTransform() {
  originTransform = new DOMMatrix();
  originTransform.translateSelf(settings.xoffset, settings.yoffset);
  originTransform.scaleSelf(settings.zoom, settings.zoom);
  inverseOriginTransform = originTransform.inverse();
} 

const ui = document.querySelector('#ui')
addSlider(settings, 'zoom', ui, 0.25, 3, draw);
addSlider(settings, 'xoffset', ui, -100, +100, draw);
addSlider(settings, 'yoffset', ui, -100, +100, draw);

draw();

function updateAndDraw() {
  draw();
}

function getElementRelativeMousePosition(e) {
  return [e.offsetX, e.offsetY];
}

function getCanvasRelativeMousePosition(e) {
  const pos = getElementRelativeMousePosition(e);
  pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
  pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
  return pos;
}

function getTransformRelativeMousePosition(e) {
  const pos = getCanvasRelativeMousePosition(e);
  const p = new DOMPoint(...pos);
  const point = inverseOriginTransform.transformPoint(p);
  return [point.x, point.y];
}

canvas.addEventListener('mousedown', (e) => {
  const pos = getTransformRelativeMousePosition(e);
  if (newStart) {
  } else {
    newStart = pos;
    newEnd = pos;
  }
});

canvas.addEventListener('mousemove', (e) => {
  if (newStart) {
    newEnd = getTransformRelativeMousePosition(e);
    draw();
  }
});

canvas.addEventListener('mouseup', (e) => {
  if (newStart) {
    lines.push([newStart, newEnd]);
    newStart = undefined;
  }
});


function draw() {
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.save();
  setZoomAndOffsetTransform();
  ctx.setTransform(
      originTransform.a,
      originTransform.b,
      originTransform.c,
      originTransform.d,
      originTransform.e,
      originTransform.f);
  ctx.beginPath();
  for (const line of lines) {
    ctx.moveTo(...line[0]);
    ctx.lineTo(...line[1]);
  }
  if (newStart) {
     ctx.moveTo(...newStart);
     ctx.lineTo(...newEnd);
  }
  ctx.stroke();
  ctx.restore();
}

function addSlider(obj, prop, parent, min, max, callback) {
  const valueRange = max - min;
  const sliderRange = 100;
  
  const div = document.createElement('div');
  div.class = 'range';
  
  const input = document.createElement('input');
  input.type = 'range';
  input.min = 0;
  input.max = sliderRange;
 
  const label = document.createElement('span');
  label.textContent = `${prop}: `;
  
  const valueElem = document.createElement('span');
  
  function setInputValue(v) {
    input.value = (v - min) * sliderRange / valueRange;
  }
  
  input.addEventListener('input', (e) => {
    const v = parseFloat(input.value) * valueRange / sliderRange + min;
    valueElem.textContent = v.toFixed(1);
    obj[prop] = v;
    callback();
  });
  
  const v = obj[prop];
  valueElem.textContent = v.toFixed(1);
  setInputValue(v);
  
  div.appendChild(input);
  div.appendChild(label);
  div.appendChild(valueElem);
  parent.appendChild(div);
}
canvas { border: 1px solid black; }
#app { display: flex; }
<div id="app"><canvas></canvas><div id="ui"></div>

Note: I didn't bother making zoom always zoom from the center. To do so would require adjusting xoffset and yoffset as the zoom changes.

gman
  • 100,619
  • 31
  • 269
  • 393
0

Use HTMLElement.prototype.getBoundingClientRect() to get displayed size and position of canvas in DOM. From the displayed size and origin size, calculates the scale of the canvas.

Example:

canvas.addEventListener("click", function (event) {
    var b = canvas.getBoundingClientRect();
    var scale = canvas.width / parseFloat(b.width);
    var x = (event.clientX - b.left) * scale;
    var y = (event.clientY - b.top) * scale;
    // Marks mouse position
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.arc(x, y, 10, 0, 2 * Math.PI);
    ctx.stroke();
});
Văn Quyết
  • 2,384
  • 14
  • 29
  • 1
    `scale` is always `1` in this scenario. I'm essentially dividing `canvas.width` by `canvas.width` – Kieran E Aug 24 '17 at 03:57