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

コイントス 9種 — 横の軸で回る3Dの表裏の入れ替えから回数が増えるほど半分へ寄る集計まで

コイントスの表裏はどう決まるのでしょうか。03番は滞空時間に回転の速さを掛けて半回転の偶奇で面を読み、05番は回数が増えるほど半分へ狭まる帯を描き、08番は積んだコインの上を弾んで落ちます。九つの場面は、回転角ひとつがコインの形と上になった面をいっしょに決めるキャンバスのデモです。

コイントスは面を選ぶことではなく角度を読むことです — コイントスのシミュレーションは回転角ひとつで形と面をいっしょに決めるところから始まります

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

九つの場面は同じ決まりで動きます。コインの平たさは角θのcosの値が決め、見える面はそのcosの値の符号が決め、投げの結果は半回転の数の偶奇だけから出ます。乱数は線形合同法の生成器 seeded() 一つだけなので、同じシードからは同じ結果ができ、ソースには Math.random・Date・fetch の呼び出しが0件です。コイン 投げ シミュレーションとして見るなら、この編は計算が見えるデモであり、本物のコインが公平かどうかを調べる道具ではありません。

並び順は一枚のコインから集計へ広がります。01・02は形と側面の光、03・04は投げとふちの着地、05・06・07は投げた結果が積み上げる割合・かたより・連続、08・09は積んだ山の上のはねと二択の決定です。

コイントス 9種 — 平たくなって入れ替わる3Dの表裏・光で塗る金属のふち・半回転の数で面を読む放物線の投げ・狭いすきまで立つふちの着地・半分へ寄る集計の帯・重心をずらしたコイン・連続が伸びる帯・積んだコインの上のはね・二択の決定ヒーロー

九つの場面の面積と動き

場面 累積面積 動いたフレーム 強さの平均
01 横軸の回転 24.5% 23/23 63.7
02 側面の光 13.8% 23/23 65.5
03 投げ 17.1% 23/23 48.5
04 ふち 18.1% 23/23 85.6
05 収束の帯 19.1% 23/23 78.3
06 かたよったコイン 11.4% 23/23 93.9
07 連続の帯 18.3% 23/23 59.7
08 はね 15.8% 23/23 69.1
09 決定ヒーロー 20.3% 23/23 64.0

コインが大きく画面を使う01が24.5%でもっとも広く、二枚のコインの割合の帯だけが伸びる06は11.4%でもっとも狭いのに、強さの平均は93.9で最大です。九つとも23/23なので、描いたフレームのあいだで止まった区間はありません。

01横の軸で回って平たくなり表裏が入れ替わるコイン

コインは直径が変わらない円盤なので、ANGLE() が回した角θでは縦の直径だけが COS_SQUASH 倍に縮み、角が90度を過ぎてcosθが負になると表の模様と裏の模様が入れ替わります。右の小さなコインは同じ平たさで表の模様だけを描きつづけ、面が替わるコインと見比べられます。一回転は OMEGA 4.2ラジアン/秒で1.50秒、最初の面の入れ替えは0.30秒ごろです。

横の軸で回って平たくなり表裏が入れ替わるコイン
新しいタブで開く

01 · 横の軸で回って平たくなり表裏が入れ替わるコイン — 計測 累積面積 24.478% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

OMEGAANGLECOS_SQUASH
    function ANGLE() { return still ? 0 : angle0 + omegaNow * sceneT; }
    function faceName(n) { return n === 0 ? '表' : '裏'; }
    function draw() {
        skyFill(BG_TOP, BG_BOT);
        tableBack(230);
        const th = ANGLE();
        const COS_SQUASH = Math.cos(th);
        const cx = 168;
        const cy = 154;
        // コインの周りの後光 — 縦にどれだけ開いたか(|cos|)に比例して広がる
        disc(cx, cy, COIN_R * (1.15 + 0.35 * Math.abs(COS_SQUASH)), COS_SQUASH >= 0 ? GOLD : SKY, 0.34 * Math.abs(COS_SQUASH));
        oval(cx, 240, COIN_R * (0.8 + 0.3 * Math.abs(COS_SQUASH)), COIN_R * 0.26, INK, 0.2);
        drawCoin(cx, cy, COIN_R, th, THICK_PX, 0.36, -0.46, 0.81, 1, 0);

02側面の厚みと光の向きで塗る金属のふち

コインをななめに立てると二枚の円盤のあいだに側面の帯が見え、側面の周りを RIM_N 40の部分に分けて、部分ごとに法線と光の向きの内積 dotLight で明るさを決めます。回るにつれて明るい部分が片側へ寄って流れ、下の40マスの目盛りも同じ関数の値をそのまま塗ります。右のコインは厚みが0なので側面の部分がなく、紙のように薄く見えます。

側面の厚みと光の向きで塗る金属のふち
新しいタブで開く

02 · 側面の厚みと光の向きで塗る金属のふち — 計測 累積面積 13.775% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

THICK_RIMRIM_NdotLight
    function lightLen() { return Math.hypot(LIGHT[0], LIGHT[1], LIGHT[2]); }
    // 部分一つの明るさ — 部分の法線と光の向きの内積を0から1へ
    function dotLight(pm, th) {
        const n = lightLen();
        const d = (Math.cos(pm) * LIGHT[0] + Math.sin(pm) * Math.cos(th) * LIGHT[1] + Math.sin(pm) * Math.sin(th) * LIGHT[2]) / n;
        return clamp(0.5 + 0.5 * d, 0, 1);
    }

03滞空時間に回転の速さを掛けて半回転の数で面を読む放物線の投げ

手を離れたコインは重力だけが働く放物線を描き、AIR_T 0.9秒に毎秒の半回転の数 hr(= 2 * spinOf(k))を掛けた値が halfTurns です。着地の瞬間に Math.round で四捨五入して偶数なら投げる前の面、奇数なら反対の面が上になります。最初の投げは半回転12.6が13で奇数なので裏、二回目は13.68が14で偶数なので表、三回目は14.76が15で奇数なので裏です。

滞空時間に回転の速さを掛けて半回転の数で面を読む放物線の投げ
新しいタブで開く

03 · 滞空時間に回転の速さを掛けて半回転の数で面を読む放物線の投げ — 計測 累積面積 17.137% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AIR_TSPIN_RATEhalfTurns
        // 半回転の数の読み取り — 投げるあいだ数字が上がり、着地すると四捨五入して面を告げる
        const e = tosses[k];
        const live = tossHalf(e, sceneT);
        const landed = sceneT >= e.t0 + e.T;
        box(286, 66, 184, 74, 12, PAPER, 0.9);
        text('半回転の数', 296, 82, INK, 1, 11, 700, 'left');
        text(live.toFixed(1), 462, 84, INK, 1, 22, 800, 'right');
        if (landed) {
            const r = Math.round(landHalf(e));
            const verdict = '半回転 {r}・{p} → {f}'.replace('{r}', String(r)).replace('{p}', r % 2 === 0 ? '偶数' : '奇数').replace('{f}', faceName(upFace(r)));
            tag(verdict, 462, 124, upFace(r) === 0 ? GOLD : SKY, INK, 12, 'right');
        }
        else {
            text('毎秒の回転数 {s}'.replace('{s}', spinOf(k).toFixed(1)), 296, 124, INK, 1, 11, 700, 'left');
        }

04厚みがつくる狭いすきまでふちを下に立つ着地

コインが立つには、着地の角が EDGE_WIN(thick) で決まる狭い窓に入る必要があり、窓は厚みと直径の比のatanです。N_THROWS 100回を同じシードの数列で投げると、厚み8のコインは8回、厚み16なら14回立ち、厚み0は一度も立ちません。厚み8のとき窓の全幅は半円の7.9%なので、立つのはまれです。

厚みがつくる狭いすきまでふちを下に立つ着地
新しいタブで開く

04 · 厚みがつくる狭いすきまでふちを下に立つ着地 — 計測 累積面積 18.149% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

EDGE_WINtiltAtLandN_THROWS
    function EDGE_WIN(thick) { return Math.atan(thick / DIAM_E); }
    // 投げるたびの着地の傾き(ラジアン、0からπ) — 同じ決まった数列
    function tiltAtLand(seed, n) {
        const rng = seeded(seed);
        const out = [];
        for (let i = 0; i < n; i += 1) {
            out.push(rng() * PI);
        }
        return out;
    }
    // 0 表 · 1 裏 · 2 ふち
    function landKind(ang, thick) {
        if (thick > 0 && Math.abs(ang - PI / 2) < EDGE_WIN(thick)) {
            return 2;
        }
        return ang < PI / 2 ? 0 : 1;
    }

05投げる回数が増えるほど表の割合が半分へ寄って狭まる帯

決まった数列で投げた結果を一回ずつ積み、ratioLine で表の割合を折れ線につなぎ、その周りに bandHalf(n) = 1/(2√n) の幅の帯を重ねます。割合は10回目が0.600、25回目が0.560、50回目が0.520、100回目が0.560で、帯の外へ出た点は outsideCount で数えると100回目までに7個です。横軸がln nなので、最初の数回が横に大きく広がります。

投げる回数が増えるほど表の割合が半分へ寄って狭まる帯
新しいタブで開く

05 · 投げる回数が増えるほど表の割合が半分へ寄って狭まる帯 — 計測 累積面積 19.085% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TOSS_NbandHalfratioLine
    function bandHalf(n) { return 1 / (2 * Math.sqrt(n)); }
    // n回目の投げまでの表の割合(n = 1から始まる)
    function ratioLine(seq) {
        const out = [];
        let heads = 0;
        for (let i = 0; i < seq.length; i += 1) {
            if (seq[i] === 0) {
                heads += 1;
            }
            out.push(heads / (i + 1));
        }
        return out;
    }
    // 帯の外へ出た投げの回数 — n回目の割合が0.5からbandHalf(n)より離れていれば数える
    function outsideCount(seq) {
        const r = ratioLine(seq);
        let c = 0;
        for (let i = 0; i < r.length; i += 1) {
            if (Math.abs(r[i] - 0.5) > bandHalf(i + 1) + 1e-9) {
                c += 1;
            }
        }
        return c;
    }

06重心をずらしたコインが片面へ寄る割合

コインの片側に重さをかたよらせると重い面が下へ沈む角の取り分が大きくなり、ソースの headsP(bias) は0.5にかたよりの半分を足します。同じ数 u でふつうのコイン(u < 0.5)とかたよったコイン(u < 0.55)を分ける pairSeq なので、二枚は同じ投げのなかで分かれます。100回投げると、ふつうのコインは表が51回、BIAS_OFFSET 0.10のかたよったコインは58回です。

重心をずらしたコインが片面へ寄る割合
新しいタブで開く

06 · 重心をずらしたコインが片面へ寄る割合 — 計測 累積面積 11.394% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BIAS_OFFSETheadsPpairSeq
    // かたよったコインの表が上になる確率 — 重い裏が下へ沈む角の取り分なので、0.5にかたより(直径比)の半分を足す
    function headsP(bias) { return 0.5 + bias * 0.5; }
    function pairSeq(seed, n, bias) {
        const rng = seeded(seed);
        const fair = [];
        const heavy = [];
        const p = headsP(bias);
        for (let i = 0; i < n; i += 1) {
            const u = rng();
            fair.push(u < 0.5 ? 0 : 1);
            heavy.push(u < p ? 0 : 1);
        }
        return [fair, heavy];
    }

07同じ面が続くと長さが伸びる連続の帯

投げた結果を一マスずつ横の帯に塗り、cellRun がマスごとに終わる連続の長さを数えて、いちばん長い連続に太い枠を付けます。この200回の数列でいちばん長い連続は8マスで、下の帯の handMade の数列は同じ面を30%の確率でつなぎ、三マス続くと面を替えるので3マスを超えません。表の数は投げた数列が95、作った数列が92なので、個数だけでは見分けられません。

同じ面が続くと長さが伸びる連続の帯
新しいタブで開く

07 · 同じ面が続くと長さが伸びる連続の帯 — 計測 累積面積 18.313% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STRIP_NcellRunhandMade
    // マスごとにそのマスで終わる連続の長さ
    function cellRun(seq) {
        const out = [];
        for (let i = 0; i < seq.length; i += 1) {
            out.push(i > 0 && seq[i] === seq[i - 1] ? out[i - 1] + 1 : 1);
        }
        return out;
    }
    // いちばん長い連続の終わりのマス番号(前のほうを優先)
    function runEnd(seq, upto) {
        const r = cellRun(seq);
        let best = -1;
        let bl = 0;
        for (let i = 0; i < upto; i += 1) {
            if (r[i] > bl) {
                bl = r[i];
                best = i;
            }
        }
        return best;
    }

08積んだコインの上へ落ちて弾む連鎖

積んだ山の上へ落ちると、hopPeaks が落ちた距離から最初の着地の速さを REST_E 0.45倍にして弾む高さを一つずつ出し、速さが HOP_MIN 30を下回ると横になります。山が3枚、4枚、5枚のとき落ちる距離は127.7、116.7、105.7ピクセルで、最初のはねの高さは25.8、23.6、21.4ピクセルと低くなります。はねは三回(25.8、5.2、1.1ピクセル)で、3枚のときの弾んだ時間の合計は0.614秒です。

積んだコインの上へ落ちて弾む連鎖
新しいタブで開く

08 · 積んだコインの上へ落ちて弾む連鎖 — 計測 累積面積 15.761% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

REST_EhopPeaksSTACK0
    // 一回の落下 — 落ちた距離dから最初の着地の速さと弾んだ高さたち
    function hopPeaks(d, e) {
        const out = [];
        let v = Math.sqrt(2 * GRAV * d) * e;
        while (v >= HOP_MIN) {
            out.push(v * v / (2 * GRAV));
            v *= e;
        }
        return out;
    }

09二つの選択肢を掛けて投げる机の決定ヒーロー

机の左に二つの選択肢(本を読む・散歩する)が置かれ、コインが着地すると halfFor が作った半回転の数の偶奇が面を決め、その面の選択肢が明るくなります。最初の一回分の四投は裏・裏・表・裏で、半回転17.15・13.11・15.98・17.09が17・13・16・17に四捨五入されて、本を読む1、散歩する3が積み上がります。tally(face) は着地の0.25秒後に回数を数えます。

二つの選択肢を掛けて投げる机の決定ヒーロー
新しいタブで開く

09 · 二つの選択肢を掛けて投げる机の決定ヒーロー — 計測 累積面積 20.299% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TOSS_COUNTtallyhalfFor
    function build() {
        const rng = seeded(SEED + lap * 31);
        tosses = [];
        faces = [];
        for (let k = 0; k < TOSS_COUNT; k += 1) {
            const f = rng() < 0.5 ? 0 : 1;
            faces.push(f);
            const h = halfFor(f, rng, 6);
            tosses.push({ t0: 0.2 + TOSS_GAP * k, T: AIR_T, x0: 262, y0: 214, x1: 344 + (k % 2) * 36, y1: GROUND_Y, peak: 120, hr: h / AIR_T, h0: 0 });
        }
    }

コイントスのコードを書くと、コインはいくつの値で動くのでしょうか?

コイン一枚の形と面を決める値は四つです

drawCoin の引数は十個で、そのうち回転角 th、厚み thick、光の向き lx·ly·lz、始まりの面 startFace の四つが形と面を決めます。上の円盤は縦の半径が R·|cos th| の楕円で、cos th が正なら始まりの面、負なら反対の面を描きます。側面は周りの40の部分のうち sin(pm) * s が正のものだけを塗ります。

    function drawCoin(cx, cy, R, th, thick, lx, ly, lz, a, startFace) {
        const c = Math.cos(th);
        const s = Math.sin(th);
        const sg = c >= 0 ? 1 : -1;
        const ac = Math.max(Math.abs(c), 0.012);
        const half = thick / 2;
        const near = -sg * s * half;
        const far = -near;
        const nearFace = sg > 0 ? startFace : 1 - startFace;
        if (thick > 0) {
値 意味
th 回転角。形と面がここから出ます
thick 側面の厚み。0なら側面の部分がありません
lx·ly·lz 光の向き。部分の明るさの内積に使います
startFace 角が0のとき見える面。0が表です

最初の画面に置くとき

コイントス サイトに頼らず自分のページの最初の画面に置くなら、09の二つの選択肢のカードが決定の手伝いになります。カードの文字を替えるだけで、昼ごはんや発表の順番のような二つの道から選ぶ場面に使え、面を決める計算は halfFor と upFace の二つの関数に集まっています。この編の03は半回転の数の偶奇で二つの面を分け、サイコロの編は回転行列で立方体の上の面を読む点が違います。

確率の授業で使うとき

授業なら05の帯がまず使われます。投げる回数を100から400にして一ラウンドの時間も延ばすと、帯が半分の幅へ狭まる様子が見え、06は同じ数列で二枚のコインを並べてかたよりが割合にどれだけ出るかを比べさせます。07は人が作った数列と投げた数列を並べ、いちばん長い連続を探させる課題に使えます。

投げる強さを決め、滞空時間を求め、回転数から着地の角を読み、上になった面を決めるまで — コイントスが一回計算される順番です

一回の投げの計算の順番

まず Toss のオブジェクトに出発点・到着点・滞空時間 T・頂点の高さ peak・毎秒の半回転の数 hr を決めます。位置は tossXY が放物線で、角は tossHalf が時間に比例して増やし、着地のあとは halfAt が最も近い整数の半回転へ減衰しながら振動して横にします。最後に upFace が四捨五入した半回転の数の偶奇で上になった面を決めます。

    function tossXY(k, t) {
        const u = clamp((t - k.t0) / k.T, 0, 1);
        return [lerp(k.x0, k.x1, u), lerp(k.y0, k.y1, u) - 4 * k.peak * u * (1 - u)];
    }
    // 着地のあと — いちばん近い平らな角(半回転の数の四捨五入)へ減衰しながら振動して横になる
    function halfAt(k, t) {
        const q = t - (k.t0 + k.T);
        if (q <= 0) {
            return tossHalf(k, t);
        }
        const hl = landHalf(k);
        const hf = Math.round(hl);
        return hf + (hl - hf) * Math.exp(-9 * q) * Math.cos(26 * q);
    }
    function hopLift(k, t) {
        const q = t - (k.t0 + k.T);
        return q <= 0 ? 0 : 12 * Math.exp(-7 * q) * Math.abs(Math.sin(15 * q));
    }
    // 上になった面 — 半回転の数を四捨五入して偶数なら投げる前の面(0)、奇数なら反対の面(1)
    function upFace(h) { return Math.abs(Math.round(h)) % 2; }

一ラウンドがくり返されるときシードが替わる

一ラウンドは LAP_S が過ぎると lap が1上がり、build() が新しい投げと数列を作りますが、04・05・06・07ではシードが SEED + lap に替わります。デモが同じ結果ばかりをくり返さないのはこのシードのおかげで、人がEnter・Space・クリックで投げると restart() が lap を上げて新しいラウンドを開きます。最初のラウンドは START_S だけ先から始まるので、04・06・07は開いた瞬間にもう積み上がった姿で見えます。

コイントスが壊れるのは、面が替わっても模様が同じままのコイン・着地の角が半回転の境目でゆらぐ判定・厚みが0でふちに立てないコインです

面が替わっても模様が同じままのコイン

01の右のコインは scale(1, |cos θ|) だけをかけて faceArt(0, sr, 1) で表の模様ばかりを描くので、いちばん細くなる瞬間にも裏が出ず、平たいシールのように見えます。面の入れ替えをcosθの符号で決める一行が抜けると、コインではなく平たいシールの絵になります。

        // 右の見比べ版 — 面を替えず表の模様だけを描く。いちばん細いときも模様が同じで不自然
        const sx = 372;
        const sy = 160;
        const sr = 42;
        coinShadow(sx, 240, sr, 0, 1);
        ctx.save();
        ctx.translate(sx, sy);
        ctx.scale(1, Math.max(Math.abs(COS_SQUASH), 0.012));
        faceArt(0, sr, 1);
        ctx.restore();

着地の角が半回転の境目でゆらぐ判定

四捨五入の境目は、半回転の数の小数部分が0.5になるところです。03の最初の投げの12.6は境目の12.5から0.1しか離れておらず、SPIN_RATE が7から8に上がるだけで14.4になって面が替わります。09は halfFor が整数の半回転に±0.2しか振らないので境目から0.3以上離れ、判定がゆらぎません。

厚みが0でふちに立てないコイン

04の landKind は thick > 0 のときだけふちを判定し、EDGE_WIN(0) も0なので窓がありません。04の厚み0のコインが100回投げても立たないのはこのためで、厚みを8にすると同じ数列で8回立ちます。立つ回数を増やしたいなら、直径を縮めるか厚みを増やして窓を広げます。

コイントス九場面の完成ソースとReact版のファイルは下のzipに入っていて、展開に使うパスワードはjfztqwghです。

投げる回数とおもりのかたよりと回転の速さを変えると結果の割合はどれだけ変わるのでしょうか — 回数・かたより・速さを変えて比べます

名前 変えた値 感じ
投げる回数を増やして 05 TOSS_N 100 → 400 帯の半幅 1/(2√n) が0.050から0.025へ半分になり、シード5055の数列の表の割合は100回目の0.560(半分から0.060離れて帯の外)から400回目の0.4925(0.0075の差で帯の中)へ落ち着きます。帯の外の点の数は累積なので7個から25個に増えます。ただし TOSS_GAP が0.07なので400回目は28秒ごろに描かれ、画面で見るには LAP_S・RESULT_S・REST_S もいっしょに増やす必要があり、そのままだと一ラウンドで136回目までしか出ません
重さをもっとかたよらせて 06 BIAS_OFFSET 0.10 → 0.25 headsP が0.55から0.625へ上がり、シード6056の数列100回で表が58回から66回に増え、ふつうのコインは51回のままです
もっと速く回して 03 SPIN_RATE 7 → 8 滞空0.9秒のあいだに最初の投げの半回転の数が12.6から14.4になり、四捨五入が13(奇数)から14(偶数)に変わって、上になる面が裏から表へ入れ替わります

値を変える前に見ること

三つの値は別々の場面の定数なので、一つの場面で一つずつだけ変えると比べがきれいです。05と06はシードが固定で同じ値なら同じ数が出ますが、03の SPIN_RATE は投げるたびに SPIN_STEP 0.6ずつ上がるので、最初の投げだけを比べの基準にします。

動きを減らす設定では結果の一枚です — 表裏の結果を文で読み上げ、キーボードでもう一度投げるアクセシビリティです

Enter・Spaceで投げてP・N・Rで操作する

九つの場面ともステージの箱が role="application" で、aria-label に操作が書いてあります。Enter・Space・クリックは新しいシードでの投げ直し、Pは一時停止、Nは一歩進めるキーですが、九つの項目とも時間表の script に項目がないので stepOnce は restart へ行き、止まったまま次の投げの最初の場面を開くだけです。Rは最初からやり直しで、キャンバスは role="img" の aria-label が場面を文で説明します。

    pad.addEventListener('keydown', function (e) {
        if (e.altKey || e.ctrlKey || e.metaKey) {
            return;
        }
        const k = e.key;
        if (k === 'Enter' || k === ' ') {
            e.preventDefault();
            wake();
            restart();
        }
        else if (k === 'p' || k === 'P') {
            togglePause();
        }
        else if (k === 'n' || k === 'N') {
            stepOnce();
        }
        else if (k === 'r' || k === 'R') {
            wake();
            restart();
        }
    }, { signal: off.signal });

状態の行はデモのあいだ静かです

状態の行はデモ中 aria-live="off" なので、コインが回っているあいだ文が流れ込まず、人が最初に触れた瞬間に wake() が polite に替えます。一ラウンドの結果の文は role="status" の data-role="done" の段落に、場面ごとに決めた RESULT_S 秒に一度だけ書き、restart() が doneSaid を解いて新しいラウンドの結果をもう一度知らせます。

    // 一ラウンドのデモが終わったことをスクリーンリーダーに一度だけ知らせる — ラウンドがくり返されても読み直さない
    let doneSaid = false;
    function checkDone() {
        if (!doneSaid && sceneT >= RESULT_S) {
            doneSaid = true;
            doneP.textContent = statusText();
        }
    }

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

prefers-reduced-motion が有効なら、ループなしで showRest が REST_S 秒の地点まで時間表をまわして結果の一枚を描きます。01と02は回転なしで角を固定し(01は表の正面、02はθ = 1.15)、03・09は最初の投げが着地した場面、04・05・06・07・08は投げた結果が積み上がった場面で止まります。REST_S は03・09が1.9秒、08が3.6秒、07が5.6秒、04が6.0秒、05が8.6秒、06が11.0秒です。タブが隠れるとループを止め、戻ったときの最初のフレームの時間差を捨てます。

    function showRest() {
        stopLoop();
        sceneT = 0;
        evAt = 0;
        script = [];
        build();
        runTo(REST_S);
        doneSaid = false;
        showNow();
    }

文字は濃いインク (40, 36, 54) の上で、ピル形ラベルの地とのコントラストが金色11.7、空色9.6、ミント9.1、ピンク6.9、紙色14.0対1です。

コイントスのよくある質問 — ライブラリなしで作る方法・コイントスの確率が本当に半分かどうか・コイントスのサイトの代わりに自分で作るときに必要なもの

コイントス 確率が本当に半分かどうかはこのコードでわかりますか

わかりません。05はシード5055で固定した数列が100回目の0.560から400回目の0.4925へ半分に近づく形を見せる説明用で、本物のコインの公平さを判定する検定はしません。

コイントスにライブラリは必要ですか

必要ありません。キャンバス2Dだけで描かれ、九つの場面のソースにライブラリ・WebGL・CDNが0件で、乱数の呼び出しも0件なので同じシードは同じ結果を出します。

自分で作って抽選に使うには何を足せばよいですか

シードを毎回新しく受け取る部分です。このデモはデモが再現されるようにシードをソースに固定しているので、抽選に使うならシードをユーザーが書き換えられない値で決めるコードを足す必要があり、この編はその部分を作っていません。

参考として、MDNのellipseの文書とrequestAnimationFrameの文書、基本のアニメーションのチュートリアル、動きを減らす設定はprefers-reduced-motionの文書が基準です。回る物はこまの編を、回って止まる盤はルーレットの編を続けて見るとよいです。

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

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