ここから先は ImageData
c.getImageData で画面の生のピクセル配列を取り出すと、384 × 384 なら 384 × 384 × 4 個の数が並んだ Uint8ClampedArray が返る。× 4 は R, G, B, A の 4 チャンネル。(x, y) の赤は (y * width + x) * 4 番目で、緑がその次、青、アルファと続く。行優先の並びは Float32Array の格子と同じで、1 セルが 1 画素・4 要素になる。fillRect で粗いセルを塗ると一つのセルが数ピクセルの四角になるのに対し、こちらは画素そのものを読み書きする。
getImageData / putImageData は canvas のピクセルバッファに直接読み書きする API。getImageData(x, y, w, h) が Uint8ClampedArray を返し、Uint8ClampedArray は 0 未満や 255 超を代入すると自動で 0〜255 に丸める。新しいバッファだけ欲しいときは createImageData(w, h) で全要素 0 の配列を作る。fillRect を画素ごとに呼ぶとオーバーヘッドが画素数ぶん積もるが、配列に値を書いて一度 putImageData で流し込めば一回の転送で済む。384² フル解像度のポストプロセスが現実的な速度に乗る。
書き込みは (y * width + x) * 4 で画素の先頭を出し、R, G, B に同じ輝度、A に 255 を入れる。全画素を書き終えてから putImageData で一度に画面へ戻す。
const image = c.getImageData(0, 0, w, h)
const data = image.data // Uint8ClampedArray, 長さ w*h*4
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = (y * w + x) * 4 // この画素の先頭(R)
const luma = 0 // 0〜255 の輝度を計算
data[i] = luma // R
data[i + 1] = luma // G
data[i + 2] = luma // B
data[i + 3] = 255 // A(不透明)
}
}
c.putImageData(image, 0, 0)しきい値・ディザ・畳み込みは、格子のセルでなく画素そのものに同じ式で効く。下は sin を混ぜた場を画素ごとに評価し、Bayer 行列の閾値で二値化したものを createImageData の配列へ直接書いて putImageData で流し込んでいる。fillRect を一度も呼ばず、w × h の全画素を一回の転送で塗る。
網点が一画素単位で立ち、場の濃淡が黒白の点の密度に変わる。閾値 bayer[y & 7][x & 7] を外して value > 0.5 の単純な二値化にすると、点が消えて帯と段差が戻る。luma を value * 255 の連続値にすると、二値化を外した素の場が画素ごとに出る。(y * w + x) * 4 の + 1, + 2 を R と別の値にすれば、sky の単色 LUT を [r, g, b] で引き当てて 3 チャンネルへ展開できる。器を格子から生画素に取り替えると、しきい値もディザも畳み込みも同じ式のままフル解像度で効く。