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;
  }
}

Particle System

const particles = [];

function createParticle(x, y, color = '#ff0', count = 10) {
  for (let i = 0; i < count; i++) {
    const angle = (Math.PI * 2 / count) * i + Math.random() * 0.5;
    const speed = 50 + Math.random() * 100;
    particles.push({
      x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      life: 1,
      decay: 0.02 + Math.random() * 0.02,
      size: 2 + Math.random() * 4,
      color
    });
  }
}

function createExplosion(x, y) {
  createParticle(x, y, '#f80', 15);
  createParticle(x, y, '#ff0', 10);
  createParticle(x, y, '#f00', 5);
}

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vx *= 0.98;
    p.vy *= 0.98;
    p.life -= p.decay;
    if (p.life <= 0) particles.splice(i, 1);
  }
}

function renderParticles() {
  particles.forEach(p => {
    ctx.globalAlpha = p.life;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
    ctx.fill();
  });
  ctx.globalAlpha = 1;
}

State Machine

const GameState = {
  MENU: 'menu',
  PLAYING: 'playing',
  PAUSED: 'paused',
  GAMEOVER: 'gameover'
};

let gameState = GameState.MENU;
let stateTimer = 0;
let fadeAlpha = 0;
let fadeDirection = 0;

function setState(newState) {
  gameState = newState;
  stateTimer = 0;
}

function fadeToState(newState) {
  fadeDirection = 1;
  fadeAlpha = 0;
  setTimeout(() => {
    setState(newState);
    fadeDirection = -1;
  }, 500);
}

function update(dt) {
  stateTimer += dt;
  
  if (fadeDirection !== 0) {
    fadeAlpha += fadeDirection * dt * 2;
    fadeAlpha = Math.max(0, Math.min(1, fadeAlpha));
    if (fadeAlpha <= 0 && fadeDirection === -1) fadeDirection = 0;
  }
  
  switch (gameState) {
    case GameState.MENU: updateMenu(dt); break;
    case GameState.PLAYING: updatePlaying(dt); break;
    case GameState.PAUSED: updatePaused(dt); break;
    case GameState.GAMEOVER: updateGameOver(dt); break;
  }
}

function render() {
  switch (gameState) {
    case GameState.MENU: renderMenu(); break;
    case GameState.PLAYING: renderPlaying(); break;
    case GameState.PAUSED: renderPlaying(); renderPauseOverlay(); break;
    case GameState.GAMEOVER: renderPlaying(); renderGameOver(); break;
  }
  
  // Fade overlay
  if (fadeAlpha > 0) {
    ctx.fillStyle = '#000';
    ctx.globalAlpha = fadeAlpha;
    ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
    ctx.globalAlpha = 1;
  }
}

Animated Background (Starfield)

const stars = [];
const STAR_COUNT = 100;

function initStars() {
  for (let i = 0; i < STAR_COUNT; i++) {
    stars.push({
      x: Math.random() * GAME_WIDTH,
      y: Math.random() * GAME_HEIGHT,
      speed: 20 + Math.random() * 80,
      size: Math.random() < 0.3 ? 2 : 1,
      brightness: 0.3 + Math.random() * 0.7
    });
  }
}

function updateStars(dt) {
  stars.forEach(star => {
    star.y += star.speed * dt;
    if (star.y > GAME_HEIGHT) {
      star.y = 0;
      star.x = Math.random() * GAME_WIDTH;
    }
  });
}

function renderStars() {
  stars.forEach(star => {
    ctx.fillStyle = `rgba(255, 255, 255, ${star.brightness})`;
    ctx.fillRect(Math.floor(star.x), Math.floor(star.y), star.size, star.size);
  });
}

Score Popup System

const popups = [];

function createScorePopup(x, y, text, color = '#ff0') {
  popups.push({
    x, y,
    text,
    color,
    vy: -60,
    life: 1
  });
}

function updatePopups(dt) {
  for (let i = popups.length - 1; i >= 0; i--) {
    const p = popups[i];
    p.y += p.vy * dt;
    p.vy *= 0.95;
    p.life -= dt;
    if (p.life <= 0) popups.splice(i, 1);
  }
}

function renderPopups() {
  popups.forEach(p => {
    ctx.globalAlpha = p.life;
    ctx.fillStyle = p.color;
    ctx.font = '16px "Courier New", monospace';
    ctx.textAlign = 'center';
    ctx.fillText(p.text, p.x, p.y);
  });
  ctx.globalAlpha = 1;
  ctx.textAlign = 'left';
}

Powerup System

const PowerupType = {
  SPEED: 'speed',
  SHIELD: 'shield',
  RAPID: 'rapid',
  MULTI: 'multi'
};

const powerups = [];
const activePowerups = {};

function spawnPowerup(x, y) {
  const types = Object.values(PowerupType);
  powerups.push({
    x, y,
    type: types[Math.floor(Math.random() * types.length)],
    width: 20,
    height: 20,
    bobOffset: Math.random() * Math.PI * 2
  });
}

function updatePowerups(dt) {
  powerups.forEach(p => p.bobOffset += dt * 3);
  
  // Check collection
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    if (collides(player, p)) {
      activatePowerup(p.type);
      powerups.splice(i, 1);
      createScorePopup(p.x, p.y, p.type.toUpperCase(), '#0ff');
    }
  }
  
  // Decay active powerups
  Object.keys(activePowerups).forEach(type => {
    activePowerups[type] -= dt;
    if (activePowerups[type] <= 0) delete activePowerups[type];
  });
}

function activatePowerup(type) {
  activePowerups[type] = 10; // 10 second duration
}

function hasPowerup(type) {
  return activePowerups[type] && activePowerups[type] > 0;
}

function renderPowerups() {
  powerups.forEach(p => {
    const bob = Math.sin(p.bobOffset) * 3;
    ctx.fillStyle = getPowerupColor(p.type);
    ctx.fillRect(p.x, p.y + bob, p.width, p.height);
    // Glow effect
    ctx.shadowBlur = 10;
    ctx.shadowColor = getPowerupColor(p.type);
    ctx.fillRect(p.x, p.y + bob, p.width, p.height);
    ctx.shadowBlur = 0;
  });
}

function getPowerupColor(type) {
  switch (type) {
    case PowerupType.SPEED: return '#0ff';
    case PowerupType.SHIELD: return '#0f0';
    case PowerupType.RAPID: return '#f0f';
    case PowerupType.MULTI: return '#ff0';
    default: return '#fff';
  }
}

Invincibility Frames

let invincible = false;
let invincibleTimer = 0;
const INVINCIBLE_DURATION = 2;

function takeDamage() {
  if (invincible) return;
  
  lives--;
  invincible = true;
  invincibleTimer = INVINCIBLE_DURATION;
  shake(8);
  flash('#f00');
  
  if (lives <= 0) {
    setState(GameState.GAMEOVER);
  }
}

function updateInvincibility(dt) {
  if (invincible) {
    invincibleTimer -= dt;
    if (invincibleTimer <= 0) {
      invincible = false;
    }
  }
}

function renderPlayer() {
  // Blink when invincible
  if (invincible && Math.floor(invincibleTimer * 10) % 2 === 0) {
    return; // Skip rendering this frame
  }
  
  // Draw player...
  ctx.fillStyle = '#0f0';
  ctx.fillRect(player.x, player.y, player.width, player.height);
}

Touch Virtual Buttons

const touchButtons = {
  left: { x: 20, y: GAME_HEIGHT - 80, width: 60, height: 60, pressed: false },
  right: { x: 100, y: GAME_HEIGHT - 80, width: 60, height: 60, pressed: false },
  action: { x: GAME_WIDTH - 80, y: GAME_HEIGHT - 80, width: 60, height: 60, pressed: false }
};

function initTouchControls() {
  canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
  canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
  canvas.addEventListener('touchend', handleTouchEnd, { passive: false });
}

function handleTouchStart(e) {
  e.preventDefault();
  updateTouches(e.touches);
}

function handleTouchMove(e) {
  e.preventDefault();
  updateTouches(e.touches);
}

function handleTouchEnd(e) {
  e.preventDefault();
  Object.values(touchButtons).forEach(b => b.pressed = false);
  updateTouches(e.touches);
}

function updateTouches(touches) {
  Object.values(touchButtons).forEach(b => b.pressed = false);
  
  const rect = canvas.getBoundingClientRect();
  const scaleX = GAME_WIDTH / rect.width;
  const scaleY = GAME_HEIGHT / rect.height;
  
  for (let touch of touches) {
    const x = (touch.clientX - rect.left) * scaleX;
    const y = (touch.clientY - rect.top) * scaleY;
    
    Object.values(touchButtons).forEach(btn => {
      if (pointInRect(x, y, btn)) {
        btn.pressed = true;
      }
    });
  }
}

function renderTouchButtons() {
  if (!('ontouchstart' in window)) return;
  
  Object.entries(touchButtons).forEach(([name, btn]) => {
    ctx.fillStyle = btn.pressed ? 'rgba(255,255,255,0.5)' : 'rgba(255,255,255,0.2)';
    ctx.fillRect(btn.x, btn.y, btn.width, btn.height);
    ctx.strokeStyle = '#fff';
    ctx.strokeRect(btn.x, btn.y, btn.width, btn.height);
    
    ctx.fillStyle = '#fff';
    ctx.font = '20px sans-serif';
    ctx.textAlign = 'center';
    const icon = name === 'left' ? '◀' : name === 'right' ? '▶' : '●';
    ctx.fillText(icon, btn.x + btn.width/2, btn.y + btn.height/2 + 7);
  });
  ctx.textAlign = 'left';
}

Difficulty Scaling

let difficulty = 1;
let difficultyTimer = 0;
const DIFFICULTY_INTERVAL = 30;
const MAX_DIFFICULTY = 5;

function updateDifficulty(dt) {
  difficultyTimer += dt;
  if (difficultyTimer >= DIFFICULTY_INTERVAL && difficulty < MAX_DIFFICULTY) {
    difficultyTimer = 0;
    difficulty++;
    createScorePopup(GAME_WIDTH/2, GAME_HEIGHT/2, `LEVEL ${difficulty}!`, '#0ff');
  }
}

function getSpawnRate() {
  return Math.max(0.5, 2 - difficulty * 0.3);
}

function getEnemySpeed() {
  return 50 + difficulty * 20;
}

function getEnemyHealth() {
  return 1 + Math.floor(difficulty / 2);
}