ここから先は未踏
seeded PRNG。乱数を Math.random で直に呼ぶと、同じ絵を二度出せない。気に入った一枚が出ても、リロードで消える。種(seed)を一個の整数で持って、そこから決定的に乱数列を作れば、同じ種なら同じ絵が何度でも出る。種を URL に焼けば、誰かに「この絵」を渡せる。
定番の seeded PRNG が mulberry32。状態を 32bit 整数ひとつだけ持ち、毎回ビット演算(imul / シフト / xor)で撹拌して次の値を出す、数行の純粋関数。
const mulberry32 = (seed) => () => {
seed |= 0
seed = (seed + 0x6d2b79f5) | 0
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
Math.random との違いは「状態を自分で持つ」点だけで、返り値は同じ 0..1。const rng = mulberry32(12345) としてから Math.random() を rng() に置き換えれば、同じ seed で毎回ピクセル単位まで一致する。新しく要る概念は「状態を持つ純粋関数」これひとつ。
ImageData 直接操作。格子を fillRect で塗ると粗いタイルに留まる。ctx.createImageData(w, h) でピクセルの生バッファを取ると、(y·w + x)·4 の番地に直接 r,g,b,a を書ける。一画面ぶん書いてから putImageData で一気に貼ると、格子の刻みなしに全ピクセルが埋まる。
createImageData が返す image.data は Uint8ClampedArray で、ピクセルを左上から行優先に並べ、各ピクセルが r,g,b,a の 4 バイトを占める。座標 (x, y) のピクセル先頭は (y · width + x) · 4 の番地。a を 255 にすると不透明になる。tint が返す css 文字列の代わりに、輝度から r,g,b の数値を引く LUT を持てば、明度から直接 3 バイト書ける。fillRect のたびに発生する状態切り替えの呼び出しがなくなり、毎フレーム全ピクセルを触っても重くならない。
colors.tint(g) が返す rgb(r, g, b) を setup で 256 段ぶん読んで、輝度から r,g,b を引く小さな LUT に詰める。フレームごとに全ピクセルで sin を四本足した plasma の値を出し、輝度に写して LUT を引き、バッファへ直接書く。最後に putImageData で一枚貼る。
四本の sin は -4..4 を動くので、(v + 4) · (255 / 8) で 0..255 の輝度に写している。三本は縦横と斜めの波、一本は中心からの距離 Math.hypot で同心円の波。一ピクセルずつ書くので継ぎ目がなく、格子のタイルではなくフル解像度の濃淡になる。image と lut は setup で一度だけ確保して、フレームごとは中身を上書きするだけにしている。0.03 などの係数を上げ下げすると縞の細かさが変わる。
オフスクリーン canvas と drawImage。同じ絵を毎フレーム計算し直す代わりに、別の canvas へ一度焼いておいて貼る。光の点をスタンプとして一枚作り、乱数の位置に何度も貼る、という使い方になる。多段ブラーの中間置き場にもなる。
seeded PRNG は、作曲を保存したり、物理を同じ初期値で比べたり、タイルを共有したりする全部の前提になる。最初に置く一個。