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

256 lines
14 KiB
JavaScript
Raw Permalink 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-backlit2.mjs — SUN-ABOVE SCALE STUDIES.
One frame (plate Nº 217, exposure 19:04, MESON-5113), colours
and composition held constant; each render plays with the
RELATIVE SCALE of the marks:
· traces — bubble size / density of the particle ink
· waves — row count (few = broad swells, many = fine ripple)
· depth — overlap: wave amplitude relative to row gap
· line — stroke weight of the wave rows
No hand. Same sun (hollow heart), same sea hue, same aging.
Usage: node tools/refined-backlit2.mjs [size]
→ fable/REFINED_Backlit2/
============================================================ */
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 { splitTracksAtWaterline } from '../src/compose/waterline.js';
import { makeRng } from '../src/rng.js';
const SIZE = +(process.argv[2] || 1500);
const OUT = 'fable/REFINED_Backlit2';
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 ?? 46, overlap = sea.overlap ?? 1.9, sm = sea.strokeMul ?? 1;
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: 1.4, 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(0.8 * 0.8, 0.8, 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 ?? 46, overlap = sea.overlap ?? 1.9;
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 ---- */
function bcAssembleParams(over = {}) {
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: 0.8,
shockStriations: 1.0, diskHollow: 0.85,
depth: 0.3, aging: 0.2,
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);
return p;
}
function renderVariant(v) {
const p = bcAssembleParams(v.traces || {});
const scene = generateScene(p);
const { above, below } = splitTracksAtWaterline(scene.tracks, YH, { kink: 0.016, compress: 0.95 });
const bare = { ...scene, instrument: null, artifacts: null, media: null };
const sea = v.sea || {};
const wamp = 0.003;
const belowWavy = below.map(t => ({
...t,
pts: t.pts.map(pt => ({ ...pt, y: pt.y + wamp * Math.sin(pt.x * 34 + pt.y * 18) })),
}));
const trackSVG = (tracks) => renderSVG({ ...bare, tracks, shock: null }, { ...p, emit: ['bubble'] }, SIZE);
const diskSVG = () => renderSVG({ ...bare, tracks: [], shock: scene.shock }, { ...p, emit: ['disk'] }, SIZE);
const aboveImg = dataUri(trackSVG(above));
const belowImg = dataUri(trackSVG(belowWavy));
const diskImg = dataUri(diskSVG());
const sample = buildAttraction(belowWavy, scene.shock, false);
const sheets = deck(sea, warpFor(sample, 7 * u, sea)).map(dataUri);
const film = dataUri(filmSVG(FILM));
const grain = dataUri(grainSVG({ amount: 0.42 }));
const aging = dataUri(agingSVG({ seed: 5, scratches: 6, dust: 0.5, foxing: 0.55 }));
const hpx = (HORIZON * SIZE).toFixed(1);
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
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="subT" x="-6%" y="-6%" width="112%" height="112%">
<feColorMatrix type="matrix" values="0.78 0 0 0 0.010 0 0.94 0 0 0.022 0 0 0.96 0 0.022 0 0 0 0.85 0"/>
<feGaussianBlur stdDeviation="${(1.0 * 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>
<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)}</g>
${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')}
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)"')}</g>
${IMG(belowImg, 'filter="url(#subT)"')}
<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(aboveImg)}
${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.
============================================================ */
const VARIANTS = [
{ name: '00_reference', note: 'baseline (traces 1.0 · rows 46 · overlap 1.9 · line ×1)',
traces: {}, sea: {} },
{ name: '01_traces-bold', note: 'evidence enlarged (bubble size 1.6, density 0.9)',
traces: { size: 1.6, density: 0.9 } },
{ name: '02_traces-fine', note: 'evidence miniaturised (size 0.62, density 1.3)',
traces: { size: 0.62, density: 1.3 } },
{ name: '03_sea-broad', note: 'few broad swells (rows 26)',
sea: { rows: 26 } },
{ name: '04_sea-fine', note: 'fine ripple (rows 78, line ×0.75)',
sea: { rows: 78, strokeMul: 0.75 } },
{ name: '05_sea-deep', note: 'deep undulation (overlap 3.1)',
sea: { overlap: 3.1 } },
{ name: '06_sea-etched', note: 'heavy wave line (line ×2.3)',
sea: { strokeMul: 2.3 } },
{ name: '07_giant-evidence', note: 'large ink over fine calm water (size 1.7 · rows 78 · line ×0.7 · overlap 1.6)',
traces: { size: 1.7, density: 0.85 }, sea: { rows: 78, strokeMul: 0.7, overlap: 1.6 } },
{ name: '08_woodcut', note: 'small ink in a heavy carved sea (size 0.7 · rows 24 · line ×2.4 · overlap 2.6)',
traces: { size: 0.7, density: 1.3 }, sea: { rows: 24, strokeMul: 2.4, overlap: 2.6 } },
];
console.log(`SUN-ABOVE SCALE STUDIES — ${VARIANTS.length} renders → ${OUT}/`);
VARIANTS.forEach(renderVariant);
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit2 · sun-above scale 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 — scale studies (colours &amp; composition held)</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`);