Smash the Wall!
Browser Breakout
The canvas classic: mouse-driven paddle, spin physics, a glowing brick wall, lives and levels, plus canvas gradients and particle effects that make the browser version the prettiest one yet.
The canvas classic: mouse-driven paddle, spin physics, a glowing brick wall, lives and levels, plus canvas gradients and particle effects that make the browser version the prettiest one yet.
<canvas><canvas>, the 2D ctxctx, the draw()draw() function and the requestAnimationFramerequestAnimationFrame loop. If you're starting here or need that boilerplate, grab it in one click: Episode 1 full template โ ยท JS Cheatsheet โStandard canvas setup with a paddle glued to the mouse.
let mouseX = 350;
canvas.addEventListener("mousemove", function(e) {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
});
// in the loop:
const padX = Math.max(0, Math.min(590, mouseX - 55));
ctx.fillStyle = "#ff7700";
ctx.beginPath();
ctx.roundRect(padX, 480, 110, 14, 7);
ctx.fill();let mouseX = 350;
canvas.addEventListener("mousemove", function(e) {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
});
// in the loop:
const padX = Math.max(0, Math.min(590, mouseX - 55));
ctx.fillStyle = "#ff7700";
ctx.beginPath();
ctx.roundRect(padX, 480, 110, 14, 7);
ctx.fill();
A ball object bouncing off the walls and ceiling.
const ball = { x: 350, y: 300, vx: 4, vy: -4, r: 7 };
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.x - ball.r <= 0 || ball.x + ball.r >= 700) ball.vx = -ball.vx;
if (ball.y - ball.r <= 0) ball.vy = -ball.vy;
const g = ctx.createRadialGradient(ball.x-2, ball.y-2, 1, ball.x, ball.y, ball.r);
g.addColorStop(0, "#fff");
g.addColorStop(1, "#bbb");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();const ball = { x: 350, y: 300, vx: 4, vy: -4, r: 7 };
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.x - ball.r <= 0 || ball.x + ball.r >= 700) ball.vx = -ball.vx;
if (ball.y - ball.r <= 0) ball.vy = -ball.vy;
const g = ctx.createRadialGradient(ball.x-2, ball.y-2, 1, ball.x, ball.y, ball.r);
g.addColorStop(0, "#fff");
g.addColorStop(1, "#bbb");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
if (ball.x - ball.if (ball.x - ball. <= 0 || ball.x + ball.r >= <= 0 || ball.x + ball.r >= )
ball.vx = )
ball.vx = ;;
Rebound with player-controlled angle off the paddle.
if (ball.vy > 0 &&
ball.y + ball.r >= 480 && ball.y + ball.r <= 494 &&
ball.x >= padX && ball.x <= padX + 110) {
ball.vy = -ball.vy;
let spin = (ball.x - (padX + 55)) / 12;
spin = Math.max(-7, Math.min(7, spin));
ball.vx = spin === 0 ? 1 : spin;
}if (ball.vy > 0 &&
ball.y + ball.r >= 480 && ball.y + ball.r <= 494 &&
ball.x >= padX && ball.x <= padX + 110) {
ball.vy = -ball.vy;
let spin = (ball.x - (padX + 55)) / 12;
spin = Math.max(-7, Math.min(7, spin));
ball.vx = spin === 0 ? 1 : spin;
}
Build the brick wall and give it a neon glow with canvas shadows.
const COLOURS = ["#f87171","#fb923c","#facc15","#4ade80","#60a5fa"];
let bricks = [];
for (let r = 0; r < 5; r++)
for (let c = 0; c < 10; c++)
bricks.push({ x: c*68+12, y: r*26+50, row: r, alive: true });
// drawing:
for (const b of bricks) {
if (!b.alive) continue;
ctx.shadowBlur = 12;
ctx.shadowColor = COLOURS[b.row];
ctx.fillStyle = COLOURS[b.row];
ctx.fillRect(b.x, b.y, 64, 22);
}
ctx.shadowBlur = 0; // stop the glow leaking!const COLOURS = ["#f87171","#fb923c","#facc15","#4ade80","#60a5fa"];
let bricks = [];
for (let r = 0; r < 5; r++)
for (let c = 0; c < 10; c++)
bricks.push({ x: c*68+12, y: r*26+50, row: r, alive: true });
// drawing:
for (const b of bricks) {
if (!b.alive) continue;
ctx.shadowBlur = 12;
ctx.shadowColor = COLOURS[b.row];
ctx.fillStyle = COLOURS[b.row];
ctx.fillRect(b.x, b.y, 64, 22);
}
ctx.shadowBlur = 0; // stop the glow leaking!
ctx.ctx. = 12; ctx.shadowColor = COLOURS[b.row]; ctx.fillRect(b.x, b.y, 64, 22); // after the brick loop: ctx.shadowBlur = = 12; ctx.shadowColor = COLOURS[b.row]; ctx.fillRect(b.x, b.y, 64, 22); // after the brick loop: ctx.shadowBlur = ;;
Spawn a burst of physics particles when a brick dies.
let particles = [];
function burst(x, y, colour) {
for (let i = 0; i < 8; i++)
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6 - 1,
life: 30, colour: colour
});
}
// each frame:
for (const p of particles) {
p.x += p.vx; p.y += p.vy;
p.vy += 0.15; // particle gravity
p.life--;
}
particles = particles.filter(p => p.life > 0);
for (const p of particles) {
ctx.globalAlpha = p.life / 30;
ctx.fillStyle = p.colour;
ctx.fillRect(p.x, p.y, 3, 3);
}
ctx.globalAlpha = 1;let particles = [];
function burst(x, y, colour) {
for (let i = 0; i < 8; i++)
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6 - 1,
life: 30, colour: colour
});
}
// each frame:
for (const p of particles) {
p.x += p.vx; p.y += p.vy;
p.vy += 0.15; // particle gravity
p.life--;
}
particles = particles.filter(p => p.life > 0);
for (const p of particles) {
ctx.globalAlpha = p.life / 30;
ctx.fillStyle = p.colour;
ctx.fillRect(p.x, p.y, 3, 3);
}
ctx.globalAlpha = 1;
Finish with lives, escalating levels and the end screens.
let lives = 3, level = 1;
// wall cleared:
if (bricks.every(b => !b.alive)) {
level++;
bricks = buildWall();
ball.x = 350; ball.y = 300;
ball.vx *= 1.15;
ball.vy = -Math.abs(ball.vy) * 1.15;
}let lives = 3, level = 1;
// wall cleared:
if (bricks.every(b => !b.alive)) {
level++;
bricks = buildWall();
ball.x = 350; ball.y = 300;
ball.vx *= 1.15;
ball.vy = -Math.abs(ball.vy) * 1.15;
}
if (bricks.if (bricks.(b => !(b => !)) {
level++;
bricks = buildWall();
})) {
level++;
bricks = buildWall();
}
Mouse paddle, ball physics, paddle spin, the neon brick wall, particle bursts, lives and escalating levels , one file. Save it as breakout.htmlbreakout.html, open it, and play. Stuck on a step? Compare your file against this.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Neon Breakout</title>
<style>
body { display:flex; flex-direction:column; align-items:center; gap:10px;
background:#0d1117; color:#e8eaf2; font-family:system-ui, sans-serif; padding:16px; }
canvas { background:#0a0e14; border:2px solid #1e2733; border-radius:8px; cursor:none; }
</style>
</head>
<body>
<h2>๐งฑ Neon Breakout</h2>
<canvas id="game" width="700" height="520"></canvas>
<div>Move the mouse to steer ยท click to restart after Game Over</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const COLOURS = ["#f87171","#fb923c","#facc15","#4ade80","#60a5fa"];
let mouseX = 350, ball, bricks, particles, lives, level, score, gameOver;
canvas.addEventListener("mousemove", function(e) {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
});
canvas.addEventListener("click", function() { if (gameOver) start(); });
function buildWall() {
const arr = [];
for (let r = 0; r < 5; r++)
for (let c = 0; c < 10; c++)
arr.push({ x: c*68+12, y: r*26+50, row: r, alive: true });
return arr;
}
function serve() { ball = { x: 350, y: 300, vx: 4, vy: -4, r: 7 }; }
function start() {
bricks = buildWall();
particles = [];
lives = 3; level = 1; score = 0; gameOver = false;
serve();
}
function burst(x, y, colour) {
for (let i = 0; i < 8; i++)
particles.push({ x:x, y:y, vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6-1, life:30, colour:colour });
}
function update() {
if (gameOver) return;
const padX = Math.max(0, Math.min(590, mouseX - 55));
ball.x += ball.vx;
ball.y += ball.vy;
// walls + ceiling
if (ball.x - ball.r <= 0 || ball.x + ball.r >= 700) ball.vx = -ball.vx;
if (ball.y - ball.r <= 0) ball.vy = -ball.vy;
// paddle rebound with spin
if (ball.vy > 0 && ball.y + ball.r >= 480 && ball.y + ball.r <= 494 &&
ball.x >= padX && ball.x <= padX + 110) {
ball.vy = -ball.vy;
let spin = (ball.x - (padX + 55)) / 12;
spin = Math.max(-7, Math.min(7, spin));
ball.vx = spin === 0 ? 1 : spin;
}
// brick collision (one per frame)
for (const b of bricks) {
if (!b.alive) continue;
if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + 64 &&
ball.y + ball.r > b.y && ball.y - ball.r < b.y + 22) {
b.alive = false;
ball.vy = -ball.vy;
score += (5 - b.row) * 10;
burst(ball.x, ball.y, COLOURS[b.row]);
break;
}
}
// floor miss
if (ball.y - ball.r > 520) {
lives--;
if (lives <= 0) { gameOver = true; }
else serve();
}
// wall cleared -> next level
if (bricks.every(b => !b.alive)) {
level++;
bricks = buildWall();
serve();
ball.vx *= 1.15;
ball.vy = -Math.abs(ball.vy) * 1.15;
}
// particles
for (const p of particles) { p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life--; }
particles = particles.filter(p => p.life > 0);
}
function draw() {
ctx.clearRect(0, 0, 700, 520);
const padX = Math.max(0, Math.min(590, mouseX - 55));
// bricks with glow
for (const b of bricks) {
if (!b.alive) continue;
ctx.shadowBlur = 12;
ctx.shadowColor = COLOURS[b.row];
ctx.fillStyle = COLOURS[b.row];
ctx.fillRect(b.x, b.y, 64, 22);
}
ctx.shadowBlur = 0;
// particles
for (const p of particles) {
ctx.globalAlpha = p.life / 30;
ctx.fillStyle = p.colour;
ctx.fillRect(p.x, p.y, 3, 3);
}
ctx.globalAlpha = 1;
// paddle
ctx.fillStyle = "#ff7700";
ctx.beginPath();
ctx.roundRect(padX, 480, 110, 14, 7);
ctx.fill();
// glossy ball
const g = ctx.createRadialGradient(ball.x-2, ball.y-2, 1, ball.x, ball.y, ball.r);
g.addColorStop(0, "#fff"); g.addColorStop(1, "#bbb");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2);
ctx.fill();
// HUD
ctx.fillStyle = "#e8eaf2";
ctx.font = "16px system-ui";
ctx.textAlign = "left"; ctx.fillText("Score " + score, 12, 30);
ctx.textAlign = "center"; ctx.fillText("Level " + level, 350, 30);
ctx.textAlign = "right"; ctx.fillText("โค๏ธ".repeat(Math.max(0, lives)), 688, 30);
if (gameOver) {
ctx.fillStyle = "rgba(0,0,0,.65)";
ctx.fillRect(0, 0, 700, 520);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "bold 40px system-ui";
ctx.fillText("Game Over", 350, 250);
ctx.font = "18px system-ui";
ctx.fillText("Score " + score + " ยท click to play again", 350, 290);
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
start();
requestAnimationFrame(loop);
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Neon Breakout</title>
<style>
body { display:flex; flex-direction:column; align-items:center; gap:10px;
background:#0d1117; color:#e8eaf2; font-family:system-ui, sans-serif; padding:16px; }
canvas { background:#0a0e14; border:2px solid #1e2733; border-radius:8px; cursor:none; }
</style>
</head>
<body>
<h2>๐งฑ Neon Breakout</h2>
<canvas id="game" width="700" height="520"></canvas>
<div>Move the mouse to steer ยท click to restart after Game Over</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const COLOURS = ["#f87171","#fb923c","#facc15","#4ade80","#60a5fa"];
let mouseX = 350, ball, bricks, particles, lives, level, score, gameOver;
canvas.addEventListener("mousemove", function(e) {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
});
canvas.addEventListener("click", function() { if (gameOver) start(); });
function buildWall() {
const arr = [];
for (let r = 0; r < 5; r++)
for (let c = 0; c < 10; c++)
arr.push({ x: c*68+12, y: r*26+50, row: r, alive: true });
return arr;
}
function serve() { ball = { x: 350, y: 300, vx: 4, vy: -4, r: 7 }; }
function start() {
bricks = buildWall();
particles = [];
lives = 3; level = 1; score = 0; gameOver = false;
serve();
}
function burst(x, y, colour) {
for (let i = 0; i < 8; i++)
particles.push({ x:x, y:y, vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6-1, life:30, colour:colour });
}
function update() {
if (gameOver) return;
const padX = Math.max(0, Math.min(590, mouseX - 55));
ball.x += ball.vx;
ball.y += ball.vy;
// walls + ceiling
if (ball.x - ball.r <= 0 || ball.x + ball.r >= 700) ball.vx = -ball.vx;
if (ball.y - ball.r <= 0) ball.vy = -ball.vy;
// paddle rebound with spin
if (ball.vy > 0 && ball.y + ball.r >= 480 && ball.y + ball.r <= 494 &&
ball.x >= padX && ball.x <= padX + 110) {
ball.vy = -ball.vy;
let spin = (ball.x - (padX + 55)) / 12;
spin = Math.max(-7, Math.min(7, spin));
ball.vx = spin === 0 ? 1 : spin;
}
// brick collision (one per frame)
for (const b of bricks) {
if (!b.alive) continue;
if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + 64 &&
ball.y + ball.r > b.y && ball.y - ball.r < b.y + 22) {
b.alive = false;
ball.vy = -ball.vy;
score += (5 - b.row) * 10;
burst(ball.x, ball.y, COLOURS[b.row]);
break;
}
}
// floor miss
if (ball.y - ball.r > 520) {
lives--;
if (lives <= 0) { gameOver = true; }
else serve();
}
// wall cleared -> next level
if (bricks.every(b => !b.alive)) {
level++;
bricks = buildWall();
serve();
ball.vx *= 1.15;
ball.vy = -Math.abs(ball.vy) * 1.15;
}
// particles
for (const p of particles) { p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life--; }
particles = particles.filter(p => p.life > 0);
}
function draw() {
ctx.clearRect(0, 0, 700, 520);
const padX = Math.max(0, Math.min(590, mouseX - 55));
// bricks with glow
for (const b of bricks) {
if (!b.alive) continue;
ctx.shadowBlur = 12;
ctx.shadowColor = COLOURS[b.row];
ctx.fillStyle = COLOURS[b.row];
ctx.fillRect(b.x, b.y, 64, 22);
}
ctx.shadowBlur = 0;
// particles
for (const p of particles) {
ctx.globalAlpha = p.life / 30;
ctx.fillStyle = p.colour;
ctx.fillRect(p.x, p.y, 3, 3);
}
ctx.globalAlpha = 1;
// paddle
ctx.fillStyle = "#ff7700";
ctx.beginPath();
ctx.roundRect(padX, 480, 110, 14, 7);
ctx.fill();
// glossy ball
const g = ctx.createRadialGradient(ball.x-2, ball.y-2, 1, ball.x, ball.y, ball.r);
g.addColorStop(0, "#fff"); g.addColorStop(1, "#bbb");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2);
ctx.fill();
// HUD
ctx.fillStyle = "#e8eaf2";
ctx.font = "16px system-ui";
ctx.textAlign = "left"; ctx.fillText("Score " + score, 12, 30);
ctx.textAlign = "center"; ctx.fillText("Level " + level, 350, 30);
ctx.textAlign = "right"; ctx.fillText("โค๏ธ".repeat(Math.max(0, lives)), 688, 30);
if (gameOver) {
ctx.fillStyle = "rgba(0,0,0,.65)";
ctx.fillRect(0, 0, 700, 520);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "bold 40px system-ui";
ctx.fillText("Game Over", 350, 250);
ctx.font = "18px system-ui";
ctx.fillText("Score " + score + " ยท click to play again", 350, 290);
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
start();
requestAnimationFrame(loop);
</script>
</body>
</html>