The basic idea comes from the map of a game. According to the my code review, the map is a full-page canvas. I have no problem with drawing images on canvas. My question is how to detect map houses and update the canvas or even add click ability to it. I have attached a GIF and HTML code from the original game to better understand my request.
<div id="canvasBorder"><canvas id="canvasMap"></canvas></div>
Okay, This is my code. It's simple. I have drawn the map houses according to the main image which is large on the canvas.
function onClick2() {
const imagePath = '/lobby/map.png';
//Image Positions and Width/Height
const array = [
{ x: 1764, y: 1104, w: 126, h: 84 },
{ x: 0, y: 1188, w: 126, h: 84 },
{ x: 126, y: 1188, w: 126, h: 84 },
{ x: 2090, y: 340, w: 126, h: 68 },
{ x: 126, y: 1188, w: 126, h: 84 },
];
if (canvasRef?.current) {
let x = canvasRef?.current.getContext('2d');
let img = new Image();
img.src = path;
//Draw Map Blocks
//Here I deleted the extra codes, I just wanted to show that it was done this way.
if (x) {
x.drawImage(
img,
array[3].x,
array[3].y,
array[3].w,
array[3].h,
0,
0,
array[3].w,
array[3].h
);
}
}
}
Here I need your guidance to understand the implementation trick. Here we need to recognize the mouse movement on the image or we need a number of squares that are rotated and have the image and work with the isPointInPath function.
If we proceed with the second way that I mentioned, to draw the squares, we need rotate(-0.25 * Math.PI);
You need to track the cursor position with mousemove
and then do a simple change of basis to translate screen coordinates to game grid coordinates:
The cursor's position v_C = (x, y)
is in the canonical basis, C
.
You have an alternate basis B
(your 2.5D grid), and its alternate basis vectors in basis C
are:
columnVector_C = (cellHalfSizeLong, cellHalfSizeShort)
rowsVector_C = (-cellHalfSizeLong, cellHalfSizeShort)
You put them in columns on a matrix M_BC
:
const M_BC = [
[cellHalfSizeLong, -cellHalfSizeLong],
[cellHalfSizeShort, cellHalfSizeShort],
];
This matrix helps you translate vectors from basis B
to C
Invert that matrix to get M_CB
. This matrix helps you translate vectors from basis C
to B
.
v_B = M_CB * v_C
Lastly, floor the coordinates. This tells you what cell to select/highlight in the 2.5D grid.
Still:
I'm not a mathematician, so the nomenclature I'm using might be off, specially considering I'm mixing more formal vector/matrix expressions with regular camelCase variable names.
It's easier to see it than to read it, so, first, watch this: 3Blue1Brown - Change of basis | Chapter 13, Essence of linear algebra.
Here's a working example:
// Some basic utils to work with matrices and vectors:
function matrixVectorMultiply(matrix, vector) {
let result = [];
for (let i = 0; i < matrix.length; i++) {
let sum = 0;
for (let j = 0; j < vector.length; j++) {
sum += matrix[i][j] * vector[j];
}
result.push(sum);
}
return result;
}
function invertMatrix(matrix) {
const n = matrix.length;
let identity = [];
for (let i = 0; i < n; i++) {
identity.push([]);
for (let j = 0; j < n; j++) {
identity[i].push(i === j ? 1 : 0);
}
}
// Apply Gauss-Jordan elimination:
for (let i = 0; i < n; i++) {
let pivot = matrix[i][i];
for (let j = 0; j < n; j++) {
matrix[i][j] /= pivot;
identity[i][j] /= pivot;
}
for (let k = 0; k < n; k++) {
if (k !== i) {
let factor = matrix[k][i];
for (let j = 0; j < n; j++) {
matrix[k][j] -= factor * matrix[i][j];
identity[k][j] -= factor * identity[i][j];
}
}
}
}
return identity;
}
// Define the grid data (colors of each cell):
const gridData = [
['#008800', '#00FF00', '#008800', '#00FF00', '#008800', '#00FF00'],
['#00FF00', '#008800', '#00FF00', '#008800', '#00FF00', '#008800'],
['#008800', '#00FF00', '#000088', '#00FF00', '#008800', '#00FF00'],
['#00FF00', '#008800', '#00FF00', '#008800', '#00FF00', '#000088'],
['#008800', '#00FF00', '#008800', '#00FF00', '#000088', '#0000FF'],
['#00FF00', '#008800', '#00FF00', '#000088', '#0000FF', '#000088'],
];
// This is just for the demo. In a real application, the grid data matrix would
// probably contain all the information on each cell objects (array items):
const gridColorToType = {
'#008800': 'Grass',
'#00FF00': 'Grass',
'#000088': 'Water',
'#0000FF': 'Water',
};
const selectedCellBolor = '#000000';
// Get the UI elements:
const positionLabelElement = document.getElementById('positionLabel');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
positionLabelElement.textContent = ' ';
// Adjust the canvas to the window:
const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;
// Grid sizing params:
const cellSizeLong = 100;
const cellHalfSizeLong = cellSizeLong / 2;
const cellSizeShort = cellSizeLong / 3;
const cellHalfSizeShort = cellSizeShort / 2;
// Keep track of the selected/highlighted cell:
let currentRow = 0;
let currentCol = 0;
// Drawing functions:
function drawCell(ctx, color, row, col) {
ctx.fillStyle = color;
// Calculate the position of the cell
const x = (col - row) * cellHalfSizeLong + width / 2;
const y = (col + row) * cellHalfSizeShort;
// Fill:
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + cellHalfSizeLong, y + cellHalfSizeShort);
ctx.lineTo(x, y + cellSizeShort);
ctx.lineTo(x - cellHalfSizeLong, y + cellHalfSizeShort);
ctx.closePath();
ctx.fill();
// Border:
ctx.strokeStyle = '#000000';
ctx.stroke();
}
function drawBoard() {
ctx.clearRect(0, 0, width, height);
const numRows = gridData.length;
const numCols = gridData[0].length;
// Draw all the cells in their respective color:
for (let row = 0; row < numRows; ++row) {
for (let col = 0; col < numCols; ++col) {
drawCell(ctx, gridData[row][col], row, col);
}
}
// And re-draw the selected one on top (you might want to do this differently):
drawCell(ctx, selectedCellBolor, currentRow, currentCol);
}
canvas.addEventListener('mousemove', () => {
const x_C = width / 2 - event.clientX;
const y_C = event.clientY;
// First column is the columns vector in the 2.5D grid.
// Second column is the rows vector in the 2.5 grid.
const M_BC = [
[cellHalfSizeLong, -cellHalfSizeLong],
[cellHalfSizeShort, cellHalfSizeShort],
];
// We need the inverse of that matrix to translate canonical basis
// coordinates to coordinates in the 2.5D space's base:
const M_CB = invertMatrix(M_BC);
const [x_B, y_B] = matrixVectorMultiply(M_CB, [x_C, y_C]);
const int_x_B = Math.floor(x_B);
const int_y_B = Math.floor(y_B);
currentRow = int_x_B;
currentCol = int_y_B;
const cellType = gridColorToType[gridData[currentRow]?.[currentCol]] || 'Void';
positionLabelElement.textContent = `(${
(x_C | 0).toFixed().padStart(4, ' ')
}, ${
(y_C | 0).toFixed().padStart(4, ' ')
}) => (${
x_B.toFixed(2).padStart(5, ' ')
}, ${
y_B.toFixed(2).padStart(5, ' ')
}) => (${
int_x_B.toFixed().padStart(2, ' ')
}, ${
int_y_B.toFixed().padStart(2, ' ')
}) => ${ cellType }`;
requestAnimationFrame(() => {
drawBoard();
});
});
drawBoard();
body {
background: #777;
}
#canvas {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
#positionLabel {
position: fixed;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, .5);
padding: 8px;
border-radius: 0 4px 0 0;
font-family: monospace;
font-weight: bold;
white-space: pre;
backdrop-filter: blur(8px);
pointer-events: none;
}
<canvas id="canvas"></canvas>
<div id="positionLabel"> <div>
If you don't care about pixel-perfect accuracy (e.g. you don't care whether if a cell has a tree that overflows a bit, covering the one behind, the mouse is not able to recognize being on top of the tree), then you don't need isPointInPath
, and you also gain a lot of performance.