← → or A D · drag on touch
Neon Breakout
Bounce the ball, clear the wall, and protect your last life.
Free browser games with source code
Start a game instantly, inspect the HTML, CSS, and JavaScript, or build a variation from a local template. No sign-up and no code upload.
Choose your game
Pick one to load it in the workspace below. Nothing starts—or makes sound—until you press Play.
← → or A D · drag on touch
Bounce the ball, clear the wall, and protect your last life.
↑ ↓ or W S · drag on touch
Hold your side of the court against a compact local opponent.
Arrow keys or WASD · swipe on touch
Collect every spark without crossing your own trail.
Space, click, or tap
Time each lift and pass through a moving skyline.
Play first · inspect next
Game preview is loading.
Every visible game is assembled from the files below. Copy a file, open an editable version, or download the complete project.
/* 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 });
Template-powered builder
Choose clear settings, rebuild the project, then play, inspect, or download the result.
Sandboxed preview
Game preview is loading.
Editable project
A short, visible path
Start with Breakout, Pong, Snake, or Sky Hopper.
Adjust clear settings and rebuild the local project.
Read the HTML, CSS, and JavaScript behind the game.
Get a runnable ZIP with a README and license.
Built to be understood
Every template favors readable source, browser-native APIs, and a complete handoff over a flashy black box.
Why JS Breakouts is play-firstQuestions, answered plainly
Yes. Every game starts in this page and there is no account, email gate, or usage limit.
Yes. Each download includes the working HTML, CSS, JavaScript, README, license, and third-party notices.
Choose the template, framework, difficulty, controls, theme, and advanced settings. The builder regenerates the preview and source files locally.
You can download Vanilla, native Phaser, or native PixiJS projects. The live preview stays network-free while framework downloads include npm dependencies and matching source structure.
No. Settings, edits, drafts, scores, and preview code stay on your device.
The original template code is provided under the MIT License. Keep the included license and check any third-party notices.
Browsers can restrict modules and local resources opened through file://. A small static server gives you the same behavior as a normal website.
Yes. All four templates include touch controls and resize inside the available screen.