/** Small deterministic RNG (mulberry32) — seeded so a run can be replayed. */ export class Rng { private s: number; constructor(seed = 1) { this.s = seed >>> 0; } next(): number { this.s = (this.s + 0x6d2b79f5) >>> 0; let t = this.s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } range(a: number, b: number): number { return a + this.next() * (b - a); } int(a: number, b: number): number { return Math.floor(this.range(a, b + 1)); } pick(arr: readonly T[]): T { return arr[Math.floor(this.next() * arr.length)]; } chance(p: number): boolean { return this.next() < p; } } /** Cheap 2D value noise, used to give each toasting run its own character. */ export function valueNoise2D(rng: Rng, w: number, h: number, octaves = 3): Float32Array { const out = new Float32Array(w * h); let amp = 1; let total = 0; for (let o = 0; o < octaves; o++) { const cells = 2 << o; const grid = new Float32Array((cells + 1) * (cells + 1)); for (let i = 0; i < grid.length; i++) grid[i] = rng.next(); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const fx = (x / (w - 1)) * cells; const fy = (y / (h - 1)) * cells; const x0 = Math.floor(fx); const y0 = Math.floor(fy); const tx = fx - x0; const ty = fy - y0; const sx = tx * tx * (3 - 2 * tx); const sy = ty * ty * (3 - 2 * ty); // Clamp: at x === w-1, fx lands exactly on `cells`, so the x0+1 lookup // runs one past the end of the grid. A typed array returns undefined // there, which silently turns the whole field into NaN. const g = (gx: number, gy: number) => grid[Math.min(gy, cells) * (cells + 1) + Math.min(gx, cells)]; const a = g(x0, y0) * (1 - sx) + g(x0 + 1, y0) * sx; const b = g(x0, y0 + 1) * (1 - sx) + g(x0 + 1, y0 + 1) * sx; out[y * w + x] += (a * (1 - sy) + b * sy) * amp; } } total += amp; amp *= 0.5; } for (let i = 0; i < out.length; i++) out[i] /= total; return out; }