web interface

This commit is contained in:
2026-06-02 19:17:19 -04:00
parent 52453fba67
commit 219eb6632c
140 changed files with 2793 additions and 40 deletions

View File

@@ -7,6 +7,7 @@
============================================================ */
import { writeFileSync, mkdirSync } from 'node:fs';
import { carpetSVG } from '../src/qft/carpet.js';
import { perspectiveGridSVG } from '../src/qft/perspgrid.js';
import { generateScene } from '../src/scene/scene.js';
import { renderSVG } from '../src/render/svgVector.js';
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
@@ -116,6 +117,28 @@ function compose(v) {
}
const dir = v.dir ? `${OUT}/${v.dir}` : OUT;
if (v.dir) mkdirSync(dir, { recursive: true });
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
const carpetEls = [
IMG(sheets[0], `filter="url(#b3)" opacity="0.5"`),
IMG(sheets[1], `filter="url(#b2)" opacity="0.72"`),
IMG(sheets[2], `opacity="0.95"`),
].join('\n');
// optional secondary PERSPECTIVE-GRID plex layer (dual blur/fine deck)
let perspEls = '';
if (v.persp) {
const pb = { mode: 'plate', ...v.persp };
// BACK: ripple A, heavier stroke (blurred in composite). FRONT: a DISTINCT
// ripple (shifted freq + phase) and fine line → the two interfere (moiré).
const back = dataUri(perspectiveGridSVG(SIZE, { ...pb, salt: 'pA', stroke: (pb.stroke ?? 1.4) * 1.6, strokeFar: (pb.strokeFar ?? 0.5) * 1.6 }));
const front = dataUri(perspectiveGridSVG(SIZE, {
...pb, salt: 'pB', stroke: (pb.stroke ?? 1.4) * 0.8, strokeFar: (pb.strokeFar ?? 0.5) * 0.8,
rippleFreqR: (pb.rippleFreqR ?? 2.4) * 1.24, rippleFreqA: (pb.rippleFreqA ?? 5) + 1.5, ripplePhase: (pb.ripplePhase ?? 0) + 0.95,
}));
const op = v.perspOpacity ?? 0.42;
perspEls = IMG(back, `filter="url(#b2)" opacity="${op}"`) + '\n' + IMG(front, `opacity="${(op + 0.18).toFixed(2)}"`);
}
const mid = v.perspPos === 'behind' ? (perspEls + '\n' + carpetEls) : (carpetEls + '\n' + perspEls);
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
<defs>
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
@@ -123,9 +146,7 @@ function compose(v) {
</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"/>
${mid}
${bcEl}
${aging ? `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${aging}" opacity="${v.agingOpacity ?? 0.6}" 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"/>
@@ -337,6 +358,110 @@ for (const v of VAST04) compose(v);
<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">${VAST04.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}</figcaption></figure>`).join('')}</div></body></html>`);
}
// ============================================================
// PERSPECTIVE — add the secondary depth-grid plex layer (VP off-horizon) to a
// strong vast04-style frame. Cool draughtsman's-pencil grid, dual blur/fine.
// Vary VP position + over/behind the sea.
// ============================================================
const DEG = Math.PI / 180;
const PG = (vp, dir, spread, extra = {}) => ({ vp, dir, spread, rays: 26, depthLines: 15, hue: 0.56, hue2: 0.5, sat: 0.4, ...extra });
const v5 = (name, sunAt, sun, ev, persp, perspPos = 'over', pencil = '#39312a') => ({
name, dir: 'perspective', base: 'rgb(229,222,203)', film: { seed: 8, density: 0.5 },
grain: { amount: 0.4 }, aging: { seed: (name.charCodeAt(2) * 7) % 97, scratches: 5, dust: 0.45, foxing: 0.5 }, agingOpacity: 0.55,
carpet: RIPPLE, persp, perspPos, perspOpacity: 0.48,
bcSeed: 'MESON-5113', bcFloat: true, bcScale: 0.78, sunAt,
bcOver: {
palette: 'magentarise', saturation: 1.05, shockX: 0, shockY: 0, eventX: ev[0], eventY: ev[1],
diskHue: sun.h, diskSat: sun.s, shockSize: sun.size, diskPressure: sun.p,
annotate: 0.5, annoRing: false, annoExtras: false, annoArrow: true, annoNum: true, annoLabel: 'No 001', annoWidth: 0.4, annotateInk: pencil,
sweepers: 5, primaries: 18, eloss: 0.34,
},
});
const PERSP = [
v5('01_center-funnel-over', [0, -0.25], { h: 0.06, s: 0.82, size: 0.16, p: 0.85 }, [0.26, -0.14], PG([0, 0.02], 0, Math.PI * 2, { rays: 30, depthLines: 16 }), 'over'),
v5('02_corner-left-over', [0.1, -0.3], { h: 0.0, s: 0.9, size: 0.17, p: 0.9 }, [-0.22, 0.14], PG([-1.3, -1.05], 35 * DEG, 60 * DEG, { rMax: 3.4, rays: 22 }), 'over'),
v5('03_high-vp-behind', [0, -0.24], { h: 0.08, s: 0.85, size: 0.16, p: 0.6 }, [0.24, 0.12], PG([0, -0.62], 95 * DEG, 150 * DEG, { rays: 26 }), 'behind'),
v5('04_low-into-sea-over', [0.05, -0.42], { h: 0.06, s: 0.82, size: 0.15, p: 0.8 },[-0.2, -0.14], PG([0, 0.5], -90 * DEG, 150 * DEG, { rays: 26 }), 'over'),
v5('05_center-funnel-behind',[0, -0.25], { h: 0.05, s: 0.78, size: 0.16, p: 0.7 }, [0.2, 0.16], PG([0.05, -0.05], 0, Math.PI * 2, { rays: 32, depthLines: 18 }), 'behind'),
v5('06_offright-corridor', [-0.35, -0.05], { h: 0.02, s: 0.85, size: 0.16, p: 0.8 },[0.26, 0.12], PG([1.35, -0.1], 178 * DEG, 70 * DEG, { rMax: 3.2, rays: 22 }), 'over'),
];
console.log(`PERSPECTIVE (secondary depth-grid layer) — ${PERSP.length}${OUT}/perspective/`);
for (const v of PERSP) compose(v);
{
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
writeFileSync(`${OUT}/perspective/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">${PERSP.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}</figcaption></figure>`).join('')}</div></body></html>`);
}
// ============================================================
// PERSPECTIVE2 — 20 iterations of the RIPPLED depth grid (two interfering
// layers, faint/near-paper), to know the unknowable: edge-filling lattices,
// vacuum-drains, ghost corridors, two infinities, dissolving nets.
// ============================================================
// grid colour presets (faint, near the ground)
const GC = {
cool: { hue: 0.56, hue2: 0.5, sat: 0.26, lightNear: 0.44, lightFar: 0.66 },
warm: { hue: 0.1, hue2: 0.08, sat: 0.18, lightNear: 0.5, lightFar: 0.68 }, // near cream
teal: { hue: 0.5, hue2: 0.55, sat: 0.34, lightNear: 0.42, lightFar: 0.64 },
violet:{ hue: 0.7, hue2: 0.66, sat: 0.3, lightNear: 0.44, lightFar: 0.66 },
ash: { hue: 0.6, hue2: 0.6, sat: 0.08, lightNear: 0.46, lightFar: 0.66 },
};
const SUN = {
ember: { h: 0.0, s: 0.9, size: 0.16, p: 0.9 }, orange: { h: 0.06, s: 0.82, size: 0.16, p: 0.85 },
gold: { h: 0.11, s: 0.85, size: 0.15, p: 0.5 }, rose: { h: 0.95, s: 0.7, size: 0.15, p: 0.5 },
teal: { h: 0.5, s: 0.6, size: 0.16, p: 0.6 }, magenta: { h: 0.9, s: 0.78, size: 0.17, p: 0.7 },
copper: { h: 0.05, s: 0.72, size: 0.16, p: 0.55 }, amber: { h: 0.085, s: 0.9, size: 0.17, p: 0.3 },
};
// rip(ampPerp, ampRad, freqR, freqA, phase)
const rip = (a, ar, fr, fa, ph = 0) => ({ rippleAmp: a, rippleAmpRad: ar, rippleFreqR: fr, rippleFreqA: fa, ripplePhase: ph });
// pg(vp, dir, spread, color, ripple, extra)
const pg = (vp, dir, spread, color, ripple, extra = {}) => ({ vp, dir, spread, rays: 30, depthLines: 17, depthPow: 2.3, rMin: 0.04, rMax: 2.8, ...GC[color], ...ripple, ...extra });
const EDGE = { rMax: 3.7, rMin: 0.03 }; // push rings past the frame → fills to edges
const p2 = (name, sun, ev, persp, o = {}) => ({
name, dir: 'perspective2', base: 'rgb(229,222,203)', film: { seed: 8, density: 0.5 },
grain: { amount: 0.4 }, aging: { seed: (name.charCodeAt(3) * 7) % 97, scratches: 5, dust: 0.45, foxing: 0.5 }, agingOpacity: 0.52,
carpet: RIPPLE, persp, perspPos: o.pos || 'over', perspOpacity: o.op ?? 0.4,
bcSeed: 'MESON-5113', bcFloat: true, bcScale: o.scale || 0.78, sunAt: o.sunAt || [0, -0.25],
bcOver: {
palette: 'magentarise', saturation: 1.05, shockX: 0, shockY: 0, eventX: ev[0], eventY: ev[1],
diskHue: SUN[sun].h, diskSat: SUN[sun].s, shockSize: SUN[sun].size, diskPressure: SUN[sun].p,
annotate: 0.5, annoRing: false, annoExtras: false, annoArrow: true, annoNum: true, annoLabel: o.label || 'No 001', annoWidth: 0.4, annotateInk: o.pencil || '#39312a',
sweepers: o.sweepers ?? 5, primaries: o.primaries ?? 18, eloss: o.eloss ?? 0.34,
},
});
const F = Math.PI * 2; // full radial
const PERSP2 = [
p2('01_lattice-behind-things', 'orange', [0.26, -0.14], pg([0, 0.0], 0, F, 'cool', rip(0.06, 0.05, 2.2, 5), EDGE), { op: 0.38 }),
p2('02_a-guessed-geometry', 'ember', [-0.2, 0.16], pg([0.1, -0.05], 0, F, 'warm', rip(0.08, 0.06, 2.6, 6, 0.6), EDGE), { pos: 'behind', op: 0.5 }),
p2('03_interference-of-readings','teal', [0.3, -0.1], pg([0, -0.02], 0, F, 'teal', rip(0.1, 0.08, 2.8, 7, 1.1), { rays: 34, depthLines: 20 }), { op: 0.42 }),
p2('04_drain-of-the-vacuum', 'ember', [0.22, -0.16], pg([0.05, -0.05], 0, F, 'cool', rip(0.07, 0.06, 3.0, 6), { rMax: 2.5, rays: 36, depthLines: 22, depthPow: 3.0 }), { op: 0.44 }),
p2('05_scaffolding-of-space', 'copper', [-0.24, -0.1], pg([-1.3, -1.05], 35 * DEG, 60 * DEG, 'ash', rip(0.05, 0.04, 2.0, 4), { rMax: 3.6, rays: 24 }), { op: 0.46 }),
p2('06_what-the-field-rests-on', 'gold', [0.24, 0.12], pg([0, -0.6], 95 * DEG, 160 * DEG, 'cool', rip(0.07, 0.06, 2.4, 5), EDGE), { pos: 'behind', op: 0.5 }),
p2('07_rumor-of-order', 'rose', [-0.18, 0.18], pg([-1.2, -1.0], 38 * DEG, 70 * DEG, 'violet', rip(0.06, 0.05, 2.2, 5, 0.4), { rMax: 3.4 }), { op: 0.36 }),
p2('08_the-unseen-floor', 'orange', [-0.2, -0.14], pg([0, 0.5], -90 * DEG, 170 * DEG, 'warm', rip(0.08, 0.07, 2.6, 6), EDGE), { op: 0.42 }),
p2('09_two-infinities', 'amber', [0.2, 0.16], pg([0, -0.03], 0, F, 'cool', rip(0.05, 0.05, 2.0, 5), EDGE), { op: 0.36 }),
p2('10_net-over-the-deep', 'teal', [0.26, -0.12], pg([0.05, -0.05], 0, F, 'teal', rip(0.11, 0.09, 2.9, 7, 0.8), EDGE), { op: 0.4 }),
p2('11_measured-and-the-wild', 'ember', [0.24, 0.14], pg([1.35, -0.1], 178 * DEG, 75 * DEG, 'ash', rip(0.05, 0.04, 2.1, 4), { rMax: 3.4, rays: 24 }), { op: 0.46 }),
p2('12_standing-wave-cathedral', 'gold', [-0.18, -0.12], pg([0, -0.68], 95 * DEG, 150 * DEG, 'violet', rip(0.12, 0.1, 3.0, 8, 1.3), { rays: 28 }), { pos: 'behind', op: 0.5 }),
p2('13_a-point-that-isnt-there', 'copper', [0.26, -0.1], pg([1.6, -1.3], 215 * DEG, 55 * DEG, 'cool', rip(0.06, 0.05, 2.3, 5), { rMax: 4.2 }), { op: 0.34 }),
p2('14_architecture-of-absence', 'orange', [0.24, 0.12], pg([0, -0.02], 0, F, 'warm', rip(0.05, 0.05, 2.2, 5), EDGE), { op: 0.28 }),
p2('15_ripple-vortex-rose', 'rose', [0.22, -0.14], pg([0.04, -0.04], 0, F, 'teal', rip(0.1, 0.09, 3.1, 7, 0.5), { rMax: 2.6, rays: 34, depthPow: 2.8 }), { op: 0.42 }),
p2('16_cold-sun-cold-grid', 'teal', [-0.22, -0.1], pg([0, -0.04], 0, F, 'teal', rip(0.07, 0.06, 2.5, 6), EDGE), { op: 0.44 }),
p2('17_ghost-corridor', 'amber', [0.26, 0.12], pg([-1.25, -0.2], 20 * DEG, 70 * DEG, 'cool', rip(0.06, 0.05, 2.2, 5), { rMax: 3.4 }), { pos: 'behind', op: 0.5 }),
p2('18_mandala-of-depth', 'magenta', [0.2, -0.12], pg([0, -0.03], 0, F, 'ash', rip(0.09, 0.08, 2.7, 8, 0.9), { rays: 36, depthLines: 20 }), { op: 0.4 }),
p2('19_the-grid-dissolving', 'ember', [-0.2, -0.12], pg([0.05, -0.05], 0, F, 'warm', rip(0.13, 0.11, 3.2, 7, 1.0), EDGE), { op: 0.26 }),
p2('20_quiet-coordinates', 'gold', [0.24, 0.12], pg([0.4, -0.5], 100 * DEG, 110 * DEG, 'ash', rip(0.05, 0.04, 2.0, 5), {}), { op: 0.42 }),
];
console.log(`PERSPECTIVE2 (20 rippled-grid iterations) — ${PERSP2.length}${OUT}/perspective2/`);
for (const v of PERSP2) compose(v);
{
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
writeFileSync(`${OUT}/perspective2/m.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>perspective2 · rippled depth grid · 20</title>
<style>html,body{margin:0;background:#1a1a1a}.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">${PERSP2.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}</figcaption></figure>`).join('')}</div></body></html>`);
}
{
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
writeFileSync(`${OUT}/vast/m.html`, `<!DOCTYPE html><html><head><meta charset="utf-8">

129
tools/perspective3.mjs Normal file
View File

@@ -0,0 +1,129 @@
/* ============================================================
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`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>perspective3 · schema-built · decoupled</title>
<style>html,body{margin:0;background:#1a1a1a}.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 sheet -> ${OUT}/m.html`);

61
tools/perspective4.mjs Normal file
View File

@@ -0,0 +1,61 @@
/* ============================================================
perspective4.mjs — Field 2 reborn as a STRAIGHT perspective floor
grid (one/two-point), through the grouped schema. Constructed space
receding to a vanishing point, under the decoupled sun + collision.
Usage: node tools/perspective4.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/perspective4';
mkdirSync(OUT, { recursive: true });
const 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', style: 'floor', transform: { x: 0, y: 0, rotation: 0, scale: 1 },
color: { hue: 0.56, hue2: 0.5, sat: 0.26, lightNear: 0.32, lightFar: 0.64 }, opacity: 0.4,
pitch: 28 * D, yaw: 0, persp: 1.0, dist: 3.0, nx: 16, nz: 24, originY: 0.34,
ripple: { amp: 0 }, layers: 2, blurPerLayer: [1.1, 0],
},
disk: { enabled: true, transform: { x: 0, y: -0.26, rotation: 0, scale: 0.78 }, hue: 0.06, sat: 0.82, size: 0.16, pressure: 0.85 },
bubble: { enabled: true, transform: { x: 0.28, y: -0.1, rotation: 10, scale: 0.78 }, 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 V = [
// 1 — a floor receding to its OWN horizon, below the sea's: two horizons.
mk('01_second-horizon', { fieldGrid: { originY: 0.46, pitch: 22 * D, nz: 28, opacity: 0.4 }, bubble: { transform: { x: 0.3, y: -0.08 } } }),
// 2 — a two-point room: the event measured in built space.
mk('02_two-point-room', { fieldGrid: { yaw: 24 * D, pitch: 26 * D, dist: 2.8, opacity: 0.44 }, fiduciaries: { corners: true } }),
// 3 — the floor itself tilted (field-layer rotation) under a low sun.
mk('03_tilted-floor', { fieldGrid: { transform: { rotation: 9, scale: 1.05 }, pitch: 30 * D, opacity: 0.42 }, disk: { transform: { x: -0.04, y: -0.24 } }, bubble: { transform: { x: 0.26, y: -0.06, rotation: -6 } } }),
// 4 — ghost floor behind the sea: constructed space half-sunk.
mk('04_ghost-floor-behind', { fieldGrid: { pos: 'behind', opacity: 0.36, pitch: 24 * D, originY: 0.4 } }),
// 5 — steep tight tiles crowding toward a low vanishing point.
mk('05_steep-tiles', { fieldGrid: { pitch: 46 * D, dist: 2.4, nx: 14, nz: 20, originY: 0.2, opacity: 0.44 }, disk: { transform: { x: 0, y: 0.05, scale: 0.7 } }, bubble: { transform: { x: 0.22, y: 0.16 } } }),
// 6 — the floor breathes: a gentle ripple, still legibly a grid.
mk('06_gentle-rippled-floor', { fieldGrid: { ripple: { amp: 0.55, freqI: 0.5, freqK: 0.35, phase: 0.4 }, pitch: 26 * D, opacity: 0.42 } }),
// 7 — coordinate-system dwarfs the event: zoomed grid, tiny far ember.
mk('07_zoomed-grid-tiny-event', { fieldGrid: { transform: { scale: 1.45, x: -0.08 }, pitch: 24 * D, dist: 2.6, opacity: 0.42 }, disk: { transform: { x: 0.44, y: -0.4, scale: 0.42 }, hue: 0.0 }, bubble: { transform: { x: 0.4, y: -0.3, scale: 0.42 } }, fiduciaries: { from: [0.1, -0.2] } }),
// 8 — teal built-space, magenta event: cool/warm two-colour.
mk('08_teal-grid-magenta', { fieldGrid: { color: { hue: 0.5, hue2: 0.55, sat: 0.34, lightNear: 0.34, lightFar: 0.62 }, opacity: 0.46, pitch: 26 * D, yaw: 14 * D } }),
// 9 — two-point, vanishing off to the side; the event near the far VP.
mk('09_two-point-shifted', { fieldGrid: { yaw: -28 * D, pitch: 22 * D, originX: 0.18, dist: 2.6, opacity: 0.44 }, disk: { transform: { x: -0.35, y: -0.22, scale: 0.62 } }, bubble: { transform: { x: 0.34, y: -0.04 } }, fiduciaries: { from: [-0.3, -0.34] } }),
// 10 — the floor dissolving into the paper: a warm near-blank coordinate ghost.
mk('10_dissolving-floor', { fieldGrid: { color: { hue: 0.1, hue2: 0.08, sat: 0.16, lightNear: 0.46, lightFar: 0.68 }, opacity: 0.3, pitch: 20 * D, nz: 30, originY: 0.42 }, disk: { hue: 0.04 } }),
];
console.log(`perspective4 — straight floor grid · ${V.length} schema comps → ${OUT}/`);
for (const v of V) { 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`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>perspective4 · straight floor grid</title>
<style>html,body{margin:0;background:#1a1a1a}.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">${V.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}</figcaption></figure>`).join('')}</div></body></html>`);
console.log(`-> ${OUT}/m.html`);

24
tools/qft-perspfloor.mjs Normal file
View File

@@ -0,0 +1,24 @@
/* qft-perspfloor.mjs — sweep the straight-lined perspective floor grid.
Usage: node tools/qft-perspfloor.mjs [size] */
import { writeFileSync, mkdirSync } from 'node:fs';
import { perspFloorSVG } from '../src/qft/perspgrid.js';
const SIZE = +(process.argv[2] || 1400);
const OUT = 'output/qft/perspfloor';
mkdirSync(OUT, { recursive: true });
const D = Math.PI / 180;
const S = [
{ name: '01_floor-1pt', label: 'one-point floor to horizon', o: { pitch: 30 * D, persp: 1.0, dist: 3.0, nx: 16, nz: 24, originY: 0.34 } },
{ name: '02_floor-shallow', label: 'shallow pitch · distant horizon', o: { pitch: 16 * D, persp: 1.1, dist: 3.2, nx: 18, nz: 28, originY: 0.42 } },
{ name: '03_floor-2pt', label: 'two-point (yaw 22°)', o: { pitch: 28 * D, yaw: 22 * D, persp: 1.1, dist: 2.8, nx: 16, nz: 22, originY: 0.32 } },
{ name: '04_steep-tile', label: 'steeper · tighter tiles', o: { pitch: 46 * D, persp: 1.2, dist: 2.4, nx: 14, nz: 20, originY: 0.2 } },
{ name: '05_dense-fine', label: 'dense fine grid', o: { pitch: 26 * D, persp: 1.0, dist: 3.0, nx: 26, nz: 38, originY: 0.36, stroke: 1.0, strokeFar: 0.3 } },
{ name: '06_gentle-ripple', label: 'gentle floor ripple (still straight-ish)', o: { pitch: 28 * D, persp: 1.0, dist: 3.0, nx: 16, nz: 24, originY: 0.34, rippleAmp: 0.5, rippleFreqI: 0.5, rippleFreqK: 0.35 } },
{ name: '07_teal-zoom', label: 'teal · zoomed in', o: { pitch: 24 * D, persp: 1.0, dist: 2.6, scale: 1.3, nx: 16, nz: 26, originY: 0.34, hue: 0.5, hue2: 0.55, sat: 0.34 } },
{ name: '08_2pt-shifted', label: 'two-point · shifted VP', o: { pitch: 22 * D, yaw: -28 * D, persp: 1.15, dist: 2.6, nx: 18, nz: 24, originX: 0.2, originY: 0.3 } },
];
console.log(`perspfloor sweep (${S.length}) → ${OUT}/`);
for (const v of S) { writeFileSync(`${OUT}/${v.name}.svg`, perspFloorSVG(SIZE, { ...v.o, mode: 'solid' })); console.log(` ${v.name}${v.label}`); }
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:2100px}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:13px ui-monospace,monospace;color:#fff;background:linear-gradient(transparent,#000d)}</style></head><body>
<div class="grid">${S.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.label}</figcaption></figure>`).join('')}</div></body></html>`);
console.log(`-> ${OUT}/m.html`);

35
tools/qft-perspgrid.mjs Normal file
View File

@@ -0,0 +1,35 @@
/* qft-perspgrid.mjs — sweep the perspective depth grid (VP off-horizon),
plus a dual blur/fine plate deck. Usage: node tools/qft-perspgrid.mjs [size] */
import { writeFileSync, mkdirSync } from 'node:fs';
import { perspectiveGridSVG } from '../src/qft/perspgrid.js';
const SIZE = +(process.argv[2] || 1400);
const ROOT = 'output/qft/perspgrid';
mkdirSync(`${ROOT}/explore`, { recursive: true });
mkdirSync(`${ROOT}/deck`, { recursive: true });
const D = Math.PI / 180;
const EXPLORE = [
{ name: '01_center-funnel', label: 'VP centre · full radial funnel', o: { vp: [0, 0], spread: Math.PI * 2, rays: 30, depthLines: 16 } },
{ name: '02_high-vp-fan', label: 'VP high (sky) · fan downward', o: { vp: [0, -0.55], dir: 95 * D, spread: 150 * D, rays: 26 } },
{ name: '03_low-vp-into-sea', label: 'VP low (in the sea) · fan upward', o: { vp: [0, 0.5], dir: -90 * D, spread: 150 * D, rays: 26 } },
{ name: '04_corner-upper-left', label: 'VP off-frame upper-left · diagonal', o: { vp: [-1.3, -1.1], dir: 35 * D, spread: 60 * D, rays: 22, rMax: 3.4 } },
{ name: '05_offright-corridor', label: 'VP off-frame right · corridor', o: { vp: [1.4, -0.1], dir: 178 * D, spread: 70 * D, rays: 22, rMax: 3.2 } },
{ name: '06_center-tight-tunnel', label: 'VP centre · tight crowded tunnel', o: { vp: [0.1, -0.1], spread: Math.PI * 2, rays: 40, depthLines: 22, depthPow: 3.0 } },
{ name: '07_upper-cone-wide', label: 'VP upper · wide shallow cone', o: { vp: [0.2, -0.8], dir: 100 * D, spread: 120 * D, rays: 24, depthPow: 1.8 } },
{ name: '08_teal-mandala', label: 'VP centre · teal radial mandala-grid', o: { vp: [0, -0.05], spread: Math.PI * 2, rays: 36, depthLines: 18, hue: 0.5, hue2: 0.55, sat: 0.5 } },
];
console.log(`perspgrid · explore (${EXPLORE.length}) → ${ROOT}/explore/`);
for (const v of EXPLORE) { writeFileSync(`${ROOT}/explore/${v.name}.svg`, perspectiveGridSVG(SIZE, { ...v.o, mode: 'solid' })); console.log(` ${v.name}${v.label}`); }
writeFileSync(`${ROOT}/explore/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:2100px}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:13px ui-monospace,monospace;color:#fff;background:linear-gradient(transparent,#000d)}</style></head><body>
<div class="grid">${EXPLORE.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name.replace(/^\d+_/, '').replace(/-/g, ' ')} · ${v.label}</figcaption></figure>`).join('')}</div></body></html>`);
// dual deck: back (thick, will be blurred in composite) + front (fine, crisp), VP slightly nudged → parallax
const deckBase = { mode: 'plate', vp: [0.05, -0.12], spread: 150 * D, dir: 95 * D, rays: 26, depthLines: 16, hue: 0.55, hue2: 0.5, sat: 0.45 };
writeFileSync(`${ROOT}/deck/P2_back.svg`, perspectiveGridSVG(SIZE, { ...deckBase, salt: 'pA', stroke: 2.2, strokeFar: 0.8, vp: [0.02, -0.14] }));
writeFileSync(`${ROOT}/deck/P1_front.svg`, perspectiveGridSVG(SIZE, { ...deckBase, salt: 'pB', stroke: 1.0, strokeFar: 0.36, vp: [0.08, -0.10] }));
writeFileSync(`${ROOT}/deck/stack.html`, `<!DOCTYPE html><html><head><meta charset="utf-8">
<style>html,body{margin:0;background:#0c0c0c}.stage{position:relative;width:${SIZE}px;height:${SIZE}px;margin:24px auto;background:rgb(226,219,199)}.stage img{position:absolute;inset:0;width:100%;height:100%}.b{filter:blur(2.2px);opacity:.55}.f{opacity:.95}</style></head><body>
<div class="stage"><img class="b" src="P2_back.svg"><img class="f" src="P1_front.svg"></div></body></html>`);
console.log(`deck -> ${ROOT}/deck/stack.html ; explore -> ${ROOT}/explore/m.html`);

View File

@@ -0,0 +1,20 @@
/* render-composition.mjs — render a grouped composition file to SVG.
Usage: node tools/render-composition.mjs <composition.mjs> [out.svg] [size]
*/
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { renderComposition } from '../src/compose/composition.js';
const file = process.argv[2];
if (!file) { console.error('usage: node tools/render-composition.mjs <composition.mjs> [out.svg] [size]'); process.exit(1); }
const out = process.argv[3] || '/tmp/composition.svg';
const size = process.argv[4] ? +process.argv[4] : undefined;
const mod = await import(pathToFileURL(resolve(file)).href);
const comp = mod.composition || mod.default;
if (!comp) { console.error('composition file must `export const composition = {...}`'); process.exit(1); }
const svg = renderComposition(comp, size);
writeFileSync(out, svg);
console.log(`composition -> ${out} (${(svg.length / 1024).toFixed(0)} KB, size=${size || comp.size || 1500})`);