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

Logo Loading Animation: 9 Progress-Driven Marks

What should a loading logo turn 0-to-1 progress into? 03 springs pixel-sampled points into place, 05 lowers a noise threshold so it bleeds in, and 07 melts

None of the nine plays a timeline. Each scene takes a single progress value between 0 and 1 and draws the mark at that value on one 2D <canvas>, while the stylesheet holds only the stage ratio, the buttons, the focus ring and the progress strip underneath. There is no CSS @keyframes rule anywhere, and motion comes from a loop that adds up requestAnimationFrame timestamps and runs fixed 16ms steps, at most six per frame, with no library or CDN loaded.

Run Nine Progress-Driven Logo Loaders Right Here

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

Logo Loading Animation: 9 Ways — Outline Trace, Liquid Fill, Point Gather, Tile Flip, Noise Bleed, Comet Tail, Shape Melt, Depth Stack and Count-Up

The first pair fills the mark with a line (01) and with a surface (02). The middle three reveal it unit by unit, as points (03), tiles (04) and pixels (05). The last four hand the job to a tail circling the edge (06), an outline that turns from a circle into the mark (07), depth that piles up behind it (08) and a calculation that evens out a jumpy number (09).

A hand-drawn geometric mark and a seeded fake download

The mark on screen is a pointy-top hexagon, 104px from center to corner, with a four-pointed star hole cut through it (outer points 44px, inner 14px), and it is not the logo of any company or brand. roundCorners trims each corner and rounds it with a quadratic curve, which leaves 42 points on the outer outline (601.7px around) and 32 on the star hole (260.1px), and the fill uses the evenodd rule so paint lands only inside the hexagon and outside the star. The progress is simulated too. makeLoad(469) uses an integer-mixing random generator seeded with 469 to schedule 7 chunks arriving over 1,300ms, and from 484ms to 758ms the received amount sits at 49% for 274ms. Scenes 01 to 08 read loadAt, which joins arrivals with straight lines, while 09 reads reportedAt, a step that jumps only when a chunk lands, so at 900ms the first eight see 0.585 and 09 sees 0.49. The source contains no fetch, Math.random or Date.now, so the same seed gives the same picture at the same moment each time the page opens.

How much each scene moved in the render capture

A two-second capture of 24 frames holds 23 frame-to-frame changes, and the frames column counts how many of those 23 actually altered the picture.

Scene Cumulative area Moving frames Mean intensity
01 Outline trace 21.6% 17/23 94.0
02 Liquid fill 15.5% 22/23 114.8
03 Pixel point gather 39.6% 23/23 79.0
04 Tile flip 22.2% 17/23 80.7
05 Noise bleed 17.7% 17/23 120.1
06 Comet tail 26.9% 23/23 64.3
07 Circle to mark 16.2% 20/23 92.2
08 Depth stack 12.3% 23/23 73.8
09 Percent count 28.8% 23/23 137.6

One demo cycle lasts 2,000ms: progress stays at 0 for the first 200ms, and the scene holds the finished state for the last 500ms. 01, 04 and 05 draw from progress alone, which likely explains why they stop at 17/23, since those two stretches give them little to change. In 03 the points still waiting for their turn drift around their scattered spots, 06's head keeps circling and 08 sways from side to side, so something changes in all 23 frames. 08 has the smallest cumulative area at 12.3%, likely because each new layer lands right behind the front face and adds only a narrow strip of fresh paint, while 03 has the widest at 39.6%, probably because its points fly in from a ring 128 to 224px from the center.

Matching a loader to the screen it will sit on

When the loading animation for website visitors is also their first look at the brand, 01's outline trace is a safe choice, and 02's rising water explains itself on a screen where people wait for an app install or an update. 03 suits a service start splash, 04 a dashboard's first load and 05 a portfolio intro. For a wait of unknown length such as payment processing, 06 fits, while 07 handles the hand-off to the next screen after sign-in, 08 the entrance to a game or event page, and 09 the waits where people watch a number for a while, like file exports and report generation.

Why Tie a Logo Loader Animation to Progress Instead of Time?

A sequence tied to time ends at its set length, whether that is one second or three. When the real load runs longer, the finished mark just sits there, and when it runs shorter, the screen moves on with the drawing half done. A logo loader animation driven the other way, by one progress value between 0 and 1, decides its picture from that value, so during the 274ms stall in the simulated load 01's pen stops where it is and 04's wave stops advancing. Showing the received amount as it is makes a stall look like a stall, and 09 is the scene that smooths it out instead of hiding it.

Progress enters through a single function, targetProgress(), which returns the dragged or key-set value (manual) when there is one and the simulated table's loadAt(loadMs) otherwise. In a real service you replace only that simulated value with your own progress, plus the incoming() value in 09, and the drawing code stays untouched.

One Number Between 0 and 1, Split Into Line Length, Water Level and Point Count

The table lists, scene by scene, which quantity in the picture p becomes and which value the strip underneath reports.

Scene How p becomes the picture Value the strip reports
01 drawn length = 861.4px × (1 − (1 − p)^1.6) p
02 the level closes 8% of its gap to p each step water level
03 only points whose shuffled rank is below p × 718 get targets share of points within 1.5px
04 flip = (p × 1,120 − (d − 2) × 60) ÷ 400 p
05 threshold = 1 − p × 1.07 p
06 tail points = 4 + round(p × 86), up to 90 p
07 blend = curve(p), giving 0.109, 0.5 and 0.891 at 25, 50 and 75% p
08 layers = p × 14, the fraction being the layer sliding in p
09 the shown value closes 6% of its gap to the stepped value shown value

01's curve lays down the front quickly and closes the end slowly, so at 25, 50 and 75% progress it has already drawn 36.9% (318px), 67.0% (577px) and 89.1% (768px). The strip reports a different value per scene so that the strip and the picture stay in agreement. 02 fills more slowly than progress (the largest gap is 19.4 points), so the strip carries the water level, and 03's points are still in flight, so it carries the share that has arrived. The shared function syncBar does that job.

    // Bottom strip (role="progressbar") — reports the value drawn on screen through the strip length and aria-valuenow. The attribute changes only when the whole-number % changes
    let barPct = -1;
    function syncBar(v) {
        const c = clamp(v, 0, 1);
        fill.style.transform = 'scaleX(' + c.toFixed(4) + ')';
        const p = Math.round(c * 100);
        if (p !== barPct) {
            barPct = p;
            bar.setAttribute('aria-valuenow', String(p));
        }
    }

The strip's length changes through transform: scaleX() alone, so layout is not recalculated, and aria-valuenow is rewritten only when the whole percent changes, which keeps screen readers from hearing the same value again and again.

01A Loader That Traces the Logo Outline at an Even Pace With a Glowing Pen Tip

Points placed at evenly spaced vertex indices bunch up on the rounded corners, leaving neighbor gaps from 0.22 to 12.85px, a 59.3-fold spread, so instead of stepping by point index the code binary-searches a cumulative arc-length table (362 spans, 861.4px in total), finds the span in 9 comparisons and moves the pen tip by the fraction of distance left inside it. The outer outline and the star hole are stroked in different colors, the tip carries an 18px glow and the interior starts filling at 70% progress, which suits a brand site's first screen where the mark is introduced one stroke at a time. In the render this scene covered 21.6% cumulative area with a mean intensity of 94.0 across 17/23 frames.

A Loader That Traces the Logo Outline at an Even Pace With a Glowing Pen Tip
Open in a new tab

01 · A Loader That Traces the Logo Outline at an Even Pace With a Glowing Pen Tip — Measured area 21.629% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

TRACE_SAMPLESPEN_GLOW_PXTRACE_EASE_POW
    function findSeg(d) {
        let lo = 0;
        let hi = CUM.length - 1;
        while (hi - lo > 1) {
            const mid = (lo + hi) >> 1;
            if (CUM[mid] <= d) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return lo;
    }
    function traceEase(p) { return 1 - Math.pow(1 - clamp(p, 0, 1), TRACE_EASE_POW); }
    // Pen tip — inside that span, it moves between the two points by the fraction of distance left, so the pen advances in proportion to length whatever the point spacing
    function penAt(d) {
        const i = findSeg(d);
        if (i >= TRACE.length - 1) {
            return { i: i, x: TRACE[i].x, y: TRACE[i].y };
        }
        const seg = CUM[i + 1] - CUM[i];
        const f = seg > 0 ? clamp((d - CUM[i]) / seg, 0, 1) : 0;
        return { i: i, x: lerp(TRACE[i].x, TRACE[i + 1].x, f), y: lerp(TRACE[i].y, TRACE[i + 1].y, f) };
    }

02A Loader That Fills the Logo Shape With Water and a Sloshing Surface

The surface is split into 48 columns, and each step first pulls every column toward the rest level with 0.985 damping, then passes neighbor height differences times 0.22 back and forth twice so ripples spread sideways, before the whole picture is clipped to the mark with only the star hole left empty. Each rise in the water level pushes the column under the pour downward to start a fresh slosh, so the filling itself tells the story on screens like an app install or an update wait. The render measured 15.5% cumulative area, 114.8 mean intensity and 22/23 frames.

A Loader That Fills the Logo Shape With Water and a Sloshing Surface
Open in a new tab

02 · A Loader That Fills the Logo Shape With Water and a Sloshing Surface — Measured area 15.474% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

WAVE_SPREADWAVE_DAMPFILL_RISE_RATE
    // One step of the column wave — (1) pull toward the rest level and multiply by the damping, then (2) trade height differences with neighbors by WAVE_SPREAD (run twice so it spreads sideways faster)
    function waveStep() {
        for (let i = 0; i < WAVE_COLS; i += 1) {
            vs[i] += -WAVE_TENSION * hs[i];
            vs[i] *= WAVE_DAMP;
            hs[i] += vs[i];
        }
        for (let pass = 0; pass < 2; pass += 1) {
            for (let i = 0; i < WAVE_COLS; i += 1) {
                if (i > 0) {
                    ld[i] = WAVE_SPREAD * (hs[i] - hs[i - 1]);
                    vs[i - 1] += ld[i];
                }
                if (i < WAVE_COLS - 1) {
                    rd[i] = WAVE_SPREAD * (hs[i] - hs[i + 1]);
                    vs[i + 1] += rd[i];
                }
            }
            for (let i = 0; i < WAVE_COLS; i += 1) {
                if (i > 0) {
                    hs[i - 1] += ld[i];
                }
                if (i < WAVE_COLS - 1) {
                    hs[i + 1] += rd[i];
                }
            }
        }
    }

03A Loader Where Points Sampled From Logo Pixels Spring Into Place

The mark is painted once on an offscreen canvas and read back with getImageData, and keeping only the cells of a 6px grid whose alpha is above 127 yields 718 target points in the browser render; points whose rank, shuffled with seed 4693, falls below p × 718 leave the outer ring and settle through a spring with acceleration −90(x − goal) − 11·v. With a damping ratio of 0.58, a point starting 120px away overshoots by 9.1%, first enters the 1.5px zone at 272ms and stays inside it from 624ms, which fits a service splash that treats the moment the dots become the mark as its start signal. A point changes color as soon as it has a target (rank below need) rather than on arrival, and the render gave this scene the widest cumulative area of the nine at 39.6%, with a mean intensity of 79.0 over 23/23 frames.

A Loader Where Points Sampled From Logo Pixels Spring Into Place
Open in a new tab

03 · A Loader Where Points Sampled From Logo Pixels Spring Into Place — Measured area 39.592% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SAMPLE_STEP_PXSPRING_KSPRING_DAMP
    // Damped spring — acceleration = −k(x − goal) − d·v, a semi-implicit step that updates velocity first (dt 0.016 s)
    function step() {
        const need = progress * N;
        const dt = FIXED_DT / 1000;
        for (let i = 0; i < DOTS.length; i += 1) {
            const d = DOTS[i];
            const g = goal(d, need);
            const ax = -SPRING_K * (d.x - g[0]) - SPRING_DAMP * d.vx;
            const ay = -SPRING_K * (d.y - g[1]) - SPRING_DAMP * d.vy;
            d.vx += ax * dt;
            d.vy += ay * dt;
            d.x += d.vx * dt;
            d.y += d.vy * dt;
        }
        countArrived();
    }

04A Loader That Splits the Logo Into Tiles and Flips Them in a Diagonal Wave

A 216px square is cut into 24px tiles and only the 64 that touch the mark remain; each diagonal stage, set by column plus row (13 stages), starts 60ms after the one before and flips over 400ms, and progress sweeps the resulting 1,120ms timetable from start to finish. A flipping tile folds and unfolds with a width of |cos(πf)|, its shading drops to 0.45 at the edge-on moment and it lifts slightly to cast a shadow, a look that matches a dashboard's first load where panels light up in turn. Recorded values were 22.2% cumulative area, 80.7 mean intensity and 17/23 frames.

A Loader That Splits the Logo Into Tiles and Flips Them in a Diagonal Wave
Open in a new tab

04 · A Loader That Splits the Logo Into Tiles and Flips Them in a Diagonal Wave — Measured area 22.244% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

TILE_SIZE_PXSWEEP_DELAY_MSFLIP_SHADE
    // Wave timetable — a tile in diagonal stage d starts at (d − DMIN) × SWEEP_DELAY_MS and flips over FLIP_MS. Progress sweeps this whole timetable from 0 to the end
    const WAVE_MS = (DMAX - DMIN) * SWEEP_DELAY_MS + FLIP_MS;
    function flipAt(t, p) {
        return clamp((p * WAVE_MS - (t.d - DMIN) * SWEEP_DELAY_MS) / FLIP_MS, 0, 1);
    }
    function shown() { return progress; }
    function step() { }
    function settle() { }
    function onRestart() { }
        for (let i = 0; i < TILES.length; i += 1) {
            const t = TILES[i];
            const raw = flipAt(t, progress);
            const f = still ? (raw >= 0.5 ? 1 : 0) : smooth(raw);
            const ang = f * Math.PI;
            // Width = |cos| — full width at 0° and 180°, standing edge-on with width 0 at 90°
            const wide = Math.abs(Math.cos(ang));
            const lift = Math.sin(ang);
            const shade = 1 - FLIP_SHADE * (1 - wide);
            const front = f >= 0.5;

05A Loader Where a Falling Noise Threshold Lets the Logo Bleed In

For the 25,952 pixels inside the mark, an even blend of four-octave value noise and closeness to the edge is computed once at the start, and each frame compares it with a threshold that falls as 1 − p × 1.07, paints the pixels that pass and leaves a 0.07-wide band just above the threshold glowing white. At 20% progress the pixels already showing sit an average of 3.3px from the edge (against 15.3px for the whole mark), so the outline surfaces first, which suits a creative portfolio intro where the form slowly comes into focus. The render reported 17.7% cumulative area, 120.1 mean intensity and 17/23 frames.

A Loader Where a Falling Noise Threshold Lets the Logo Bleed In
Open in a new tab

05 · A Loader Where a Falling Noise Threshold Lets the Logo Bleed In — Measured area 17.708% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

NOISE_SCALENOISE_OCTAVESEDGE_BAND
    function paint(thr) {
        if (!img || !offCtx) {
            return;
        }
        const px = img.data;
        let shownPx = 0;
        for (let i = 0; i < FIELD.length; i += 1) {
            const o = i * 4;
            if (!INSIDE[i]) {
                px[o + 3] = 0;
                continue;
            }
            const v = FIELD[i];
            if (v >= thr + EDGE_BAND) {
                // Pixels past the threshold — higher values (toward the edge) lean sky blue, lower values violet
                const c = mix(PAL[3], PAL[0], v);
                px[o] = c[0];
                px[o + 1] = c[1];
                px[o + 2] = c[2];
                px[o + 3] = 255;
                shownPx += 1;
            }
            else if (v >= thr) {
                // Band — the closer to the threshold, the whiter the glow
                const k = (v - thr) / EDGE_BAND;
                const c = mix(PAPER, PAL[4], k);
                px[o] = c[0];
                px[o + 1] = c[1];
                px[o + 2] = c[2];
                px[o + 3] = Math.round(255 * (0.55 + 0.45 * k));
            }
            else {
                px[o + 3] = 0;
            }
        }
        revealed = shownPx;
        offCtx.putImageData(img, 0, 0);
        ctx.drawImage(off, BX0, BY0);
    }

06A Loader Where a Comet Circling the Logo Grows Its Tail With Progress

A head runs at 0.55 laps per second around a rounded-rectangle path 965.6px long that wraps the mark, past positions go into a 90-slot ring buffer, and the tail grows to 4, 26, 47 and 90 points at 0, 25, 50 and 100% progress, with point i fading to 0.975^i brightness. Because the head keeps circling while progress stalls, a payment screen with a wait that can stretch does not read as frozen. It moved in 23/23 frames, with 26.9% cumulative area and a mean intensity of 64.3.

A Loader Where a Comet Circling the Logo Grows Its Tail With Progress
Open in a new tab

06 · A Loader Where a Comet Circling the Logo Grows Its Tail With Progress — Measured area 26.93% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ORBIT_SPEEDCOMET_TAIL_LENTAIL_DECAY
    function push(p) {
        ringX[ringHead] = p[0];
        ringY[ringHead] = p[1];
        ringHead = (ringHead + 1) % COMET_TAIL_LEN;
        ringCount = Math.min(ringCount + 1, COMET_TAIL_LEN);
    }
    // The i-th most recent position (0 is the head)
    function ringAt(i) {
        const k = (ringHead - 1 - i + COMET_TAIL_LEN * 2) % COMET_TAIL_LEN;
        return [ringX[k], ringY[k]];
    }
    function tailCount() {
        return Math.min(ringCount, TAIL_MIN + Math.round(clamp(progress, 0, 1) * (COMET_TAIL_LEN - TAIL_MIN)));
    }

07A Loader Where Points on a Circle Melt Into the Logo Outline

The circle and the mark outline are both resampled into 96 points at equal arc-length spacing, the circle's starting point is tried at 32 candidate offsets to find the one with the smallest sum of squared paired distances (an offset of 27), and each pair is interpolated along a slow-fast-slow curve. That one alignment cuts the sum from 1,501,470 to 76,223, 19.7 times smaller, and shortens the longest trip of any point from 129.1px to 32.8px, so a round spinner can melt into the mark during the hand-off after sign-in. Captured numbers were 16.2% cumulative area, 92.2 mean intensity and 20/23 frames.

A Loader Where Points on a Circle Melt Into the Logo Outline
Open in a new tab

07 · A Loader Where Points on a Circle Melt Into the Logo Outline — Measured area 16.233% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

MORPH_POINTSALIGN_SEARCHMORPH_EASE
    // Sum of squared paired distances — point i on the circle against point (i + off) on the logo
    function alignCost(a, b, off) {
        let s = 0;
        for (let i = 0; i < a.length; i += 1) {
            const q = b[(i + off) % b.length];
            const dx = a[i][0] - q[0];
            const dy = a[i][1] - q[1];
            s += dx * dx + dy * dy;
        }
        return s;
    }
    function bestOffset(a, b) {
        let best = 0;
        let bestCost = Infinity;
        for (let k = 0; k < ALIGN_SEARCH; k += 1) {
            const off = Math.round((k * b.length) / ALIGN_SEARCH) % b.length;
            const cost = alignCost(a, b, off);
            if (cost < bestCost) {
                bestCost = cost;
                best = off;
            }
        }
        return best;
    }

08A Loader That Stacks Layers Behind the Logo to Grow Its Depth With Progress

Up to 14 copies of the mark stack behind it at 4px spacing, the layer count comes from the fractional value p × 14 so each new layer slides in from 18px deeper, and the stack is projected with a 30° forward tilt and perspective (focal 560), then painted from the back layer forward with the rear ones darker. Fully stacked, the 56px depth opens 19.5px at the bottom corner and shows 13.5px at the side at each end of the 22° sway, which suits entry screens for games or event pages that want some volume. The motion is small at 12.3% cumulative area and 73.8 mean intensity, yet it filled 23/23 frames.

A Loader That Stacks Layers Behind the Logo to Grow Its Depth With Progress
Open in a new tab

08 · A Loader That Stacks Layers Behind the Logo to Grow Its Depth With Progress — Measured area 12.282% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

EXTRUDE_LAYERSLAYER_GAP_PXTILT_DEG
    // Turn by yaw around the vertical axis, tilt by −TILT_DEG around the horizontal axis, then divide for perspective with screen scale s = FOCAL / (FOCAL + z)
    function project3(x, y, z, yaw, pitch) {
        const cy = Math.cos(yaw);
        const sy = Math.sin(yaw);
        const x1 = x * cy + z * sy;
        const z1 = -x * sy + z * cy;
        const cp = Math.cos(pitch);
        const sp = Math.sin(pitch);
        const y2 = y * cp - z1 * sp;
        const z2 = y * sp + z1 * cp;
        const s = FOCAL / (FOCAL + z2);
        return [CX + x1 * s, CY + y2 * s, s];
    }

09A Percent Loader That Smoothly Follows Uneven Real Progress and Pulses the Logo at the End

The target is a stepped progress value that jumps only when a chunk arrives, the shown value closes 6% of the remaining gap every 16ms, and if no new value comes for more than 96ms the target creeps forward a little, with the creep multiplied by 0.96 each step so it keeps slowing. On the demo run at the default PROGRESS_SMOOTH of 0.06 the shown value stayed at or below the received amount with zero backward steps, and when it reaches 100% at 1,536ms from the start of loading, the mark pulses once for 320ms, fitting long waits where people watch the number, such as report generation. The render put it at 28.8% cumulative area and 137.6 mean intensity, the strongest of the nine, over 23/23 frames.

A Percent Loader That Smoothly Follows Uneven Real Progress and Pulses the Logo at the End
Open in a new tab

09 · A Percent Loader That Smoothly Follows Uneven Real Progress and Pulses the Logo at the End — Measured area 28.847% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PROGRESS_SMOOTHSTALL_SLOWDOWNDONE_PULSE_MS
    function step() {
        const r = incoming();
        if (r !== reported) {
            reported = r;
            sameMs = 0;
            creep = 0;
            creepV = CREEP_V0;
        }
        else {
            sameMs += FIXED_DT;
        }
        // Stall — while following the simulated load, if no new value arrives for more than STALL_AFTER_MS, nudge the target a little past the received value, multiplying the nudge speed by STALL_SLOWDOWN each time so it keeps slowing down
        if (sameMs > STALL_AFTER_MS && reported < 1 && manual < 0) {
            creep = Math.min(creep + creepV, (1 - reported) * CREEP_CAP);
            creepV *= STALL_SLOWDOWN;
        }
        const target = reported + creep;
        // While following the simulated load it does not go backward — if a value smaller than the crept target arrives, the shown value waits where it is
        if (target >= disp || manual >= 0) {
            disp += (target - disp) * (reported >= 1 ? FINISH_SMOOTH : PROGRESS_SMOOTH);
        }
        if (reported >= 1 && disp > 0.995) {
            disp = 1;
            if (doneAt < 0) {
                doneAt = clock;
            }
        }
        if (disp < 1) {
            doneAt = -1;
        }

Progress Running Backward, Stalling for Ages, a Thin Logo Stroke — Where a Loading Logo Breaks

Every figure in this section comes from calling the functions in the vanilla index.html directly, and frame processing time on different devices was not measured.

Each scene reacts differently when progress runs backward

01, 04 and 05 hold no state, so dragging progress back rewinds the line, the tiles and the pixels along with it. A received amount can shrink in real use too, so if rewinding looks wrong in your layout, keep only the running maximum before passing the value on. 09 waits in place when the target falls below the shown value while it follows the simulated load, and moves backward only under manual control.

The count running ahead during the 274ms stall

Without a cap and a slowdown on the creep, the shown value passes the received amount before the next chunk lands and then has to fall back. 09 caps the creep at 40% of what is left and applies the 0.96 slowdown, so the creep actually peaked at 2.44 points. Setting the slowdown to 1 lets it reach 3.68 points, and raising the follow rate to 0.15 catches up with the crept target, so the shown value leads the received amount by up to 1.82 points. The earlier claim that the count stays at or below what was received holds for the default 0.06. Narrowing by 6% even after the finish signal leaves the number hovering near 99%, so the rate switches to 0.25 at that point, and the number is written with Math.floor so 100 does not appear at 99.5%.

Uneven outline spacing and the line across the seam

Moving the pen by point index mixes spans that cover 0.22px with spans that cover 12.85px in the same time, so the pen hesitates at every corner. When the two outlines are joined in one table, adding the seam's distance would draw a straight line from the outer end into the star hole, and arcTable adds 0 there so the pen lifts and skips across.

Thin strokes leave the pixel sample empty, tight spacing multiplies the points

03 samples one cell every 6px starting at 3px, so a stroke thinner than 6px can miss every cell center and get no points at all. Tightening the spacing makes the count climb fast. The 718 in this post is counted from the browser's own render, antialiasing included; calling the functions directly and imitating the same read with an even-odd inside test gives 721, and on that basis a 4px spacing gives 1,621 points (about 2.25 times as many) and an 8px spacing 400. Lowering the alpha threshold toward 0 turns semi-transparent edge pixels into points as well, and repeating the read each frame multiplies the work by the pixel count, so it happens once at setup.

Ripples that blow up and damping that lets them run

Raising 02's exchange strength to 0.6 sends the surface height to 4.3×10^307 within 600 steps. As the source comment warns, anything above 0.5 hands back more than it received, and damping set to 1 leaves the slosh undiminished. Solving the ripples once per frame would make them faster on high refresh rate screens, which is why this source uses fixed 16ms steps.

The end of the timetable and the zero-width flicker

If 04's timetable length leaves out the last tile's 400ms flip, the final diagonal row ends half-turned at 100% progress. A frame where the width hits 0 makes the tile vanish and flicker, so the width stays at 0.02 or more. Cutting the start gap to 30ms shrinks the timetable to 760ms, so all 13 rows flip together, all 64 tiles stand half-turned at 50% and the wave disappears.

A morph with mismatched starting points and winding

Without aligning the start in 07, points cross up to 129.1px and the shape tangles before it untangles. Sampling the circle by angle and the mark by vertex index bunches points near the corners and breaks the pairing, so both are resampled by arc length. Two outlines wound in opposite directions do not lower the cost at any offset, so both run clockwise.

Noise computed per frame, a threshold that stops at 0

If 05 recomputed its noise every frame, it would redo four octaves for 25,952 pixels each time, so the values are built once and each frame only compares. Ending the threshold at 0 leaves the band in place at 100% with the edge still glowing, so it goes down to −0.07.

A strong tail fade hides any extra length

With 06's fade set to a hard 0.9, brightness drops below 5% by the 29th point, so the visible tail stays the same length even when rising progress extends it to 90 points. At 0.975 that cutoff is the 119th point, so the full 90-point tail shows. Inserting each past position at the front of an array would shift 90 slots every step, while the ring buffer only turns its head index.

Layer paint order and tilt

Painting 08 from the front layer back lets the rear layers cover the face. Reducing the tilt to 10° makes the bottom-corner opening −0.7px, so even a full stack hides its depth behind the face, and growing the layer count in whole numbers makes the stack jump in fourteen visible steps.

If you want to run the nine vanilla folders with your own progress value and your own mark swapped in, the downloaded zip opens with 5t93rr73, and React versions carrying the same calculations come in the same archive.

Shifting Wave Damping, Tile Stagger and Count Speed, Plus Accessibility With Reduced Motion

Variant Value Feel
Increase the wave damping 02 WAVE_DAMP 0.985 → 0.94 After one column is kicked, the slosh used to take 182 steps (2,912ms) to drop below 10% of its first peak and stay there, and now takes 52 steps (832ms). On the demo load the largest distance of the surface from its rest level also falls from 49.3px to 40.0px, so the water shakes briefly and calms soon.
Spread out the tile starts 04 SWEEP_DELAY_MS 60 → 120 The timetable grows from 1,120ms to 1,840ms, the diagonal rows flipping at the same moment drop from 7 to 4, and the tiles standing half-turned at 50% progress go from 42 to 18. A broad, soft wave becomes a narrow, crisp diagonal ripple. Going the other way to 30ms leaves all 64 tiles half-turned at 50%, and the ripple is lost.
Let the count catch up faster 09 PROGRESS_SMOOTH 0.06 → 0.15 After a drag from 0 to 50% in one jump, the remaining gap shrinks to 10% in 15 steps (240ms) instead of 38 steps (608ms). At 640ms on the demo load, with 49% received, the shown value reads 44.9% instead of 32.5%, and 100% arrives 1,504ms after loading starts instead of 1,536ms. The price is that during the stall the shown value runs up to 1.82 points ahead of what was received.

Two values outside the table are worth a one-line edit too. Setting 01's TRACE_EASE_POW to 1 draws exactly 215, 431 and 646px at 25, 50 and 75%, a flat pace with no curve, and going below 1 makes the start slow and the end rushed. Raising 03's SPRING_DAMP to 20 brings the damping ratio to 1.054, which removes the overshoot but takes 800ms before a point stays inside 1.5px.

What the progress strip announces

The strip under each scene has role="progressbar", aria-valuemin="0", aria-valuemax="100" and the name "Logo loading progress", and aria-valuenow carries the value drawn on screen at that moment rather than raw progress. That is why 02 reports its water level, 03 the share of arrived points and 09 its shown value, so the number a listener hears matches the picture a viewer sees.

Driving the scenes from the keyboard

Each canvas has tabindex="0" and an English description, and once it has focus, Enter restarts from the top, Space pauses and resumes, → and ↑ step 5% forward, ← and ↓ step 5% back, Home picks 0%, End picks 100%, and Esc lets go of the manual value and returns to the simulated load. Only the handled keys have their default action blocked, so page scrolling keeps working. "Restart" and "Pause" are real <button> elements, and the pause button reports its pressed state with aria-pressed. The status line at the lower left stays quiet with aria-live="off" while the automatic demo runs, then switches to polite the first time a person drags, presses a key or touches a button, and reads out only the states after that.

What changes under reduced motion

With reduce chosen for prefers-reduced-motion, the requestAnimationFrame loop does not start at all. A setTimeout moves the simulated load forward one notch every 400ms instead, and the scene redraws the end state and number for that progress in steps, skipping the ripples, springs and flips in between. 04's tiles show either their front or their back with no half-turn, 09 shows the received value with no pulse, 06 parks its head and fills the tail at once, and the clock-driven drift in 03, the highlight in 07 and the sway in 08 stop as well. A change listener on matchMedia watches the setting, so switching it on with the page open stops the loop and swaps in the end state for the current progress, and switching it off starts the loop again.

What Changes When a Logo Animation Loading Screen Is Code Instead of a Video File?

A video plays for a fixed length, so it keeps running on its own track whether loading stalls or goes backward. A mark kept as code takes the progress value and draws that value's state, so it stops when loading stops and can pulse once right at the finish, the way 09 does. The strip's aria-valuenow, the reduced-motion handling and keyboard control all live in the same source. Colors are palette constants and speed or damping each sit on a single line as named constants, so matching a brand color or a mood does not mean exporting a new video.

For pieces assembled on a timeline instead, see 9 intro and opening animations, loaders that run on physics without a mark are in 9 canvas physics loaders, and loading screens styled with CSS alone are in 9 loading screen designs. The pixel-to-point technique from 03 has a close relative in glyph pixel sampling typography, and scenes built from per-pixel math sit in 9 canvas scene effects.

Questions That Come Up When Adding a Logo Loading Screen to Your Site

Where in the code does real loading progress go?

The demo's progress is a simulated table built by makeLoad(469), and it sends no network requests. In 01 to 08, replace the loadAt(loadMs) that targetProgress() returns with your own progress value from 0 to 1, and in 09, put the values that arrive in bursts where reportedAt(loadMs) sits inside incoming(). 09's smoothing and stall slowdown exist to tidy exactly that kind of stepped value.

Can the loader use my own mark instead of the hexagon?

The mark is two point arrays, LOGO_OUTER = roundCorners(hexVerts(LOGO_R), 0.2, 6) and LOGO_HOLE = roundCorners(starVerts(44, 14), 0.16, 3), so swapping those two lines for your own outline coordinates puts that shape into all nine scenes. 01 draws any outline at an even pace thanks to its arc-length table, and 03 reads painted pixels, so the shape does not matter to it. 07 needs both outlines wound clockwise like the circle for the alignment cost to drop as it should.

Does this logo loader animation work when the wait has no known end?

All nine assume a progress value running from 0 to 1. For a wait with no progress value, 06 comes closest: its head advances by STEP_LEN every step regardless of progress and only the tail length follows the value, so feed the tail whatever you do know and let the circling head cover the rest.

Enter the archive password

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