Brick Breaker - Day 1
Advanced LevelA classic arcade game built entirely with native browser APIs - no libraries needed.
// Core APIs used
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d'); // 2D rendering context
requestAnimationFrame(gameLoop); // ~60 FPS game loop
Canvas 2D is perfect for simple 2D games: no setup overhead, immediate mode rendering, and works on every browser including mobile Safari.
Every game needs a loop that runs continuously: update state, then render. requestAnimationFrame syncs with the display refresh rate.
function gameLoop() {
// 1. Clear the entire canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. Update ball position
ball.x += ball.dx;
ball.y += ball.dy;
// 3. Check all collisions
checkWallCollision();
checkPaddleCollision();
checkBrickCollisions();
// 4. Draw everything
drawBricks();
drawPaddle();
drawBall();
drawScore();
// 5. Schedule next frame (~16.67ms later for 60fps)
requestAnimationFrame(gameLoop);
}
This simple loop assumes 60fps. For variable frame rates, multiply velocities by deltaTime (time since last frame).
AABB (Axis-Aligned Bounding Box) is the simplest collision method: check if two rectangles overlap.
Two rectangles overlap if and only if they overlap on BOTH the X and Y axes simultaneously.
function checkCollision(rect1, rect2) {
// Check if rect1 and rect2 overlap
return rect1.x < rect2.x + rect2.width && // rect1 left < rect2 right
rect1.x + rect1.width > rect2.x && // rect1 right > rect2 left
rect1.y < rect2.y + rect2.height && // rect1 top < rect2 bottom
rect1.y + rect1.height > rect2.y; // rect1 bottom > rect2 top
}
// For circle-vs-rect (ball vs brick), we approximate the ball as a rect
const ballRect = {
x: ball.x - ball.radius,
y: ball.y - ball.radius,
width: ball.radius * 2,
height: ball.radius * 2
};
function checkBrickCollisions() {
for (const brick of bricks) {
if (brick.destroyed) continue;
if (checkCollision(ballRect, brick)) {
brick.destroyed = true;
score += 10;
// Determine bounce direction based on approach angle
const fromLeft = ball.dx > 0;
const fromTop = ball.dy > 0;
// Calculate overlap to determine which side was hit
const overlapX = fromLeft
? (ball.x + ball.radius) - brick.x
: (brick.x + brick.width) - (ball.x - ball.radius);
const overlapY = fromTop
? (ball.y + ball.radius) - brick.y
: (brick.y + brick.height) - (ball.y - ball.radius);
// Bounce based on which axis had less penetration
if (overlapX < overlapY) {
ball.dx *= -1; // Hit side - reverse X
} else {
ball.dy *= -1; // Hit top/bottom - reverse Y
}
break; // Only hit one brick per frame
}
}
}
The paddle isn't just a wall - where the ball hits affects its bounce angle. This gives players control over the ball's trajectory.
function checkPaddleCollision() {
if (ball.y + ball.radius >= paddle.y &&
ball.x > paddle.x &&
ball.x < paddle.x + paddle.width &&
ball.dy > 0) { // Only if moving downward
// Calculate hit position: -1 (left edge) to 1 (right edge)
const hitPos = (ball.x - paddle.x) / paddle.width;
const normalized = (hitPos - 0.5) * 2; // -1 to 1
// Convert to angle: -60deg to +60deg
const maxAngle = Math.PI / 3; // 60 degrees
const angle = normalized * maxAngle;
// Calculate new velocity (preserve speed)
const speed = Math.sqrt(ball.dx**2 + ball.dy**2);
ball.dx = speed * Math.sin(angle);
ball.dy = -speed * Math.cos(angle); // Always upward
}
}
Limiting the max angle (60deg here) prevents the ball from bouncing too horizontally, which would make gameplay frustrating.
Bricks are generated procedurally in a grid pattern with padding and margins.
function createBricks() {
const bricks = [];
const config = {
rows: 5,
cols: 8,
width: 60,
height: 20,
padding: 10,
offsetTop: 60,
offsetLeft: 35
};
// Row colors from top to bottom
const colors = ['#ff6b6b', '#ffa502', '#ffd93d', '#6bcb77', '#4d96ff'];
for (let row = 0; row < config.rows; row++) {
for (let col = 0; col < config.cols; col++) {
bricks.push({
x: config.offsetLeft + col * (config.width + config.padding),
y: config.offsetTop + row * (config.height + config.padding),
width: config.width,
height: config.height,
color: colors[row],
destroyed: false,
points: (config.rows - row) * 10 // Top rows worth more
});
}
}
return bricks;
}
Supporting both mouse and touch is essential for mobile play.
// Mouse control
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
// Center paddle on mouse, clamped to canvas
paddle.x = Math.max(0,
Math.min(canvas.width - paddle.width, mouseX - paddle.width/2)
);
});
// Touch control (mobile)
canvas.addEventListener('touchmove', (e) => {
e.preventDefault(); // Prevent scrolling
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const touchX = (touch.clientX - rect.left) * scaleX;
paddle.x = Math.max(0,
Math.min(canvas.width - paddle.width, touchX - paddle.width/2)
);
}, { passive: false });
When CSS scales the canvas, coordinates don't match. Use getBoundingClientRect() and scale factors to convert screen coords to canvas coords.
Track the game's current state to control what happens in the game loop.
const GameState = {
START: 'start', // Waiting for player to begin
PLAYING: 'playing', // Active gameplay
PAUSED: 'paused', // Game paused
WIN: 'win', // All bricks destroyed
LOSE: 'lose' // Out of lives
};
let currentState = GameState.START;
function gameLoop() {
switch (currentState) {
case GameState.START:
drawStartScreen();
break;
case GameState.PLAYING:
update();
draw();
// Check win/lose conditions
if (bricks.every(b => b.destroyed)) {
currentState = GameState.WIN;
}
if (lives <= 0) {
currentState = GameState.LOSE;
}
break;
case GameState.WIN:
case GameState.LOSE:
drawGameOver();
break;
}
requestAnimationFrame(gameLoop);
}