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

css プログレスバー 9選 — ゲーム風に満ちる9つの方式

css プログレスバー:9つのうち1つは値が減ると帯が先に狭まり赤い残光が後を追って縮み、1つは値が上限を超えるとその超過分が半透明の層として重なり、もう1つは補助値が上限に達すると帯の不透明度の値が跳ねてから消えます。

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

これら9つのデモはいずれもサーバーと通信せず、localStorage にも触れず、Math.random も一度も呼びません。それぞれが1つの数値状態と少数の定数だけで、値の増減がどう見えるかを決める小さなHPバー・リソースバーの実装です。この狙いの狭さのおかげで、どの値が変わるのか——体力・上限・補助のスタガー値のどれか——を先に確認すれば、コードを開く前にパターンの名前が分かります。「css プログレスバー デザイン」で検索して出てくる記事の多くは色や角丸といった見た目だけを比べますが、この記事はゲーム内の状態イベントが帯の意味そのものを変える9つに絞っています。

9つのパターンの解剖 — どの値がいつ変わるか

9つとも自前の requestAnimationFrame ループを持ち、どれも Math.random を一度も呼びません。07番の4本のレーンは固定のサイン波(LANES が各レーンの freq と phase を持つ)で動き、残りは固定の EVENTS・BUILD_EVENTS・JOLTS 配列を順に踏むため、何度開いても同じ再生になります。9つのうち6つはCSSトランジション任せではなく、フレームごとに本当の減衰や振動を計算します。01番の trailHP は指数的な LERP_RATE で現在値へ遅れて近づき、02番のエッジの発光と05番の明るさの拍動はどちらも Math.exp ベースの減衰(GLOW_TAU と PERIOD_MIN/PERIOD_MAX)で落ち着き、04番の超過分は DECAY_RATE で上限の下へ戻り、06番の burstAt() は指数減衰にコサインを掛けて放出スプリングの RELEASE_ZETA を形にし、08番の snapOpacity() は SPIKE_DUR の間だけ跳ねてから DROP_DUR で線形に0へ落ち、その後は別枠の RECOVER_DUR が帯を再び満たします。残り3つは違う仕組みで収まります。03番の stateFor() は固定の CRACK_DUR/STAGGER の窓でピップの亀裂と塗りを進めるだけで継続的な減衰はなく、07番は valuesAt() が毎フレーム各レーンのサイン波を読み続けるため訪問者が何もしなくても止まらず動き、09番の右側の帯はフレームごとの計算を一切せず目標値へ中間段階なしにスナップします。リスナーはどれも実際の操作にのみ答え、自動再生からのイベントは0件です。01〜05・07・08・09番の8つは click に Enter・Space 用の keydown を組み合わせ、06番だけが例外で pointerdown・pointerup・pointerleave・pointercancel に keydown・keyup まで加えて、押し続ける操作とキーボードの等価な操作の両方を同じ扱いで受け付けます。9つとも isDemo の自動再生状態から始まり、最初のクリックかキー入力が human() を呼ぶまで自前の配列を再生し続け、human() は is-demo クラスを外してその自動再生を完全に止めます — 09番だけが例外で、左右を並べて見せることが項目の目的そのものなので、訪問者が操作を始めた後も左側のループは requestAnimationFrame の上で回り続けます。

パターン別コード

01被弾フラッシュ減少切替 — 値が減ると帯がすぐ縮み、その後を赤い残光がゆっくり追って狭まる方式

値が減った瞬間に帯の幅が先に狭まり、その差分の赤い幅は遅れてゆっくり縮んでいきます。戦闘画面のHP表示や、瞬間的な被害を強調したい状態バーに向く方式で、赤い残光そのものが直前にどれだけ減ったかを見せる記録になります。

被弾フラッシュ減少切替 — 値が減ると帯がすぐ縮み、その後を赤い残光がゆっくり追って狭まる方式
新しいタブで開く

01 · 被弾フラッシュ減少切替 — 値が減ると帯がすぐ縮み、その後を赤い残光がゆっくり追って狭まる方式 — 計測 累積面積 2.853% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyHit()LERP_RATEFLASH_TAU
  function render() {
    var mp = Math.max(0, Math.min(100, mainHP));
    var tp = Math.max(0, Math.min(100, trailHP));
    fill.style.width = mp.toFixed(2) + '%';
    trailEl.style.width = tp.toFixed(2) + '%';
    track.setAttribute('aria-valuenow', String(Math.round(mp)));
    valueEl.textContent = Math.round(mp) + ' / ' + HP_MAX;
  }

  function applyHit(dmg, ts) {
    trailHP = mainHP; // 残光は被弾直前の値から始まり、遅れて追いつく
    mainHP = Math.max(0, mainHP - dmg);
    flashStart = ts;
  }

数値が語ること

01番は累積面積2.85%・平均強度97.7を23サンプル中21フレームで示し、applyHit() が被弾の瞬間に残光の値を被弾直前の値へ合わせたあとは、毎フレーム LERP_RATE が新しい値へ指数的に近づけ、FLASH_TAU が赤いフラッシュを減衰させています。

02回復ティック発光切替 — 値が一定間隔で少しずつ上がるたび帯の先端が光る方式

値が短い間隔で一段ずつ上がるたび、帯の先端の明るさの値が一瞬強くなってから落ち着きます。自動回復中の体力・気力表示や、待機中に少しずつ満ちていくリソースバーに向く方式で、一段ごとの光がティックそのものを目に見える出来事にします。

回復ティック発光切替 — 値が一定間隔で少しずつ上がるたび帯の先端が光る方式
新しいタブで開く

02 · 回復ティック発光切替 — 値が一定間隔で少しずつ上がるたび帯の先端が光る方式 — 計測 累積面積 5.433% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

tick()GLOW_TAUTICK_INTERVAL
  function render(glow) {
    var v = Math.max(0, Math.min(RES_MAX, value));
    fill.style.width = v.toFixed(2) + '%';
    edge.style.left = v.toFixed(2) + '%';
    var g = glow < 0.02 ? 0 : glow;
    // 計算層: グロー値が明るさとにじみの2つのCSS値を同時に駆動する
    edge.style.opacity = (0.35 + g * 0.65).toFixed(3);
    edge.style.transform = 'translate(-50%, -50%) scale(' + (1 + g * 0.9).toFixed(3) + ')';
    track.setAttribute('aria-valuenow', String(Math.round(v)));
    valueEl.textContent = Math.round(v) + ' / ' + RES_MAX;
  }

  function tick(ts) {
    value = Math.min(RES_MAX, value + TICK_AMOUNT);
    glowStart = ts;
  }

  function human() {
    if (isDemo) { gb.classList.remove('is-demo'); isDemo = false; }
  }

数値が語ること

02番は累積面積5.43%・平均強度39.9を23サンプル中23フレーム全部で示し、9つの中で最も動いたフレーム数となっていますが、これは値そのものが固定の TICK_INTERVAL ごとにしか上がらない一方で GLOW_TAU の減衰がその合間も毎フレーム先端の明るさと拡大率を再計算し続けているためです。

03区切りピップ亀裂切替 — 帯が複数のピップに分かれ、値が1ピップ下がるとそのピップに亀裂模様が残る方式

値が1ピップ分下がった瞬間、そのピップの亀裂度の値が大きくなり、以降のピップは塗りを失います。ボス体力の段階区間表示や、残機をピップで見せる表示に向く方式で、割れたピップがそのまま被弾の履歴として画面に残ります。

区切りピップ亀裂切替 — 帯が複数のピップに分かれ、値が1ピップ下がるとそのピップに亀裂模様が残る方式
新しいタブで開く

03 · 区切りピップ亀裂切替 — 帯が複数のピップに分かれ、値が1ピップ下がるとそのピップに亀裂模様が残る方式 — 計測 累積面積 1.917% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

stateFor()CRACK_DURSTAGGER
  function stateFor(pipIdx, ts) {
    var eventIndex = PIPS - 1 - pipIdx;
    if (eventIndex < 0 || eventIndex >= appliedIdx) {
      return { c1: 0, c2: 0, fill: 1 };
    }
    var t0 = eventTs[eventIndex];
    var dt = Math.max(0, ts - t0);
    var c1 = Math.min(1, dt / (CRACK_DUR * 0.45));
    var c2 = Math.min(1, Math.max(0, dt - STAGGER) / (CRACK_DUR * 0.45));
    var fill = 1 - Math.min(1, Math.max(0, dt - STAGGER) / (CRACK_DUR - STAGGER));
    return { c1: c1, c2: c2, fill: fill };
  }

数値が語ること

03番は累積面積1.92%・平均強度71.8を23サンプル中14フレームで示し、stateFor() が9つ全部のピップに同じ CRACK_DUR/STAGGER の計算を毎フレーム走らせる一方で、appliedIdx の範囲に入っているピップだけが実際に変化し、残りは常に { c1: 0, c2: 0, fill: 1 } を返すだけです。

04過剰回復シールド重ね切替 — 値が上限を超えるとその超過分が半透明の層として帯の上に重なる方式

値が上限値を超えた瞬間、その差分の幅が半透明の層として既存の帯の上に重なって現れます。シールドが乗った体力表示や、上限を超えて溢れたリソースバーに向く方式で、超過分がひとつの層として見えることで「今どれだけ余っているか」が一目で伝わります。

過剰回復シールド重ね切替 — 値が上限を超えるとその超過分が半透明の層として帯の上に重なる方式
新しいタブで開く

04 · 過剰回復シールド重ね切替 — 値が上限を超えるとその超過分が半透明の層として帯の上に重なる方式 — 計測 累積面積 2.868% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

render()OVERHEAL_CAPDECAY_RATE
  function render() {
    var base = Math.min(value, HP_MAX);
    var excess = Math.max(0, value - HP_MAX);
    var shieldRatio = Math.min(1, excess / OVERHEAL_CAP);
    fill.style.width = base.toFixed(2) + '%';
    // 計算層: 超過幅 = 上限を超えた値 ÷ シールド表示基準値 — 半透明レイヤーの幅と不透明度を同時に駆動する
    shield.style.width = (shieldRatio * 100).toFixed(2) + '%';
    shield.style.opacity = shieldRatio < 0.02 ? '0' : (0.4 + shieldRatio * 0.5).toFixed(3);
    track.setAttribute('aria-valuenow', String(Math.round(base)));
    valueEl.textContent = Math.round(value) + ' / ' + HP_MAX;
  }

数値が語ること

04番は累積面積2.87%・平均強度99.7——9つの中で最も高い強度——を23サンプル中13フレームで示し、OVERHEAL_CAP が上限の上にどこまで表示を伸ばせるかを決め、DECAY_RATE がその超過分を毎フレーム自分で上限まで戻すため、次の被弾がなくてもシールドは自然に薄くなっていきます。

05危険域拍動切替 — 値が定めた低い区間に入ると帯全体が一定のリズムで明滅する方式

値が定めた低い境界を下回ると、帯の明るさの値が定めたリズムで上下し始めます。体力が低いときの警告表示や、残り時間が少ないときの通知バーに向く方式で、境界を下回った瞬間から拍動が始まることで危険そのものをリズムとして伝えます。

危険域拍動切替 — 値が定めた低い区間に入ると帯全体が一定のリズムで明滅する方式
新しいタブで開く

05 · 危険域拍動切替 — 値が定めた低い区間に入ると帯全体が一定のリズムで明滅する方式 — 計測 累積面積 1.985% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pulseFor()THRESHOLDPERIOD_MIN
  function pulseFor(v, ts) {
    if (v >= THRESHOLD) return { brightness: 1, glow: 0 };
    // 計算層: 残量が低いほど拍動周期は短く振幅は大きくなる(反比例) — 毎フレーム v から再計算
    var ratio = (THRESHOLD - v) / THRESHOLD; // 0..1
    var period = PERIOD_MAX - (PERIOD_MAX - PERIOD_MIN) * ratio;
    var amp = AMP_MIN + (AMP_MAX - AMP_MIN) * ratio;
    var phase = (ts % period) / period * Math.PI * 2;
    var osc = Math.sin(phase);
    return { brightness: 1 + amp * osc, glow: Math.max(0, osc) * amp };
  }

数値が語ること

05番は累積面積1.99%・平均強度75.4を23サンプル中7フレーム——9つの中で最も動いたフレームが少ない項目——で示し、pulseFor() は値が THRESHOLD を下回るまで { brightness: 1, glow: 0 } を返すだけで何もせず、残量がゼロに近づいてはじめて PERIOD_MIN の短い周期が主導権を握ります。

06チャージ保持放出切替 — 押している間に値がゆっくり満ち、離した瞬間に一度に放出される方式

押している間に値が定めた速さで満ち、離した瞬間にその値が0まで一気に落ち、別の放出表示が点灯します。強攻撃のチャージ表示や、溜めてから放つリソースバーに向く方式で、放出の瞬間に別枠の表示が跳ねることで「今出た」という手触りを帯そのものの動きとは別に加えます。

チャージ保持放出切替 — 押している間に値がゆっくり満ち、離した瞬間に一度に放出される方式
新しいタブで開く

06 · チャージ保持放出切替 — 押している間に値がゆっくり満ち、離した瞬間に一度に放出される方式 — 計測 累積面積 3.328% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

burstAt()RELEASE_ZETACHARGE_RATE
  function burstAt(t) {
    // 計算層: 放出スプリング — 減衰コサイン振動で目標を通り過ぎてから戻る位置を計算する
    if (t < 0 || t > RELEASE_DUR) return { scale: 0, alpha: 0 };
    var x = t / RELEASE_DUR;
    var amp = Math.exp(-RELEASE_ZETA * x);
    var pos = 1 - amp * Math.cos(RELEASE_FREQ * t);
    return { scale: 1 + pos * 1.1, alpha: Math.max(0, amp * 1.15) };
  }

  function render(ts) {
    fill.style.width = value.toFixed(2) + '%';
    valueEl.textContent = Math.round(value) + '%';
    track.setAttribute('aria-valuenow', String(Math.round(value)));
    var b = burstAt(ts - releaseStart);
    burst.style.left = releaseFromPct.toFixed(2) + '%';
    burst.style.opacity = b.alpha < 0.02 ? '0' : b.alpha.toFixed(3);
    burst.style.transform = 'translate(-50%, -50%) scale(' + b.scale.toFixed(3) + ')';
  }

数値が語ること

06番は累積面積3.33%・平均強度76.6を23サンプル中14フレームで示し、CHARGE_RATE が押している1ミリ秒あたり何%満ちるかを決め、離した瞬間からは burstAt() が RELEASE_ZETA と RELEASE_FREQ で減衰コサインの行き過ぎを計算するという、CSSの @keyframes からではない動きになっています。

07複数レーン比較切替 — 並んだ2本以上の帯がそれぞれ異なる値で並行して満ちる方式

並んだ各帯の塗り幅の値がそれぞれ別の速さで上がり、帯同士の差が広がったり狭まったりします。複数対象の体力を一画面で比較する表示や、順位・進行度の比較に向く方式で、常に動き続けるサイン波の上にクリックの一撃だけがオフセットとして加わります。

複数レーン比較切替 — 並んだ2本以上の帯がそれぞれ異なる値で並行して満ちる方式
新しいタブで開く

07 · 複数レーン比較切替 — 並んだ2本以上の帯がそれぞれ異なる値で並行して満ちる方式 — 計測 累積面積 2.979% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

valuesAt()JOLTSclassList.toggle()
  function render(t) {
    var values = valuesAt(t);
    var sorted = values.slice().sort(function (a, b) { return b - a; });
    var leaderVal = sorted[0];
    var secondVal = sorted[1];
    var leaderIdx = values.indexOf(leaderVal);

    for (var i = 0; i < rows.length; i++) {
      var v = values[i];
      rows[i].fill.style.width = v.toFixed(2) + '%';
      var isLeader = i === leaderIdx;
      rows[i].fill.classList.toggle('gb__laneFill--lead', isLeader);
      // 計算層: 先頭と2位の値の差分だけを明るい層として重ね、「どれだけ差をつけているか」を示す
      if (isLeader && leaderVal - secondVal > 0.3) {
        rows[i].lead.style.left = secondVal.toFixed(2) + '%';
        rows[i].lead.style.width = (leaderVal - secondVal).toFixed(2) + '%';
        rows[i].lead.style.opacity = '1';
      } else {
        rows[i].lead.style.opacity = '0';
      }
    }
    // 計算層: translateYで王冠表示を現在の先頭レーンの行へ移動する(DOM再配置なしで順位だけを計算する)
    crown.style.transform = 'translateY(' + (leaderIdx * rowStep) + 'px)';
  }

数値が語ること

07番は累積面積2.98%・平均強度127.9——9つの中で最も高い強度——を23サンプル中23フレーム全部で示し、valuesAt() が毎フレーム全レーンのサイン波を読み続けるためクリックが一度もなくても帯は止まらず、JOLTS はクリックごとに1レーンへ固定オフセットを加えるだけで classList.toggle() がそのとき先頭のレーンだけを示します。

08スタン崩壊切替 — 補助値が上限に達すると帯が一気に崩れて消える方式

補助値が上限に達した瞬間、その帯の不透明度の値が一度跳ねてから0に落ちて消えます。スタン・崩壊状態を知らせる補助バーや、限界に達すると消えるリソース表示に向く方式で、崩れた後の再チャージ窓を別に持つことで「まだ何もできない時間」がはっきり分かります。

スタン崩壊切替 — 補助値が上限に達すると帯が一気に崩れて消える方式
新しいタブで開く

08 · スタン崩壊切替 — 補助値が上限に達すると帯が一気に崩れて消える方式 — 計測 累積面積 2.878% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

snapOpacity()SPIKE_DURDROP_DUR
  function render(ts) {
    var dtPhase = ts - phaseStart;
    var opacity = 1, width = value, shake = 0;

    if (phase === 'snap') {
      opacity = snapOpacity(dtPhase);
      shake = dtPhase < SPIKE_DUR ? Math.sin(dtPhase * .9) * 4 : 0;
      width = STAGGER_MAX;
    } else if (phase === 'recover') {
      var r = Math.min(1, dtPhase / RECOVER_DUR);
      opacity = r;
      width = r * STAGGER_MAX * 0.9; // 計算層: 回復は上限近く(90%)までしか戻らない — 残りは次のヒットが埋める
      value = width;
    }

    fill.style.width = Math.max(0, Math.min(100, width)).toFixed(2) + '%';
    fill.style.opacity = Math.max(0, Math.min(1, opacity)).toFixed(3);
    fill.style.transform = 'translateX(' + shake.toFixed(2) + 'px)';
    var shown = Math.round(Math.max(0, Math.min(100, width)));
    track.setAttribute('aria-valuenow', String(shown));
    valueEl.textContent = shown + '%';
  }

数値が語ること

08番は累積面積2.88%・平均強度83.1を23サンプル中13フレームで示し、SPIKE_DUR が上限に達した直後の一瞬だけ不透明度を1に保ち、DROP_DUR がそこから0まで線形に落ちる長さで、RECOVER_DUR は帯が再び90%まで満ちるための別枠の窓になっています。

09モーション削減即時切替 — モーション削減設定がオンだと前の8方式のアニメーションが全て即座に値へ変わる方式

モーション削減の値がオンのとき、前の8本の帯はすべて中間段階なしに目標値へ直接切り替わります。アクセシビリティ設定をオンにしたユーザーの画面や、アニメーションを減らしたいダッシュボードで実際に何が変わるのかを、通常側と並べて隠さずに見せる方式です。

モーション削減即時切替 — モーション削減設定がオンだと前の8方式のアニメーションが全て即座に値へ変わる方式
新しいタブで開く

09 · モーション削減即時切替 — モーション削減設定がオンだと前の8方式のアニメーションが全て即座に値へ変わる方式 — 計測 累積面積 1.271% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

render()EVENTSmatchMedia()
  function render() {
    var v = Math.max(0, Math.min(100, value));
    var tv = Math.max(0, Math.min(100, trailValue));
    fillNormal.style.width = v.toFixed(2) + '%';
    trailNormal.style.width = tv.toFixed(2) + '%';
    valueNormalEl.textContent = Math.round(v);
    trackNormal.setAttribute('aria-valuenow', String(Math.round(v)));

    // 即時側: 残光や遷移なしに同じ値へそのままスナップする — 計算ではなく代入
    fillReduced.style.width = v.toFixed(2) + '%';
    valueReducedEl.textContent = Math.round(v);
    trackReduced.setAttribute('aria-valuenow', String(Math.round(v)));
  }

数値が語ること

09番は累積面積1.27%・平均強度136.1——9つの中で最も高い強度——を23サンプル中13フレームで示し、左側の通常の帯だけが LERP_RATE の指数的な遅れを持ち、右側の即時の帯は毎フレーム同じ値をそのまま代入するだけなので、両者がはっきり分かれて見えるのは EVENTS が変わった直後の数フレームだけです。

値が帯を満たし・減らす基準 — 体力値・上限値・補助値基準

01番の applyHit() と FLASH_TAU 70ms が、普通のHPバーとこの方式を分けています。被弾の瞬間にメインの幅は新しい低い値へ即座に切り替わり、赤い残光の幅はその場に留まって LERP_RATE の指数的なイーズだけで後から追いつきます — 二つの幅の差がそのままフラッシュそのものです。

04番の OVERHEAL_CAP 50 と DECAY_RATE .00065 が、値が100をどこまで超えられて、どれだけの速さで戻るかを決めます。render() は HP_MAX を超えた分をすべて半透明のシールド層に変換し、その幅と不透明度は両方とも excess / OVERHEAL_CAP を基準にするので、新しい被弾がなくてもシールドは自分で薄くなっていきます。

08番の SPIKE_DUR 130ms・DROP_DUR 330ms・RECOVER_DUR 950ms は、9つの中で塗りだけでなく帯そのものの状態を変える唯一の組です。補助値が STAGGER_MAX に達すると snapOpacity() が SPIKE_DUR の間だけ不透明度を保ち、DROP_DUR で線形に0まで落とし、その後は別の RECOVER_DUR の窓だけが帯を再び90%まで満たすことを許します。

この一編を超えて見ると、CSS プログレスバー 円形ゲージ9選 — 数値を変えるだけ は別の軸を扱っています——ゲーム状態のイベントで帯の意味が変わる9つではなく、同じウェブ用プログレスバーを9通りにスタイリングする軸です。円形プログレスバー css スタットリング9選 — 数値も実際に動く はこの記事に近いスタット表示の目的を保ったまま、横棒を stroke-dashoffset で動く円形のリングに置き換えています。【コピペOK】ローディング 画面 デザイン CSSだけ 9選 は値で満ちる帯そのものを捨て、画面全体を待ち時間の演出として扱う、計測というより雰囲気に近い方式です。この3つとこの記事の9つを見比べれば、プログレスバー css アニメーションのどの定数が実際に効いているのかが、コードを1行も書く前に分かります。

どこで壊れるか — 連続ヒット・大きな値の跳躍・モーション削減

9つのどれも外部から値を受け取りません——変化はすべてデモ自身のクリック・ポインター・キーボードの処理から来ており、それが内側で本物の物理計算をしていても、こうしてテストしやすい挙動を保っています。

スタン崩壊の最中にクリックする

08番の poiseBtn のクリック処理は phase が 'build' のときしか何もしません——phase が 'snap' の間はコード自身の注釈が「スタン中は無視」と書いてあるとおりクリックは完全に無視され、phase が 'recover' になってはじめてクリックが帯を0と 'build' に戻します。帯が崩れた瞬間に何度クリックしても、SPIKE_DUR と DROP_DUR を合わせたおよそ460msの間は何も返ってきません。

チャージボタンから外れるドラッグ

06番は pointerleave と pointercancel を pointerup と全く同じ releaseCharge() に結び付けているため、押したままポインターをボタンの外へドラッグして外側で離しても放出は起きます——きれいに離した場合に期待する値ではなく、ポインターが外れた瞬間に CHARGE_RATE が到達していたその時点のパーセントで放出されます。

モーション削減

01番から08番までの全部が読み込み時に一度だけ matchMedia('(prefers-reduced-motion: reduce)') を確認し、一致すればその場の見た目を一度だけ描いて requestAnimationFrame のループを最初から一度も始めません——ゲージは開始値のまま止まります。09番だけが意図的な例外で、コード自身の注釈が説明しているとおり、通常側のループはシステム設定に関わらず requestAnimationFrame の上で回り続けます。この項目の目的そのものが、通常版と削減版を訪問者自身のOS設定によって片方だけ失わせず、並べて比較させることだからです。09番を含む9つのソース全部は上のzipに入っていて、273ynvg7を入力すれば同じように開けます。

3つのバリエーションとアクセシビリティ

放出スプリングの減衰比を下げる(RELEASE_ZETA 3.6→1.8) — 減衰定数を3.6から1.8に下げると、放出の瞬間に帯が静止点を一度で通り過ぎず、数回余分に跳ねてから収まります。

被弾残光の減衰時間を延ばす(FLASH_TAU 70→220) — 減衰時間の値を70から220に伸ばすと、赤い残光の帯がずっと長く残ってからゆっくり消えていきます。

崩壊後の再チャージ時間を短くする(RECOVER_DUR 950→300) — 再チャージ時間の値を950から300に短くすると、帯が崩れて消えた直後にほぼすぐ再び満ち始めます。

バリエーション 変わるもの 変わらないもの
放出スプリングの減衰比を下げる(RELEASE_ZETA 3.6→1.8) 06番の放出オーバーシュートの回数だけ CHARGE_RATE・RELEASE_DUR・RELEASE_FREQ
被弾残光の減衰時間を延ばす(FLASH_TAU 70→220) 01番の赤い残光帯の消え方の速さだけ LERP_RATE・HIT_LIST・CYCLE
崩壊後の再チャージ時間を短くする(RECOVER_DUR 950→300) 08番の崩壊後の再充填速度だけ SPIKE_DUR・DROP_DUR・BUILD_EVENTS

9つのうち07番を除く8つが role="progressbar" に aria-valuemin・aria-valuemax、そして render() が毎フレーム更新する aria-valuenow を持ち、値が実際に何を表すかを示す aria-label(HP・MP・Boss HP・チャージ量・気絶値、そして09番の対になったHP通常・HPモーション低減)も添えています。07番だけは例外で、4本のレーンのどれにもroleも aria-valuenow も一切なく、スクリーンリーダーの訪問者には王冠の見た目以外にどのレーンが先頭かを知る手段がありません——複数の対象を一度に比較する実装をつくるなら、そこは自分で追加する必要があります。9つのどれも disabled 属性を使わず、どのボタンも click・pointerdown とキーボードの等価な操作の両方に答えるので、フォーカスの順序がゲージの今のフェーズによって変わることはありません。

FAQ

css プログレスバーがなぜ9通りも必要なのか、1つの汎用バーではだめなのか

帯を動かす値が毎回同じ意味を持つわけではないからです。単純なパーセンテージと、上限を超えても表示だけは超えられる値、そして減るのではなく一気に崩れる補助値は、それぞれ別の計算を必要とします。まず値の種類——体力・過剰回復・気絶値——を決めることが、9つのうちどれが本当に合うのかを、コードを1行もコピーする前に教えてくれます。

06番の放出バーストはCSSの@keyframesで動いていますか、それとも計算で作られていますか

計算で作られています。burstAt() はボタンを離した後、毎アニメーションフレームで指数減衰 Math.exp(-RELEASE_ZETA * x) に RELEASE_FREQ で駆動するコサイン振動を掛け合わせているため、行き過ぎてから収まる動きはこの式から来ていて、CSSの @keyframes からは来ていません。

この9つの中に「css プログレスバー 円」のような円形ゲージはありますか

いいえ、この9つはすべて横棒型です。どれも幅か不透明度の値を動かすだけで、stroke-dasharray の円を使うものは一つもありません。上でリンクしたCSS プログレスバー 円形ゲージ9選 — 数値を変えるだけが、その円形の形に絞って作られた記事です。

Sources: MDN — Using CSS transitions · MDN — Window: requestAnimationFrame() method · MDN — prefers-reduced-motion · MDN — ARIA progressbar role

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

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