0

I am trying to zoom into a given point and i honestly cannot understand what i am doing wrong.

First i get the mouse point and use the canvas transform matrix to get the point in canvas context (https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-gettransform).

const domPoint = new window.DOMPoint(event.offsetX, event.offsetY);
const currMatrix = this.canvasManager.ctx.getTransform();
const canvasPoint = domPoint.matrixTransform(currMatrix);

Then i translate, scale and translate back using the matrix returned in getTransform (DOMMatrix object).

const m = currMatrix
.translateSelf(canvasPoint.x, canvasPoint.y)
.scaleSelf(scale, scale)
.translateSelf(-canvasPoint.x, -canvasPoint.y);

Finally i set the transform using the last matrix.

this.ctx.setTransform(this.zoom.matrix)

The method i am using is based on one stack overflow answer.

Here's my code

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var currMatrix = ctx.getTransform();

var plusBtn = document.getElementById("plus");
var minusBtn = document.getElementById("minus");
var infoP = document.getElementById("info");

var zoom;
var scale = 1;
const scaleFactor = 0.1;

function draw() {
 ctx.clearRect(0, 0, canvas.width, canvas.height);
 ctx.setTransform(currMatrix)
 ctx.fillStyle = "blue";
 ctx.fillRect(50, 50, 100, 100);
}
draw()

function getDomPoint(event) {
 const offSetCanvasLeft = canvas.getBoundingClientRect().left;
  const offSetCanvasTop = canvas.getBoundingClientRect().top;

 return {
    x: event.pageX - offSetCanvasLeft,
    y: event.pageY - offSetCanvasTop,
  }
}

function domToCanvasPoint(point) {
 const domPoint = new window.DOMPoint(point.x, point.y);
 return domPoint.matrixTransform(currMatrix);
}

function updateScale() {
  scale = zoom === 'in' ? scale + scaleFactor : scale - scaleFactor ;
  setDebugInfo(scale)
}

function zoomMatrixIntoPoint(point) {
 currMatrix = currMatrix
    .translateSelf(point.x, point.y)
    .scaleSelf(scale, scale)
    .translateSelf(-point.x, -point.y);
}

function setDebugInfo(msg) {
 infoP.innerHTML = msg;
}

canvas.addEventListener('mousedown', function(event) {  
 const domPoint = getDomPoint(event);
  const canvasPoint = domToCanvasPoint(domPoint);
  updateScale();
  zoomMatrixIntoPoint(canvasPoint);
  draw()
}, false);

plusBtn.addEventListener('click', function(event) {    
 zoom = 'in'
  setDebugInfo(`ZOOM IN with scale ${scale}`)
}, false);

minusBtn.addEventListener('click', function(event) {    
 zoom = 'out'
  setDebugInfo(`ZOOM OUT with scale ${scale}`)
}, false);
<canvas id="myCanvas" width="300" height="300" style="border:1px solid black"></canvas>
<button id="plus">+</button>
<button id="minus">-</button>
<p id="info">info here!</p>

Would appreciate any help. Thank you.

gman
  • 100,619
  • 31
  • 269
  • 393
Diogo Aleixo
  • 841
  • 1
  • 8
  • 20

1 Answers1

1

Your issue is a simple logical error.

You are keeping curMatrix and update it every time. This means that all the values you do pass to translateSelf and scaleSelf are relative to the previous values.

However, when you do scale -= scaleFactor or scale += scaleFactor, scale is the absolute scale value.
So when you use it later in scaleSelf, you are quickly setting an huge absolute scale value, which won't get down until it becomes less than 1.

(1.1 * 1.2 * 1.3 * 1.4 * 1.5 * 1.6) => real scale is 5.8

and

(1.1 * 1.2 * 1.3 * 1.2 * 1.1 * 1) => real scale is 2.3
                 ^-- click zoom-out

So all you need to fix is the line which sets this scale value: Instead of incrementing this value, set it always 1 based.

scale = zoom === 'in' ? 1 + scaleFactor : 1 - scaleFactor ;

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var currMatrix = ctx.getTransform();

var plusBtn = document.getElementById("plus");
var minusBtn = document.getElementById("minus");
var infoP = document.getElementById("info");

var zoom;
var scale = 1;
const scaleFactor = 0.1;

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.setTransform(currMatrix)
  ctx.fillStyle = "blue";
  ctx.fillRect(50, 50, 100, 100);
}
draw()

function getDomPoint(event) {
  const offSetCanvasLeft = canvas.getBoundingClientRect().left;
  const offSetCanvasTop = canvas.getBoundingClientRect().top;

  return {
    x: event.pageX - offSetCanvasLeft,
    y: event.pageY - offSetCanvasTop,
  }
}

function domToCanvasPoint(point) {
  const domPoint = new window.DOMPoint(point.x, point.y);
  return domPoint.matrixTransform(currMatrix);
}

function updateScale() {
  scale = zoom === 'in' ? 1 + scaleFactor : 1 - scaleFactor;
  setDebugInfo(scale)
}

function zoomMatrixIntoPoint(point) {
  currMatrix
    .translateSelf(point.x, point.y)
    .scaleSelf(scale, scale)
    .translateSelf(-point.x, -point.y);
}

function setDebugInfo(msg) {
  infoP.innerHTML = msg;
}

canvas.addEventListener('mousedown', function(event) {
  const domPoint = getDomPoint(event);
  const canvasPoint = domToCanvasPoint(domPoint);
  updateScale();
  zoomMatrixIntoPoint(canvasPoint);
  draw()
}, false);

plusBtn.addEventListener('click', function(event) {
  zoom = 'in'
  setDebugInfo(`ZOOM IN with scale ${scale}`)
}, false);

minusBtn.addEventListener('click', function(event) {
  zoom = 'out'
  setDebugInfo(`ZOOM OUT with scale ${scale}`)
}, false);
<canvas id="myCanvas" width="300" height="300" style="border:1px solid black"></canvas>
<button id="plus">+</button>
<button id="minus">-</button>
<p id="info">info here!</p>

Also note that DOMMatrix#scale() accepts optional origin arguments, which would allow you to avoid the two transform calls:

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var currMatrix = ctx.getTransform();

var plusBtn = document.getElementById("plus");
var minusBtn = document.getElementById("minus");
var infoP = document.getElementById("info");

var zoom;
var scale = 1;
const scaleFactor = 0.1;

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.setTransform(currMatrix)
  ctx.fillStyle = "blue";
  ctx.fillRect(50, 50, 100, 100);
}
draw()

function getDomPoint(event) {
  const offSetCanvasLeft = canvas.getBoundingClientRect().left;
  const offSetCanvasTop = canvas.getBoundingClientRect().top;

  return {
    x: event.pageX - offSetCanvasLeft,
    y: event.pageY - offSetCanvasTop,
  }
}

function domToCanvasPoint(point) {
  const domPoint = new window.DOMPoint(point.x, point.y);
  return domPoint.matrixTransform(currMatrix);
}

function updateScale() {
  scale = zoom === 'in' ? 1 + scaleFactor : 1 - scaleFactor;
  setDebugInfo(scale)
}

function zoomMatrixIntoPoint(point) {
  currMatrix
  // scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ)  
    .scaleSelf(scale, scale, 1, point.x, point.y, 0)
}

function setDebugInfo(msg) {
  infoP.innerHTML = msg;
}

canvas.addEventListener('mousedown', function(event) {
  const domPoint = getDomPoint(event);
  const canvasPoint = domToCanvasPoint(domPoint);
  updateScale();
  zoomMatrixIntoPoint(canvasPoint);
  draw()
}, false);

plusBtn.addEventListener('click', function(event) {
  zoom = 'in'
  setDebugInfo(`ZOOM IN with scale ${scale}`)
}, false);

minusBtn.addEventListener('click', function(event) {
  zoom = 'out'
  setDebugInfo(`ZOOM OUT with scale ${scale}`)
}, false);
<canvas id="myCanvas" width="300" height="300" style="border:1px solid black"></canvas>
<button id="plus">+</button>
<button id="minus">-</button>
<p id="info">info here!</p>

And if you need all your values to be absolute (i.e translate too), then simply create a new DOMMatrix every time, and here keep your original scale increment:

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var currMatrix = ctx.getTransform();

var plusBtn = document.getElementById("plus");
var minusBtn = document.getElementById("minus");
var infoP = document.getElementById("info");

var zoom;
var scale = 1;
const scaleFactor = 0.1;

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.setTransform(currMatrix)
  ctx.fillStyle = "blue";
  ctx.fillRect(50, 50, 100, 100);
}
draw()

function getDomPoint(event) {
  const offSetCanvasLeft = canvas.getBoundingClientRect().left;
  const offSetCanvasTop = canvas.getBoundingClientRect().top;

  return {
    x: event.pageX - offSetCanvasLeft,
    y: event.pageY - offSetCanvasTop,
  }
}

function domToCanvasPoint(point) {
  const domPoint = new window.DOMPoint(point.x, point.y);
  return domPoint.matrixTransform(currMatrix);
}

function updateScale() {
  scale = zoom === 'in' ? scale + scaleFactor : scale - scaleFactor;
  setDebugInfo(scale)
}

function zoomMatrixIntoPoint(point) {
  // create a new DOMMatrix
  currMatrix = new DOMMatrix()
    .scaleSelf(scale, scale, 1, point.x, point.y, 0)
}

function setDebugInfo(msg) {
  infoP.innerHTML = msg;
}

canvas.addEventListener('mousedown', function(event) {
  const domPoint = getDomPoint(event);
  const canvasPoint = domToCanvasPoint(domPoint);
  updateScale();
  zoomMatrixIntoPoint(canvasPoint);
  draw()
}, false);

plusBtn.addEventListener('click', function(event) {
  zoom = 'in'
  setDebugInfo(`ZOOM IN with scale ${scale}`)
}, false);

minusBtn.addEventListener('click', function(event) {
  zoom = 'out'
  setDebugInfo(`ZOOM OUT with scale ${scale}`)
}, false);
<canvas id="myCanvas" width="300" height="300" style="border:1px solid black"></canvas>
<button id="plus">+</button>
<button id="minus">-</button>
<p id="info">info here!</p>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • You are right. That was an error. But it is not the error. I have updated the fiddle to use SVG matrix. Using SVG matrix everything runs fine. Using DOMMatrix, i run into unexpected behaviour. https://jsfiddle.net/diogoaleixo/8rsyv4oL/55/ – Diogo Aleixo Oct 22 '18 at 01:21
  • Because then you are creating a new SVGMatrix every time, and you do set its values to **absolute** values. – Kaiido Oct 22 '18 at 01:26
  • Sorry i am not getting it. Yes i am creating one SVGMatrix every time. But assigning the a,b,c,d,e and f values to it from the currMatrix variable. Anyway, the code you posted does not appear to do the right thing. – Diogo Aleixo Oct 22 '18 at 01:34
  • And what is the right thing? How is this not the right thing? And It is absolute because `curMatrix` never gets updated there. So it is always `1 0 0 1 0 0` and your svg matrix initialisation step is a noop, you're just setting the same values, until you call xxxSelf, which are then absolute values. My code only fixed scale, maybe you want `translate` to be set absolutely every time? – Kaiido Oct 22 '18 at 01:35
  • You seem like someone who know this. What is the best solutions ? If you go to the last fiddle i posted, you can see the correct behaviour using the SVGMatrix. – Diogo Aleixo Oct 22 '18 at 01:50
  • I just saw that your last example does not work ok. If i zoom inside the element, the zoom is moving on the wrong way. And if i come go zoom 100,90,80,70,40,25, and then all the way arrow the currMatrix.a value will not be 1 like it would suppose to be. Do you know what is wrong? – Diogo Aleixo Oct 23 '18 at 00:19