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

電光掲示板 9種 — 5x7のドット文字から残光と行の走査まで

電光掲示板は文字をどうやって点いくつに変えるのでしょうか。01番は5x7の表で1のマスだけを灯し、03番は消えた点に残光を残し、09番は行を一つずつ点ける走査で一枚に見せます。九つの場面は点の格子のバッファを計算して描くコードを、デモと一緒に解きます。

電光掲示板は画面に文字を書くことではなく、文字一つを横5マス縦7マスの点の表に変えて一マスずつ灯す計算です — 電光掲示板を作るには、文字の表と流れる列番号をどう取るかを決めることから始まります

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

九つの場面の枠は一つです。480x300のキャンバスに点の格子の盤を二枚から三枚描き、盤一つは横64列縦7行、点の間隔 PITCH は7pxが基本です。ライブラリもWebGLもCDNも使わず、九つのファイルとも乱数・日付・fetch・外部アドレスの呼び出しが0件なので、場面の時計の経過時間だけで動きます。

点一つの明るさは引数の関数が決めます

board は盤の位置・点の間隔・半径の比・lvAt(c, r) を受け取って点を塗り、点いた点の明るさは lvAt が返す0から1の値一つだけから出ます。暗い下地の点を先に塗り、明るさが0.03を超えたら薄いにじみの円と点を重ね、明るさが0.85を超える丸い点には小さなハイライトをもう一重足します。この呼び出しは九つの場面を合わせて23回あり、場面ごとに変わるのは lvAt の中の計算だけです。

文字一つは7ビットの整数五つになります

文字の表は英大文字・数字・! . - : % + > *・空白の45文字を、'01110' のような文字列7行で書いたものです。colsOf は文句を列の並びに広げ、列一つは7ビットの整数で0番のビットが一番上の行になり、文字と文字の間に空の列が一つ入って、一番最後の空の列は省きます。

    // 文句を列の並びに広げる — 列一つは7ビットの整数(0番のビットが一番上の行)。文字の間の空の列も並びに入り、最後の空の列は省く
    function colsOf(s) {
        const out = [];
        for (let i = 0; i < s.length; i += 1) {
            const rows = glyphRows(s.charAt(i));
            for (let c = 0; c < GLYPH_W; c += 1) {
                let v = 0;
                for (let r = 0; r < GLYPH_H; r += 1) {
                    if (rows[r].charAt(c) === '1') {
                        v |= 1 << r;
                    }
                }
                out.push(v);
            }
            if (i < s.length - 1) {
                out.push(0);
            }
        }
        return out;
    }

時間表だけで動く自動デモ

場面は入力なしで時間表だけで動きます。最初の盤は START_S 秒ぶん先から始まって、すでに文字が流れている途中の姿で開き、LAP_S が過ぎると最初からくり返します。一フレームで進める時間は DT_MAX の0.05秒を超えないように切ってあります。

電光掲示板 9種 — 5x7のドット文字・途切れない列スクロール・消えた点の残光・行を押し上げる切り替え・明るさの段階・点の間隔と半径・幅を測って止めるか流すか・列の位置ごとの色の帯・行の走査駆動

九つの場面がこの順に並ぶ理由と描画で測った値

01はどの場面も頼る文字の表なので先頭に置き、02から05までは時間とともに点の灯り方が変わる仕組み、06から08までは点の形・盤の幅・色を決める盤単位の判断です。09は本物の電光掲示板がなぜ一枚に見えるのかを解くので最後に置きます。

場面 累積面積 動いたフレーム 強さの平均
01 5x7の文字 14.1% 22/23 73.8
02 列スクロール 37.4% 23/23 105.8
03 残光 24.0% 23/23 80.8
04 行の押し上げ 20.4% 7/23 104.6
05 明るさの段階 9.7% 12/23 43.0
06 点の間隔 28.3% 23/23 110.8
07 幅の測定 12.0% 23/23 118.7
08 色の帯 28.1% 23/23 114.5
09 行の走査 19.9% 23/23 58.2

描画で測った面積は02の37.4%がもっとも広く、05の9.7%がもっとも狭く、強さの平均は07の118.7が最高で05の43.0が最低です。04は一区切り2.16秒のうち1.6秒が停止なので動いたフレームが7/23にとどまり、05は明るくなる区間と消える区間の間に止まった区間が挟まって12/23です。

015x7のドット文字を格子に打つ

フォントファイルなしで短い文句を点で描く、いちばん小さな方法です。OPEN 24H を一文字1.0秒ずつ打ち、一文字の中では前の84%を使って表35マスを列の順に読み、左の盤は四角い点、右の盤は丸い点で同じ表を描きます。lvOf は文字の中の列位置が GLYPH_W 以上なら文字の間の空の列とみなして0を返します。

5x7のドット文字を格子に打つ
新しいタブで開く

01 · 5x7のドット文字を格子に打つ — 計測 累積面積 14.124% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GLYPH_WGLYPH_HglyphRows
    function lvOf(c, r, first, idx, n) {
        const s = Math.floor(c / ADV);
        const cc = c % ADV;
        if (cc >= GLYPH_W) {
            return 0;
        }
        const k = first + s;
        if (k > idx) {
            return 0;
        }
        if (glyphRows(MSG.charAt(k))[r].charAt(cc) !== '1') {
            return 0;
        }
        if (k < idx) {
            return 1;
        }
        const cell = cc * GLYPH_H + r;
        return cell < n ? (cell === n - 1 ? 1 : 0.9) : 0;
    }

02列単位で途切れなく流れるスクロール

案内や告知の一行が止まらずに流れる、電光掲示板の基本の動きです。開始の列番号を毎秒 SCROLL_SPEED の24列ずつ進め、Math.floor で整数だけを使うので点がにじまず、SALE 30% OFF TODAY の107列のあとに GAP_COLS の13列の空きを足して一周を120列にします。終わりから始まりへ移る場所には空の列が連続13本入り、空きを0にすると連続0本になって、終わりの文字Yと最初の文字Sがくっつきます。

列単位で途切れなく流れるスクロール
新しいタブで開く

02 · 列単位で途切れなく流れるスクロール — 計測 累積面積 37.431% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCROLL_SPEEDGAP_COLScolAt
    // 一周の列数 — 文句の列のあとにgap個の空の列
    function loopLen(gap) { return cols.length + gap; }
    // 開始の列番号iで見えるマスcの灯り — 循環する並びから読む
    function colAt(i, c, r, gap) { return loopBit(cols, gap, i + c - LEAD, r); }
    function headCol() { return Math.floor(clock() * SCROLL_SPEED); }
    // 文句の最初の列が見えるマス番号 — 見える範囲の外なら-1
    function seamCol(gap) {
        const n = loopLen(gap);
        const c = (((LEAD - headCol()) % n) + n) % n;
        return c < VIEW_COLS ? c : -1;
    }

03消えた点に残る残光

目で見たとき滑らかにつながって見える、残像の処理です。点ごとに明るさを別に持ち、点くべきならすぐ1に上げ、消えたら前の明るさに減衰を掛けて減らします。上の盤は TICK_S の0.05秒で DECAY の0.55倍になるよう経過時間に合わせて掛け、中の盤は144 Hz画面のフレームごとに0.55を掛けるので、一列過ぎたあとに残る明るさは時間基準で0.608、フレーム基準で0.028になり、明るさ0.1を超える尾が4列対0列に分かれます。

消えた点に残る残光
新しいタブで開く

03 · 消えた点に残る残光 — 計測 累積面積 24.035% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DECAYlevelfadeStep
    // 先頭から後ろへk列離れた点に残る明るさ — 時間基準とフレーム基準
    function tailT(k) { return Math.pow(DECAY, k / SCROLL_SPEED / TICK_S); }
    function tailF(k) { return Math.pow(DECAY, k / SCROLL_SPEED * FRAME_HZ); }
    function tailLen(f) {
        let n = 0;
        while (n < 60 && f(n + 1) > 0.1) {
            n += 1;
        }
        return n;
    }

04文が替わるとき行を押し上げる切り替え

複数の文句を順に見せる案内板や待ち番号の表示で使う、入れ替えの方法です。古い文句の7行と空の一行、新しい文句の7行をつないで15行の山を立て、7行の窓を ROLL_STEP の0.07秒ごとに一行ずつ上げて8行ぶん押すので、整数の行だから点がにじまず、終わるとちょうど新しい文句の位置で止まります。中の盤は swapAt で一度にカットで替わり、下の盤は小数の行で押すので点がにじみます。

文が替わるとき行を押し上げる切り替え
新しいタブで開く

04 · 文が替わるとき行を押し上げる切り替え — 計測 累積面積 20.402% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROLL_STEProwOffsetswapAt
    // 押し上げが始まってから何行上がったか — 0からROLL_ROWSまでの整数
    function rowOffset(u) {
        return clamp(Math.floor((u - HOLD_S) / ROLL_STEP), 0, ROLL_ROWS);
    }
    // カットでの入れ替え — 押し上げの区間のちょうど真ん中で一度に替わる
    function swapAt(u) { return u >= HOLD_S + (ROLL_ROWS / 2) * ROLL_STEP; }
    // 山のs番の行のc番の列 — 0から6は古い文句、7は空の行、8から14は新しい文句
    function stackBit(pi, s, c) {
        if (s === GLYPH_H || s < 0 || s > GLYPH_H + GLYPH_H) {
            return 0;
        }
        const old = s < GLYPH_H;
        const cols = PH_COLS[old ? pi : (pi + 1) % PHRASES.length];
        const start = Math.floor((VIEW_COLS - cols.length) / 2);
        return bitOf(cols, c - start, old ? s : s - GLYPH_H - 1);
    }

05明るさを数段階に分けて点け消しする

安い部品のような粗い明るさと滑らかな明るさのどちらを選ぶかを決める基準です。文句が左から RAMP_W の22列幅のにじみの帯で明るくなって同じ順に消え、点ごとに求めた連続の明るさを quantize が stepOf で一番近い段階に丸めます。同じフェードを2段階・LEVELS の4段階・16段階で並べると、段階が少ないほどにじみの帯が階段のように途切れて見えます。

明るさを数段階に分けて点け消しする
新しいタブで開く

05 · 明るさを数段階に分けて点け消しする — 計測 累積面積 9.708% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LEVELSquantizestepOf
    // 何番目の段階か — 0からn - 1
    function stepOf(v, n) { return Math.round(clamp(v, 0, 1) * (n - 1)); }
    // 一番近い段階の明るさに — n段階なら0、1/(n-1)、2/(n-1)、そして最後が1
    function quantize(v, n) { return n <= 1 ? 0 : stepOf(v, n) / (n - 1); }
    // 列cの連続の明るさ — 入ってくるにじみの帯から出ていくにじみの帯を引いた値
    function fadeAt(c) {
        const xin = lerp(0, VIEW_COLS + RAMP_W, clamp((sceneT - IN_T0) / (IN_T1 - IN_T0), 0, 1));
        const xout = lerp(0, VIEW_COLS + RAMP_W, clamp((sceneT - OUT_T0) / (OUT_T1 - OUT_T0), 0, 1));
        return clamp((xin - c) / RAMP_W, 0, 1) - clamp((xout - c) / RAMP_W, 0, 1);
    }
    function lv(c, r, n) {
        return bitOf(cols, c - START_COL, r) === 1 ? quantize(fadeAt(c), n) : 0;
    }

06点の間隔と半径の比

画面の大きさが違っても点の感じを保つ、格子の設定です。点の中心の間隔 PITCH の7pxに対する半径の比 DOT_FILL は大きさのない値なので、0.4なら半径2.8pxで点の間の隙間が1.4px、細い点の0.2なら半径1.4pxで隙間4.2pxになります。下の盤は比が0.18から0.5まで上下して、0.5では隙間が0になって隣の点が触れ合い、softEdge は比が大きいほど縁の輪の倍率を下げます。

点の間隔と半径の比
新しいタブで開く

06 · 点の間隔と半径の比 — 計測 累積面積 28.342% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PITCHDOT_FILLsoftEdge
    // 縁をほぐす — 半径の比が大きいほど隣と重なるので輪の倍率を下げる。0ならきっちりした点
    function softEdge(fill) { return clamp(0.5 / fill * 0.92, 1.05, 1.9); }
    // 上下する盤の今の比 — 一周期でFILL_LOからFILL_HIへ行って戻る
    function fillNow() { return lerp(FILL_LO, FILL_HI, 0.5 - 0.5 * Math.cos(TAU * clock() / FILL_PERIOD)); }
    function lvAt(c, r) {
        return loopBit(cols, GAP_COLS, Math.floor(clock() * SCROLL_SPEED) + c - LEAD, r);
    }
    function gapPx(fill) { return PITCH - 2 * PITCH * fill; }

07幅を測って止めるか流すかを決める

文句の長さが毎回違う掲示板で、読みやすい方を自動で選ぶ決まりです。colCount が数えた列数が VIEW_COLS の64より長ければ流れ、そうでなければ盤の中央に止まり、短い OPEN 24H は47列なので両脇に8列と9列が残ります。盤は6.5秒ごとに短い文句と209列の長い文句 WELCOME TO THE DOT MATRIX SIGN 5X7 を入れ替えてかけ、長い文句のほうが流れます。

幅を測って止めるか流すかを決める
新しいタブで開く

07 · 幅を測って止めるか流すかを決める — 計測 累積面積 12.04% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

VIEW_COLScolCountholdOrScroll
    // 列数を数える関数一つが二つの分岐を決める — 盤のマス数より長ければ流れ、そうでなければ止まる
    function colCount(cols) { return cols.length; }
    function holdOrScroll(cols) { return colCount(cols) > VIEW_COLS ? 'scroll' : 'hold'; }
    // 短い文句は循環させず盤の中央に一度だけ置き、長い文句は循環する並びから読む
    function lvAt(c, r) {
        if (curIdx() === 0) {
            return bitOf(SHORT_COLS, c - Math.floor((VIEW_COLS - SHORT_COLS.length) / 2), r);
        }
        return loopBit(LONG_COLS, GAP_COLS, headCol() + c - LEAD, r);
    }

08列の位置で色が変わる点

速さや状態を色で知らせる案内板や待ち表示板で使う方式です。色は文字ではなく盤の列番号に付くので、文句が流れて通り過ぎると同じ文字が赤から琥珀、黄緑へと変わって読め、colorAt が三つの色の段階 RAMP_STOPS の間を lerpRgb で線形補間するので途切れません。中の盤の zoneAt は補間せずに三つの区域で切り、下の帯は sampleCol で選んだ一列の rgb を5秒周期でなぞって読みます。

列の位置で色が変わる点
新しいタブで開く

08 · 列の位置で色が変わる点 — 計測 累積面積 28.085% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RAMP_STOPScolorAtlerpRgb
    // 二つの色をkの割合で混ぜる — 色の三つのチャンネルを別々に線形で
    function lerpRgb(a, b, k) { return [lerp(a[0], b[0], k), lerp(a[1], b[1], k), lerp(a[2], b[2], k)]; }
    // 盤の中の位置u(0から1)の色 — 二つの段階の間では補間する
    function colorAt(u) {
        const x = clamp(u, 0, 1);
        for (let i = 1; i < RAMP_STOPS.length; i += 1) {
            if (x <= RAMP_STOPS[i].at) {
                const a = RAMP_STOPS[i - 1];
                const b = RAMP_STOPS[i];
                return lerpRgb(a.c, b.c, (x - a.at) / (b.at - a.at));
            }
        }
        return RAMP_STOPS[RAMP_STOPS.length - 1].c;
    }
    // 補間せずに三つの区域で切る — 段階の色そのまま
    function zoneAt(u) {
        const i = Math.min(RAMP_STOPS.length - 1, Math.floor(clamp(u, 0, 0.999) * RAMP_STOPS.length));
        return RAMP_STOPS[i].c;
    }

09行を順に点ける走査駆動

安い駆動方式がなぜ一枚に見えるのかを、目で確かめる場面です。盤は一度に一行だけを7倍の強さで点け、SCAN_HZ は七行が毎秒一周する回数で、目の残像は EYE_S の0.09秒の指数平均で真似ています。60周なら一行の揺れが16%なので一枚に合わさって見え、SCAN_LOW の10は94%で行ごとにちらつき、下の盤は SLOW_HZ の1.5で点いた行が上から下へなぞる様子を見せます。

行を順に点ける走査駆動
新しいタブで開く

09 · 行を順に点ける走査駆動 — 計測 累積面積 19.942% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCAN_HZrowIndexpersist
    // 一拍 — 一行だけを7倍の強さで点けて目の平均に混ぜる。平均の明るさは一枚で点けたものと同じ
    function stepEye(lv, ncols, row, t, h) {
        const e = Math.exp(-h / EYE_S);
        for (let c = 0; c < ncols; c += 1) {
            for (let r = 0; r < GLYPH_H; r += 1) {
                const i = c * GLYPH_H + r;
                const on = r === row ? 7 * targetOf(t, c, r) : 0;
                lv[i] = lv[i] * e + on * (1 - e);
            }
        }
    }

電光掲示板をコードで描くと、文一つはどんな値いくつで点の動きになるのでしょうか?

文一つは列の並び一つと開始の列番号一つで決まり、場面ごとにその点の明るさを直す値が一つずつ足されます。だから九つの場面が colsOf を共有し、loopBit は02・03・06・07・08・09が、bitOf は04・05・07・09が使います。

点一つが持つ値とその役目

値 作られる場所 役目
cols colsOf 文句を7ビットの整数の列の並びに広げたもの
SCROLL_SPEED 02・03・07は24、06は13、08は12、09は9 開始の列番号が毎秒上がる列数
GAP_COLS 13 一周を作るために文句の後ろへつなぐ空の列
lvAt(c, r) 場面ごとに違う board に渡す0から1の明るさ
DECAY・LEVELS・SCAN_HZ 03・05・09 残光の掛け率・明るさの段階数・行の走査の速さ

読む向きが逆になる回

数字を一つずつ回す計算は数字ローリングの回が扱っていて、この回は点が集まって文句になる計算にとどまります。そちらは数字の桁を動かし、こちらは文字の表の1のマスから点を灯す点が違います。

文を文字表の列の並びに広げ、開始の列番号を時間に合わせて進め、点ごとに明るさを下げながら塗る — 電光掲示板の一コマが描かれる順番

一コマが通り過ぎる五つの歩み

  1. colsOf が文句を列の整数の並びに広げます。文字一つが列5本で、文字の間に空の列が一本です。
  2. 開始の列番号を時間から求めます。整数の列で進めるときは Math.floor(clock * SCROLL_SPEED) です。
  3. 盤のマス (c, r) ごとに loopBit(cols, gap, start + c - LEAD, r) で灯るべきかを読みます。LEAD の4は、文句の最初の列を盤の四番目のマスから始めさせます。
  4. 03・05・09は点ごとに明るさを別に持ち、掛けるか、段階に切るか、目の平均に混ぜます。
  5. board が下地の点・にじみ・点・ハイライトの順に塗ります。

電光掲示板 文字 流れる動きを作りたい人は、colsOf の一関数から読むと、残りがこの並びの上に乗っていることが見えてきます。

開始の列番号が一周する循環の並び

文句の列のあとに空の列を gap 個つないで一周を作り、番号は一周の長さで余りを取るので、開始の列番号がどれだけ大きくなっても同じ文句が続けて戻ってきます。

    // 循環する並び — 文句の列のあとにgap個の空の列をつないで一周を作る。番号は一周の長さで余りを取る
    function loopBit(cols, gap, c, r) {
        const n = cols.length + gap;
        const k = ((c % n) + n) % n;
        return k < cols.length ? (cols[k] >> r) & 1 : 0;
    }

電光掲示板のシミュレーターのように実際の画面に付けるとき — ライブラリなしで作る・文句を配列で書く方法・文句が替わる時点

ライブラリなしで一つのキャンバスに付ける

index.html 一つの中にキャンバス・一時停止ボタン・状態の行があり、点は roundRect・arc・fillRect で直接塗ります。電光掲示板 サイトのように店の案内や告知の帯を載せたい人が、場面の時計と board の二つの部分だけを切り出して持っていけるコードです。

文句を配列で書き、幅を測って方式を選ぶ

04は PHRASES の配列に OPEN 24H・SALE 30%・NEW MENU・THANK YOU の四つの文句を書いておき、一区切りごとに一つずつ回します。文句が増えても減っても LAP_S が PERIOD と文句の数の積で決まるので、くり返しの長さがついてきます。長さがばらばらの文句は07の holdOrScroll 一つで、止めるか流すかを分けます。

文句が替わる時点を一か所に集める

04の slot は文句の番号とその区切りの中の経過時間を一つの関数で求め、状態の文は押し上げた行が ROLL_ROWS に届く瞬間に次の文句の名前へ替わります。待ち番号の表示のように番号が替わる場所は、この構造に番号だけを差し替えればよく、文句が替わる時刻を場面の時計一か所だけで決めておくと、盤と読み上げの文がずれません。

電光掲示板が壊れる瞬間 — 小数の列で動かして点がにじむ落とし穴・文の終わりと始まりがくっついて読めない継ぎ目・フレームの間隔に頼って残光が遅くなること

小数の列で動かすと点がにじむ

02の中の盤は同じ速さを小数の列で動かします。開始の列30.5では点いた点203個のうち159個が中間の明るさになる一方で、整数の列で進めると点いた点120個が完全に点いて中間の明るさは0個です。

終わりと始まりがくっつくと読めない

空きが13列なら一周のつなぎ目に空の列が連続13本あり、0なら0本です。文句の中の単語の間の空の列は7本なので、空きを単語の間くらいまで縮めても、文の終わりと始まりが一つの単語のように読めてしまうことがあります。

フレームの間隔に頼った残光は画面によって変わる

03の真ん中の盤はフレームごとに0.55を掛けるので、144 Hz画面では一列後に残る明るさが0.028まで落ちて尾が0列になり、経過時間で掛けた側は同じ画面でも4列が残ります。減衰をフレームに結びつけると、リフレッシュレートの違う機器で尾の長さがばらばらになります。

幅の合わない文句を一方の方式に固定すると

長い文句の209列を止めておくと盤の64列しか見えず31%で、点いた点421個のうち139個(33%)しか見えずに145列が隠れます。逆に短い文句の47列を流すと7秒のあいだ盤の列の41%が空いていて、止めれば27%(両脇の8列と9列)です。

文字の表にない文字は空白になる

glyphRows は表にない文字を空白の表に置き換えます。表は英大文字・数字・記号7種と空白の45文字なので、小文字とハングルは空の列で出るため、文句を大文字で書くか、表に文字を足す必要があります。

九つの場面の完成ソースは下のzipにまとめてあり、展開するときに 7hnz4qjz を入力すると開きます。

スクロールの速さと残光の減衰と走査の速さを変えると、感じはどれほど変わるのでしょうか — SCROLL_SPEED・DECAY・SCAN_HZ を変えた違い

名前 変えた値 感じ
スクロールを速くする 02 SCROLL_SPEED 24 → 60 一周が5.00秒から2.00秒に縮み、文字一つ(6列)が通り過ぎる時間が250msから100msになって読む前に過ぎます。下げると文字はくっきりしますが、長い文句は一周が長く退屈です
残光を長くする 03 DECAY 0.55 → 0.9 時間基準の尾が4列から26列に伸びて文字四つ分ほど引きずり、文字の形がにじんで読みにくくなります。下げるとくっきりしますがちらつきが増えます
走査を半分にする 09 SCAN_HZ 60 → 30 一行の揺れ幅が16%から32%へ倍になりますが、盤全体の明るさの揺れは2.1%から4.2%にとどまり、一枚に読めます

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

02の速さを上げると、文句が盤を通り過ぎる時間が2.67秒から1.07秒に縮み、長い案内文は一度通る間に読めません。03の DECAY はフレーム基準で見ると、同じ0.9でも尾が0列から3列に伸びるだけなので、値一つが二つの方式で違う大きさに読まれます。

走査の速さには下げられる限界がある

SCAN_HZ を12に下げると一行の揺れが79%で盤全体も10.3%揺れ、SCAN_LOW の10では94%と12.3%です。盤全体が明滅して見え、光の点滅に敏感な人には発作を誘発するおそれがあるため、変形としてお勧めする一番低い値は30です。10前後まで下げるのは避けてください。

動きを減らす設定では止まった一行の電光掲示板 — 場面名と今見えている文句を文で読み上げるアクセシビリティ

動きを減らす設定なら一枚だけ描く

prefers-reduced-motion が有効だと requestAnimationFrame のループを始めず、場面の時計を REST_S にして一枚だけ描きます。止まった場面の時刻は01が5.2秒、02が0秒、03が3.1秒、04が0.8秒、05が3.3秒、06が2.0秒、07が0.6秒、08が2.4秒、09が2.6秒の地点で、ページを開いたまま設定を変えても change リスナーが読み直します。

キーボードで止めて最初から見直す

舞台は role="application" で tabindex が0なので、フォーカスを受けるとEnter・Space・Pが一時停止、Nが場面の時計を0.4秒ぶん一歩進め、Rが最初からやり直しです。ボタンは一時停止(aria-pressed)と一歩の二つで、舞台を押しても一時停止が切り替わります。

今見えている文句を文で知らせる

キャンバスは role="img" で aria-label があり、九つの項目とも説明に表示の文句をそのまま書きます。画面の状態の行は statusText が開始の列・読んだマス数のような値を入れて変わり続けますが、読み上げ機に届く文は別に置いた speakText で、表示の文句と盤の名前(04・07は今の文句と押し上げ・流れの方式)だけを含みます。状態の行自体は aria-live が off のままで、pushSpeak が文の変わったときだけ role="status" の段落に書くので、デモ中は一つの盤が終わる0.6秒前に一度、人が触ったあとは文句かモードが変わったときだけ読まれ、値が流れ続けても読み上げが溢れません。

隠れたタブでは止まる

タブが隠れると stopLoop でループを止め、戻ると lastTs を空にして最初のフレームの間隔を捨てるので、隠れていた数秒が一フレームに押し寄せません。

電光掲示板のよくある質問 — ライブラリなしで作る・ドット文字はどこで手に入れるか・ウェブに載せるときに必要なこと

電光掲示板のプログラムはライブラリなしで作れますか

作れます。キャンバス2Dの roundRect・arc・fillRect と requestAnimationFrame だけで九つの場面が動き、外部のスクリプトはありません。

電光掲示板 フォントはどこで手に入りますか

この回はフォントファイルを受け取らず、GL の表に文字一つを '00000' のような7行で直接書きます。文字を足すには同じ形で一マスを書けば glyphRows が探して使い、書いていない文字は空白になります。

ウェブに載せるときに必要なことはありますか

スクリプトが index.html の中に入っていて fetch や外部アドレスの呼び出しがないので、別に用意するサーバー側の作業はありません。キャンバスの倍率は devicePixelRatio を2で切って使うため、倍率3の機器でも画素が2.25倍に増えません。

キャンバスの描画はMDNのCanvas 2D、アニメーションのループはrequestAnimationFrame、タブが隠れる場合はPage Visibility API、動きを減らす設定はprefers-reduced-motionの文書が基準です。流れる相場表示はリアルタイム相場表示の回で続けて見られます。

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

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