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