The context
I'm creating a very simple game with html canvas (inside a next.js app). In this canvas I draw a few rectangles with some gap in between. If you know coloring pixels the game I bascially recreating this. This is what it looks like so far:
I also implemented a zoom feature and drag feature. The logic for it I found on google. But here is how the update function looks like:
update = (ctx) => {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
ctx.translate(window.innerWidth / 2, window.innerHeight / 2);
ctx.scale(this.cameraZoom, this.cameraZoom);
ctx.translate(
-window.innerWidth / 2 this.cameraOffset.x,
-window.innerHeight / 2 this.cameraOffset.y
);
ctx.clearRect(0, 0, window.width, window.height);
this.drawTiles(ctx);
};
this.cameraZoom
just changes on mousewheel and the this.cameraOffset
changes on mousemove and starts with cameraOffset = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
. This works fine, I can zoom in and drag the canvas around.
The problem I have is when I click the squares - the wrong rectangles get filled after dragging
The Problem
Before the zoom and drag feature I implemented a logic, so I can click on the canvas and the correct rectangle will be filled with a color. That works fine. Then I implemented zooming, and I adjusted that logic until I made it work. But now I have to readjust it further because after dragging the "coordinates" aren't correct anymore, but I can't figure out a correct formula. Here is what I have so far (this function sits on every tile
or rect
I draw:
setPaintingColor = (game) => {
const minX = (this.x - game.gap) * game.cameraZoom;
const maxX = (this.x this.width - game.gap) * game.cameraZoom;
const minY = (this.y - game.gap) * game.cameraZoom;
const maxY = (this.y this.height - game.gap) * game.cameraZoom;
if (!game.mouse.down) {
return;
}
if (
game.mouse.x >= minX &&
game.mouse.x <= maxX &&
game.mouse.y >= minY &&
game.mouse.y <= maxY
) {
this.color = "yellow";
}
};
So this sets the fill color of the rect and then the game update loop constantly calls draw of each rect (which fills the rect with the set color this.color
.
The question
So now I want to find out a logic that works after dragging and zooming. When I drag the canvas and then try to click rects, nothing get painted.
I played around with the this.cameraOffset.x y
values adding it, subtracting it etc, the closest I got was, when i clicked a tile, the tile to the left got painted but it wasn't logical to me at all.
So what I also tried is I calculate the distance I have moved after dragging (I set this.dragStart
and this.dragEnd
at the game object. Then adding it to the setPaintingColor
function like that:
const distanceMovedX = game.dragStart.x - game.dragEnd.x;
const distanceMovedY = game.dragStart.y - game.dragEnd.y;
const minX = (this.x - game.gap distanceMovedX) * game.cameraZoom;
const maxX =
(this.x this.width - game.gap distanceMovedX) * game.cameraZoom;
const minY = (this.y - game.gap distanceMovedY) * game.cameraZoom;
const maxY =
(this.y this.height - game.gap distanceMovedY) * game.cameraZoom;
Now when I drag and then click rects, about 6 - 8 tiles to the right where i clicked the rects get painted.
Does anybody know which logic I need to make it work again?
Thank you very much!
CodePudding user response:
I solved the problem.
The game
object has a translateX
and translateY
property. That is calculated after the canvas have been moved and also some other calculation with screen width etc.
This is the logic that works now:
setPaintingColor = (game) => {
if (game.wantsToDrag) {
return;
}
const minX = (this.x - game.gap game.translateX) * game.cameraZoom;
const maxX =
(this.x this.width - game.gap game.translateX) * game.cameraZoom;
const minY = (this.y - game.gap game.translateY) * game.cameraZoom;
const maxY =
(this.y this.height - game.gap game.translateY) * game.cameraZoom;
if (!game.mouse.down) {
return;
}
if (
game.mouse.x >= minX &&
game.mouse.x <= maxX &&
game.mouse.y >= minY &&
game.mouse.y <= maxY
) {
this.color = "yellow";
}
};
}
So basically I added game.translateX
and game.translateY
to the calculation and then multiplying it with the camera zoom.
Now everything works as expected.
CodePudding user response:
To get from a pixel to a context coordinate, you can use getTransform
like so:
const x = e.clientX - cvs.offsetLeft;
const y = e.clientY - cvs.offsetTop;
const t = ctx.getTransform();
const dx = t.e;
const dy = t.f;
const zx = t.a;
const zy = t.d;
const xt = (x - dx) / zx;
const yt = (y - dy) / zy;
Here's a working snippet:
const cvs = document.querySelector("canvas");
const ctx = cvs.getContext("2d");
const zoomSlider = document.querySelector("[data-slide=zoom]");
const xSlider = document.querySelector("[data-slide=panx]");
const ySlider = document.querySelector("[data-slide=pany]");
const S = 8;
const grid = Array.from({ length: 256 / S }, () => Array(256 / S).fill(0));
const drawGrid = () => {
grid.forEach((row, r) => {
row.forEach((cell, c) => {
ctx.fillStyle = cell ? "blue": "white";
ctx.fillRect(
c * 8 1,
r * 8 1,
S - 2,
S - 2
);
});
});
}
const setTransform = () => {
const dx = xSlider.valueAsNumber;
const dy = ySlider.valueAsNumber;
const zoom = zoomSlider.valueAsNumber;
ctx.translate(dx, dy);
ctx.scale(zoom, zoom);
}
const update = () => {
ctx.resetTransform();
ctx.clearRect(0, 0, cvs.width, cvs.height);
setTransform();
drawGrid();
}
// Interactivity
[zoomSlider, xSlider, ySlider].forEach(el => el.addEventListener("input", update));
cvs.addEventListener("click", e => {
const x = e.clientX - cvs.offsetLeft;
const y = e.clientY - cvs.offsetTop;
const t = ctx.getTransform();
const dx = t.e;
const dy = t.f;
const zx = t.a;
const zy = t.d;
const xt = (x - dx) / zx;
const yt = (y - dy) / zy;
// Color the right cell
const cx = Math.floor(xt / S);
const cy = Math.floor(yt / S);
grid[cy][cx] = 1;
update();
});
update();
document.body.appendChild(cvs);
<label>
Zoom: <input data-slide="zoom" type="range" min="0.5" max="2" value="1" step="0.1">
</label>
<br>
<label>
Horizontal pan: <input data-slide="panx" value="0" type="range" min="-128" max="128" step="1">
</label>
<br>
<label>
Vertical pan: <input data-slide="pany" value="0" type="range" min="-128" max="128" step="1">
</label>
<br>
<canvas width="256" height="256" style="border: 1px solid red; background: grey"></canvas>