/** * Deterministic scalar fields for Orby. * * Coordinate convention * --------------------- * `x` and `y` are continuous field-space coordinates (normally normalized to * -1..1 across a centered glyph), and `time` is an arbitrary monotonically * increasing value, conventionally seconds. Every named field returns a finite * value in 0..1. Options are optional plain objects and are never mutated. * * The hot sampling path is allocation-free. `FieldKit` owns its permutation * table and a few scalar scratch values; pass an output array to `curl2` when * calling that vector helper in an animation loop. */ export const TAU = Math.PI * 2; export const BAYER8_SIZE = 8; /** Clamp `value` to an inclusive range. Non-finite values become `min`. */ export function clamp(value, min = 0, max = 1) { if (!Number.isFinite(value)) return Number.isFinite(min) ? min : 0; if (value < min) return min; if (value > max) return max; return value; } /** Linear interpolation without implicit clamping. */ export function lerp(a, b, amount) { return a + (b - a) * amount; } /** Hermite interpolation from 0 to 1 between `edge0` and `edge1`. */ export function smoothstep(edge0, edge1, value) { if (!Number.isFinite(value)) return 0; if (edge0 === edge1) return value < edge0 ? 0 : 1; const t = clamp((value - edge0) / (edge1 - edge0)); return t * t * (3 - 2 * t); } /** Quintic interpolation from 0 to 1 between `edge0` and `edge1`. */ export function smootherstep(edge0, edge1, value) { if (!Number.isFinite(value)) return 0; if (edge0 === edge1) return value < edge0 ? 0 : 1; const t = clamp((value - edge0) / (edge1 - edge0)); return t * t * t * (t * (t * 6 - 15) + 10); } /** Positive fractional part, including for negative inputs. */ export function fract(value) { if (!Number.isFinite(value)) return 0; return value - Math.floor(value); } /** Standard 8x8 Bayer ordered-dither matrix, containing each value 0..63. */ export const BAYER8 = Object.freeze([ Object.freeze([0, 48, 12, 60, 3, 51, 15, 63]), Object.freeze([32, 16, 44, 28, 35, 19, 47, 31]), Object.freeze([8, 56, 4, 52, 11, 59, 7, 55]), Object.freeze([40, 24, 36, 20, 43, 27, 39, 23]), Object.freeze([2, 50, 14, 62, 1, 49, 13, 61]), Object.freeze([34, 18, 46, 30, 33, 17, 45, 29]), Object.freeze([10, 58, 6, 54, 9, 57, 5, 53]), Object.freeze([42, 26, 38, 22, 41, 25, 37, 21]), ]); /** Return the centered Bayer threshold for an integer pixel coordinate. */ export function bayer8(x, y) { const ix = Number.isFinite(x) ? Math.floor(x) & 7 : 0; const iy = Number.isFinite(y) ? Math.floor(y) & 7 : 0; return (BAYER8[iy][ix] + 0.5) / 64; } export const bayerThreshold = bayer8; /** * Quantize a 0..1 value with ordered dithering. * `levels=2` produces a binary pixel switch; larger values produce terraces. */ export function orderedDither(value, x, y, levels = 2) { const count = clamp(Math.floor(levels), 2, 256); const scaled = clamp(value) * (count - 1); const low = Math.floor(scaled); const high = Math.min(count - 1, low + 1); const mix = scaled - low; return (mix > bayer8(x, y) ? high : low) / (count - 1); } /** Stable 32-bit hash for numeric, string, bigint, boolean, or null seeds. */ export function hashSeed(seed = 0) { let hash = 0x811c9dc5; if (typeof seed === "number" && Number.isFinite(seed)) { if (Number.isInteger(seed)) { hash ^= seed >>> 0; hash = Math.imul(hash, 0x01000193); } else { const text = String(seed); for (let i = 0; i < text.length; i += 1) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 0x01000193); } } } else { const text = typeof seed === "string" ? seed : String(seed ?? 0); for (let i = 0; i < text.length; i += 1) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 0x01000193); } } hash ^= hash >>> 16; hash = Math.imul(hash, 0x7feb352d); hash ^= hash >>> 15; hash = Math.imul(hash, 0x846ca68b); hash ^= hash >>> 16; return hash >>> 0; } const EMPTY_OPTIONS = Object.freeze({}); const SQRT2 = Math.SQRT2; const INV_255 = 1 / 255; function finite(value, fallback = 0) { return Number.isFinite(value) ? value : fallback; } function option(options, key, fallback, min = -Infinity, max = Infinity) { if (!options) return fallback; const value = options[key]; if (!Number.isFinite(value)) return fallback; return clamp(value, min, max); } function optionInt(options, key, fallback, min, max) { return Math.floor(option(options, key, fallback, min, max)); } function contrast(value, amount) { return clamp((value - 0.5) * amount + 0.5); } function fade(value) { return value * value * value * (value * (value * 6 - 15) + 10); } function grad2(hash, x, y) { switch (hash & 7) { case 0: return x; case 1: return -x; case 2: return y; case 3: return -y; case 4: return (x + y) * Math.SQRT1_2; case 5: return (-x + y) * Math.SQRT1_2; case 6: return (x - y) * Math.SQRT1_2; default: return (-x - y) * Math.SQRT1_2; } } function grad3(hash, x, y, z) { const h = hash & 15; const u = h < 8 ? x : y; const v = h < 4 ? y : h === 12 || h === 14 ? x : z; return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); } function ensureKit(kit) { return kit instanceof FieldKit ? kit : DEFAULT_FIELD_KIT; } /** * Seeded noise and named-field sampler. * * Construct once and reuse: * `const fields = new FieldKit("product-icon");` * `const alpha = fields.sample("electric", x, y, seconds);` */ export class FieldKit { constructor(seed = 0) { const initialSeed = seed && typeof seed === "object" && "seed" in seed ? seed.seed : seed; this.seed = hashSeed(initialSeed); this.permutation = new Uint8Array(512); // Scalar scratch state keeps cellular/curl samplers allocation-free. this._cellF1 = 0; this._cellF2 = 0; this._cellHash = 0; this._curlX = 0; this._curlY = 0; this._curlMagnitude = 0; this._buildPermutation(); } /** Rebuild the lookup table from a new seed and return this instance. */ reseed(seed = 0) { this.seed = hashSeed(seed); this._buildPermutation(); return this; } setSeed(seed = 0) { return this.reseed(seed); } /** Create an independent sampler with the same seed. */ clone() { const copy = new FieldKit(0); copy.seed = this.seed; copy.permutation.set(this.permutation); return copy; } /** Frozen list of canonical named scalar fields. */ list() { return FIELD_IDS; } has(id) { return resolveFieldId(id) !== null; } /** * Return the standalone sampler function for an ID, or undefined. * Standalone samplers accept `(x, y, time, options, kit)`. */ get(id) { const resolved = resolveFieldId(id); return resolved === null ? undefined : FIELDS[resolved]; } /** Sample a canonical field or alias. Unknown IDs safely return 0. */ sample(id, x, y, time = 0, options = EMPTY_OPTIONS) { const resolved = resolveFieldId(id); if (resolved === null) return 0; const value = FIELDS[resolved]( finite(x), finite(y), finite(time), options || EMPTY_OPTIONS, this, ); return clamp(value); } /** * Seeded lattice hash in 0..1. Useful for deterministic sprite decisions. * Inputs are treated as integer lattice coordinates. */ hash2(x, y, salt = 0) { const p = this.permutation; const xi = finite(Math.floor(x)) & 255; const yi = finite(Math.floor(y)) & 255; const si = finite(Math.floor(salt)) & 255; return p[xi + p[yi + p[si]]] * INV_255; } /** Signed 2D gradient noise in -1..1. */ signedNoise2(x, y) { return this._signedNoise2(finite(x), finite(y)); } /** 2D gradient noise remapped to 0..1. */ noise2(x, y) { return this._signedNoise2(finite(x), finite(y)) * 0.5 + 0.5; } /** Signed 3D gradient noise in -1..1; use z as animation time. */ signedNoise3(x, y, z) { return this._signedNoise3(finite(x), finite(y), finite(z)); } /** 3D gradient noise remapped to 0..1. */ noise3(x, y, z) { return this._signedNoise3(finite(x), finite(y), finite(z)) * 0.5 + 0.5; } /** Seeded smooth value noise in 0..1. */ value2(x, y) { x = finite(x); y = finite(y); const x0 = Math.floor(x); const y0 = Math.floor(y); const tx = fade(x - x0); const ty = fade(y - y0); const p = this.permutation; const ax = x0 & 255; const ay = y0 & 255; const bx = (x0 + 1) & 255; const by = (y0 + 1) & 255; const a = p[ax + p[ay]] * INV_255; const b = p[bx + p[ay]] * INV_255; const c = p[ax + p[by]] * INV_255; const d = p[bx + p[by]] * INV_255; return clamp(lerp(lerp(a, b, tx), lerp(c, d, tx), ty)); } /** Fractal gradient noise in 0..1. */ fbmNoise(x, y, z = 0, options = EMPTY_OPTIONS) { const octaves = optionInt(options, "octaves", 5, 1, 9); const lacunarity = option(options, "lacunarity", 2, 1.01, 4); const gain = option(options, "gain", 0.5, 0.05, 0.95); return this._fbmSigned( finite(x), finite(y), finite(z), octaves, lacunarity, gain, ) * 0.5 + 0.5; } /** Multi-octave ridged noise in 0..1. */ ridgedNoise(x, y, z = 0, options = EMPTY_OPTIONS) { return this._ridged( finite(x), finite(y), finite(z), optionInt(options, "octaves", 5, 1, 9), option(options, "lacunarity", 2.05, 1.01, 4), option(options, "gain", 0.52, 0.05, 0.95), ); } /** * Normalized curl vector of a scalar noise potential. * Supply `out` (array or typed array) to avoid the one fallback allocation. */ curl2(x, y, z = 0, epsilon = 0.0125, out) { this._setCurl( finite(x), finite(y), finite(z), clamp(finite(epsilon, 0.0125), 0.0001, 0.25), ); const target = out || new Float32Array(2); target[0] = this._curlX; target[1] = this._curlY; return target; } /** Nearest animated Worley feature distance, normalized to 0..1. */ cellular2(x, y, time = 0, options = EMPTY_OPTIONS) { this._setCellular( finite(x), finite(y), finite(time), option(options, "jitter", 0.88, 0, 1), option(options, "motion", 0.1, 0, 0.45), ); return clamp(Math.sqrt(this._cellF1) / SQRT2); } /** Difference between smoothly seed-morphed Voronoi features, in 0..1. */ voronoi2(x, y, time = 0, options = EMPTY_OPTIONS) { this._setCellular( finite(x), finite(y), finite(time) + option(options, "phase", 0, -10000, 10000), option(options, "jitter", 0.88, 0, 1), option(options, "motion", 0.16, 0, 0.45), option(options, "seedRate", 0.9, 0, 8), ); return clamp((Math.sqrt(this._cellF2) - Math.sqrt(this._cellF1)) / SQRT2); } // Named convenience methods mirror `sample` without string lookup. fbm(x, y, time = 0, options) { return fbm(x, y, time, options, this); } ridged(x, y, time = 0, options) { return ridged(x, y, time, options, this); } domainWarp(x, y, time = 0, options) { return domainWarp(x, y, time, options, this); } curl(x, y, time = 0, options) { return curl(x, y, time, options, this); } flow(x, y, time = 0, options) { return flow(x, y, time, options, this); } worley(x, y, time = 0, options) { return worley(x, y, time, options, this); } voronoi(x, y, time = 0, options) { return voronoi(x, y, time, options, this); } plasma(x, y, time = 0, options) { return plasma(x, y, time, options, this); } interference(x, y, time = 0, options) { return interference(x, y, time, options, this); } vortex(x, y, time = 0, options) { return vortex(x, y, time, options, this); } metaballs(x, y, time = 0, options) { return metaballs(x, y, time, options, this); } caustics(x, y, time = 0, options) { return caustics(x, y, time, options, this); } strata(x, y, time = 0, options) { return strata(x, y, time, options, this); } radar(x, y, time = 0, options) { return radar(x, y, time, options, this); } constellation(x, y, time = 0, options) { return constellation(x, y, time, options, this); } liquid(x, y, time = 0, options) { return liquid(x, y, time, options, this); } electric(x, y, time = 0, options) { return electric(x, y, time, options, this); } ripple(x, y, time = 0, options) { return ripple(x, y, time, options, this); } kaleidoscope(x, y, time = 0, options) { return kaleidoscope(x, y, time, options, this); } _buildPermutation() { const p = this.permutation; for (let i = 0; i < 256; i += 1) p[i] = i; let state = this.seed || 0x6d2b79f5; for (let i = 255; i > 0; i -= 1) { state += 0x6d2b79f5; let random = state; random = Math.imul(random ^ (random >>> 15), random | 1); random ^= random + Math.imul(random ^ (random >>> 7), random | 61); random = (random ^ (random >>> 14)) >>> 0; const j = Math.floor((random / 0x100000000) * (i + 1)); const swap = p[i]; p[i] = p[j]; p[j] = swap; } for (let i = 0; i < 256; i += 1) p[i + 256] = p[i]; } _signedNoise2(x, y) { const x0 = Math.floor(x); const y0 = Math.floor(y); const xf = x - x0; const yf = y - y0; const u = fade(xf); const v = fade(yf); const p = this.permutation; const xi = x0 & 255; const yi = y0 & 255; const aa = p[xi + p[yi]]; const ba = p[xi + 1 + p[yi]]; const ab = p[xi + p[yi + 1]]; const bb = p[xi + 1 + p[yi + 1]]; const low = lerp(grad2(aa, xf, yf), grad2(ba, xf - 1, yf), u); const high = lerp( grad2(ab, xf, yf - 1), grad2(bb, xf - 1, yf - 1), u, ); return clamp(lerp(low, high, v) * 1.55, -1, 1); } _signedNoise3(x, y, z) { const x0 = Math.floor(x); const y0 = Math.floor(y); const z0 = Math.floor(z); const xf = x - x0; const yf = y - y0; const zf = z - z0; const u = fade(xf); const v = fade(yf); const w = fade(zf); const p = this.permutation; const xi = x0 & 255; const yi = y0 & 255; const zi = z0 & 255; const a = p[xi] + yi; const aa = p[a] + zi; const ab = p[a + 1] + zi; const b = p[xi + 1] + yi; const ba = p[b] + zi; const bb = p[b + 1] + zi; const zLow = lerp( lerp( grad3(p[aa], xf, yf, zf), grad3(p[ba], xf - 1, yf, zf), u, ), lerp( grad3(p[ab], xf, yf - 1, zf), grad3(p[bb], xf - 1, yf - 1, zf), u, ), v, ); const zHigh = lerp( lerp( grad3(p[aa + 1], xf, yf, zf - 1), grad3(p[ba + 1], xf - 1, yf, zf - 1), u, ), lerp( grad3(p[ab + 1], xf, yf - 1, zf - 1), grad3(p[bb + 1], xf - 1, yf - 1, zf - 1), u, ), v, ); return clamp(lerp(zLow, zHigh, w) * 0.94, -1, 1); } _fbmSigned(x, y, z, octaves, lacunarity, gain) { let sum = 0; let amplitude = 0.5; let normalization = 0; for (let octave = 0; octave < octaves; octave += 1) { sum += this._signedNoise3(x, y, z) * amplitude; normalization += amplitude; // Rotate and offset between octaves to suppress axial lattice artifacts. const nextX = (x * 0.8 - y * 0.6) * lacunarity + 17.17; y = (x * 0.6 + y * 0.8) * lacunarity - 9.23; x = nextX; z = z * lacunarity + 5.71; amplitude *= gain; } return normalization > 0 ? clamp(sum / normalization, -1, 1) : 0; } _ridged(x, y, z, octaves, lacunarity, gain) { let sum = 0; let amplitude = 0.5; let normalization = 0; let weight = 1; for (let octave = 0; octave < octaves; octave += 1) { let ridge = 1 - Math.abs(this._signedNoise3(x, y, z)); ridge *= ridge; ridge *= weight; weight = clamp(ridge * 2.25); sum += ridge * amplitude; normalization += amplitude; const nextX = (x * 0.764 - y * 0.645) * lacunarity + 11.37; y = (x * 0.645 + y * 0.764) * lacunarity + 3.19; x = nextX; z = z * lacunarity - 4.31; amplitude *= gain; } return normalization > 0 ? clamp(sum / normalization) : 0; } _setCurl(x, y, z, epsilon) { const dY = (this._signedNoise3(x, y + epsilon, z) - this._signedNoise3(x, y - epsilon, z)) / (epsilon * 2); const dX = (this._signedNoise3(x + epsilon, y, z) - this._signedNoise3(x - epsilon, y, z)) / (epsilon * 2); const vx = dY; const vy = -dX; const magnitude = Math.hypot(vx, vy); this._curlMagnitude = Number.isFinite(magnitude) ? magnitude : 0; if (magnitude > 1e-9 && Number.isFinite(magnitude)) { this._curlX = vx / magnitude; this._curlY = vy / magnitude; } else { this._curlX = 0; this._curlY = 0; } } _setCellular(x, y, time, jitter, motion, seedRate) { const baseX = Math.floor(x); const baseY = Math.floor(y); let f1 = Infinity; let f2 = Infinity; let nearestHash = 0; const p = this.permutation; const morphing = seedRate !== undefined && motion > 0; const seedPhase = morphing ? time * seedRate : 0; const seedEpoch = Math.floor(seedPhase); const seedU = morphing ? fract(seedPhase) : 0; const seedU2 = seedU * seedU; const seedU3 = seedU2 * seedU; const seedB0 = ((1 - seedU) * (1 - seedU) * (1 - seedU)) / 6; const seedB1 = (4 - 6 * seedU2 + 3 * seedU3) / 6; const seedB2 = (1 + 3 * seedU + 3 * seedU2 - 3 * seedU3) / 6; const seedB3 = seedU3 / 6; const seedSalt0 = morphing ? p[(seedEpoch - 1) & 255] : 0; const seedSalt1 = morphing ? p[seedEpoch & 255] : 0; const seedSalt2 = morphing ? p[(seedEpoch + 1) & 255] : 0; const seedSalt3 = morphing ? p[(seedEpoch + 2) & 255] : 0; const anchorScale = morphing ? Math.max(0, 1 - motion * 2) : 1; for (let oy = -1; oy <= 1; oy += 1) { const cellY = baseY + oy; const py = cellY & 255; for (let ox = -1; ox <= 1; ox += 1) { const cellX = baseX + ox; const px = cellX & 255; const hashA = p[px + p[py]]; const hashB = p[px + p[py + 71]]; const hashC = p[px + p[py + 149]]; let featureX = 0.5 + (hashA * INV_255 - 0.5) * jitter * anchorScale; let featureY = 0.5 + (hashB * INV_255 - 0.5) * jitter * anchorScale; if (morphing) { const offsetX = (p[hashA + seedSalt0] * seedB0 + p[hashA + seedSalt1] * seedB1 + p[hashA + seedSalt2] * seedB2 + p[hashA + seedSalt3] * seedB3) * INV_255 * 2 - 1; const offsetY = (p[hashB + seedSalt0] * seedB0 + p[hashB + seedSalt1] * seedB1 + p[hashB + seedSalt2] * seedB2 + p[hashB + seedSalt3] * seedB3) * INV_255 * 2 - 1; featureX += offsetX * motion; featureY += offsetY * motion; } else { const phase = hashC * INV_255 * TAU; featureX += Math.sin(time + phase) * motion; featureY += Math.cos(time * 0.91 + phase) * motion; } featureX = clamp(featureX, 0.015, 0.985); featureY = clamp(featureY, 0.015, 0.985); const dx = cellX + featureX - x; const dy = cellY + featureY - y; const distance = dx * dx + dy * dy; if (distance < f1) { f2 = f1; f1 = distance; nearestHash = hashC; } else if (distance < f2) { f2 = distance; } } } this._cellF1 = Number.isFinite(f1) ? f1 : 0; this._cellF2 = Number.isFinite(f2) ? f2 : this._cellF1; this._cellHash = nearestHash; } } /** Soft multi-octave gradient noise. */ export function fbm(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 2.35, 0.001, 256); const speed = option(options, "speed", 0.18, -20, 20); const value = kit._fbmSigned( finite(x) * frequency, finite(y) * frequency, finite(time) * speed + option(options, "phase", 0, -10000, 10000), optionInt(options, "octaves", 5, 1, 9), option(options, "lacunarity", 2, 1.01, 4), option(options, "gain", 0.5, 0.05, 0.95), ) * 0.5 + 0.5; return contrast(value, option(options, "contrast", 1.08, 0, 8)); } /** Sharp mountain/filament ridges with octave feedback. */ export function ridged(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 2.7, 0.001, 256); const value = kit._ridged( finite(x) * frequency, finite(y) * frequency, finite(time) * option(options, "speed", 0.22, -20, 20), optionInt(options, "octaves", 5, 1, 9), option(options, "lacunarity", 2.04, 1.01, 4), option(options, "gain", 0.53, 0.05, 0.95), ); return contrast(value, option(options, "contrast", 1.32, 0, 8)); } /** fBm evaluated through a second pair of animated fBm coordinate fields. */ export function domainWarp(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 2.1, 0.001, 256); const speed = option(options, "speed", 0.2, -20, 20); const strength = option(options, "warp", 0.72, 0, 6); const px = finite(x) * frequency; const py = finite(y) * frequency; const pz = finite(time) * speed; const qx = kit._fbmSigned(px + 5.2, py + 1.3, pz, 3, 2, 0.5); const qy = kit._fbmSigned(px - 2.8, py + 8.1, pz + 3.4, 3, 2, 0.5); const value = kit._fbmSigned( px + qx * strength, py + qy * strength, pz + (qx - qy) * 0.22, optionInt(options, "octaves", 5, 1, 9), option(options, "lacunarity", 2.02, 1.01, 4), option(options, "gain", 0.5, 0.05, 0.95), ) * 0.5 + 0.5; return contrast(value, option(options, "contrast", 1.18, 0, 8)); } /** Curl magnitude of animated gradient noise, useful as turbulent density. */ export function curl(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 2.2, 0.001, 256); const px = finite(x) * frequency; const py = finite(y) * frequency; const pz = finite(time) * option(options, "speed", 0.18, -20, 20); kit._setCurl( px, py, pz, option(options, "epsilon", 0.018, 0.0001, 0.2), ); const density = 1 - Math.exp( -kit._curlMagnitude * option(options, "strength", 0.72, 0.001, 20), ); const modulation = kit._fbmSigned(px * 0.63, py * 0.63, pz + 4.7, 3, 2, 0.5) * 0.5 + 0.5; return contrast( density * 0.74 + modulation * 0.26, option(options, "contrast", 1.2, 0, 8), ); } /** Animated ribbons advected along a normalized curl-noise direction. */ export function flow(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 2.15, 0.001, 256); const speed = option(options, "speed", 0.24, -20, 20); const px = finite(x) * frequency; const py = finite(y) * frequency; const pz = finite(time) * speed; kit._setCurl(px * 0.72, py * 0.72, pz, 0.02); const warp = option(options, "warp", 0.65, 0, 6); const ax = px + kit._curlX * warp; const ay = py + kit._curlY * warp; const base = kit._fbmSigned(ax, ay, pz, 4, 2.03, 0.5); const directional = ax * kit._curlX * 0.37 + ay * kit._curlY * 0.37 + base * 1.35; const ribbon = 0.5 + Math.sin( directional * TAU - pz * option(options, "travel", 1.7, -20, 20), ) * 0.5; const value = ribbon * 0.72 + (base * 0.5 + 0.5) * 0.28; return contrast(value, option(options, "contrast", 1.28, 0, 8)); } /** Animated Worley feature islands (bright centers, dark gaps). */ export function worley(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 3.4, 0.001, 256); kit._setCellular( finite(x) * frequency, finite(y) * frequency, finite(time) * option(options, "speed", 0.52, -20, 20), option(options, "jitter", 0.9, 0, 1), option(options, "motion", 0.1, 0, 0.45), ); const distance = Math.sqrt(kit._cellF1) / SQRT2; const value = 1 - smoothstep( option(options, "inner", 0.035, 0, 1), option(options, "outer", 0.62, 0.001, 2), distance, ); return contrast(value, option(options, "contrast", 1.1, 0, 8)); } /** Voronoi borders whose feature sites ease between deterministic seed states. */ export function voronoi(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 3.25, 0.001, 256); kit._setCellular( finite(x) * frequency, finite(y) * frequency, finite(time) * option(options, "speed", 0.43, -20, 20) + option(options, "phase", 0, -10000, 10000), option(options, "jitter", 0.92, 0, 1), option(options, "motion", 0.16, 0, 0.45), option(options, "seedRate", 0.9, 0, 8), ); const edgeDistance = Math.sqrt(kit._cellF2) - Math.sqrt(kit._cellF1); const width = option(options, "width", 0.085, 0.001, 1); const value = 1 - smoothstep(width, width * 2.75, edgeDistance); return contrast(value, option(options, "contrast", 1.32, 0, 8)); } /** Layered sinusoidal plasma with a noise-driven phase field. */ export function plasma(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 1.45, 0.001, 256); const px = finite(x) * frequency; const py = finite(y) * frequency; const phase = finite(time) * option(options, "speed", 0.62, -20, 20) + option(options, "phase", 0, -10000, 10000); const noise = kit._fbmSigned(px * 0.7, py * 0.7, phase * 0.18, 3, 2, 0.5); const a = Math.sin(px * TAU + phase + noise * 1.6); const b = Math.sin(py * TAU * 1.13 - phase * 1.17 - noise * 1.35); const c = Math.sin((px + py) * TAU * 0.61 + phase * 0.71 + noise * 2.1); const d = Math.sin(Math.hypot(px - 0.5, py - 0.5) * TAU * 2.2 - phase); return contrast( 0.5 + (a + b + c + d) * 0.125, option(options, "contrast", 1.12, 0, 8), ); } /** Traveling circular waves from multiple moving emitters. */ export function interference(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 8.5, 0.01, 256); const phase = finite(time) * option(options, "speed", 2.1, -50, 50); const px = finite(x); const py = finite(y); const sourceA = phase * 0.23 + kit.seed * 1e-7; const sourceB = -phase * 0.19 + kit.seed * 1.7e-7; const ax = 0.24 + Math.sin(sourceA) * 0.09; const ay = 0.35 + Math.cos(sourceA * 0.87) * 0.11; const bx = 0.76 + Math.cos(sourceB) * 0.1; const by = 0.64 + Math.sin(sourceB * 1.09) * 0.09; const da = Math.hypot(px - ax, py - ay); const db = Math.hypot(px - bx, py - by); const waveA = Math.sin(da * frequency * TAU - phase); const waveB = Math.sin(db * frequency * TAU * 1.037 - phase * 1.11); const beat = Math.sin((da - db) * frequency * TAU * 0.53 + phase * 0.31); return contrast( 0.5 + (waveA + waveB) * 0.19 + beat * 0.12, option(options, "contrast", 1.2, 0, 8), ); } /** Rotating noisy spiral arms around a configurable center. */ export function vortex(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const centerX = option(options, "centerX", 0, -1000, 1000); const centerY = option(options, "centerY", 0, -1000, 1000); const frequency = option(options, "frequency", 1, 0.001, 256); const dx = (finite(x) - centerX) * frequency; const dy = (finite(y) - centerY) * frequency; const radius = Math.hypot(dx, dy); const angle = Math.atan2(dy, dx); const phase = finite(time) * option(options, "speed", 1.4, -50, 50); const turbulence = kit._fbmSigned( dx * 2.1 + 4.2, dy * 2.1 - 3.7, phase * 0.11, 4, 2, 0.5, ); const arms = optionInt(options, "arms", 4, 1, 16); const turns = option(options, "turns", 3.8, -30, 30); const spiral = angle * arms + radius * turns * TAU + turbulence * 2.2 - phase; const value = 0.5 + Math.sin(spiral) * 0.5; const limit = option(options, "radius", 1.15, 0.01, 20); const envelope = 1 - smoothstep(limit * 0.72, limit, radius); return contrast( value * (0.58 + envelope * 0.42), option(options, "contrast", 1.25, 0, 8), ); } /** Smooth union of deterministic, independently moving metaballs. */ export function metaballs(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 1, 0.001, 256); const px = fract(finite(x) * frequency); const py = fract(finite(y) * frequency); const phase = finite(time) * option(options, "speed", 0.7, -20, 20); const count = optionInt(options, "count", 6, 2, 16); const radius = option(options, "radius", 0.115, 0.005, 0.5); const radiusSquared = radius * radius; let influence = 0; for (let i = 0; i < count; i += 1) { const seedX = kit.hash2(i, 17, 41); const seedY = kit.hash2(i, 79, 113); const seedP = kit.hash2(i, 151, 197) * TAU; const cx = fract(seedX + Math.sin(phase * (0.52 + seedY) + seedP) * 0.17); const cy = fract(seedY + Math.cos(phase * (0.48 + seedX) + seedP) * 0.17); let dx = Math.abs(px - cx); let dy = Math.abs(py - cy); dx = Math.min(dx, 1 - dx); dy = Math.min(dy, 1 - dy); const distanceSquared = dx * dx + dy * dy; influence += radiusSquared / (distanceSquared + radiusSquared); } const threshold = option(options, "threshold", 0.72, 0.05, 8); return contrast( smoothstep(threshold * 0.55, threshold * 1.35, influence), option(options, "contrast", 1.15, 0, 8), ); } /** Refractive, sharp cellular light bands resembling water caustics. */ export function caustics(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 4, 0.001, 256); const phase = finite(time) * option(options, "speed", 0.56, -20, 20); const px = finite(x) * frequency; const py = finite(y) * frequency; const warpX = kit._fbmSigned(px * 0.41, py * 0.41, phase * 0.2, 3, 2, 0.5); const warpY = kit._fbmSigned( px * 0.41 + 8.7, py * 0.41 - 2.4, phase * 0.2, 3, 2, 0.5, ); kit._setCellular( px + warpX * 0.42, py + warpY * 0.42, phase, 0.96, 0.12, ); const delta = Math.sqrt(kit._cellF2) - Math.sqrt(kit._cellF1); const width = option(options, "width", 0.075, 0.001, 0.5); const cellularLine = 1 - smoothstep(width, width * 3.1, delta); const noiseLine = 1 - smoothstep( 0.04, 0.3, Math.abs( kit._signedNoise3(px * 0.72 + warpX, py * 0.72 + warpY, phase * 0.24), ), ); return contrast( Math.max(cellularLine, noiseLine * 0.68), option(options, "contrast", 1.45, 0, 8), ); } /** Noise-warped sedimentary bands with controllable slope and sharpness. */ export function strata(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 1, 0.001, 256); const px = finite(x) * frequency; const py = finite(y) * frequency; const phase = finite(time) * option(options, "speed", 0.12, -20, 20); const warp = kit._fbmSigned(px * 1.7, py * 1.1, phase, 5, 2, 0.5); const grain = kit._signedNoise3(px * 6.3, py * 2.2, phase * 0.7); const bands = option(options, "bands", 8, 0.1, 128); const slope = option(options, "slope", 0.12, -20, 20); const position = (py + px * slope + warp * option(options, "warp", 0.16, 0, 5) - phase * 0.03) * bands; let value = 0.5 + Math.sin(position * TAU + grain * 0.35) * 0.5; value = Math.pow(value, option(options, "sharpness", 1.5, 0.1, 12)); value = value * 0.86 + (grain * 0.5 + 0.5) * 0.14; return contrast(value, option(options, "contrast", 1.18, 0, 8)); } /** Rotating radar beam, concentric rings, and deterministic target blips. */ export function radar(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const dx = finite(x) - option(options, "centerX", 0, -1000, 1000); const dy = finite(y) - option(options, "centerY", 0, -1000, 1000); const radius = Math.hypot(dx, dy); const angle = Math.atan2(dy, dx); const sweep = finite(time) * option(options, "speed", 1.15, -50, 50) + option(options, "phase", 0, -10000, 10000); let angleDelta = angle - sweep; angleDelta = Math.atan2(Math.sin(angleDelta), Math.cos(angleDelta)); const beamWidth = option(options, "beamWidth", 0.18, 0.005, Math.PI); const beam = Math.exp(-Math.abs(angleDelta) / beamWidth); const limit = option(options, "radius", 1.05, 0.01, 20); const envelope = 1 - smoothstep(limit * 0.86, limit, radius); const rings = option(options, "rings", 5, 1, 64); const ringPhase = fract((radius / limit) * rings); const ringDistance = Math.min(ringPhase, 1 - ringPhase); const ringLine = (1 - smoothstep( option(options, "ringWidth", 0.025, 0.001, 0.49), 0.12, ringDistance, )) * envelope * 0.34; let blip = 0; const targets = optionInt(options, "targets", 7, 0, 20); for (let i = 0; i < targets; i += 1) { const tx = kit.hash2(i, 31, 97) * 1.72 - 0.86; const ty = kit.hash2(i, 83, 181) * 1.72 - 0.86; const bx = finite(x) - tx; const by = finite(y) - ty; const spot = Math.exp( -(bx * bx + by * by) / option(options, "blipSize", 0.00085, 0.00001, 0.1), ); if (spot > blip) blip = spot; } return clamp( Math.max(beam * envelope * 0.78, ringLine, blip * (0.28 + beam * 0.72)), ); } /** Twinkling cellular stars connected by faint Voronoi filaments. */ export function constellation(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 4.2, 0.001, 256); const px = finite(x) * frequency; const py = finite(y) * frequency; const phase = finite(time) * option(options, "speed", 1.7, -50, 50); kit._setCellular(px, py, 0, option(options, "jitter", 0.96, 0, 1), 0); const nearest = Math.sqrt(kit._cellF1); const edge = Math.sqrt(kit._cellF2) - nearest; const starSize = option(options, "starSize", 0.12, 0.005, 1); const star = 1 - smoothstep(starSize * 0.16, starSize, nearest); const twinkle = 0.68 + 0.32 * Math.sin(phase + kit._cellHash * INV_255 * TAU + nearest * 8.0); const lineWidth = option(options, "lineWidth", 0.035, 0.001, 0.5); const network = (1 - smoothstep(lineWidth, lineWidth * 3.8, edge)) * option(options, "lineOpacity", 0.28, 0, 1); return contrast( Math.max(star * twinkle, network), option(options, "contrast", 1.3, 0, 8), ); } /** Layered, domain-warped water surface with traveling highlights. */ export function liquid(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 2.3, 0.001, 256); const phase = finite(time) * option(options, "speed", 0.35, -20, 20); const px = finite(x) * frequency; const py = finite(y) * frequency; const qx = kit._fbmSigned(px * 0.72, py * 0.72, phase, 4, 2, 0.52); const qy = kit._fbmSigned( px * 0.72 + 6.2, py * 0.72 - 1.7, phase + 2.1, 4, 2, 0.52, ); const warp = option(options, "warp", 0.58, 0, 6); const surface = kit._fbmSigned( px + qx * warp, py + qy * warp, phase * 0.72, optionInt(options, "octaves", 5, 1, 9), 2.03, 0.51, ); const wave = 0.5 + Math.sin((py + qx * 0.42) * TAU * 1.35 - phase * 2.4) * 0.5; const highlight = Math.pow( 1 - Math.abs(kit._signedNoise3(px * 1.8 + qy, py * 1.8, phase) || 0), 5, ); return contrast( (surface * 0.5 + 0.5) * 0.52 + wave * 0.31 + highlight * 0.17, option(options, "contrast", 1.18, 0, 8), ); } /** Repeating branching lightning channels with traveling spark intensity. */ export function electric(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const frequency = option(options, "frequency", 1.6, 0.001, 256); const px = fract(finite(x) * frequency) - 0.5; const py = finite(y) * frequency; const phase = finite(time) * option(options, "speed", 1.8, -50, 50); const path = kit._fbmSigned(py * 0.46, phase * 0.17, phase * 0.11, 6, 2.12, 0.54) * option(options, "wander", 0.26, 0, 0.48); const forkNoise = kit._fbmSigned( py * 0.93 + 8.4, phase * 0.23, phase * 0.19, 4, 2, 0.5, ); const width = option(options, "width", 0.025, 0.001, 0.3); const main = Math.exp(-Math.abs(px - path) / width); const forkOffset = 0.1 + Math.abs(forkNoise) * 0.17; const forkGate = smoothstep(-0.2, 0.65, forkNoise); const forkA = Math.exp(-Math.abs(px - path - forkOffset) / (width * 1.8)) * forkGate; const forkB = Math.exp(-Math.abs(px - path + forkOffset) / (width * 1.8)) * (1 - forkGate); const pulse = 0.67 + 0.33 * Math.sin( py * TAU * option(options, "travel", 2.4, -30, 30) - phase * 4.1, ); const sparks = kit._ridged(px * 14, py * 8, phase * 0.5, 3, 2.2, 0.46); return contrast( Math.max(main, forkA * 0.7, forkB * 0.7) * pulse * (0.72 + sparks * 0.28), option(options, "contrast", 1.65, 0, 8), ); } /** Noise-distorted radial rings radiating from a configurable origin. */ export function ripple(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const centerX = option(options, "centerX", 0, -1000, 1000); const centerY = option(options, "centerY", 0, -1000, 1000); const dx = finite(x) - centerX; const dy = finite(y) - centerY; const phase = finite(time) * option(options, "speed", 1.5, -50, 50); const distortion = kit._fbmSigned(dx * 4, dy * 4, phase * 0.12, 4, 2, 0.5); const radius = Math.hypot(dx, dy) + distortion * option(options, "warp", 0.035, 0, 1); const rings = option(options, "frequency", 11, 0.01, 256); const wave = 0.5 + Math.sin(radius * rings * TAU - phase * TAU) * 0.5; const envelope = 1 - smoothstep( option(options, "radius", 1.15, 0.01, 20) * 0.82, option(options, "radius", 1.15, 0.01, 20), radius, ); return contrast( Math.pow(wave, option(options, "sharpness", 2.1, 0.1, 16)) * envelope, option(options, "contrast", 1.2, 0, 8), ); } /** Folded polar noise producing rotating mandala-like symmetry. */ export function kaleidoscope(x, y, time = 0, options = EMPTY_OPTIONS, kit) { kit = ensureKit(kit); options = options || EMPTY_OPTIONS; const dx = finite(x) - option(options, "centerX", 0, -1000, 1000); const dy = finite(y) - option(options, "centerY", 0, -1000, 1000); const radius = Math.hypot(dx, dy); const segments = optionInt(options, "segments", 8, 2, 32); const sector = TAU / segments; let angle = Math.atan2(dy, dx) + finite(time) * option(options, "speed", 0.32, -20, 20); angle = fract(angle / sector) * sector; angle = Math.abs(angle - sector * 0.5); const px = Math.cos(angle) * radius; const py = Math.sin(angle) * radius; const frequency = option(options, "frequency", 7, 0.001, 256); const noise = kit._fbmSigned( px * frequency, py * frequency, finite(time) * 0.16, optionInt(options, "octaves", 5, 1, 9), 2.03, 0.5, ); const rings = 0.5 + Math.sin( radius * TAU * option(options, "rings", 5, 0, 64) + noise * 2.3, ) * 0.5; return contrast( (noise * 0.5 + 0.5) * 0.58 + rings * 0.42, option(options, "contrast", 1.24, 0, 8), ); } /** * Canonical named samplers. Each function accepts * `(x, y, time = 0, options = {}, kit = DEFAULT_FIELD_KIT)` and returns 0..1. */ export const FIELDS = Object.freeze({ fbm, ridged, "domain-warp": domainWarp, curl, flow, worley, voronoi, plasma, interference, vortex, metaballs, caustics, strata, radar, constellation, liquid, electric, ripple, kaleidoscope, }); export const FIELD_IDS = Object.freeze(Object.keys(FIELDS)); /** Compatibility aliases accepted by `FieldKit.sample`, `get`, and `has`. */ export const FIELD_ALIASES = Object.freeze({ noise: "fbm", turbulence: "fbm", ridge: "ridged", domainWarp: "domain-warp", domain_warp: "domain-warp", warp: "domain-warp", "curl-flow": "flow", curlFlow: "flow", cellular: "worley", cells: "voronoi", cell: "voronoi", water: "liquid", lightning: "electric", waves: "ripple", mandala: "kaleidoscope", }); function resolveFieldId(id) { if (typeof id !== "string") return null; if (Object.prototype.hasOwnProperty.call(FIELDS, id)) return id; if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, id)) { return FIELD_ALIASES[id]; } // Normalize only the uncommon path so canonical per-pixel sampling allocates // no temporary strings. const normalized = id.trim().toLowerCase().replace(/[\s_]+/g, "-"); if (Object.prototype.hasOwnProperty.call(FIELDS, normalized)) { return normalized; } if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, normalized)) { return FIELD_ALIASES[normalized]; } return null; } /** Shared seed-0 kit for the standalone sampler functions. */ export const DEFAULT_FIELD_KIT = new FieldKit(0); /** Factory form for consumers that prefer functions over constructors. */ export function createFieldKit(seed = 0) { return new FieldKit(seed); } export default FieldKit;