GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

ローディング アニメーション ロゴ 9種 — 進み具合を描く

読み込み中のロゴは、0から1の進み具合を何に変えて見せればよいでしょうか。03番はロゴの画素から拾った点がばねで定位置に戻り、05番はノイズのしきい値が下がってロゴがにじみ出し、07番は円周の点をロゴの輪郭と組にして溶け込ませます。

進み具合でロゴが仕上がるローディング画面を九つ、この場で動かしてみる

自動再生 · タイルを押すとその項目へ · すべて1つのzip

ローディング アニメーション ロゴ 9種 — 輪郭なぞり・水の満ち引き・点の集合・タイル返し・ノイズのにじみ・彗星の尾・形の溶け合い・厚みの重なり・数字の進み

前の二つは線(01)と面(02)でマークを満たし、真ん中の三つは点(03)・タイル(04)・画素(05)の単位で形を見せ、後ろの四つはまわりを回る尾(06)、円からマークへ変わる輪郭(07)、奥へ重なる厚み(08)、ばらつく数字をならす計算(09)を受け持ちます。九つの場面はどれも<canvas>の2D一枚に描かれ、スタイルシートにあるのは舞台の比率とボタン、フォーカスの枠、下の進み具合の帯だけです。CSSの@keyframesは一行もなく、動きはrequestAnimationFrameのタイムスタンプをためて16msの固定ステップを1フレームに最大6回解くループがすべて受け持ちます。

ロゴは自作の幾何マーク、進み具合はシードで作った仮の値

画面のマークは、頂点が上を向いた六角形(中心から頂点まで104px)に、外の頂点44px・内側14pxの四つ又の星の穴を開けた形で、どこかの会社やブランドのロゴではありません。roundCornersが頂点の両わきを切って2次曲線で丸めるので、外の輪郭は42点(周の長さ601.7px)、星の穴は32点(260.1px)になり、塗るときはevenoddの規則で外の内側かつ星の外側だけを満たします。進み具合も本物のダウンロードではありません。makeLoad(469)がシード469の整数を混ぜる乱数で1,300msのあいだに7つのかけらが届く表を作り、そのうち484msから758msまでの274msは受け取る量が49%で止まります。01〜08は到着のあいだを直線で結んだloadAtを、09は届いた瞬間だけ跳ねる階段のreportedAtを受け取るので、900msの時点の値は前者が0.585、09は0.49と食い違います。ソースのどこにもfetch・Math.random・Date.nowがないため、同じシードで開けば同じ絵が同じ時刻に出ます。

描画をキャプチャして数えた九つの場面の動き

動いたフレームの欄は、2秒のあいだに撮った24枚の間に起きた23回の変化のうち、画面が実際に変わった回数です。

場面 累積面積 動いたフレーム 強度の平均
01 輪郭なぞり 21.6% 17/23 94.0
02 水を満たす 15.5% 22/23 114.8
03 画素の点集め 39.6% 23/23 79.0
04 タイル返し 22.2% 17/23 80.7
05 ノイズのにじみ 17.7% 17/23 120.1
06 彗星の尾 26.9% 23/23 64.3
07 円からマークへ 16.2% 20/23 92.2
08 厚みの重なり 12.3% 23/23 73.8
09 パーセントの数え上げ 28.8% 23/23 137.6

一回の実演は2,000msで、最初の200msは進み具合0、最後の500msは受け取り終えた状態のまま止まります。01・04・05が17/23にとどまるのは、進み具合だけを見て描く場面なので、この二つの区間では描くものが変わらないためと考えられます。03はまだ順番が来ない点が散らばった位置でただよい、06は頭が回り続け、08はマークが左右に揺れるので、23枚のあいだずっとどこかが変わります。08の累積面積が12.3%で一番小さいのは、層が前面のすぐ後ろに重なって新しく塗られる面が狭いからだと考えられ、03はマークのまわりの半径128〜224pxの輪から点が飛び込んでくるので、39.6%と一番広くなると考えられます。

九つのうちどれをどの待ち画面に置くか

ブランドサイトに初めて入るときのように、ローディング 画面 ロゴがそのまま第一印象になる場所には01の輪郭なぞりが無難で、アプリのインストールや更新を待たせる画面では02の満ちていく水が状況を説明してくれます。サービス起動のスプラッシュには03、ダッシュボードの初回読み込みには04、ポートフォリオのイントロには05が合います。決済処理のように待ちがどれだけ続くか読めない表示には06、ログイン後に次の画面を待つ切り替えには07、ゲームやイベントページの入口には08、ファイルの書き出しやレポート生成のように数字を長く見守る待ちには09が向いています。

ローディング画面のロゴは、なぜ時間ではなく進み具合に結びつけるのか?

時間に結びつけた演出は、1秒でも3秒でも決めた長さで終わります。実際の読み込みがそれより長ければ描き終えたマークがいつまでも立ち尽くし、短ければ描きかけのまま次の画面へ移ってしまいます。この編の九つの場面は逆に、0から1のあいだの進み具合の値をひとつ受け取ってその値で絵を決めるので、仮の読み込みが274ms止まる区間では01のペンもその場で止まり、04の波面も先へ進まなくなります。受け取った量をそのまま見せれば止まった区間は画面でも止まって見え、それを隠さずにならす役を受け持つのが09です。進み具合が入ってくる道はtargetProgress()の一行で、ドラッグやキーで動かした値(manual)があればそれを、なければ仮の表のloadAt(loadMs)を返します。実サービスに付けるときはこの場所の仮の値だけを自分の進み具合に差し替えればよく、描く側には手を入れなくてすみます。

0から1までの数ひとつが、線の長さ・水面の高さ・点の数に分かれる計算

場面ごとに進み具合pを絵のどの量に変えるか、そして下の帯がどの値を知らせるかを表にまとめました。

場面 pを変える式 帯が知らせる値
01 引いた長さ = 861.4px × (1 − (1 − p)^1.6) p
02 水位がステップごとにpとの差を8%ずつ縮める 水位
03 混ぜた順番がp × 718より小さい点だけを目標へ 1.5px以内に入った点の割合
04 裏返り = (p × 1,120 − (d − 2) × 60) ÷ 400 p
05 しきい値 = 1 − p × 1.07 p
06 尾の点の数 = 4 + round(p × 86)、最大90 p
07 混ぜ具合は曲線をかけたpで、25・50・75%のとき0.109・0.5・0.891 p
08 層の数 = p × 14、小数部分は滑り込んでくる層 p
09 表示の値が階段の値との差を6%ずつ縮める 表示の値

01の曲線は前を速く引いて終わりをゆっくり閉じるので、進み具合25・50・75%の時点ですでに36.9%(318px)・67.0%(577px)・89.1%(768px)を引き終えています。帯の値が場面ごとに違うのは、帯と画面がずれないようにするためです。02は水が進み具合より遅れて満ちるので(一番大きな差は19.4ポイント)水位を、03は点が飛んでいる途中なので着いた点の割合を帯にのせます。その仕事をするのが共通の関数syncBarです。

    // 下の帯(role="progressbar") — 画面に描いた値と同じ値を帯の長さと aria-valuenow で知らせる。整数の % が変わったときだけ属性を書き換える
    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));
        }
    }

帯の長さはtransform: scaleX()だけで変わるのでレイアウトを計算し直さず、aria-valuenowは整数のパーセントが変わったときだけ書き換えるので、スクリーンリーダーに同じ値を何度も送りません。

01ロゴの輪郭を同じ速さでなぞり、ペン先が光るローダー

頂点の番号を同じ間隔で区切って打った点は丸い角に集まり、隣との間隔が0.22〜12.85px、59.3倍まで開くため、点の番号の代わりに累積の弧長表(362区間、合計861.4px)を二分探索して9回の比較で区間を見つけ、その区間の中で残りの距離の割合だけペン先を進めます。外の輪郭と星の穴を別々の色で引き、ペン先に半径18pxの光をのせ、進み具合70%から中が満ちていくので、ブランドサイトの初回ローディング画面のようにマークを一筆ずつ紹介する場所に合います。この場面の描画の値は累積面積21.6%、強度94.0、17/23フレームです。

ロゴの輪郭を同じ速さでなぞり、ペン先が光るローダー
新しいタブで開く

01 · ロゴの輪郭を同じ速さでなぞり、ペン先が光るローダー — 計測 累積面積 21.629% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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); }
    // ペン先 — その区間の中で残りの距離の割合だけ二点のあいだを結ぶ。だからペンは点の間隔に関係なく長さに比例して動く
    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) };
    }

02ロゴの形の中に水が満ち、水面が揺れるローダー

水面を48本の柱に分け、柱ごとに基準の水位へ引く力と減衰0.985を先にかけ、隣との高さの差に0.22を掛けてやり取りする処理を二回まわして波を横へ広げたあと、全体をマークの形で切り抜いて星の穴だけを空けておきます。水位が上がった分だけ水が落ちる位置の柱を下へ押して揺れを新しく起こすので、アプリのインストールや更新の待ちのように、満ちていくこと自体が状況の説明になる画面に向いています。測った結果は累積面積15.5%、強度114.8、22/23フレームでした。

ロゴの形の中に水が満ち、水面が揺れるローダー
新しいタブで開く

02 · ロゴの形の中に水が満ち、水面が揺れるローダー — 計測 累積面積 15.474% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WAVE_SPREADWAVE_DAMPFILL_RISE_RATE
    // 柱の波の1ステップ — ① 基準の水位へ引き寄せて減衰を掛けたあと ② 隣との高さの差を WAVE_SPREAD だけやり取りする(二回まわして横へ速く広げる)
    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];
                }
            }
        }
    }

03ロゴの画素から拾った点が、ばねで定位置に集まるローダー

画面外のキャンバスにマークを一度塗ってgetImageDataで読み、6pxの格子でアルファが127を超えるマスだけを目標点にするとブラウザの描画で718個になり、シード4693で混ぜた順番がp × 718より小さい点だけが外側の輪から出発して、加速度が −90(x − 目標)− 11·v のばねで寄っていきます。減衰比が0.58なので120px離れた点は9.1%行き過ぎて戻り、272msで初めて1.5px以内に入って624ms以降はその中にとどまるため、点が集まってマークになる瞬間を始まりの合図に使うサービス起動のスプラッシュ画面に合います。描画では九つの中で一番広い累積面積39.6%に、強度79.0、23/23フレームが出ました。

ロゴの画素から拾った点が、ばねで定位置に集まるローダー
新しいタブで開く

03 · ロゴの画素から拾った点が、ばねで定位置に集まるローダー — 計測 累積面積 39.592% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SAMPLE_STEP_PXSPRING_KSPRING_DAMP
    // 減衰ばね — 加速度 = −k(x − 目標) − d·v、速度を先に直してから位置を進める半陰的な積分(dt 0.016秒)
    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();
    }

04ロゴをタイルに割り、斜めの波の順に裏返して埋めるローダー

216pxの正方形を24pxのタイルに分けてマークにかかる64枚だけを残し、列と行の和で決まる斜めの段(13段)ごとに60msずつ遅れて出発して400msかけて裏返る1,120msの時間表を、進み具合が最初から最後までなぞります。裏返るタイルは横幅が|cos(πf)|で折れては開き、横向きに立った瞬間に明暗が0.45まで落ち、少し持ち上がって影を落とすので、マスが順に灯っていく感じが似合うダッシュボードの初回読み込みに使えます。測った値は累積面積22.2%、強度80.7、17/23フレームです。

ロゴをタイルに割り、斜めの波の順に裏返して埋めるローダー
新しいタブで開く

04 · ロゴをタイルに割り、斜めの波の順に裏返して埋めるローダー — 計測 累積面積 22.244% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TILE_SIZE_PXSWEEP_DELAY_MSFLIP_SHADE
    // 波面の時間表 — 斜めの段 d のタイルは (d − DMIN) × SWEEP_DELAY_MS に出発して FLIP_MS のあいだ裏返る。進み具合がこの時間表全体を 0 → 終わりまでなぞる
    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;
            // 横幅 = |cos| — 0° と 180° で元の幅、90° で横向きに立って幅 0
            const wide = Math.abs(Math.cos(ang));
            const lift = Math.sin(ang);
            const shade = 1 - FLIP_SHADE * (1 - wide);
            const front = f >= 0.5;

05ノイズのしきい値が下がり、ロゴがにじみ出すローダー

マークの中の画素25,952個それぞれに、4層の値ノイズと縁への近さを半分ずつ混ぜた値を最初に一度だけ求めておき、フレームごとに1 − p × 1.07で下がるしきい値と比べて超えた画素だけを塗り、しきい値のすぐ上の0.07の幅は白く焼けた帯として残します。進み具合20%で先に現れた画素は縁から平均3.3px(マーク全体の平均は15.3px)のところにあり、輪郭から浮かび上がってくるので、クリエイティブなポートフォリオのイントロのように形が少しずつ結ばれていく演出に向きます。描画の数値は累積面積17.7%、強度120.1、17/23フレームです。

ノイズのしきい値が下がり、ロゴがにじみ出すローダー
新しいタブで開く

05 · ノイズのしきい値が下がり、ロゴがにじみ出すローダー — 計測 累積面積 17.708% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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) {
                // 超えた画素 — 値が高いほど(縁の側)水色、低いほど紫
                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) {
                // 帯 — しきい値に近いほど白く焼く
                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);
    }

06ロゴのまわりを回る彗星の尾が、進み具合の分だけ伸びるローダー

マークを囲む周の長さ965.6pxの角の丸い四角の道を頭が1秒に0.55周ずつ回り、通った位置を90マスのリングバッファに残して、進み具合0・25・50・100%で尾を4・26・47・90点まで伸ばし、i番目の点は0.975^iの明るさで薄れていきます。進みが止まっても頭は回り続けるので、決済処理のように待ちが長引くかもしれない画面でも、止まった画面だと思われずにすみます。23/23フレームのあいだずっと動き、累積面積は26.9%、強度は64.3です。

ロゴのまわりを回る彗星の尾が、進み具合の分だけ伸びるローダー
新しいタブで開く

06 · ロゴのまわりを回る彗星の尾が、進み具合の分だけ伸びるローダー — 計測 累積面積 26.93% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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);
    }
    // i 番目に新しい位置(0 が頭)
    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)));
    }

07円周の点がロゴの輪郭へ溶け込むローダー

円とマークの輪郭を同じ弧長の間隔で96点に取り直したあと、円の始点を32個の候補で回してみて、組にした距離の二乗の和が一番小さい位置(マーク側の番号を27ずらした位置)を選び、ゆっくり・速く・ゆっくりの曲線で組どうしを補間します。この位置合わせひとつで和が1,501,470から76,223へ19.7分の1に減り、一番遠くまで動く点も129.1pxから32.8pxに縮むので、ログイン後に丸いスピナーがマークへ溶け込む切り替えに使えます。数値は累積面積16.2%、強度92.2、20/23フレームでした。

円周の点がロゴの輪郭へ溶け込むローダー
新しいタブで開く

07 · 円周の点がロゴの輪郭へ溶け込むローダー — 計測 累積面積 16.233% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MORPH_POINTSALIGN_SEARCHMORPH_EASE
    // 組にした距離の二乗の和 — 円の i 番目の点とロゴの (i + off) 番目の点
    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;
    }

08進み具合の分だけ層が重なり、厚みが生まれる立体ロゴのローダー

マークを4px間隔で最大14枚後ろに重ねますが、層の数をp × 14の小数で受け取って新しい層が18px深いところから滑り込むようにし、30°傾けた遠近(焦点560)で投影して奥の層から暗く塗っていきます。重なり終えると厚み56pxが下の頂点で19.5px開いて見え、左右22°の揺れの端では横に13.5px現れるので、ゲームやイベントページの入口画面のように立体感がほしい場所に合います。この場面は累積面積12.3%、強度73.8と小さく動きますが、23/23フレームを埋めました。

進み具合の分だけ層が重なり、厚みが生まれる立体ロゴのローダー
新しいタブで開く

08 · 進み具合の分だけ層が重なり、厚みが生まれる立体ロゴのローダー — 計測 累積面積 12.282% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

EXTRUDE_LAYERSLAYER_GAP_PXTILT_DEG
    // 縦軸まわりに yaw だけ回してから横軸まわりに −TILT_DEG 傾け、画面の倍率 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];
    }

09ばらつく実際の進みをなめらかに追って数え、終わるとロゴが一度ふくらむパーセントローダー

届いたときだけ跳ねる階段の進み具合を目標にして、表示の値が16msごとに残りの差の6%ずつ追いかけ、新しい値が96msを超えて来なければ目標を少しずつ前へ押しますが、押す量に0.96を繰り返し掛けてだんだん遅くします。既定の0.06での実演では表示の値が受け取った量を一度も超えず、逆戻りしたステップも0で、読み込み開始から1,536msで100%に届くとマークが320msのあいだ一度ふくらむので、レポート生成のように数字を長く見守る待ち画面に合います。描画の結果は累積面積28.8%、強度137.6で九つの中で一番強く、23/23フレームです。

ばらつく実際の進みをなめらかに追って数え、終わるとロゴが一度ふくらむパーセントローダー
新しいタブで開く

09 · ばらつく実際の進みをなめらかに追って数え、終わるとロゴが一度ふくらむパーセントローダー — 計測 累積面積 28.847% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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_AFTER_MS を超えて来なければ、目標を受け取った値より少しずつ前へ押す。押す速さに STALL_SLOWDOWN を繰り返し掛けてだんだん遅くする
        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;
        // 仮の進みを追うときは逆戻りしない — 這い進んだ分より小さい値が来ても、表示の値はその場で待つ
        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;
        }

進み具合が逆戻りする・長く止まる・ロゴの線が細い — ローディングのロゴが壊れるとき

この節の数値は、バニラのindex.htmlの関数を直接呼んで回し直した結果で、端末ごとに違うフレームの処理時間は測っていません。

進み具合が戻るとき、場面ごとに違う反応

01・04・05は状態を持たないので、進み具合をドラッグで戻すと線もタイルも画素もいっしょに巻き戻ります。受け取った量が減ることは実際にも起こりうるので、巻き戻りが演出として不自然な場所なら、値を渡す前に最大値だけを残しておくほうが安全です。09は仮の進みを追うあいだ目標が表示の値より小さくなるとその場で待ち、手で動かしたときだけ後ろへ戻ります。

274msの停止のあいだに数字が追い越してから引き返す

止まった区間で目標を押す量に上限と減速がないと、次の値が届く前に表示の値が受け取った量を超え、そのあと後ろへ下がります。09は押す量を残りの量の40%までに抑え、0.96の減速をかけているので、実際に押した量は最大2.44ポイントで済みました。減速を1にほどくと3.68ポイントまで押し出され、追いつく速さを0.15に上げると押した目標にまで追いついて、表示の値が受け取った量より最大1.82ポイント先に出ます。上で書いた「一度も超えない」は既定値0.06での結果です。終わりの合図を受けたあとも6%ずつしか縮めないと99%あたりで長くとどまるので、そのときだけ0.25で速く閉じ、数字はMath.floorで切り捨てて、99.5%で100が先に出ないようにしています。

点の間隔がそろわない輪郭と、つなぎ目の直線

点の番号でペンを進めると、同じ時間に0.22px進む区間と12.85px進む区間が混ざって、角ごとにペンがつかえます。二本の輪郭をひとつの表につなぐとき、つなぎ目にも距離を足すと外の端から星の穴へ渡る直線が一本引かれてしまいますが、arcTableはそこに0を足してペンを持ち上げたまま飛び越します。

線が細いと画素の標本が空になり、間隔を詰めると点が増えすぎる

03の標本は3pxから始めて6pxごとにマスをひとつ見る格子なので、6pxより細い線はマスの中心をよけると点をひとつも受け取れないことがあります。間隔を詰めると点は急に増えます。本文の718個はブラウザがアンチエイリアスまで塗った画面で数えた値で、関数を直接呼んで同じ読み取りを偶奇判定でまねると721個になり、その基準では4px間隔で1,621個(約2.25倍)、8px間隔で400個です。アルファのしきい値を0より大きいだけに下げると半透明の縁の画素まで点になり、読み取りを毎フレーム繰り返すと画素の数だけ仕事がふくらむので、初期化のときに一度だけ読みます。

波が暴れる強さと、いつまでも止まらない減衰

02のやり取りの強さを0.6に上げると、600ステップのうちに水面の高さが4.3×10^307まで跳ね上がります。ソースのコメントが注意しているとおり、0.5を超えると受け取った分より多く返してしまうためで、減衰を1にすると揺れが小さくなりません。波をフレームごとに一回ずつ解くとリフレッシュレートの高い画面ほど速く揺れるので、このソースは16msの固定ステップで解いています。

時間表の終わりと、幅0のちらつき

04の時間表の長さに最後のタイルの裏返り400msを入れないと、進み具合100%で最後の斜めの列が半分立ったまま終わります。横幅が0になるフレームではタイルが消えてちらつくので、幅は0.02より下げません。出発の間隔を30msに詰めるだけでも時間表が760msまで短くなり、13本の斜めの列がいっしょに裏返って、進み具合50%では64枚すべてが半分立ったままになるので、波面が見えなくなります。

始点と巻く向きがずれたモーフィング

07で始点をそろえないと、点が最大129.1pxを横切って形がねじれてからほどけます。円を角度で、マークを頂点の番号で取ると角のほうに点が集まって組がずれるので、どちらも弧長で取り直しました。二本の輪郭が逆向きに巻いているとどの始点でも和が減らないため、どちらも時計回りにそろえています。

先に求めていないノイズと、0で止まるしきい値

05がノイズをフレームごとに計算し直すと、25,952個の画素それぞれで4層ぶんの計算を毎回繰り返すことになるので、値は最初に一度求め、フレームでは比べるだけにしています。しきい値の終わりを0にすると100%でも帯が残って縁が焼けたまま終わるので、−0.07まで下げます。

尾の減衰が強いと、長さを伸ばしても見えない

06の減衰を0.9まで強めると明るさが29点目で5%を下回り、進み具合が上がって尾を90点まで伸ばしても見える長さは変わりません。0.975ではその境目が119点目なので、90点の尾が最後まで見えます。過去の位置を配列の先頭に差し込むやり方でバッファを扱うとステップごとに90マスを動かすことになり、リングバッファなら頭のマスの番号を回すだけで済みます。

層を塗る順番と傾き

08で手前の層から塗ると、奥の層が前面を覆ってしまいます。傾きを10°に減らすと下の頂点の開きが−0.7pxになって層を重ね終えても厚みが前面の後ろに隠れ、層の数を整数でしか増やさないと14回かくかくと途切れながら積み上がります。

九つのフォルダのバニラのソースを自分の進み具合と自分のマークに替えて回してみたいなら、ダウンロードしたzipを開くときに5t93rr73を入れてください。同じ計算を移したReact版もいっしょに入っています。

波の減衰・タイルの出発間隔・数字の追いつく速さを動かす変形と、動きを減らした画面のアクセシビリティ

名前 値 感じ
波の減衰を強める 02 WAVE_DAMP 0.985 → 0.94 柱を一本押したときの揺れが最初の山の10%を下回ってそのまま収まるまで、182ステップ(2,912ms)だったのが52ステップ(832ms)に縮みます。実演の進みで水面が基準の水位から外れる一番大きな高さも49.3pxから40.0pxに下がり、水が短く揺れてすぐ静まります。
タイルの出発間隔を広げる 04 SWEEP_DELAY_MS 60 → 120 時間表が1,120msから1,840msに伸び、一度にいっしょに裏返る斜めの列が7本から4本に、進み具合50%で半分立ったタイルが42枚から18枚に減ります。広くやわらかい波面が、細くくっきりした斜めの波に変わります。
表示の追いつく速さを上げる 09 PROGRESS_SMOOTH 0.06 → 0.15 ドラッグで0から50%へ一気に動かしたとき、残りの差が10%に縮むまで38ステップ(608ms)かかっていたのが15ステップ(240ms)になります。読み込み開始から640msの時点で受け取った量が49%のとき表示の値は32.5%から44.9%へ近づき、100%に届くのは読み込み開始から1,504msで、既定値の1,536msより早くなります。そのかわり止まった区間では、表示の値が受け取った量より最大1.82ポイント先に出ます。

表にない値も、一行ずつ変えてみる価値があります。01のTRACE_EASE_POWを1にすると25・50・75%でちょうどその割合(215・431・646px)を引く曲線なしの一定の速さになり、1より下げると前がゆっくりで終わりにまとめて閉じます。03のSPRING_DAMPを20に上げると減衰比が1.054になり、行き過ぎが消えるかわりに1.5px以内にとどまるまで800msかかります。

下の進み具合の帯はrole="progressbar"・aria-valuemin="0"・aria-valuemax="100"と「ロゴの読み込みの進み具合」という名前を持ち、aria-valuenowには進み具合ではなく、その瞬間に画面へ描いた値をのせます。だから02は水位を、03は着いた点の割合を、09は表示の値を知らせ、耳で聞く数字と目に見える絵が食い違いません。

キーボードだけでも九つの場面をすべて扱えます。キャンバスごとにtabindex="0"と説明がついていて、フォーカスが入るとEnterで最初からやり直し、Spaceで一時停止と再開、→・↑で5%進め、←・↓で5%戻し、Homeで0%、Endで100%を選び、Escを押すと手で動かした値を手放して仮の読み込みに戻ります。扱うキーだけ既定の動作を止めるのでページのスクロールはそのまま生きていて、「やり直す」・「一時停止」は本物の<button>、一時停止ボタンはaria-pressedで押された状態を知らせます。左下の状態の行は自動の実演が回るあいだaria-live="off"で黙っていて、人が初めてドラッグしたりキーを押したりボタンに触れたりした瞬間にpoliteへ変わり、その後の状態だけを読み上げます。

prefers-reduced-motionでreduceを選んだ画面では、requestAnimationFrameのループを始めません。かわりに400msごとにsetTimeoutが仮の読み込みを一段進め、波・ばね・裏返りのような途中の過程なしに、その進み具合の最後の姿と数字だけを階段のように描き替えます。04のタイルは半分立った姿がなく表か裏のどちらかで、09はふくらまずに受け取った値をそのまま見せ、06は頭を止めたまま尾を一度に満たし、時間の値clockで動いていた03のただよいと07のハイライト、08の揺れも止まります。この設定はmatchMediaのchangeリスナーが見張っているので、ページを開いたまま設定をオンにするとループを止めて今の進み具合の最後の姿に切り替え、オフにするとループがまた回り出します。

ロゴ アニメーション 作り方として、動画ファイルではなくコードで置くと何が変わるのか?

動画は決まった長さで再生される絵なので、読み込みが止まっても逆戻りしても、画面は自分の道筋どおりに流れていきます。コードで置いたマークは進み具合の値を受け取ってその値の姿を描くので、止まれば止まり、終わる瞬間に合わせて09のように一度ふくらむこともできます。進み具合の帯のaria-valuenow、動きを減らす設定への対応、キーボード操作も同じソースの中にいっしょに入ります。色はパレットの定数、速さと減衰は名前の付いた定数一行なので、ブランドの色や雰囲気に合わせるために動画を書き出し直す必要もありません。ローディング アニメーション おしゃれな例をいくつも見比べて選ぶときも、色と速さを一行で替えられるかどうかで手間が大きく変わります。

時間に結びつけてかけらを組み立てるロゴ アニメーション 作り方が必要ならイントロアニメーション9種、マークなしで物理計算によって回るローダーはキャンバスの物理ローダー9種、CSSだけで飾るローディング画面はローディング画面のデザイン9種に続きがあります。03のように画素を点にほどく手法は文字の画素を標本にするタイポグラフィ、画素単位の計算で場面を作るほうはキャンバスの特殊効果9種が近い内容です。

ロゴのローディング画面をサイトのトップに付けるときのよくある質問

実際の読み込みの進み具合は、コードのどこに入れればよいですか?

デモの進み具合はmakeLoad(469)が作った仮の表で、ネットワークへのリクエストはひとつもありません。01〜08はtargetProgress()が返すloadAt(loadMs)を自分の進み具合(0〜1)に替えればよく、09はincoming()の中のreportedAt(loadMs)の場所に、とびとびに届く値をそのまま入れます。09のならしと停止中の減速は、まさにそういう階段の値を整えるための計算です。

ローディング 画面 ロゴを自分のマークの形に替えられますか?

マークはLOGO_OUTER = roundCorners(hexVerts(LOGO_R), 0.2, 6)とLOGO_HOLE = roundCorners(starVerts(44, 14), 0.16, 3)の二つの点の配列なので、この二行を自分の輪郭の座標に替えれば九つの場面がその形を使います。01は弧長表のおかげでどんな輪郭でも同じ速さで引き、03は塗られた画素を読むので形を選びません。07は二本の輪郭が円と同じ時計回りに巻いていないと、位置合わせの和がきちんと減りません。

終わりの見えない待ちにも使えますか?

九つの場面は、0から1までの進み具合の値がある前提で描いています。進み具合の値がない待ちなら、06が一番近い選択です。06の頭は進みと関係なくステップごとにSTEP_LENずつ回り、尾の長さだけが進み具合に従うので、尾には分かっている範囲の値を入れ、残りは回る頭にまかせられます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。