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

ストップウォッチ 9種 — 滑らかに回る秒針からラップ記録とタイム比較まで

ストップウォッチは時間をどうやって数字と針に変えるのでしょうか。01番は整数のセンチ秒ひとつから秒針の角度を決め、03番はラップと累計を一つの表で計算し、08番は前回の記録の影の針と今の針の差を読みます。

ストップウォッチは時計を読むことではなく、経過した時間を整数ひとつで数え、その整数から数字も針の角度もすべて計算する仕組みです — ストップウォッチを作るときは数える単位をひとつに決めるところから始まります

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

九つの場面が時間を数えて読む方法は、ひとつで同じです。経過時間はセンチ秒(100分の1秒)の整数ひとつで、数字盤の分・秒・センチ秒も文字盤の針の角度もラップの区間も、この整数からだけ計算されます。時間の元は場面の時計 sceneT ひとつで、フレームごとに rAF のタイムスタンプの差 dt を足し、一コマが DT_MAX の0.05秒を超えないように切ります。

並び順は針ひとつから記録へ広がります。01・02は針と数字盤を、03・04はラップの記録を表と棒で、05・06は停止のときの弾みと時間が繰り上がる補助文字盤を、07・08・09はラップが積み上げる輪と影の針と電光掲示を扱います。

ストップウォッチ 9種 — 秒針が滑らかに回る文字盤・分 秒 センチ秒に分ける数字表示・ラップと累計を並べる記録表・ラップの長さを比べる棒・止めるときに針が弾むバネ・分と時間を数える補助文字盤・ラップごとに弧を残す軌跡のリング・前回の記録と競う影の針・電光掲示のヒーロー

九つの場面の面積と動き

場面 累計面積 動いたフレーム 強度の平均
01 連続の秒針 9.9% 23/23 43.9
02 センチ秒の数字盤 12.3% 23/23 68.1
03 ラップの分割表 11.1% 23/23 56.9
04 ラップの棒 8.9% 23/23 49.6
05 バネの停止 21.0% 23/23 95.7
06 補助文字盤 11.1% 23/23 39.7
07 軌跡のリング 9.2% 23/23 102.4
08 影の針 18.3% 23/23 90.6
09 電光掲示 20.6% 23/23 95.7

針が大きく弾んで二つの盤がいっしょに光る05が21.0%でいちばん広く、棒だけが伸びる04が8.9%でいちばん狭くなっています。強度の平均は07が102.4で最も高く06が39.7で最も低く、動いたフレームは九つの場面とも23/23なので、止まる区間がなく変わり続けます。

01途切れず回る秒針の文字盤

秒針の角度はセンチ秒の整数ひとつから HAND_ANGLE(cs) で決まり、TURN_CS の6000で一周します。目盛りは TICK_COUNT の60本で MAJOR_EVERY の5本ごとに太く、右の盤は秒単位の値 stepCs() でしか針を立てないのでカクカク動き、秒が変わるたびにハブからふちへ波紋が広がります。運動の記録・実験・料理の時間を測る文字盤のストップウォッチに使えます。

途切れず回る秒針の文字盤
新しいタブで開く

01 · 途切れず回る秒針の文字盤 — 計測 累積面積 9.893% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TURN_CSHAND_ANGLETICK_COUNT
    const TURN_CS = 6000;
    const TICK_COUNT = 60;
    const MAJOR_EVERY = 5;
    const WINDOW_S = 4;
    const LAP_S = 15.0;
    const START_S = 3.7;
    const REST_S = 3.7;
    const RESULT_S = 13.0;
    let hist = [];
    function build() {
        swReset();
        hist = [];
    }
    function update(dt) {
        if (dt < 0) {
            return;
        }
        hist.push({ t: sceneT, cs: swCs(sceneT) });
        while (hist.length > 1 && hist[0].t < sceneT - WINDOW_S) {
            hist.shift();
        }
    }
    function handCs() { return swCs(sceneT); }
    function stepCs() { return Math.floor(handCs() / CS_PER_SEC) * CS_PER_SEC; }
    function HAND_ANGLE(cs) { return dialAng(cs, TURN_CS); }

02分・秒・センチ秒に分けて表示する数字盤

splitCs はセンチ秒を6000で割った商を分、余りを100で割った商を秒、残りをセンチ秒として、整数の割り算だけで分けます。右の盤は0.01をセンチ秒の数だけ足した合計を読むため、0.01を11回足すと0.10999999999999999になって11でなく10と読まれ、1700センチ秒のうち1498マスがずれます。記録が小数点以下で揺れてはいけない大会や計測の画面の数字は、この方式で作ります。

分・秒・センチ秒に分けて表示する数字盤
新しいタブで開く

02 · 分・秒・センチ秒に分けて表示する数字盤 — 計測 累積面積 12.269% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CS_PER_MINsplitCsSTEP_F
    function build() {
        swReset();
        errs = [0];
        sums = ['0'];
        bad = [0];
        let acc = 0;
        for (let n = 1; n <= MAX_CS; n += 1) {
            acc += STEP_F;
            errs.push(n - Math.floor(acc * CS_PER_SEC));
            sums.push(acc.toPrecision(17));
            bad.push(bad[n - 1] + (errs[n] !== 0 ? 1 : 0));
        }
    }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function nowCs() { return Math.min(swCs(sceneT), MAX_CS); }
    function floatCs() { return nowCs() - errs[nowCs()]; }

03ラップと累計をひとつの表で計算する記録表

ランニングの区間記録や発表の時間配分の表では、lapSpan(i) が今の累計から直前の累計を引いて区間の時間を出し、累計は隣の列にそのまま残します。ストップウォッチ ラップタイムの表として、最初の盤は累計200 452 714 936 1208 1410 1712センチ秒から区間200 252 262 222 272 202 302センチ秒を出します。右の盤は押した時刻を区間の列に書いてその合計を累計の列に書くので、累計が6632センチ秒まで膨らみます。

ラップと累計をひとつの表で計算する記録表
新しいタブで開く

03 · ラップと累計をひとつの表で計算する記録表 — 計測 累積面積 11.107% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PRESS_ATlapSpanTABLE_ROWS
    let pressT = [];
    function build() {
        swReset();
        pressT = [];
        for (let i = 0; i < PRESS_AT.length; i += 1) {
            const t = PRESS_AT[i] + ((lap * 7 + i * 3) % 5) * 0.04;
            pressT.push(t);
            script.push({ t: t, f: function () { pressLap(t); } });
        }
    }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function lastPress() { return marks.length > 0 ? pressT[marks.length - 1] : -9; }
    function liveSpan() { return swCs(sceneT) - (marks.length > 0 ? marks[marks.length - 1] : 0); }

04ラップの長さを棒で比べ最速と最遅を示す比較

棒の長さは BAR_RANGE(i) が(区間 − 基準)÷(最大値 − 基準)で決め、基準 baseOf() は最小値から範囲の BASE_PAD 0.25だけ下に置きます。最初の盤の区間は324 247 320 264 298 239 279センチ秒で(最初のラップ324は開始の0.4秒後から数えた値なので長い)、最小値239の行にミント色の最速の帯、最大値324の行にピンク色の最遅の帯が敷かれ、点線は平均の281.57です。基準を最小値に置くといちばん短いラップの棒が0になって空に見えるため、区間ごとの速さの比較やペース確認の画面はこの余白を残します。

ラップの長さを棒で比べ最速と最遅を示す比較
新しいタブで開く

04 · ラップの長さを棒で比べ最速と最遅を示す比較 — 計測 累積面積 8.885% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BASE_PADBAR_RANGESPANS
    function baseOf() { return minOf() - (maxOf() - minOf()) * BASE_PAD; }
    // 棒の長さの割合 0〜1 — ラップがひとつだけだと範囲が0になるので固定値
    function BAR_RANGE(i) {
        const hi = maxOf();
        const lo = baseOf();
        return hi - lo > 0 ? (lapSpan(i) - lo) / (hi - lo) : 0.6;
    }
    function update(dt) {
        const k = 1 - Math.exp(-EASE_RATE * dt);
        for (let i = 0; i < marks.length; i += 1) {
            if (i >= disp.length) {
                disp.push(0);
            }
            disp[i] += (BAR_RANGE(i) - disp[i]) * k;
        }
    }

05止めるときに針が一度弾んで止まるバネの停止

停止を押すと数字はその値で止まり、vel に KICK_V の3.6が入って、針は目標の角度に OVERSHOOT() を足したまま SPRING_K 420・DAMPING_C 14のバネに沿って戻ります。最初の頂点は6.18度(0.067秒)で、反対側の−1.93度(0.217秒)まで行ってから0.417秒に0.5度以内に収まって止まり、右の盤は針がその場で急に止まるので停止が途切れて見えます。停止・再開ボタンの操作感や計器の針の停止に使います。

止めるときに針が一度弾んで止まるバネの停止
新しいタブで開く

05 · 止めるときに針が一度弾んで止まるバネの停止 — 計測 累積面積 20.954% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPRING_KDAMPING_COVERSHOOT
    function build() {
        swReset();
        stopT = 4.0 + 0.43 * (lap % 3);
        ov = 0;
        vel = 0;
        trace = [];
        script.push({ t: stopT, f: function () { swStop = stopT; vel = KICK_V; } });
    }
    // 一コマを0.004秒以下の欠片に分けて積分 — 一コマが長くてもバネが暴走しない
    function update(dt) {
        const n = Math.max(1, Math.ceil(dt / 0.004));
        const h = dt / n;
        for (let i = 0; i < n; i += 1) {
            vel += (-SPRING_K * ov - DAMPING_C * vel) * h;
            ov += vel * h;
        }
        if (sceneT >= stopT - 0.3) {
            trace.push({ t: sceneT, o: ov });
        }
        while (trace.length > 1 && trace[0].t < sceneT - TRACE_S - 0.3) {
            trace.shift();
        }
    }

06分と時間を数える補助文字盤

vcs() は場面の時間に SCALE の300を掛けて同じ swCs からセンチ秒を得て、MIN_QUOTIENT() はその値を6000で割った商、HOUR_QUOTIENT() は分の商を60で割った商です。三本の針の角度は一つの値の商と余りから出て、商が増えるとその補助文字盤の枠が光り、12.00秒に360000センチ秒で時の商が1になり、17.0秒には510000センチ秒で1時間25分まで進みます。ストップウォッチ 勉強用のタイマーや、マラソン・長時間作業・長い実験のように一時間を超える測定に使えます。

分と時間を数える補助文字盤
新しいタブで開く

06 · 分と時間を数える補助文字盤 — 計測 累積面積 11.069% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MIN_QUOTIENTHOUR_QUOTIENTSUBDIAL_ANGLE
    function vcs() { return swCs(sceneT * SCALE); }
    function MIN_QUOTIENT() { return Math.floor(vcs() / CS_PER_MIN); }
    function HOUR_QUOTIENT() { return Math.floor(MIN_QUOTIENT() / 60); }
    function update(dt) {
        if (dt < 0) {
            return;
        }
        if (MIN_QUOTIENT() !== prevMin) {
            prevMin = MIN_QUOTIENT();
            flashMin = sceneT;
        }
        if (HOUR_QUOTIENT() !== prevHour) {
            prevHour = HOUR_QUOTIENT();
            flashHour = sceneT;
        }
    }
    function SUBDIAL_ANGLE(frac) { return frac * TAU - PI / 2; }

07ラップごとに色の弧を残す軌跡のリング

ARC_SPAN(c0, c1) は直前のラップ地点から今までのセンチ秒の区間を一周 TURN_CS の1200(12秒)の境目で切り、(輪の番号、開始角、終了角)で返し、輪の半径は RING_R0 の100から番号ごとに RING_STEP の17ずつ縮みます。11ラップの累計250 515 735 1050 1220 1465 1755 1990 2300 2505 2765センチ秒は輪3まで積み上がり、5番目と10番目のラップが境目で二つに切られ、ラップを押した瞬間に文字盤の背景がそのラップの色で光ります。区間の割合をひと目で見せる要約リングや運動アプリの記録画面に使います。

ラップごとに色の弧を残す軌跡のリング
新しいタブで開く

07 · ラップごとに色の弧を残す軌跡のリング — 計測 累積面積 9.233% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ARC_SPANRING_STEPTURN_CS
    function ARC_SPAN(c0, c1) {
        const out = [];
        let s = c0;
        while (s < c1) {
            const ring = Math.floor(s / TURN_CS);
            const e = Math.min(c1, (ring + 1) * TURN_CS);
            out.push({ ring: ring, a0: dialAng(s, TURN_CS), a1: ((e - ring * TURN_CS) / TURN_CS) * TAU - PI / 2 });
            s = e;
        }
        return out;
    }
    function paintSpan(c0, c1, col, a) {
        const arcs = ARC_SPAN(c0, c1);
        for (let i = 0; i < arcs.length; i += 1) {
            arcBand(CX, CY, RING_R0 - arcs[i].ring * RING_STEP, arcs[i].a0, arcs[i].a1, ARC_W, col, a);
        }
    }
    function ringOf(cs) { return Math.floor((cs - 1) / TURN_CS) + 1; }

08前回の記録の影の針と競う比較

progress(m, t) は時刻列 m で t 秒までに何番目の地点の間のどのあたりにいるかを返し、reachAt はその逆で進み具合 p に着いた時刻です。この二つから GAP_SEC(t)(同じ進み具合で前回の記録が着いた時刻 − 今の時刻)を出し、正ならミント色、負ならピンク色で SIGN_COLOR が塗り、最初の盤の地点ごとの差は−0.18 +0.54 −0.14 +0.98 +1.20秒で、時間に沿った差の符号は3回入れ替わります。自己ベストへの挑戦や繰り返し作業の時間短縮の確認に使います。

前回の記録の影の針と競う比較
新しいタブで開く

08 · 前回の記録の影の針と競う比較 — 計測 累積面積 18.317% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GAP_SECSIGN_COLORGHOST
    // 時刻列 m での進み具合 — 地点の間は直線でつなぎ、最後の地点のあとは待つ
    function progress(m, t) {
        if (t <= 0) {
            return 0;
        }
        for (let k = 0; k < m.length - 1; k += 1) {
            if (t < m[k + 1]) {
                return k + (t - m[k]) / (m[k + 1] - m[k]);
            }
        }
        return LEGS;
    }
    // 進み具合 p に着いた時刻 — progress の逆
    function reachAt(m, p) {
        const k = Math.min(Math.floor(p), LEGS - 1);
        return m[k] + (p - k) * (m[k + 1] - m[k]);
    }
    function GAP_SEC(t) {
        const tn = Math.min(t, nowMarks[LEGS]);
        return reachAt(GHOST, progress(nowMarks, tn)) - tn;
    }
    function gapCs(t) { return Math.round(GAP_SEC(t) * CS_PER_SEC); }
    function SIGN_COLOR(g) { return g >= 0 ? MINT : ROSE; }
    function signed(cs) { return (cs >= 0 ? '+' : '−') + (Math.abs(cs) / CS_PER_SEC).toFixed(2); }

09大きな数字盤とラップ一覧を組み合わせた電光掲示のヒーロー

update は毎フレーム fmtCs の文字列を前の文字と比べ、変わった桁の時刻 chAt を書き、その時刻から FLIP_S の0.07秒のあいだ上の半分が折れて下の半分が開きます(いちばん右のセンチ秒の桁は変わるのが速すぎて、折らずにそのまま変わります)。新しいラップは一覧の先頭に入って残りが SLIDE_S の0.25秒で一行ずつ下がり、左は LIST_MAX の5行を超えるといちばん古い行が下へ消えます。行数の制限がない右の盤は8ラップから数字盤の下側が切れ、10ラップからパネルの外へ押し出され、スポーツ中継やイベントのランディングページの時間表示に使います。

大きな数字盤とラップ一覧を組み合わせた電光掲示のヒーロー
新しいタブで開く

09 · 大きな数字盤とラップ一覧を組み合わせた電光掲示のヒーロー — 計測 累積面積 20.579% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIST_MAXFLIP_SSLIDE_S
    function update(dt) {
        if (dt < 0) {
            return;
        }
        const s = fmtCs(swCs(sceneT));
        for (let i = 0; i < s.length; i += 1) {
            if (s[i] !== curCh[i]) {
                prevCh[i] = curCh[i];
                curCh[i] = s[i];
                chAt[i] = sceneT;
            }
        }
    }

ストップウォッチのタイマーをコードで書くと、数字ひとつと針ひとつはいくつの値で決まるのでしょうか?

数字ひとつはセンチ秒の整数ひとつから出ます

センチ秒の整数 cs がひとつあれば、splitCs が分・秒・センチ秒を整数の割り算で分けます。開始前は0で、停止後は停止の瞬間の値で止まるのも swCs(t) の中の swStop ひとつの役目です。ストップウォッチ タイマーの数字は、この二つの関数のほかに時間に触れる場所がありません。

    const CS_PER_SEC = 100;
    const CS_PER_MIN = 6000;
    let swStart = 0;
    let swStop = -1;
    // 場面時刻 t での経過センチ秒 — 開始前は0、停止後は停止の瞬間の値で止まる
    function swCs(t) {
        const e = Math.min(t, swStop >= 0 ? swStop : 1e9) - swStart;
        return e <= 0 ? 0 : Math.floor(e * CS_PER_SEC + 1e-6);
    }
    // ラップ — 押した瞬間の累計センチ秒の一覧。値は場面時刻 t で計算するのでコマの長さと関係ない
    let marks = [];
    function pressLap(t) { marks.push(swCs(t)); }
    // ラップ区間 = 今回の累計 − 直前の累計
    function lapSpan(i) { return marks[i] - (i > 0 ? marks[i - 1] : 0); }
    function pad2(n) { return n < 10 ? '0' + n : String(n); }

針ひとつはセンチ秒と一周の長さの二つの値から出ます

針の角度は cs と一周にあたるセンチ秒 turnCs の二つの値で、dialAng が計算します。01・05は turnCs が6000(60秒)、07は1200(12秒)なので、同じ関数が別の文字盤を描きます。ラップが加わると直前の累計 marks[i−1] がもうひとつ必要になり、区間は lapSpan がこの二つを引いて出します。

    // センチ秒 → [分, 秒, センチ秒] — 整数の割り算だけを使う
    function splitCs(cs) {
        const m = Math.floor(cs / CS_PER_MIN);
        const r = cs - m * CS_PER_MIN;
        const s = Math.floor(r / CS_PER_SEC);
        return [m, s, r - s * CS_PER_SEC];
    }
    function fmtCs(cs) {
        const p = splitCs(cs);
        return pad2(p[0]) + ':' + pad2(p[1]) + '.' + pad2(p[2]);
    }
    // 文字盤の角度(ラジアン、12時が -π/2)— turnCs センチ秒で一周
    function dialAng(cs, turnCs) { return ((cs % turnCs) / turnCs) * TAU - PI / 2; }

経過時間をセンチ秒の整数で数え、分と秒とセンチ秒に分け、秒針の角度に変え、ラップなら直前の値を引くまで — ストップウォッチが一コマ計算される順番です

一コマの計算の順番

一コマは四つの段階で終わります。

  1. frame(ts) が今回と前回のタイムスタンプの差を求め、DT_MAX の0.05秒で切って advance(dt) に渡します。
  2. advance が sceneT に dt を足し、時間表からこの時刻を過ぎた出来事(ラップの押下・停止)を実行します。
  3. draw() が swCs(sceneT) でセンチ秒の整数を得て、splitCs で数字盤を、dialAng で針の角度を描きます。
  4. ラップが押されていれば、その時刻 t で計算しておいた marks から lapSpan が直前の値を引きます。
    const DT_MAX = 0.05;
    function showNow() {
        draw();
        pushStatus(statusText());
        checkDone();
    }
    let rafId = 0;
    let lastTs = -1;
    function frame(ts) {
        if (still || paused) {
            rafId = 0;
            return;
        }
        if (lastTs < 0) {
            lastTs = ts;
        }
        const dt = Math.min((ts - lastTs) / 1000, DT_MAX);
        lastTs = ts;
        if (dt > 0) {
            advance(dt);
        }
        showNow();
        rafId = window.requestAnimationFrame(frame);
    }

ラップの値がコマの長さと関係ない理由

pressLap(t) は描画されたコマではなく押した時刻 t で swCs を呼ぶので、コマが長くなってもラップの値は同じセンチ秒の整数で記録されます。03のラップの時刻が2.00・4.52・7.14秒とコマの間隔に合っていないのも、そのためです。

ストップウォッチの無料サイトで精度を守る方法 — ライブラリなしで作る方法・タブが隠れて戻ると時間がずれる理由・ラップをいくつまで残すか

実際のアプリでは performance.now を時間の元にします

このデモは Date も performance も使わず、rAF のタイムスタンプの差だけを足し上げます。同じ時間表がどの端末でも同じ順番で再生されるようにした選択です。実際のストップウォッチは開始の瞬間の performance.now() を保存しておき、毎フレーム performance.now() − 開始 で経過を計算し直す必要があります。setInterval で10msずつ足すとタイマーが遅れて誤差がたまり、タブが隠れているあいだ止まります。

その経過ミリ秒を Math.floor(ms / 10) でセンチ秒の整数に変えたあとは、このソースの splitCs・dialAng・lapSpan がそのまま使えます。停止するときはその瞬間の値を別に保存しておき、デモでは swStop がその役目です。規格は MDN の requestAnimationFrame と Page Visibility API のドキュメントにあります。

タブが隠れて戻ったとき

デモはタブが隠れるとループを止め、戻ったら最初のフレームの dt を捨てます。止まっていたあいだの時間を一コマに詰め込むと、針が瞬間移動するからです。開始時刻を基準に引き算する実際のアプリでは、隠れていた時間も経過にそのまま入るのでこの処理は要らない代わりに、停止と再開のときに保存する値を正確に持つ必要があります。

どこに付けるか

運動の記録は03の区間表と04の棒が合う場所で、発表の時間配分は03の累計の列と08の影の針で前回の練習との差を読めます。勉強の記録は06の補助文字盤が一時間を超える記録を支え、最初の画面の時間表示は09の電光掲示が受け持ちます。ライブラリ・WebGL・CDNは九つの場面のソースに0件なので、コピーしたファイルひとつで動きます。

ラップの記録をいくつまで残すか

03の表は TABLE_ROWS の6行、09の一覧は LIST_MAX の5行で古い行が消えます。行数の制限がない09の右の盤はラップがたまるほど数字盤を押し出すので、画面に見せる行数を決めて、残りはスクロールや別の記録画面に回すほうがよいです。

ストップウォッチが壊れるのは、小数を足し続けて誤差がたまり数字が揺れるとき・ラップを押した時刻と累計が混ざって記録が狂うとき・止めるときに針が一瞬で止まり途切れて見えるときです

小数を足し続けてたまる誤差

0.01をセンチ秒の数だけ足す方式は、すぐにずれます。11回足した合計が0.10999999999999999なのでセンチ秒が10と読まれ、最初の500センチ秒のうち298マス、1700センチ秒のうち1498マスがずれます。ずれの大きさはいつも1センチ秒なので目立ちにくいのですが、100回足した合計1.0000000000000007は切り捨てるとたまたま100に合ってしまい、どのマスが間違うかを予測しにくくなります。02の開始点である6.55秒では、整数の盤が00:06.55、合計の盤が00:06.54と読まれます。

ラップを押した時刻と区間が混ざる記録

押した時刻 marks[i] を区間の列にそのまま書くと、行ごとに値が増えるだけになります。03の間違った盤は区間の列に200 452 714を書き、その合計を累計の列に書くので、累計が7行目で6632センチ秒になります。正解は同じ押した時刻から直前の累計を引いた200 252 262 222 272 202 302センチ秒です。

止めるときに針が一瞬で止まる停止

数字は停止の瞬間の値で止まるのに針がその場で急に止まると、目には停止が途切れて見えます。05の右の盤がその形で、左は KICK_V の3.6で最初の頂点6.18度まで行き過ぎてから、0.417秒に0.5度以内に収まって止まります。停止の瞬間に二つの盤が一度光るのも、この差を目立たせてくれます。

九つの場面のHTMLとReact版のファイルは下のzipに入っていて、展開するパスワードはvj4wavrbです。

ラップの基準と停止の弾みと一周の長さを変えると手ざわりはどれだけ変わるのでしょうか — BASE_PAD・KICK_V・TURN_CS を変えて比べます

名前 変えた値 手ざわり
基準を最小値に置く 04 BASE_PAD 0.25 → 0 基準が最小値の239に来ると、いちばん短いラップの棒の長さが0.200から0になって棒が消え、二番目に短いラップ(247)も0.275から0.094に縮みます。短いラップどうしの差は大きく見えますが、いちばん短いラップが空に見えます
停止でもっと弱く押す 05 KICK_V 3.6 → 2.0 最初の頂点は6.18度から3.43度に低くなり(頂点の時刻0.067秒は同じ)、0.5度以内に収まるまでの時間が0.417秒から0.283秒に短くなって、弾みが小さくすぐ静まります
一周をもっと短く 07 TURN_CS 1200 → 600 一周が12秒から6秒になると、同じ11ラップで最も内側の輪の番号が3から5に増え、一周の境目で二つに切られるラップが2個から4個になって、弧がより頻繁に内側の輪へつながります

値を変える前に見ること

この三つの値はそれぞれ04・05・07の定数なので、場面ひとつで値をひとつだけ変えると比較が合います。04はラップを押す時刻が盤ごとに変わるため、最初の盤(lap 0)の区間を基準にすると上の数値と合います。05の減衰 DAMPING_C の14とバネ SPRING_K の420はそのままにして KICK_V だけ変えると、頂点の時刻0.067秒が変わらないことも確かめられます。

動きを減らす設定では止まった一枚の文字盤です — 場面の名前といまの記録を文で読み上げるアクセシビリティです

Enter・Space・R でやり直し、P・N で操作します

ステージの枠は九つの場面とも role="application" で、その aria-label に使える操作が書いてあります。Enter・Space・R・クリックは最初からやり直し、P は一時停止、N は時間表の次の出来事まで一歩進めますが、時間表が空の 01・02・06 ではそのまま最初からやり直しになります。ラップのボタンとリセットのボタンはなく、ラップの押下と停止は時間表が自動で見せます。

状態の行はデモの間は静かです

状態の行 data-role="status" はデモの間 aria-live="off" なので毎フレームの更新が読み上げられず、人が最初に触れた瞬間に polite に変わります。結果の案内は data-role="done"(role="status")の一か所に、RESULT_S 秒になったとき一度だけ書きます。キャンバスは role="img" で、aria-label に場面の説明が入っています。

動きを減らす設定と隠れたタブ

prefers-reduced-motion がオンならループを回さず、REST_S 秒の地点の場面を一枚だけ描きます。九つの項目はどれも visibilitychange を受け取り、タブが隠れるとループを止めます。

ストップウォッチ FAQ — ライブラリなしで作る方法・タブが隠れて戻ると時間がずれる理由・ウェブに載せるときに必要なこと

ストップウォッチ 無料のページに載せるとき、このデモの時間の元をそのまま使えますか

そのまま使うことはできません。デモの sceneT は時間表が再現されるように rAF のタイムスタンプの差を足し上げた値なので、実際のページでは開始の瞬間の performance.now() を保存し、performance.now() − 開始 からセンチ秒の整数を作ってから、このソースの計算につなげます。

ストップウォッチにライブラリは必要ですか

必要ありません。キャンバス2Dだけで描かれ、九つの場面のソースにライブラリ・WebGL・CDNが0件で、乱数と Date の呼び出しも0件なので、同じ時間表は同じ順番で再生されます。

ラップの記録はいくつまで残せばよいですか

画面に見せる行数を先に決めれば足ります。03は6行、09は5行で古い行が消えますが、行数の制限がないと一覧が伸びて数字盤が押し出されるので、それ以上は別に保存しておき、画面には最近の行だけを見せます。

ほかの読みものとして、時計の文字盤のデザインを扱った アナログ時計 9種、カウントダウン側の タイマーのリングと砂時計、数字が転がる 数字ローリング が隣にあり、キャンバスの弧は MDN の arc のドキュメントを見てください。

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

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