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

214 lines
12 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-backlit4.mjs — ONE SUN SETTING, GOLDEN.
The whole composition is built on φ (1.6180…):
· CANVAS a golden rectangle, W = φ·H (landscape)
· HORIZON the waterline splits the height 1/φ : 1 → y = W·0
(sky = 0.382 H · sea = 0.618 H, ratio 1/φ)
· DISK the sun's centre sits on the LEFT golden line
(0.382 W) at the golden division of the sky; its
radius is 0.382 of the sky band
· TRACES the event vertex sits on the RIGHT golden line
(0.618 W) where it meets the horizon
· WAVES amplitude:gap = φ · near:far width = φ · the three
stacked sheets stepped in opacity by 1/φ
Each iteration re-rolls only the bubble-trace seed; the golden
armature is identical. Grain + distressing stay on.
Usage: node tools/refined-backlit4.mjs [width]
→ fable/REFINED_Backlit5/
============================================================ */
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 PHI = (1 + Math.sqrt(5)) / 2; // 1.6180339887…
const IP = 1 / PHI; // 0.6180… (long section)
const IP2 = 1 / (PHI * PHI); // 0.3820… (short section)
const W = +(process.argv[2] || 1001);
const H = Math.round(W * PHI); // golden rectangle, PORTRAIT (H = φ·W)
const OUT = 'fable/REFINED_Backlit5';
mkdirSync(OUT, { recursive: true });
const u = Math.min(W, H) / 1000; // stroke unit keyed to the short side (width)
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
/* ---- golden armature (px) ---- */
const SEED = 'MESON-5113';
const BASE = 'rgb(227,220,200)';
const HORIZON_F = IP2; // waterline at 0.382 H → sky:sea = 1/φ
const HY = HORIZON_F * H;
const SKY_H = HY; // sky band height
const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02); // renderSVG's isotropic scene→px scale
const XL = IP2 * W; // left golden line 0.382 W
const XR = IP * W; // right golden line 0.618 W
const SUN = { x: XL, y: IP * SKY_H }; // sun centre: left line, 0.618 down the sky
const SUN_R = IP2 * SKY_H; // sun radius = 0.382 of the sky band
const VERTEX = { x: XR, y: HY }; // event vertex: right line ∩ horizon
/* ---- film / grain / aging, now golden-canvas sized ---- */
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="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<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="${W}" height="${H}" 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="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<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="${W}" height="${H}" filter="url(#g)"/></svg>`;
}
function agingSVG(o = {}) {
const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, 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() * W, y0 = rng() * H * 0.4, len = (0.3 + rng() * 0.6) * H;
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="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<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="${W}" height="${H}" filter="url(#fox)"/>
<rect width="${W}" height="${H}" filter="url(#dust)"/>
${lines}</svg>`;
}
/* ---- the golden sea: three sheets, EQUAL line count, φ-ratioed waves ---- */
function deck(mirror) {
const base = {
mode: 'plate', width: W, height: H, horizon: HORIZON_F,
wFar: 0.5, wNear: 0.5 * PHI, // near:far width = φ
overlap: PHI, // wave amplitude : row gap = φ
rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
chaos: 0.82, blips: 1.5,
};
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(H, { ...base, salt: (mirror ? 'mfield' : 'field') + i,
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
chaos: lerp(0.82 * 0.8, 0.82, t),
strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
});
}
/* ---- the event: golden-placed disk + traces ---- */
function bcAssembleParams(seed, 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: 1.4,
shockStriations: 1.0, diskHollow: 1.0,
depth: 0.3, aging: 0.28,
// event + shock both at scene origin; each is PLACED on the page by origin
eventX: 0, eventY: 0, shockX: 0, shockY: 0,
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
bgEvents: 2,
shockSize: SUN_R / S_SCALE, // radius = 0.382 of the sky band
shockIntensity: 0.8,
size: 1.6, // larger bubbles
bubbleHollow: 1, bubbleWeight: 0.7, // delicate hollow bubbles: pale core, saturated rim
canvasW: W, canvasH: H,
}, over, { seed });
return p;
}
const TRACE_ZOOM = 2.1; // the evidence grows past the frame
function renderVariant(v, i) {
const sun = v.mirror ? { x: XR, y: SUN.y } : SUN;
const vtx = v.mirror ? { x: XL, y: HY } : VERTEX;
const p = bcAssembleParams(`${SEED}#${String(i).padStart(2, '0')}`);
const scene = generateScene(p);
const bare = { ...scene, instrument: null, artifacts: null, media: null };
const tracksImg = dataUri(renderSVG(
{ ...bare, tracks: scene.tracks, shock: null },
{ ...p, emit: ['bubble'], originX: vtx.x, originY: vtx.y, sceneZoom: TRACE_ZOOM }, W));
const diskImg = dataUri(renderSVG(
{ ...bare, tracks: [], shock: scene.shock },
{ ...p, emit: ['disk'], originX: sun.x, originY: sun.y }, W));
const sheets = deck(v.mirror).map(dataUri);
const film = dataUri(filmSVG({ seed: 41, density: 0.6 }));
const grain = dataUri(grainSVG({ amount: 0.52 }));
const aging = dataUri(agingSVG({ seed: 5 + i }));
const hpx = HY.toFixed(1);
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${W}" height="${H}" href="${href}" ${attrs}/>`;
// three sea planes stepped in opacity by 1/φ (front → back)
const OP = [0.95 * IP2, 0.95 * IP, 0.95];
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<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="${W}" height="${hpx}"/></clipPath>
<clipPath id="seac"><rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}"/></clipPath>
</defs>
<rect width="${W}" height="${H}" fill="${BASE}"/>
${IMG(film, 'opacity="0.6"')}
<g clip-path="url(#skyc)">${IMG(diskImg, 'filter="url(#diskback)" opacity="0.5"')}</g>
${IMG(sheets[0], `filter="url(#b3)" opacity="${OP[0].toFixed(2)}"`)}
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)" opacity="0.85"')}</g>
<rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}" fill="rgb(96,138,128)" opacity="0.07"/>
${IMG(sheets[1], `filter="url(#b2)" opacity="${OP[1].toFixed(2)}"`)}
${IMG(sheets[2], `opacity="${OP[2].toFixed(2)}"`)}
${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} sun(${sun.x.toFixed(0)},${sun.y.toFixed(0)}) vtx(${vtx.x.toFixed(0)},${vtx.y.toFixed(0)}) ${v.mirror ? 'mirrored' : ''}`);
}
const VARIANTS = [
{ name: '00_golden', mirror: false },
{ name: '01_golden', mirror: false },
{ name: '02_golden', mirror: false },
{ name: '03_golden-mirror', mirror: true },
{ name: '04_golden-mirror', mirror: true },
{ name: '05_golden-mirror', mirror: true },
];
console.log(`ONE SUN SETTING · GOLDEN · DELICATE BUBBLES — ${W}×${H} (φ) — ${VARIANTS.length} renders → ${OUT}/`);
VARIANTS.forEach(renderVariant);
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit5 · golden · delicate bubbles</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(2,1fr);gap:14px;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>One sun setting — golden (${W}×${H}, φ)</h1>
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name}</figcaption></figure>`).join('')}</div>
</body></html>`);
console.log(`gallery -> ${OUT}/index.html`);