GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Brick Breaker Game JavaScript — 9 Physics Patterns

brick breaker game javascript: Hit the paddle edge, not its center — the hit position value sets the reflection angle.

Auto-plays · click a tile to jump to its section · all nine in one zip

Nine JavaScript Patterns Behind This Brick Breaker, Each Driven by One Value

All nine demos below run the same 480×300 court, paddle, and ball, laid out with a seeded pseudo-random level so the page looks identical on every reload. What changes from one pattern to the next is which single value the code reads to decide what happens next: item 1 reads a drag gap, item 2 reads a hit position, item 5 reads a fall speed. The three items the opening line calls out sit at the center of the set on purpose — item 2 is the reflection rule the rest lean on, item 6 turns that same rule's output into a trail length, and item 8 is the boundary case of item 2's angle math, mirrored instead of recalculated.

01Paddle That Trails the Dragged Position

Instead of snapping straight to the pointer, the paddle's target x value chases the dragged position through an exponential-smoothing step — every frame it closes part of the gap, never all of it, so the paddle reads as if it carries some weight, and PADDLE_LERP_FACTOR sets how much of that gap closes per frame while the arrow keys nudge the same target value the pointer does.

Paddle That Trails the Dragged Position
Open in a new tab

01 · Paddle That Trails the Dragged Position — Measured area 30.092% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PADDLE_LERP_FACTORdragGapPxrebuildLevel
  function paddleStep(dt) {
    if (keys.left) paddle.target -= 160 * dt;
    if (keys.right) paddle.target += 160 * dt;
    paddle.target = clamp(paddle.target, WALL + paddle.w / 2, LW - WALL - paddle.w / 2);
    var goal = paddle.target - paddle.w / 2;
    var k = 1 - Math.pow(1 - PADDLE_LERP, dt * 60);
    paddle.x += (goal - paddle.x) * k;
    paddle.x = clamp(paddle.x, WALL, LW - WALL - paddle.w);
  }

The one calculated layer here is the frame-rate-independent smoothing constant k = 1 - Math.pow(1 - PADDLE_LERP, dt * 60) — it keeps the same drag feel at 30fps or 144fps, instead of the plain paddle.x += (goal - paddle.x) * PADDLE_LERP_FACTOR most snippets settle for.

Measured: cumulative area 30.1% of the frame, average intensity 80.4, moved 23/23 frames.

02Ball Whose Reflection Angle Splits by Hit Position

The ball's outgoing angle is not a fixed bounce — the code reads where across the paddle's width the ball landed and turns that position into an angle, so a center hit goes nearly straight up and an edge hit rockets out sideways.

Ball Whose Reflection Angle Splits by Hit Position
Open in a new tab

02 · Ball Whose Reflection Angle Splits by Hit Position — Measured area 35.241% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

MAX_REFLECT_ANGLElastHitPosguideTimer
  function onPaddleHit() {
    var hitPos = clamp((ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2), -1, 1);
    var angle = hitPos * MAX_REFLECT_ANGLE;
    var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
    ball.vx = Math.sin(angle) * speed;
    ball.vy = -Math.abs(Math.cos(angle)) * speed;
    ball.y = PADDLE_Y - BALL_R - 0.5;
    spawnFlash(ball.x, PADDLE_Y);
    lastHitPos = hitPos; lastAngle = angle; guideTimer = 0.4;
  }

hitPos is clamped to -1..1 and multiplied by MAX_REFLECT_ANGLE (62°) before it feeds Math.sin/Math.cos — that one multiplication is the whole reflection rule the rest of this brick breaker javascript set builds on.

Measured: cumulative area 35.2% of the frame, average intensity 103.6, moved 23/23 frames.

03Brick Row Whose Score Rises on Each Collision

Ball and brick are both treated as rectangles, and an axis-aligned bounding-box check decides the instant they overlap; the matched brick disappears and the score climbs by the same fixed amount for every brick destroyed — a running count, not a per-hit bonus.

Brick Row Whose Score Rises on Each Collision
Open in a new tab

03 · Brick Row Whose Score Rises on Each Collision — Measured area 30.576% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCORE_PER_BRICKscoreaabb
  function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.alive = false; cleared += 1; score = cleared * SCORE_PER_BRICK;
    if (cleared >= bricks.length) { onLevelClear(); }
  }

aabb() is the one function nearly half of this set leans on — four comparisons decide overlap, and score = cleared * SCORE_PER_BRICK turns that overlap count directly into the number shown on screen.

Measured: cumulative area 30.6% of the frame, average intensity 86.5, moved 23/23 frames.

04Brick That Cracks and Crumbles After Multiple Hits

A brick doesn't vanish on the first hit here — its hits value climbs, one crack line is drawn per hit, and only once hits reaches BRICK_HIT_LIMIT does the brick actually break and clear the level check.

Brick That Cracks and Crumbles After Multiple Hits
Open in a new tab

04 · Brick That Cracks and Crumbles After Multiple Hits — Measured area 30.507% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BRICK_HIT_LIMITcrackLinesdrawBrick
  function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.hits += 1;
    if (b.hits >= BRICK_HIT_LIMIT) {
      b.alive = false; cleared += 1;
      if (cleared >= bricks.length) { onLevelClear(); }
    }
  }

Fill opacity is driven by the same ratio used elsewhere in the file — damage = b.hits / BRICK_HIT_LIMIT fades the brick toward transparent as it takes more hits, so the crack count and the fade always agree with each other.

Measured: cumulative area 30.5% of the frame, average intensity 82.2, moved 23/23 frames.

05Paddle Catching an Item That Falls by a Gravity Value

Roughly every other brick that's destroyed drops an item at its own position, and that item's fall speed isn't constant — a gravity-acceleration value adds to its vertical speed every frame, so it starts slow and picks up speed the longer it falls.

Paddle Catching an Item That Falls by a Gravity Value
Open in a new tab

05 · Paddle Catching an Item That Falls by a Gravity Value — Measured area 28.728% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITY_ACCELpowerupscatchFlash
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.alive = false; cleared += 1;
    dropSeq += 1;
    if (dropSeq % 2 === 0) {
      powerups.push({ x: b.x + b.w / 2, y: b.y + b.h / 2, vy: 0 });
    }
    if (cleared >= bricks.length) { onLevelClear(); }
  }

dropSeq % 2 === 0 is the actual drop rule — one destroyed brick in two spawns an item, not every single one — and the acceleration itself lives a few lines further down, added to velocity before position so the fall speeds up the way a real falling object does.

Measured: cumulative area 28.7% of the frame, average intensity 82.1, moved 23/23 frames.

06Ball Afterimage That Lengthens With the Speed Value

The ball leaves a trail of past positions behind it, and how many of those positions stay visible depends on how fast the ball is moving right now — slow, the trail is short; sped up off a few paddle hits, it stretches into a long tail.

Ball Afterimage That Lengthens With the Speed Value
Open in a new tab

06 · Ball Afterimage That Lengthens With the Speed Value — Measured area 31.421% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPEED_STEPtrailpushTrail
  function pushTrail() { trail.push({ x: ball.x, y: ball.y }); }
  function stepExtra(dt) {
    pushTrail();
    var speedNow = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
    var count = Math.round(clamp((speedNow - BASE_SPEED) / (SPEED_MAX - BASE_SPEED), 0, 1) * (TRAIL_MAX - 3)) + 3;
    while (trail.length > count) trail.shift();
  }

count is recomputed fresh every frame from where the current speed sits between BASE_SPEED and SPEED_MAX, then the trail array is trimmed down to exactly that length — the tail isn't a fixed size that fades, it's a length value recalculated on every frame.

Measured: cumulative area 31.4% of the frame, average intensity 82.5, moved 23/23 frames.

07Combo Value Whose Multiplier Rises Each Consecutive Hit

Every brick hit without a miss raises a combo counter by one, and the score multiplier shown in the corner is that counter times a step value — one miss and the counter, and the multiplier with it, drops straight back to its starting point.

Combo Value Whose Multiplier Rises Each Consecutive Hit
Open in a new tab

07 · Combo Value Whose Multiplier Rises Each Consecutive Hit — Measured area 30.928% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

COMBO_MULT_STEPcomboCountresetCombo
  function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.alive = false; cleared += 1; comboCount += 1;
    if (cleared >= bricks.length) { onLevelClear(); }
  }
  function onMiss() {
    spawnFlash(ball.x, LH - 4);
    resetCombo();
    resetBall();
  }

The multiplier itself is only ever 1 + comboCount * COMBO_MULT_STEP — one line, recomputed on every draw, which is why missing resets the number instantly instead of animating it back down.

Measured: cumulative area 30.9% of the frame, average intensity 87.1, moved 23/23 frames.

08Bounce Whose Angle Mirrors on Wall Contact

When the ball touches a side wall, its horizontal velocity doesn't just reverse in general — only the sign of the horizontal component flips while the vertical component is left untouched, so the ball leaves the wall at exactly the angle it arrived at.

Bounce Whose Angle Mirrors on Wall Contact
Open in a new tab

08 · Bounce Whose Angle Mirrors on Wall Contact — Measured area 30.296% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

mirrorWallBounceWALL_MARGINmirrorFlashTimer
  function ballStep(dt) {
    ball.x += ball.vx * dt;
    ball.y += ball.vy * dt;
    if (ball.x - BALL_R <= WALL) { ball.x = WALL + BALL_R; mirrorWallBounce('left'); }
    else if (ball.x + BALL_R >= LW - WALL) { ball.x = LW - WALL - BALL_R; mirrorWallBounce('right'); }
    if (ball.y - BALL_R <= WALL) { ball.y = WALL + BALL_R; ball.vy = Math.abs(ball.vy); }
    if (ball.vy > 0 && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, paddle.x, PADDLE_Y, paddle.w, PADDLE_H)) {
      onPaddleHit();
    }
    for (var i = 0; i < bricks.length; i += 1) {
      var b = bricks[i];
      if (b.alive && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, b.x, b.y, b.w, b.h)) {
        ball.vy = -ball.vy;
        onBrickHit(b);
        break;
      }
    }
    if (ball.y - BALL_R > LH) onMiss();
  }

mirrorWallBounce() is a single ternary — flipping a sign instead of recalculating an angle is what keeps the incoming and outgoing angles identical, which is also why item 2's angle math never has to run twice for the same bounce.

Measured: cumulative area 30.3% of the frame, average intensity 93.6, moved 23/23 frames.

09Particles That Burst When All Bricks Clear

The instant the last brick's alive flag goes false, a burst of particles spawns from the middle of the court and spreads outward in every direction at once, signaling the level is about to reset.

Particles That Burst When All Bricks Clear
Open in a new tab

09 · Particles That Burst When All Bricks Clear — Measured area 29.451% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PARTICLE_COUNTspawnBurstparticles
  function spawnBurst() {
    for (var i = 0; i < PARTICLE_COUNT; i += 1) {
      var ang = (i / PARTICLE_COUNT) * Math.PI * 2 + rand() * 0.3;
      var sp = 60 + rand() * 90;
      particles.push({ x: LW / 2, y: LH / 2 - 20, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp, life: 1 });
    }
  }

Each particle's launch angle is spread evenly around a full circle (i / PARTICLE_COUNT * Math.PI * 2) plus a small random offset, and every particle starts from the same x: LW / 2, y: LH / 2 - 20 point — the court's own center, not the position of the brick that happened to break last.

Measured: cumulative area 29.5% of the frame, average intensity 90.4, moved 23/23 frames.

Where the Value Drives the Reaction — Position, Gravity, and Streak

Three of the nine make the link between a value and its visible reaction the easiest to see side by side. Item 1's drag-gap value only ever changes the paddle's x position, smoothed. Item 5's gravity value only ever changes an item's fall speed, added up frame by frame. Item 7's streak count only ever changes a multiplier shown as text — comboCount plus one, then straight back to zero the moment a miss happens. None of the three touches the others' state, which makes them a reasonable place to start if the goal is bending one constant and watching exactly one thing move.

Where a Brick Breaker Demo Like This Actually Earns Its Spot

A working brick breaker rarely ships as a standalone browser game anymore. It shows up as the thing a waiting screen plays while a build finishes, the easter egg behind a broken-link 404 page, or the interactive centerpiece of a portfolio page proving someone can write real canvas math instead of dropping in a heavier game framework. A queue-position screen on a small dashboard, a coffee-shop kiosk's idle timeout, or a conference booth's spare monitor are all places nine small physics rules like these are more useful than one finished game loop with lives, levels, and a title screen nobody asked for.

Where It Breaks — prefers-reduced-motion, Low-End Devices, Touch Input

All nine check prefers-reduced-motion before their first frame; when it's set, root.classList.remove('is-demo') turns off the automatic replay and every pattern waits for a real pointer or arrow-key input instead of pretending to play itself. Low-end devices feel it differently — nothing here touches WebGL or a heavy particle count, but redrawing a 480×300 canvas at 60fps in a background tab still costs a full clear-and-redraw every frame, and item 9's 36-particle burst is the one moment that briefly stacks the most draw calls at once. Touch devices are covered by the same pointerdown/pointermove listeners a mouse uses, so dragging the paddle behaves the same on a phone as on a desktop. The trap worth knowing before opening the zip: every one of the nine runs on a fixed seed (seedRand), so the layout on this page never changes between reloads — the actual constants and the nnb29y4j that unlocks the nine source files are the two things that turn a demo you're watching into a game you can actually tune.

What Nine Brick Breaker Patterns Actually Moved, Frame by Frame

Cumulative area is the share of the 480×300 canvas that changed pixel by pixel across the capture; every pattern here moved on all 23 measured frames, which tracks with a ball that's never actually at rest.

Item Cumulative area Avg. intensity Moved frames
01 Paddle drag lerp 30.1% 80.4 23/23
02 Ball reflect hit angle 35.2% 103.6 23/23
03 Collision score 30.6% 86.5 23/23
04 Crack multihit 30.5% 82.2 23/23
05 Powerup gravity catch 28.7% 82.1 23/23
06 Trail speed afterimage 31.4% 82.5 23/23
07 Combo multiplier streak 30.9% 87.1 23/23
08 Wall bounce mirror 30.3% 93.6 23/23
09 Clear particle burst 29.5% 90.4 23/23

Three Constants Worth Bending in This Build

Each variant below changes exactly one constant and nothing about the surrounding logic, so the difference in feel can be traced back to a single number.

Constant Change What shifts
PADDLE_LERP_FACTOR 0.15 → 0.4 The paddle closes the drag gap faster each frame — less lag, more of a snap
BRICK_HIT_LIMIT 3 → 5 Bricks take two more hits before breaking, so item 4's crack lines accumulate longer
COMBO_MULT_STEP 0.5 → 1 Item 7's multiplier climbs twice as fast per consecutive hit before a miss resets it

Brick Breaker Game Search vs. This Nine-Function Build

A plain brick breaker game search mostly turns up finished apps — store downloads, game-engine exports, and full games with menus and save state already built in. This build is the opposite of finished on purpose: nine separate, readable functions, each isolated enough to drop into an existing page as a small interactive widget or a teaching example, none of them wired to lives, levels, or a win screen. MDN's 2D collision detection guide covers the general AABB approach item 3 uses above in more depth than one demo page can. For a wider set built on the same idea of one value driving a whole scene, 9 CSS Game Demos — Mini Games in HTML & CSS is the survey this set belongs to; Spin Wheel: 9 Canvas Roulettes, Flick to Stop applies the same value-to-angle math item 2 uses here to a spinning wheel instead of a bouncing ball; and Button Press Animation UI: 9 Calculation Patterns is worth comparing item 1's drag-lerp against, since both smooth a target value the same way for a different kind of input.

Questions to Settle Before You Wire This Up

Are these nine patterns real, playable game mechanics or just canvas animations?

Every one runs on real velocity, collision, and timing math — the paddle, ball, and bricks all use the same aabb() overlap check and dt-based physics a shipped game would use. What's fixed for the demo is the seed (seedRand) and the constants, not the mechanics themselves.

Does the zip package these nine as one combined game or nine separate files?

Nine separate index.html/style.scss pairs, one per pattern, so a single reflection rule or gravity value can be lifted into an existing project without pulling in the other eight.

Is item 5's power-up drop something this brick breaker game with power ups actually needs, or is it optional?

Optional — dropSeq % 2 === 0 is the only line gating it, so removing that check, or the whole powerups array, still leaves items 1 through 4 and 6 through 9 working exactly as measured above.

Enter the archive password

The password is inside this article. You will find it as you read.