← Back to Blogs

Creating 2D Browser Games with HTML5 Canvas & JavaScript

Design High-Performance Arcade Games in the Browser

The HTML5 Canvas API unlocks real-time 60fps rendering in modern web browsers. Build your own game loop, handle keyboard and touch inputs, implement bounding box physics, and code a complete playable arcade game from scratch.


Part 1 - Introduction to HTML5 Canvas & The 2D Context

The HTML5 <canvas> element is a high-performance bitmap display surface. It acts as an empty digital canvas upon which you can draw 2D shapes, sprites, animations, and entire interactive games purely through JavaScript.

Unlike DOM-based graphics, canvas renders pixels directly. This delivers the blistering speed needed for 60fps arcade games, particle simulations, and visual data engines.

<!-- HTML -->
<canvas id="gameCanvas" width="800" height="450"></canvas>
// JavaScript Setup
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d'); // Obtain the 2D rendering context

Part 2 - Drawing Primitives: Shapes, Paths & Text

The 2D context provides methods for drawing geometric primitives:

// 1. Clear previous frame
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 2. Draw a filled rectangle (Player)
ctx.fillStyle = '#0ea5e9'; // Theme primary color
ctx.fillRect(50, 50, 40, 40); // x, y, width, height

// 3. Draw an outlined circle (Ball / Projectile)
ctx.beginPath();
ctx.arc(200, 150, 20, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fillStyle = '#ef4444';
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();

// 4. Draw Score Text
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 20px Inter, sans-serif';
ctx.fillText('Score: 100', 20, 35);

Part 3 - The Game Loop Architecture

Every video game runs on an infinite loop that updates physics, processes inputs, and renders frames:

function gameLoop(timestamp) {
    // 1. Process User Input
    handleInput();

    // 2. Update Game State (Move player, check collisions)
    update();

    // 3. Draw the new frame
    draw();

    // 4. Request next frame synchronized with monitor refresh rate
    requestAnimationFrame(gameLoop);
}

// Kick off the loop
requestAnimationFrame(gameLoop);

Part 4 - Capturing Keyboard & Touch Input

Smooth controls require tracking which keys are currently held down:

const keys = {};

window.addEventListener('keydown', (e) => {
    keys[e.key] = true;
});

window.addEventListener('keyup', (e) => {
    keys[e.key] = false;
});

function handleInput() {
    if (keys['ArrowLeft'] || keys['a'])  player.x -= player.speed;
    if (keys['ArrowRight'] || keys['d']) player.x += player.speed;
    if (keys['ArrowUp'] || keys['w'])    player.y -= player.speed;
    if (keys['ArrowDown'] || keys['s'])  player.y += player.speed;
}

Part 5 - Collision Detection: AABB (Axis-Aligned Bounding Box)

To detect when two rectangular sprites touch, compare their boundaries:

function checkCollision(rect1, rect2) {
    return (
        rect1.x < rect2.x + rect2.width &&
        rect1.x + rect1.width > rect2.x &&
        rect1.y < rect2.y + rect2.height &&
        rect1.y + rect1.height > rect2.y
    );
}

Part 6 - Complete Playable Mini-Game: Arcade Meteor Dodger

Here is a complete, self-contained mini-game you can run in your browser right now:

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const player = { x: 380, y: 380, width: 30, height: 30, speed: 6, color: '#0ea5e9' };
let meteors = [];
let score = 0;
let gameOver = false;

function spawnMeteor() {
    if (gameOver) return;
    meteors.push({
        x: Math.random() * (canvas.width - 25),
        y: -30,
        width: 25,
        height: 25,
        speed: 3 + Math.random() * 4,
        color: '#f43f5e'
    });
}
setInterval(spawnMeteor, 600);

function update() {
    if (gameOver) return;

    // Movement
    if (keys['ArrowLeft'] && player.x > 0) player.x -= player.speed;
    if (keys['ArrowRight'] && player.x + player.width < canvas.width) player.x += player.speed;

    // Meteors update
    for (let i = meteors.length - 1; i >= 0; i--) {
        const m = meteors[i];
        m.y += m.speed;

        // Collision Check
        if (checkCollision(player, m)) {
            gameOver = true;
        }

        // Screen boundary
        if (m.y > canvas.height) {
            meteors.splice(i, 1);
            score += 10;
        }
    }
}

function draw() {
    // Clear screen
    ctx.fillStyle = '#0b1120';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Draw Player
    ctx.fillStyle = player.color;
    ctx.fillRect(player.x, player.y, player.width, player.height);

    // Draw Meteors
    meteors.forEach(m => {
        ctx.fillStyle = m.color;
        ctx.fillRect(m.x, m.y, m.width, m.height);
    });

    // Score
    ctx.fillStyle = '#f8fafc';
    ctx.font = '18px Inter, sans-serif';
    ctx.fillText(`Score: ${score}`, 20, 30);

    if (gameOver) {
        ctx.fillStyle = '#ef4444';
        ctx.font = 'bold 36px Outfit, sans-serif';
        ctx.fillText('GAME OVER', 290, 220);
    }
}

function loop() {
    update();
    draw();
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);