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

Brick Breaker Game: 9 Canvas Scenes

How does brick breaker turn a hit into a path? 01 sets the angle from paddle offset, 02 splits one frame so no fast ball skips a brick, 08 unfolds wall bounces

Brick Breaker Is Not a Picture of a Bouncing Ball but a Device That Computes the Bounce Direction, the Collision Order and the Brick State From Two Values, the Hit Position and the Travel Segment: Playing a Brick Breaker Game Online Starts by Knowing Which Value Sets the Direction

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

The nine scenes share one calculation frame and differ by a switch or two and a few constants. Seen as state rather than picture, a ball is position, velocity and spin, a brick is hit points and timestamps, a paddle is a horizontal position and a speed, and the canvas only paints those values.

The ball, paddle and bricks come from one calculation frame

World holds Ball, Brick and Mode, and one step stepWorld changes values in the order paddle control, then moveBall for each ball, then reflectBricks. drawWorld only reads those values, so the collision result comes from the same function in every scene, and the board on the right is the same calculation with one Mode switch turned the other way.

A fixed step of 1/120 second and DT_MAX 0.05

The elapsed time from requestAnimationFrame is cut at DT_MAX 0.05 seconds, and the stored time is split into STEP_H 1/120 second steps that each call stepWorld. A 60Hz and a 144Hz monitor run the same calculation per step, so the path is the same, and a tab hidden for a long time resumes without advancing more than 0.05 seconds at once. Scene 02 is the one exception: it sets w.step to FRAME_H (1/60) and passes dt × SLOW to advanceWorld, because it has to show 42px of travel per frame.

    // Call in fixed steps — time beyond DT_MAX in one go is not carried over
    function advanceWorld(w, dt) {
        w.acc += Math.min(dt, DT_MAX);
        while (w.acc >= w.step - 1e-9) {
            stepWorld(w, w.step);
            w.acc -= w.step;
        }
    }

Random numbers shape only the cracks

The motion of the ball has no random number. Launch angles come from LAUNCH_DEG, hit spots from AIMS and aims, and split angles from ANGLE_TABLE, all tables written in advance and read in order. The only random source is the seeded generator seeded (multiplier 1664525, increment 1013904223), seeded by brick row and column and used for the shape of the crack lines alone.

    function crackLines(k, n) {
        const rng = seeded(k.row * 131 + k.col * 17 + 7);
        for (let i = 0; i < n; i += 1) {
            const sx = k.x + k.w * (0.2 + 0.6 * rng());
            const top = (i % 2) === 0;
            let cx = sx;
            let cy = top ? k.y : k.y + k.h;
            const ty = top ? k.y + k.h * 0.9 : k.y + k.h * 0.1;
            const steps = 3;
            for (let s = 0; s < steps; s += 1) {
                const nx = cx + (rng() - 0.5) * k.w * 0.34;
                const ny = cy + (ty - cy) / (steps - s);
                seg(cx, cy, nx, ny, 1.4, INK, 0.62);
                cx = nx;
                cy = ny;
            }
        }
    }

Brick Breaker Game: 9 Scenes — A Bounce Angle Set by Paddle Offset, a Swept Collision That Splits One Frame, Corner Normal Picking, a Curving Path From Paddle Speed, a Fixed Angle Table Multiball, Cracks by Hit Points, Speed Steps With a Trail, a Predicting Paddle and a String Grid Level With a Chain Blast

What nine renders measured for area and motion

Scene Cumulative area Moving frames Mean intensity
01 Bounce angle 11.2% 23/23 90.7
02 Swept collision 29.8% 22/23 62.5
03 Corner normal 11.3% 23/23 87.4
04 Spin transfer 15.1% 23/23 67.9
05 Multiball 12.9% 23/23 88.9
06 Brick cracks 7.9% 21/23 86.8
07 Speed steps 9.1% 23/23 93.9
08 Landing prediction 14.8% 23/23 87.4
09 String level 12.4% 23/23 86.8

02 covers the most area at 29.8% and 06 the least at 7.9%. The moving frames are 22/23 for 02, 21/23 for 06 and 23/23 for the other seven, and the mean intensity peaks at 93.9 in 07.

01A Bounce Angle Set by the Paddle Hit Offset

The first value to touch when learning to aim with a paddle: hitOffset is the distance from the paddle center to the ball divided by half the width, from -1 to 1, and the bounce angle is u × MAX_ANGLE, 58 degrees. The right board uses only the mirror bounce vy = -|vy| and goes up and down one lane. Hits at -1, -0.5, 0, 0.5 and 1 gave -58.0, -29.0, 0, 29.0 and 57.9 degrees, and in 12 seconds the left cleared 11 bricks and the right 4.

A Bounce Angle Set by the Paddle Hit Offset
Open in a new tab

01 · A Bounce Angle Set by the Paddle Hit Offset — Measured area 11.156% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PADDLE_WhitOffsetMAX_ANGLE
                b.y = w.py - BALL_R;
                const u = hitOffset(w, b);
                if (m.angle === 0) {
                    const a = u * MAX_ANGLE;
                    b.vx = Math.sin(a) * w.speed;
                    b.vy = -Math.cos(a) * w.speed;
                }
                else {
                    b.vy = -Math.abs(b.vy);
                }

02A Swept Collision That Catches a Fast Ball

A fast ball must not skip a row, so one frame of travel is split into pieces of at most STEP_MAX 4px, rounded up, and each piece checks the bricks. This scene moves 42px per frame, which is 11 pieces, and any travel over 16px (brick thickness 6 plus ball diameter 10) lets an end-position check jump a row; fired from 200 start phases, the sweep let 0% through and the end-only board 62.0%, close to the 61.9% of (travel - 16) ÷ travel. In the 12-second scene both sides cleared 16 bricks, but the right board passed through 12 times and the left 0.

A Swept Collision That Catches a Fast Ball
Open in a new tab

02 · A Swept Collision That Catches a Fast Ball — Measured area 29.834% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

STEP_MAXFRAME_HBALL_SPEED
    // Travel per frame in px = BALL_SPEED × FRAME_H. Longer than brick thickness plus ball diameter, it skips a whole row
    const FRAME_H = 1 / 60;
    const BALL_SPEED = 2520;
    const BRICK_H = 6;
    // The sweep splits one frame of travel into pieces of at most STEP_MAX (ceil(travel ÷ STEP_MAX) pieces)
    const STEP_MAX = 4;
    // Slow the scene clock for viewing — the ball is too fast for the eye to follow
    const SLOW = 0.3;

03A Normal That Picks the Face at a Corner

A corner graze should not send the ball somewhere odd, so the overlap depth is measured as ox across and oy down, the ball is pushed out of the shallower axis and only the speed on that axis flips. The right board flips vy = -vy alone and digs into a brick when it hits a side. Firing at one pillar from 5 spots and 41 angles, 205 shots, the shots that left the ball center inside the pillar for 6 or more steps were 0 with face picking and 66 with the speed flip.

A Normal That Picks the Face at a Corner
Open in a new tab

03 · A Normal That Picks the Face at a Corner — Measured area 11.336% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

reflectBricksLAUNCH_DEGface
        if (w.mode.face === 0) {
            // The face on the shallower overlap axis is the face hit — push out on that side and flip only that axis speed
            const ox = BALL_R + k.w / 2 - Math.abs(b.x - cx);
            const oy = BALL_R + k.h / 2 - Math.abs(b.y - cy);
            if (ox < oy) {
                const dir = b.x < cx ? -1 : 1;
                b.x += dir * ox;
                b.vx = dir * Math.abs(b.vx);
            }
            else {
                const dir = b.y < cy ? -1 : 1;
                b.y += dir * oy;
                b.vy = dir * Math.abs(b.vy);
            }
        }
        else {
            // Contrast board — no face picking, only the vertical speed flips. A side hit digs into the brick
            if (w.t - k.hitT < h * 1.5) {
                w.stuck += 1;
            }
            b.vy = -b.vy;
        }

04A Spin Transfer That Curves the Ball From Paddle Speed

To make hitting while the paddle moves a real skill, the paddle speed times SPIN_GAIN 0.15 passes into spin at the hit, then each step the spin pushes the ball sideways by SIDE_PUSH and decays at SPIN_DECAY. The left paddle ran at -300px/s and took spin -45, while the right one stood still with spin 0. Both hit at x 111 with a horizontal ball speed of 86.7px/s, yet 0.5 seconds later the horizontal travel split into +19.3px against +43.4px.

A Spin Transfer That Curves the Ball From Paddle Speed
Open in a new tab

04 · A Spin Transfer That Curves the Ball From Paddle Speed — Measured area 15.136% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPIN_GAINSPIN_DECAYSIDE_PUSH
        const m = w.mode;
        if (m.spin > 0) {
            // Spin becomes a sideways push each frame and slowly decays
            b.vx += b.spin * m.push * h;
            b.spin *= Math.exp(-m.decay * h);
        }

05A Multiball That Splits by a Fixed Angle Table

To add balls without a random number, a brick that splits the ball takes the next value of ANGLE_TABLE as the angle for SPLIT_N new balls and stops at BALL_MAX. Running the same board twice and hashing the ball positions and the broken count gave a match at every second, 12 of 12 over 12 seconds. Those 12 seconds held 5 splits, at most 5 balls and 23 bricks cleared, and one step costs the same checks for each ball.

A Multiball That Splits by a Fixed Angle Table
Open in a new tab

05 · A Multiball That Splits by a Fixed Angle Table — Measured area 12.935% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ANGLE_TABLESPLIT_NBALL_MAX
    function onSplit(w, k) {
        if (k.kind !== 1) {
            return;
        }
        for (let i = 0; i < SPLIT_N; i += 1) {
            if (w.balls.length >= BALL_MAX) {
                return;
            }
            const b = newBall(w, w.balls.length + splits * 10);
            const a = ANGLE_TABLE[tableAt % ANGLE_TABLE.length] * Math.PI / 180;
            tableAt += 1;
            b.x = k.x + k.w / 2;
            b.y = k.y + k.h + BALL_R;
            b.vx = Math.sin(a) * w.speed;
            b.vy = Math.cos(a) * w.speed;
            b.st = 0;
            w.balls.push(b);
        }
        splits += 1;
    }

06Brick Cracks That Spread With Hit Points

A tough brick is one integer: hurt does hp -= 1 on each hit, the remaining hit points become a color step and the number of hits becomes the crack line count. The 10×4 board with hp 1, 2, 3 and 4 from the top has 16 bricks of hp 1 and 8 each of 2, 3 and 4, so clearing it takes 88 hits. In 12 seconds it took 28 hits and broke 9 bricks, with at most 6 bricks cracked at once.

Brick Cracks That Spread With Hit Points
Open in a new tab

06 · Brick Cracks That Spread With Hit Points — Measured area 7.872% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

HP_MAXCRACK_NTINT_STEP
    function hurt(w, k) {
        k.hitT = w.t;
        w.hits += 1;
        if (k.hp >= 90) {
            return;
        }
        k.hp -= 1;
        if (k.hp <= 0) {
            k.deadT = w.t;
            w.broken += 1;
            w.lastBreak = w.t;
            if (w.onBreak) {
                w.onBreak(w, k);
            }
        }
    }

07A Trail That Lengthens With the Speed Step

To raise tension as a round goes on, speedFor adds SPEED_STEP 70 for each whole CLEAR_N 3 bricks cleared on top of 190 and cuts the result at SPEED_MAX 590, and the trail grows as round(speed × TRAIL_LEN) + 2. With 0 cleared the speed is 190 with a trail of 16, and with 18 cleared it is 590 with a trail of 46. In 12 seconds 24 bricks fell, the top speed came at 10.0 seconds and the ball tunneled 0 times.

A Trail That Lengthens With the Speed Step
Open in a new tab

07 · A Trail That Lengthens With the Speed Step — Measured area 9.067% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPEED_STEPTRAIL_LENCLEAR_N
    function update(dt) {
        const w = WS[0];
        // When the step rises, speed and trail length change together — one step of travel is still split into pieces of 4px or less (same as the swept collision in 02)
        w.speed = speedFor(w.broken);
        w.trailN = Math.round(w.speed * TRAIL_LEN) + 2;
        advanceWorld(w, dt);
    }

08A Paddle That Unfolds Wall Bounces to Predict the Landing

Auto play demos and a practice opponent need the landing spot, so at the time t the ball reaches paddle height, b.x + b.vx × t is folded into the board width by foldW and the paddle eases to that spot with FOLLOW_EASE, while the right board follows only the current horizontal position of the ball. Over 24 seconds the misses were 0 against 7. A prediction made 0.25 seconds before the landing differed from the actual hit position by 1.36px on average and 2.67px at most over 17 hits.

A Paddle That Unfolds Wall Bounces to Predict the Landing
Open in a new tab

08 · A Paddle That Unfolds Wall Bounces to Predict the Landing — Measured area 14.792% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

foldWlandXFOLLOW_EASE
    function foldW(v, lo, hi) {
        const len = hi - lo;
        const per = 2 * len;
        const u = ((v - lo) % per + per) % per;
        return lo + (u <= len ? u : per - u);
    }
    // Time left until the ball reaches paddle height (s) and its horizontal position then — bricks ignored, only walls unfolded
    function landing(w, b) {
        const down = w.py - BALL_R;
        let t = 0;
        if (b.vy > 0) {
            t = (down - b.y) / b.vy;
        }
        else {
            t = ((b.y - BALL_R) + (down - BALL_R)) / Math.max(1e-6, -b.vy);
        }
        return [foldW(b.x + b.vx * t, BALL_R, w.w - BALL_R), t];
    }

09A String Grid Level With a Chain Blast Wave

Level editing, chain effects and a landing hero intro all suit a level written as six LEVEL_ROWS strings. When a blast brick breaks, bricks within BLAST_R 1.25 cells get a fuse set to now plus distance in cells × WAVE_DELAY 0.08 × 2 seconds, and a brick whose fuse has passed breaks regardless of hit points. Of 60 bricks 8 are blast bricks, and in 12 seconds 2 blasts (at 0.80 and 0.97 seconds) broke 9 bricks by fuse between 0.87 and 1.14 seconds.

A String Grid Level With a Chain Blast Wave
Open in a new tab

09 · A String Grid Level With a Chain Blast Wave — Measured area 12.41% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LEVEL_ROWSWAVE_DELAYBLAST_R
    function onBlast(w, k) {
        if (k.kind !== 2) {
            return;
        }
        const cx = k.x + k.w / 2;
        const cy = k.y + k.h / 2;
        blasts.push([cx, cy, w.t]);
        const cell = k.w + 2;
        for (const o of w.bricks) {
            if (o.deadT >= 0 || o === k) {
                continue;
            }
            const d = Math.hypot(o.x + o.w / 2 - cx, o.y + o.h / 2 - cy) / cell;
            if (d > BLAST_R) {
                continue;
            }
            const f = w.t + d * WAVE_DELAY * 2;
            if (o.fuse < 0 || f < o.fuse) {
                o.fuse = f;
            }
        }
    }

When You Code Brick Breaker, How Many Values Decide One Frame of One Ball?

The values one ball carries

A ball has position x and y, velocity vx and vy, spin spin, state st and a trail trail. State 0 is flying, 1 is on the paddle and 2 is lost, and the speed magnitude is set by the board value w.speed and not by the ball.

Value Meaning Changed by
vx and vy Direction Paddle hit spot, wall, brick face
spin Sideways push Paddle speed (04)
st Flying, on paddle, lost Launch, floor
hp, deadT, fuse Brick hit points, break time, fuse hurt and onBlast (06, 09)

One switch builds the contrast board

The sweep, face, angle and spin fields of Mode each change one place in the calculation. 01 turns angle the other way on the right board, 02 turns sweep, 03 face and 04 spin, so every other line is the same and the difference between the boards belongs to that one rule.

Take the Ball Position and Velocity, Split the Travel, Find the First Brick Touched and Reflect the Velocity About the Face Normal: the Order in Which One Brick Breaker Frame Is Computed

The order of one step

moveBall adds spin as a sideways push, sets the speed magnitude to w.speed and, when the ball is nearly horizontal, keeps the vertical part at 20% of the speed. It then splits the travel into pieces of at most STEP_MAX and for each piece runs the wall bounce, the paddle top line check and reflectBricks. Only the deepest overlapping brick is picked, so one piece handles one brick.

The piece count rounds up

The count is the travel divided by STEP_MAX, rounded up. 02 at 42px per frame is 11 pieces, and the top speed of 590px per second in 07 is 4.92px per 1/120 second step, which is 2 pieces. Smaller pieces leave less room for a ball to jump over a brick box but cost more checks, and splitting the travel extends the collision tests in the MDN 2D collision detection page.

Putting Rules on Top Like a Brick Breaker Game With Power Ups: Building Without a Library, Writing a Level as a String and When to Raise the Ball Speed

Building without a library

Canvas 2D is enough, and the nine scenes use no library, no WebGL and no CDN. Score, lives and menus sit outside this frame and can live on the side that reads the World values. If you search for a brick breaker game download, what the zip holds is source to run, not an installer.

Writing a level as a string

09 builds the board from a string of 12 columns and 6 rows where . is an empty cell, 1 to 3 are hit points and B is a blast brick. For a game that swaps boards often, editing the row strings is how you add levels, and parseLevel reads one character as one brick cell.

When to raise the ball speed

speedFor computes the speed from the cleared count and trailN changes in the same moment. Since the piece count grows with the speed, the 12-second scene of 07 had 0 tunnels, so the difficulty curve is tuned by two values, SPEED_STEP and CLEAR_N.

How this differs from the pinball post

The pinball post is about picking flippers and bumpers, and this one is about getting the paddle position, the travel and the brick state from one frame. Searching for a brick breaker game free online finds hosted pages to play, while this post is the calculation behind one, ready to copy onto any static host. The minesweeper post uses the same timetable style for a grid rule.

Where Brick Breaker Breaks: a Trap of Fast Balls That Tunnel Through Bricks, Corner Hits That Bounce the Wrong Way and a Paddle Edge Angle That Traps the Ball Flat

A fast ball passes through a brick

On the right board of 02, which checks only the end position of each frame, the ball skips a row once its travel passes 16px. At 20px per frame 20.5% passed through, at 30px 46.5% and at 42px 62.0%, while 10px gave 0%. The fix is one change, splitting the travel at STEP_MAX or less.

A corner hit bounces the wrong way

The right board of 03 only flips the vertical speed, so a ball that hits the side of a brick keeps hitting inside it. In the 12-second scene it hit 254 times and 242 of those steps were buried. The pillars of 03 hold 99 hit points, and hurt leaves a brick of 90 or more standing and only counts the hit, so the test measures the bounce and not the cleanup.

A paddle edge traps the ball flat

The right board of 01 uses the mirror bounce alone, cleared four bricks in one column and then stalled going up and down the same lane, against 11 bricks on the left. A nearly horizontal ball is easy to trap in one row, so moveBall does not let the vertical part fall under 20% of the speed.

The zip in the box below holds the finished source of all nine scenes and the edit files, and the password for unzipping it is fj24x8mt.

Change the Paddle Width, the Speed Steps and the Spin Gain and How Much Does the Feel Shift? PADDLE_W, SPEED_STEP and SPIN_GAIN Compared

Name Changed value Feel
A narrower paddle 01 PADDLE_W 96 → 56 The hit-spot ratio is unchanged, so the angle table (-1 to 1 maps to ±58 degrees) is the same, and only the pixel width moves: the same 0.7 spot goes from 33.6px to 19.6px off center and the half width that can catch the ball from 51px to 31px. The autopilot stands under the landing spot, so the 12-second result stayed at 11 bricks on the left and 4 on the right with 0 misses, but a person steering has that much less margin
Steeper speed steps 07 SPEED_STEP 70 → 150 Speed goes 190, 340, 490, 590 (cut by SPEED_MAX), so the top speed arrives at 5.3 seconds instead of 10.0, and the bricks cleared in 12 seconds go from 24 to 27 with misses from 1 to 0. At a 1/30 second frame the 590px/s ball travels 19.7px, over the 16px of thickness plus diameter, so by the formula of 02 about 19% would pass through
A bigger spin transfer 04 SPIN_GAIN 0.15 → 0.45 On the same first hit (paddle speed 300px/s, ball horizontal speed 86.7px/s) the spin size goes from 45 to 135, the horizontal travel in 0.5 seconds changes from +19.3px to -28.2px and the horizontal speed from -4.8 to -163px/s. With no spin handed over, the right board goes +43.4px straight

What to look at before changing a value

The three values do not touch each other: PADDLE_W changes only the pixel width, SPEED_STEP only the speed steps and SPIN_GAIN only the spin. Change one at a time so you can see which value moved the feel.

One Frozen Brick Board Under Reduced Motion: Accessibility Through a Sentence for the Scene and the Bricks Left

Arrow keys move the paddle and Enter launches

The stage box in all nine scenes is role="application" with tabindex 0, so it takes focus, and the first arrow key press stops the auto demo and hands the paddle to the person. Enter and Space launch a waiting ball, or pause when no ball waits, P pauses, N steps 0.1 seconds and R restarts. A mouse or a touch makes the paddle follow that spot.

The status line rewrites only when the sentence changes

The status line is aria-live="off" during the demo so it is not read, turns polite the moment a person takes over and is rewritten only when the sentence changes. 0.6 seconds before a lap ends, the result sentence is written once into a hidden role="status" paragraph.

Reduced motion and a hidden tab

With prefers-reduced-motion on there is no loop, and showRest draws a single frozen frame at the REST_S second mark. All nine scenes listen for visibilitychange, stop the loop when the tab hides and restart it on return, following the MDN Page Visibility API and prefers-reduced-motion pages.

    // Stop when the tab hides and restart with the clock re-synced on return — so the hidden seconds do not land in one frame
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            stopLoop();
        }
        else if (!paused && !still) {
            startLoop();
        }
    }, { signal: off.signal });
    // When the setting changes — on shows one still frame, off moves again
    reduceQuery.addEventListener('change', function () {
        still = reduceQuery.matches;
        if (still) {
            showRest();
        }
        else {
            lap = 0;
            play();
        }
    }, { signal: off.signal });

Brick Breaker FAQ: Building Without a Library, What to Do When the Ball Passes Through a Brick and What You Need Before Putting It on the Web

Can I make a brick breaker game online without a library?

Yes. Canvas 2D and requestAnimationFrame are enough, the nine scenes use no library, no WebGL and no CDN, and the scene source keeps the calculation frame and the scene in one file.

What do I do when the ball passes through a brick?

Split the travel. Keep STEP_MAX below the brick thickness plus the ball diameter and the pass count is 0, as in the sweep of 02.

What do I need to put it on the web?

One set of static files. The canvas multiplies devicePixelRatio by 2 at most, and there is no external font or server call, so it runs on any static host.

The source for the frame loop is the MDN requestAnimationFrame page.

Enter the archive password

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