Breakout
Render60fps
SCORE 0000 LIVES 3 BRICKS 32 SPEED 3.40 STATE READY
Cabinet 01 · Phosphor cyan · 60 Hz

Breakout32 bricks · 3 balls · 320 points

Canvas 400 × 600AABB + substep solverTouch · Mouse · KeysNo libraries
Insert coin ←→ / A D paddle Space launch P pause Drag finger on screen
Playfield · 8 × 4 gridNORMAL 3.40 px/f
400 × 600Touch drag60 fps

Operations Manual

Solver · Edge cases

Collision resolution, per substep

  1. Substep the sweepThe 3.4 px frame vector is split into N steps of ≤2 px so the ball can never tunnel through a 20 px brick row.
  2. Gather every AABB overlapThe ball box (x±3, y±3) is tested against all live bricks in the touched column band.
  3. Pick the closest penetrationOnly ONE brick resolves per substep — the smallest axis overlap — so a corner hit never flips both axes.
  4. Choose the facedxPen < dyPen → side impact → flip dx. Otherwise the ball landed on a top/bottom face → flip dy.
  5. De-penetrate, then reflectThe ball is pushed out along that axis before the sign flips: no brick can be re-entered on the next frame.
  6. Preserve |v|Sign flips only. Speed is constant, so a shallow bounce never degenerates into a vertical crawl.

Face detection geometry

dyPendxPen top face → dy = +|dy| side face → dx = −|dx| if (dxPen < dyPen) → horizontal else → vertical, push out, destroy 1 brick resolved per substep · |v| unchanged

The paddle is deliberately excluded from the brick pass and handled first: the contact is clamped to the paddle face, then the outgoing angle is derived from the offset of the contact point from the paddle centre — rel = (ball.x − centre) / 30, mapped to a ±60° cone, with a floor on |vy| so an edge graze can never flatten out.

System Readout

Field · physics · budget

Canvas
400×600 css px
Grid
8×4 50×20 px
Ball
r 3 px, yellow
Paddle
60×10 y = 580
Paddle range
0–340 px
Ball speed
3.0–4.0 px/frame
Fixed step
1/60 s accumulator
Max score
320 32 × 10

Rendering is 100 % Canvas 2D: bricks, paddle, ball, trail, particles, score plate, lives, launch hints, pause and end-of-game screens with a canvas-drawn restart pad (also hit-tested by touch). The DOM rails mirror the same state at a low tick rate so the 60 fps loop is never blocked by layout work.

Core solver (real source)

// one substep: sweep, then resolve the nearest face
const n = Math.max(
  Math.abs(ball.x - cx) - 25,
  Math.abs(ball.y - cy) - 10
);
if (n > R) continue;              // no overlap
const dxPen = (25 + R) - Math.abs(ball.x - cx);
const dyPen = (10 + R) - Math.abs(ball.y - cy);
if (dxPen >= bestPen || dyPen >= bestPen) continue;
best = b; bestPen = Math.min(dxPen, dyPen);  // closest

// resolve exactly one brick, de-penetrate first
if (dxPen < dyPen) { ball.x += sx * dxPen; ball.dx = sx * abs(dx); }
else             { ball.y += sy * dyPen; ball.dy = sy * abs(dy); }
best.alive = false; score += 10;