Canvas setup
Create a stable 800 × 500 coordinate system that scales visually.
Free HTML5 canvas game
Move the paddle, clear every brick, then inspect or download the HTML, CSS, and JavaScript that power the game.
Game preview is loading.
Choose a variation
Changing a variant resets the current run and updates the source package you download.
Readable, runnable source
/* 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
Create a stable 800 × 500 coordinate system that scales visually.
Use requestAnimationFrame and time-based movement.
Read focused keyboard input and direct touch position.
Update velocity while clamping each frame step.
Compare the ball against walls, paddle, and live bricks.
Generate compact rows from predictable geometry.
Update visible state only when meaningful events occur.
Pause on visibility loss and cancel work when destroyed.
Make it yours
Open this exact template in Browser Game Maker. Your edits stay on this device.
Breakout FAQ
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.
Yes. Drag to move the paddle. The canvas scales to the screen and pauses when the page loses focus.
Yes. The ZIP includes a runnable HTML, CSS, and JavaScript project with a README and license.
Yes. The game uses the HTML Canvas 2D API for the loop, rendering, collisions, and input.
Yes. Open the editable version in Browser Game Maker and run changes in its sandbox.
Original JS Breakouts template code is provided under the MIT License. Keep the included notice with reused code.
Auto-pause prevents hidden gameplay, unnecessary CPU use, and unexpected sound. Return and choose Resume.
No. It is stored only in this browser and is never uploaded or placed on a global leaderboard.