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

アスキーアート 9種 — 明るさを文字に置き換える段階表から輪郭の記号まで

アスキーアートは絵をどうやって文字に変えるのでしょうか。01番はセルの平均の明るさから濃さの段階表で文字を選び、05番は明るさが変わる向きに合わせてスラッシュや縦線を選び、06番は動く模様をフレームごとに文字として読み直します。

アスキーアートは文字を描くことではなく、セルごとの明るさを読んで文字ひとつで答える計算です — アスキーアートを作るときは絵を格子に分けるところから始まります

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

九つの場面は同じ骨組みで動きます。場面は画面に貼らない二枚目のキャンバスに描き、画面に打たれる文字は、その画素をセルごとに平均した値からだけ出ます。ライブラリ・WebGL・CDNは使わず乱数もなく、場面の時計から出た位相だけで動くので、LAP_S の10秒がたつと継ぎ目なくくり返されます。

場面は隠れたキャンバスに描き、文字は読んだ値でだけ打ちます

setSrc が元のキャンバスの大きさと倍率を決め、readGrid が getImageData を一度呼んで、セルごとに元の画素の平均のr・g・bと明るさを求めます。明るさは (0.2126 r + 0.7152 g + 0.0722 b) / 255 で、文字盤はセルごとに fillText を呼ばず、行の中で同じ色が続く塊を一度に打ちます。書体は等幅のシステム書体の組で、文字の幅は100pxの書体で M の幅を測って比にしています。

一盤は時間表で流れます

デモは場面の時計0.6秒から始まり、フレームの間隔は0.05秒で切るので、タブが隠れて戻っても一気に飛びません。文字盤はキャンバスに描かれるため、アスキーアート コピペ用の平文はコピーボタンかCキーで行末の空白を除いてクリップボードに入れる方式で、貼り付けるときは等幅の書体にするとセルがそろいます。一盤が終わる0.6秒前に、role="status" の段落へ状態の文が一度だけ書かれます。

場面ごとにのせた計算ひと重ね

属性をひとつ動かす代わりに、計算をひと重ねずつのせました。01は明るさを段階表の番号に、02はセルの縦横比を、03はガンマとコントラストの曲線を、04は規則格子ディザのしきい値を、05は明るさの勾配の向きを、06は正弦波四つの和を、07は拡散の陰影を、08は平均の色とパレットの距離を、09は行ごとにずれた位相を計算します。CSSディザの編はCSSで画素を打ち、ジェネラティブアートの編は線とタイルを描き、落ちる文字の編は文字の物理を扱いますが、この編の主題は絵の画素を読んで文字を選ぶ計算です。

アスキーアート 9種 — 明るさを文字の濃さに移す段階表・文字セルの縦横比の補正・ガンマとコントラストの曲線・二文字だけで描く順序ディザ・明るさの向きを読む輪郭の記号・動くプラズマの文字盤・球の陰影の文字・文字ごとに色をのせるパレット・波打つ文字バナーのヒーロー

九つの場面を描画して得た面積・フレーム・強さ

場面 累積面積 動いたフレーム 強さの平均
01 段階表 18.8% 23/23 78.5
02 セルの比 10.9% 23/23 67.1
03 ガンマ曲線 32.5% 23/23 65.5
04 順序ディザ 13.0% 23/23 103.3
05 向きの記号 15.0% 23/23 75.2
06 プラズマ 39.8% 23/23 60.9
07 球の陰影 15.6% 23/23 73.9
08 色の着色 45.1% 23/23 61.2
09 波のバナー 28.8% 23/23 72.4

08が45.1%でもっとも広く、02が10.9%でもっとも狭く、二文字だけで描く04は面積が13.0%なのに強さの平均が103.3で最大です。九つとも23/23で動いているので、止まった絵として読める場面はありません。

01セルの明るさから文字の濃さの段階表で選んで打つ段階表

画像を文字に変換する画面の基本の仕組みで、readGrid が38×20のセルごとに平均の明るさを求め、glyphIndex がその明るさを段階表の番号に変えて文字をひとつ選びます。左は10段階の段階表、右は rampOf(2) で空白と @ だけを残して中間の明るさが消える対比版です。段階表を5段階にすると一つの場面で平均5種、10段階で10種、16段階で15種の文字が使われます。

セルの明るさから文字の濃さの段階表で選んで打つ段階表
新しいタブで開く

01 · セルの明るさから文字の濃さの段階表で選んで打つ段階表 — 計測 累積面積 18.842% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RAMP_LENreadGridglyphIndex
        const cw = cellW(FONT_PX);
        const ch = cellH(FONT_PX);
        const cols = Math.floor(PW2 / cw);
        const rows = Math.floor(PH / ch);
        const g = readGrid(cols, rows, cw, ch);
        const c1 = [];
        const c2 = [];
        for (let i = 0; i < g.lum.length; i += 1) {
            c1.push(RAMP.charAt(glyphIndex(g.lum[i], RAMP_LEN)));
            c2.push(RAMP_2.charAt(glyphIndex(g.lum[i], 2)));
        }
        lastKinds = kinds(usage(c1, RAMP));
        lastKinds2 = kinds(usage(c2, RAMP_2));
        lastCols = cols;
        lastRows = rows;
        plainText = plainOf(g, c1);

02文字セルが縦に長い比を合わせて円を保つ補正

比率を保ちたいロゴ・顔・円形の図案を文字にするとき、まず引っかかるのが文字セルの形です。セルは横5.9px縦12.0pxで縦が約2.05倍長く、正方形で読むと絵が縦に伸びます。補正した盤は読むセルの縦を GLYPH_RATIO 倍にとって半径40pxの円を80px × 80pxで打ち、右の失敗版は同じ20行で元の上半分だけを読み、同じ円が80px × 約164pxの楕円になります。

文字セルが縦に長い比を合わせて円を保つ補正
新しいタブで開く

02 · 文字セルが縦に長い比を合わせて円を保つ補正 — 計測 累積面積 10.906% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GLYPH_RATIOSAMPLE_HMEASURE_W
        MEASURE_W = cellW(FONT_PX);
        SAMPLE_H = cellH(FONT_PX);
        GLYPH_RATIO = SAMPLE_H / MEASURE_W;
        setSrc(PW2, PH, 1);
        paintScene(PW2, PH, a);
        const cols = Math.floor(PW2 / MEASURE_W);
        const rows = Math.floor(PH / SAMPLE_H);
        // 補正あり: 読むセルの縦を行の高さぶん(横のGLYPH_RATIO倍)にとる。補正なし: 読むセルを正方形(横 = 縦)にとる
        const gA = readGrid(cols, rows, MEASURE_W, SAMPLE_H);
        const gB = readGrid(cols, rows, MEASURE_W, MEASURE_W);
        const cA = [];
        const cB = [];
        for (let i = 0; i < gA.lum.length; i += 1) {
            cA.push(RAMP.charAt(glyphIndex(gA.lum[i], RAMP_LEN)));
            cB.push(RAMP.charAt(glyphIndex(gB.lum[i], RAMP_LEN)));
        }

03ガンマとコントラストの曲線で文字の分布をならす調整

写真の露出が偏っても文字の絵がかたまらないようにする前処理です。curve は明るさを PIVOT 0.72のまわりで CONTRAST 2.6倍に引き伸ばしてから GAMMA 1.2乗で曲げ、0.5から0.95の狭い帯に詰まった霧の場面を段階表全体へ広げます。曲線ありでは平均10種、なしでは4.9種が使われ、もっとも多く使われた文字の割合は21%と42%に分かれます。

ガンマとコントラストの曲線で文字の分布をならす調整
新しいタブで開く

03 · ガンマとコントラストの曲線で文字の分布をならす調整 — 計測 累積面積 32.474% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GAMMACONTRASTPIVOT
        const g = readGrid(cols, rows, cw, ch);
        const cA = [];
        const cB = [];
        for (let i = 0; i < g.lum.length; i += 1) {
            cA.push(RAMP.charAt(glyphIndex(curve(g.lum[i]), RAMP_LEN)));
            cB.push(RAMP.charAt(glyphIndex(g.lum[i], RAMP_LEN)));
        }
        useA = usage(cA, RAMP);
        useB = usage(cB, RAMP);

04二文字だけで明暗を表す順序ディザ

文字の種類を増やせない環境で白黒の文字絵を作るときに、規則格子ディザを使います。セルの明るさを、セルの位置 (r % 4, c % 4) で選んだ ORDER_4 のしきい値 (値 + 0.5) / 16 と比べて @ と . のどちらかだけを打ち、右の盤はしきい値 THRESHOLD 0.5ひとつで切ります。濃い文字 @ の割合は、規則格子が平均45.8%、しきい値ひとつが40.4%です。

二文字だけで明暗を表す順序ディザ
新しいタブで開く

04 · 二文字だけで明暗を表す順序ディザ — 計測 累積面積 13.044% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ORDER_NTHRESHOLDTWO_GLYPH
        for (let r = 0; r < rows; r += 1) {
            for (let c = 0; c < cols; c += 1) {
                const v = g.lum[r * cols + c];
                const t = (ORDER_4[(r % ORDER_N) * ORDER_N + (c % ORDER_N)] + 0.5) / (ORDER_N * ORDER_N);
                if (v > t) {
                    cA.push(TWO_GLYPH.charAt(1));
                    nA += 1;
                }
                else {
                    cA.push(TWO_GLYPH.charAt(0));
                }
                if (v > THRESHOLD) {
                    cB.push(TWO_GLYPH.charAt(1));
                    nB += 1;
                }
                else {
                    cB.push(TWO_GLYPH.charAt(0));
                }
            }
        }

05明るさが変わる向きを読んでスラッシュや縦線を選ぶ輪郭

輪郭がはっきり必要なロゴや図形を文字にするときに使います。gradientAt が左右のセルの明るさの差と上下のセルの明るさの差から明るさの勾配を求め、強さが EDGE_MIN 0.012を超えたセルは接線の角を4区間に分けて、横線・バックスラッシュ・縦線・スラッシュのどれかを打ちます。セル760個のうち境界の記号は平均275セルで、記号4種がすべて出ており、右の盤は明るさの段階表だけを読むので輪郭がにじみます。

明るさが変わる向きを読んでスラッシュや縦線を選ぶ輪郭
新しいタブで開く

05 · 明るさが変わる向きを読んでスラッシュや縦線を選ぶ輪郭 — 計測 累積面積 15.004% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

gradientAtEDGE_MINANGLE_BIN
        for (let r = 0; r < rows; r += 1) {
            for (let c = 0; c < cols; c += 1) {
                const v = g.lum[r * cols + c];
                const gr = gradientAt(g, c, r, ch);
                const mag = Math.hypot(gr[0], gr[1]);
                if (mag > EDGE_MIN) {
                    // 接線 = 勾配の向き + 90°。0から180°へ折りたたんで区間の番号を求める
                    let phi = Math.atan2(gr[1], gr[0]) + Math.PI / 2;
                    phi = ((phi % Math.PI) + Math.PI) % Math.PI;
                    const bin = Math.floor((phi + Math.PI / (2 * ANGLE_BIN)) / (Math.PI / ANGLE_BIN)) % ANGLE_BIN;
                    cA.push(EDGE_SYMBOLS.charAt(bin));
                    seen[bin] = true;
                    edges += 1;
                }
                else {
                    cA.push(FILL_RAMP.charAt(glyphIndex(v, FILL_RAMP.length)));
                }
                cB.push(RAMP.charAt(glyphIndex(v, RAMP.length)));
            }
        }

06動くプラズマをフレームごとに文字として読み直す文字盤

ターミナル風のランディング背景やローディング画面に使います。sineSum が横・縦・斜めの正弦波と、動く中心から広がる輪の正弦波を足した値を半分の解像度の元の絵に書き、readGrid がそれを読み直して79×20セルの12段階の文字にします。12段階の文字は平均で12種すべて使われ、一コマの draw はこのPCのChromeで平均約2.1msかかりました。

動くプラズマをフレームごとに文字として読み直す文字盤
新しいタブで開く

06 · 動くプラズマをフレームごとに文字として読み直す文字盤 — 計測 累積面積 39.769% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

sineSumSINE_TERMSFIELD_SS
    function sineSum(x, y, a) {
        const s1 = Math.sin(x * 0.04 + a);
        const s2 = Math.sin(y * 0.065 - a);
        const s3 = Math.sin((x + y) * 0.028 + 2 * a);
        const s4 = Math.sin(Math.hypot(x - 232 + 120 * Math.cos(a), y - 126 + 60 * Math.sin(a)) * 0.05 - 2 * a);
        return clamp(((s1 + s2 + s3 + s4) / SINE_TERMS) * 0.5 * 1.7 + 0.5, 0, 1);
    }

07球の表面の光と影を文字の濃さに移す陰影

立体感のある文字ロゴやヒーローのオブジェクトに使います。画素ごとに球の上の一点とみなして拡散の陰影を求め、normalDot が法線と光の向きの内積が正のときだけ明るさを与え、縞模様の球は一周に2回自転し、月は mz + nz > zBest のときだけ球の手前に描かれて球の裏へ回ると隠れます。12段階の段階表で平均11.6種が使われ、一コマはこのPCのChromeで平均約1.6msです。

球の表面の光と影を文字の濃さに移す陰影
新しいタブで開く

07 · 球の表面の光と影を文字の濃さに移す陰影 — 計測 累積面積 15.562% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

normalDotLIGHT_ANGLERAMP_SHADE
                if (r2 < SPHERE_R * SPHERE_R) {
                    const nz = Math.sqrt(SPHERE_R * SPHERE_R - r2);
                    const nx = dx / SPHERE_R;
                    const ny = dy / SPHERE_R;
                    const nzn = nz / SPHERE_R;
                    // 自転 — 法線を縦軸のまわりで回して緯度・経度を求め、縞模様をのせる
                    const rx = nx * Math.cos(rot) + nzn * Math.sin(rot);
                    const rz = -nx * Math.sin(rot) + nzn * Math.cos(rot);
                    const lon = Math.atan2(rx, rz);
                    const lat = Math.asin(clamp(ny, -1, 1));
                    const tex = 0.5 + 0.5 * Math.sin(5 * lon + 3 * Math.sin(lat * 3));
                    const alb = 0.42 + 0.58 * smooth(0.35, 0.65, tex);
                    v = 0.05 + 0.95 * normalDot(nx, ny, nzn, lx, ly, lz) * alb;
                    zBest = nz;
                }

08セルの平均の色を文字ごとにのせるパレットの着色

アスキーアート かわいい 雰囲気のポスターやプロフィール画像に、文字絵へ色を加えて使います。chroma でセルの平均の色から明るさを除いた色の向きを作り、nearestColor がパステル六色の PALETTE から最も近い番号を選ぶので、光の塊が重なって混ざった色も六色のなかに落ちます。平均5.6色が使われ、ある時刻にはミント169・ピンク153・バター0・空色341・ラベンダー533・灰白384セルが打たれました。

セルの平均の色を文字ごとにのせるパレットの着色
新しいタブで開く

08 · セルの平均の色を文字ごとにのせるパレットの着色 — 計測 累積面積 45.06% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PALETTE_NnearestColorchroma
    // 色の向き — いちばん大きいチャンネルが255になるよう合わせた色
    function chroma(r, g, b) {
        const m = Math.max(r, g, b, 1);
        return [r * 255 / m, g * 255 / m, b * 255 / m];
    }
    const PAL_DIR = PALETTE.map(function (c) { return chroma(c[0], c[1], c[2]); });
    function nearestColor(r, g, b) {
        const c = chroma(r, g, b);
        let best = 0;
        let bd = 1e9;
        for (let i = 0; i < PALETTE_N; i += 1) {
            const dr = c[0] - PAL_DIR[i][0];
            const dg = c[1] - PAL_DIR[i][1];
            const db = c[2] - PAL_DIR[i][2];
            const dd = dr * dr + dg * dg + db * db;
            if (dd < bd) {
                bd = dd;
                best = i;
            }
        }
        return best;
    }

09波打つ文字バナーでつくるヒーロー

紹介画面やイベント案内の上部に置く文字バナーに使います。行ごとに位相が ROW_PHASE 0.5ずつずれた正弦波に WAVE_COLS 4セルを掛けて丸めた分だけ横にずらして読むので波打ち、元の絵の両側に PAD_COLS 5セルを足してずれた行が端で切れません。20行の盤で一周 loopCols 68セルが10秒で流れ、driftX は秒あたり6.8セルです。

波打つ文字バナーでつくるヒーロー
新しいタブで開く

09 · 波打つ文字バナーでつくるヒーロー — 計測 累積面積 28.763% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROW_PHASEloopColsdriftX
        const sCols = cols + 2 * PAD_COLS;
        setSrc(sCols * cw, PH, 1);
        paintScene(sCols * cw, PH, ph);
        const g = readGrid(sCols, rows, cw, ch);
        const cA = [];
        for (let r = 0; r < rows; r += 1) {
            // 行ごとに位相がROW_PHASEずつずれた正弦波で横にずらす(セル単位に丸める)
            const off = Math.round(WAVE_COLS * Math.sin(ROW_PHASE * r - a));
            for (let c = 0; c < cols; c += 1) {
                cA.push(RAMP.charAt(glyphIndex(g.lum[r * sCols + c + PAD_COLS + off], RAMP.length)));
            }
        }

アスキーアートの変換をコードで書くと、文字ひとつはいくつの値で決まるのでしょうか?

文字ひとつはセルの平均の明るさひとつと段階表ひとつで決まり、残りの計算はこの明るさを文字の番号に移す前に直す仕事です。そのため九つの場面は glyphIndex の一つの関数を共有し、場面ごとにその前の値だけを変えます。

セルが持つ値としていること

値 作られる場所 すること
lum readGrid セルの元の画素の平均の明るさ、0から1
RAMP_FULL ソースの定数 薄い文字から濃い文字まで16字
glyphIndex 共通の関数 明るさを切り捨てて段階表の番号にする
GLYPH_RATIO 02 文字セルの縦÷横の比、約2.05
curve 03 明るさを引き伸ばして曲げ、番号を均等にする

段階表は RAMP_FULL の16文字で、rampOf(10) はそのうち10個を均等に取り出して、空白と , : - = + o # % @ の順になります。

ターミナル風のランディング背景とローディング場面

06と09は文字盤が流れ続けるので、ターミナル画面のような背景やローディング場面にそのまま重ねられます。06は1行79セルで20行なので横に長い領域に、09は20行がバナーの文字を波にして押すので紹介文の領域に合います。文字盤の上に文を重ねるときは、盤の地 #221e38 の上で基本の文字のコントラストが12.0:1なので、重ねても読めます。

画像を文字に変換する画面

変換画面は、01の段階表と02の比の補正と03の曲線が一本につながったものです。写真を載せるなら比がゆがまないよう先に02を合わせ、露出が片側に偏った写真は03の曲線を通して文字を選び、04で二文字だけにするか05で輪郭だけを別に打つかの選択肢を置けばよいです。

絵をセルに分け、セルの平均の明るさを求め、曲線で明るさを直し、段階表から文字をひとつ選ぶまで — アスキーアートが一コマ計算される順番です

一コマが通る五つの歩み

  1. 場面を元のキャンバスに描きます。06・07は画素を直接書き、半分の解像度(FIELD_SS 0.5)で計算します。
  2. cellW・cellH で文字セルの大きさを測ってからセルの数を決めます。盤ひとつは79×20、盤ふたつは38×20セルです。
  3. readGrid がセルごとに平均の明るさと平均の色を読みます。
  4. 03の曲線、04のしきい値、05の勾配のように、場面がのせた計算を通します。
  5. glyphIndex で文字を選び、drawGlyphs が行ごとに同じ色の塊を一度に打ちます。

盤がふたつの場面は左右を見比べる

01から05は左の盤と右の対比版が並び、違いは文字を選ぶ方法かセルを読む形にあります。二つの盤の差がその場面がのせた計算の効果なので、値を変えて試すときも一画面で比べられます。

アスキーアートが壊れるのは、文字セルが縦に長くて絵が縦に伸びて見えるとき・明るさの曲線がなく文字が片側に偏るとき・セルが大きすぎて輪郭がにじむときです

正方形のセルで読むと縦に伸びる絵

02の右の盤がその例です。文字セルひとつが5.9px × 12.0pxなので、正方形のセルで読むと同じ20行が元の上半分しか覆わず、円は幅80pxに縦が約164pxに伸びます。

曲線なしで読むと数文字しか残らない

03の曲線なしの盤は明るさが狭い帯に詰まり、文字は平均4.9種しか使われず、もっとも多く使われた文字ひとつがセルの42%を占めます。曲線を通すと同じ場面が平均10種に広がり、その割合は21%に下がります。

セルが大きいと輪郭の記号を打つセルの数が少ない

05のセルの高さ12pxでは、セル760個のうち境界の記号は平均275セルです。6pxに縮めるとセル3,157個のうち645セルが境界の記号になり、同じ図形の線がよりはっきりつながります。

この編のコードをファイルで持ち帰るには、下のzipをダウンロードしてamzukyh6で開けば、九つの場面の完成ソースが入っています。

セルの大きさと段階の数とガンマを変えると形と読みやすさはどれだけ変わるのでしょうか — セルの大きさ・段階の数・ガンマを変えて比べます

名前 変えた値 感じ
段階をもっと細かく 01 RAMP_LEN 10 → 16 同じ場面で使われる文字が平均10種から15種に増えて陰影が細かく分かれますが、となり合う段階の濃さの差が小さく、小さなセルでは見分けにくくなります。セルの数は38×20 = 760のままです
ガンマをもっと強く 03 GAMMA 1.2 → 2.2 曲線を通したセルのうち最も暗い五文字が占める割合が平均55%から77%に増えて、中間の明るさが暗い文字へ下がり、影が濃くなります
セルをもっと小さく 05 CELL_PX 12 → 6 セルが760個から3,157個へ約4倍になり、境界の記号のセルも平均275個から645個に増えて線がはっきりしますが、一コマの draw はこのPCのChromeで平均0.64msから1.04msに延びました

値を変える前に見ること

段階表は長いほどよいわけではありません。16段階で使われる文字は15種なので一文字はこの場面で一度も使われず、となりの段階が似て、小さなセルでは区別がぼやけます。

ガンマは0.6で17.7%、1.2で約55%、1.8で70.4%、2.2で約77%と暗い五文字の割合が上がるので、数字だけで盤を選べます。セルを6pxにしても計算時間は4倍ではなく1.6倍ほどの伸びで、この値はこのPCのヘッドレスChromeで20フレームを平均したものなので、機器ごとに違います。

動きを減らす設定では止まった一枚の文字盤です — 場面の説明と状態の行の数値を文で読み上げ、コピーボタンで文字盤をテキストとして持ち出せるアクセシビリティです

キーボードで止めて一歩ずつ見てコピーする

舞台は role="application" で、フォーカスを置くとEnter・Space・Pが一時停止、Nが場面の時計を0.4秒進める一歩、Rが最初から、Cが文字盤のコピーです。ボタンは一時停止(aria-pressed)・一歩・コピーの三つで、舞台を押すと一時停止が切り替わります。コピーは navigator を使わず、一時的な textarea と execCommand('copy') で行い、成功すると状態の行に1.6秒のあいだ知らせます。

文字盤は読み上げに読まれないので数値を文で渡す

キャンバスは role="img" で場面の説明の aria-label を持ちますが、文字盤の文字は絵なので画面読み上げには読まれません。そこで状態の行がセルの数と文字の種類数を文で知らせ、デモのあいだは aria-live="off"、手を触れると polite に変わります。盤の地 #221e38 の上の基本の文字のコントラストは12.0:1、06のもっとも暗い文字の段階は5.4:1、08のパレットは8.8から13.6:1です。

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

prefers-reduced-motion が有効だとループなしで showRest が REST_S 秒の場面を一枚だけ描き、設定が変わると change リスナーが読み直します。タブが隠れるとループを止め、戻ると最初のフレームの間隔を捨てますが、この場合も一時停止・一歩・コピー・Rは画面に反映されます。

    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            stopLoop();
        }
        else if (!paused && !still) {
            startLoop();
        }
    }, { signal: off.signal });
    // 設定が変わったら — 有効になれば止まった一枚、無効になればまた動く
    reduceQuery.addEventListener('change', function () {
        still = reduceQuery.matches;
        if (still) {
            showRest();
        }
        else {
            sceneT = START_S;
            showNow();
            if (!paused) {
                startLoop();
            }
        }
    }, { signal: off.signal });

アスキーアートのよくある質問 — ライブラリなしで作る方法・文字の幅がそろわない書体で崩れる理由・ハングルや絵文字を段階表に入れてよいか

アスキーアートの作り方にライブラリは必要ですか

必要ありません。外部のコードなしでブラウザのキャンバス2Dだけを使い、画素を読む getImageData と文字を打つ fillText の二つの呼び出しが仕事を分けています。

文字の幅がそろわない書体ではなぜ絵が崩れるのですか

セルの格子は文字ひとつの幅が同じという前提で組んであるので、比例書体では行ごとに文字の幅が違って列がずれます。この編は等幅の書体の組を使い、100pxで M の幅を測って比にし、セルの高さは文字の大きさの1.2倍の LINE_K にしています。

ハングルや絵文字を段階表に入れてもよいですか

この編はASCIIの16字だけを使い、ハングルや絵文字は試していません。文字ひとつが英字一つ分のセルより広いと格子がずれ、絵文字は charAt で取り出すと半分だけ出るので、使うなら rampOf を文字単位の配列に変えて幅を測り直す必要があります。

関数の基準はMDNのfillTextの文書、文字の幅はmeasureTextの文書、ループはrequestAnimationFrameの文書、動きを減らす設定はprefers-reduced-motionの文書が基準です。キャンバスで地形を作る値ノイズはキャンバスのノイズ地形の編を続けて見るとよいです。

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

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