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

traffic light css 9パターン — 信号で車が止まり緑の波がつながる

traffic light css を色を切り替えるだけのタイマーで作ると、車は赤でも突っ込み、信号は互いにずれ、歩行者は残り秒数が分からない — 02番・03番・04番が計算で直す。交差点の九つの場面を、キャンバス一枚に描いた信号と車の動きで見せます。

交差点の九つの場面をこのページで見る

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

traffic light css 9パターン — 間隔・ずれ・検知を計算して交差点が生きる

九つの場面はどれも、480×300(W・H)のキャンバス一枚と状態の文字一行、ボタン一つ、キャプションでできています。入力と時間を受け持つ main は共通で、場面ごとに違うのは makeScene だけです。時間は16ミリ秒(FIXED_DT)ごとに区切って進め、乱数を使う場面(02・03・04・06・07・09)は Math.random ではなく種446の mulberry32 で動かすので、開き直しても同じ画面が繰り返されます。01・05・08 は乱数を使いません。

信号の色をキーフレームで順に変えるだけの traffic light animation css は、赤・黄・緑を時間割どおりに点けるだけで、車も人もその信号を知りません。そこでこの記事は三つの計算を足しました。前の車との間隔と停止線までの距離から加速度を求める式、信号ごとに始まりを遅らせる式、感知線を通る車の間隔から緑の長さを決める式です。

車の計算の骨組みは Lane.accelFor 一つです。目標の間隔を s = g0 + max(0, v·headway + v·(v − vL) ÷ (2·√(accel·brake))) とおき、加速度を accel·(1 − (v ÷ vmax)^δ − (s ÷ s)²) で返します。headway・accel・brake は Lane に渡す引数で、02 では定数 HEADWAY(10)・ACCEL(0.04)・BRAKE_DECEL(0.07)がその値になり、03・07・09 はそれぞれ自分の値を渡します。前の車との間隔から出た値と停止線までの距離から出た値のうち小さいほうを使い、下限は −3·brake です(02 では −0.21)。δ は 02・07・09 で4、03 で40なので、03 は巡航速度の近くまで加速が続き、車群が散りません。Lane の定義は九つのファイルにありますが、実際に車列を作るのは 02・03・07・09 の四つで、01・04・05・06・08 は時間割・弧・視野・反射・衝突の表で解きます。

Math.random・Date・fetch・localStorage・navigator・AudioContext・getImageData は九つのファイルのどこにも出てこず、WebGL も使いません。グラフの線を曲線でつなぐ考え方は css グラフ をSVGで描く9種 — ライブラリなしが扱っていて、07 は同じ発想をキャンバスの上で信号の色に重ねます。

技法別のコードで見る信号

下のコードは各場面の index.html から連続した区間をそのまま載せ、一つの断片は25行以内で切っています。traffic light html css code として貼って試せる大きさで、最初から最後までつながったファイルは圧縮ファイルに入っています。

01緑・黄・赤が時間割どおりに切り替わる信号

phaseAt がフレーム番号を一周期(緑100・黄30・赤100)で割った余りから信号を選び、relax が三つのレンズの明るさを毎フレーム LIT_RISE の0.16ずつ目標へ近づけます。にじみの半径は明るさ1で78、アルファは明るさの1.6乗(LENS_GAMMA)なので、点くレンズは膨らみ、消えるレンズは静まります。測定では動いたフレームが23/23、累積面積は45.4%でした。

緑・黄・赤が時間割どおりに切り替わる信号
新しいタブで開く

01 · 緑・黄・赤が時間割どおりに切り替わる信号 — 計測 累積面積 45.421% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PHASE_FRAMESYELLOW_FRAMESBLOOM_RADIUSLENS_GAMMA
    function phaseAt(f, g, y, r) {
        const total = g + y + r;
        const m = ((f % total) + total) % total;
        return m < g ? SIG_GREEN : (m < g + y ? SIG_YELLOW : SIG_RED);
    }
    function stateText(state) {
        return state === SIG_GREEN ? TXT_GO : (state === SIG_YELLOW ? TXT_READY : TXT_STOP);
    }
        // 点く側は膨らみ消える側は静まる — 目標の明るさへ 1 フレームに LIT_RISE ずつ近づく
        function relax() {
            const t = litOf(state());
            for (let i = 0; i < 3; i += 1) {
                lit[i] += (t[i] - lit[i]) * (reduce ? 1 : LIT_RISE);
            }
        }

02赤信号で前の車との間隔を保って止まる車

accelFor が前の車との間隔から出した値と、停止線を速度0の前の車とみなした値のうち小さいほうを使い、信号が緑でなくなった最初のフレームで v² ÷ (2·BRAKE_DECEL) が停止線までの距離より大きいかを一度だけ見て、止まるか通るかを決めます。シミュレーションを6000フレーム回して数えた64台のうち、赤で停止線を越えた車は0台でした。測定では動いたフレームが23/23、累積面積は10.2%でした。

赤信号で前の車との間隔を保って止まる車
新しいタブで開く

02 · 赤信号で前の車との間隔を保って止まる車 — 計測 累積面積 10.192% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SAFE_GAPBRAKE_DECELSTOP_LINE_XACCEL
                    else {
                        if (!c.decided) {
                            c.decided = true;
                            c.go = c.v * c.v / (2 * p.brake) > sg.x - c.x;
                        }
                        if (!c.go) {
                            acc = Math.min(acc, this.accelFor(c.v, sg.x - c.x - 2, 0, p.lineGap));
                            clampX = sg.x - 1.5;
                        }
                    }
                acc = Math.max(acc, -p.brake * 3);
                const prevX = c.x;
                c.v = Math.max(0, c.v + acc);
                c.x += c.v;
                if (c.x > clampX && prevX <= clampX) {
                    c.x = clampX;
                    c.v = 0;
                }
            // 押すと信号が今の状態からすぐ次の段階へ進む
            tap: function () {
                const m = (((f + skew) % cycle) + cycle) % cycle;
                const target = sig.state === SIG_RED ? 0 : (sig.state === SIG_GREEN ? GREEN_LEN : GREEN_LEN + YELLOW_LEN);
                skew += ((target - m) % cycle + cycle) % cycle;
                tickSignal();
            },

03ブロックごとにずらした四つの信号と続く緑の波

信号 k の状態は phaseAt に f - k * OFFSET_PER_BLOCK を渡して求め、信号ごとに始まりを90フレームずつ遅らせます。100pxのブロックを毎フレーム1.15pxで進むのに87.0フレームかかるので、三台の車群は四つの信号を止まらずに通り、ブロックごとに3.0フレームずつ遅れて四つ目で累積9フレームずれても、緑が150フレームあって吸収されます。測定では動いたフレームが23/23、累積面積は12.7%でした。

ブロックごとにずらした四つの信号と続く緑の波
新しいタブで開く

03 · ブロックごとにずらした四つの信号と続く緑の波 — 計測 累積面積 12.669% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CYCLE_LENOFFSET_PER_BLOCKPLATOON_SPEEDBLOCK_LEN
        // 信号 k は前の信号より OFFSET_PER_BLOCK フレーム遅れて同じ周期を始める
        function stateOf(k) {
            return phaseAt(f - k * OFFSET_PER_BLOCK, GREEN_LEN, YELLOW_LEN, CYCLE_LEN - GREEN_LEN - YELLOW_LEN);
        }
        function progress(k) {
            return ((((f - k * OFFSET_PER_BLOCK) % CYCLE_LEN) + CYCLE_LEN) % CYCLE_LEN) / CYCLE_LEN;
        }
                const m = f % CYCLE_LEN;
                if (m === PLATOON_AT) {
                    toSpawn = PLATOON_SIZE;
                }
                if (toSpawn > 0 && lane.maybeSpawn(f, 0, 0, PLATOON_SPEED)) {
                    toSpawn -= 1;
                }
                lane.step(sigs, none, -1);

04残り秒数を縮む弧で見せる歩行者信号

残り12秒を円周 RING_CIRCUMFERENCE ÷ 12 = 30.4pxから SEG_GAP の5を引いた25.4pxの弧の断片に分けて一つずつ消し、残りが FLASH_AT の5秒以下になると9フレームごとに弧は赤と黄、数字は赤と白へ交互に変わります。すでに渡っている人は HURRY_SPEED の1.5で速くなり、待っている人は点滅が始まると歩き出しません。測定では動いたフレームが14/23、累積面積は13.6%でした。

残り秒数を縮む弧で見せる歩行者信号
新しいタブで開く

04 · 残り秒数を縮む弧で見せる歩行者信号 — 計測 累積面積 13.559% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WALK_SECONDSFLASH_ATRING_CIRCUMFERENCETICK_STEP
        function inWalk() { return (f % cycle) >= WAIT_FRAMES; }
        function secondsLeft() {
            if (!inWalk()) {
                return 0;
            }
            return (cycle - (f % cycle)) / SECOND_FRAMES;
        }
        function flashing() { return inWalk() && secondsLeft() <= FLASH_AT; }
                for (let i = 0; i < peds.length; i += 1) {
                    const p = peds[i];
                    if (!p.crossing) {
                        if (inWalk() && !flashing() && f >= p.startAt) {
                            p.crossing = true;
                        }
                    }
                    else {
                        p.x += p.dir * (flashing() || !inWalk() ? HURRY_SPEED : WALK_SPEED);
                        const done = p.dir > 0 ? p.x >= ROAD_X1 + 12 : p.x <= ROAD_X0 - 12;
                        if (done) {
                            p.crossing = false;
                            p.dir = -p.dir;
                            p.x = p.dir > 0 ? ROAD_X0 - 12 : ROAD_X1 + 12;
                            p.startAt = lastWalkStart + 99999;
                        }
                    }
                }

05見る角度でひさしの影がレンズを覆う信号

目の位置とレンズ中心の差を EYE_DISTANCE の190で割った比に VISOR_DEPTH の24を掛けて、ひさしの開口円をレンズの上でずらし、レンズを9×9の格子(SAMPLE_GRID)でなぞって開口に入った升目の割合を、信号ごとにパーセントで出します。ポインターがないときは autoPath が目を自動で動かし、周期5200ミリ秒で、横は振幅214の正弦波、縦は1.7倍の速さで振幅96の正弦波を重ねた曲線をたどります。測定では動いたフレームが23/23、累積面積は18.5%でした。

見る角度でひさしの影がレンズを覆う信号
新しいタブで開く

05 · 見る角度でひさしの影がレンズを覆う信号 — 計測 累積面積 18.474% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

VISOR_DEPTHVIEW_TILT_MAXSHADOW_BLURLENS_OFFSET
        // 目の位置から見たひさしの開口円のずれ量 — 目が右なら開口が左へ寄り、レンズの右側が隠れる
        function shiftOf(hx, ly) {
            const dx = (ex - hx) / EYE_DISTANCE;
            const dy = (ey - ly) / EYE_DISTANCE;
            const lim = VIEW_TILT_MAX / VISOR_DEPTH;
            return [-VISOR_DEPTH * clamp(dx, -lim, lim), -VISOR_DEPTH * clamp(dy, -lim, lim)];
        }
        // レンズの中を格子でなぞり、開口に入った点の割合を出す
        function visibleOf(hx, ly) {
            const s = shiftOf(hx, ly);
            let hit = 0;
            let all = 0;
            for (let i = 0; i < SAMPLE_GRID; i += 1) {
                for (let j = 0; j < SAMPLE_GRID; j += 1) {
                    const x = (i + 0.5) / SAMPLE_GRID * 2 - 1;
                    const y = (j + 0.5) / SAMPLE_GRID * 2 - 1;
                    if (x * x + y * y > 1) {
                        continue;
                    }
                    all += 1;
                    const ax = x * LENS_R - s[0];
                    const ay = y * LENS_R - s[1];
                    if (ax * ax + ay * ay <= rimRadius() * rimRadius()) {
                        hit += 1;
                    }
                }
            }
            return hit / all;
        }

06濡れた道路に長くにじむ信号の光の反射

上の場面を別に描いておき、道路の行ごとに反転した元の行を REFLECT_STRETCH の1.7倍に伸ばして取り込み、20pxの断片ごとに vnoise の値ノイズで横へ揺らして、遠いほどアルファを下げます。一フレームに drawImage が90行×24断片 = 2160回呼ばれます。測定では動いたフレームが23/23、累積面積は45.0%でした。

濡れた道路に長くにじむ信号の光の反射
新しいタブで開く

06 · 濡れた道路に長くにじむ信号の光の反射 — 計測 累積面積 45.003% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

REFLECT_STRETCHRIPPLE_AMPFADE_RATENOISE_SCALE
                // 反射 — 道路の行ごとに元の行を反転して取り込み、ノイズで横へ揺らし、遠いほど薄くする
                const depth = H - HORIZON;
                const t = reduce ? 0 : f * 0.06;
                for (let dy = 0; dy < depth; dy += ROW_STEP) {
                    const sy = HORIZON - 1 - dy / REFLECT_STRETCH;
                    if (sy < 0) {
                        break;
                    }
                    const near = dy / depth;
                    const alpha = Math.max(0, 0.82 - near * FADE_RATE * 0.8);
                    if (alpha <= 0.02) {
                        continue;
                    }
                    ctx.globalAlpha = alpha;
                    for (let sx = 0; sx < W; sx += SEG_W) {
                        const n = vnoise(sx * 0.031 + t * 0.7, dy * NOISE_SCALE + t) * 2 - 1;
                        const dx = n * RIPPLE_AMP * (0.35 + near * 1.1);
                        ctx.drawImage(src, sx, Math.floor(sy), SEG_W, 1, sx + dx, HORIZON + dy, SEG_W, ROW_STEP + 0.6);
                    }
                }

07信号の周期ごとに行列の長さが上下するリアルタイムグラフ

6フレームごと(SAMPLE_EVERY)に停止線の前で待つ車の数を数えて56点(GRAPH_POINTS)に積み、点の間は隣の点との中点をつなぐ二次曲線で結び、LINE_SMOOTH の0.5が曲がり具合を決めます。点ごとにその瞬間の信号の色を背景に敷くので、列がどの信号で伸び縮みしたかが読めます。測定では動いたフレームが23/23、累積面積は29.7%でした。

信号の周期ごとに行列の長さが上下するリアルタイムグラフ
新しいタブで開く

07 · 信号の周期ごとに行列の長さが上下するリアルタイムグラフ — 計測 累積面積 29.664% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SAMPLE_EVERYGRAPH_POINTSY_MAX_CARSLINE_SMOOTH
        function sample() {
            hist.push(lane.waiting(STOP_LINE_X));
            histState.push(sig.state);
            if (hist.length > GRAPH_POINTS + 1) {
                hist.shift();
                histState.shift();
            }
        }
        function px(i, shift) {
            return GRAPH_X1 - (hist.length - 1 - i + shift) * (GRAPH_X1 - GRAPH_X0) / GRAPH_POINTS;
        }
        function py(v) {
            return GRAPH_Y0 - clamp(v / Y_MAX_CARS, 0, 1) * (GRAPH_Y0 - GRAPH_Y1);
        }
                    for (let i = 1; i < n; i += 1) {
                        const bx = px(i, shift);
                        const by = py(hist[i]);
                        const ex = i < n - 1 ? (bx + px(i + 1, shift)) / 2 : bx;
                        const ey = i < n - 1 ? (by + py(hist[i + 1])) / 2 : by;
                        ctx.quadraticCurveTo(lerp((sx + ex) / 2, bx, LINE_SMOOTH), lerp((sy + ey) / 2, by, LINE_SMOOTH), ex, ey);
                        sx = ex;
                        sy = ey;
                    }

08四つ角でぶつからない方向ごとにまとめて点く矢印

8方向ごとにぶつかる組をビット表(CONFLICT_MASK)にし、前の方向から順にぶつからない組へ入れて四つの段階(PHASE_ORDER)を得ます。段階ごとに緑72フレームのあと全部が赤の間16フレーム(ALL_RED_GAP)を置くので、一周は352フレームです。測定では動いたフレームが20/23、累積面積は20.4%でした。

四つ角でぶつからない方向ごとにまとめて点く矢印
新しいタブで開く

08 · 四つ角でぶつからない方向ごとにまとめて点く矢印 — 計測 累積面積 20.43% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PHASE_ORDERARROW_LENALL_RED_GAPCONFLICT_MASK
        function collides(a, b) {
            if (a.approach === b.approach) {
                return true;
            }
            if ((a.approach + 2) % 4 === b.approach) {
                return a.left !== b.left;
            }
            return true;
        }
        // 位相のまとめ — 先にできた組に衝突する方向がなければ、そこへ入れる
        const PHASE_ORDER = [];
        for (let i = 0; i < moves.length; i += 1) {
            let placed = false;
            for (let g = 0; g < PHASE_ORDER.length && !placed; g += 1) {
                let ok = true;
                for (let k = 0; k < PHASE_ORDER[g].length; k += 1) {
                    if ((CONFLICT_MASK[i] >> PHASE_ORDER[g][k]) & 1) {
                        ok = false;
                    }
                }
                if (ok) {
                    PHASE_ORDER[g].push(i);
                    placed = true;
                }
            }
            if (!placed) {
                PHASE_ORDER.push([i]);
            }
        }

09車が途切れると緑を切り上げ続くと延ばす感応式信号

感知線を車が通るとき gapTimer が60未満なら、緑の限度(budget)を今の緑の長さに EXTEND_STEP の50を足した値へ延ばしますが MAX_GREEN の300は超えず、MIN_GREEN の90を過ぎて gapTimer が60以上になると限度を今に縮めて緑を終えます。延ばすたびにバーへ点が一つ打たれます。測定では動いたフレームが23/23、累積面積は10.0%でした。

車が途切れると緑を切り上げ続くと延ばす感応式信号
新しいタブで開く

09 · 車が途切れると緑を切り上げ続くと延ばす感応式信号 — 計測 累積面積 10.003% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GAP_THRESHOLDEXTEND_STEPMAX_GREENDETECTOR_X
        function onDetect(c, x) {
            detLit = 8;
            if (sig.state !== SIG_GREEN) {
                return;
            }
            if (gapTimer < GAP_THRESHOLD) {
                budget = Math.min(MAX_GREEN, Math.max(budget, greenElapsed + EXTEND_STEP));
                pips.push(greenElapsed);
            }
            gapTimer = 0;
        }
                if (sig.state === SIG_GREEN) {
                    greenElapsed += 1;
                    gapTimer += 1;
                    if (greenElapsed >= MIN_GREEN && gapTimer >= GAP_THRESHOLD && greenElapsed < budget) {
                        budget = greenElapsed;
                        ended = 'gap';
                    }
                    if (greenElapsed >= MAX_GREEN) {
                        budget = greenElapsed;
                        ended = 'max';
                    }
                    if (greenElapsed >= budget) {
                        sig.state = SIG_YELLOW;
                        phaseF = 0;
                        if (ended === '') {
                            ended = 'gap';
                        }
                    }
                }

値が場面を変えるところ — 停止間隔・信号のずれ・緑の上限

値一つで場面の性格が変わるのは三か所です。02 の停止間隔 SAFE_GAP、03 の信号のずれ OFFSET_PER_BLOCK、09 の緑の上限 MAX_GREEN で、下の数字は同じソースをノードのヘッドレスで6000〜9000フレーム回して数えた値なので、ブラウザの画面で直接測った数値とは区別してください。

停止間隔を狭めると列が長くなる

SAFE_GAP が24のとき、止まった車どうしの間隔は18.8〜19.1pxで、停止線の前に並ぶ車は最大3台です。12に減らすと間隔は10.1〜10.3px、並ぶ車は最大5台になります(6000フレームのシミュレーション)。

信号のずれが87.0から外れると車が止まる

OFFSET_PER_BLOCK が90のときは、62台のうち止まった車が一台もありません(stopFrames が0)。40に減らすと38台が止まり、同時に2台まで並び、止まった車のフレーム合計は3915になります。

緑の上限を下げると切られる回数が増える

9000フレームで緑は25回点き、長さは最短90・平均204・最長300、上限に届いて切られたのが9回、車が途切れて切り上げたのが16回です。MAX_GREEN を150に減らすと、緑31回・平均143・上限到達26回・切り上げ5回に変わります。

九つの場面を描画して測った値は次の表です。10%を下回る場面はなく、最小の 09 が10.0%、次の 02 が10.2%で、01 と 06 は45%前後です。動いたフレームは 04 が14/23、08 が20/23で、残りの七つは23/23なので、デモが回っているあいだ画面はほとんど止まりません。

項目 累積面積 平均強度 動いたフレーム
01 時間割の信号 45.4% 109.2 23/23
02 間隔を保って止まる車 10.2% 48.2 23/23
03 緑の波 12.7% 51.2 23/23
04 歩行者のカウントダウン弧 13.6% 40.5 14/23
05 ひさしの影と視野角 18.5% 50.5 23/23
06 濡れた道の反射 45.0% 33.4 23/23
07 行列の長さのグラフ 29.7% 46.3 23/23
08 四つ角の段階の矢印 20.4% 71.4 20/23
09 感応式の緑の延長 10.0% 63.3 23/23

どこで壊れるか — 狭い画面・車が停止線を越えるとき・色だけの区別

向かい合う左折の曲線が角で触れ合う

08 の collides の表は、向かい合う左折どうしを衝突とみなしませんが、描かれた二本の曲線は交差点の中央の右下と左上の角(約±10px)でほとんど触れます。正しいのは表で、絵は単純化です。交差点の中を走る点は点いている段階だけに描かれる飾りで、段階が終わるとその場で消えるので、車の物理ではありません。

歩き出して153フレーム以降の人は赤でも道に残る

04 は歩行の開始から182フレーム(残り5秒)を過ぎると、新しく出発させません。幅218pxの道を毎フレーム0.8pxで渡ると272.5フレーム、点滅のあと毎フレーム1.5pxなら145.3フレームかかるので、歩行の153フレーム以降に出発した人は、歩行(312フレーム)が終わったあとも道にいます。最後に許される出発の181フレームなら14フレーム(0.2秒)余計にかかり、赤の表示のあとも渡っています。

止まった車の間隔が SAFE_GAP より5px短い

02 は SAFE_GAP の24を目標にしますが、止まった車どうしの実測は18.8〜19.1pxです。減速の下限 −0.21 と、停止線の手前1.5pxで速度を0に切る処理を原因の候補と見ていますが、原因を切り分けて確かめてはいません。

反射から赤いレンズが消える値

06 の道路180行は、REFLECT_STRETCH が1.7のとき元の106行(y 14以降)を覆い、手前の信号の赤いレンズ(元の y 24.5)が反射に入ります。2.1に上げると同じ180行が86行(y 34以降)しか覆わず、そのレンズは反射から外れて緑のレンズだけが一番下に残ります。揺れは20px断片の単位なので、断片の境目でずれが見え、手前の道路では最大±10pxまで揺れます。

矢印キーがページのスクロールを止める

キャンバスにフォーカスがあると、矢印キー・Enter・Space に対して各ファイルが preventDefault を呼びます。矢印キーに反応する(point を動かす)のは 05 だけなので、ほかの場面では何も起きないのにスクロールだけが止まります。このコードを長いページへ貼るときは、キャンバスの上でキーボードのスクロールが止まらないかを先に確かめてください。Enter と Space は、02 と 09 では tap として信号の切り替えと車の投入になり、残りの七つでは一時停止と再生になります。02 と 09 の一時停止はボタンだけでできます。

信号を色だけで読ませない

レンズは上から赤・黄・緑の順(LENS_RGBS)で並び、状態は文字でも aria-live の領域に書かれます。色が見分けにくい状況でも位置と文字が状態を伝える作りで、信号の色三つ(GREEN_RGB・YELLOW_RGB・RED_RGB)は機能色なので背景のパレットとは別にしています。画面の文字は日本語版でも決まった文言のままなので、ここでは引用せず、文字でも表示されるとだけ書きます。

狭い画面と動きの判定

キャンバスは aspect-ratio の480 / 300で幅だけが縮み、九つの場面のはみ出し・スマホ幅のはみ出しの判定はすべて「いいえ」です。320px以下ではキャプション・ボタン・状態の文字が10pxに縮み、余白が $sp-2 に減ります。動いたフレームが23に届かない場面の原因は確かめていません。

値を自分で変えて境目を確かめる

02 の SAFE_GAP、03 の OFFSET_PER_BLOCK、06 の REFLECT_STRETCH、09 の MAX_GREEN を一つずつ変えて、上の境目が現れる値を探せます。どの値でどう崩れるかを手で見たいときは、下のファイルをパスワード 5d7zsm93 で開いて、これらの定数を書き換えてみてください。

変形三つとアクセシビリティ

三つの定数を動かした結果

名前 値 感じ
停止間隔を変える 02 SAFE_GAP 24 → 12 間隔を12に減らすと止まった車の間が19pxから10pxに狭まり、停止線前に並ぶ車が最大3台から5台に増えて列が長くなります。
信号のずれを変える 03 OFFSET_PER_BLOCK 90 → 40 ずれを40に減らすと信号が緑の波の拍から外れ、6000フレームで作った62台のうち止まる車が0台から38台に増え、同時に2台まで列を作ります。
緑の上限を変える 09 MAX_GREEN 300 → 150 上限を150に減らすと、緑が延びきって打ち切られる回数が9回から26回に増え、緑の平均の長さは204フレームから143フレームに縮みます。

三つの変形の数字は、すべてノードのシミュレーションで数えた値です。

reduce と矢印キーのときの動き

prefers-reduced-motion が有効だと、九つの場面は requestAnimationFrame のループを始めず、最初の場面を75ステップ(1200ミリ秒)進めた一枚だけを描きます。ボタンの文字は「次の場面」に替わり、押すたびに56ステップ(900ミリ秒)ぶん進めて描き直します。ポインターの動きは場面ごとに違い、05 は目の位置の補間(lerp の0.16)を飛ばして入力値をそのまま使い、02 は押すと信号はすぐ替わりますが車は「次の場面」を押すまで止まったままで、01 は relax の補間係数が1なので明るさがすぐ目標へ行きます。

キャンバスは tabindex が0でフォーカスを受け、矢印キーは仮想のポインターを KEY_STEP の12pxずつ動かしますが、反応するのは 05 だけです。状態の文字は、場面が決めた文言が替わったときだけ aria-live="polite" の領域を書き換えるので、同じ文言が繰り返し読み上げられることはありません。

traffic light html css 風の信号は、タイマーで色を切り替えるだけで足りるか?

信号一つが案内の絵なら足ります。01 の phaseAt 一行とレンズの明るさの補間がそのすべてで、車や人が信号を見て動く必要が出た時点から、ずれ・間隔・検知の計算が要ります。色を点けるだけのタイマーは、信号がいくつあっても互いの開始時刻を知らないので、03 のようにずれを数字で合わせることになります。

場面ごとに合う置き場所も違います。01 は信号の紹介・案内画面、02 は交通の流れを説明する場面、03 は都市の交通を紹介するヒーロー、04 は横断歩道の案内ウィジェット、05 は製品の詳細ビュー、06 は夜の街の背景場面、07 は交通ダッシュボードの指標カード、08 は交差点の構造を説明する図、09 はスマート交通サービスの紹介に向きます。06 は一フレームに2160回描くぶん最も重い場面なので、背景に敷くときは先にこの場面のフレームの重さを見てください。

光源から影の形を計算する側は css 影 おしゃれ 9パターン — 光の位置から影を計算する場面へ続き、05 のひさしの影は同じ考え方をレンズの中で格子に数えたものです。

traffic light css を作る前によくある質問

traffic light using css で車まで動かすには、どこから読みますか?

02 の Lane と accelFor から読みます。最初に変える値は SAFE_GAP・FREE_SPEED・BRAKE_DECEL の三つで、信号一つの前で車が止まって出発する構造なので、03・07・09 へ進む前に読むのに向いています。

信号の色や周期を自分のページに合わせて変えるには?

色は GREEN_RGB・YELLOW_RGB・RED_RGB の定数三つを、周期は phaseAt に渡す緑・黄・赤のフレーム数三つを替えます。場面ごとに定数の名前は違い、01 は PHASE_FRAMES と YELLOW_FRAMES、02 は GREEN_LEN・YELLOW_LEN・RED_LEN です。

ライブラリや画像は必要ですか?

必要ありません。九つのファイルのどこにも外部への呼び出しがなく、場面はすべてキャンバスの2dで描いているので、ファイル一つを移すだけで動きます。

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

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