Files
bubblechambersimart/tools/templates4.mjs
2026-06-10 18:23:47 +01:00

206 lines
16 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.
/* ============================================================
templates4.mjs — the ELEMENT-LIBERATION typology.
The earlier waves liberated whole COMPOSITIONS (one signature move per
famous picture). This set instead breaks each ELEMENT free from the role
it was built for, one element at a time, against a calm neutral host so the
break is unmistakable. Every element is asked to be something it is not:
the background becomes the subject, the sea becomes a sky, the floor becomes
a wall or a membrane, the sun becomes atmosphere or a distant star, the
event becomes an all-over field or a horizon, the annotation becomes the
only figure. Each plate is the answer to "what else can this part be?"
Usage: node tools/templates4.mjs [size] [outDir]
============================================================ */
import { writeFileSync, mkdirSync } from 'node:fs';
import { DEFAULT_COMPOSITION } from '../src/compose/schema.js';
import { renderComposition } from '../src/compose/composition.js';
const SIZE = +(process.argv[2] || 1300);
const OUT = process.argv[3] || 'output/templates4';
mkdirSync(OUT, { recursive: true });
const clone = (o) => JSON.parse(JSON.stringify(o));
const merge = (a, b) => { const o = { ...a }; for (const k in b) o[k] = (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k])) ? merge(a[k] || {}, b[k]) : b[k]; return o; };
// ---- a calm, legible HOST: every element present but quiet, so liberating
// ONE element reads as a clear break rather than noise. ----
const BASE = {
background: { color: 'rgb(228,221,202)', glow: { strength: 0.4, followSun: true }, vignette: { strength: 0.3 }, aging: { opacity: 0.34, foxing: 0.4, dust: 0.35, scratches: 4 }, grain: { opacity: 0.3, intensity: 0.34 } },
fieldSea: { color: { hueBack: 0.55, hueFront: 0.5, sat: 0.4, light: 0.36 }, chaos: 0.2, blips: 0.4, mound: 0.15, horizonY: 0.42, lines: 44, layerOpacity: 0.82 },
fieldGrid: { opacity: 0.26, pos: 'over', pitch: 0.45, yaw: 0.3, color: { hue: 0.52, hue2: 0.5, sat: 0.24, lightNear: 0.34, lightFar: 0.62 } },
disk: { transform: { x: 0, y: -0.22, rotation: 0, scale: 0.5 }, hue: 0.06, sat: 0.78, size: 0.13, pressure: 0.5, intensity: 0.8, striations: 0.5 },
bubble: { transform: { x: 0.26, y: 0.06, rotation: 8, scale: 0.5 }, palette: 'magentarise', saturation: 1.0, primaries: 11, sweepers: 3, cosmics: 4, showBoundary: true, boundaryOpacity: 0.32, instrument: 0.25 },
fiduciaries: { label: 'auto', width: 1.0, arrow: true },
};
const host = merge(clone(DEFAULT_COMPOSITION), BASE);
// quiet-everything helpers (so a single liberated element owns the frame)
const QUIET = {
sea: { fieldSea: { layerOpacity: 0.5 } },
grid: { fieldGrid: { opacity: 0.12 } },
fields: { fieldSea: { enabled: false }, fieldGrid: { enabled: false } },
disk: { disk: { enabled: false } },
bubble: { bubble: { enabled: false } },
figs: { disk: { enabled: false }, bubble: { enabled: false } },
fids: { fiduciaries: { enabled: false } },
};
const q = (...keys) => keys.reduce((acc, k) => merge(acc, QUIET[k]), {});
// ---- the liberations, grouped by element. each: {title, note, seed, over} ----
const LIB = [
// ===================== BACKGROUND — passive ground → active surface =====================
{ element: 'Background', title: 'Decay as subject', seed: 'PALIMPSEST-0001',
note: 'the plate\'s own wear is the image · all figures dissolved to a whisper',
over: merge(q('fids'), {
background: { color: 'rgb(214,201,176)', aging: { opacity: 1, foxing: 1, dust: 0.95, scratches: 19, seed: 31 }, grain: { opacity: 0.7, intensity: 0.6 }, glow: { strength: 0.22 }, vignette: { mode: 'radial', cx: 0.4, cy: 0.55, radius: 0.95, strength: 0.4 } },
fieldSea: { layerOpacity: 0.08 }, fieldGrid: { opacity: 0.05 },
disk: { layerOpacity: 0.08, transform: { scale: 0.4 } }, bubble: { layerOpacity: 0.1, primaries: 5 },
}) },
{ element: 'Background', title: 'Cleaved ground', seed: 'SEAM-1957',
note: 'a directional split turns the bare substrate into a Rothko/Malevich field — no figure at all',
over: merge(q('fields', 'figs', 'fids'), {
background: { color: 'rgb(176,96,82)', glow: { strength: 0.5, followSun: false }, vignette: { mode: 'linear', angle: 8, start: 0.32, strength: 0.85 }, aging: { opacity: 0.4 }, grain: { opacity: 0.4 } },
}) },
// ===================== FIELD · SEA — ocean floor → sky / rain / frieze =====================
{ element: 'Field · Sea', title: 'Sea as a luminous sky', seed: 'AURORA-7782',
note: 'flipped overhead + waves lighter than the ground → a canopy of light, not water',
over: merge(q('grid', 'figs'), {
background: { color: 'rgb(30,32,44)', glow: { strength: 0.35, followSun: false }, vignette: { strength: 0.3 } },
fieldSea: { transform: { rotation: 180, y: -0.18 }, color: { hueBack: 0.55, hueFront: 0.6, sat: 0.42, light: 0.86 }, chaos: 0.45, blips: 1.0, mound: 0.4, horizonY: 0.46, lines: 56, layerOpacity: 1 },
fiduciaries: { enabled: true, label: 'the unseen', target: [0.0, 0.55], from: [-0.4, 0.7] },
}) },
{ element: 'Field · Sea', title: 'Sea as falling rain', seed: 'NOTUNG-9001',
note: 'rotated upright → the ridgelines become a vertical downpour / reed screen',
over: merge(q('grid', 'figs', 'fids'), {
background: { color: 'rgb(206,206,198)' },
fieldSea: { transform: { rotation: 90, scale: 1.25 }, color: { hueBack: 0.58, hueFront: 0.56, sat: 0.32, light: 0.34 }, chaos: 0.5, blips: 0.6, mound: 0.1, horizonY: 0.4, lines: 60, layerOpacity: 0.85 },
}) },
{ element: 'Field · Sea', title: 'Sea as a spectrum frieze', seed: 'BANDPASS-0440',
note: 'clipped to a thin mid-band, detached from any horizon → an emission-line strip',
over: merge(q('grid', 'figs'), {
background: { color: 'rgb(222,216,200)' },
fieldSea: { clip: [0, 0.42, 1, 0.58], color: { hueBack: 0.0, hueFront: 0.15, sat: 0.7, light: 0.5 }, chaos: 0.35, blips: 1.4, mound: 0.0, horizonY: 0.5, lines: 64, layerOpacity: 1 },
fiduciaries: { enabled: true, label: 'a trace', target: [0.0, 0.0], from: [-0.5, -0.35] },
}) },
// ===================== FIELD · GRID — floor → wall / membrane / ceiling =====================
{ element: 'Field · Grid', title: 'Grid as a standing wall', seed: 'FACADE-1901',
note: 'rotated upright → the receding floor becomes a rectilinear facade behind the event',
over: merge(q('sea', 'disk'), {
background: { color: 'rgb(224,216,196)' },
fieldGrid: { opacity: 0.5, transform: { rotation: 90, scale: 1.15 }, pitch: 0.5, yaw: 0.06, nx: 18, nz: 22, color: { hue: 0.08, hue2: 0.06, sat: 0.3, lightNear: 0.32, lightFar: 0.6 } },
bubble: { transform: { x: 0, y: 0.0, scale: 0.55 }, palette: 'kind' },
}) },
{ element: 'Field · Grid', title: 'Grid as a vibrating membrane', seed: 'CHLADNI-0440',
note: 'high-Q resonance on BOTH axes → the floor stops being a floor; a standing-wave drumhead',
over: merge(q('sea', 'figs'), {
background: { color: 'rgb(22,24,34)', glow: { strength: 0.3, followSun: false }, vignette: { strength: 0.28 } },
fieldGrid: { opacity: 0.62, pitch: 0.42, yaw: 0.0, originY: 0.36, nx: 22, nz: 26, color: { hue: 0.55, hue2: 0.5, sat: 0.2, lightNear: 0.4, lightFar: 0.6 }, resonance: { amp: 0.85, q: 22, axis: 'both', hue: 0.5, sat: 0.85, light: 0.6 } },
fiduciaries: { enabled: true, label: 'resonance', target: [0.0, 0.0], from: [0.5, -0.4] },
}) },
{ element: 'Field · Grid', title: 'Grid as a coffered ceiling', seed: 'VAULT-1465',
note: 'horizon lifted + flipped → the grid recedes UP and overhead, an architectural vault',
over: merge(q('sea', 'disk', 'fids'), {
background: { color: 'rgb(220,212,194)', glow: { strength: 0.5, followSun: false } },
fieldGrid: { opacity: 0.5, transform: { rotation: 180, y: -0.12 }, pitch: 0.55, yaw: 0.42, originY: 0.36, nx: 16, nz: 24, color: { hue: 0.09, hue2: 0.07, sat: 0.28, lightNear: 0.34, lightFar: 0.62 } },
bubble: { transform: { x: 0.0, y: 0.42, scale: 0.4 }, palette: 'mono', primaries: 8, showBoundary: false, instrument: 0 },
}) },
// ===================== DISK — focal sun → atmosphere / distant star / horizon =====================
{ element: 'Disk · sun', title: 'Sun dissolved into atmosphere', seed: 'CORONA-9000',
note: 'overfilled to 3× at low intensity → the striations become rays of weather, the disk is gone',
over: merge(q('sea', 'grid', 'bubble', 'fids'), {
background: { color: 'rgb(232,224,204)', glow: { strength: 0.95, followSun: true }, vignette: { mode: 'radial', cx: 0.42, cy: 0.4, radius: 1.1, strength: 0.4 } },
disk: { transform: { x: -0.12, y: -0.1, scale: 3.0 }, hue: 0.09, sat: 0.7, size: 0.34, pressure: 0.15, intensity: 0.55, striations: 0.9, stain: 0.2, soften: 2.0 },
}) },
{ element: 'Disk · sun', title: 'Sun demoted to a distant star', seed: 'KEPLER-0042',
note: 'a tiny high pinprick · the field it once dominated now owns the frame (hierarchy inverted)',
over: merge(q('fids'), {
background: { color: 'rgb(26,28,38)', glow: { strength: 0.2, followSun: true }, vignette: { strength: 0.34 } },
fieldSea: { color: { hueBack: 0.6, hueFront: 0.58, sat: 0.4, light: 0.7 }, chaos: 0.4, blips: 1.0, mound: 0.35, horizonY: 0.5, lines: 54, layerOpacity: 1 },
fieldGrid: { opacity: 0.12 },
disk: { transform: { x: 0.34, y: -0.62, scale: 0.16 }, hue: 0.12, sat: 0.6, size: 0.1, pressure: 0.2, intensity: 0.95 },
bubble: { enabled: false },
}) },
{ element: 'Disk · sun', title: 'Sun spread into a horizon dawn', seed: 'DAWNLINE-1844',
note: 'pushed low and off the bottom edge → a glowing horizon band, no longer a disk',
over: merge(q('grid', 'bubble'), {
background: { color: 'rgb(234,221,198)', glow: { strength: 0.85, followSun: true }, vignette: { mode: 'linear', angle: 180, start: 0.45, strength: 0.4 } },
fieldSea: { color: { hueBack: 0.08, hueFront: 0.06, sat: 0.4, light: 0.4 }, chaos: 0.28, horizonY: 0.5, layerOpacity: 0.7 },
disk: { transform: { x: 0.0, y: 1.02, scale: 2.4 }, hue: 0.08, sat: 0.85, size: 0.3, pressure: 0.2, intensity: 0.9, striations: 0.7, soften: 1.8 },
fiduciaries: { enabled: true, label: 'evidence', target: [0.0, 0.5], from: [-0.45, 0.1] },
}) },
// ===================== BUBBLE — contained event → all-over field / horizon / aperture =====================
{ element: 'Bubble · event', title: 'Event as an all-over field', seed: 'SHOWER-7331',
note: 'overscaled, off-centre, boundary off → the tracks become a cosmic-ray scatter, not an event',
over: merge(q('sea', 'grid', 'disk', 'fids'), {
background: { color: 'rgb(20,21,30)', glow: { strength: 0.25, followSun: false }, vignette: { strength: 0.34 } },
bubble: { transform: { x: -0.2, y: -0.1, rotation: 0, scale: 2.7 }, palette: 'kindrise', saturation: 1.1, primaries: 30, sweepers: 8, cosmics: 14, deltaRate: 0.75, eloss: 0.3, showBoundary: false, instrument: 0, transparentBase: true },
}) },
{ element: 'Bubble · event', title: 'Event laid down as a horizon', seed: 'STRATA-0100',
note: 'flattened and lined up low → the explosion reads as a landscape ridgeline',
over: merge(q('grid', 'disk'), {
background: { color: 'rgb(226,220,202)' },
fieldSea: { layerOpacity: 0.4 },
bubble: { transform: { x: 0.0, y: 0.5, rotation: 0, scale: 1.6 }, palette: 'mono', saturation: 0.6, primaries: 22, sweepers: 7, cosmics: 2, bfield: 0.4, eloss: 0.5, showBoundary: false, instrument: 0 },
fiduciaries: { enabled: true, label: 'a trace', target: [0.2, 0.45], from: [-0.3, 0.0] },
}) },
{ element: 'Bubble · event', title: 'Chamber arc as a porthole', seed: 'APERTURE-2755',
note: 'tracks suppressed, the structural boundary cranked → the instrument geometry becomes the subject',
over: merge(q('sea', 'grid', 'disk', 'fids'), {
background: { color: 'rgb(222,216,200)', glow: { strength: 0.5, followSun: false }, vignette: { strength: 0.36 } },
bubble: { transform: { x: 0.0, y: 0.0, rotation: 0, scale: 1.0 }, palette: 'mono', primaries: 4, sweepers: 1, cosmics: 0, deltaRate: 0.2, showBoundary: true, boundaryR: 0.92, boundaryY: 0.0, boundaryOpacity: 0.85, instrument: 0.85 },
}) },
// ===================== FIDUCIARIES — documentary furniture → the only figure =====================
{ element: 'Fiduciaries', title: 'The annotation is the artwork', seed: 'NOTATION-0001',
note: 'everything else off · a frame-spanning arrow + one word — a diagram of nothing',
over: merge(q('fields', 'figs'), {
background: { color: 'rgb(230,224,206)', glow: { strength: 0.45, followSun: false }, vignette: { strength: 0.28 } },
fiduciaries: { label: 'the vacuum', width: 1.8, arrow: true, corners: true, target: [0.55, 0.4], from: [-0.7, -0.55], caption: 'No. 001 — Traces of the Invisible' },
}) },
{ element: 'Fiduciaries', title: 'Pointing at absence', seed: 'MA-0042',
note: 'the gesture of attention aimed at an empty corner — the subject is where nothing is',
over: merge(q('grid'), {
background: { color: 'rgb(228,224,210)', glow: { strength: 0.4, followSun: false }, vignette: { mode: 'radial', cx: 0.72, cy: 0.7, radius: 0.7, strength: 0.4 } },
fieldSea: { color: { sat: 0.28, light: 0.36 }, chaos: 0.18, horizonY: 0.7, mound: 0.1, layerOpacity: 0.7 },
disk: { transform: { x: -0.62, y: -0.5, scale: 0.32 }, size: 0.1 },
bubble: { transform: { x: -0.5, y: -0.36, scale: 0.3 }, primaries: 7, showBoundary: false, instrument: 0 },
fiduciaries: { label: 'the unseen', width: 1.4, arrow: true, target: [0.66, 0.62], from: [-0.4, -0.3] },
}) },
];
let n = 0; const items = [];
for (const L of LIB) {
const comp = merge(host, L.over);
comp.seed = L.seed;
const slug = L.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const name = `${String(++n).padStart(2, '0')}_${slug}`;
writeFileSync(`${OUT}/${name}.svg`, renderComposition(comp, SIZE));
writeFileSync(`${OUT}/${name}.mjs`, `/* ${L.element} liberated — ${L.title}\n ${L.note} */\nexport const composition = ${JSON.stringify(comp, null, 2)};\n`);
items.push({ name, ...L });
console.log(` ${name} [${L.element}] ${L.title}`);
}
// ---- contact sheet: an archive wall grouped by element ----
const byEl = [];
for (const it of items) { let g = byEl.find((x) => x.el === it.element); if (!g) { g = { el: it.element, list: [] }; byEl.push(g); } g.list.push(it); }
const section = (g) => `<section><h2>${g.el}</h2><div class="wall">${g.list.map((it) => `<figure><div class="mat"><img src="${it.name}.svg"></div><figcaption><span class="ttl">${it.title}</span><span class="note">${it.note}</span></figcaption></figure>`).join('')}</div></section>`;
writeFileSync(`${OUT}/matrix.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Liberating the Elements</title>
<style>html,body{margin:0;background:#15140f;color:#cabfa6}body{padding:58px 52px 76px;font:12px/1.5 ui-monospace,Menlo,monospace}
.head{letter-spacing:.34em;text-transform:uppercase;font-size:15px;color:#9fb7af;margin:0 0 4px}
.sub{color:#6f6a5c;font-size:12px;margin:0 0 44px;letter-spacing:.06em}
h2{letter-spacing:.22em;text-transform:uppercase;font-size:12px;color:#b98c5a;border-bottom:1px solid #3a352a;padding-bottom:7px;margin:42px 0 26px}
.wall{display:grid;grid-template-columns:repeat(3,1fr);gap:40px 34px}figure{margin:0}
.mat{background:#f3ecdb;padding:14px;box-shadow:0 2px 0 #0008,0 20px 44px -20px #000c;border:1px solid #000}
.mat img{display:block;width:100%;aspect-ratio:1/1;object-fit:cover;background:#fff}
figcaption{margin-top:10px}.ttl{color:#cabfa6;display:block}.note{color:#6f6a5c;font-style:italic;display:block;font-size:11px;margin-top:2px}</style></head><body>
<p class="head">Liberating the Elements</p>
<p class="sub">every part asked to be something it was not — ${n} plates · one element broken free per plate</p>
${byEl.map(section).join('\n')}
</body></html>`);
console.log(`\ntemplates4 — ${n} plates → ${OUT}/ (+ matrix.html)`);