I'm trying to create zoom effect on canvas, and I've managed to do that, but there's a small problem. Zooming (scaling) origin is top left of the canvas. How can I specify a zoom/scale origin?
I suppose I need to use translate
but I don't know how and where I should implement it.
What I want to use as zoom origin is the mouse position, but for simplicity, center of canvas will do.
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 400;
var global = {
zoom: {
origin: {
x: null,
y: null,
},
scale: 1,
},
};
function zoomed(number) {
return Math.floor(number * global.zoom.scale);
}
function draw() {
context.beginPath();
context.rect(zoomed(50), zoomed(50), zoomed(100), zoomed(100));
context.fillStyle = 'skyblue';
context.fill();
context.beginPath();
context.arc(zoomed(350), zoomed(250), zoomed(50), 0, 2 * Math.PI, false);
context.fillStyle = 'green';
context.fill();
}
draw();
canvas.addEventListener("wheel", trackWheel);
canvas.addEventListener("wheel", zoom);
function zoom() {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
draw();
}
function trackWheel(e) {
if (e.deltaY < 0) {
if (global.zoom.scale < 5) {
global.zoom.scale *= 1.1;
}
} else {
if (global.zoom.scale > 0.1) {
global.zoom.scale *= 0.9;
}
}
global.zoom.scale = parseFloat(global.zoom.scale.toFixed(2));
}
body {
background: gainsboro;
margin: 0;
}
canvas {
background: white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>
Update 1
It seems there are few other question related to this subject on SO, but none that I can directly implement in my code.
I've tried to examine the demo Phrogz provided in Zoom Canvas to Mouse Cursor, but it's far too complex (for me, at least). Tried to implement his solution:
ctx.translate(pt.x,pt.y);
ctx.scale(factor,factor);
ctx.translate(-pt.x,-pt.y);
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 400;
var global = {
zoom: {
origin: {
x: null,
y: null,
},
scale: 1,
},
};
function draw() {
context.beginPath();
context.rect(50, 50, 100, 100);
context.fillStyle = 'skyblue';
context.fill();
context.beginPath();
context.arc(350, 250, 50, 0, 2 * Math.PI, false);
context.fillStyle = 'green';
context.fill();
}
draw();
canvas.addEventListener("wheel", trackWheel);
canvas.addEventListener("wheel", trackMouse);
canvas.addEventListener("wheel", zoom);
function zoom() {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
context.translate(global.zoom.origin.x, global.zoom.origin.y);
context.scale(global.zoom.scale, global.zoom.scale);
context.translate(-global.zoom.origin.x, -global.zoom.origin.y);
draw();
}
function trackWheel(e) {
if (e.deltaY > 0) {
if (global.zoom.scale > 0.1) {
global.zoom.scale *= 0.9;
}
} else {
if (global.zoom.scale < 5) {
global.zoom.scale *= 1.1;
}
}
global.zoom.scale = parseFloat(global.zoom.scale.toFixed(2));
}
function trackMouse(e) {
global.zoom.origin.x = e.clientX;
global.zoom.origin.y = e.clientY;
}
body {
background: gainsboro;
margin: 0;
}
canvas {
background: white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>
but it didn't really help. It seems to use the mouse position as the zoom origin but there are "jumps" when I zoom in.
Update 2
I've managed to isolate and simplify the zoom effect from Blindman67's example to understand how it works better. I gotta admit, I still don't fully understand it :) I'm gonna share it here. Future visitors might benefit.
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 400;
var zoom = {
scale : 1,
screen : {
x : 0,
y : 0,
},
world : {
x : 0,
y : 0,
},
};
var mouse = {
screen : {
x : 0,
y : 0,
},
world : {
x : 0,
y : 0,
},
};
var scale = {
length : function(number) {
return Math.floor(number * zoom.scale);
},
x : function(number) {
return Math.floor((number - zoom.world.x) * zoom.scale + zoom.screen.x);
},
y : function(number) {
return Math.floor((number - zoom.world.y) * zoom.scale + zoom.screen.y);
},
x_INV : function(number) {
return Math.floor((number - zoom.screen.x) * (1 / zoom.scale) + zoom.world.x);
},
y_INV : function(number) {
return Math.floor((number - zoom.screen.y) * (1 / zoom.scale) + zoom.world.y);
},
};
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.rect(scale.x(50), scale.y(50), scale.length(100), scale.length(100));
context.fillStyle = 'skyblue';
context.fill();
context.beginPath();
context.arc(scale.x(350), scale.y(250), scale.length(50), 0, 2 * Math.PI, false);
context.fillStyle = 'green';
context.fill();
}
canvas.addEventListener("wheel", zoomUsingCustomScale);
function zoomUsingCustomScale(e) {
trackMouse(e);
trackWheel(e);
scaleShapes();
}
function trackMouse(e) {
mouse.screen.x = e.clientX;
mouse.screen.y = e.clientY;
mouse.world.x = scale.x_INV(mouse.screen.x);
mouse.world.y = scale.y_INV(mouse.screen.y);
}
function trackWheel(e) {
if (e.deltaY < 0) {
zoom.scale = Math.min(5, zoom.scale * 1.1);
} else {
zoom.scale = Math.max(0.1, zoom.scale * (1/1.1));
}
}
function scaleShapes() {
zoom.screen.x = mouse.screen.x;
zoom.screen.y = mouse.screen.y;
zoom.world.x = mouse.world.x;
zoom.world.y = mouse.world.y;
mouse.world.x = scale.x_INV(mouse.screen.x);
mouse.world.y = scale.y_INV(mouse.screen.y);
draw();
}
draw();
body {
background: gainsboro;
margin: 0;
}
canvas {
background: white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>
Since this is a simplified version, I suggest you check out the Blindman67's example first. Also, even though I've accepted Blindman67's answer, you can still post an answer. I find this subject interesting. So I'd like to know more about it.