224 lines
12 KiB
JavaScript
224 lines
12 KiB
JavaScript
|
|
/* ============================================================
|
|||
|
|
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_Backlit6/
|
|||
|
|
============================================================ */
|
|||
|
|
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_Backlit6';
|
|||
|
|
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)
|
|||
|
|
bubbleOpacity: +(process.env.BUBOP ?? 0.9), // master bubble opacity slider — start at 90%
|
|||
|
|
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' : ''}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// OPACITY SWEEP — 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`;
|
|||
|
|
const VARIANTS = [0.20, 0.32, 0.44, 0.56, 0.68, 0.80].map(op => ({
|
|||
|
|
name: `op-${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 · OPACITY SWEEP — ${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 — bubble-opacity sweep 20→80% (${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`);
|