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

AI処理中 アイコン 9パターン — 値で動くローディング

処理中 アイコンとは:軌道半径の値が大きいと粒子は広く散らばるが、値が0に近づくとコアに集まり、その間ハローリングの色相の値は完了色へ動き、トークン番号の値が上がるたびプレビューの文字が一つずつ増える。

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

これから紹介する九つはすべて requestAnimationFrame の上で動いており、画面に何を出すかを決める材料はただ一つ、0から1の進捗値か経過時間の値、または一度だけ反転する真偽値です。モデルもAPIもストリーミングも呼び出していません。08番の「表示された」文もページのソースにすでに書かれた固定文字列で、残り八つも同じ種類のタイマー駆動の数値を別のCSSプロパティやcanvasの描画呼び出しに振り分けているだけです。以下の順番は、01番の色相値による単純な一プロパティ更新から、09番のように値が表示中のマークアップ全体を丸ごと入れ替えるところまで進みます。

9つのパターンに入る前に、どこで壊れるかを先に知っておきます

九つとも固定の2秒サイクル(LOOP = 2.0)で回っていて、そのサイクルは本物のリクエストが実際どれだけかかるかをまったく知りません。01番を本物のAPI呼び出しの前に置いて停止条件を一切書かなければ、リングは完了色へ動いては開始色へ戻ることを2秒ごとに無限に繰り返します。三周目、四周目を見ているユーザーにとってそれは「まだ処理中」ではなく「壊れている」としか読めません。アニメーション自体には、これがシミュレーションだと伝える手段が何もないからです。01番のAI_LOADER_HUE_TRANSITION_MSの窓や09番のSWITCH_ATによる分岐は本物の進捗マーカーのように見えますが、実際はLOOPに結びついたカレンダー時間の定数であって、実際の完了イベントとは無関係です——これはデモの既定値であって約束ではないと考えるべきです。期待値を正しく管理するとは、自分の実際のリクエストが解決した瞬間に本物の停止関数を呼ぶこと(requestAnimationFrameを取り消し、完了状態に切り替えること)であって、「処理中」が真でなくなった地点を過ぎても九つのどれかをループさせ続けないことです。

9つのAIローディングパターンを測ります

以下の数値は各デモ自身の画面録画パスから得たものです。累積面積はループ全体でタイルのピクセルがどれだけ変化したかを示し、平均強度は変化した箇所のピクセルごとの平均バイト差、動いたフレーム数は23回のフレーム間差分のうち実際にピクセルが動いた回数です。

# パターン 累積面積 平均強度 動いたフレーム
01 ハローリング色相シフト 22.139% 30.8 23/23
02 オーブモーフパルス 30.089% 40.9 23/23
03 粒子の渦とコア収束 25.456% 60.7 23/23
04 ニューラルノードパルス 11.583% 69.1 23/23
05 カードコンテンツストリーム充填 14.256% 105.4 20/23
06 ボタンアイコンスパークルモーフ 10.111% 175.4 23/23
07 聴き取り中波形 17.854% 100.9 23/23
08 ゴーストテキストキャレット表示 46.62% 52.6 20/23
09 モーション低減時の静止バッジ 2.808% 120.3 17/23

08番のゴーストテキスト表示は文中のほぼすべての文字が有効なフレームごとに色を塗り直すため、累積面積が46.62%ともっとも大きくなります。一方09番は2.808%ともっとも低く、これは意図した設計です——モーション低減が検知された時点で動きをすべて静止テキストのバッジに置き換えることが目的なので、面積が小さいこと自体が不足ではなく正解です。09番の動いたフレームが17/23と他より少ないのも同じ理由で、バッジは現れる瞬間とドットが止まる瞬間の二回だけ描き直せば十分で、23回の全サンプルで動く必要がありません。

01色相値が完了色へ動くハローリングAIローダー

conic-gradientで描いたリングは一定のROTATE_SPEEDで回り続ける一方、色相の値はループ末尾のAI_LOADER_HUE_TRANSITION_MSの窓に入ってようやく完了色へ動き始めます。smooth()による三次イージングがその窓の中でBLUEからDONEへ滑らかに混ぜ合わせるので、色の変化が唐突な切り替えではなく落ち着いた収束として見えます。リングとコアは毎フレーム同じ補間色を共有しており、二つの要素がばらばらに動いて見えることがありません。

色相値が完了色へ動くハローリングAIローダー
新しいタブで開く

01 · 色相値が完了色へ動くハローリングAIローダー — 計測 累積面積 22.139% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

conic-gradientAI_LOADER_HUE_TRANSITION_MSROTATE_SPEED
  function draw(sec) {
    var angle = (sec * ROTATE_SPEED) % 360;
    var p = (sec % LOOP) / LOOP;
    var winStart = 1 - (AI_LOADER_HUE_TRANSITION_MS / 1000) / LOOP;
    var mixT = p <= winStart ? 0 : smooth((p - winStart) / (1 - winStart));
    var col = mix(BLUE, DONE, mixT);
    var col2 = mix(BLUE, DONE, mixT * 0.6);

    ring.style.setProperty('--angle', angle + 'deg');
    ring.style.setProperty('--c1', rgbStr(col, 1));
    ring.style.setProperty('--c2', rgbStr(col2, .35));
    var corePulse = 1 + 0.08 * Math.sin(sec * Math.PI * 2 / LOOP * 3);
    core.style.setProperty('--core-scale', corePulse.toFixed(3));
    core.style.setProperty('--core-c', rgbStr(col, 1));

    var pct = Math.round(p * 100);
    pctEl.textContent = pct + '%';
  }

02待機時間の値が伸びるほど呼吸が広がるオーブパルス

accumの値がループ全体で0から1へ上がり、それをeaseSine()に通すことで、オーブの呼吸そのものの振れ幅がループの経過とともに広がっていきます。実際の呼吸の上下運動は別のBREATH_HZサイン波が担っており、スケール・ぼかし・グローの三つはすべて同じ合成値から毎フレーム読み出されるため、広い呼吸は必ず柔らかい輪郭と明るいグローを同時に伴います。

待機時間の値が伸びるほど呼吸が広がるオーブパルス
新しいタブで開く

02 · 待機時間の値が伸びるほど呼吸が広がるオーブパルス — 計測 累積面積 30.089% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BREATH_HZAMP_BASEAMP_GROW
  function draw(sec) {
    var t = sec % LOOP;
    var accum = Math.min(sec, LOOP) / LOOP;      // 待機時間の値がどれだけ積み上がったか(0-1)
    var amp = AMP_BASE + AMP_GROW * easeSine(accum);
    var wave = Math.sin(t * Math.PI * 2 * BREATH_HZ);
    var scale = 1 + amp * wave;
    var blur = 12 + 9 * Math.max(0, wave);
    var glow = 0.35 + 0.25 * Math.max(0, wave);

    orb.style.setProperty('--scale', scale.toFixed(4));
    orb.style.setProperty('--blur', blur.toFixed(1) + 'px');
    orb.style.setProperty('--glow', glow.toFixed(3));

    var pct = Math.round((t / LOOP) * 100);
    pctEl.textContent = pct + '%';
  }

03軌道半径の値が縮みコアに集まる粒子の渦

26個の粒子は黄金角(約137.508度)ずつずらして配置されており、同じ光線上に二つ重なることがありません。それぞれの粒子は自分専用の軌道半径の値を持ち、進捗が上がるにつれてMAX_RからCORE_Rへ向かって縮んでいきますが、その縮み方はAI_LOADER_ORBIT_SHRINK_EASEでイージングされているため線形ではありません。粒子ごとのjitter値が同じ進捗地点でも半径をわずかに違わせているおかげで、渦が一枚のリングとして均一に収縮するのではなく、緩やかな雲のように見えます。

軌道半径の値が縮みコアに集まる粒子の渦
新しいタブで開く

03 · 軌道半径の値が縮みコアに集まる粒子の渦 — 計測 累積面積 25.456% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AI_LOADER_ORBIT_SHRINK_EASEGOLDENMAX_R
    var cx = W / 2, cy = H / 2;
    var p = (sec % LOOP) / LOOP;
    var shrink = Math.pow(p, 1 / AI_LOADER_ORBIT_SHRINK_EASE);   // 軌道半径の縮小イージング

    for (var i = 0; i < particles.length; i++) {
      var q = particles[i];
      var angle = q.baseAngle + sec * q.speed;
      var r = CORE_R + (MAX_R - CORE_R) * (1 - shrink) * q.jitter;
      var x = cx + r * Math.cos(angle);
      var y = cy + r * Math.sin(angle) * 0.86;
      ctx.beginPath();
      ctx.arc(x, y, q.size, 0, Math.PI * 2);
      ctx.fillStyle = i % 2 === 0 ? DOT : DOT_DEEP;
      ctx.shadowColor = DOT;
      ctx.shadowBlur = 10;
      ctx.fill();
      ctx.shadowBlur = 0;
    }

04遅延値をたどってノードを渡るニューラルパルス

4列3行のノード格子が辺でつながれており、各ノードの遅延値は起点の角からの格子距離(dist * STEP、さらに小さなノードごとのゆらぎを加えたもの)で決まるため、パルスはいつも左上から始まって外側へ広がります。各辺は両端の遅延値のうち小さい方を受け継ぎ、経過時間がその遅延を超えると暗い線の上にもう一本明るい線が重なって描かれ、EDGE_MSの間に辺の長さの0%から100%まで伸びます——光は本当にノードからノードへ伝わっていくのであって、すべての線が一斉に光るわけではありません。

遅延値をたどってノードを渡るニューラルパルス
新しいタブで開く

04 · 遅延値をたどってノードを渡るニューラルパルス — 計測 累積面積 11.583% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STEPEDGE_MSPULSE_W
    for (var i = 0; i < edges.length; i++) {
      var ed = edges[i];
      var pa = pos(nodes[ed.a]), pb = pos(nodes[ed.b]);
      ctx.beginPath();
      ctx.moveTo(pa.x, pa.y);
      ctx.lineTo(pb.x, pb.y);
      ctx.strokeStyle = LINE_DIM;
      ctx.lineWidth = 2;
      ctx.stroke();

      var local = t - ed.delay;
      if (local > 0) {
        var frac = clamp01(local / EDGE_MS);
        ctx.beginPath();
        ctx.moveTo(pa.x, pa.y);
        ctx.lineTo(pa.x + (pb.x - pa.x) * frac, pa.y + (pb.y - pa.y) * frac);
        ctx.strokeStyle = LINE_LIT;
        ctx.lineWidth = 3;
        ctx.stroke();
      }
    }

05埋まった行数の値が増える結果カードのストリーム読み込み

6本のスケルトン行はそれぞれインデックス番号にLINE_MSを掛けた分だけずらして充填を始め、そこからFILL_MSの間にsmooth()イージングで幅を0%から100%まで伸ばしつつ、色もSKELETONの平板な色調からTEXTの本文色調へ同じ速さで混ざっていきます。doneCountの集計はある行の局所進捗が本当に1に達したときだけ増えるので、パーセント表示は「充填し始めた行数」ではなく「完全に実色になった行数」と正確に一致します。

埋まった行数の値が増える結果カードのストリーム読み込み
新しいタブで開く

05 · 埋まった行数の値が増える結果カードのストリーム読み込み — 計測 累積面積 14.256% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LINE_MSFILL_MS--skeleton
  function draw(sec) {
    var t = sec % LOOP;
    var doneCount = 0;
    for (var i = 0; i < lines.length; i++) {
      var local = t - i * LINE_MS;
      var raw = clamp01(local / FILL_MS);
      var eased = smooth(raw);
      fills[i].style.width = (eased * 100).toFixed(1) + '%';
      fills[i].style.background = rgbStr(mix(SKELETON, TEXT, eased));
      lines[i].classList.toggle('is-active', raw > 0 && raw < 1);
      if (raw >= 1) doneCount++;
    }
    pctEl.textContent = Math.round((doneCount / lines.length) * 100) + '%';
  }

06完了値が反転すると輝きに変わるボタンアイコン

スピナー形と輝き(スパークル)形は同じ8個の半径値として、異なる大きさ(スピナー用のR_RING、交互のR_OUT・R_IN)であらかじめ用意されています。DONEという真偽値がtrueに反転した瞬間、二つの独立したSVGパスを入れ替えるのではなく、morphTがMORPH_MSの間にその8個の半径値を一方の形の数値からもう一方へイージングしながら補間します。反転後の0.5秒間は短いスケールのパルスが重なり、スピナーの回転自体も反転後は元の速度の12%まで落ちるので、完了したはずの形の下でスピナーがまだ回り続けているようには見えません。

完了値が反転すると輝きに変わるボタンアイコン
新しいタブで開く

06 · 完了値が反転すると輝きに変わるボタンアイコン — 計測 累積面積 10.111% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pathDONEMORPH_MS
    var t = sec % LOOP;
    var localFlip = t - FLIP_AT;

    if (localFlip >= 0) { DONE = true; } else { DONE = false; }

    var morphT = DONE ? smooth(clamp01(localFlip / MORPH_MS)) : 0;
    var radii = [];
    for (var i = 0; i < 8; i++) {
      var spinnerR = R_RING;
      var sparkleR = (i % 2 === 0) ? R_OUT : R_IN;
      radii[i] = spinnerR + (sparkleR - spinnerR) * morphT;
    }
    var angleOffset = (sec * ROT_SPEED) % 360 * Math.PI / 180;
    var rot = DONE ? angleOffset * 0.12 : angleOffset;
    path.setAttribute('d', buildPath(radii, rot));

    var pulse = 1;
    if (DONE && localFlip < 0.5) {
      pulse = 1 + 0.22 * Math.sin(clamp01(localFlip / 0.5) * Math.PI);
    }
    path.setAttribute('transform', 'scale(' + pulse.toFixed(3) + ')');
    path.setAttribute('fill', DONE ? 'var(--done)' : 'var(--spin)');

    pctEl.textContent = DONE ? '完了' : '処理中';

07入力レベルの値がバーの高さを揺らす聴き取り中波形

20本のバーはそれぞれ固定シードの二つのサイン周波数(主な包絡線を作るfreqAと、その上に乗る小さなゆらぎのfreqB)を持ち、毎フレームそれらを合成して1本ごとの入力レベルの値を作り、その値がBASE_HからMAX_Hの間で高さを決めます。マイクにはどこも触れていません——このファイルのどこにもgetUserMediaの呼び出しはなく、seeded()はバーの番号を種にした決定的な擬似乱数関数なので、波形は有機的に見えながら毎回まったく同じ形で再現されます。

入力レベルの値がバーの高さを揺らす聴き取り中波形
新しいタブで開く

07 · 入力レベルの値がバーの高さを揺らす聴き取り中波形 — 計測 累積面積 17.854% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

seededfreqABASE_H
  function draw(sec) {
    var t = sec;
    for (var i = 0; i < N; i++) {
      var q = bars[i];
      var env = 0.5 + 0.5 * Math.sin(t * q.freqA * Math.PI * 2 + q.phase);
      var jit = Math.sin(t * q.freqB * Math.PI * 2 + q.phase * 1.7) * 0.22;
      var level = clamp01(env + jit);
      var h = BASE_H + (MAX_H - BASE_H) * level;
      bels[i].style.height = h.toFixed(1) + 'px';
      bels[i].style.opacity = (0.55 + level * 0.45).toFixed(2);
    }
    var avg = 0;
    for (var j = 0; j < N; j++) avg += parseFloat(bels[j].style.height);
    avg = Math.round((avg / N - BASE_H) / (MAX_H - BASE_H) * 100);
    pctEl.textContent = avg > 8 ? '聞き取り中' : '待機';
  }

08トークン番号の値が上がるたび一文字ずつ現れるゴーストテキスト

読み込み時に一度だけ決まった文が文字配列に分割され、AI_LOADER_TOKEN_REVEAL_MSごとに一段ずつ上がるトークン番号の値が、その番号までの文字を薄いゴースト色から本来の色へ何文字切り替えるかを決めます。切り替えは瞬時ではなく各文字ごとのFADE_MSの窓でゆっくりフェードします。キャレット要素は現在の文字のoffsetLeft・offsetTopを使って表示済みと未表示の境目を追いかけ、本物のテキストカーソルのように表示の先頭を歩きながら、独立したBLINK_MSのタイマーで点滅します。

トークン番号の値が上がるたび一文字ずつ現れるゴーストテキスト
新しいタブで開く

08 · トークン番号の値が上がるたび一文字ずつ現れるゴーストテキスト — 計測 累積面積 46.62% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AI_LOADER_TOKEN_REVEAL_MSFADE_MSBLINK_MS
  function draw(sec) {
    if (!GHOST) readColors();
    var t = sec % LOOP;
    var elapsedMs = t * 1000;
    var counter = Math.max(0, Math.min(LEN, Math.floor(elapsedMs / AI_LOADER_TOKEN_REVEAL_MS)));

    for (var i = 0; i < LEN; i++) {
      var revealAt = i * AI_LOADER_TOKEN_REVEAL_MS;
      var fade = clamp01((elapsedMs - revealAt) / FADE_MS);
      els[i].style.color = rgbStr(mix(GHOST, REAL, fade));
    }

    var target = counter < LEN ? els[counter] : els[LEN - 1];
    if (target) {
      var left = counter < LEN ? target.offsetLeft : target.offsetLeft + target.offsetWidth;
      var top = target.offsetTop;
      caret.style.transform = 'translate(' + left.toFixed(1) + 'px,' + top.toFixed(1) + 'px)';
      fill.style.width = left.toFixed(1) + 'px';
    }
    var blinkOn = Math.floor(elapsedMs / BLINK_MS) % 2 === 0;
    caret.style.opacity = blinkOn ? '1' : '.15';
  }

09モーション低減値を検知すると即座に文言へ変わる処理中バッジ

prefers-reduced-motionは一度だけ読み取られますが、この項目はその値がループ途中のSWITCH_ATで反転する瞬間をあえてシミュレートしています。反転前は三つの点がそれぞれ独立したBOUNCE_HZのサイン位相で跳ね、反転後はバッジがフェードインしながら、1をわずかに超えてから落ち着く臨界減衰のspring()近似でスケールアップします——実際のコードが毎フレームではなく一度だけ行うはずの検知処理の代わりを、ここではこの近似が担っています。この項目以外の八つはモーション低減がオフのときは通常どおり動き続けており、09番はオンになったとき八つ全部がどう収束すべきかを示すために存在します。

モーション低減値を検知すると即座に文言へ変わる処理中バッジ
新しいタブで開く

09 · モーション低減値を検知すると即座に文言へ変わる処理中バッジ — 計測 累積面積 2.808% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SWITCH_ATspringBOUNCE_HZ
  function draw(sec) {
    var t = sec % LOOP;
    var on = t >= SWITCH_AT;   // モーション低減値のシミュレーション

    if (on) {
      var localT = (t - SWITCH_AT) / SPRING_MS;
      var s = spring(localT);
      badge.style.opacity = '1';
      badge.style.transform = 'scale(' + (0.7 + 0.3 * s).toFixed(3) + ')';
      dots.length && (root.querySelector('.ld__dots').style.opacity = '0');
      stateEl.textContent = '動きの軽減: オン・処理中バッジ';
    } else {
      badge.style.opacity = '0';
      badge.style.transform = 'scale(.7)';
      root.querySelector('.ld__dots').style.opacity = '1';
      for (var i = 0; i < dots.length; i++) {
        var phase = i * (Math.PI * 2 / 3);
        var y = Math.sin(t * BOUNCE_HZ * Math.PI * 2 + phase);
        dots[i].style.transform = 'translateY(' + (y * 7).toFixed(1) + 'px)';
        dots[i].style.opacity = (0.55 + 0.45 * (0.5 + 0.5 * y)).toFixed(2);
      }
      stateEl.textContent = '動きの軽減: オフ・動作中';
    }
  }

チャット・ボタン・物理演算ローダーとの違いを説明します

9種のAIチャットローディングアニメーションはメッセージ吹き出しとチャットUI特有の三点または流し込みテキストの比喩を中心に組み立てられていますが、上の九つはボタンやカードヘッダー、ツールバーのアイコンに単独で置くための表示であって、会話スレッドの中身ではありません。もっとも近い親戚は05番のカードストリーム充填ですが、それも吹き出しに返信を打ち込むのではなく、結果カード内のスケルトン行を埋めていくものです。9種のローディングボタン状態はクリックからスピナー、成功チェックまで一つの要素のライフサイクル全体をモデル化していますが、06番はそのアイドルから完了までの流れを借りつつ、待機状態自体はPromiseが解決する状態機械ではなくタイマーで無期限に動き続けます。

9種のキャンバス物理ローダーは流体の表面や衝突する粒子群、セルオートマトンといった本物の物理シミュレーションを計算しており、03番の粒子の渦と04番のノードと辺のcanvas描画も同じctx.fillRect・ctx.arcの描画方式を共有しています。違いはそれぞれの値が何を表しているかです——あの九つの値は物理演算のステップを進めますが、03番と04番の値はそれぞれ軌道半径と伝搬遅延にすぎず、衝突も力の計算も一切含まれていません。

試せる3つのバリエーションとreduced-motionで消えるもの

バリエーション 変更した値 どう感じるはずか
ハローリングの色相遷移速度値を調整する 01 AI_LOADER_HUE_TRANSITION_MS 640 → 260 値を短く縮めると進捗値が少し上がっただけで色が一気に変わり、もう終わりそうに見えてしまい、値を伸ばすと色はほぼ最後まで変わらず、余裕がある印象になるはずです。
粒子の軌道半径縮小イージング値を調整する 03 AI_LOADER_ORBIT_SHRINK_EASE 2.2 → 4.8 イージング値を線形に近づけると粒子は一定の速さでコアに集まる感じになり、値を急なカーブに変えると序盤はほとんど動かず終盤で一気に集まる方に近づくはずです。
ゴーストテキストのトークン表示間隔値を調整する 08 AI_LOADER_TOKEN_REVEAL_MS 62 → 26 間隔値を短く縮めると文字がほぼ一気に出てくるはずで、値を伸ばすと一文字ずつはっきり読める速さに近づくはずです。

九つともprefers-reduced-motionをページ読み込み時に一度だけ読み、毎フレームは読みません。そのメディアクエリがreduceを返すとループは一度も始まらず、サイクル上の決まった一点だけを静止フレームとして描きます(09番はドットではなく、すでに落ち着いたバッジの状態そのものを描きます)。どのタイルもrole="application"・tabindex="0"・aria-labelを備え、同じハンドラがクリック・Enterキー・スペースキー(どちらもループの切り替え)・Rキー(再スタート)に応え、さらにリサイズ時にはそのサイズでタイルを描き直すリスナーも付いているので、マウスを使わないキーボードだけの訪問者でも九つのどれでも再生・一時停止・再スタートできます。九つのソースファイルは1パターンにつき1本、この記事のデモと同じフォルダの379w2wxyの奥にあります——上の三つの値をバリエーションの目標値へ動かす前に一本開いておくと、測定表の上では問題なく見えても実際の処理中表示では止まって見える値に気づけます。

どこで実際に使うのかを説明します

06番のボタンアイコンモーフは「要約する」「書き直す」といったインラインのAIアクションボタンに向いています。DONEが反転する前後でボタン自体の外形はまったく同じままアイコンの中身だけが入れ替わるので、レイアウトの他の部分が動く必要がありません。05番のカードストリーム充填は、本当にセクションごとに内容が埋まっていくAI要約カードやレポートカードに合っています。各行のスケルトンから本文色調への切り替えは、その行の本物のコンテンツが実際に準備できたときだけ進むので、まだ存在しないコンテンツの上を一律にアニメーションさせることがありません。

09番は残り八つのどれか一つの代わりではなく、そのすべてが必要とする受け皿としてどこにでも登場します。ai loading animation css的な処理中表示を出荷する製品なら、訪問者のOSがモーション低減を報告した瞬間にプレーンなテキストへ収束する仕組みと必ず組み合わせるべきで、09番のバッジはすでに作られ、すでに上で測定済みのその収束そのものです。

AI処理中の表示は他にどこで見られますか

処理中 アイコン くるくるで検索すると@keyframesだけで回る単純なスピナーがたくさん見つかり、それも出発点としては悪くありませんが、その多くは01番の色相シフトや06番の形状モーフのように「まだ処理中」と「たった今終わった」を区別していません。keyframesのループは二つの固定状態の間を補間するだけで、リクエストが本当に解決したかどうかを知らないからです。処理中 アイコン フリーで配布素材を探す場合も同じ問いを当てはめてください——本物の停止条件を持っているかどうかです。上の罠の節はあなたが自分で書いたパターンだけでなく、配布されているパターンにも同じように当てはまります。

よくある質問にお答えします

九つのうちどれかは本物のAIモデルやAPI、ストリーミング応答とつながっていますか

いいえ。九つとも requestAnimationFrame を通してローカルのタイマー値を読んでいるだけです。08番の表示文はページのソースにすでに書かれた固定文字列で、この記事のzipの中のどのファイルもネットワークリクエストを一切行いません。

08番のゴーストテキスト表示はAIと関係ない普通のタイプライター演出としても使えますか

はい——SENTENCEを別の文字列に差し替えるだけで、AI_LOADER_TOKEN_REVEAL_MS・FADE_MS・BLINK_MSはそのままで構いません。表示のロジック自体は文の意味がAIに関係あるかどうかに依存しておらず、ただ文字配列を順番にたどっているだけだからです。

09番はモーション低減のパターンなのに、なぜ動いたフレームが23回中17回しかないのですか

静止バッジはほとんどの時間じっとしているのが正しい姿だからです。ドットがフェードアウトしてバッジがフェードインする瞬間と、スプリングが落ち着く瞬間の二回だけ描き直せば十分で、サンプルされた全フレームで動く必要はありません。もしここで動いたフレーム数が高ければ、それは実際にはモーションが低減されていないことを意味してしまいます。DOMバッジをインラインの考え中 アニメーション用svgに置き換えても、駆動する値そのものは変わらないので同じ低いフレーム数のままです。

出典: MDN — prefers-reduced-motion · MDN — conic-gradient() · MDN — progressbar ロール · MDN — status ロール · MDN — Window.requestAnimationFrame()

関連記事: 【コピペOK】AIチャット ローディング アニメーション css 9選 · ボタン ローディング 9種 — スピナーから成功チェックまでの状態遷移 · ローディング アニメーション js 9パターン — 物理演算で作る — 一つ目は上の九つと同じ構成をチャット吹き出し専用の比喩に置き換えたもの、二つ目はクリックから成功までボタン一つの全ライフサイクルをモデル化したもの、三つ目はDOM要素の代わりにキャンバスのピクセルを使いながら03番・04番と同じ「一つの値がすべてを動かす」考え方を保っています。

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

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