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

Splash Screen Animation: 9 Canvas Opening Scenes

How does a splash screen turn loading into a logo? 01 sets a logo mask from one loaded fraction, 02 exits at the larger of load finish and a minimum hold, 06

A Splash Screen Is Not a Picture That Hides Loading but a Device That Computes the Logo, the Number and the Opening From Two Values, the Loaded Fraction and the Timetable: Building a Splash Starts by Choosing Which Value Moves the Screen

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

The nine scenes share one model. The screen is a single 480×300 canvas, the loaded fraction comes from a seeded virtual resource table built by makeLoad (02 skips the table and uses two fixed load times), and the clock is one scene clock, sceneT. The logo, the number and the opening are screens that take those two values, so on a real site you swap one line for the true fraction and the rest of the source stays as it is.

The order starts at the loading indicator and widens to scene changes. 01, 02 and 03 cover the logo mask, the minimum hold and the percent number, 04, 05 and 06 cover the curtain, the letters and the circular opening, and 07, 08 and 09 cover the ring, the header handoff and the landing intro. If you are collecting splash screen animation examples, that is a path from the smallest piece to a whole landing screen.

Splash Screen Animation: 9 Scenes — A Logo Wipe Tied to Loading, an Exit That Honors a Minimum Hold, a Percent Number That Follows, a Two Panel Curtain, a Spring Wordmark, a Circular Opening, a Ring Split by Resource Weight, a Logo That Flies to the Header and a Landing Hero Intro

What nine renders measured for area and motion

Scene Cumulative area Moving frames Mean intensity
01 Logo wipe 6.0% 23/23 48.6
02 Minimum hold exit 22.8% 23/23 117.2
03 Percent counter 8.3% 23/23 105.2
04 Two panel curtain 30.9% 16/23 67.6
05 Spring wordmark 14.1% 15/23 112.0
06 Circular opening 26.8% 6/23 63.3
07 Ring segments 6.9% 23/23 81.8
08 Logo flight 31.1% 9/23 65.6
09 Landing intro 11.1% 23/23 61.8

08 is widest at 31.1% because a big logo flies across the screen while a whole page crossfades, 04 follows at 30.9%, and 01 is narrowest at 6.0% because only one logo fills. Mean intensity is highest for 02 at 117.2 with its flashing cover and lowest for 01 at 48.6.

Scenes 01, 02, 03, 07 and 09 moved in 23 of 23 frame gaps, while 04 moved in 16 and 05 in 15. 06 moved in only 6 and 08 in 9, since the opening finishes within 0.9 s and the flight and crossfade end between 2.75 s and 3.75 s, after which the page holds still.

01A Logo Wipe Coupled to the Loaded Fraction

The loaded fraction LOAD_FRAC becomes the mask width MASK_W(f) = f × (LOGO_S 74 + EDGE_SOFT 18), so when the fraction stops the wipe stops with it. The 18 px edge is six alpha steps, and the right board, filled by time alone, shows 0.457 at 3.6 s where the real fraction is 0.303, drifting by up to 20.8 points at 2.90 s. Use it where an app or site first screen shows load progress through the logo.

A Logo Wipe Coupled to the Loaded Fraction
Open in a new tab

01 · A Logo Wipe Coupled to the Loaded Fraction — Measured area 6.018% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LOAD_FRACMASK_WEDGE_SOFT
    function build() {
        makeLoad(SEED_BASE + (lap % 3), 8, LOAD_START, LOAD_SPAN);
    }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function LOAD_FRAC(t) { return loadFrac(t); }
    function TIME_FRAC(t) { return clamp((t - LOAD_START) / LOAD_SPAN, 0, 1); }
    function MASK_W(f) { return maskW(LOGO_S, EDGE_SOFT, f); }

02An Exit Time That Honors a Minimum Hold

The exit start EXIT_START is the larger of the load finish time and MIN_HOLD 0.6 s, and the cover fades over EXIT_MS 450 ms. On the fast visit, where loading takes 0.35 s, the left side leaves at 600 ms while the right side, which leaves the moment loading ends, goes at 350 ms, under the FLASH_MS 500 mark, and on the slow visit of 2.4 s both leave at 2400 ms. It is the rule that prevents a flicker when loading is fast.

An Exit Time That Honors a Minimum Hold
Open in a new tab

02 · An Exit Time That Honors a Minimum Hold — Measured area 22.769% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

MIN_HOLDEXIT_STARTEXIT_MS
    function visitOf(t) { return t >= VISIT_AT[1] ? 1 : 0; }
    // time measured from the start of the visit
    function localT(t) { return t - VISIT_AT[visitOf(t)]; }
    function LOAD_DONE(t) { return VISIT_LOAD[visitOf(t)]; }
    // exit start time: the left side honors the minimum hold, the right side leaves the moment loading ends
    function EXIT_START(t, keep) { return keep ? Math.max(LOAD_DONE(t), MIN_HOLD) : LOAD_DONE(t); }
    function splashAlpha(t, keep) { return 1 - ease((localT(t) - EXIT_START(t, keep)) / (EXIT_MS / 1000)); }

03A Percent Counter That Eases After the Real Value

Where a progress number must not jump, the target is the KB that arrived whole divided by the total KB, and the left number closes a part of the gap each frame with disp += (target - disp) * (1 - e^(-5*dt)). At CATCH_RATE 5 it reaches 95 percent of one arrival in 0.60 s and trails the target by up to 26 points. The right number prints the target as is, so the last 270 KB chunk moves it 25 points at once and it looks stalled for 1.46 s between arrivals.

A Percent Counter That Eases After the Real Value
Open in a new tab

03 · A Percent Counter That Eases After the Real Value — Measured area 8.297% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CATCH_RATEDISPLAY_PCTARRIVED_KB
    function ARRIVED_KB(t) { return arrivedKb(t); }
    function target(t) { return ARRIVED_KB(t) / totalKb; }
    // close a part of the gap, remaining gap × (1 - e^(-rate·dt)); while the target holds, many small dt steps or one big step give the same value
    function update(dt) {
        const tg = target(sceneT);
        if (Math.abs(tg - lastTarget) > 1e-9) {
            lastTarget = tg;
            lastChange = sceneT;
        }
        disp += (tg - disp) * (1 - Math.exp(-CATCH_RATE * dt));
        while (nextSample <= sceneT) {
            hist.push([nextSample, target(nextSample), disp]);
            nextSample += SAMPLE_S;
        }
    }

04A Two Panel Curtain That Splits to Reveal the Page

The curtain opens HOLD_S 0.3 s after loading ends, and the opened width HALF_W = easeInOut(p) × 103 px makes it start slowly over OPEN_S 1.1 s and move fastest in the middle. On the left a clip cuts the logo into halves that slide out while the page settles from 1.08 times to 1 times, and on the right one sheet lifts and the whole logo rises with it. Use it for an intro that opens the page behind the logo on first entry.

A Two Panel Curtain That Splits to Reveal the Page
Open in a new tab

04 · A Two Panel Curtain That Splits to Reveal the Page — Measured area 30.906% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

HALF_WHOLD_SOPEN_S
    function openP(t) { return clamp((t - (doneT + HOLD_S)) / OPEN_S, 0, 1); }
    // how far one curtain has slid = opening curve × half width
    function HALF_W(t) { return easeInOut(openP(t)) * (PW / 2); }
    function drawCover(px, py, cx, f, shift, c, cl, cr) {
        // left half: the clip is a half rectangle, and the logo is drawn at the center so only half shows
        const half = PW / 2;
        ctx.save();
        ctx.beginPath();
        ctx.rect(px - shift, py, half, PH);
        ctx.clip();
        box(px - shift, py, half + 1, PH, 0, mix(c, FACE, cl), 1);
        ctx.translate(-shift, 0);
        logoWipe(cx, py + PH / 2 - 6, 64, f, 14, LAV_DEEP, 1);
        ctx.restore();
        ctx.save();
        ctx.beginPath();
        ctx.rect(px + half + shift, py, half, PH);
        ctx.clip();
        box(px + half + shift - 1, py, half + 1, PH, 0, mix(c, FACE, cr), 1);
        ctx.translate(shift, 0);
        logoWipe(cx, py + PH / 2 - 6, 64, f, 14, LAV_DEEP, 1);
        ctx.restore();
    }

05A Wordmark Whose Letters Rise on a Staggered Spring

The first letter starts at timeAtFrac(0.15), when the loaded fraction reaches 15 percent (1.07 s in the first run), and each next letter is STAGGER_MS 90 later. A spring with a damping ratio of 0.465 overshoots the target by 19 percent and settles within 2 percent 0.51 s later, so the whole thing ends at 960 ms, while the right side rises together for 500 ms without overshoot. Use it for a wordmark that brings the brand name in letter by letter.

A Wordmark Whose Letters Rise on a Staggered Spring
Open in a new tab

05 · A Wordmark Whose Letters Rise on a Staggered Spring — Measured area 14.11% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

STAGGER_MSSPRING_KDAMPING_C
    function build() {
        makeLoad(SEED + (lap % 3), 6, 0.3, 2.5);
        t0 = timeAtFrac(START_FRAC);
        // the time the spring stays within 2% of the target, searched backward in 0.01 s steps
        settleS = 0;
        for (let q = 2.0; q > 0; q -= 0.01) {
            if (Math.abs(1 - springStep(q, SPRING_K, DAMPING_C)) > 0.02) {
                settleS = q;
                break;
            }
        }
    }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function letterStart(i) { return t0 + (i * STAGGER_MS) / 1000; }
    // how far letter i has risen; the spring passes 1 once
    function letterRise(i, t, spring) {
        const q = t - (spring ? letterStart(i) : t0);
        return spring ? springStep(q, SPRING_K, DAMPING_C) : ease(q / FLAT_S);
    }
    // total length in ms: the start delay of the last letter plus the time the spring takes to settle
    function TOTAL_MS() { return (WORD.length - 1) * STAGGER_MS + Math.round(settleS * 1000); }

06A Circular Opening That Grows From the Logo Center

The needed radius MAX_RADIUS() is the largest Math.hypot distance from the center to the four corners. The center sits toward the lower left (FX 0.3, FY 0.62), so the distances split into 111.7, 171.6, 84.1 and 155.1 px and the maximum is 171.6 px, while the right side pins FIXED_RADIUS() to 103 px and keeps three corners (111.7, 171.6 and 155.1) covered. Use it for a circular transition that opens the screen from a button or logo position.

A Circular Opening That Grows From the Logo Center
Open in a new tab

06 · A Circular Opening That Grows From the Logo Center — Measured area 26.758% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

EXIT_MSMAX_RADIUSFIXED_RADIUS
    function openP(t) { return clamp((t - OPEN_AT) / (EXIT_MS / 1000), 0, 1); }
    // distance from the center (fx, fy) to the four rectangle corners: top left, top right, bottom left, bottom right
    function cornerDist(fx, fy) {
        const w = PW;
        const h = PH;
        return [Math.hypot(fx, fy), Math.hypot(w - fx, fy), Math.hypot(fx, h - fy), Math.hypot(w - fx, h - fy)];
    }
    function MAX_RADIUS() {
        const d = cornerDist(FX * PW, FY * PH);
        return Math.max(d[0], d[1], d[2], d[3]);
    }
    // the fixed radius failure version: pinned to half the screen width
    function FIXED_RADIUS() { return PW / 2; }
    function radiusAt(t, full) { return ease(openP(t)) * (full ? MAX_RADIUS() : FIXED_RADIUS()); }

07A Ring Split by Resource Weight

Eight resources split the ring by size, with ARC_BY_KB setting each arc to KB ÷ total KB × (2π - 8 × SEG_GAP), and each arc fills by how much of that resource has been read. In the first run the largest arc (293 KB) spans 86.8 degrees and the smallest (34 KB) 10.1 degrees, and the center number is the fraction of KB read. The right ring, split at equal angles, shows an average of arcs and is off from the real value by up to 6.2 points, so the weighted version suits a progress ring for an app that fetches several resources.

A Ring Split by Resource Weight
Open in a new tab

07 · A Ring Split by Resource Weight — Measured area 6.937% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ARC_BY_KBSEG_GAPRING_W
    // start angle and size of arc i: by KB share if weighted, otherwise equal size
    function ARC_BY_KB(i, weighted) {
        const room = TAU - SEG_GAP * RES_N;
        let a = -PI / 2;
        for (let k = 0; k < i; k += 1) {
            a += (weighted ? (res[k].kb / totalKb) * room : room / RES_N) + SEG_GAP;
        }
        return [a, weighted ? (res[i].kb / totalKb) * room : room / RES_N];
    }
    function meanFrac(t) {
        let s = 0;
        for (let i = 0; i < res.length; i += 1) {
            s += resFrac(res[i], t);
        }
        return s / res.length;
    }

08A Handoff Where the Logo Flies to the Header Slot

The big logo starts 0.25 s after loading ends and flies along the FLY_PATH curve to the header spot over FLY_S 0.8 s, and SCALE_LERP shrinks it from 70 px to 22 px on the same progress value. From 0.4 s into the flight the page crossfades with XFADE from a skeleton to real content over 0.6 s. On the right the header logo appears only 0.35 s after the big one has vanished, so for that gap there is no logo, which is why the left version suits a shared element transition from splash to the real screen.

A Handoff Where the Logo Flies to the Header Slot
Open in a new tab

08 · A Handoff Where the Logo Flies to the Header Slot — Measured area 31.064% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLY_PATHSCALE_LERPXFADE
    function flyAt() { return doneT + 0.25; }
    function flyP(t) { return clamp((t - flyAt()) / FLY_S, 0, 1); }
    // page crossfade: from XF_AT after the flight starts, the skeleton fades out and the content fades in over XF_S
    function XFADE(t) { return ease((t - flyAt() - XF_AT) / XF_S); }
    // start, end and control point: the control point takes the start x and the end y, so the path rises first and then bends to the left
    function FLY_PATH(x0, y0, x1, y1, p) {
        const e = easeInOut(p);
        const cxp = x0;
        const cyp = y1;
        const a = (1 - e) * (1 - e);
        const b = 2 * (1 - e) * e;
        const c = e * e;
        return [a * x0 + b * cxp + c * x1, a * y0 + b * cyp + c * y1];
    }
    function SCALE_LERP(p) { return lerp(LOGO_BIG, LOGO_SMALL, easeInOut(p)); }

09A Brand Intro That Continues Into a Landing Hero

Six blinds lift 0.25 s after loading ends, each BLIND_STAGGER 0.07 s later than the one before and taking BLIND_S 0.5 s (3.05 s to 3.90 s in the first run), and the logo fills to the loaded fraction and then goes. In the hero behind, a two line headline rises through a clip, the button springs in through springStep, and six orbs circle on elliptical orbits. Use it for the first screen intro of a landing page.

A Brand Intro That Continues Into a Landing Hero
Open in a new tab

09 · A Brand Intro That Continues Into a Landing Hero — Measured area 11.057% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BLIND_OPENBLIND_STAGGERSPRING_K
    function build() { makeLoad(SEED + (lap % 3), 8, LOAD_START, LOAD_SPAN); }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function blindStart(i) { return doneT + 0.25 + i * BLIND_STAGGER; }
    // opening of blind i: the height of the strip shrinks by this value
    function BLIND_OPEN(i, t) { return ease((t - blindStart(i)) / BLIND_S); }
    function heroT() { return blindStart(BLIND_N - 1) + 0.15; }

When You Code Splash Screen Design, How Many Values Decide One Logo and One Number?

One progress value comes from one virtual resource table

makeLoad(seed, n, start, spanS) picks the size of n resources (24 to 300 KB) and their arrival windows from a seed and fits the loading to end within spanS seconds. The loaded fraction is loadFrac(t) = KB read ÷ total KB, and the first run of 01 receives 1103 KB in all from 0.4 s to 7.4 s. The seed shifts by seed + lap % 3 on each run, so three different loadings take turns. Scene 02 is the exception: it does not call makeLoad and uses two fixed load times, VISIT_LOAD 0.35 s and 2.4 s.

    function makeLoad(seed, n, start, spanS) {
        const rnd = seeded(seed);
        const raw = [];
        let sum = 0;
        let mx = 0;
        for (let i = 0; i < n; i += 1) {
            const kb = Math.round(24 + rnd() * 276);
            const a = rnd() * 0.5;
            const b = a + 0.12 + (kb / 300) * 0.38;
            raw.push({ kb: kb, t0: a, t1: b });
            sum += kb;
            mx = Math.max(mx, b);
        }
        res = raw.map(function (r) { return { kb: r.kb, t0: start + (r.t0 / mx) * spanS, t1: start + (r.t1 / mx) * spanS }; });
        totalKb = sum;
        doneT = start + spanS;
    }
    // how much one resource has been read at time t, from 0 to 1

The scenes compute from that value

The logo mask width (01), the number target (03), the letter start (05), the ring arcs (07) and the blind start (09) all branch off the same table. On a real site you put bytes read ÷ total bytes in the place of loadFrac and the rest of the source runs unchanged. The demo calls no network, and its random numbers come from one seeded generator.

One clock, sceneT

The nine sources contain no Date, performance or Math.random call, and the scene clock grows in one place, sceneT += dt. A frame is cut at DT_MAX 0.05 s, so a tab that hides and returns does not dump a lump of time at once. The same seed plays the same values on every run.

Take the Loaded Fraction and Elapsed Time, Honor a Minimum Hold, Let the Number Follow, and Turn It Into a Mask Width and an Opening Radius: the Order in Which One Splash Frame Is Computed

The order of one frame

The requestAnimationFrame time difference is cut at DT_MAX, advance(dt) adds it to sceneT, and when LAP_S is passed lap goes up and begin() starts over. Then update(dt) fixes the state of the item and draw() paints the canvas, and the status line, the progress bar and the done notice update after that. The first run starts START_S ahead, so the scene first appears with the effect already under way.

Finding the time the loaded fraction reaches f

The loaded fraction does not decrease over time, so timeAtFrac(f) in 05 halves the span from 0 to the finish time 40 times to find the first time it reaches f. That one value fixes when the first letter starts, and each next letter follows STAGGER_MS later.

    function timeAtFrac(f) {
        let lo = 0;
        let hi = doneT;
        for (let i = 0; i < 40; i += 1) {
            const mid = (lo + hi) / 2;
            if (loadFrac(mid) < f) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return hi;
    }

Putting a Splash Screen on a Real Site: Building Without a Library, Why Loading That Is Too Fast or Too Slow Feels Wrong and When to Hand Over to the Page

Where it fits on an app first screen and a web app startup

While a mobile web app fetches its first screen, the mask wipe of 01 and the percent counter of 03 fit, and where images, fonts and data arrive separately the ring of 07 divides arcs by resource size. A brand landing page suits the blinds of 09, which lift into the hero, and a structure where the logo climbs into the header, as on an install screen, suits 08. Among splash screen animation ideas, picking the scene for the screen comes first, and that settles half of the work.

Why loading that is too fast or too slow feels wrong

When loading ends in 0.35 s the logo shows for only 350 ms and vanishes as a flash, so you add a minimum hold as in 02. When loading is slow the number stands still for 1.46 s, as on the right board of 03, and looks stalled, so you switch to a number that follows the target and keep it moving. Splash screen design is the job of choosing how long the logo stays between those two extremes.

When to hand over to the page

The moment to end the splash is the load finish time plus a margin of 0.25 to 0.3 s. 04 waits HOLD_S 0.3 s, 08 and 09 wait 0.25 s before they open or fly, and 06 starts its opening at OPEN_AT 3.2 s. The loading state of a whole page transition is covered separately in page transitions and loading states.

Building without a library

It paints with canvas 2D alone and the nine sources contain no library, WebGL or CDN, so one copied file runs. The logo is a rounded tile with two holes cut in it, held in the single function logoPath, so you can swap in another shape. A logo whose single line draws itself sits next to logo loading animation, spinners and bars sit next to CSS loading screen design, and a pure CSS opening sits next to CSS intro opening animation.

Where a Splash Screen Breaks: a Trap of Fast Loading Where the Logo Flashes and Vanishes, Progress That Skips and Looks Stalled and a Fixed Radius Opening That Leaves a Corner

The logo flashes and vanishes when loading is fast

On the fast visit of 02 loading ends at 0.35 s, so the version that leaves the moment loading ends is gone at 350 ms, under the FLASH_MS 500 mark. The version that honors the hold fills 600 ms and the logo can be read. With the same code the slow visit of 2.4 s gives both versions 2400 ms, so this trap appears only on the fast visit.

A number that skips and looks stalled

A target that rises only when a resource arrives whole, printed as is, lets one 270 KB chunk raise the right board of 03 by 25 points at once. Between arrivals the same number stands for as long as 1.46 s, from 3.54 s to 5.00 s. The left board follows at CATCH_RATE 5 and reaches 95 percent of one arrival in 0.60 s.

A fixed radius that leaves a corner

In 06, pinning the radius to half the screen width, 103 px, leaves three corners (111.7, 171.6 and 155.1 px) covered after the opening ends. There are three and not two because the center sits toward the lower left, which splits the distances four ways, and 103 px passes only the nearest one at 84.1 px. The fix is MAX_RADIUS(), the distance 171.6 px to the farthest corner.

The HTML and React source of the nine scenes, which run on three loading seeds, is packed in the zip below, and you open it with du9w8544 as the password.

Change the Minimum Hold, the Letter Gap and the Opening Speed and How Much Does the Feel Shift? MIN_HOLD, STAGGER_MS and EXIT_MS Compared

Name Changed value Feel
A longer minimum hold 02 MIN_HOLD 0.6 → 1.4 On the fast visit the left exit start moves from 600 ms to 1400 ms so the logo stays 800 ms longer, while the 350 ms flash on the right does not change. The slow visit, where loading takes 2.4 s, exits at 2400 ms either way, so the value changes nothing there.
A tighter letter gap 05 STAGGER_MS 90 → 30 The total length drops from 960 ms to 660 ms (the last letter starts at 150 ms instead of 450 ms), but the 510 ms the spring needs to settle stays, so the letters nearly overlap and rise as one lump.
A faster opening 06 EXIT_MS 900 → 450 The average speed at which the radius grows to 171.6 px doubles from 191 px/s to 381 px/s (peak speed from 286 to 572 px/s), so the opening gets shorter but the eye struggles to follow the moment the page appears.

Before you change a value

Each of the three values is a constant in one scene, 02, 05 or 06, so change one value in one scene to keep the comparison fair. Read 02 against the 600 ms and 350 ms of the fast visit, 05 against the 960 ms of the first run and 06 against the 171.6 px of the first run. Once lap changes the seed changes and the loading timetable changes with it, so compare on the first run to match the figures above.

One Frozen Logo Screen Under Reduced Motion: Accessibility Through a Sentence for the Scene and the Current Progress

Enter, Space and R restart, and P and N drive the stage

The stage box is role="application" in all nine scenes, where Enter, Space, R and a click restart, P pauses and N steps once. The timetable of all nine scenes is empty, so N also restarts from the beginning. The demo plays to the end by itself with no input.

The progress bar, the status line and the done notice

Progress is read through aria-valuenow on a role="progressbar" element from 0 to 100 and updates only when the rounded value changes. The status line data-role="status" is aria-live="off" during the demo, so no frame is read out, and it switches to polite once a person first touches the stage. The done notice is written once to data-role="done" (role="status") when RESULT_S seconds are reached.

Reduced motion and a hidden tab

With prefers-reduced-motion on, the loop does not run and showRest() draws the still at REST_S seconds, and any input redraws that same still. For 02, 04, 06, 08 and 09 it is the page after the effect has finished, and for 01, 03, 05 and 07 it is the loading-complete screen with every value full. In all nine scenes visibilitychange stops the loop when the tab hides and resumes it on return.

Splash Screen FAQ: Building Without a Library, What to Do When Loading Is Too Fast and What You Need Before Putting It on the Web

Can I build a splash screen without a library

Yes. Canvas 2D and requestAnimationFrame run all nine scenes, and the sources contain no library, WebGL or CDN. For the animation side read the MDN requestAnimationFrame page, and for cutting by a radius read clip-path.

What should a splash do when loading is too fast

Add a minimum hold as in 02. When the exit start is the larger of the load finish and MIN_HOLD, the logo stays 600 ms even on the fast visit, and the slow visit leaves after the 2400 ms it waited. Raise the value to 1.4 s and the fast visit stretches to 1400 ms while the slow visit does not change.

Is this splash screen animation free download ready for the web, and what do I need first

The zip on this page holds the HTML and React source of the nine scenes. Build the value for loadFrac from the bytes really read and the total bytes, and connect one line that tells when loading ends. Reduced motion and the hidden tab are already in the source, so leave them as they are; the Page Visibility API page explains the hidden tab and the prefers-reduced-motion page explains the motion setting.

Enter the archive password

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