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

Falling Text Animation: 9 Letters With Physics

Drop a headline and every letter bounces alike. Here 01 spins corner landings into a pile, 03 squashes letters like jelly by impact speed, and 05 floats them

No physics engine, library or WebGL is loaded in any of them. Every scene measures each glyph with measureText, treats it as a box, and recomputes forces and collisions on a fixed 16ms step, drawing the result on one 2D canvas.

A Stage Where Nine Sets of Letters Drop, Hang and Float

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

Each cell holds one canvas laid out on scene coordinates 480 wide and 300 tall, and the English stage words are STACK IT UP, HANG TIGHT, SQUISH, HELLO THERE, SUMMER SPLASH, SHATTER, ADD TO CART, AUTUMN WIND and BACK IN LINE. Until you touch a cell, a demo loop resets its scene every 3,400ms; the first press on the canvas, or the first key after you focus it, removes the demo label and hands the scene over to you. The two buttons under each cell restart the scene and pause it, and while the canvas has focus, Enter and Space do the same two jobs.

What your pointer and keys do in each cell

Scene Pointer Keyboard
01 Pile-up Flicks the letters where you press ←/→ shake, ↑ toss
02 Ropes While you drag, a circle of radius 22px shoves the ropes aside ←/→ shake
03 Jelly Flicks the nearest letter ↓ slam, ↑ toss
04 Dodge Moving is enough, the pointer itself is the force Arrow keys move a keyboard pointer 22px per press, Esc removes it
05 Water Pushes down the surface where you press ↓ dunk, ←/→ push
06 Shards Slams the letter you press ↓ slam faster than the threshold
07 Tilted box Horizontal position becomes the tilt ←/→ tilt 4° per press, Esc returns to auto
08 Wind Lifts the letters where you press ←/→ gust, ↑ lift them all
09 Headline Knocks it down right away ↓ collapse, ↑ bring it back

Falling Text Animation: 9 Scenes — Pile-Up, Hanging Ropes, Jelly Squash, Dodging, Floating, Shattering, Tilted Box, Wind and a Hero Headline

Rigid boxes that fall and stack in 01 are the base of the set. 02 hangs those boxes from ropes, 03 swaps the rigid box for a soft body, 04 gives up falling and flees the pointer instead, 05 trades the floor for water, and 06 breaks a letter apart only when it lands hard enough. 07 tilts the floor itself, 08 lets wind and drag take the lead over gravity, and 09 borrows the box math from 01 to knock a headline down and then stand it back up in reading order.

Screen change counted across a two-second, 24-frame capture

For this table every scene was recorded at 480×300 for two seconds; of the 23 steps between its 24 frames, the middle column counts the ones that changed the picture.

Scene Cumulative area Moving frames Mean intensity
01 Corner-spin pile 24.1% 21/23 101.0
02 Rope-hung sign 16.6% 23/23 121.3
03 Jelly squash 23.0% 23/23 104.3
04 Pointer dodge 52.1% 23/23 67.3
05 Water float 27.1% 22/23 88.5
06 Shatter on impact 21.9% 15/23 117.2
07 Tilted box 26.9% 23/23 94.0
08 Wind drift 22.6% 23/23 90.4
09 Collapse and realign 26.9% 20/23 105.1

04 covers by far the most ground, likely because its virtual pointer sweeps a figure-eight across the whole stage and drags the radius circle and the letters along with it. 06 changed in only 15 of the 23 steps, probably because the two letters that land under the threshold and the scattered shards come to rest and leave the picture nearly still. No scene dropped below 10% cumulative area.

Pages where these letters earn their place

The first screen of a product launch page suits 01, where the title pours down into a pile, or 09, where it falls apart and then stands in one line again, while a banner announcing an event fits the hanging sign in 02. 03 works on the start screen of an app for kids, 05 on the headline of a summer event page and 08 on an autumn sale banner, because each of them names its season or mood through the motion itself.

A portfolio greeting that should react to the visitor's pointer is where 04 belongs. If you want a falling text effect on a game results screen, 06 can break only the score digits that land hard, which reads as a high-score moment, and the tilting box in 07 works as a short picture of an item sliding into the cart.

Interactive Text Animation Could Use Keyframes — So Why Compute Speed and Force Every Tick?

Keyframes replay a path that someone wrote in advance, and no one can write down beforehand the angle at which six letters will come to rest after knocking into each other. In 01 on the Korean build, the resting angles came out as 0, 89, 89, 1, 137 and 323°, a different one for each letter, and once you flick a letter, everything after depends on where it was and how fast it moved at that instant. Results like these only appear by repeating one calculation: take the current position and velocity, then work out the next ones. That is why none of the nine stylesheets has a single @keyframes rule, and the only transition left in them is a button's background color.

If you would rather start from a path that is fixed ahead of time, the CSS text animation post covers keyframe bounces. This post goes the other way: it sets forces and leaves the path open.

A clock that catches up six steps per frame at most

The script banks the time differences that requestAnimationFrame passes in and spends them in fixed 16ms steps (FIXED_DT). A single frame can bank 100ms at most, so it catches up at most six steps. The step length does not depend on the display's refresh rate, so a 60Hz screen and a 120Hz screen likely walk through the same sequence of calculations.

One seed, one identical pile

Placement comes only from formulas or a seeded random generator (makeRng with 4721, 4725, 4726+i, 4727 and 4729), and neither Math.random nor Date.now shows up anywhere in the nine sources. Because the step and the seed are both fixed, dropping the letters again without touching them lands the same shapes in the same spots. For the first 200ms after each restart (LEAD_MS) the step does nothing at all, so the letters hang where they were placed for a beat before they begin to move.

Treating Each Letter as a Box: Collision Checks and the Impulse That Spins a Corner Landing

A glyph box measured with measureText

In all nine scenes, layoutWord splits the source text into glyphs with Array.from and measures each one with measureText. When the whole line runs wider than its allowed width, every glyph shrinks by the same ratio. A box is as wide as its measured glyph, with 0.34 of the font size as the narrowest it can be, 0.9 of the font size tall, and spaced from its neighbor by 0.1 of the font size, while a space takes room without getting a box. In 01 the English STACK IT UP comes out at 55.1px, with box widths running from 19.2px for the I to 41.3px for the A, whereas the Korean words land at 58px with six equal widths and the Japanese ones stay at the full 74px, equal widths again.

Each letter is painted twice with fillText: first in its color darkened to half and offset by (1.4, 2.8)px, then in its own color on top, which gives the glyph some thickness. The type is a list of system fonts at weight 800, and no web font is fetched.

Mass and rotational inertia set by box area

In 01 and 09, every box takes its area divided by 900 as its mass and m(w² + h²)/12 as its rotational inertia. A wider letter is heavier and harder to spin, and the I box, 0.46 times as wide as the A box at the same height, is lighter by that same ratio.

How a corner contact turns impulse into spin

A contact appears wherever a box corner pokes past the floor or a wall, or into another box, and the axis with the shallower overlap becomes the push direction, called the normal. effMass adds 1/mass to the square of (contact − center of mass) × normal divided by the rotational inertia. When the contact sits right under the center, that cross product is 0 and the whole impulse goes into pushing up; when it sits off to one side, as a corner does, part of the same impulse turns into angular velocity. If the normal speed just before contact is below −1.2px per tick, that is, faster than 1.2px per tick into the surface, the target is a rebound at 0.32 of that speed, and slower contacts target 0, so stacked letters settle quietly.

This is what each scene works out in one tick, and what makes its motion die down.

Scene Work done in one tick What calms it
01 Gravity integration twice per tick, corner contacts, 6 impulse passes Rebound 0.32, friction 0.55
02 Position integration that carries velocity as the difference from the previous position, 8 segment-length fixes Air damping 0.012
03 Best-fit rotation of the rest shape, shape spring 0.45, pressure 0.8 Point damping 0.02, vertical speed × −0.1 at the floor
04 Home spring 0.05, push by inverse distance Velocity × 0.88
05 Column springs plus two neighbor handoffs, submerged share from 20 sample points Wave damping 0.02, drag in the water
06 Gravity 0.5, first impact speed compared with a threshold of 9 Shards × −0.3 at the floor, sideways × 0.7
07 Gravity turned by the box angle, 4 push passes × 0.96 once per tick
08 Value-noise wind, drag on speed squared, glide from the tilt Drag 0.02, angular damping 0.06
09 The box math from 01, then a return spring from 0.9 seconds on Damping 2√0.03

The per-scene figures in the next nine sections come from calling the source functions directly on the Korean build of the page, whose stage words run four to six glyphs. The English words carry more letters, so counts such as six letters or the middle letter refer to that build, and timings on this page can drift from them.

01Letters That Spin on a Corner Landing and Pile Up

Six letters, pulled toward the middle and stacked in the air a little over 62px apart, fall onto one another: those that land on a corner tip over spinning, those that land flat sit still, and the resting angles spread across 0, 89, 89, 1, 137 and 323°. solveContact runs six times per contact, clips the accumulated impulse so it cannot go below 0, which keeps boxes from pulling on each other, and keeps the sliding impulse within 0.55 of the pressing one. The first floor touch came at 768ms, the fastest spin was 16.7° per tick, all six were still for good from 2,688ms, and the pile stood 77.1px tall, 1.48 times the 52.2px letter height.

Letters That Spin on a Corner Landing and Pile Up
Open in a new tab

01 · Letters That Spin on a Corner Landing and Pile Up — Measured area 24.095% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYRESTITUTIONFRICTION
    function effMass(c, dx, dy) {
        const ra = (c.px - c.a.x) * dy - (c.py - c.a.y) * dx;
        let k = c.a.im + ra * ra * c.a.ii;
        if (c.b) {
            const rb = (c.px - c.b.x) * dy - (c.py - c.b.y) * dx;
            k += c.b.im + rb * rb * c.b.ii;
        }
        return k;
    }
    function solveContact(c, fric) {
        const rel = relVel(c);
        const vn = rel[0] * c.nx + rel[1] * c.ny;
        const jn0 = c.jn;
        c.jn = Math.max(jn0 + (c.target - vn) / effMass(c, c.nx, c.ny), 0);
        const dj = c.jn - jn0;
        pushPair(c, c.nx * dj, c.ny * dj);
        // friction — the sliding impulse cannot exceed the pressing impulse x fric
        const tx = -c.ny;
        const ty = c.nx;
        const rel2 = relVel(c);
        const vt = rel2[0] * tx + rel2[1] * ty;
        const lim = fric * c.jn;
        const jt0 = c.jt;
        c.jt = clamp(jt0 - vt / effMass(c, tx, ty), -lim, lim);
        const djt = c.jt - jt0;
        pushPair(c, tx * djt, ty * djt);
    }

02Letters Swinging on Hanging Ropes

Each letter hangs from a rope that starts at 46px and grows 10px longer from one letter to the next, and when they are released from a pose lifted 30° to the right, every rope swings on its own period, so the row ripples out of step. Each rope point carries its velocity as the gap between its current and previous position times 0.988, and fixSegment runs eight times to restore segment lengths, splitting each correction by inverse mass (0 at the hook, 1 along the rope, 0.3 at the letter). Rope stretch peaked at 2.57%, the last letter leaned as far as 33.6°, and the swing took 7,984ms to drop under 0.05px per tick and stay there.

Letters Swinging on Hanging Ropes
Open in a new tab

02 · Letters Swinging on Hanging Ropes — Measured area 16.641% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROPE_SEGMENTSCONSTRAINT_ITERATIONSAIR_DAMPING
    function fixSegment(r, k) {
        const dx = r.px[k + 1] - r.px[k];
        const dy = r.py[k + 1] - r.py[k];
        const d = Math.max(Math.hypot(dx, dy), 0.0001);
        const wa = invMass(k);
        const wb = invMass(k + 1);
        const diff = (d - r.rest) / d / (wa + wb);
        r.px[k] += dx * diff * wa;
        r.py[k] += dy * diff * wa;
        r.px[k + 1] -= dx * diff * wb;
        r.py[k + 1] -= dy * diff * wb;
    }

03Letters That Squash Like Jelly by Their Impact Speed

Each letter's body is a ring of 28 outline points found by casting rays toward its pixels from 28 directions, and the faster it meets the floor, the flatter it squashes before it springs back up. pose finds the rotation that best lays the rest shape over the current points; each point is then pulled 0.45 of the way toward a rest shape turned by only 0.85 of that angle, which creates a righting force, and pushed outward by the lost-area ratio × 0.8 × 2. First impacts of 7.42, 9.35, 11.41 and 13.09px per tick gave lowest heights of 0.706, 0.623, 0.599 and 0.594 of the original, and any height under 0.58 is stretched back up from the floor side.

Letters That Squash Like Jelly by Their Impact Speed
Open in a new tab

03 · Letters That Squash Like Jelly by Their Impact Speed — Measured area 22.985% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPRING_STIFFNESSPRESSURESQUASH_LIMIT
    function pose(j) {
        let cx = 0;
        let cy = 0;
        for (let k = 0; k < HULL_POINTS; k += 1) {
            cx += j.px[k];
            cy += j.py[k];
        }
        cx /= HULL_POINTS;
        cy /= HULL_POINTS;
        let num = 0;
        let den = 0;
        for (let k = 0; k < HULL_POINTS; k += 1) {
            const dx = j.px[k] - cx;
            const dy = j.py[k] - cy;
            num += j.qx[k] * dy - j.qy[k] * dx;
            den += j.qx[k] * dx + j.qy[k] * dy;
        }
        j.cx = cx;
        j.cy = cy;
        j.theta = Math.atan2(num, den);
    function stepJelly(j) {
        pose(j);
        // the rest shape is set a little less tilted than the current angle — that gap is the force that stands the letter up
        const c = Math.cos(j.theta * (1 - UPRIGHT_PULL));
        const s = Math.sin(j.theta * (1 - UPRIGHT_PULL));
        const area = polyArea(j.px, j.py);
        const deficit = (j.area0 - area) / j.area0;
        for (let k = 0; k < HULL_POINTS; k += 1) {
            // shape spring — toward the rest shape moved into the current pose
            const gx = j.cx + j.qx[k] * c - j.qy[k] * s;
            const gy = j.cy + j.qx[k] * s + j.qy[k] * c;
            j.vx[k] += (gx - j.px[k]) * SPRING_STIFFNESS;
            j.vy[k] += (gy - j.py[k]) * SPRING_STIFFNESS + JELLY_GRAVITY;
        // squash cap — below the minimum height, stretch upward from the floor side (maxY) back to the minimum
        const minH = (1 - SQUASH_LIMIT) * j.ext[1];
        const hgt = maxY - minY;
        if (hgt < minH && hgt > 0.0001) {
            const k2 = minH / hgt;
            for (let k = 0; k < HULL_POINTS; k += 1) {
                j.py[k] = maxY - (maxY - j.py[k]) * k2;
                if (j.vy[k] > 0) {
                    j.vy[k] *= 0.5;
                }
            }
        }

04Letters That Dodge the Pointer and Spring Back Home

Inside a 90px radius around the pointer the letters are shoved away, then a spring of strength 0.05 pulls them home, and the farther a letter strays, the farther its shadow moves and the bigger the letter grows, up to 1.12 times. The push is 44 × (1/distance − 1/90), so it fades to 0 at the rim of the radius, and any distance under 12px is read as 12, which keeps the force finite when the pointer crosses the middle of a letter. Chasing the figure-eight virtual pointer, the middle letter moved as far as 63.1px and the two end letters 8.9px, and once the pointer left, every letter was back within 0.5px at 624ms and stayed there.

Letters That Dodge the Pointer and Spring Back Home
Open in a new tab

04 · Letters That Dodge the Pointer and Spring Back Home — Measured area 52.056% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

REPEL_RADIUSRETURN_SPRINGDAMPING
    function step() {
        for (let i = 0; i < movers.length; i += 1) {
            const m = movers[i];
            let ax = -(m.x - m.g.hx) * RETURN_SPRING;
            let ay = -(m.y - m.g.hy) * RETURN_SPRING;
            if (pointer.on) {
                const dx = m.x - pointer.x;
                const dy = m.y - pointer.y;
                const d = Math.max(Math.hypot(dx, dy), MIN_DIST);
                if (d < REPEL_RADIUS) {
                    const f = REPEL_FORCE * (1 / d - 1 / REPEL_RADIUS);
                    ax += (dx / d) * f;
                    ay += (dy / d) * f;
                }
            }
            m.vx = (m.vx + ax) * (1 - DAMPING);
            m.vy = (m.vy + ay) * (1 - DAMPING);
            m.x += m.vx;
            m.y += m.vy;
            // tilt — leans with the sideways speed and stands straight again once it stops
            m.a = lerp(m.a, clamp(m.vx * 0.07, -0.5, 0.5), 0.2);
        }
    }

05Letters That Float by How Much of Them Sits Underwater

Letters drop onto a surface cut into 61 columns 8px apart, raise waves, and float up with a buoyancy proportional to how many of 20 sample points inside each box sit below the surface. Each column is pulled back in proportion to its height, and twice per tick it hands 0.24 of its height difference to each neighbor, so a single bump spreads up to two columns per tick. The letters entered the water between 736 and 928ms at 13.16 to 17.34px per tick, the surface dipped 23.9px at its deepest, and at one moment 78 droplets were in the air.

Letters That Float by How Much of Them Sits Underwater
Open in a new tab

05 · Letters That Float by How Much of Them Sits Underwater — Measured area 27.119% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

WATER_DENSITYWAVE_SPREADSPLASH_FORCE
    function stepWater() {
        for (let k = 0; k < COLS; k += 1) {
            colV[k] += -WAVE_K * colH[k] - WAVE_DAMP * colV[k];
            colH[k] += colV[k];
        }
        // neighbor spread — two passes let a ripple reach two columns per tick
        for (let pass = 0; pass < 2; pass += 1) {
            const lD = [];
            const rD = [];
            for (let k = 0; k < COLS; k += 1) {
                lD.push(k > 0 ? WAVE_SPREAD * (colH[k] - colH[k - 1]) : 0);
                rD.push(k < COLS - 1 ? WAVE_SPREAD * (colH[k] - colH[k + 1]) : 0);
            }
            for (let k = 0; k < COLS; k += 1) {
                if (k > 0) {
                    colV[k - 1] += lD[k];
                    colH[k - 1] += lD[k];
                }
                if (k < COLS - 1) {
                    colV[k + 1] += rD[k];
                    colH[k + 1] += rD[k];
                }
            }
        }
    }

06Letters That Shatter Into Shards on a Hard Hit

Letters fall alternately from high up and from 60px above the floor, and the first and third, which landed at 18.5 and 19.5px per tick, shattered while the other two at 8.0px per tick stayed under the threshold of 9 and only bounced. At setup each letter is drawn on an offscreen canvas and its pixels are split by nearest seed point among 12 seeds; when a letter breaks, each shard flies off in the direction its own center sits from the letter's center, at a speed proportional to the impact. There are 12 seeds, yet the split made at setup cut the first letter into 10 shards and the fourth, which only bounced, into 11, and the fastest shard spin was 0.299rad per tick, inside the 0.32 cap.

Letters That Shatter Into Shards on a Hard Hit
Open in a new tab

06 · Letters That Shatter Into Shards on a Hard Hit — Measured area 21.919% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

SHARD_COUNTIMPACT_SPEEDSHARD_SPIN
        const src = octx.getImageData(0, 0, side, side);
        const rng = makeRng(seed);
        const seeds = [];
        for (let k = 0; k < SHARD_COUNT; k += 1) {
            seeds.push([side / 2 + (rng() - 0.5) * g.w * SHARD_SCALE, side / 2 + (rng() - 0.5) * g.h * SHARD_SCALE]);
        }
        const label = new Int16Array(side * side).fill(-1);
        const box = [];
        for (let k = 0; k < SHARD_COUNT; k += 1) {
            box.push([side, side, -1, -1]);
        }
        for (let y = 0; y < side; y += 1) {
            for (let x = 0; x < side; x += 1) {
                const p = y * side + x;
                if (src.data[p * 4 + 3] < 8) {
                    continue;
                }
                let best = 0;
                let bd = 1e12;
                for (let k = 0; k < SHARD_COUNT; k += 1) {
                    const dx = x - seeds[k][0];
                    const dy = y - seeds[k][1];
                    const d = dx * dx + dy * dy;
                    if (d < bd) {
                        bd = d;
                        best = k;
                    }
                }
                label[p] = best;

07Letters Sliding to One Side as Their Box Tilts

Six tiles in a box that rocks up to 28° each way slide and crowd toward the low side, then cross back over when the tilt flips. Rather than rotate the box, the code rotates gravity as seen from inside it to (0.42 sin θ, 0.42 cos θ), pushes overlapping tiles apart along the shallower axis by 0.5 of the depth shared between the two over four passes, and applies friction once per tick after the passes end, as × 0.96 on each touching tile, a 4% slowdown per tick. The tiles' average horizontal offset reached 69.4px at a 25.9° tilt and −56.7px at −25.9°, the first wall touch came at 960ms, and leftover overlap stayed within 0.28px.

Letters Sliding to One Side as Their Box Tilts
Open in a new tab

07 · Letters Sliding to One Side as Their Box Tilts — Measured area 26.91% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TILT_ANGLE_MAXCOLLISION_PUSHSLIDE_FRICTION
    function step() {
        tilt = tiltNow();
        const th = tilt * DEG;
        // gravity seen from inside the box — turn the world's down direction back by the box angle
        const gx = TILE_GRAVITY * Math.sin(th);
        const gy = TILE_GRAVITY * Math.cos(th);
        for (let i = 0; i < tiles.length; i += 1) {
            const t = tiles[i];
            t.floor = false;
            t.wall = false;
            t.vx += gx;
            t.vy += gy;
            t.x += t.vx;
            t.y += t.vy;
        }
        for (let it = 0; it < PUSH_ITERATIONS; it += 1) {
            for (let i = 0; i < tiles.length; i += 1) {
                for (let j = i + 1; j < tiles.length; j += 1) {
                    pushApart(tiles[i], tiles[j]);
                }
            }
            for (let i = 0; i < tiles.length; i += 1) {
                keepInBox(tiles[i]);
            }
        }
        // friction — once per tick, however many push passes ran. Resting on the floor or a tile below drains sideways speed; touching a wall or a side tile drains vertical speed
        for (let i = 0; i < tiles.length; i += 1) {
            const t = tiles[i];
            if (t.floor) {
                t.vx *= 1 - SLIDE_FRICTION;
            }
            if (t.wall) {
                t.vy *= 1 - SLIDE_FRICTION;
            }
        }
    }

08Letters Drifting Down and Fluttering Like Leaves in the Wind

Letters are pushed by a wind whose strength changes with height, tilting left and right and sliding off the way a leaf does until they come to rest on the floor. The wind comes from value noise fed with height × 0.011 and time × 0.0006, drag is speed × velocity × 0.02 so it climbs faster than the speed does, and the tilt chases a target angle of 0.5 times the sideways speed, but the angular velocity carries inertia, so the tilt lags behind, overshoots and swings back. The fastest fall, 3.16px per tick, matches the windless terminal speed √(0.2/0.02) worked out by hand, the tilt peaked at 69.2°, and the first letter reversed its sideways direction 10 times.

Letters Drifting Down and Fluttering Like Leaves in the Wind
Open in a new tab

08 · Letters Drifting Down and Fluttering Like Leaves in the Wind — Measured area 22.611% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WIND_STRENGTHDRAG_COEFFFLUTTER_RATE
    // value noise — joins two neighboring lattice values with a smooth curve
    function valueNoise(u) {
        const i = Math.floor(u);
        return lerp(latticeValue(i), latticeValue(i + 1), smooth(u - i));
    }
    function windAt(y, t) {
        return (valueNoise(y * WIND_SCALE_Y + t * WIND_SPEED) * 2 - 1 + WIND_BIAS) * WIND_STRENGTH;
    }
        const speed = Math.hypot(l.vx, l.vy);
        const ax = windAt(l.y, age) - DRAG_COEFF * speed * l.vx - Math.sin(l.a) * GLIDE * LEAF_GRAVITY;
        const ay = LEAF_GRAVITY - DRAG_COEFF * speed * l.vy;
        l.vx += ax;
        l.vy += ay;
        // tilt — trails a target angle set by the sideways speed (angular speed has inertia, so it overshoots and swings back)
        l.w += (l.vx * FLUTTER_RATE - l.a) * TILT_FOLLOW - l.w * TILT_DAMPING;
        l.a += l.w;
        l.x += l.vx;
        l.y += l.vy;

09A Hero Headline That Collapses, Then Lines Back Up

The headline gives way at 208ms and scatters across the floor through the same box math as 01, and from 0.9 seconds on, each letter sets off home 45ms after the one before it, in reading order. The return uses a spring with k at 0.03 whose damping is set to 2√k, so the overshoot above its spot measured 0px, and the angle snaps to the nearest full turn, so a letter that spun several times does not unwind every one of those turns. At the collapse the line error reached 110.6px, the first letter started home at 1,120ms, and the average error across the letters stayed within 0.5px from 1,984ms on.

A Hero Headline That Collapses, Then Lines Back Up
Open in a new tab

09 · A Hero Headline That Collapses, Then Lines Back Up — Measured area 26.865% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

ASSEMBLE_DELAYSETTLE_SPRINGLOOP_SECONDS
    function collapse() {
        const rng = makeRng(4729);
        fallAt = age;
        for (let i = 0; i < bodies.length; i += 1) {
            const b = bodies[i];
            b.free = true;
            b.vx = (rng() - 0.5) * 3;
            b.vy = -2 - rng() * 3;
            b.w = (rng() - 0.5) * 0.24;
        }
    }
    // one critically damped spring step — accel = -strength x offset - 2 x sqrt(strength) x velocity
    function springTo(v, x, target) {
        return v - SETTLE_SPRING * (x - target) - 2 * Math.sqrt(SETTLE_SPRING) * v;
    }

Why a Pile of Letters Breaks Apart: Crossed Boxes, Fast Falls, Idle Tabs and Uneven Widths

When a drop text animation of your own starts misbehaving, these are the spots to check first. The numbers here came from calling the source functions directly, and cases that were not actually run are marked as likely.

A corner-only check and two boxes crossed like a plus sign

01 finds contacts by asking one thing only: is a box corner inside another box? When two boxes overlap in a plus shape and no corner of either sits inside the other, the check finds no contact between them. The layout pulls the letters' horizontal spots toward the center by 0.28 and keeps their tilt within ±0.4rad, so they come down corner first onto one another.

Why one tick is split into two moves

Raise gravity to 1.1 and a falling letter's vertical speed passes 20px per tick. Moving that far in one jump would sink it close to half a box height into its neighbor within a single tick, which would likely flip the push direction and fire the letter off the wrong way. SUBSTEPS 2 cuts each tick in two and makes each move sink less deeply.

Stronger gravity, longer jitter

At gravity 0.45 the pile went still at 2,688ms, but at 1.1 it kept twitching until 9,328ms. The first floor touch came sooner, at 576ms instead of 768ms, yet the letters on top press harder on the ones below and likely keep trading small rebounds far longer. The pile also came out a little lower, 73.7px instead of 77.1px.

Coming back to a tab left idle

Return from another tab after tens of seconds and a frame still catches up only 100ms, six steps. The letters do not teleport to the floor; they carry on falling from the pose they held when you left.

Uneven English letter widths

The Korean and Japanese stages get boxes of equal width, but STACK IT UP mixes a 19.2px I with a 41.3px A. Narrow boxes are light and have a short base, so they likely topple more easily on top of the pile, and the 0.34 minimum width likely exists to stop even thinner glyphs from turning into sticks.

More rope segments turn a rope into a rubber band

Raise the segment count in 02 from 6 to 14 while keeping 8 fixes, and the rope stretches to 15.77% instead of 2.57%. One fix only corrects two neighboring points, so with more segments the tick likely ends before the hook's hold reaches the far end. With air damping at 0 this integration loses no energy, and the swing would likely go on and on.

A jelly whose outline points are not centered

03 moves the average of its 28 outline points to the origin before using them as the rest shape. When that average sat off zero during the build, the shape spring became a constant force on the whole body, and a falling jelly stopped gaining speed and drifted off to one side. With UPRIGHT_PULL at 0, letters with a lopsided base rolled almost onto their side, which the render showed as well.

A soft spring hides the speed difference

Lower the shape spring from 0.45 to 0.16 and an impact at 6.04px per tick already squashes to 0.603, while one at 13.2px per tick gives 0.584, both pinned at the 0.58 floor. For the squash to tell you the speed, the spring has to hold out before that floor is reached.

Seeds that land where there is no stroke

06 scatters its 12 seeds at random inside the glyph box, so a seed that lands in an empty spot with no stroke collects no pixels and yields no shard. Shards do not collide with each other and stack through one another, and the split happens once at setup, so changing the text size means rebuilding the shards.

Friction placed inside the push loop

While 07 was being built, friction first sat inside the push passes and was applied four times per tick, and the tiles barely slid even with the box leaning far over. Moving it out of the loop, so it applies once per tick, let the tiles follow the tilt. Spinning the box fast still adds no centrifugal effect on the tiles, only a change in gravity's direction, which is a limit of this approximation.

Lower angular damping, upside-down leaves

Cut the angular damping in 08 from 0.06 to 0.03 and the letters tilted as far as 146.3°, close to upside down. Since drag grows with the square of speed, halving DRAG_COEFF would likely raise the terminal speed by √2.

The 0.05 staircase from 20 sample points

With the water density in 05 at 1.7, a floating letter should in theory sit 1/1.7 = 0.588 under. Counting with 20 sample points makes the share move in steps of 0.05, so after a long wait the letters read 0.50, 0.75, 0.75, 0.55 and 0.50, and they likely bob slightly between steps. Within 900 ticks (14,400ms), the moment when every letter sat in the water with vertical speed under 0.15 first came on the very last tick, which shows how long they keep rocking with the waves.

Returning letters that pass through each other

In 09 a letter that starts heading home leaves the collision step, so it slides straight through letters still lying on the floor. Without the line that snaps the angle to the nearest full turn, a letter that spun several turns while falling would unwind each of them, and damping under 2√k would likely make letters overshoot their spots and wobble.

To try all nine with a headline of your own, the archive holds the nine vanilla folders plus React versions that port the same math, and the password that opens it is f3yv8pu7.

How Gravity, Bounce and Spring Strength Reshape the Pile of Letters

Change Value What you see
Heavier gravity 01 GRAVITY 0.45 → 1.1 The first letter lands at 576ms instead of 768ms and hits harder, and the fastest spin rises from 16.7° to 21.6° per tick. Stacked letters jitter longer, so the pile takes 9,328ms instead of 2,688ms to go still
More rope segments 02 ROPE_SEGMENTS 6 → 14 With the 8 fixes unchanged, stretch grows from 2.57% to 15.77% and the ropes sag. The last letter's widest lean grows from 33.6° to 39.1°
Wider dodge radius 04 REPEL_RADIUS 90 → 160 The end letters move 12.5px instead of 8.9px, and with the pointer parked 40px above the middle letter, its neighbors get pushed 4.6px instead of 0.7px. After the pointer leaves, letters slip out of the home zone once and come back, so the moment they stay put moves from 624ms to 736ms

On the bounce side, RESTITUTION 0.32 in 01 is the share of incoming speed sent back, so raising it would likely keep letters hopping longer before they pile up. Contacts slower than 1.2px per tick target 0 either way, so the slow contacts inside a settled pile get no rebound. Spring strength means different things in 03 and 09: the 0.45 in 03 holds the shape, so lowering it pins the squash at the floor value, while the 0.03 in 09 pulls letters home, and since the source derives damping as 2 * Math.sqrt(SETTLE_SPRING), raising it likely raises damping too and lines the headline up sooner.

Falling Letters Still Need to Be Read — Screen Reader Text and Reduced-Motion Accessibility

The words live in one line per cell, a <p class="pc__sr" data-role="word">. That paragraph shrinks to a 1px box hidden with clip-path: inset(50%), so only screen readers pick it up, and because the script reads the same text and paints it on the canvas as loose glyphs, the scattered letters stay out of the accessibility tree. The canvas aria-label describes the scene and its controls without repeating the words, so STACK IT UP is read once, not twice.

The canvas carries tabindex="0", so keyboard focus can land on it, and focus-visible draws a focus ring on both the canvas and the buttons. The status line starts as aria-live="off" so the demo captions are not read aloud, turns polite on the first input, and rewrites its text only when the message changes. On phones, touch-action: pan-y keeps vertical scrolling working even with a finger on the canvas.

When prefers-reduced-motion is set to reduce, the rAF loop does not start; settle() computes 360 steps (5,760ms worth) in one pass without drawing any of them and paints only the settled result. After a press or a key, after() reruns the same calculation and shows the final layout that input leads to, and none of the nine sources contains a setTimeout. Each of the nine scenes attaches a change listener to matchMedia, so flipping the setting with the page open switches the drawing mode from the next moment on.

Time-driven touches are stopped one by one. The virtual pointer in 04 runs only in the demo and does not appear under reduce, the automatic tilt in 07 holds at 16.8°, and the collapse that 09 repeats on its own every 3.4 seconds does not run. The status lines in 02, 03, 05 and 07 switch to messages that report the settled state, such as The swing has settled, Settled softly, Floating on the water and Held at a tilt.

Physics Letters in a Real Headline: Three Questions Before You Ship

Where do I swap the stage words for my own headline?

Change only the text inside the hidden <p data-role="word"> in each cell. The script reads it at startup and layoutWord measures it again, so a longer line shrinks evenly to fit the stage width. Screen readers read your new words as written.

Do javascript text animation examples like these need a physics engine library?

None of these nine loads a library, physics engine, WebGL or CDN, and the whole calculation fits in one index.html per folder. The trade-offs are that 01 checks only corner contacts, the letters in 03 do not collide with each other, and the tiles in 07 do not rotate. If dozens of letters have to roll and lock together, that is the point to pick an engine, and for physics scenes built from shapes, the physics simulation post picks up from here.

Is this heavy on a phone?

Canvas pixels follow the device pixel ratio only up to 2, and one tick works on a handful of letters plus a few dozen contacts, outline points or samples. The heavier jobs are the split in 06, which measures every glyph pixel against 12 seeds, and the outline ray scan in 03, and both run once at setup. Frame times were not measured per device, so if older phones matter to you, opening the page on one is the reliable check, and if you would rather pull glyphs apart into pixel particles, see the glyph pixel sampling post.

Enter the archive password

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