/* ============================================================ refined-backlit3.mjs — SUN-ABOVE, BOLDER · LARGE TRACES. One frame (plate Nº 217, exposure 19:04, MESON-5113). This generation commits to LARGER bubble traces throughout and a generally BOLDER read; each render plays the waves differently (count / depth / line weight / chaos). The pressure disc is FADED INTO THE BACK — its heart fully hollowed and the whole sun dimmed + pushed behind the sea, a ghost the tracks cross. Film grain + distressing (foxing / dust / scratches) stay ON. Usage: node tools/refined-backlit3.mjs [size] → fable/REFINED_Backlit3/ ============================================================ */ import { writeFileSync, mkdirSync } from 'node:fs'; import { carpetSVG } from '../src/qft/carpet.js'; import { generateScene } from '../src/scene/scene.js'; import { renderSVG } from '../src/render/svgVector.js'; import { paramsFromSeed as bcParams } from '../src/scene/params.js'; import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js'; import { makeRng } from '../src/rng.js'; const SIZE = +(process.argv[2] || 1500); const OUT = 'fable/REFINED_Backlit3'; mkdirSync(OUT, { recursive: true }); const u = SIZE / 1000; const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64'); /* ---- the world constants: identical to One Sun Setting ---- */ const SEED = 'MESON-5113'; const BASE = 'rgb(227,220,200)'; const HORIZON = 0.37; const YH = HORIZON * 2 - 1; const FILM = { seed: 41, density: 0.6 }; /* ---- film / grain / aging (unchanged from refined-backlit) ---- */ function filmSVG(o = {}) { const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o; const t = tone.map(v => (v / 255).toFixed(3)); return ` `; } function grainSVG(o = {}) { const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o; const t = tone.map(v => (v / 255).toFixed(3)); return ` `; } function agingSVG(o = {}) { const { seed = 5, scratches = 6, dust = 0.5, foxing = 0.55, tone = [60, 48, 34] } = o; const t = tone.map(v => (v / 255).toFixed(3)); const rng = makeRng(SEED, 'aging' + seed); let lines = ''; for (let i = 0; i < scratches; i++) { const x = rng() * SIZE, y0 = rng() * SIZE * 0.4, len = (0.3 + rng() * 0.6) * SIZE; const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len; lines += ``; } return ` ${lines}`; } /* ---- the sea deck, scale-parameterised ---- rows: wave count · overlap: amplitude vs row gap (depth) · strokeMul: line weight multiplier on the per-sheet stroke table */ function deck(sea, warpFn) { const rows = sea.rows ?? 40, overlap = sea.overlap ?? 2.2, sm = sea.strokeMul ?? 1.35; // v3: bolder default line const chaosMax = sea.chaos ?? 0.8, blips = sea.blips ?? 1.4; const base = { mode: 'plate', rows, horizon: HORIZON, wFar: 0.58, wNear: 0.7, overlap, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56, blips, warpFn }; const lerp = (a, b, t) => a + (b - a) * t; const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }]; return [0, 1, 2].map(i => { const t = i / 2; return carpetSVG(SIZE, { ...base, salt: 'field' + i, hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035, chaos: lerp(chaosMax * 0.8, chaosMax, t), strokeNear: strokes[i].near * sm, strokeFar: strokes[i].far * sm }); }); } /* ---- iron filings (identical logic, geometry follows the sea's scale) ---- */ function buildAttraction(below, shock, sunSubmerged) { const S = (v) => SIZE / 2 + v * (SIZE / 2) * 0.98; const pts = []; for (const t of below) for (let i = 0; i < t.pts.length; i += 5) pts.push({ x: S(t.pts[i].x), y: S(t.pts[i].y), w: 1 }); if (sunSubmerged) pts.push({ x: S(shock.x), y: S(shock.y), w: 12 }); const G = 72, cell = SIZE / G, sig = 0.045 * SIZE; const fy = new Float32Array(G * G); for (let gy = 0; gy < G; gy++) for (let gx = 0; gx < G; gx++) { const x = (gx + 0.5) * cell, y = (gy + 0.5) * cell; let dy = 0, wsum = 0; for (const p of pts) { const dx0 = p.x - x, dy0 = p.y - y; const k = p.w * Math.exp(-(dx0 * dx0 + dy0 * dy0) / (2 * sig * sig)); dy += k * Math.max(-sig, Math.min(sig, dy0)); wsum += k; } fy[gy * G + gx] = wsum > 0.02 ? (dy / (wsum + 1.2)) / sig : 0; } return (x, y) => { const fx0 = Math.min(G - 1.001, Math.max(0, x / cell - 0.5)); const fy0 = Math.min(G - 1.001, Math.max(0, y / cell - 0.5)); const ix = Math.floor(fx0), iy = Math.floor(fy0), ax = fx0 - ix, ay = fy0 - iy; const v00 = fy[iy * G + ix], v10 = fy[iy * G + ix + 1]; const v01 = fy[(iy + 1) * G + ix], v11 = fy[(iy + 1) * G + ix + 1]; return (v00 * (1 - ax) + v10 * ax) * (1 - ay) + (v01 * (1 - ax) + v11 * ax) * ay; }; } function warpFor(sample, pullPx, sea) { const rows = sea.rows ?? 40, overlap = sea.overlap ?? 2.2; const hY = HORIZON * SIZE, bottom = 0.99 * SIZE; return (t, d) => { const half = (0.58 + 0.12 * d) * SIZE; const x = SIZE / 2 - half + t * 2 * half; const y = hY + (bottom - hY) * Math.pow(d, 1.7); const localGap = (bottom - hY) * 1.7 * Math.pow(Math.max(d, 0.02), 0.7) / (rows - 1); const amp = Math.max(2 * u, overlap * localGap); const n = sample(x, y) * pullPx / amp; return Math.max(-0.9, Math.min(0.9, n)); }; } /* ---- the event: sun-above config, colours fixed ---- */ // seed drives ONLY the stochastic realization; the MESON-5113 parameter // fingerprint (event position, counts, physics, sun) is held constant, so a // new seed re-rolls the bubble layout without changing the piece's character. function bcAssembleParams(over = {}, seed = SEED) { const p = { ...FIXED, ...bcParams(SEED) }; for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value; for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value; Object.assign(p, { invert: true, transparentPaper: true, vign: 0, showHeader: false, showBoundary: false, showFiducials: false, annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0, palette: 'magbeta', saturation: 1.0, diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4, // softer → the disc reads as a haze behind, not an object shockStriations: 1.0, diskHollow: 1.0, // heart fully open: the sun is a ring of light, not a plug depth: 0.3, aging: 0.28, // distressing up a touch (bolder document) eventX: -0.14, eventY: -0.37, sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8, bgEvents: 2, shockX: -0.55, shockY: -0.72, shockSize: 0.17, shockIntensity: 0.8, }, over, { seed }); return p; } /* ---- the SUN and the WAVE-FIELD are fixed across the whole set (base seed), so the sea is byte-identical per iteration and only the ink is re-rolled ---- */ const baseP = bcAssembleParams(); const baseScene = generateScene(baseP); const baseBare = { ...baseScene, instrument: null, artifacts: null, media: null }; const diskImg = dataUri(renderSVG({ ...baseBare, tracks: [], shock: baseScene.shock }, { ...baseP, emit: ['disk'] }, SIZE)); const baseBelow = baseScene.tracks .map(t => ({ ...t, pts: t.pts.filter(pt => pt.y > YH) })) .filter(t => t.pts.length); const fieldSample = buildAttraction(baseBelow, baseScene.shock, false); // waves attend to the base event only function renderVariant(v, i) { // unique bubble-trace seed per iteration; waves + sun stay put const p = bcAssembleParams(v.traces || {}, `${SEED}#${String(i).padStart(2, '0')}`); const scene = generateScene(p); const bare = { ...scene, instrument: null, artifacts: null, media: null }; const sea = v.sea || {}; // REVERTED 2026-07-17: no waterline split, no refraction — the traces are ONE // object drawn cleanly ON TOP of the sea; the water neither fronts nor tints them. const tracksImg = dataUri(renderSVG({ ...bare, tracks: scene.tracks, shock: null }, { ...p, emit: ['bubble'] }, SIZE)); const sheets = deck(sea, warpFor(fieldSample, 7 * u, sea)).map(dataUri); const film = dataUri(filmSVG(FILM)); const grain = dataUri(grainSVG({ amount: 0.52 })); // bolder film grain const aging = dataUri(agingSVG({ seed: 5, scratches: 8, dust: 0.6, foxing: 0.62 })); // heavier distressing const hpx = (HORIZON * SIZE).toFixed(1); const IMG = (href, attrs = '') => ``; const diskFade = v.diskFade ?? 0.5; // the sun receded into the back: a dim haze the evidence crosses const svg = ` ${IMG(film, 'opacity="0.6"')} ${IMG(diskImg, `filter="url(#diskback)" opacity="${diskFade.toFixed(2)}"`)} ${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')} ${IMG(diskImg, `filter="url(#subD)" opacity="${(0.7 + 0.3 * diskFade).toFixed(2)}"`)} ${IMG(sheets[1], `filter="url(#b2)" opacity="0.72"`)} ${IMG(sheets[2], 'opacity="0.95"')} ${IMG(tracksImg)} ${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')} ${IMG(grain, `opacity="0.45" style="mix-blend-mode:multiply"`)} `; writeFileSync(`${OUT}/${v.name}.svg`, svg); console.log(` ${v.name} ${v.note}`); } /* ============================================================ THE STUDIES — single axes first, then two deliberate chords. ============================================================ */ // Every variant carries LARGE bubble traces (evidence on top of the sea) and a // generally bolder read; the sun is faded into the back. Each render moves the // WAVES somewhere different — count, depth, line weight, chaos. const VARIANTS = [ { name: '00_bold-base', note: 'large ink · sea baseline (rows 40 · overlap 2.2 · line ×1.35)', traces: { size: 1.5, density: 1.0 }, sea: {}, diskFade: 0.5 }, { name: '01_broad-swell', note: 'few broad swells (rows 24 · overlap 2.6)', traces: { size: 1.55, density: 1.0 }, sea: { rows: 24, overlap: 2.6 }, diskFade: 0.5 }, { name: '02_fine-ripple', note: 'dense fine ripple (rows 82 · line ×0.9)', traces: { size: 1.5, density: 1.0 }, sea: { rows: 82, strokeMul: 0.9 }, diskFade: 0.5 }, { name: '03_deep-heave', note: 'deep undulation (overlap 3.4)', traces: { size: 1.55, density: 1.0 }, sea: { overlap: 3.4 }, diskFade: 0.45 }, { name: '04_etched-sea', note: 'heavy carved wave line (line ×2.6)', traces: { size: 1.5, density: 1.05 }, sea: { strokeMul: 2.6 }, diskFade: 0.5 }, { name: '05_glassy-calm', note: 'shallow glassy water (overlap 1.3 · rows 52)', traces: { size: 1.6, density: 0.95 }, sea: { overlap: 1.3, rows: 52 }, diskFade: 0.55 }, { name: '06_turbulent', note: 'turbulent, blip-rich sea (chaos 1.0 · blips 2.2 · overlap 2.8)', traces: { size: 1.5, density: 1.05 }, sea: { chaos: 1.0, blips: 2.2, overlap: 2.8 }, diskFade: 0.45 }, { name: '07_woodcut', note: 'heavy carved swell (rows 26 · line ×2.8 · overlap 3.0)', traces: { size: 1.55, density: 1.0 }, sea: { rows: 26, strokeMul: 2.8, overlap: 3.0 }, diskFade: 0.5 }, { name: '08_giant-ink-calm', note: 'giant evidence over quiet fine water (size 1.9 · rows 78 · line ×0.8 · overlap 1.5)', traces: { size: 1.9, density: 0.9 }, sea: { rows: 78, strokeMul: 0.8, overlap: 1.5 }, diskFade: 0.55 }, { name: '09_max-bold', note: 'everything loud (size 1.8 · rows 32 · line ×2.4 · overlap 3.0 · chaos 0.95)', traces: { size: 1.8, density: 1.1 }, sea: { rows: 32, strokeMul: 2.4, overlap: 3.0, chaos: 0.95, blips: 1.8 }, diskFade: 0.4 }, ]; console.log(`SUN-ABOVE · BOLD/LARGE-TRACE STUDIES — ${VARIANTS.length} renders → ${OUT}/`); VARIANTS.forEach(renderVariant); writeFileSync(`${OUT}/index.html`, `REFINED_Backlit3 · bold large-trace studies

Sun-above — bold, large traces on top · waves varied · sun faded back

${VARIANTS.map(v => `
${v.name.replace(/^\d+_/, '')} — ${v.note}
`).join('')}
`); console.log(`gallery -> ${OUT}/index.html`);