/* ============================================================ perspective3.mjs — 12 compositions built through the REFACTORED grouped schema (renderComposition). Shows off what the refactor unlocked: disk & collision decoupled (independent centre / rotation / scale), frame-level fiduciary arrows that span the frame to find a distant event, and per-layer transforms on the fields themselves. Usage: node tools/perspective3.mjs [size] ============================================================ */ import { writeFileSync, mkdirSync } from 'node:fs'; import { renderComposition } from '../src/compose/composition.js'; const SIZE = +(process.argv[2] || 1500); const OUT = 'output/layering/perspective3'; mkdirSync(OUT, { recursive: true }); const F = Math.PI * 2, D = Math.PI / 180; 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; }; const BASE = { size: SIZE, seed: 'MESON-5113', background: { color: 'rgb(229,222,203)', film: { opacity: 0.6, density: 0.5, seed: 8 }, aging: { opacity: 0.5, scratches: 5, dust: 0.45, foxing: 0.5, seed: 5 }, grain: { opacity: 0.42, intensity: 0.42, seed: 19 } }, fieldSea: { enabled: true, transform: { x: 0, y: 0, rotation: 0, scale: 1 }, seed: 'VACUUM-5113', color: { hueBack: 0.54, hueFront: 0.47, sat: 0.6 }, layers: 3, blurPerLayer: [2.6, 1.1, 0], chaos: 0.3, blips: 0.7, mound: 0.3, horizonY: 0.36, lines: 46 }, fieldGrid: { enabled: true, pos: 'over', transform: { x: 0, y: 0, rotation: 0, scale: 1 }, color: { hue: 0.56, hue2: 0.5, sat: 0.26, lightNear: 0.44, lightFar: 0.66 }, opacity: 0.38, vp: [0, 0], dir: 0, spread: F, rays: 30, depthLines: 17, rMin: 0.04, rMax: 3.0, ripple: { amp: 0.07, ampRad: 0.06, freqR: 2.4, freqA: 5, phase: 0 }, layers: 2, blurPerLayer: [1.1, 0] }, disk: { enabled: true, transform: { x: 0, y: -0.25, rotation: 0, scale: 0.95 }, hue: 0.06, sat: 0.82, size: 0.16, pressure: 0.85 }, bubble: { enabled: true, transform: { x: 0.28, y: -0.1, rotation: 12, scale: 0.82 }, palette: 'magentarise', saturation: 1.05, primaries: 18, sweepers: 5, eloss: 0.34 }, fiduciaries: { enabled: true, label: 'No 001', pencil: '#39312a', width: 1.0, arrow: true }, }; const mk = (name, over) => ({ name, comp: merge(BASE, over) }); const VARIATIONS = [ // 1 — the WITNESS and the EVENT: a quiet sun, the collision flung to a far // corner; the hand's arrow has to reach all the way across to name it. mk('01_witness-and-event', { disk: { transform: { x: -0.05, y: -0.3, scale: 0.62 }, hue: 0.08 }, bubble: { transform: { x: 0.52, y: 0.44, rotation: -8, scale: 0.5 } }, fiduciaries: { from: [-0.02, -0.22], label: 'No 001', caption: 'good event' }, }), // 2 — ECLIPSE: an enormous low-pressure sun, the event tucked at its rim. mk('02_eclipse', { disk: { transform: { x: 0, y: -0.06, scale: 1.55 }, hue: 0.0, sat: 0.9, size: 0.18, pressure: 0.95 }, bubble: { transform: { x: 0.34, y: -0.16, scale: 0.46 } }, fieldGrid: { opacity: 0.3, vp: [0, -0.05] }, }), // 3 — INVERSION: sun sunk into the sea, the collision risen high above it. mk('03_sunk-sun-risen-event', { disk: { transform: { x: -0.02, y: 0.4, scale: 0.8 }, hue: 0.05 }, bubble: { transform: { x: 0.05, y: -0.46, rotation: 20, scale: 0.7 } }, fieldGrid: { pos: 'behind', opacity: 0.34 }, }), // 4 — TILTED ARCHIVE: the whole event canted 32°; studied with corners + caption. mk('04_tilted-archive', { disk: { transform: { x: -0.32, y: -0.22, scale: 0.66 }, hue: 0.11 }, bubble: { transform: { x: 0.22, y: 0.0, rotation: 32, scale: 0.82 } }, fiduciaries: { corners: true, caption: 'check θ', from: [0.5, -0.35] }, }), // 5 — VORTEX PULL: the grid is a whirlpool; the collision caught at its mouth, // a small far sun looking on. mk('05_vortex-pull', { fieldGrid: { vp: [0.08, -0.04], color: { hue: 0.5, hue2: 0.55, sat: 0.34, lightNear: 0.42, lightFar: 0.64 }, opacity: 0.46, ripple: { amp: 0.11, ampRad: 0.09, freqR: 2.9, freqA: 7, phase: 0.8 }, rMax: 2.6, rays: 34 }, disk: { transform: { x: -0.42, y: -0.34, scale: 0.55 }, hue: 0.95 }, bubble: { transform: { x: 0.08, y: -0.03, scale: 0.72 } }, }), // 6 — GRID ASKEW: the depth grid itself rotated + zoomed (per-layer transform). mk('06_grid-askew', { fieldGrid: { transform: { x: 0.1, y: -0.05, rotation: 16, scale: 1.15 }, vp: [-0.35, -0.2], spread: 90 * D, dir: 35 * D, rMax: 3.6, color: { hue: 0.6, hue2: 0.6, sat: 0.1, lightNear: 0.46, lightFar: 0.66 } }, disk: { transform: { x: 0.05, y: -0.26, scale: 0.8 }, hue: 0.06 }, bubble: { transform: { x: -0.18, y: 0.06, rotation: -10, scale: 0.78 } }, }), // 7 — CORNER DAWN: a rose sun in the upper-left, the event mid-frame, long reach. mk('07_corner-dawn-rose', { disk: { transform: { x: -0.56, y: -0.52, scale: 0.5 }, hue: 0.95, sat: 0.7, pressure: 0.5 }, bubble: { transform: { x: 0.16, y: 0.08, scale: 0.86 } }, fiduciaries: { from: [-0.5, -0.4] }, }), // 8 — SCALE RHYME: two small foci mirrored across the frame — one teal sun, // one event rotated 180°, the rhyme across the void. mk('08_scale-rhyme-mirror', { disk: { transform: { x: -0.42, y: -0.12, scale: 0.5 }, hue: 0.5, sat: 0.6 }, bubble: { transform: { x: 0.42, y: 0.12, rotation: 180, scale: 0.5 } }, fieldGrid: { opacity: 0.3 }, }), // 9 — MEASURED DAWN: classic — sun on the horizon, studied event, registration // corners, a faint cool grid. The archive at its calmest. mk('09_measured-dawn', { disk: { transform: { x: 0, y: -0.26, scale: 0.72 }, hue: 0.07 }, bubble: { transform: { x: 0.3, y: -0.08, rotation: 6, scale: 0.74 } }, fiduciaries: { corners: true, caption: 'good event' }, fieldGrid: { opacity: 0.32 }, }), // 10 — COLD CATHEDRAL: a violet vaulting grid from on high (behind), a teal // sun, a verdigris sea. The numinous, gone cold. mk('10_cold-cathedral', { fieldSea: { color: { hueBack: 0.46, hueFront: 0.42, sat: 0.42 }, chaos: 0.28 }, fieldGrid: { pos: 'behind', vp: [0, -0.66], dir: 95 * D, spread: 150 * D, color: { hue: 0.7, hue2: 0.66, sat: 0.3, lightNear: 0.44, lightFar: 0.66 }, opacity: 0.5, ripple: { amp: 0.1, ampRad: 0.08, freqR: 3.0, freqA: 8, phase: 1.2 } }, disk: { transform: { x: 0.05, y: -0.24, scale: 0.78 }, hue: 0.5, sat: 0.6 }, bubble: { transform: { x: -0.22, y: -0.06, scale: 0.8 } }, }), // 11 — GIANT GRID, TINY EVENT: the coordinate-system dwarfs the collision; // the field zoomed huge, a small ember far off. mk('11_giant-grid-tiny-event', { fieldGrid: { transform: { scale: 1.4, x: -0.1 }, opacity: 0.42, vp: [0.15, 0.05], ripple: { amp: 0.09, ampRad: 0.07, freqR: 2.6, freqA: 6, phase: 0.4 } }, disk: { transform: { x: 0.46, y: -0.42, scale: 0.42 }, hue: 0.0, pressure: 0.9 }, bubble: { transform: { x: 0.4, y: -0.3, scale: 0.4 } }, fiduciaries: { from: [0.1, -0.2] }, }), // 12 — DISSOLVING COORDINATES: the grid a warm near-paper whisper to the edges; // the event the only firm thing. (white pencil — backlit-intent, faint unlit.) mk('12_dissolving-coordinates', { fieldGrid: { color: { hue: 0.1, hue2: 0.08, sat: 0.16, lightNear: 0.5, lightFar: 0.68 }, opacity: 0.26, rMax: 3.7, ripple: { amp: 0.12, ampRad: 0.1, freqR: 3.1, freqA: 7, phase: 1.0 } }, disk: { transform: { x: 0, y: -0.24, scale: 0.74 }, hue: 0.04, pressure: 0.85 }, bubble: { transform: { x: 0.26, y: -0.1, scale: 0.78 } }, fiduciaries: { pencil: '#f4efe6', caption: 'No 001' }, }), ]; console.log(`perspective3 — ${VARIATIONS.length} schema-built compositions → ${OUT}/`); for (const v of VARIATIONS) { writeFileSync(`${OUT}/${v.name}.svg`, renderComposition(v.comp, SIZE)); console.log(` ${v.name}`); } const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/m.html`, `perspective3 · schema-built · decoupled
${VARIATIONS.map(v => `
${cap(v)}
`).join('')}
`); console.log(`contact sheet -> ${OUT}/m.html`);