Initial
This commit is contained in:
93
src/render/noise.js
Normal file
93
src/render/noise.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/* ============================================================
|
||||
noise.js — value noise / fbm helpers for the analog layer.
|
||||
Used for film grain structure and low-frequency tonal mottle
|
||||
(uneven gas glow & development stains).
|
||||
============================================================ */
|
||||
import { mulberry32, cyrb53 } from '../rng.js';
|
||||
|
||||
/* Hash-based value noise on an integer lattice, smoothstep-interpolated. */
|
||||
function lattice(seedInt) {
|
||||
const cache = new Map();
|
||||
return (ix, iy) => {
|
||||
const key = ix * 73856093 ^ iy * 19349663;
|
||||
let v = cache.get(key);
|
||||
if (v === undefined) {
|
||||
let h = (seedInt ^ Math.imul(ix, 374761393) ^ Math.imul(iy, 668265263)) >>> 0;
|
||||
h = Math.imul(h ^ (h >>> 13), 1274126177) >>> 0;
|
||||
v = (h >>> 0) / 4294967296;
|
||||
cache.set(key, v);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
const smooth = (t) => t * t * (3 - 2 * t);
|
||||
|
||||
function valueNoise2D(lat, x, y) {
|
||||
const x0 = Math.floor(x), y0 = Math.floor(y);
|
||||
const fx = smooth(x - x0), fy = smooth(y - y0);
|
||||
const v00 = lat(x0, y0), v10 = lat(x0 + 1, y0);
|
||||
const v01 = lat(x0, y0 + 1), v11 = lat(x0 + 1, y0 + 1);
|
||||
const a = v00 + (v10 - v00) * fx;
|
||||
const b = v01 + (v11 - v01) * fx;
|
||||
return a + (b - a) * fy;
|
||||
}
|
||||
|
||||
/* Fractal (fbm) value noise, octaves of valueNoise2D. Returns [0,1]. */
|
||||
export function fbm(lat, x, y, octaves = 4, lacunarity = 2, gain = 0.5) {
|
||||
let amp = 1, freq = 1, sum = 0, norm = 0;
|
||||
for (let o = 0; o < octaves; o++) {
|
||||
sum += amp * valueNoise2D(lat, x * freq, y * freq);
|
||||
norm += amp;
|
||||
amp *= gain;
|
||||
freq *= lacunarity;
|
||||
}
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
/* Build a low-frequency tonal mottle canvas (uneven illumination + stains).
|
||||
`cells` ≈ how many noise cells across the image (low = broad blotches). */
|
||||
export function mottleCanvas(seedStr, size, cells = 5, octaves = 4) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = size;
|
||||
const cx = c.getContext('2d');
|
||||
const img = cx.createImageData(size, size);
|
||||
const d = img.data;
|
||||
const lat = lattice(parseInt(cyrb53(seedStr + '::mottle').slice(0, 8), 16));
|
||||
const s = cells / size;
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
let n = fbm(lat, x * s, y * s, octaves, 2.1, 0.55);
|
||||
// bias toward mid, widen contrast a touch
|
||||
n = Math.min(1, Math.max(0, (n - 0.5) * 1.6 + 0.5));
|
||||
const v = Math.round(n * 255);
|
||||
const i = (y * size + x) * 4;
|
||||
d[i] = d[i + 1] = d[i + 2] = v;
|
||||
d[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
cx.putImageData(img, 0, 0);
|
||||
return c;
|
||||
}
|
||||
|
||||
/* Build a film-grain tile. Grain is generated at `grainSize` then meant to be
|
||||
drawn upscaled, giving clumps larger than one device pixel (real emulsion
|
||||
grain is not per-pixel). Returns an offscreen canvas of grain in alpha. */
|
||||
export function grainCanvas(seedStr, grainSize, contrast = 1) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = grainSize;
|
||||
const cx = c.getContext('2d');
|
||||
const img = cx.createImageData(grainSize, grainSize);
|
||||
const d = img.data;
|
||||
const rng = mulberry32(parseInt(cyrb53(seedStr + '::grain').slice(0, 8), 16));
|
||||
for (let i = 0; i < d.length; i += 4) {
|
||||
// signed grain centred on 0.5, gaussian-ish via two uniforms
|
||||
let g = (rng() + rng() - 1) * 0.5 + 0.5;
|
||||
g = Math.min(1, Math.max(0, (g - 0.5) * contrast + 0.5));
|
||||
const v = Math.round(g * 255);
|
||||
d[i] = d[i + 1] = d[i + 2] = v;
|
||||
d[i + 3] = 255;
|
||||
}
|
||||
cx.putImageData(img, 0, 0);
|
||||
return c;
|
||||
}
|
||||
Reference in New Issue
Block a user