206 lines
16 KiB
JavaScript
206 lines
16 KiB
JavaScript
/* ============================================================
|
||
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)`);
|