Search code examples
javascriptcanvaszooming

JS Canvas Zoom in and Zoom out translate doesn't settle to center


I'm working on my own canvas drawer project, and just stuck in the zoom in/out function. In my project I'm using scale and translate to make the zoom, as I want to keep all the canvas and its elements in the center. After sketching a little bit(not a math genius), I succeeded to draw out the following formula to use in the translate process, so the canvas will be kept in the middle of its view port after zooming: Old width and height / 2 - New width and height(which are old width and height multiply by scale step, which is 1.1 in my case) / 2. Logically speaking, that should make it work. But after trying few times the zoom in and zoom out, I can clearly see that the canvas has a little offset and it's not being centered to the middle of the viewport(by viewport I mean the stroked square representing the canvas).

I took my code out of my project and put it in fiddle, right here:

https://jsfiddle.net/s82qambx/3/

index.html

<div id="letse-canvas-container">
<canvas id="letse-canvas" width="300px" height="300px"></canvas>
<canvas id="letse-upper-canvas" width="300px" height="300px"></canvas>
</div>
<div id="buttons">
<button id="zoomin">
Zoom-in
</button>
<button id="zoomout">
Zoom-out
</button>
</div>

main.js

const canvas = {
canvas: document.getElementById('letse-canvas'),
upperCanvas: document.getElementById('letse-upper-canvas')
};

canvas.canvas.ctx = canvas.canvas.getContext('2d');
canvas.upperCanvas.ctx = canvas.upperCanvas.getContext('2d');

const CANVAS_STATE = {
    canvas: {
    zoom: 1,
width: 300,
height: 300
}
}

const Elements = [
    {
    x: 20,
    y: 20,
    width: 30,
    height: 40
  },
  {
    x:170,
    y:30,
    width: 100,
    height: 100
  }
];

const button = {
zoomin: document.getElementById('zoomin'),
zoomout: document.getElementById('zoomout')
}

button.zoomin.addEventListener('click', (e) => {
canvasZoomIn(e, canvas);
});
button.zoomout.addEventListener('click', (e) => {
canvasZoomOut(e, canvas);
});

function canvasZoomIn(e, canvas) {
const zoomData = getZoomData('in');

canvas.upperCanvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.upperCanvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.upperCanvas.ctx.clearRect(0, 0, 300, 300);

canvas.canvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.canvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.canvas.ctx.clearRect(0, 0, 300, 300);
Elements.forEach((element) => {
canvas.canvas.ctx.strokeRect(element.x, element.y, element.width, element.height);
});

CANVAS_STATE.canvas.zoom = zoomData.scale;
CANVAS_STATE.canvas.width = zoomData.docWidth;
CANVAS_STATE.canvas.height = zoomData.docHeight;

console.log(CANVAS_STATE.canvas.zoom, 'zoom');
console.log(CANVAS_STATE.canvas.width, 'width');
console.log(CANVAS_STATE.canvas.height, 'height');

canvas.canvas.ctx.strokeRect(0, 0, 300, 300);
canvas.canvas.ctx.beginPath();
canvas.canvas.ctx.moveTo(0, 150);
canvas.canvas.ctx.lineTo(300, 150);
canvas.canvas.ctx.stroke();
CANVAS_STATE.canvas.draggable = canvas.canvas.width < CANVAS_STATE.canvas.width || canvas.canvas.height < CANVAS_STATE.canvas.height;
}

function canvasZoomOut(e, canvas) {
const zoomData = getZoomData('out');

canvas.upperCanvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.upperCanvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.upperCanvas.ctx.clearRect(0, 0, canvas.canvas.width, canvas.canvas.height);

canvas.canvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.canvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.canvas.ctx.clearRect(0, 0, canvas.canvas.width, canvas.canvas.height);
Elements.forEach((element) => {
canvas.canvas.ctx.strokeRect(element.x, element.y, element.width, element.height);
});

CANVAS_STATE.canvas.zoom = zoomData.scale;
CANVAS_STATE.canvas.width = zoomData.docWidth;
CANVAS_STATE.canvas.height = zoomData.docHeight;

console.log(CANVAS_STATE.canvas.zoom, 'zoom');
console.log(CANVAS_STATE.canvas.width, 'width');
console.log(CANVAS_STATE.canvas.height, 'height');

canvas.canvas.ctx.strokeRect(0, 0, 300, 300);
canvas.canvas.ctx.beginPath();
canvas.canvas.ctx.moveTo(0, 150);
canvas.canvas.ctx.lineTo(300, 150);
canvas.canvas.ctx.stroke();
CANVAS_STATE.canvas.draggable = canvas.canvas.width < CANVAS_STATE.canvas.width || canvas.canvas.height < CANVAS_STATE.canvas.height;
}

function getZoomData(zoom) {
const zoomStep = zoom === 'in' ? 1.1 : 1 / 1.1;
const scale = CANVAS_STATE.canvas.zoom * zoomStep;
const docWidth = CANVAS_STATE.canvas.width * zoomStep;
const docHeight = CANVAS_STATE.canvas.height * zoomStep;
const translateX = CANVAS_STATE.canvas.width / 2 - docWidth / 2;
const translateY = CANVAS_STATE.canvas.height / 2 - docHeight / 2;

console.log(zoomStep);
console.log(scale, 'check');
console.log(docWidth);
console.log(docHeight);
console.log(translateX, 'check');
console.log(translateY, 'check');

return {
zoomStep,
scale,
docWidth,
docHeight,
translateX,
translateY
};
}

main.css

#letse-canvas-container {
position: relative;
float: left;
}

#letse-canvas {
border: 1px solid rgb(0, 0, 0);
/* visibility: hidden; */
}

#letse-upper-canvas {
/* position: absolute; */
/* top: 0px; */
left: 0px;
border: 1px solid;
/* visibility: hidden; */
}

Can someone suggest a reason? What am I missing here?


Solution

  • OK! So I managed to derive the right formula after searching in the net and testing few options. I used:

     function getZoomData(zoom) {
        const zoomStep = zoom === 'in' ? 1.1 : 1 / 1.1;
        const oldZoom = CANVAS_STATE.canvas.zoom;
        const newZoom = oldZoom * zoomStep;
        const zoomDifference = newZoom - oldZoom;
        const docWidth = CANVAS_STATE.canvas.width * newZoom;
        const docHeight = CANVAS_STATE.canvas.height * newZoom;
        const translateX = (-(canvas.canvas.width / 2 * zoomDifference / newZoom));
        const translateY = (-(canvas.canvas.height / 2 * zoomDifference / newZoom));