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

カウントアップ アニメーション css 9種 — 転がって止まる計数器から桁上がりが続く掲示板まで

カウントアップ アニメーション css は、桁ごとに0〜9を書いた帯を値の分だけ押し上げて、数字を転がして止める動きです。数字はなぜ一瞬で切り替わるより転がって止まるほうが信じられるのでしょうか。

数字は変わる値ではなく転がる帯です — カウントアップ アニメーション css は桁ごとに0〜9の帯を値の分だけ押し上げるところから始まります

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

十二マスの帯は9から0へ変わっても途切れません

01〜05・08・09 は桁ひとつが窓ひとつで、窓の中には0〜9のあとに0と1をもう一度つなげた十二マスの帯が入っています。帯は translateY(-(pos mod 10)/12·100%) で押されるので、9から0へ変わる境目のうしろにも0が続き、帯は巻き戻らずに上へ進み続けます。九つの場面は tabular-nums を使っていて、数字ごとの幅の違いで桁がぶれることはありません。

転がすマスの数を先に決めて、最後の一マスだけ目標に合わせます

数字を値としてではなく、何マス転がすかで数えます。01 は 10 + mod(b−a, 10) で一周多く回り、03 は 30 + 6i マスから始めて目標の数字に合わせて0〜9マスを足し、07 の時刻ドラムは12〜23マスを回ります。距離が長いから速さがつき、速さがつくからにじみやばねに意味が出ます。

数字 カウントアップ アニメーション のほかの記事とちがう点

css 数字 カウントアップ の記事は css だけで数字が上がるコードを扱い、この記事は js の時間表で桁ごとの速さ・にじみ・桁上がりを計算するところがちがいます。値を手で上げ下げする入力部品は 数字 ステッパー ui の記事に、減っていく残り時間は css カウントダウン タイマー の記事があります。摩擦で止まる08と同じ計算の仲間は、ルーレット 回す の記事の回転盤です。

カウントアップ アニメーション css 9種 — 桁ごとに転がって止まる計数器・目標を行き過ぎて戻るばねの数字・速いとにじむリール・桁上がりが続く連鎖の計数器・向きで光る値・半分に折れるフリップボード・円柱ドラムの遠近の数字・摩擦で減速するダイヤル・統計一行のヒーロー

01〜05 は同じ縦の帯に、桁の遅れ・ばね・速さのにじみ・桁上がり・向きという別々の決まりを重ねます。06 は帯をやめて羽根を二枚折り、07 は円柱の角度で数字をつぶして描きます。08 は摩擦で目盛りに吸いつき、09 は前の桁の遅れを統計一行のヒーローにまとめます。

九つの場面の面積・動き・強さの実測

場面 累積面積 動いたフレーム 強さの平均
01 桁ごとの数字の帯 13.2% 13/23 164.6
02 ばねの落ち着き 15.7% 14/23 148.9
03 速さのにじみのリール 12.3% 21/23 84.7
04 桁上がりの連鎖 11.1% 18/23 132.3
05 向きで光る値 22.9% 15/23 120.9
06 羽根のフリップ 10.6% 15/23 117.7
07 ドラムの遠近 13.9% 16/23 88.5
08 摩擦のダイヤル 6.2% 10/23 105.2
09 統計一行 21.6% 16/23 143.3

いちばん広く変わるのは板全体が光る05の22.9%で、いちばん狭いのは目盛りに吸いついてすぐ止まる08の6.2%です。動いたフレームは、リール五つが長く回る03が21/23で最多、08が10/23で最少になります。強さの平均は01の164.6が最高、03の84.7が最低です。

01桁ごとの数字の帯が転がって止まる計数器

ダッシュボードの累計訪問者数カードに使うと、五桁が0.25秒に一の位から出発して右の桁が先に止まり、1.67秒に左の桁が止まります。placeU(k) が桁 k(一の位が0)の進み具合を PLACE_DELAY_MS 80ms ずつ遅らせ、転がすマスの数は 10 + mod(b−a, 10) なので一周ぶん多く回ります。下の小さな行は数字の値をそのまま帯の位置に使う posBad で、小さな数へ向かうと逆向きになぞります。

桁ごとの数字の帯が転がって止まる計数器
新しいタブで開く

01 · 桁ごとの数字の帯が転がって止まる計数器 — 計測 累積面積 13.194% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PLACE_DELAY_MSROLL_SplaceU
    function placeU(k) {
        return (sceneT - START_S - k * PLACE_DELAY_MS / 1000) / ROLL_S;
    }
    function draw() {
        const a = digitsOf(fromV, PLACES);
        const b = digitsOf(toV, PLACES);
        for (let i = 0; i < PLACES; i += 1) {
            const k = PLACES - 1 - i;
            const u = clamp(placeU(k), 0, 1);
            // 一周(10マス)多く回って目標の数字に止まる
            const adv = 10 + mod(b[i] - a[i], 10);
            const pos = a[i] + adv * easeOut3(u);
            const speed = u > 0 && u < 1 ? adv * 3 * (1 - u) * (1 - u) / ROLL_S : 0;
            moveStrip(strips[i], pos, speed);
            glowStrip(strips[i], speed / BLUR_REF * 0.9);
            // 巻き戻る側 — 数字の値をそのまま帯の位置に使うので、小さな数へ向かうと逆向きになぞる
            const posBad = lerp(a[i], b[i], easeOut3(u));
            moveStrip(naive[i], posBad, 0);
            glowStrip(naive[i], u > 0 && u < 1 && b[i] < a[i] ? 0.8 : 0);
        }
    }

02目標を行き過ぎてばねで戻って落ち着く数字

決済完了画面の合計金額に使うと、数字が目標を少し行き過ぎてから戻って落ち着きます。update が一フレームを ceil(dt·240) 個に刻んで acc = K(goal−x) − C·v を積むので、フレームが長くてもばねが暴れません。SPRING_K 120 と SPRING_C 12 では減衰比が0.548で、行き過ぎる幅は移動距離の12.8%、最初の到達は出発の0.23秒後です。

目標を行き過ぎてばねで戻って落ち着く数字
新しいタブで開く

02 · 目標を行き過ぎてばねで戻って落ち着く数字 — 計測 累積面積 15.678% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPRING_KSPRING_CPLACE_DELAY_S
    function update(dt) {
        // 240Hz に細かく刻んで積む — フレームが長くてもばねが暴れない
        const n = Math.max(1, Math.ceil(dt * 240));
        const h = dt / n;
        for (let i = 0; i < springs.length; i += 1) {
            const sp = springs[i];
            if (sceneT < sp.start) {
                continue;
            }
            for (let j = 0; j < n; j += 1) {
                const acc = SPRING_K * (sp.goal - sp.x) - SPRING_C * sp.v;
                sp.v += acc * h;
                sp.x += sp.v * h;
            }
        }
        const one = springs[PLACES - 1];
        if (sceneT >= one.start) {
            peakOver = Math.max(peakOver, one.x - one.goal);
        }
    }

03速いとにじみ遅くなるとくっきりするリール

イベント抽選の番号発表画面では、回っている間は読めなかった数字が止まるときにくっきりします。moveStrip は速さを BLUR_REF 60 で割った割合 sp で本体の帯の不透明度を 1 − 0.5·sp まで下げ、尾の帯二枚を sp·BLUR_LAG·g マスうしろに敷いてにじみを作ります。リールは五つで、左から1.2秒を起点に STOP_STEP 0.24秒ずつ遅れて止まり、転がすマスの数は reelDist が 30 + 6i に0〜9マスを足して30〜63マスの範囲で決めます。

速いとにじみ遅くなるとくっきりするリール
新しいタブで開く

03 · 速いとにじみ遅くなるとくっきりするリール — 計測 累積面積 12.345% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STOP_STEPreelDistBLUR_REF
    // pos = いま帯が押し上げられたマス数(小数)、speed = マス/秒(向き込み)。尾の帯があれば速さに比例してにじむ
    function moveStrip(s, pos, speed) {
        putCol(s.cols[0], pos);
        if (s.cols.length < 2) {
            return;
        }
        const sp = clamp(Math.abs(speed) / BLUR_REF, 0, 1);
        const dir = speed < 0 ? -1 : 1;
        s.cols[0].style.opacity = (1 - 0.5 * sp).toFixed(3);
        for (let g = 1; g < s.cols.length; g += 1) {
            putCol(s.cols[g], pos - dir * sp * BLUR_LAG * g);
            s.cols[g].style.opacity = (0.5 * sp / g).toFixed(3);
        }
    }

04桁上がりが上の桁へ続く連鎖の計数器

リアルタイムの接続者数カウンターでは、999が1000になるとき四つの桁が時間差で続けて上がります。placePos(k, lag) は開始値のその桁の商に、値が上がってその桁が変わったティックごとに一マスを足し、桁 k の出発を k·lag だけ遅らせます。上の行は CARRY_LAG_S 0.07秒なので、一・十・百・千の位が1.08・1.15・1.22・1.29秒に出発します。

桁上がりが上の桁へ続く連鎖の計数器
新しいタブで開く

04 · 桁上がりが上の桁へ続く連鎖の計数器 — 計測 累積面積 11.077% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CARRY_LAG_SplacePoschainOf
    function tenPow(k) { return Math.pow(10, k); }
    // 桁 k の転がり上がったマス数 — 開始値のその桁の商に、値が上がってその桁が変わったティックごとに一マス(桁上がりの順に遅れ)を足す
    function placePos(k, lag) {
        let pos = Math.floor(v0 / tenPow(k));
        for (let j = 1; j <= TICKS; j += 1) {
            if (Math.floor((v0 + j) / tenPow(k)) !== Math.floor((v0 + j - 1) / tenPow(k))) {
                const s = FIRST_S + (j - 1) * TICK_S + k * lag;
                pos += easeOut3((sceneT - s) / ROLL_S);
            }
        }
        return pos;
    }

05上がれば上へ下がれば下へ転がり色が光る値

為替・在庫数の電光掲示板では、値が上がると帯が上へ、下がると下へ転がるので、向きが先に目に入ります。goesUp() が真なら mod(b−a, 10) マスを前へ、偽なら −mod(a−b, 10) マスをうしろへ転がし、板に is-up(ミント)と is-down(ピンク)を切り替えてつけます。光の不透明度は 0.9·exp(−(t−0.2)/0.35) でしずまります。

上がれば上へ下がれば下へ転がり色が光る値
新しいタブで開く

05 · 上がれば上へ下がれば下へ転がり色が光る値 — 計測 累積面積 22.903% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FLASH_TAUgoesUpSTAGGER_S
    function goesUp() { return toV > fromV; }
    function fixed2(v) { return (v / 100).toFixed(2); }
    function placeU(k) { return (sceneT - START_S - k * STAGGER_S) / ROLL_S; }
    function draw() {
        const a = digitsOf(fromV, PLACES);
        const b = digitsOf(toV, PLACES);
        const up = goesUp();
        panel.classList.toggle(cls('is-up'), up);
        panel.classList.toggle(cls('is-down'), !up);
        for (let i = 0; i < PLACES; i += 1) {
            const k = PLACES - 1 - i;
            const u = clamp(placeU(k), 0, 1);
            // 上がったら前へ(上へ)、下がったら後ろへ — 最短の向きではなく値の向きどおりに転がす
            const adv = up ? mod(b[i] - a[i], 10) : -mod(a[i] - b[i], 10);
            const pos = a[i] + adv * easeOut3(u);
            const speed = u > 0 && u < 1 ? adv * 3 * (1 - u) * (1 - u) / ROLL_S : 0;
            moveStrip(strips[i], pos, speed);
            glowStrip(strips[i], 0);
        }
        const since = sceneT - START_S;
        flash.style.opacity = (since >= 0 ? 0.9 * Math.exp(-since / FLASH_TAU) : 0).toFixed(3);
    }

06半分に折れてめくれる羽根のフリップボード

到着案内の時刻板のように、マスごとに数字が何枚もめくれてから目標に止まります。flipsOf(a, b) が 5 + mod(b−a−5, 10) で5〜14枚をめくり、一枚ごとに前半は上の羽根が rotateX(−180u°) で折れ、後半は下の羽根が90度から0度へ開きます。0618から0847へ行くとマスごとに10・12・13・9枚、合わせて44枚がめくれ、マスが止まる時刻は1.25・1.55・1.75・1.45秒で、いちばん遅いのは3番目のマスの1.75秒です。

半分に折れてめくれる羽根のフリップボード
新しいタブで開く

06 · 半分に折れてめくれる羽根のフリップボード — 計測 累積面積 10.572% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FLIP_SflipsOfCELL_DELAY_S
    function draw() {
        const a = digitsOf(fromV, 4);
        const b = digitsOf(toV, 4);
        for (let i = 0; i < 4; i += 1) {
            const total = flipsOf(a[i], b[i]);
            const e = clamp(elapsed(i), 0, total - 0.0001);
            const s = Math.floor(e);
            const u = e - s;
            const cur = mod(a[i] + s, 10);
            const nxt = mod(a[i] + s + 1, 10);
            const c = cards[i];
            setNum(c.top, nxt);
            setNum(c.bot, cur);
            setNum(c.ft, cur);
            setNum(c.fb, nxt);
            // 前半は上の羽根が前へ-90度まで折れて下り、後半は下の羽根が90度から0度へ開く
            const front = u < 0.5;
            c.ft.style.opacity = front ? (1 - u * 0.5).toFixed(3) : '0';
            c.ft.style.transform = 'rotateX(' + (front ? (-180 * u).toFixed(2) : '-90') + 'deg)';
            c.fb.style.opacity = front ? '0' : '1';
            c.fb.style.transform = 'rotateX(' + (front ? '90' : (90 - 180 * (u - 0.5)).toFixed(2)) + 'deg)';
            c.shade.style.opacity = (0.45 * Math.sin(Math.PI * u)).toFixed(3);
        }
    }

07丸いドラムの遠近でつぶれて見える数字

時・分の選択ホイールでは、円柱のまわりに置いた数字が角度でつぶれ、真ん中の一つだけが大きく明るく見えます。マスごとに θ = (idx−pos)·STEP_DEG 30度を求め、translateY に R·sinθ、scale に (0.7+0.3cosθ, cosθ)、不透明度に cos²θ を与え、cosθ が0.05以下の裏側のマスは描きません。5時52分から7時25分へ行くと、時のドラムは14マスを1.55秒に、分のドラムは33マスを1.95秒に止まります。

丸いドラムの遠近でつぶれて見える数字
新しいタブで開く

07 · 丸いドラムの遠近でつぶれて見える数字 — 計測 累積面積 13.901% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STEP_DEGRADIUSdistOf
            const e = w.items[j + half];
            const c = Math.cos(th);
            if (c <= 0.05) {
                e.style.opacity = '0';
                continue;
            }
            const text = String(mod(idx, w.n) + w.offset).padStart(2, '0');
            if (e.textContent !== text) {
                e.textContent = text;
            }
            // 高さは cos θ 倍につぶれ幅も少し縮み、薄くなる度合いは cos θ の二乗に速さのにじみを掛ける
            // 中心からの距離(ホイールの高さの比)を、マス自身の高さの比(%)に直して translateY に渡す
            const y = RADIUS * Math.sin(th) * 100 / (ITEM_H / 100);
            e.style.transform = 'translateY(' + y.toFixed(2) + '%) scale(' + (0.7 + 0.3 * c).toFixed(3) + ',' + c.toFixed(3) + ')';
            e.style.opacity = (c * c * fade).toFixed(3);

08摩擦で減速して目盛りに吸いつく慣性ダイヤル

数量・年齢入力用のダイヤルのように、最初の速さをもらったあとすべって、最も近い目盛りに吸いつきます。step が毎フレーム v *= FRICTION^(dt·60) で速さを削り、SNAP_V 3 を下回ると Math.round(x) の目盛りへ 1 − e^(−14dt) の割合ずつ引かれます。速さ90マス/秒で出発すると0.68秒に引き込みが始まり、2から18マス進んだ20、つまり数字の0に約1.07秒で止まります。

摩擦で減速して目盛りに吸いつく慣性ダイヤル
新しいタブで開く

08 · 摩擦で減速して目盛りに吸いつく慣性ダイヤル — 計測 累積面積 6.247% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FRICTIONSNAP_VSPIKE_EXTRA
    function step(d, dt) {
        if (!d.flicked && sceneT >= START_S) {
            d.flicked = true;
            d.v = FLICKS[cycle % FLICKS.length];
        }
        if (!d.flicked) {
            return;
        }
        if (!d.snapping) {
            d.x += d.v * dt;
            d.v *= Math.pow(FRICTION, dt * 60);
            if (Math.abs(d.v) < SNAP_V) {
                d.snapping = true;
                d.snapTo = Math.round(d.x);
                d.v = 0;
            }
        }
        else {
            d.x += (d.snapTo - d.x) * (1 - Math.exp(-SNAP_RATE * dt));
            if (Math.abs(d.snapTo - d.x) < 0.002) {
                d.x = d.snapTo;
            }
        }
    }

09統計の一行が順に転がり上がるヒーロー

サービス紹介ページの最初の画面では、訪問者・レビュー・評価が順に入ってきて桁ごとに転がり上がります。startOf(i) がカードを0.2・0.5・0.8秒に入れ、statU(i, k) が低い桁から PLACE_DELAY_S 0.05秒ずつ遅らせて転がし、各桁は0から 10 + 目標の数字 マスを転がり上がります。評価は整数48を10で割って4.8と見せ、2.03秒に全部が終わります。

統計の一行が順に転がり上がるヒーロー
新しいタブで開く

09 · 統計の一行が順に転がり上がるヒーロー — 計測 累積面積 21.611% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ENTER_GAP_SPLACE_DELAY_SstatU
    function startOf(i) { return START_S + i * ENTER_GAP_S; }
    function statU(i, k) { return (sceneT - startOf(i) - FADE_S * 0.5 - k * PLACE_DELAY_S) / ROLL_S; }
    function draw() {
        for (let i = 0; i < 3; i += 1) {
            const st = stats[i];
            // 入ってくるとき低いところから浮かぶ — 最初もぼんやり見えるよう底の明るさを置く
            const e = easeOut3((sceneT - startOf(i)) / FADE_S);
            st.card.style.opacity = (0.35 + 0.65 * e).toFixed(3);
            st.card.style.transform = 'translateY(' + ((1 - e) * 14).toFixed(2) + '%)';
            const d = digitsOf(target[i], st.digits);
            for (let j = 0; j < st.digits; j += 1) {
                const k = st.digits - 1 - j;
                const u = clamp(statU(i, k), 0, 1);
                const adv = 10 + d[j];
                const speed = u > 0 && u < 1 ? adv * 3 * (1 - u) * (1 - u) / ROLL_S : 0;
                moveStrip(st.strips[j], adv * easeOut3(u), speed);
                glowStrip(st.strips[j], speed / BLUR_REF * 0.8);
            }
        }
    }

カウントアップ アニメーション css は何桁まで、どれだけゆっくり転がせば読めるのでしょうか?

止まる時刻は2.16秒の内側に収めました

桁の遅れがあっても全部の桁が止まる時刻は、01が1.67秒、06が1.75秒、07が1.95秒、09が2.03秒、03が2.16秒です。一桁が転がって止まるのに01と09は1.0〜1.1秒、05は0.7秒を使い、桁のあいだの出発の間隔は80ms・50ms・40msと短くしてあります。桁を五つより増やすときは間隔を縮めないと、左の桁が遅れすぎて止まります。

値が一度出て終わる画面

ダッシュボードの累計カードや決済完了の合計のように、値がひとつ出て終わる画面には01と02が合います。ランディングの最初の画面なら09のように、入ってくる順番をカードごとに0.3秒あけて、一行をひとつの場面にします。

値が続けて変わる画面

接続者数カウンター(04)、為替・在庫の掲示板(05)、到着の時刻板(06)は、同じ桁が何度も変わります。js カウントアップ アニメーション では前の値から新しい値への向きと桁上がりの順が読めないと混乱するので、04の chainOf(n) のように、いっしょに変わった桁の数を状態の行に書いておくと確かめやすくなります。

人が値を選ぶ画面

時・分の選択ホイール(07)と数量・年齢のダイヤル(08)は、見せるのではなく選ぶ部品として使います。どちらも目盛りか溝を基準に止まる位置が決まるので、止まった桁の値を読んでフォームに入れれば足ります。

帯を値の分だけ押し、桁ごとに出発を遅らせ、ばねで止めます — 数字一つが転がって落ち着くまでの順番です

位置の微分がそのままにじみになります

01・03・05・09 は位置を a + adv·easeOut3(u) で先に求め、速さをその微分の adv·3(1−u)²/ROLL_S で出して、同じ式からにじみと光を取り出します。出だしがいちばん速く、止まるときに0になるので、別に減速の曲線を用意する必要がありません。帯を押す仕事は translateY ひとつが受け持ち、時計は requestAnimationFrame の時刻で数えます。

桁の遅れは低い桁から積みます

一の位を k = 0 として出発を k·PLACE_DELAY だけ遅らせると、右の桁が先に止まります。02 はこの遅れを0.06秒、05 は0.04秒にして、桁のあいだの間隔を場面の性格に合わせて変えています。

ばねはフレームではなく240Hzの欠片で積みます

一フレームの時間が長くなるとばねの計算が暴れるので、02 はフレームを240Hzの欠片に刻んで積み、時計のほうも一フレームの時間を DT_MAX 0.05秒で切りつめて入れます。位置と速さをフレームごとに積むのは02のばねと08の摩擦で、残りは時計だけで描きます。

数字のカウントアップが壊れるのは、桁上がりなしで一桁だけ変わって999が000に見える数字・帯の端ではねて最初へ巻き戻る数字・隠れたタブのあとで一気に転がる数字です

桁上がりが遅れて届くと値が違って見えます

04 の下の行は LATE_S 0.3秒の間隔なので、999から1000へ行くとき一・十・百・千の位が1.08・1.38・1.68・1.98秒に出発します。低い桁が0に戻っているあいだ上の桁はまだ9のままで、しばらく値が違って見え、上の行は0.07秒間隔なのでこのずれが1.29秒に終わります。

帯の端で逆向きに巻き戻る数字

01 の下の小さな行は lerp(a, b, easeOut3(u)) で数字の値を帯の位置にそのまま使います。9から2へ行くと帯が2のある場所まで逆向きになぞるので、上の行のように一周多く回して mod で余りを合わせるほうが、同じ向きにつながります。

隠れたタブのあとで一気に転がる数字

08 の下の小さな数字は、0.5秒に長い一フレームを dt + SPIKE_EXTRA 0.4秒のまま受けます。そのフレームで v が一度に掛けられて位置が大きく跳び、上のダイヤルとは違う数字に止まります。

    function update(dt) {
        step(good, dt);
        // 悪い側は時間の上限なしで一度だけ大きく受ける
        let dtBad = dt;
        if (!bad.spiked && sceneT >= SPIKE_S) {
            bad.spiked = true;
            dtBad = dt + SPIKE_EXTRA;
        }
        step(bad, dtBad);
    }

実際にタブが隠れると九つの場面が使うループは止まり、戻ったときは lastTs を −1 に戻して、隠れていた時間が最初の一コマに乗らないようにします。それでも長いフレームが入ったときに備えて DT_MAX をもう一度かけて切りつめます。圧縮ファイルはパスワード qyyem5sk で開き、01・04・08 のフォルダの index.html を一時停止ボタンで止めておくと、下の小さな行がずれる地点を目で確かめられます。

ばねを強くすると数字はどれだけゆれるのでしょうか — ばねの強さ・減衰・桁ごとの遅れを変えて比べます

名前 変えた値 感じ
ばねを強く 02 SPRING_K 120 → 260 減衰比が0.55から0.37に下がって行き過ぎる幅が距離の12.8%から28.4%に増え(8マスなら1.0マスから2.2マス)、震える周波数が1.46Hzから2.38Hzに速くなって最初の到達が0.23秒から0.13秒に早まります
桁の遅れを長く 01 PLACE_DELAY_MS 80 → 240 いちばん左の桁の出発が右より320msから960ms遅れ、全部の桁が止まる時刻が1.67秒から2.31秒へ0.64秒延びます
摩擦を小さく 08 FRICTION 0.92 → 0.97 一フレームに残る速さが0.92から0.97に増えて1秒後に残る速さが0.7%から16.1%になり、引き込みの開始が0.68秒から1.86秒へ、すべる距離が18マスから48マスへ延びます

値を変える前に見ておくこと

02 は SPRING_C が12のままなので、SPRING_K だけ上げると落ち着く時間は0.7〜0.9秒で変わらず、行き過ぎる幅だけが広がります。01 は桁の遅れを240msにしても2.31秒に止まって LOOP_S 3.4秒に収まりますが、桁を増やすとその余裕は減ります。08 は摩擦を0.97にすると引き込みが1.86秒に始まるので、LOOP_S 3.0秒の中で目盛りに吸いつく時間が短くなります。

動きを減らす設定では最終の数字が一枚です — 値を読み上げる文と一時停止ボタンのアクセシビリティです

動きを減らす設定と場面の説明

prefers-reduced-motion: reduce のとき、メディアクエリ を読む reduceQuery がループを止め、showRest() が REST_T 秒後の場面、つまり最終の数字を一枚だけ描きます。九つの場面の舞台は role="img" と aria-label で何が起きているかを伝え、転がっている途中の数字が読み上げに回らないよう、状態の丸い表示(.vj__status)は aria-hidden です。転がりが終わって isSettled() が最初に真になったときだけ、隠した通知の領域に finalText() の一文が書かれ、自動デモのあいだは aria-live="off"、人が最初に触れると polite に変わります。

キーボードと一時停止

舞台のボタンはクリック・Enter・Space・右矢印・上矢印で nextNow() を呼んで次の値を転がし、動きを減らす状態では同じ入力がすぐ次の最終の数字を描きます。一時停止ボタンは aria-pressed で状態を伝え、タブが隠れると visibilitychange がループを止めます。

カウントアップ アニメーション css のよくある質問 — CSSだけで転がす方法・三桁ごとのカンマ・値が頻繁に変わるときの負荷

カウントアップ アニメーション 素材を探していますが、CSSだけで同じ場面は作れますか

値がひとつに決まっていて、帯を translateY で一度押すだけなら css の transition で足ります。この記事のばねの落ち着き・速さのにじみ・桁上がりの連鎖は、フレームごとに位置と速さを読んで次の値を決めるので js で計算します。ソースにスクロールの検知(IntersectionObserver)はなく、すべて時間表だけで動きます。

三桁ごとのカンマはどう入れますか

fmtNum が文字列の長さから三桁ごとにカンマを直接差し込み、09 は dd,ddd・d,ddd・d.d のようなマスの型でカンマのマスを別に置いています。国ごとに表記を変えたいときは Intl.NumberFormat で文字列を作ってから、桁数に合わせて帯に分ければ足ります。

値が頻繁に変わっても重くなりませんか

インラインで書き換える属性は transform と opacity が中心で、07 の溝の線の top と 08 の目盛りの left だけが座標の配置です。値が一場面の転がる時間(05 は0.7秒)より頻繁に入ってくるなら、転がりを継ぎ足すより目標だけを最新の値に差し替えるほうが簡単です。

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

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