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
- 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.
- Gather every AABB overlapThe ball box (x±3, y±3) is tested against all live bricks in the touched column band.
- Pick the closest penetrationOnly ONE brick resolves per substep — the smallest axis overlap — so a corner hit never flips both axes.
- Choose the face
dxPen < dyPen→ side impact → flip dx. Otherwise the ball landed on a top/bottom face → flip dy. - 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.
- Preserve |v|Sign flips only. Speed is constant, so a shallow bounce never degenerates into a vertical crawl.
Face detection geometry
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;