Canvas build · 400×600 · grid 8×4 · 32 bricks · 320 max
No physics engine. Each fixed 1/60s tick is split into sub-steps so the ball can never tunnel through a 20px brick, then the closest brick to the ball centre wins — one brick per frame, one reflection, one score event. The contact normal is snapped to its dominant axis so the ball always leaves along a clean horizontal or vertical vector.
// closest point on the brick's AABB to the ball centre const nx = clamp(ball.x, b.x, b.x + BW); const ny = clamp(ball.y, b.y, b.y + BH); const dx = ball.x - nx, dy = ball.y - ny; if (dx*dx + dy*dy <= R*R) { // circle vs AABB // snap normal to dominant axis → flip exactly one component (Math.abs(dx) > Math.abs(dy)) ? ball.dx = sign(dx)*speed : ball.dy = sign(dy)*speed; ball.x = nx + sign(dx) * (R + .5); // depenetrate, never stick }