slice icon Context Slice

Purpose

Reference patterns for composing playable Canvas-based games. Use these as building blocks when generating game code.

HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>[Game Name]</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      background: #0a0a0a;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      font-family: 'Courier New', monospace;
    }

    #game-container {
      position: relative;
    }

    canvas {
      display: block;
      background: #111;
      border: 3px solid #333;
      image-rendering: pixelated;
    }

    #ui {
      position: absolute;
      top: 10px;
      left: 10px;
      color: #0f0;
      font-size: 16px;
      text-shadow: 2px 2px #000;
    }

    #game-over {
      position: absolute;
      inset: 0;
      display: none;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background: rgba(0,0,0,0.8);
      color: #fff;
    }

    #game-over.visible { display: flex; }

    #game-over h1 { font-size: 48px; margin-bottom: 20px; }

    #restart-btn {
      padding: 15px 30px;
      font-size: 20px;
      background: #0f0;
      color: #000;
      border: none;
      cursor: pointer;
      font-family: inherit;
    }
  </style>
</head>
<body>
  <div id="game-container">
    <canvas id="game"></canvas>
    <div id="ui">
      <div>SCORE: <span id="score">0</span></div>
      <div>LIVES: <span id="lives">3</span></div>
    </div>
    <div id="game-over">
      <h1>GAME OVER</h1>
      <p>Final Score: <span id="final-score">0</span></p>
      <button id="restart-btn">PLAY AGAIN</button>
    </div>
  </div>

  <script>
    // Game code here
  </script>
</body>
</html>

Canvas Setup

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

// Standard retro resolution (scale up for display)
const GAME_WIDTH = 400;
const GAME_HEIGHT = 300;
const SCALE = 2;

canvas.width = GAME_WIDTH;
canvas.height = GAME_HEIGHT;
canvas.style.width = `${GAME_WIDTH * SCALE}px`;
canvas.style.height = `${GAME_HEIGHT * SCALE}px`;

Game Loop

let lastTime = 0;
let gameRunning = true;

function gameLoop(timestamp) {
  if (!gameRunning) return;

  const deltaTime = (timestamp - lastTime) / 1000; // Convert to seconds
  lastTime = timestamp;

  update(deltaTime);
  render();

  requestAnimationFrame(gameLoop);
}

function update(dt) {
  // Update game state
  // Move objects: object.x += object.vx * dt;
}

function render() {
  // Clear canvas
  ctx.fillStyle = '#111';
  ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);

  // Draw game objects
}

// Start the game
requestAnimationFrame(gameLoop);

Input Handling

Keyboard

const keys = {};

document.addEventListener('keydown', (e) => {
  keys[e.code] = true;
  e.preventDefault();
});

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

// Usage in update():
if (keys['ArrowLeft']) player.x -= player.speed * dt;
if (keys['ArrowRight']) player.x += player.speed * dt;
if (keys['Space']) shoot();

Touch (Mobile)

let touchX = null;

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  touchX = e.touches[0].clientX;
});

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  const newX = e.touches[0].clientX;
  const diff = newX - touchX;
  player.x += diff * 0.5;
  touchX = newX;
});

canvas.addEventListener('touchend', () => {
  touchX = null;
});

// Tap to action
canvas.addEventListener('click', () => {
  if (gameState === 'playing') {
    jump(); // or shoot(), etc.
  }
});

Collision Detection

Rectangle vs Rectangle (AABB)

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Circle vs Circle

function circleCollides(a, b) {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  const distance = Math.sqrt(dx * dx + dy * dy);
  return distance < a.radius + b.radius;
}

Point in Rectangle

function pointInRect(px, py, rect) {
  return px >= rect.x && px <= rect.x + rect.width &&
         py >= rect.y && py <= rect.y + rect.height;
}

Drawing Sprites (No Assets)

Pixel Rectangle

function drawRect(x, y, width, height, color) {
  ctx.fillStyle = color;
  ctx.fillRect(Math.floor(x), Math.floor(y), width, height);
}

Simple Spaceship

function drawShip(x, y, color = '#0f0') {
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.moveTo(x, y - 15);      // Top point
  ctx.lineTo(x - 10, y + 10); // Bottom left
  ctx.lineTo(x + 10, y + 10); // Bottom right
  ctx.closePath();
  ctx.fill();
}

Pixel Character (8x8)

function drawPixelSprite(x, y, pixels, scale = 4) {
  // pixels is 2D array of color strings or null
  pixels.forEach((row, py) => {
    row.forEach((color, px) => {
      if (color) {
        ctx.fillStyle = color;
        ctx.fillRect(x + px * scale, y + py * scale, scale, scale);
      }
    });
  });
}

// Example: Simple face
const face = [
  [null, '#ff0', '#ff0', null],
  ['#ff0', '#000', '#000', '#ff0'],
  ['#ff0', '#ff0', '#ff0', '#ff0'],
  [null, '#f00', '#f00', null],
];
drawPixelSprite(100, 100, face);

Text

function drawText(text, x, y, color = '#fff', size = 16) {
  ctx.fillStyle = color;
  ctx.font = `${size}px 'Courier New', monospace`;
  ctx.fillText(text, x, y);
}

Game State Management

let gameState = 'playing'; // 'playing', 'paused', 'gameover'
let score = 0;
let lives = 3;

function updateUI() {
  document.getElementById('score').textContent = score;
  document.getElementById('lives').textContent = lives;
}

function gameOver() {
  gameState = 'gameover';
  gameRunning = false;
  document.getElementById('final-score').textContent = score;
  document.getElementById('game-over').classList.add('visible');
}

function restart() {
  score = 0;
  lives = 3;
  gameState = 'playing';
  gameRunning = true;
  document.getElementById('game-over').classList.remove('visible');
  // Reset game objects...
  updateUI();
  requestAnimationFrame(gameLoop);
}

document.getElementById('restart-btn').addEventListener('click', restart);

Common Game Mechanics

Wrap Around Screen

function wrapPosition(obj) {
  if (obj.x < 0) obj.x = GAME_WIDTH;
  if (obj.x > GAME_WIDTH) obj.x = 0;
  if (obj.y < 0) obj.y = GAME_HEIGHT;
  if (obj.y > GAME_HEIGHT) obj.y = 0;
}

Clamp to Screen

function clampPosition(obj) {
  obj.x = Math.max(0, Math.min(GAME_WIDTH - obj.width, obj.x));
  obj.y = Math.max(0, Math.min(GAME_HEIGHT - obj.height, obj.y));
}

Spawn Enemies

let enemies = [];
let spawnTimer = 0;
const SPAWN_INTERVAL = 2; // seconds

function update(dt) {
  spawnTimer += dt;
  if (spawnTimer >= SPAWN_INTERVAL) {
    spawnTimer = 0;
    enemies.push({
      x: Math.random() * GAME_WIDTH,
      y: -20,
      width: 20,
      height: 20,
      vy: 50 + Math.random() * 50
    });
  }

  enemies.forEach(e => e.y += e.vy * dt);
  enemies = enemies.filter(e => e.y < GAME_HEIGHT + 50);
}

Projectiles

let bullets = [];

function shoot() {
  bullets.push({
    x: player.x,
    y: player.y,
    width: 4,
    height: 10,
    vy: -300
  });
}

function updateBullets(dt) {
  bullets.forEach(b => b.y += b.vy * dt);
  bullets = bullets.filter(b => b.y > -10 && b.y < GAME_HEIGHT + 10);

  // Check collisions
  bullets.forEach((bullet, bi) => {
    enemies.forEach((enemy, ei) => {
      if (collides(bullet, enemy)) {
        bullets.splice(bi, 1);
        enemies.splice(ei, 1);
        score += 10;
        updateUI();
      }
    });
  });
}

Simple Gravity

const GRAVITY = 500;
const JUMP_FORCE = -250;

function update(dt) {
  player.vy += GRAVITY * dt;
  player.y += player.vy * dt;

  // Ground collision
  if (player.y > GROUND_Y) {
    player.y = GROUND_Y;
    player.vy = 0;
    player.grounded = true;
  }
}

function jump() {
  if (player.grounded) {
    player.vy = JUMP_FORCE;
    player.grounded = false;
  }
}

Retro Color Palettes

const PALETTE = {
  // Classic arcade
  black: '#0a0a0a',
  darkGray: '#333',
  white: '#fff',
  green: '#0f0',
  red: '#f00',
  yellow: '#ff0',
  cyan: '#0ff',
  magenta: '#f0f',

  // Game Boy
  gb0: '#0f380f',
  gb1: '#306230',
  gb2: '#8bac0f',
  gb3: '#9bbc0f',

  // CGA
  cgaBlack: '#000',
  cgaCyan: '#55ffff',
  cgaMagenta: '#ff55ff',
  cgaWhite: '#ffffff',
};

High Score (localStorage)

function getHighScore() {
  return parseInt(localStorage.getItem('highScore') || '0');
}

function saveHighScore(score) {
  const current = getHighScore();
  if (score > current) {
    localStorage.setItem('highScore', score.toString());
    return true; // New high score!
  }
  return false;
}

Screen Shake Effect

let shakeAmount = 0;

function shake(intensity = 5) {
  shakeAmount = intensity;
}

function render() {
  ctx.save();
  if (shakeAmount > 0) {
    ctx.translate(
      (Math.random() - 0.5) * shakeAmount,
      (Math.random() - 0.5) * shakeAmount
    );
    shakeAmount *= 0.9;
    if (shakeAmount < 0.5) shakeAmount = 0;
  }

  // ... draw everything ...

  ctx.restore();
}

Flash Effect

let flashAlpha = 0;
let flashColor = '#fff';

function flash(color = '#fff') {
  flashColor = color;
  flashAlpha = 1;
}

function render() {
  // ... draw game ...

  if (flashAlpha > 0) {
    ctx.fillStyle = flashColor;
    ctx.globalAlpha = flashAlpha;
    ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
    ctx.globalAlpha = 1;
    flashAlpha -= 0.1;
  }
}