/* ============================================================ params.js — derive a full, tasteful parameter set from a seed. This makes the seed the COMPLETE fingerprint of a plate: the same seed always yields the same parameters, so an inspiration thumbnail and its print-resolution master are identical. Pass a seed anywhere and get the same image at any size. A seed first selects a weighted "archetype" (overall character) then jitters within it, so the gallery is varied but every plate stays plausible. ============================================================ */ import { makeRng } from '../rng.js'; const ARCHETYPES = [ { name: 'archival', w: 0.42 }, { name: 'dense', w: 0.20 }, { name: 'cosmic', w: 0.16 }, { name: 'clean', w: 0.12 }, { name: 'negative', w: 0.10 }, ]; function pickArchetype(rng) { const total = ARCHETYPES.reduce((s, a) => s + a.w, 0); let t = rng() * total; for (const a of ARCHETYPES) { if ((t -= a.w) <= 0) return a.name; } return 'archival'; } export function archetypeOf(seed) { return pickArchetype(makeRng(seed, 'params')); } export function paramsFromSeed(seed) { const rng = makeRng(seed, 'params'); const arch = pickArchetype(rng); const r = (lo, hi) => lo + (hi - lo) * rng(); const ri = (lo, hi) => Math.round(r(lo, hi)); const chance = (p) => rng() < p; // base (archival) defaults const p = { seed, primaries: ri(12, 24), burst: r(0.55, 0.92), vdecay: ri(2, 6), cosmics: ri(4, 10), sweepers: ri(2, 7), bfield: r(0.8, 1.6), eloss: r(0.45, 0.75), pspread: r(0.6, 0.85), deltaRate: r(0.6, 0.95), deltaTight: r(0.6, 1.0), shock: true, diskBubbles: false, shockIntensity: r(0.6, 0.9), shockSize: r(0.26, 0.4), shockStriations: r(0.45, 0.85), shockY: r(0.4, 0.6), shockX: r(-0.12, 0.12), shockStain: Math.pow(rng(), 1.5), // skew cleaner, allow grime diskSoften: r(0.4, 1.1), // gaussian softening of disk edges instrument: r(0.25, 0.6), bgEvents: ri(3, 8), bgIntensity: r(0.35, 0.5), depth: r(0.4, 0.8), aging: r(0.4, 0.8), // chamber depth + exposure age density: r(1.0, 1.35), size: r(0.9, 1.15), bubbleSoft: r(0.25, 0.45), bloom: r(0.4, 0.65), mottle: r(0.4, 0.7), grain: r(0.4, 0.6), vign: r(0.35, 0.6), artifacts: r(0.4, 0.8), showFiducials: chance(0.85), showBoundary: chance(0.7), showHeader: chance(0.9), invert: true, palette: 'mono', saturation: 1.0, // canonical look is B&W; colour is opt-in hueShift: 0, hueCycles: 3, diskSpectrum: 0, halo: 0, haloHue: 0.55, traceHue: 0, diskHue: 0.06, diskSat: 0.82, // magentarise: trace-family rotation + disk accent hue/sat paperTone: 'cream', toneStrength: 1.0, paperBright: 1.0, glow: 0.5, annotate: 0, reseau: 0, filmEdge: false, splice: false, // media & hand layer (opt-in) }; if (arch === 'dense') { Object.assign(p, { primaries: ri(26, 40), burst: r(0.85, 1.0), vdecay: ri(4, 8), cosmics: ri(8, 14), sweepers: ri(5, 9), bfield: r(1.4, 2.4), deltaRate: r(0.85, 1.0), deltaTight: r(0.8, 1.2), bgEvents: ri(7, 11), density: r(1.25, 1.5), shockIntensity: r(0.6, 0.85), }); } else if (arch === 'cosmic') { Object.assign(p, { primaries: ri(4, 10), burst: r(0.25, 0.55), vdecay: ri(0, 2), cosmics: ri(10, 16), sweepers: ri(7, 11), bfield: r(0.6, 1.1), pspread: r(0.8, 0.95), deltaRate: r(0.35, 0.6), instrument: r(0.45, 0.7), shock: chance(0.4), shockIntensity: r(0.3, 0.7), bgEvents: ri(2, 5), density: r(0.85, 1.1), }); } else if (arch === 'clean') { Object.assign(p, { primaries: ri(8, 14), burst: r(0.4, 0.65), vdecay: ri(1, 3), cosmics: ri(1, 4), sweepers: ri(1, 4), deltaRate: r(0.35, 0.6), deltaTight: r(0.55, 0.8), shock: chance(0.6), shockIntensity: r(0.3, 0.7), shockStain: Math.pow(rng(), 3), // mostly clean disk instrument: r(0.1, 0.3), bgEvents: ri(0, 3), bgIntensity: r(0.2, 0.35), bloom: r(0.25, 0.45), mottle: r(0.15, 0.4), grain: r(0.12, 0.35), vign: r(0.2, 0.4), artifacts: r(0.1, 0.4), }); } else if (arch === 'negative') { Object.assign(p, { invert: false, vign: r(0.4, 0.6), shockStain: Math.pow(rng(), 1.3) }); } p.archetype = arch; return p; }