Free HTML5 canvas game

JavaScript Breakout Game — Play Online & Download the Source

Move the paddle, clear every brick, then inspect or download the HTML, CSS, and JavaScript that power the game.

● No sign-up● Keyboard and touch● Runs locally
loading
Score0
Lives3
Level1
Device best0

← → or A D · drag on touch

Game preview is loading.

Choose a variation

Same core, different rhythm.

Changing a variant resets the current run and updates the source package you download.

Readable, runnable source

See how the game works

Open editable version
/* JS Breakouts Neon Breakout — original geometry-only canvas implementation */
const CONFIG = {"game":"breakout","framework":"vanilla","difficulty":"normal","controls":"keyboard-touch","theme":"neon","sound":true,"reducedEffects":false,"variant":"classic","palette":"arcade","speed":1,"lives":3,"size":1,"colors":{"bg":"#08111f","panel":"#111e33","primary":"#2ad1c9","secondary":"#8b5cf6","text":"#f4f7ff","danger":"#fb7185"}};
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 800;
const H = 500;
const keys = new Set();
let status = 'ready';
let muted = !CONFIG.sound;
let score = 0;
let lives = CONFIG.lives;
let level = 1;
let last = 0;
let raf = 0;
let pointerStart = null;
let world = {};
let audioContext = null;
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
let reducedEffects = Boolean(CONFIG.reducedEffects || reducedMotionQuery.matches);
const ui = {
  score: document.getElementById('score'),
  best: document.getElementById('best'),
  lives: document.getElementById('lives'),
  level: document.getElementById('level'),
  status: document.getElementById('status'),
  play: document.getElementById('play'),
  stop: document.getElementById('stop'),
  restart: document.getElementById('restart'),
  mute: document.getElementById('mute')
};
const highScoreKey = 'jsbreakouts:high:' + CONFIG.game;
let highScore = readStoredNumber(highScoreKey);
try { muted = localStorage.getItem('jsbreakouts:muted') === 'true' || !CONFIG.sound; } catch {}

function readStoredNumber(key) {
  try { return Number(localStorage.getItem(key) || 0); } catch { return 0; }
}

function storeValue(key, value) {
  try { localStorage.setItem(key, String(value)); } catch {}
}

function resizeCanvas() {
  const ratio = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.round(W * ratio);
  canvas.height = Math.round(H * ratio);
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}

function tone(kind) {
  if (muted || !CONFIG.sound) return;
  const AudioCtor = window.AudioContext || window.webkitAudioContext;
  if (!AudioCtor) return;
  audioContext ||= new AudioCtor();
  if (audioContext.state === 'suspended') audioContext.resume();
  const frequencies = { paddle: 220, brick: 520, score: 660, life: 130, hop: 360, gameover: 90 };
  const oscillator = audioContext.createOscillator();
  const gain = audioContext.createGain();
  oscillator.type = kind === 'gameover' ? 'sawtooth' : 'square';
  oscillator.frequency.value = frequencies[kind] || 300;
  gain.gain.setValueAtTime(0.035, audioContext.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + (kind === 'gameover' ? 0.22 : 0.075));
  oscillator.connect(gain);
  gain.connect(audioContext.destination);
  oscillator.start();
  oscillator.stop(audioContext.currentTime + (kind === 'gameover' ? 0.23 : 0.08));
}

function syncUi() {
  if (score > highScore) {
    highScore = score;
    storeValue(highScoreKey, highScore);
  }
  if (ui.score) ui.score.textContent = String(score);
  if (ui.best) ui.best.textContent = String(highScore);
  if (ui.lives) ui.lives.textContent = String(lives);
  if (ui.level) ui.level.textContent = String(level);
  if (ui.status) {
    ui.status.dataset.state = status;
    ui.status.textContent = status === 'ready' ? 'Ready to play' : status === 'playing' ? 'Playing' : status === 'paused' ? 'Paused' : status === 'stopped' ? 'Stopped' : 'Game over';
  }
  if (ui.play) ui.play.textContent = status === 'playing' ? 'Pause' : status === 'paused' ? 'Resume' : status === 'gameover' ? 'Play again' : 'Play';
  if (ui.mute) {
    ui.mute.textContent = muted ? 'Unmute' : 'Mute';
    ui.mute.setAttribute('aria-pressed', String(muted));
  }
}

const send = (type, payload = {}) => {
  if (window.parent !== window) window.parent.postMessage({ source: 'js-breakouts-preview', type, payload }, '*');
};

function resetWorld() {
  score = 0;
  lives = CONFIG.lives;
  level = 1;
  if (CONFIG.game === 'breakout') resetBreakout();
  if (CONFIG.game === 'pong') resetPong();
  if (CONFIG.game === 'snake') resetSnake();
  if (CONFIG.game === 'sky-hopper') resetHopper();
  emitScore();
  draw();
}

function emitScore() {
  syncUi();
  send('SCORE_UPDATE', { score, lives, level });
}

function start() {
  if (status === 'gameover') resetWorld();
  status = 'playing';
  canvas.focus();
  syncUi();
  send('STATE_CHANGE', { status });
}

function pause(reason = 'user') {
  if (status !== 'playing') return;
  status = 'paused';
  syncUi();
  send('STATE_CHANGE', { status, reason });
}

function restart() {
  status = 'ready';
  resetWorld();
  syncUi();
  send('STATE_CHANGE', { status });
}

function stop() {
  status = 'stopped';
  resetWorld();
  status = 'stopped';
  syncUi();
  send('STATE_CHANGE', { status });
}

function gameOver() {
  status = 'gameover';
  tone('gameover');
  syncUi();
  send('GAME_OVER', { score });
  send('STATE_CHANGE', { status });
}

function difficultyScale() {
  return CONFIG.difficulty === 'easy' ? 0.8 : CONFIG.difficulty === 'hard' ? 1.22 : 1;
}

function resetBreakout() {
  const wide = CONFIG.variant === 'wide-paddle' ? 1.35 : 1;
  const fast = CONFIG.variant === 'fast-ball' ? 1.25 : 1;
  world = {
    paddle: { x: W / 2 - 66 * wide, y: H - 42, w: 132 * wide * CONFIG.size, h: 14 },
    ball: { x: W / 2, y: H - 65, r: 9 * CONFIG.size, vx: 250 * CONFIG.speed * fast, vy: -250 * CONFIG.speed * fast },
    bricks: []
  };
  const rows = CONFIG.difficulty === 'hard' ? 7 : 6;
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < 10; col++) {
      world.bricks.push({ x: 34 + col * 74, y: 56 + row * 30, w: 64, h: 18, alive: true, row });
    }
  }
}

function updateBreakout(dt) {
  const p = world.paddle;
  const b = world.ball;
  const move = 430 * dt;
  if (keys.has('arrowleft') || keys.has('a')) p.x -= move;
  if (keys.has('arrowright') || keys.has('d')) p.x += move;
  p.x = Math.max(10, Math.min(W - p.w - 10, p.x));
  b.x += b.vx * dt;
  b.y += b.vy * dt;
  if (b.x < b.r || b.x > W - b.r) { b.vx *= -1; b.x = Math.max(b.r, Math.min(W - b.r, b.x)); }
  if (b.y < b.r) { b.vy = Math.abs(b.vy); }
  if (b.vy > 0 && b.y + b.r >= p.y && b.y - b.r <= p.y + p.h && b.x >= p.x && b.x <= p.x + p.w) {
    b.y = p.y - b.r;
    b.vy = -Math.abs(b.vy);
    b.vx += ((b.x - (p.x + p.w / 2)) / p.w) * 120;
    tone('paddle');
  }
  for (const brick of world.bricks) {
    if (!brick.alive) continue;
    if (b.x + b.r > brick.x && b.x - b.r < brick.x + brick.w && b.y + b.r > brick.y && b.y - b.r < brick.y + brick.h) {
      brick.alive = false;
      b.vy *= -1;
      score += 10;
      tone('brick');
      emitScore();
      break;
    }
  }
  if (world.bricks.every((brick) => !brick.alive)) {
    level += 1;
    const oldScore = score;
    resetBreakout();
    score = oldScore;
    emitScore();
  }
  if (b.y > H + b.r) {
    lives -= 1;
    tone('life');
    emitScore();
    if (lives <= 0) gameOver();
    else {
      b.x = W / 2; b.y = H - 65; b.vy = -Math.abs(b.vy);
      status = 'paused'; syncUi(); send('STATE_CHANGE', { status, reason: 'life-lost' });
    }
  }
}

function drawBreakout() {
  const p = world.paddle;
  const b = world.ball;
  for (const brick of world.bricks) {
    if (!brick.alive) continue;
    ctx.fillStyle = brick.row % 2 ? CONFIG.colors.secondary : CONFIG.colors.primary;
    ctx.globalAlpha = 0.82 + (brick.row % 3) * 0.06;
    ctx.fillRect(brick.x, brick.y, brick.w, brick.h);
    ctx.globalAlpha = 1;
  }
  ctx.fillStyle = CONFIG.colors.text;
  ctx.fillRect(p.x, p.y, p.w, p.h);
  ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fillStyle = CONFIG.colors.primary; ctx.fill();
}

function resetPong() {
  world = {
    player: { x: 28, y: H / 2 - 55, w: 14, h: 110 * CONFIG.size },
    ai: { x: W - 42, y: H / 2 - 55, w: 14, h: 110 },
    ball: { x: W / 2, y: H / 2, r: 9, vx: 285 * CONFIG.speed, vy: 180 },
    aiScore: 0
  };
}

function updatePong(dt) {
  const p = world.player, ai = world.ai, b = world.ball;
  const move = 430 * dt;
  if (keys.has('arrowup') || keys.has('w')) p.y -= move;
  if (keys.has('arrowdown') || keys.has('s')) p.y += move;
  p.y = Math.max(0, Math.min(H - p.h, p.y));
  const aiScale = CONFIG.variant === 'easy-ai' ? 0.55 : CONFIG.difficulty === 'hard' ? 0.95 : 0.72;
  ai.y += Math.sign(b.y - (ai.y + ai.h / 2)) * 340 * aiScale * dt;
  ai.y = Math.max(0, Math.min(H - ai.h, ai.y));
  b.x += b.vx * dt; b.y += b.vy * dt;
  if (b.y < b.r || b.y > H - b.r) b.vy *= -1;
  const hit = (pad) => b.x + b.r > pad.x && b.x - b.r < pad.x + pad.w && b.y + b.r > pad.y && b.y - b.r < pad.y + pad.h;
  if (b.vx < 0 && hit(p)) { b.vx = Math.abs(b.vx) * 1.025; b.vy += (b.y - (p.y + p.h / 2)) * 3; tone('paddle'); }
  if (b.vx > 0 && hit(ai)) { b.vx = -Math.abs(b.vx) * 1.025; tone('paddle'); }
  if (b.x > W + 20) { score += 1; tone('score'); emitScore(); b.x = W / 2; b.y = H / 2; b.vx = -285 * CONFIG.speed; }
  if (b.x < -20) { world.aiScore += 1; lives = Math.max(0, 5 - world.aiScore); tone('life'); emitScore(); b.x = W / 2; b.y = H / 2; b.vx = 285 * CONFIG.speed; }
  if (score >= 5 || world.aiScore >= 5) gameOver();
}

function drawPong() {
  ctx.setLineDash([12, 14]); ctx.strokeStyle = CONFIG.colors.secondary; ctx.beginPath(); ctx.moveTo(W/2, 18); ctx.lineTo(W/2, H-18); ctx.stroke(); ctx.setLineDash([]);
  ctx.fillStyle = CONFIG.colors.primary; ctx.fillRect(world.player.x, world.player.y, world.player.w, world.player.h);
  ctx.fillStyle = CONFIG.colors.secondary; ctx.fillRect(world.ai.x, world.ai.y, world.ai.w, world.ai.h);
  ctx.beginPath(); ctx.arc(world.ball.x, world.ball.y, world.ball.r, 0, Math.PI*2); ctx.fillStyle = CONFIG.colors.text; ctx.fill();
}

function resetSnake() {
  world = {
    snake: [{ x: 12, y: 10 }, { x: 11, y: 10 }, { x: 10, y: 10 }],
    dir: { x: 1, y: 0 },
    next: { x: 1, y: 0 },
    food: { x: 17, y: 10 },
    timer: 0
  };
}

function snakeDirection(x, y) {
  if (x + world.dir.x === 0 && y + world.dir.y === 0) return;
  world.next = { x, y };
}

function updateSnake(dt) {
  world.timer += dt;
  const step = (CONFIG.variant === 'speed-up' ? Math.max(0.065, 0.15 - score * 0.002) : 0.14) / CONFIG.speed / difficultyScale();
  if (world.timer < step) return;
  world.timer = 0; world.dir = world.next;
  const head = { x: world.snake[0].x + world.dir.x, y: world.snake[0].y + world.dir.y };
  const wrap = CONFIG.variant === 'wrap';
  if (wrap) { head.x = (head.x + 25) % 25; head.y = (head.y + 15) % 15; }
  const hitWall = head.x < 0 || head.x >= 25 || head.y < 0 || head.y >= 15;
  const hitSelf = world.snake.some((part) => part.x === head.x && part.y === head.y);
  if (hitWall || hitSelf) { lives = 0; emitScore(); gameOver(); return; }
  world.snake.unshift(head);
  if (head.x === world.food.x && head.y === world.food.y) {
    score += 10; tone('score'); emitScore();
    do { world.food = { x: Math.floor(Math.random()*25), y: Math.floor(Math.random()*15) }; }
    while (world.snake.some((part) => part.x === world.food.x && part.y === world.food.y));
  } else world.snake.pop();
}

function drawSnake() {
  const cellW = W / 25, cellH = H / 15;
  ctx.fillStyle = CONFIG.colors.secondary; ctx.beginPath(); ctx.arc((world.food.x+.5)*cellW, (world.food.y+.5)*cellH, 10, 0, Math.PI*2); ctx.fill();
  world.snake.forEach((part, index) => {
    ctx.fillStyle = index === 0 ? CONFIG.colors.text : CONFIG.colors.primary;
    ctx.fillRect(part.x*cellW+2, part.y*cellH+2, cellW-4, cellH-4);
  });
}

function resetHopper() {
  world = { bird: { x: 190, y: H/2, vy: 0, r: 18 * CONFIG.size }, pipes: [], spawn: 0 };
}

function hop() {
  if (status !== 'playing') start();
  world.bird.vy = -370 / difficultyScale();
  tone('hop');
}

function updateHopper(dt) {
  const bird = world.bird;
  const relaxed = CONFIG.variant === 'relaxed';
  bird.vy += (relaxed ? 760 : 920) * dt * difficultyScale();
  bird.y += bird.vy * dt;
  world.spawn -= dt;
  if (world.spawn <= 0) {
    const gap = relaxed ? 190 : CONFIG.difficulty === 'hard' ? 135 : 165;
    const center = 150 + Math.random() * 200;
    world.pipes.push({ x: W + 40, center, gap, passed: false });
    world.spawn = 1.65 / CONFIG.speed;
  }
  for (const pipe of world.pipes) {
    pipe.x -= (CONFIG.variant === 'fast' ? 250 : 205) * CONFIG.speed * dt;
    if (!pipe.passed && pipe.x < bird.x) { pipe.passed = true; score += 1; tone('score'); emitScore(); }
    const nearX = bird.x + bird.r > pipe.x && bird.x - bird.r < pipe.x + 70;
    if (nearX && (bird.y - bird.r < pipe.center - pipe.gap/2 || bird.y + bird.r > pipe.center + pipe.gap/2)) gameOver();
  }
  world.pipes = world.pipes.filter((pipe) => pipe.x > -80);
  if (bird.y < bird.r || bird.y > H - bird.r) gameOver();
}

function drawHopper() {
  ctx.fillStyle = CONFIG.colors.secondary;
  for (const pipe of world.pipes) {
    ctx.fillRect(pipe.x, 0, 70, pipe.center - pipe.gap/2);
    ctx.fillRect(pipe.x, pipe.center + pipe.gap/2, 70, H);
  }
  const bird = world.bird;
  ctx.fillStyle = CONFIG.colors.primary; ctx.beginPath(); ctx.arc(bird.x, bird.y, bird.r, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = CONFIG.colors.text; ctx.beginPath(); ctx.arc(bird.x+7, bird.y-5, 4, 0, Math.PI*2); ctx.fill();
}

function update(dt) {
  if (CONFIG.game === 'breakout') updateBreakout(dt);
  if (CONFIG.game === 'pong') updatePong(dt);
  if (CONFIG.game === 'snake') updateSnake(dt);
  if (CONFIG.game === 'sky-hopper') updateHopper(dt);
}

function drawOverlay() {
  if (status === 'playing') return;
  ctx.fillStyle = 'rgba(8, 17, 31, .72)'; ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center'; ctx.fillStyle = CONFIG.colors.text; ctx.font = '700 30px system-ui';
  const title = status === 'ready' ? 'Ready to play' : status === 'paused' ? 'Paused' : status === 'stopped' ? 'Stopped' : 'Game over';
  ctx.fillText(title, W/2, H/2 - 8);
  ctx.fillStyle = CONFIG.colors.primary; ctx.font = '16px system-ui';
  ctx.fillText(status === 'gameover' ? 'Press Space to play again' : 'Press Space or use Play', W/2, H/2 + 26);
}

function draw() {
  ctx.fillStyle = CONFIG.colors.bg; ctx.fillRect(0, 0, W, H);
  if (!reducedEffects) {
    ctx.strokeStyle = CONFIG.colors.panel; ctx.lineWidth = 1;
    for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
    for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
  }
  if (CONFIG.game === 'breakout') drawBreakout();
  if (CONFIG.game === 'pong') drawPong();
  if (CONFIG.game === 'snake') drawSnake();
  if (CONFIG.game === 'sky-hopper') drawHopper();
  drawOverlay();
}

function loop(now) {
  const dt = Math.min(0.034, (now - last) / 1000 || 0);
  last = now;
  if (status === 'playing') update(dt);
  draw();
  raf = requestAnimationFrame(loop);
}

canvas.addEventListener('keydown', (event) => {
  if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' ','a','d','w','s'].includes(event.key)) event.preventDefault();
  keys.add(event.key.toLowerCase());
  if (event.key === ' ') CONFIG.game === 'sky-hopper' ? hop() : status === 'playing' ? pause() : start();
  if (CONFIG.game === 'snake') {
    if (event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a') snakeDirection(-1,0);
    if (event.key === 'ArrowRight' || event.key.toLowerCase() === 'd') snakeDirection(1,0);
    if (event.key === 'ArrowUp' || event.key.toLowerCase() === 'w') snakeDirection(0,-1);
    if (event.key === 'ArrowDown' || event.key.toLowerCase() === 's') snakeDirection(0,1);
  }
  if (event.key === 'Escape') canvas.blur();
  if (event.key.toLowerCase() === 'r') restart();
});
canvas.addEventListener('keyup', (event) => keys.delete(event.key.toLowerCase()));
canvas.addEventListener('pointerdown', (event) => {
  pointerStart = { x: event.offsetX, y: event.offsetY };
  canvas.setPointerCapture(event.pointerId);
  if (CONFIG.game === 'sky-hopper') hop();
});
canvas.addEventListener('pointermove', (event) => {
  if (!pointerStart) return;
  const x = event.offsetX * W / canvas.clientWidth;
  const y = event.offsetY * H / canvas.clientHeight;
  if (CONFIG.game === 'breakout') world.paddle.x = x - world.paddle.w/2;
  if (CONFIG.game === 'pong') world.player.y = y - world.player.h/2;
});
canvas.addEventListener('pointerup', (event) => {
  if (CONFIG.game === 'snake' && pointerStart) {
    const dx = event.offsetX - pointerStart.x, dy = event.offsetY - pointerStart.y;
    if (Math.abs(dx) > Math.abs(dy)) snakeDirection(dx > 0 ? 1 : -1, 0);
    else snakeDirection(0, dy > 0 ? 1 : -1);
  }
  pointerStart = null;
});

window.addEventListener('message', (event) => {
  const data = event.data;
  if (!data || data.source !== 'js-breakouts-parent') return;
  if (data.type === 'PLAY') start();
  if (data.type === 'PAUSE') pause();
  if (data.type === 'STOP') stop();
  if (data.type === 'RESTART') restart();
  if (data.type === 'MUTE') { muted = Boolean(data.payload && data.payload.muted); syncUi(); }
});
document.addEventListener('visibilitychange', () => { if (document.hidden) pause('hidden'); });
reducedMotionQuery.addEventListener?.('change', (event) => { reducedEffects = Boolean(CONFIG.reducedEffects || event.matches); });
ui.play?.addEventListener('click', () => status === 'playing' ? pause() : start());
ui.stop?.addEventListener('click', stop);
ui.restart?.addEventListener('click', restart);
ui.mute?.addEventListener('click', () => {
  muted = !muted;
  storeValue('jsbreakouts:muted', muted);
  syncUi();
});
window.addEventListener('error', (event) => send('RUNTIME_ERROR', { message: event.message, line: event.lineno || 0 }));
window.addEventListener('unhandledrejection', (event) => send('RUNTIME_ERROR', { message: String(event.reason || 'Unhandled promise rejection') }));
setInterval(() => send('HEARTBEAT'), 2000);
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
resetWorld();
raf = requestAnimationFrame(loop);
syncUi();
send('READY', { game: CONFIG.game });

Code walkthrough

Eight parts make the full loop.

01

Canvas setup

Create a stable 800 × 500 coordinate system that scales visually.

02

Game loop

Use requestAnimationFrame and time-based movement.

03

Paddle input

Read focused keyboard input and direct touch position.

04

Ball movement

Update velocity while clamping each frame step.

05

Collision detection

Compare the ball against walls, paddle, and live bricks.

06

Brick grid

Generate compact rows from predictable geometry.

07

Score and lives

Update visible state only when meaningful events occur.

08

Pause and cleanup

Pause on visibility loss and cancel work when destroyed.

Make it yours

Change the pace, palette, or source.

Open this exact template in Browser Game Maker. Your edits stay on this device.

Customize Breakout →

Breakout FAQ

Play, source, and local data

How do I play Breakout?

Focus the game and move the paddle with Left/Right or A/D. On touch screens, drag across the canvas. Clear every brick without losing all lives.

Can I play on a phone?

Yes. Drag to move the paddle. The canvas scales to the screen and pauses when the page loses focus.

Can I download the JavaScript source?

Yes. The ZIP includes a runnable HTML, CSS, and JavaScript project with a README and license.

Does the project use Canvas?

Yes. The game uses the HTML Canvas 2D API for the loop, rendering, collisions, and input.

Can I edit the game online?

Yes. Open the editable version in Browser Game Maker and run changes in its sandbox.

Is the code free to reuse?

Original JS Breakouts template code is provided under the MIT License. Keep the included notice with reused code.

Why does the game pause when I switch tabs?

Auto-pause prevents hidden gameplay, unnecessary CPU use, and unexpected sound. Return and choose Resume.

Is my high score public?

No. It is stored only in this browser and is never uploaded or placed on a global leaderboard.