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

Falling Leaves Animation: 9 Scenes, Sway to Pile

In a falling leaves animation, drop a leaf straight down and it reads as a pebble.

From the Branch to the Ground — Wind, Light, Color and the Pile, Each Computed on Its Own

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

The nine scenes draw every leaf on a 2D canvas from a 72-point outline and a set of veins, with no photos or image files, and a fixed seed makes each one replay the same layout and the same wind by itself. A hand on the board opens an extra path: 01 blows a 160 px/s side wind within 80 px of the pointer, 03 raises a 220 px/s gust within 70 px, and in 07 the broom leaves its set route to follow the pointer. Canvas coordinates stay at 480×300, and the real pixel count is multiplied by the screen's pixel ratio up to a cap of 2.

Autumn Event Banners and the Opening Screen of a Seasonal Landing

For the opening screen of a fall campaign, 09 fits best: front-layer leaves come down large and sharp while far-layer leaves fall small and soft, so a headline set over it gains depth behind the letters. A narrow seasonal event banner suits the eight leaves of 02, which flip and change brightness as they turn. If you came looking for autumn leaves css, keep in mind that the stylesheet here only sizes and frames the stage while the leaves themselves are computed on canvas, and 01, with just three leaves zigzagging down, is the quietest choice for a soft backdrop.

From a Footer Ornament to a Weather Screen

A seasonal footer ornament is where 06 belongs: the mound grows as leaves land and slides sideways once its flank gets too steep, so one strip at the bottom of the page holds a whole autumn scene. A weather app's wind view can take 03 with its passing gust band, a cleaning service page can use the broom in 07, an autumn greeting card can show 05 dropping one leaf at a time, an event opening can start with 08 lifting leaves in a whirlwind, and a product section that changes with the season can hold the single turning leaf of 04.

Why Does a Falling Leaves CSS Animation Look Like Dropping Pebbles When the Leaves Fall Straight?

A leaf is thin and wide, so as it falls the air pushes it sideways by an amount that depends on how far its body is tilted. A straight-down animation has no such side force, which puts it on the same path a heavy object would take, only at a different speed. The cherry petals in the CSS particle background post follow a route set in keyframes, and the falling text post shook letters in the wind like leaves; here 01 computes forces from the tilt so the route comes out of the math instead.

Sideways Slide from a Tilt

The angle of attack α is the angle between the leaf's face and the oncoming air. Lift scales with |sin α·cos α|, so it is largest at 45° and drops to 0 when the leaf lies flat or stands edge-on like a blade. A tilted leaf picks up speed as it slides, and since lift is multiplied by u², more speed means more lift; across the three leaves of 01 the lift peaks at 997, 994 and 934, and the angle of attack swings up to 88 to 90°.

A Righting Torque That Turns the Slide into a Sway

Sliding alone would carry the leaf off to one side. 01 pulls it back toward level with a torque of RIGHTING_K 14 times the sine of the tilt, and SPIN_DAMP 1.5 bleeds off the spin. When sin α changes sign on the way back, lift pushes the other way as well, and the horizontal speed reverses 4 to 5 times in a zigzag. With the lift gain lowered to 0.6 the side swing shrinks to ±1.6 to 5.5 px and the leaf drops almost straight, which shows from the opposite side why a straight fall feels wrong.

Tilt Becomes Lift, Wind Comes from Noise, Color Follows Vein Distance — the Math Behind One Leaf

The nine scenes share four bundles of math. 01, 05 and 09 use the flutter bundle, 05 and 09 the stem spring, 03, 05 and 09 the wind, and 06 and 09 the pile, while 02, 04, 07 and 08 run on their own code alone. Each bundle reacts to the state of a leaf at that instant, which is what a javascript falling leaves animation adds over a path fixed in advance. Here is how the flutter and wind bundles, plus the code of 04 itself, work out in numbers.

Lift Follows sin 2α, Drag Follows sin²α

Drag is AIR_K 0.075 × u² × (0.1 + sin²α), so it grows as the leaf meets the air face-on, while lift takes the same u² and multiplies it by LEAF_LIFT_GAIN 1.6 and |sin α·cos α|. Because |sin α·cos α| equals ½·|sin 2α|, lift tops out at 45°, half of the 90° where drag is largest. Drag points against the air velocity and lift points perpendicular to it; the drag peaks in 01 are 482, 480 and 456, roughly half the lift peaks.

Wind That Differs by Place, and a Band That Sweeps Through

The wind in 03 is smooth noise: values sit on an integer grid and a cubic curve joins them. With WIND_SCALE 0.007 it takes, by calculation, 1/0.007 ≈ 143 px to cross one grid cell, and WIND_DRIFT 0.4 slides the wind pattern sideways as time passes. Horizontal wind is a base of 34 plus noise × 46, and the gust band's center follows time × GUST_SPEED modulo the board width plus six band widths plus a gap of 420, so it crosses from left to right over and over. The drift in the soap bubble post also rides noise wind; what sets this scene apart is the band, a front crossing the screen that shoves only the leaves inside it.

Vein Distance Sets the Order of Turning

For each pixel inside the leaf, 04 measures the distance to the nearest vein segment, divides by the maximum to land on 0 to 1, and uses 0.66 × distance + 0.34 × blotch noise as the order in which that pixel turns. Order values spread from 0.01 to 0.881, and as a falling threshold passes them, the edges cross first and change color first. Unlike the generative art post, which grows its veins as it draws, this scene fixes the veins in advance and uses only the distance to them.

Falling Leaves Animation: 9 Scenes — Lift Flutter, Three-Axis Tumble, Gust Band, Color Turn, Stem Snap, Slumping Pile, Broom Wind, Whirlwind and an Autumn Tree

Each demo ran for 2 seconds while 24 frames were captured, and the 23 differences between neighboring frames gave the changed area and the intensity. 03, with 140 leaves streaming across the whole board, covers the widest share at 71.5%, and 05, where six stems sway before dropping their leaves, covers the narrowest at 8.5%. 06 moved in 22/23 frames and the other eight in 23/23.

Changed Area, Frames and Intensity per Scene

Scene Changed area (cumulative) Frames that moved Mean intensity
01 Lift flutter 13.5% 23/23 91.5
02 Three-axis tumble 29.3% 23/23 70.0
03 Gust band 71.5% 23/23 83.6
04 Color turn 22.3% 23/23 29.1
05 Stem snap 8.5% 23/23 67.5
06 Slumping pile 12.9% 22/23 84.6
07 Broom wind 31.2% 23/23 54.2
08 Whirlwind 20.8% 23/23 51.2
09 Autumn tree 34.9% 23/23 69.1

The whirlwind in 08 works from a velocity field, as the pointer swirl of light dust in the aurora background post does, but it adds solid rotation at the core, falloff outside it and an upward draw that lifts the leaves into a 3D spiral.

01A Leaf That Glides Sideways and Sways as It Tilts

Three leaves start at different tilts and come down in a zigzag as lift from the angle of attack pushes them sideways and a righting torque pulls the tilt back. flutterStep below subtracts the wind from the leaf velocity to get the air velocity and splits it along the leaf's face and its normal to find drag and lift separately, and the first leaf draws both forces as arrows. Running the source function unchanged, the three leaves swing 26.6 to 28.9 px left and 22.6 to 34.2 px right of where they started and reach the ground at 3,872 to 4,912 ms.

A Leaf That Glides Sideways and Sways as It Tilts
Open in a new tab

01 · A Leaf That Glides Sideways and Sways as It Tilts — Measured area 13.519% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LEAF_LIFT_GAINRIGHTING_KTWIST_GAIN
    function flutterStep(f, windX, windY, dt, liftGain) {
        const rx = f.vx - windX;
        const ry = f.vy - windY;
        const u2 = rx * rx + ry * ry;
        const u = Math.sqrt(u2);
        let ax = 0;
        let ay = GRAVITY;
        let torque = -RIGHTING_K * Math.sin(f.tilt);
        f.lift = 0;
        f.drag = 0;
        if (u > 1e-6) {
            const tx = Math.cos(f.tilt);
            const ty = Math.sin(f.tilt);
            const nx = -ty;
            const ny = tx;
            const cosA = (rx * tx + ry * ty) / u;
            const sinA = (rx * nx + ry * ny) / u;
            f.aoa = Math.atan2(Math.abs(sinA), Math.abs(cosA));
            const drag = AIR_K * u2 * (DRAG_BASE + sinA * sinA);
            const lift = liftGain * AIR_K * u2 * Math.abs(sinA * cosA);

02A Leaf Tumbling on Three Axes with Front and Back Shading

Eight leaves each spin on three axes, shrinking and growing by the perspective factor FOCAL_LENGTH / (FOCAL_LENGTH + z), and when a flip turns the normal's z positive the leaf switches to a back color that mixes its own color half and half with cream. The snippet below moves the 72 outline points to the screen, then sets brightness to 0.32 + 0.68 × the dot product of the visible face's normal and LIGHT_DIR, with light arriving from the upper left front. Over 6 seconds each leaf flips between front and back 6 to 8 times and the perspective factor ranges from 0.757 to 1.232.

A Leaf Tumbling on Three Axes with Front and Back Shading
Open in a new tab

02 · A Leaf Tumbling on Three Axes with Front and Back Shading — Measured area 29.32% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FOCAL_LENGTHLIGHT_DIRTUMBLE_RATES
        function toScreen(u, v) {
            const q = apply(m, u * half, v * half, 0);
            const z = cz + q[2];
            const k = FOCAL_LENGTH / (FOCAL_LENGTH + z);
            return [W / 2 + (cx - W / 2 + q[0]) * k, H / 2 + (p.y - H / 2 + q[1]) * k];
        }
        for (let i = 0; i < outline.length; i += 1) {
            pts.push(toScreen(outline[i][0], outline[i][1]));
        }
        const veins = VEINS[p.kind].map(function (s) { return [toScreen(s[0][0], s[0][1]), toScreen(s[1][0], s[1][1])]; });
        depth = cz;
        const n = apply(m, 0, 0, -1);
        // A negative normal z means the front face points at the viewer. Dot the visible face's normal with the light
        const front = n[2] < 0;
        const s = front ? 1 : -1;
        const lit = Math.max(0, s * (n[0] * LIGHT_DIR[0] + n[1] * LIGHT_DIR[1] + n[2] * LIGHT_DIR[2]));
        const base = front ? p.color : backColor(p.color);
        const bright = AMBIENT_LIGHT + (1 - AMBIENT_LIGHT) * lit;
        const fill = mixRGB(mixRGB(base, INK, 0.36 * (1 - bright)), CREAM, 0.34 * lit * lit);
        return { pts: pts, veins: veins, depth: depth, lit: lit, front: front, fill: fill, leaf: p };

03A Gust Band That Sweeps a Drift of Leaves

140 leaves follow a place-by-place wind set by smooth noise, and while the peach gust band passes, only the leaves inside it get shoved hard and swept to one side. gustWeight is a bell-shaped width that is 1 at the band center and 1/e at GUST_WIDTH 64 px away, and windAt multiplies it by GUST_GAIN 300, adds it to the horizontal wind and lifts the leaves slightly on the vertical axis. A test leaf placed mid-board at x 200 spends 1,232 ms inside the band, its sideways speed reaches 275 px/s, and it covers 329 px in 2.8 seconds, against 61.7 px with no gust.

A Gust Band That Sweeps a Drift of Leaves
Open in a new tab

03 · A Gust Band That Sweeps a Drift of Leaves — Measured area 71.459% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GUST_SPEEDGUST_WIDTHWIND_COUPLING
    // Bell-shaped width — 1 at the band center, 1/e at GUST_WIDTH away
    function gustWeight(x, center) {
        const k = (x - center) / GUST_WIDTH;
        return Math.exp(-k * k);
    }
    // Wind (px/s) at position (x, y) and time t (s) — out[0] horizontal, out[1] vertical (down is +)
    function windAt(x, y, t, center, out) {
        const n1 = windNoise(x * WIND_SCALE - t * WIND_DRIFT, y * WIND_SCALE);
        const n2 = windNoise(x * WIND_SCALE + 31.7, y * WIND_SCALE - t * WIND_DRIFT * 0.7);
        const g = gustWeight(x, center);
        out[0] = WIND_BASE + WIND_SWIRL * n1 + GUST_GAIN * g;
        // Inside the gust, lift slightly upward
        out[1] = WIND_SWIRL * 0.6 * n2 - GUST_GAIN * 0.18 * g;
    }

04Autumn Color That Creeps In from the Edges Away from the Veins

One five-lobed leaf turns over 4,200 ms from green to butter, apricot and maple, starting at the edges farthest from the veins, while blotch noise roughens the boundary. paintField below moves a single threshold per frame, th = 1 − p·(1 + RAMP_WIDTH), and compares it with each pixel's precomputed order value to pick a ramp color. Of the 35,352 pixels inside the leaf, the first to turn fully red does so at 1,796 ms and the last at 4,371 ms, while the 1,338 pixels near the veins average an order of 0.18 and turn late.

Autumn Color That Creeps In from the Edges Away from the Veins
Open in a new tab

04 · Autumn Color That Creeps In from the Edges Away from the Veins — Measured area 22.259% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RAMP_WIDTHBLOTCH_MIXFIELD_SCALE
    function paintField(p) {
        if (!fieldCv || !fieldImg) {
            return;
        }
        const d = fieldImg.data;
        const th = 1 - p * (1 + RAMP_WIDTH);
        for (let i = 0; i < insideIdx.length; i += 1) {
            const s = clamp((turnOrder[i] - th) / RAMP_WIDTH, 0, 1);
            const c = mixRGB(rampColor(s), INK, 0.34 * veinNear[i]);
            const o = insideIdx[i] * 4;
            d[o] = c[0];
            d[o + 1] = c[1];
            d[o + 2] = c[2];
            d[o + 3] = 255;
        }
        rampCompares += insideIdx.length;
        lastCompares = insideIdx.length;
        const g = fieldCv.getContext('2d');
        if (g) {
            g.putImageData(fieldImg, 0, 0);
        }
    }

05A Leaf That Snaps Off Once Its Stem Wears Out

Six leaves hang from a branch and bend in turn as the gust band crosses from left to right, and a ring around each stem fills by fatigue over its limit, turning maple once it passes 80%. stemStep integrates an angular spring with stiffness STEM_K 70 and damping STEM_DAMP 2.2 and accumulates the square of the bend angle as fatigue, and when that crosses the stem's own limit in STEM_LIMITS the leaf breaks free with its current velocity and falls through the flutterStep of 01. Taken in x order the stems snap at 1,120, 7,280, 1,728, 2,128, 2,304 and 8,240 ms, so four fall on the first gust and two on the next.

A Leaf That Snaps Off Once Its Stem Wears Out
Open in a new tab

05 · A Leaf That Snaps Off Once Its Stem Wears Out — Measured area 8.544% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

STEM_KFATIGUE_RATESTEM_LIMITS
    function stemStep(s, windX, dt) {
        if (s.snapped) {
            return false;
        }
        const bend = s.ang - s.rest;
        const torque = -Math.sin(s.ang) * windX * STEM_WIND;
        s.angVel += (-STEM_K * bend - STEM_DAMP * s.angVel + torque) * dt;
        s.ang += s.angVel * dt;
        s.fatigue += bend * bend * FATIGUE_RATE * dt;
        if (s.fatigue >= s.limit) {
            s.snapped = true;
            return true;
        }
        return false;
    }

06A Leaf Pile That Slumps When Its Slope Gets Too Steep

The floor is split into 40 cells of 12 px, 160 leaves already lie on it, 440 more pour in around x 236 at 80 per second, and dashed lines mark the limiting slope on both sides of the peak. The snippet below is the body of heapRelax: for each cell it moves the top leaf toward the steeper neighbor each time (height step × LEAF_THICK 2.6) ÷ cell width passes the slope limit, and a moved leaf slides from its old spot to its new one over 260 ms. Once the pour ends, the 600 leaves stand as a mound 124.8 px tall and 300 px wide after 1,571 slides, and the steepest neighbor step left is 0.867, just under the 0.9 limit.

A Leaf Pile That Slumps When Its Slope Gets Too Steep
Open in a new tab

06 · A Leaf Pile That Slumps When Its Slope Gets Too Steep — Measured area 12.862% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

REPOSE_SLOPECELL_WPOUR_RATE
                const h = hp.cols[c].length;
                let best = -1;
                let bestDrop = 0;
                for (let k = 0; k < 2; k += 1) {
                    const nb = c + ((k + pass) % 2 === 0 ? -1 : 1);
                    if (nb < 0 || nb >= n) {
                        continue;
                    }
                    const drop = (h - hp.cols[nb].length) * LEAF_THICK / hp.cellW;
                    if (drop > slope && drop > bestDrop) {
                        best = nb;
                        bestDrop = drop;
                    }
                }
                if (best < 0) {
                    continue;
                }
                const leaf = hp.cols[c].pop();
                leaf.fromX = heapSlotX(hp, c, leaf);
                leaf.fromY = heapSlotY(hp, h - 1);
                leaf.slideAge = 0;
                hp.cols[best].push(leaf);
                moved += 1;
                changed = true;

07A Yard Where a Broom's Wind Scatters the Leaves

Seen from above, 150 leaves cover a yard while a broom travels a 530 px route with two bends at 170 px/s and pushes away the leaves within 66 px. The weight w = (1 − d/R)² falls to 0.25 at half the radius, the impulse blends 0.55 of the outward direction with 0.45 of the heading, and the cross product of heading and outward direction spins leaves struck from the side the hardest. In 2 seconds 59 leaves get hit and 102 by the end of the route, a hit leaf moves 34.6 px on average, and spin peaks at 9.2 rad/s.

A Yard Where a Broom's Wind Scatters the Leaves
Open in a new tab

07 · A Yard Where a Broom's Wind Scatters the Leaves — Measured area 31.231% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BROOM_RADIUSPUSH_GAINSPIN_GAIN
        for (let i = 0; i < yard.length; i += 1) {
            const lf = yard[i];
            const dx = lf.x - broomX;
            const dy = lf.y - broomY;
            const d = Math.hypot(dx, dy);
            if (d < BROOM_RADIUS && mv > 0.01) {
                const w = (1 - d / BROOM_RADIUS) * (1 - d / BROOM_RADIUS);
                const ax = d > 0.01 ? dx / d : headX;
                const ay = d > 0.01 ? dy / d : headY;
                // Push direction = the outward direction blended with the broom's heading
                const jx = (0.55 * ax + 0.45 * headX) * w * PUSH_GAIN * dt;
                const jy = (0.55 * ay + 0.45 * headY) * w * PUSH_GAIN * dt;
                lf.vx += jx;
                lf.vy += jy;
                // Cross product of heading and outward direction — a leaf hit on its side spins harder
                lf.spin += (headX * ay - headY * ax) * w * PUSH_GAIN * SPIN_GAIN * dt;
                lf.hop = Math.min(1, lf.hop + w * 4 * dt);
                if (!lf.hit) {
                    lf.hit = true;
                    hits += 1;
                }
            }

08Leaves Caught in a Whirlwind, Spiraling Up and Scattering

A crowd of 120 leaves is drawn toward the center of a whirlwind, spirals upward, then scatters outward and settles during the 1,600 ms fade. tangential is solid rotation 4.2·r inside radius 30 and 4.2·30²/r outside it, so the speed peaks at 126 px/s right at the boundary, and the updraft UPDRAFT 420 is multiplied by exp(−r/70) and (1 − height/LOFT_TOP) so it weakens toward a height of 230. At 2 seconds 63 leaves are aloft, at 3,696 ms all 120 are, and the mean radius widens from 96.8 px to 122.6 px once everything has settled.

Leaves Caught in a Whirlwind, Spiraling Up and Scattering
Open in a new tab

08 · Leaves Caught in a Whirlwind, Spiraling Up and Scattering — Measured area 20.78% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CORE_RADIUSUPDRAFTINFLOW
    function tangential(r) { return r < CORE_RADIUS ? SPIN_RATE * r : SPIN_RATE * CORE_RADIUS * CORE_RADIUS / r; }
    function step(dt) {
        t08 += dt * 1000;
        strength = swirlStrength(t08);
        const s = strength;
        for (let i = 0; i < swirlers.length; i += 1) {
            const p = swirlers[i];
            const onGround = p.h <= 0 && p.vh <= 0;
            // A leaf lying on the ground moves only when the wind is strong enough
            const grip = onGround ? clamp((s - 0.15) / 0.3, 0, 1) : 1;
            const vt = tangential(p.r) * s * grip;
            p.th += vt / Math.max(p.r, 4) * dt;
            const lift = UPDRAFT * s * Math.exp(-p.r / LIFT_REACH) * clamp(1 - p.h / LOFT_TOP, 0, 1);
            p.vh += (lift - SWIRL_SINK) * dt * 2;
            p.vh -= p.vh * 1.4 * dt;
            p.h += p.vh * dt;
            if (p.h < 0) {
                p.h = 0;
                p.vh = 0;
            }

09An Autumn Tree Scene with Size and Blur Set by Depth

Leaves are split across front, middle and far layers at sizes of 1, 0.62 and 0.38, blur of 0, 0.5 and 1.6, and speeds of 1, 0.72 and 0.45, and when a leaf snaps from the tree it rides the wind down to the pile while a new leaf is hung in its place. growTree below is a recursive two-way branching with its seed fixed at 47990, where depth 6, a 0.44 rad spread and a 0.74 length ratio give 63 branches and 32 tips, and since the tree is painted offscreen once, treeDraws still reads 1 after 750 frames. Within 2 seconds 6 leaves snap, within 12 seconds 14 do, and by then the pile holds 84 leaves.

An Autumn Tree Scene with Size and Blur Set by Depth
Open in a new tab

09 · An Autumn Tree Scene with Size and Blur Set by Depth — Measured area 34.949% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LAYER_SCALELAYER_BLURBRANCH_DEPTH
    function growTree() {
        segs = [];
        tips = [];
        const rng = makeRng(47990);
        function branch(x, y, ang, len, depth, w) {
            const x1 = x + Math.cos(ang) * len;
            const y1 = y + Math.sin(ang) * len;
            segs.push({ x0: x, y0: y, x1: x1, y1: y1, w: w });
            if (depth >= BRANCH_DEPTH) {
                tips.push([x1, y1]);
                return;
            }
            const j = (rng() - 0.5) * 0.18;
            branch(x1, y1, ang - BRANCH_SPREAD + j, len * BRANCH_SHRINK, depth + 1, w * 0.68);
            branch(x1, y1, ang + BRANCH_SPREAD + j, len * BRANCH_SHRINK * 0.94, depth + 1, w * 0.68);
        }
        branch(TREE_X, TREE_GROUND, -Math.PI / 2, TRUNK_LEN, 1, 12);
    }

Weaker Lift, Faster Gusts, a Lower Pile Slope — One Number That Changes the Autumn Wind

Each row changes one constant in the source and reruns the same function. The middle row deserves a second look: making the gust nearly twice as fast pushes the leaf less, because the time it rides inside the band drops from 1,232 ms to 368 ms.

Name Changed value Feel
Weaken the lift 01 LEAF_LIFT_GAIN 1.6 → 0.6 Peak lift falls from 934–997 to 54–160 and the side swing narrows from 26.6–28.9 px left and 22.6–34.2 px right to ±1.6–5.5 px, so the leaves drop almost straight. Landing shifts only from 3,872–4,912 ms to 3,776–4,560 ms
Speed up the gust 03 GUST_SPEED 220 → 420 The mid-board test leaf peaks at 177 px/s instead of 275, and its 2.8-second travel falls from 329 px to 195.7 px. The band returns every 3,057 ms instead of 5,836 ms, and the most leaves inside it at once drops from 104 to 61
Lower the pile slope 06 REPOSE_SLOPE 0.9 → 0.4 The slope lies down from 42° to 21.8°, the 600-leaf peak halves from 124.8 px to 62.4 px, and a pile that was 300 px wide spreads to the 480 px edge of the board. Slides more than double, from 1,571 to 3,416

Where Falling Leaves Break: Hundreds of Leaves, a Jump After Returning to the Tab, and Color Turning on High-Density Screens

Hundreds of Leaves

Joining 72 outline points into a path and stroking the veins for every leaf on every step would add up to 17,500 drawings in 2 seconds (125 steps of 16 ms) for 03 alone. Instead, the seven scenes other than 02 and 04 draw 15 sprites, 3 leaf shapes × 5 colors, offscreen once at the start, and from then on only stamp them with drawImage at a new position, scale, rotation and vertical squash. Measured stamps over those 2 seconds come to 17,500 for 03, 28,190 for 06, 18,750 for 07, 15,000 for 08 and 17,375 for 09, and new sprite drawings stay at 0 in all five (09 holds 35 sprites once its blur steps are counted). The leaf count is capped too: the 140 leaves in 03 wrap to the opposite edge when they leave the board and get reused 42 times in 2 seconds, and 09 hangs a fresh leaf where a fallen one left. All nine sources in plain JavaScript, with that leaf cap, the sprite cache and the fixed-size distance field in place, come in one zip along with component ports, and the archive opens once you enter fxr3gktp.

A Jump After Returning to the Tab

Hide a tab and come back, and the gap between the first frame and the one before it can stretch to several seconds. frame below adds that gap to the accumulator only up to DT_MAX 100 ms and runs at most 6 steps of 16 ms per frame.

    function frame(ts) {
        if (paused || !alive) {
            rafId = 0;
            return;
        }
        if (lastTs < 0) {
            lastTs = ts;
        }
        // dt cap — add at most DT_MAX per frame, so leaves do not teleport when a hidden tab returns after seconds
        acc += Math.min(ts - lastTs, DT_MAX);
        lastTs = ts;
        let n = 0;
        while (acc >= FIXED_DT && n < MAX_STEPS) {
            clock += FIXED_DT;
            if (clock >= loopLength()) {
                clock = 0;
                build();
            }
            step(STEP_S);
            acc -= FIXED_DT;
            n += 1;
        }
        render();
        rafId = window.requestAnimationFrame(frame);
    }

Feed it 40 frames at 16 ms and then one frame after a 5-second gap: with the caps in place that frame simulates only 96 ms and the first leaf of 01 moves 7 px. Without them, 5,008 ms run in one go, the leaf teleports 220.4 px and all three are already on the ground. 06 suffers more, pouring 8 leaves and landing 0 in that frame with the caps, against 401 poured, 331 landed and 1,069 slides crammed into a single frame without them. That is why the scene stops its loop on visibilitychange when hidden and startLoop resets the clock reference on return.

Color Turning on High-Density Screens

Sizing the distance field of 04 to the canvas pixels would give 480×300 times a ratio of 2, or 576,000 pixels, which by calculation is 576,000/73,200 ≈ 7.9 times the fixed 300×244 board (73,200 pixels). This board keeps the field size apart from the screen ratio and runs the 35,352 inside pixels × 5 veins = 176,760 distance checks a single time in buildField through the nested loop below (fieldBuilds stays 1 even after a rebuild).

        for (let y = 0; y < FIELD_H; y += 1) {
            for (let x = 0; x < FIELD_W; x += 1) {
                if (!insidePoly(x + 0.5, y + 0.5, outline)) {
                    continue;
                }
                let best = 1e9;
                for (let s = 0; s < veins.length; s += 1) {
                    distanceChecks += 1;
                    const d = segDist(x + 0.5, y + 0.5, veins[s][0], veins[s][1]);
                    if (d < best) {
                        best = d;
                    }
                }
                idx.push(y * FIELD_W + x);
                dist.push(best);
                if (best > maxD) {
                    maxD = best;
                }
            }
        }

Each frame then only compares 35,352 thresholds, as paintField showed earlier, which is one fifth the count of the 176,760 checks it would take to measure the distances again on every frame.

Already-Landed Leaves Under Reduced Motion — Accessibility with a Pause Button and a Scene Description

A Spoken Description of the Scene

All nine canvases carry role="img" and an aria-label that puts the current scene into words, so a screen reader reads a sentence in place of the picture. Those sentences are stored in the phrase file in three languages.

A Pause You Can Press from the Keyboard

The pause control is a native <button> with aria-pressed, so Tab reaches it and Enter or Space presses it, and while paused the background flips to deep ink with cream lettering. A 2 px outline from :focus-visible shows when focus arrives.

What Remains Under Reduced Motion

When prefers-reduced-motion is set to reduce, the nine scenes start paused and draw one final frame with no loop. 01 shows the three leaves landed with their trails, 02 the eight leaves laid on the ground, 03 the 140 leaves resting along the floor strip, 04 the fully turned leaf, 05 the six snapped leaves on the ground, 06 the pile after the slides have stopped, 07 the yard after the broom has reached the end of its route, 08 the leaves lying on the ground once the whirl dies down, and 09 a pile run for 6 seconds with the drifting leaves laid down as well. Pressing play starts the motion from the beginning, and a change to the setting while the page is open gets picked up too.

Questions People Ask Before Laying Falling Leaves Over an Autumn Page or Seasonal Banner

What changes if I use this code in place of a falling leaves GIF?

This board recomputes the same scene from a fixed seed each time it opens, and when a pointer arrives 01 and 03 add wind while the broom in 07 follows along. It also reads the reduced-motion setting to show a still frame and offers a pause button, so it appears easier to fit to the needs of a given screen than a clip that loops the same frames.

Will the screen hold up if I scatter many more leaves?

In the scenes other than 02 and 04, drawing one leaf costs a single sprite stamp, and the five measured scenes make 15,000 to 28,190 stamps in 2 seconds with 0 new sprite drawings. The number of stamps does grow with the leaf count, though, so fixing the count and reusing leaves that leave the board from the opposite side, as 03 does, looks like the safer route.

Can I follow this as a falling leaf animation tutorial and recolor the leaves for my site?

Yes, and the three constants in the variants table are a good first exercise, since each one changes a single number. The five leaf colors are five RGB arrays in LEAF_COLORS, and in 02 backColor builds the back face by mixing each with cream half and half; in the scenes that stamp sprites, a sprite is keyed by shape, color and blur and drawn once on first use, so a new color costs the same. Only the turning colors of 04 live in a separate four-step TURN_RAMP from green to maple, which you edit on its own.

Enter the archive password

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