Reputation: 1248
I'm trying to implement zoom in an HTML5 Canvas. I came across other threads that explain how to do it, but they take advantage of the canvas' context, storing previous transformations. I want to avoid that.
So far I managed to do the following (https://jsfiddle.net/wfqzr538/)
<!DOCTYPE html>
<html>
<body style="margin: 0">
<canvas id="canvas" width="400" height="400" style="border: 1px solid #d3d3d3"></canvas>
<script>
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
let cursorX = 0,
cursorY = 0;
let zoom = 1;
canvas.onmousemove = mouseMove;
window.onkeydown = keyDown;
draw();
function mouseMove(evt) {
cursorX = evt.clientX;
cursorY = evt.clientY;
}
function keyDown(evt) {
if (evt.key == "p") {
zoom += 0.1;
}
if (evt.key == "m") {
zoom -= 0.1;
}
draw();
}
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
const translationX = -cursorX * (zoom - 1);
const translationY = -cursorY * (zoom - 1);
context.translate(translationX, translationY);
context.scale(zoom, zoom);
context.fillRect(100, 100, 30, 30);
context.scale(1 / zoom, 1 / zoom);
context.translate(-translationX, -translationY);
}
</script>
</body>
</html>
The code above works If I zoom at the same location, but breaks as soon as I change it. For example, if I zoom in twice at the top left corner of the square, it works. However, if I zoom once at the top left corner, followed by zooming at the right bottom corner, it breaks.
I've been trying to fix this for a while now. I think it has something with not taking into account previous translations made in the canvas, but I'm not sure.
I'd really appreciate some help.
Upvotes: 1
Views: 962
Reputation: 54026
If you don't want to keep the previous transform state then the is no way to do what you are trying to do.
There are many way to keep the previous state
You can transform all object's world state, in effect embedding the previous transform in the object's coordinates.
Eg with zoom and translate
object.x = object.x * zoom + translate.x;
object.y = object.y * zoom + translate.y;
object.w *= zoom;
object.h *= zoom;
then draw using default transform
ctx.setTransform(1,0,0,1,0,0);
ctx.fillRect(object.x, object.y, object.w, object.h);
To zoom at a point (absolute pixel coordinate) you need to know where the previous origin was so you can workout how far the zoom point is from that origin and move it correctly when zooming.
Your code does not keep track of the origin, in effect it assumes it is always at 0,0.
The example tracks the previous transform
state using an array that represents the transform. It is equivalent to what your code defines as translation
and zoom
.
// from your code
context.translate(translationX, translationY);
context.scale(zoom, zoom);
// is
transform = [zoom, 0, 0, zoom, translationX, translationY];
The example also changes the rate of zooming. In your code you add and subtract from the zoom, this will result in negative zooms, and when zooming in it will take forever to get to large scales. The scale is apply as a scalar eg zoom *= SCLAE_FACTOR
The function zoomAt
zooms in or out at a given point on the canvas
const ctx = canvas.getContext("2d");
const transform = [1,0,0,1,0,0];
const SCALE_FACTOR = 1.1;
const pointer = {x: 0, y: 0};
var zoom = 1;
canvas.addEventListener("mousemove", mouseMove);
addEventListener("keydown", keyDown);
draw();
function mouseMove(evt) {
pointer.x = evt.clientX;
pointer.y = evt.clientY;
drawPointer();
}
function keyDown(evt) {
if (evt.key === "p") { zoomAt(SCALE_FACTOR, pointer) }
if (evt.key === "m") { zoomAt(1 / SCALE_FACTOR, pointer) }
}
function zoomAt(amount, point) {
zoom *= amount;
transform[0] = zoom; // scale x
transform[3] = zoom; // scale y
transform[4] = point.x - (point.x - transform[4]) * amount;
transform[5] = point.y - (point.y - transform[5]) * amount;
draw();
}
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(...transform);
ctx.fillRect(100, 100, 30, 30);
}
function drawPointer() {
draw();
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
const invScale = 1 / transform[0]; // Assumes uniform scale
const x = (pointer.x - transform[4]) * invScale;
const y = (pointer.y - transform[5]) * invScale;
const size = 10 * invScale;
ctx.beginPath();
ctx.lineTo(x - size, y);
ctx.lineTo(x + size, y);
ctx.moveTo(x, y - size);
ctx.lineTo(x, y + size);
ctx.setTransform(1, 0, 0, 1, 0, 0); // to ensure line width is 1 px
ctx.stroke();
ctx.font = "16px arial";
ctx.fillText("Pointer X: " + x.toFixed(2) + " Y: " + y.toFixed(2), 10, 20);
}
* {margin: 0px}
canvas { border: 1px solid #aaa }
<canvas id="canvas" width="400" height="400"></canvas>
I have added the function drawPointer
which uses the transform to calculate the pointers world position, render a cross hair at the position and display the coordinates.
Upvotes: 2