141 lines
9.1 KiB
JavaScript
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`);
|