Files
bubblechambersimart/tools/refined-backlit3.mjs
2026-07-19 13:37:10 -04:00

261 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
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 `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#fog)"/></svg>`;
}
function grainSVG(o = {}) {
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
const t = tone.map(v => (v / 255).toFixed(3));
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#g)"/></svg>`;
}
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 += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
}
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
<defs>
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
</defs>
<rect width="${SIZE}" height="${SIZE}" filter="url(#fox)"/>
<rect width="${SIZE}" height="${SIZE}" filter="url(#dust)"/>
${lines}</svg>`;
}
/* ---- 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 = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
const diskFade = v.diskFade ?? 0.5; // the sun receded into the back: a dim haze the evidence crosses
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
<defs>
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
</filter>
<filter id="diskback" x="-12%" y="-12%" width="124%" height="124%"><feGaussianBlur stdDeviation="${(2.2 * u).toFixed(2)}"/></filter>
<clipPath id="skyc"><rect x="0" y="0" width="${SIZE}" height="${hpx}"/></clipPath>
<clipPath id="seac"><rect x="0" y="${hpx}" width="${SIZE}" height="${(SIZE - HORIZON * SIZE).toFixed(1)}"/></clipPath>
</defs>
<rect width="${SIZE}" height="${SIZE}" fill="${BASE}"/>
${IMG(film, 'opacity="0.6"')}
<g clip-path="url(#skyc)">${IMG(diskImg, `filter="url(#diskback)" opacity="${diskFade.toFixed(2)}"`)}</g>
${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')}
<g clip-path="url(#seac)">${IMG(diskImg, `filter="url(#subD)" opacity="${(0.7 + 0.3 * diskFade).toFixed(2)}"`)}</g>
<rect x="0" y="${hpx}" width="${SIZE}" height="${SIZE}" fill="rgb(96,138,128)" opacity="0.07"/>
${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"`)}
</svg>`;
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`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit3 · bold large-trace studies</title>
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
<h1>Sun-above — bold, large traces on top · waves varied · sun faded back</h1>
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name.replace(/^\d+_/, '')}${v.note}</figcaption></figure>`).join('')}</div>
</body></html>`);
console.log(`gallery -> ${OUT}/index.html`);