← Blog
Canvas APIGameCollision

Building a Breakout Game in the Browser — Mastering Reflection Angles and Level Design with Canvas API

14 min read

Go deeper on this topic

Browser Game Development

Game loop design, collision detection, pathfinding, and tooling for browser games

Article 2 of 7 in this series.

View topic page

Use the topic hub as a map, then continue to the next article in the series. New here? Start at the home hub →

Why Breakout?

Breakout is a teaching tool that packs the fundamental elements of game programming into a compact package. Rectangle-circle collision detection, reflection vector calculation, game state management, level design — all of these fit into about 200 lines of code. Through the BreakoutMini component implementation, we'll cover the key techniques for building games with Canvas API.

Game Loop Skeleton

The Breakout game loop runs in three phases: "input, update, render." Physics simulation and drawing are packed into a callback called every frame via requestAnimationFrame:

const onFrame = (ctx: CanvasRenderingContext2D, w: number, h: number, time: number) => {
  // 1. Calculate delta time
  const delta = time - lastTime
  lastTime = time
  const dt = Math.min(delta, 32) / 16.67

  // 2. Update ball position
  ball.x += ball.vx * speedMult * dt
  ball.y += ball.vy * speedMult * dt

  // 3. Collision detection (walls, paddle, bricks)
  // ...

  // 4. Render
  renderBricks(ctx, bricks)
  renderPaddle(ctx, paddleX, paddleY)
  renderBall(ctx, ball)
}

The Math.min(delta, 32) clamp is important. When a tab returns from the background, delta can spike to hundreds of milliseconds, causing the ball to pass through walls. Capping at 32ms limits the skip to about 2 frames.

Paddle Reflection Angle Design

What defines Breakout's feel is the reflection angle when the ball hits the paddle. Simply doing vy = -vy makes the ball bounce in the same trajectory repeatedly, which is boring.

In BreakoutMini, the reflection angle changes based on where on the paddle the ball hits:

// Normalize hit position on paddle to -1 to 1
const hitPos = (ball.x - paddleX) / (PADDLE_WIDTH / 2)

// Determine reflection angle based on hit position (-90° ± 60°)
const angle = -Math.PI / 2 + hitPos * (Math.PI / 3)
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy)
ball.vx = Math.cos(angle) * speed
ball.vy = Math.sin(angle) * speed

Why does this approach work? When hitPos is 0 (paddle center), the ball returns straight up. At ±1 (edges), it flies at 60° left or right. The player can aim by controlling where the ball hits the paddle, creating a "sense of control."

This angle range (±60°) is an intentional constraint. Extending to ±90° would allow near-horizontal trajectories where the ball bounces back and forth endlessly, stalling the game. Narrowing to ±30° would eliminate player agency.

Brick Collision Detection

Collision detection between bricks (rectangles) and the ball (circle) is implemented using a closest point algorithm:

for (const brick of bricks) {
  if (!brick.alive) continue

  // Find closest point on rectangle to ball
  const closestX = Math.max(brick.x, Math.min(ball.x, brick.x + brick.w))
  const closestY = Math.max(brick.y, Math.min(ball.y, brick.y + brick.h))

  // Check distance between closest point and ball center
  const dx = ball.x - closestX
  const dy = ball.y - closestY
  if (dx * dx + dy * dy <= BALL_RADIUS * BALL_RADIUS) {
    brick.alive = false
    // Reflection handling...
  }
}

The Math.max and Math.min combination calculates the closest point from the ball center to the rectangle. If this distance is less than or equal to the ball radius, it's a collision. Comparing squared values instead of using Math.sqrt is a classic technique to avoid the cost of square root calculation.

Reflection Direction — Minimum Overlap Method

After detecting a collision, we need to decide whether to bounce the ball in the X or Y direction. BreakoutMini uses the minimum overlap method:

const overlapLeft = ball.x + BALL_RADIUS - brick.x
const overlapRight = brick.x + brick.w - (ball.x - BALL_RADIUS)
const overlapTop = ball.y + BALL_RADIUS - brick.y
const overlapBottom = brick.y + brick.h - (ball.y - BALL_RADIUS)

const minOverlapX = Math.min(overlapLeft, overlapRight)
const minOverlapY = Math.min(overlapTop, overlapBottom)

if (minOverlapX < minOverlapY) {
  ball.vx = -ball.vx  // Hit from the side
} else {
  ball.vy = -ball.vy  // Hit from top or bottom
}

Calculate the penetration depth on all four sides and bounce in the direction with the shallowest penetration. This approach is equivalent to estimating "the last face the ball passed through" and produces intuitively correct reflections.

More sophisticated approaches like the Separating Axis Theorem (SAT) exist, but for axis-aligned rectangles and circles, minimum overlap is sufficient. Both computation and branching are minimal.

Level Design Considerations

BreakoutMini's brick layout is 3 rows by 8 columns. Simple, but several design decisions are embedded:

const ROWS = 3
const COLS = 8
const BRICK_GAP = 2
const BRICK_TOP_OFFSET = 40
const ROW_COLORS = ['#ef4444', '#f59e0b', '#10b981']

3 rows is the optimal choice. There are three reasons. First, given the component height of 400px, space needs to be reserved for bricks, paddle, and ball movement. Second, color-coding by row creates a visual hierarchy of "red at top = far away = harder." Third, 24 bricks (3×8) results in clear times of 1-2 minutes, maintaining good pacing for a mini-game.

Dynamically calculating brick width is also important:

const brickW = (canvasW - BRICK_GAP * (COLS + 1)) / COLS

Subtract gaps from the Canvas width and divide by column count. This ensures bricks line up evenly to the edges regardless of screen width. Responsive design handled on the JavaScript side rather than CSS — a pattern specific to Canvas.

Dynamic Speed Adjustment

To prevent Breakout from becoming monotonous, ball speed increases based on remaining brick count:

const aliveCount = bricks.filter(b => b.alive).length
const speedMult = 1 + (1 - aliveCount / totalBricks) * 0.5

1.0x when all bricks remain, 1.25x when half are destroyed, 1.5x with one left. Difficulty naturally increases as the player improves. The 1.5x cap prevents the ball from reaching speeds where it might pass through walls.

Wall Collision Implementation

There's a reason we write ball.vx = Math.abs(ball.vx) instead of ball.vx = -ball.vx for wall collisions:

if (ball.x - BALL_RADIUS <= 0) {
  ball.x = BALL_RADIUS
  ball.vx = Math.abs(ball.vx)
}
if (ball.x + BALL_RADIUS >= w) {
  ball.x = w - BALL_RADIUS
  ball.vx = -Math.abs(ball.vx)
}

With -ball.vx, if the wall check runs multiple times in a single frame, the sign double-flips and the ball embeds in the wall. Using Math.abs forces the correct direction regardless of how many times the check runs. Subtle, but this kind of bug is annoying to debug.

Game State Management

BreakoutMini has five states: idle, playing, active, win, gameOver. Here's the state transition map:

idle → playing (tap to start game)
playing → active (tap to launch ball)
active → playing (ball falls, lives remaining)
active → gameOver (ball falls, no lives)
active → win (all bricks destroyed)
win / gameOver → playing (tap to retry)

The separation of playing and active is key. playing is when the ball is stuck to the paddle, giving the player time to aim. This "pause before launch" significantly affects the feel of the controls.

Performance Considerations

In BreakoutMini, all game state is managed with useRef. Using useState would trigger a re-render on every ball position update, making it difficult to maintain 60fps. Separating React's rendering cycle from Canvas's drawing cycle is the fundamental design principle for building games with React.

useState is used only for overlay UI (displaying "Game Over" and "You Win!"), while physics state is contained in useRef.

Summary: Tools for Building a Canvas Breakout Game

Implementing Breakout is ideal as an introduction to game programming and physics simulation. The books on the tool shelf cover the mathematical background of reflection vector calculation and more complex collision responses (rotation, friction) in a systematic way.