Files
bubblechambersimart/tools/layering.mjs

141 lines
9.1 KiB
JavaScript

/* ============================================================
layering.mjs — the assembled piece: FILM/diffusion (back) + a QFT
vacuum-carpet DECK (middle, 3 spaced plate sheets) + a BUBBLE-CHAMBER
event (front), composited as one image (and the literal plexi stack).
Produces variations into output/layering/.
Usage: node tools/layering.mjs [size]
============================================================ */
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';
const SIZE = +(process.argv[2] || 1500);
const OUT = 'output/layering';
mkdirSync(OUT, { recursive: true });
const u = SIZE / 1000;
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
// ---- FILM / diffusion: milky clouds (light, soft) the light diffuses through ----
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>`;
}
// ---- film GRAIN veil (fine, dark, low opacity) on top ----
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>`;
}
// ---- bubble-chamber event ----
function bcSVG(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;
p.invert = true; p.showHeader = false;
Object.assign(p, over);
return renderSVG(generateScene(p), p, SIZE);
}
// build a 3-sheet carpet deck for a variation (hue gradient + chaos rising to front)
function deck(c) {
const base = { mode: 'plate', rows: 46, horizon: c.horizon ?? 0.37, wFar: 0.58, wNear: 0.7,
overlap: c.overlap ?? 1.7, mound: c.mound ?? 0.35, sat: c.sat ?? 0.58, lightNear: 0.33, lightFar: 0.56, blips: c.blips ?? 1.0 };
const lerp = (a, b, t) => a + (b - a) * t;
return [0, 1, 2].map(i => {
const t = i / 2; // 0 back → 1 front
return carpetSVG(SIZE, { ...base, salt: 'field' + i,
hue: lerp(c.hueBack, c.hueFront, t), hue2: lerp(c.hueBack, c.hueFront, t) + 0.035,
chaos: lerp((c.chaos ?? 0.6) * 0.8, c.chaos ?? 0.6, t) });
});
}
function compose(v) {
const film = dataUri(filmSVG(v.film));
const sheets = deck(v.carpet).map(dataUri);
const bc = dataUri(bcSVG(v.bcSeed, v.bcOver));
const grain = dataUri(grainSVG(v.grain));
const base = v.base || 'rgb(226,219,199)';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
<defs>
<filter id="b3" x="-5%" y="-5%" width="110%" height="110%"><feGaussianBlur stdDeviation="${(2.4 * u).toFixed(2)}"/></filter>
<filter id="b2" x="-5%" y="-5%" width="110%" height="110%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
</defs>
<rect width="${SIZE}" height="${SIZE}" fill="${base}"/>
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${film}" opacity="0.6"/>
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${sheets[0]}" filter="url(#b3)" opacity="0.5"/>
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${sheets[1]}" filter="url(#b2)" opacity="0.72"/>
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${sheets[2]}" opacity="0.95"/>
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${bc}" style="mix-blend-mode:multiply"/>
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${grain}" opacity="${v.grainOpacity ?? 0.45}" style="mix-blend-mode:multiply"/>
</svg>`;
writeFileSync(`${OUT}/${v.name}.svg`, svg);
console.log(` ${v.name} (bc=${v.bcSeed})`);
}
// ============================================================
// Variations — film + carpet + event, ranging mood & "loudness".
// Guiding thesis: calm sea, loud event.
// ============================================================
const VARIATIONS = [
{ name: '01_mono-calm-sea', base: 'rgb(228,221,201)',
film: { seed: 3, density: 0.5, tone: [236, 228, 208] }, grain: { amount: 0.4 },
carpet: { hueBack: 0.58, hueFront: 0.50, chaos: 0.35, blips: 0.7, mound: 0.32 },
bcSeed: 'LAMBDA-2648', bcOver: { palette: 'mono' } },
{ name: '02_magenta-over-teal', base: 'rgb(228,221,201)',
film: { seed: 8, density: 0.5 }, grain: { amount: 0.4 },
carpet: { hueBack: 0.54, hueFront: 0.47, chaos: 0.45, blips: 1.0, mound: 0.35 },
bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.05 } },
{ name: '03_quiet-vast', base: 'rgb(230,224,206)',
film: { seed: 12, density: 0.42, tone: [238, 231, 212] }, grain: { amount: 0.32 },
carpet: { hueBack: 0.57, hueFront: 0.52, chaos: 0.22, blips: 0.5, mound: 0.28, overlap: 1.5 },
bcSeed: 'NUCLEON-2131', bcOver: { palette: 'mono', primaries: 7, burst: 0.4, cosmics: 2, deltaRate: 0.45 } },
{ name: '04_kind-verdigris', base: 'rgb(226,221,205)',
film: { seed: 21, density: 0.5 }, grain: { amount: 0.4 },
carpet: { hueBack: 0.45, hueFront: 0.40, chaos: 0.5, blips: 1.1, sat: 0.42, mound: 0.35 },
bcSeed: 'HYPERON-8444', bcOver: { palette: 'kind', saturation: 1.0 } },
{ name: '05_ember-warm', base: 'rgb(230,222,202)',
film: { seed: 30, density: 0.52, tone: [238, 226, 204] }, grain: { amount: 0.42 },
carpet: { hueBack: 0.10, hueFront: 0.07, chaos: 0.5, blips: 1.0, sat: 0.5, mound: 0.34 },
bcSeed: 'CASCADE-2755', bcOver: { palette: 'kindrise', saturation: 1.0 } },
{ name: '06_seethe-bold', base: 'rgb(227,220,200)',
film: { seed: 41, density: 0.55 }, grain: { amount: 0.46 },
carpet: { hueBack: 0.55, hueFront: 0.47, chaos: 0.8, blips: 1.4, mound: 0.4, overlap: 1.9 },
bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.08 } },
];
console.log(`Layered piece — ${VARIATIONS.length} variations → ${OUT}/`);
for (const v of VARIATIONS) compose(v);
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Layered piece · film + QFT carpet + bubble chamber</title>
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,Menlo,monospace;padding:26px;max-width:1900px}
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#a5d4c9}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:20px;margin-top:18px}
figure{margin:0;background:#fff;border:1px solid #262626;overflow:hidden}img{width:100%;display:block}
figcaption{padding:8px 10px;color:#e8e4d8;background:#111}small{color:#777;display:block}
.notes{color:#999;background:#0e1816;padding:14px 18px;border-left:3px solid #a5d4c9;margin:18px 0}</style></head><body>
<h1>Layered piece — film · QFT vacuum carpet · bubble-chamber event</h1>
<div class="notes">back→front: cream ground · milky film/diffusion · 3 spaced QFT carpet sheets (back two blurred = air-gap depth of field) · bubble-chamber event (multiply) · film grain. The literal plexi stack, previewed as one image.</div>
<div class=grid>${VARIATIONS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}<small>${v.bcSeed} · ${v.bcOver?.palette || 'mono'}</small></figcaption></figure>`).join('\n')}</div></body></html>`);
writeFileSync(`${OUT}/m.html`, `<!DOCTYPE html><html><head><meta charset="utf-8">
<style>html,body{margin:0;background:#222}.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:10px;width:2400px}figure{margin:0;position:relative;background:#fff;overflow:hidden}img{width:100%;display:block}figcaption{position:absolute;left:0;bottom:0;right:0;padding:6px 10px;font:14px ui-monospace,monospace;color:#fff;background:linear-gradient(transparent,#000d)}</style></head><body>
<div class="grid">${VARIATIONS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}</figcaption></figure>`).join('')}</div></body></html>`);
console.log(`contact sheets -> ${OUT}/index.html , m.html`);