Isometric Mouse
You know how to move in isometric. You know how to click-to-move. Now combine them. The tricky part isn’t the movement β it’s figuring out which diamond tile the mouse is actually over:
THE CHALLENGE π€
In the previous mouse tutorial, converting a click to a tile was simple division:
const tileCol = Math.floor(mx / TILE_SIZE);
const tileRow = Math.floor(my / TILE_SIZE);
In isometric, tiles are placed at angles. The same screen pixel could be in completely different logical tiles depending on whether you’re looking at it as a row or column. Simple division doesn’t work anymore.
The diamond tile that lives at logical position (2, 1) appears at a completely different screen location than the tile at (1, 2) β even though both might be at the same screen Y coordinate. You need to invert the isoToScreen() transform.
INVERTING THE FORMULA
The forward transform is:
screenX = (worldX - worldY) + OFFSET_X
screenY = (worldX + worldY) / 2 + OFFSET_Y
To invert it, solve for worldX and worldY. First, strip the offsets:
relX = screenX - OFFSET_X β relX = worldX - worldY ... (1)
relY = screenY - OFFSET_Y β relY = (worldX + worldY) / 2 ... (2)
Add equation (1) to 2 Γ equation (2):
relX + 2 Γ relY = (worldX - worldY) + (worldX + worldY) = 2 Γ worldX
β΄ worldX = (relX + 2 Γ relY) / 2
Subtract equation (1) from 2 Γ equation (2):
2 Γ relY - relX = (worldX + worldY) - (worldX - worldY) = 2 Γ worldY
β΄ worldY = (2 Γ relY - relX) / 2
In code:
function screenToWorld(mx, my) {
const relX = mx - OFFSET_X;
const relY = my - OFFSET_Y;
return {
worldX: (relX + 2 * relY) / 2,
worldY: (2 * relY - relX) / 2
};
}
Then round worldX / TILE_SIZE and worldY / TILE_SIZE to get tile coordinates:
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const world = screenToWorld(mx, my);
mouseCol = Math.round(world.worldX / TILE_SIZE);
mouseRow = Math.round(world.worldY / TILE_SIZE);
});
Math.round (not Math.floor) gives the nearest tile center, which feels right for isometric picking.
THE DIAMOND CURSOR πΆ
A rectangular highlight rectangle looks wrong on diamond tiles. Draw the cursor as a diamond polygon matching the tile shape exactly:
// Matches makeGroundTile() exactly - same polygon, just semi-transparent
const cursor = new PIXI.Graphics()
.poly([30, 0, 60, 15, 30, 30, 0, 15])
.fill({ color: 0xffff00, alpha: 0.4 });
cursor.zIndex = 9999; // always on top
app.stage.addChild(cursor);
canvas.addEventListener('mousemove', (e) => {
// ... get mouseCol, mouseRow via screenToWorld() ...
if (isWalkable(mouseCol, mouseRow)) {
// Position using the same formula as regular tiles
const screen = isoToScreen(mouseCol * TILE_SIZE, mouseRow * TILE_SIZE);
cursor.x = screen.x - TILE_SIZE;
cursor.y = screen.y - TILE_SIZE / 2;
cursor.visible = true;
} else {
cursor.visible = false;
}
});
The cursor polygon is identical to the ground tile polygon - it just snaps to the nearest valid tile as the mouse moves.
THE MOVEMENT CODE πΆ
The tile-by-tile movement from tutorial 20 works unchanged in world space. worldX % TILE_SIZE === CENTER still detects tile centers, dirX/dirY still control horizontal/vertical movement - it’s all the same. Only the final render step changes:
function movePlayer() {
// ... same atCenter check, same direction picking as tutorial 20 ...
player.worldX += player.dirX * player.speed;
player.worldY += player.dirY * player.speed;
// Convert world position to isometric screen coords
const screen = isoToScreen(player.worldX, player.worldY);
player.sprite.x = screen.x - 8; // center 16px sprite
player.sprite.y = screen.y - 4; // center 8px sprite
player.sprite.zIndex = player.worldX + player.worldY + TILE_SIZE / 2;
}
The player visually glides diagonally along the isometric grid while internally moving on the flat world grid.
What you’ve built:
- β
screenToWorld()inverse transform derived from the forward formula - β Diamond-shaped tile cursor that snaps to the nearest tile
- β Click-to-move that targets isometric tiles correctly
- β Tile-by-tile movement adapted to render in isometric space
Next up: The iso world gets bigger than the screen. Next: Isometric Scroll