Canvas Board Game - Canvas Board Game - BoardGamer.ie | Board Games Ireland
Canvas Board Game - BoardGamer.ie | Board Games Ireland

Building a canvas board game without frameworks

HTML5 Canvas is still the most straightforward way to put a board game in a browser. No plugin, no extra dependencies, just a <canvas> element and JavaScript. I spent about three weeks building a hex-grid strategy game this way and learned a few things that aren't documented anywhere useful.

canvas board game fundamentals

The basic loop looks like this: clear the canvas, redraw every game object at its current coordinates, repeat. For a board game specifically, you're drawing tiles, pieces, and UI elements on a fixed coordinate grid. The trick is keeping the coordinate system consistent when you add zoom or pan later. I started by mapping screen pixels directly to board coordinates. A 600x600 canvas with a 12x12 board means each tile is 50x50 pixels. Simple. Then I realized that approach dies fast once you want zoom functionality, which almost every board game needs. I switched to a game-world coordinate system where the board stays the same size regardless of canvas resolution. Zoom becomes a simple scale transform applied to the rendering context before drawing anything.

For the game loop itself, I use requestAnimationFrame rather than setInterval. The difference matters when you're doing hit detection on mouse events too. If your render loop and your input handler are decoupled, things feel smoother because the canvas isn't redrawing while the player is actively clicking around. Here's a minimal structure that actually works:

const canvas = document.getElementById('board'); const ctx = canvas.getContext('2d'); function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBoard(); drawPieces(); requestAnimationFrame(draw); }

That's it for the skeleton. The real work is in drawBoard and drawPieces. When I was building the hex grid for my game, I ran into an issue where the hex outlines would render slightly misaligned at certain zoom levels. The anti-aliasing on the canvas context was shifting sub-pixel coordinates by a fraction, causing visible gaps between adjacent hexagons. The workaround was rounding all tile positions to whole numbers before applying the zoom transform. It costs almost nothing in terms of performance and eliminates the visual glitch entirely.

Another thing nobody tells you about canvas board games: state management. You need to track where every piece is, whose turn it is, what moves are legal. I kept the game state in a plain JavaScript object separate from the rendering code. The draw function reads from state but never mutates it. This separation makes debugging about ten times easier because when something looks wrong on screen, you can log the state object and immediately see if the data is correct or if the renderer is lying to you.

Interaction handling that doesn't suck

Mouse events on a canvas board game require coordinate transformation. The raw clientX and clientY values from a mouse event are in screen coordinates. You need to convert them to board coordinates, accounting for any zoom level and pan offset you've applied. Here's the conversion function I ended up using:

👉 Clique no botão abaixo para saber mais sobre o assunto!

function screenToBoard(screenX, screenY) { return { x: (screenX - canvas.offsetLeft - panX) / zoom, y: (screenY - canvas.offsetTop - panY) / zoom }; }

This is the function that trips up most beginners. If your hit detection feels off, check whether you're subtracting the canvas offset. getBoundingClientRect() is more reliable than offsetLeft if your canvas sits inside positioned containers, because offsetLeft only goes up one parent level and can give you wrong values if there are multiple nested divs. I also added a small dead zone around tile boundaries. Without it, pieces that snap to grid positions but sit exactly on the edge between two tiles register clicks inconsistently. A 5-pixel tolerance buffer solved the issue completely. You subtract that buffer from the tile width when checking which tile a click landed on.

Performance considerations specific to board games

Board games are relatively lightweight compared to other canvas applications. You're not processing thousands of particles or doing real-time physics. The bottleneck is usually redraw frequency, not computational complexity. But there are a few things that will make your game feel sluggish if you ignore them. Redraw the entire canvas every frame even when nothing changed. For a board game where pieces only move occasionally, this is wasteful. I switched to dirty-rectangle rendering, where I only redraw the regions that changed since the last frame. This reduced CPU usage by about 40% on my hex grid game. The implementation is straightforward: track which tiles were modified, clear only those areas, redraw them, and repeat.

Another optimization: don't recreate canvas paths inside the render loop. If you're drawing circles for pieces, define the path once and reuse it. The canvas API caches path data in the context, and recreating it per frame adds up. I measured a 15-millisecond improvement per second of gameplay after moving path creation out of the draw loop.

Common pitfalls and what I did instead

The first canvas board game I built had a fundamental design flaw. I stored piece positions as pixel coordinates instead of board coordinates. When I added zoom, every piece's position became wrong because the pixel values didn't scale with the zoom transform. Converting everything to board coordinates early on would have saved me two days of refactoring. Make sure your data model uses the same coordinate system as your logical board, and only convert to screen coordinates at render time. A second issue: I used fillText for piece labels and it looked terrible at any zoom level other than 1x. The text either got too small to read or too large and pixelated. I switched to rendering text on a separate overlay canvas that doesn't get scaled. This keeps text crisp at all zoom levels. It's a small change but the visual quality difference is noticeable.

For a canvas board game, I'd also recommend against trying to implement everything from scratch if you just want to get a game running. Libraries like Konva.js or Pixi.js handle a lot of the boilerplate. The trade-off is extra bundle size and less control over the rendering pipeline. For a simple board game with under 100 pieces, the vanilla canvas approach is fine. Once you start adding animations, transitions, and particle effects, the library route becomes worth considering. If your board game involves a lot of simultaneous piece movement or complex animations, pure canvas rendering will start to show its limits. In that case, switching to WebGL through a library like Three.js or Babylon.js gives you significantly better performance. The learning curve is steeper, but for games with hundreds of animated pieces, it's the difference between a smooth experience and one that chokes on older hardware.

The canvas approach works well for turn-based board games with modest visual requirements. Keep your state separate from your renderer, handle coordinate transforms correctly, and don't overdraw. Everything else is just incremental refinement.