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

css dither 9パターン — 動く画面を点の並びで描くディザ処理

css dither は色を二つしか残さないのに、なぜグラデーションがかえってなめらかに見えるのか — 01番の閾値行列と02番の誤差拡散が答えだ。

九つのディザを画面でそのまま動かしてみる

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

css dither 9パターン — 閾値・誤差・法線を計算して点が絵になる

どの場面も、160×100マスの小さな格子に点を一つずつ打ち、その格子を3倍に引き伸ばして舞台に載せています。引き伸ばすときは imageSmoothingEnabled を切り、CSS にも image-rendering: pixelated を指定してあるので、点はにじまず四角のまま残ります。舞台の幅は min(92vw, calc(160vh - 12.8vw)) で、レンダーの撮影ウィンドウでは横が約419pxで写りました。

ディザがやっているのは、明るさを数段階に減らしながら、減らしたせいで出る誤差が一か所に固まって帯にならないよう散らすことです。散らす道は二つあります。一つはマスごとに決めた閾値と明るさを比べる道(01・04・05・06・07・08・09)、もう一つは丸めて余った誤差を隣のマスへ渡す道(02・03)です。九つの場面は、まず二つの道をむき出しで見せ(01・02)、球に載せ(03)、そのあと切り替え・波紋・立体・パレット・文字・ポインター反応へ進む順に並べました。

どこから開くかは使い道で選べば足ります。ローディングの背景なら01、月のような球一枚なら03、ページ遷移なら04、クリックに反応する水のヒーローなら05、カーソルの周りだけ明るくなるファーストビューなら09から開いてください。同じキャンバスのピクセル計算を色のグラデーションで解くとレインボーグラデーション 9パターン — キャンバスで作るプラズマ背景になり、この記事は同じ明るさの場を二色と四色に削る側です。

css dither effect を技法別のコードで見る

下のコードは、場面ごとの index.html の <script> から続きの行をそのまま取り出したもので、一つの断片は25行を超えません。この手の動きも、突き詰めればこの中の二つの道、つまり閾値を比べる計算と誤差を渡す計算の上に立っています。

01ベイヤー閾値行列で動くグラデーションを二色で打つ整列ディザ

点ごとに斜めの正弦縞の明るさへ行列の閾値を足し、1以上なら点灯、未満なら消灯にします。行列は2×2から始めて倍々に育て、点(x、y)は行列の (y % n)·n + (x % n) 番目の値を使います。用途はレトロゲーム風のローディング背景です。測定は累積面積74.8%・平均強度182.8・動いたフレーム23/23でした。

ベイヤー閾値行列で動くグラデーションを二色で打つ整列ディザ
新しいタブで開く

01 · ベイヤー閾値行列で動くグラデーションを二色で打つ整列ディザ — 計測 累積面積 74.844% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BAYER_SIZELEVELSSWEEP_SPEEDTHRESHOLD_BIAS
    // ベイヤー閾値行列 — n は2の冪乗。値は0より大きく1未満に収める
    function makeBayer(n) {
        let size = 2;
        let m = [0, 2, 3, 1];
        const quad = [0, 2, 3, 1];
        while (size < n) {
            const s2 = size * 2;
            const next = [];
            for (let y = 0; y < s2; y += 1) {
                for (let x = 0; x < s2; x += 1) {
                    const q = (y >= size ? 2 : 0) + (x >= size ? 1 : 0);
                    next.push(4 * m[(y % size) * size + (x % size)] + quad[q]);
                }
            }
            m = next;
            size = s2;
        }
        const out = new Float32Array(n * n);
        for (let i = 0; i < out.length; i += 1) {
            out[i] = (m[i] + 0.5) / (n * n);
        }
        return out;
    }

02誤差を隣の画素へ流して正弦プラズマを描く誤差拡散

正弦を四つ重ねた明るさの場で、点ごとに0.5より大きければ点灯とし、丸めて余った誤差を横に7/16、下に5/16、下の後ろ側に3/16、下の前側に1/16と隣へ配ります。奇数行は右から左へ進む SERPENTINE 方式で前側の向きが行ごとに反転し、二色だけで面を埋めるヒーロー背景によく合います。累積面積74.8%・平均強度165.7で、23/23フレームが動きました。

誤差を隣の画素へ流して正弦プラズマを描く誤差拡散
新しいタブで開く

02 · 誤差を隣の画素へ流して正弦プラズマを描く誤差拡散 — 計測 累積面積 74.85% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DIFFUSEFIELD_FREQPHASE_SPEEDSERPENTINE
                    const dir = (SERPENTINE === 1 && (y & 1) === 1) ? -1 : 1;
                    for (let k = 0; k < GW; k += 1) {
                        const x = dir > 0 ? k : GW - 1 - k;
                        const i = y * GW + x;
                        const val = field(x, y, ph, f) + err[i];
                        const on = val > 0.5 ? 1 : 0;
                        buf.px[i] = on === 1 ? light : dark;
                        // Floyd-Steinberg — 横 7/16、下 5/16、下の両脇 3/16 と 1/16
                        const e = (val - on) * carry;
                        const hasNext = x + dir >= 0 && x + dir < GW;
                        const hasPrev = x - dir >= 0 && x - dir < GW;
                        if (hasNext) {
                            err[i + dir] += e * 7 / 16;
                        }
                        if (y + 1 < GH) {
                            if (hasPrev) {
                                err[i + GW - dir] += e * 3 / 16;
                            }
                            err[i + GW] += e * 5 / 16;
                            if (hasNext) {
                                err[i + GW + dir] += e * 1 / 16;
                            }
                        }
                    }

03光が一周して月の満ち欠けを点で作る球体シェーディング

球の表面の法線と光の向きの内積がそのまま明るさになり、その値を Atkinson 方式で打ちます。この方式は六つの隣へ誤差の1/8ずつしか渡さず4分の1を捨てるため、暗い側と明るい側がはっきりした色に固まり、光の位相が点の密度に変わります。累積面積30.4%・平均強度188.2で、動いたフレームは22/23でした。

光が一周して月の満ち欠けを点で作る球体シェーディング
新しいタブで開く

03 · 光が一周して月の満ち欠けを点で作る球体シェーディング — 計測 累積面積 30.397% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIGHT_SPEEDSPHERE_RAMBIENTATKINSON_KEEP
                            const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));
                            // 法線と光の向きの内積がそのまま明るさ
                            v = clamp(Math.max(0, nx * lx + ny * ly + nz * lz) + AMBIENT, 0, 1);
                        }
                        else {
                            v = HALO_GAIN * lit * Math.exp(-(r - SPHERE_R) / HALO_W);
                            if (sx.has(i) && r > SPHERE_R + 4) {
                                v = Math.max(v, 0.9);
                            }
                        }
                        const val = v + err[i];
                        const on = val > 0.5 ? 1 : 0;
                        buf.px[i] = on === 1 ? light : dark;
                        // Atkinson — 六つの隣は誤差の1/8ずつだけ受け取り、残りは捨てる
                        const e = (val - on) * ATKINSON_KEEP;
                        addErr(i + 1, x + 1, y, e);
                        addErr(i + 2, x + 2, y, e);
                        if (y + 1 < GH) {
                            addErr(i + GW - 1, x - 1, y + 1, e);
                            addErr(i + GW, x, y + 1, e);
                            addErr(i + GW + 1, x + 1, y + 1, e);
                        }
                        if (y + 2 < GH) {
                            addErr(i + 2 * GW, x, y + 2, e);
                        }

04画面が点に溶けて次の場面へ移るインターリーブノイズ遷移

点ごとに座標だけで決まる0〜1の擬似乱数の閾値が進行度 p より小さければ縞の場面、そうでなければ輪の場面を描きます。閾値は均等に散っているので p が上がるぶん点がまばらに入れ替わり、ページ遷移やスライド送りに使えます。累積面積は九つで最大の75.3%、平均強度は111.8、23/23でした。

画面が点に溶けて次の場面へ移るインターリーブノイズ遷移
新しいタブで開く

04 · 画面が点に溶けて次の場面へ移るインターリーブノイズ遷移 — 計測 累積面積 75.253% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

NOISE_ANOISE_BFADE_PERIODHOLD_FRAMES
                        // インターリーブ・グラデーションノイズ — 座標だけで決まる0〜1の値
                        const thr = frac(52.9829189 * frac(NOISE_A * x + NOISE_B * y));
                        if (thr < p) {
                            const s = Math.sin((x * 0.7 + y * 0.7) * STRIPE_FREQ + t * 0.1);
                            buf.px[y * GW + x] = mixPack(STRIPE_DARK, STRIPE_LIGHT, smooth(s * 1.5 + 0.5));
                        }
                        else {
                            const s = Math.sin(Math.hypot(x - GW * 0.35, y - GH * 0.55) * RING_FREQ - t * 0.12);
                            buf.px[y * GW + x] = mixPack(ringLo, ringHi, smooth(s * 1.5 + 0.5));
                        }

05波動方程式で広がる波紋を四階調で打つディザの池

高さの場を隣四マスの和と前のフレームで更新して波紋を広げ、高さを四つの階調に分けて8×8のベイヤー行列で打ちます。ポインターを動かすと9ティックごとに水滴が落ち、押すと大きな水滴が落ちるので、クリックに反応する背景に向きます。累積面積63.0%・平均強度84.6で、23/23フレームが動きました。

波動方程式で広がる波紋を四階調で打つディザの池
新しいタブで開く

05 · 波動方程式で広がる波紋を四階調で打つディザの池 — 計測 累積面積 62.954% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DAMPINGWAVE_CLEVELSDROP_EVERY
        // 波動方程式の一歩 — 隣の和と前のフレームから次の高さを求める
        function simulate() {
            for (let y = 1; y < GH - 1; y += 1) {
                for (let x = 1; x < GW - 1; x += 1) {
                    const i = y * GW + x;
                    const lap = h[i - 1] + h[i + 1] + h[i - GW] + h[i + GW] - 4 * h[i];
                    hn[i] = (2 * h[i] - hp[i] + WAVE_C * lap) * DAMPING;
                }
            }
            const old = hp;
            hp = h;
            h = hn;
            hn = old;
        }

06回る立体の面の明るさを点の密度に変える3Dディザキューブ

立方体の面の中心と法線を回し、カメラを向いた面だけを残して、頂点を x·FOCAL/(z+CAM_DIST) で投影します。面ごとに光との角度から明るさを決めてベイヤー閾値と比べ、点で塗ります。凸の立体で見える面どうしが重ならないため奥行きの並べ替えは要らず、ロゴの周りの回転飾りに向きます。測定は累積面積26.4%・平均強度147.3・動いたフレーム23/23です。

回る立体の面の明るさを点の密度に変える3Dディザキューブ
新しいタブで開く

06 · 回る立体の面の明るさを点の密度に変える3Dディザキューブ — 計測 累積面積 26.445% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROT_SPEEDLIGHT_DIRCAM_DISTFACE_SHADE
            const n = rot(f.n, a, b);
            const c = rot([f.n[0] * SIDE, f.n[1] * SIDE, f.n[2] * SIDE], a, b);
            // カメラ (0, 0, -CAM_DIST) を向いた面だけ描く
            const toCam = n[0] * (0 - c[0]) + n[1] * (0 - c[1]) + n[2] * (-CAM_DIST - c[2]);
            if (toCam <= 0) {
                return;
            }
            const signs = [[-1, -1], [1, -1], [1, 1], [-1, 1]];
            const pts = [];
            for (let k = 0; k < 4; k += 1) {
                const s = signs[k];
                const corner = rot([
                    (f.n[0] + s[0] * f.u[0] + s[1] * f.v[0]) * SIDE,
                    (f.n[1] + s[0] * f.u[1] + s[1] * f.v[1]) * SIDE,
                    (f.n[2] + s[0] * f.u[2] + s[1] * f.v[2]) * SIDE
                ], a, b);
                const k2 = FOCAL / (corner[2] + CAM_DIST);
                pts.push([GW / 2 + corner[0] * k2, GH / 2 - corner[1] * k2]);
            }
            const lam = Math.max(0, n[0] * lx + n[1] * ly + n[2] * lz);
            const shade = FACE_SHADE + (1 - FACE_SHADE) * lam;

07四色で空のグラデーションを量子化するパレットディザの夕焼け

空のグラデーションと太陽の光輪、海の照り返しを作ってから、画素ごとにベイヤー値で色を少しずらし、四色のパレットから最も近い色を選びます。ずらす幅が DITHER_AMP なので、色の境目が帯ではなく点でつながって見え、低解像度の雰囲気のランディング背景に使えます。累積面積44.5%・平均強度79.9で、動いたフレームは23/23でした。

四色で空のグラデーションを量子化するパレットディザの夕焼け
新しいタブで開く

07 · 四色で空のグラデーションを量子化するパレットディザの夕焼け — 計測 累積面積 44.511% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DITHER_AMPPALETTE_SIZEHORIZON_YSUN_SPEED
        // 補正した色にいちばん近いパレット色の番号
        function nearest(r, g, b) {
            let best = 0;
            let bestD = 1e9;
            for (let k = 0; k < pal.length; k += 1) {
                const dr = r - pal[k][0];
                const dg = g - pal[k][1];
                const db = b - pal[k][2];
                const d = dr * dr + dg * dg + db * db;
                if (d < bestD) {
                    bestD = d;
                    best = k;
                }
            }
            return best;
        }

08文字が点に散って再び集まるタイポグラフィのディザ

文字を見えないキャンバスに描き、getImageData で一度だけ読んで3×3の平均でぼかした明るさのマスクを作ります。毎フレーム、このマスクを時間で動く閾値と比べて左から点単位で消したり戻したりするので、タイトルやロゴの登場演出に向きます。累積面積21.9%は九つで最小、平均強度は136.9で、23/23フレームで動きが検出されました。

文字が点に散って再び集まるタイポグラフィのディザ
新しいタブで開く

08 · 文字が点に散って再び集まるタイポグラフィのディザ — 計測 累積面積 21.874% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DISSOLVE_SPEEDFONT_PXMASK_BLURBAYER_SIZE
        const mask = new Float32Array(GW * GH);
        const off = document.createElement('canvas');
        off.width = GW;
        off.height = GH;
        const octx = off.getContext('2d');
        if (octx) {
            octx.fillStyle = '#000000';
            octx.fillRect(0, 0, GW, GH);
            octx.fillStyle = '#ffffff';
            octx.font = 'bold ' + FONT_PX + 'px sans-serif';
            octx.textAlign = 'center';
            octx.textBaseline = 'middle';
            octx.fillText(WORD, GW / 2, GH / 2);
            const data = octx.getImageData(0, 0, GW, GH).data;
            const raw = new Float32Array(GW * GH);
            for (let i = 0; i < raw.length; i += 1) {
                raw[i] = data[i * 4] / 255;
            }

09ポインターの周りだけ密になる光輪ディザのヒーロー

ノイズで流れる背景の明るさへ、ポインターとの距離に応じたガウス値を足し、ベイヤー閾値と比べるとカーソルの周りだけ点が詰まります。ポインター座標を LERP で遅れて追うので光輪が滑るように動き、ヒーローの見せ場に向きます。累積面積は50.6%、平均強度は91.9で、23/23フレームが動きました。

ポインターの周りだけ密になる光輪ディザのヒーロー
新しいタブで開く

09 · ポインターの周りだけ密になる光輪ディザのヒーロー — 計測 累積面積 50.6% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HALO_RHALO_GAINNOISE_SCALELERP
                const breathe = 1 + 0.12 * Math.sin(t * 0.07) + 0.6 * smooth(pulse / PULSE_TICKS);
                const sigma = (HALO_R / 1.6) * breathe;
                const inv = 1 / (2 * sigma * sigma);
                for (let y = 0; y < GH; y += 1) {
                    for (let x = 0; x < GW; x += 1) {
                        const nx = x * NOISE_SCALE + t * 0.012;
                        const ny = y * NOISE_SCALE - t * 0.007;
                        const n = vnoise(nx, ny, NOISE_SEED) * 0.65 + vnoise(nx * 2.1 + 7, ny * 2.1 + 3, NOISE_SEED) * 0.35;
                        const dx = x - hx;
                        const dy = y - hy;
                        const halo = Math.exp(-(dx * dx + dy * dy) * inv);
                        const v = 0.05 + 0.5 * n + halo * HALO_GAIN;
                        const i = y * GW + x;
                        buf.px[i] = v > matrix[(y & 7) * 8 + (x & 7)] ? tint[Math.min(31, Math.floor(halo * 31))] : dark;

値が点模様を変えるところ — 行列の大きさ・誤差の割合・波の減衰

値一つで点模様がはっきり変わる場所は三つです。ここに並べる数字はブラウザーのレンダーではなく、ソースの同じ計算をノードの偽の DOM で直接動かして数えたものなので、あとの測定表とは分けて読んでください。

行列を4から8に広げると点の数の種類が17から44になる

01の BAYER_SIZE を4から8に替えて30ティック後の一フレームを見ると、行列の大きさのタイルごとに点灯した数の種類が17通りから44通りに増えます。理論上ありうる種類は17から65に増え、点灯率は0.520から0.523でほぼ同じです。明るさはそのままで目が細かくほぐれ、そのかわり繰り返しの周期が4マスから8マスに広がります。

誤差を渡す割合を下げると点灯が固まる

02の DIFFUSE を1.0から0.75に下げると、同じ横の行で点灯が続く平均の長さが三フレーム平均で1.88マスから2.11マスになります。0.5では2.67マス、0にすると36.3マスの帯に固まります。点灯率は0.529から0.533で、ほとんど動きません。

減衰を上げると大きな波が長く残る

05の DAMPING を0.985から0.995に上げると、いちばん暗い段階かいちばん明るい段階で打たれた画素の割合が、8フレーム平均で6.0%から27.1%になります。波の山と谷が消えずに積み重なるということです。

九つの場面を実際にレンダーして測った値は次の表のとおりです。最も広く動いたのは04の75.3%、最も狭いのは08の21.9%で、03だけが動いたフレームが22/23と一フレーム欠けました。その理由は確かめていません。

場面 累積面積 平均強度 動いたフレーム
01 整列ディザ 74.8% 182.8 23/23
02 誤差拡散 74.8% 165.7 23/23
03 球体シェーディング 30.4% 188.2 22/23
04 ノイズ遷移 75.3% 111.8 23/23
05 ディザの池 63.0% 84.6 23/23
06 3Dキューブ 26.4% 147.3 23/23
07 パレットの夕焼け 44.5% 79.9 23/23
08 タイポグラフィ 21.9% 136.9 23/23
09 光輪 50.6% 91.9 23/23

どこで壊れるか — 解像度が高いとき・狭い画面・動きを減らす設定

解像度が高くても点の格子は160×100のまま

画面の解像度を上げても計算するマスは160×100で変わらず、キャンバスの内側の解像度だけが devicePixelRatio に従い、最大2で頭打ちになります。そのかわり09は、マスごとに vnoise を二回と指数関数を一回、16000マスぶん回すので、ほかの場面より一フレームの計算が重くなります。格子を大きくすると計算がその分増えますが、大きくした値では測っていません。

誤差拡散は順番に進むので並列に分けられない

02は一つの点の結果が前の点の渡した誤差に左右されるため、マスを同時に処理できません。フレームごとに err.fill(0) で空にしてから16000マスを頭から回し直します。フレーム間で誤差を引き継がないので、明るさの場が動くと点の配置がまるごと入れ替わります。格子を小さく取って3倍に引き伸ばすのは、このためです。

Atkinson は誤差の4分の1を捨てる

03で光が正面にあるとき点灯した点は3975個、31ティック後は1251個、63ティック後に位相が反対側に届くと15個です。月がほとんど欠けた位相でも数点が残るのは、球の外に置いた背景の星26個のうち球から遠いものが点灯するからです。誤差を4分の1捨てる代わりに、暗い側と明るい側が純粋な色でよく固まるのが、この方式の特徴です。

遷移の閾値には時間の項がない

04の閾値は座標しか使わないので、同じ p なら選ばれる点はいつも同じです。thr < p の点の割合は、pを0.25・0.5・0.75にすると0.250・0.500・0.750でpと一致し、縞の側に選ばれた点が横に続く最大の長さは、pが0.25で1マス、0.5で2マス、0.75で6マスです。0.75でも6マスにとどまるので、固まらず散ります。

波の係数を上げると計算が発散する

05の WAVE_C を0.4から0.6に上げると、同じ8フレームの測定で極端な段階の割合が0.929になり、階調の種類が5に増えて、四段階の外の値が混ざります。端のマスは計算せず0のままにしてあり、減衰は一ティックに二回掛かるので、実際のティックあたりの減衰は DAMPING の二乗です。

キューブの定数を大きくすると格子の外で切れる

06の以前の定数(FOCAL 125・CAM_DIST 3.8・SIDE 0.85)では、縁の画素の外接枠が98×97で格子の上端に触れ、切れていました。いまの値(FOCAL 112・CAM_DIST 4.2・SIDE 0.8)は71×75で、125ティックのあいだ格子の中に収まります。toCam が0以下になった面は描画を飛ばすので、見える面は一度に最大三つです。06は makeBayer(8) と makeBayer(4) の二つの行列を使い分けます。

ディザの振れ幅が0だと境目が帯に固まる

07で縦に隣り合う二つの画素の色が違う回数は、振れ幅0.4のとき10921で、DITHER_AMP を0にすると1523です。およそ7分の1に減るので、ディザがなぜ要るのかを示す数字です。パレットが四色なので、画素ごとの距離計算も四回走ります。

文字の幅は端末のフォントが決める

08は bold 38px sans-serif で文字を描き、measureText では幅を測りません。端末ごとに既定のフォントが違えば文字の幅が変わり、格子の横160を超えると両端が切れるかもしれませんが、ほかのフォントでは動かしていません。キャンバスが取れないとマスが0になり、文字は出ません。

狭い画面では文字だけ小さくなり比率は変わらない

幅340px以下ではキャプション・ボタン・状態の文字が10pxに縮み、キャンバスの比率480:300は保たれます。舞台の幅がビューポートに比例するため、スマホでは点一つがそのぶん小さく見えます。

値を書き替えて境目を確かめたいときは、圧縮ファイルを開く必要があります。開くパスワードは ee8ur62p で、中には九つの場面の index.html・style.scss とリアクト版が入っています。

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

名前 値 感じ
行列の大きさを変える 01 BAYER_SIZE 4 → 8 タイルごとの点灯数の種類が17通りから44通りに増えて目がほぐれ、点灯率は0.520から0.523でほぼ同じです。
誤差の渡す割合を変える 02 DIFFUSE 1.0 → 0.75 横に続く点灯の平均が1.88マスから2.11マスになり、点が少し固まります。
波の減衰を変える 05 DAMPING 0.985 → 0.995 極端な段階の画素が平均6.0%から27.1%に増え、波がいつまでも残ります。

三つの数字は、上の値の節と同じくノードでソースを動かして数えた値です。

操作は九つの場面が同じ枠を使います。場面ごとに <button> が一つあり、キャンバスは tabindex="0" なので Tab で届きます。Enter と Space は場面の押す動作、方向キーは仮想ポインターを14単位ずつ動かします。preventDefault はこの六つのキーでしか呼ばれず、Tab を含むほかのキーはそのまま通ります。状態の文は aria-live="polite" の <p> に入り、文が変わったときだけ書き替えます。

prefers-reduced-motion がオンのときは、九つの場面のどれも requestAnimationFrame のループを始めず、最初に1400msぶん進めた静止画を一枚だけ描きます。点模様は止まっていてもそのまま読み取れ、ボタンは 一時停止 の代わりに 次の場面 になって、押すたびに900msぶん進みます。ポインターやキーの入力があると6ティック進めてすぐ一度描くので、入力が画面にすぐ反映されます。

css dither filter だけで足りるか?

css dither filter という言葉から CSS のプロパティ一行を期待しがちですが、この記事の九つの場面はキャンバスに putImageData で点を打つ方式で、CSS が受け持つのは拡大した点をにじませない image-rendering: pixelated と舞台の大きさくらいです。点ごとに閾値を比べたり誤差を流したりする計算は、スクリプトの側で行われます。

そこで、すでにある写真や要素の上にかぶせる効果が要るのか、明るさの場を自分で計算して点にできるのかを先に分けておくと迷いません。後者なら、この九つの場面が出発点になります。同じキャンバス計算を3Dの場面へ持っていくならレイマーチング9選 — WebGLなし、キャンバスのピクセル計算で描く3Dシーン、炎をパーティクルで描きたいならキャンバス 炎 アニメーション9選 — シード固定パーティクルで描く火のシーンが続きます。キャンバスの使い方は MDN の putImageData のページにまとまっています。

css dither background を使い回す前によくある質問

css dither effect を自分の写真に使うにはどこを替えればいい?

九つの場面は写真を読まず、数式で作った明るさの場を使います。ただし08がキャンバスに描いた絵を getImageData で読み、明るさの配列にしている箇所があるので、写真一枚を同じ方法で読めば、その値が明るさになります。写真に差し替えては動かしていません。

格子を160×100より大きくしてもいい?

計算量はマスの数に比例して増えます。02は毎フレーム16000マスを順番に頭から回し直し、09はマスごとにノイズを二回計算するので、重いのはこの二つですが、大きくした値では測っていません。

dither pattern css にライブラリや WebGL は必要?

必要ありません。九つのファイルとも、ライブラリ・CDN・WebGL は使わず、Math.random も呼びません。乱数が要る03と05は種を固定した lcg を、04は座標のハッシュを、09は vnoise を使うので、同じティックなら同じフレームが出ます。

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

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