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

227 lines
13 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_Backlit7/
============================================================ */
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_Backlit7';
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
bubbleSoft: 0.7, // softer, wider rings (was 0.3)
bubbleMerge: 1, // crowded bubbles fuse to flat union (uniform lace, backlit-even)
bubbleOpacity: +(process.env.BUBOP ?? 0.9), // = uniform target density / backlit brightness
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(v.seed ?? `${SEED}#${String(i).padStart(2, '0')}`,
v.bubOp != null ? { bubbleOpacity: v.bubOp } : {});
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: v.agingSeed ?? (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} bubbleOpacity=${(v.bubOp ?? 0.9).toFixed(2)} ${v.mirror ? 'mirrored' : ''}`);
}
// MERGE · UNIFORM LACE — one composition (the liked mirror event, fixed seed), the
// bubble-opacity slider stepped 0.50 → 0.95. Only the ink density changes.
const SWEEP_SEED = `${SEED}#03`;
// MERGE — uniform-lace target sweep. bubOp is now the CAPPED union density
// (dense knot = same density as a lone bubble), i.e. the backlit brightness.
const VARIANTS = [0.35, 0.45, 0.55, 0.65, 0.75, 0.85].map(op => ({
name: `merge-${Math.round(op * 100)}`, mirror: true, seed: SWEEP_SEED, bubOp: op, agingSeed: 8,
}));
const _UNUSED = [
{ 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 · MERGE · UNIFORM LACE — ${W}×${H} (φ) — ${VARIANTS.length} renders → ${OUT}/`);
VARIANTS.forEach(renderVariant);
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit6 · bubble-opacity sweep</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: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 — merge · uniform-lace target sweep (${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`);