/* ============================================================ qft-perspective-sweep.mjs — workshop the QFT cartesian/wavy grid viewpoint. Isolates the cubic lattice (other fields off) and sweeps the new camera: yaw / pitch / roll / perspective / distance. Usage: node tools/qft-perspective-sweep.mjs [size] ============================================================ */ import { writeFileSync, mkdirSync } from 'node:fs'; import { generateQFTScene } from '../src/qft/scene.js'; import { paramsFromSeed } from '../src/qft/params.js'; import { renderQFTSVG } from '../src/qft/renderer.js'; const SIZE = +(process.argv[2] || 1100); const OUT = 'output/qft/perspective'; mkdirSync(OUT, { recursive: true }); const SEED = 'LATTICE-1003'; const F = (h0, h1, s, l, o, st) => ({ hueStart: h0, hueEnd: h1, saturation: s, lightness: l, opacity: o, stroke: st }); const OFF = F(0, 0, 0, 0.5, 0, 0); // isolate the cartesian grid: cubic only, mid teal, thin-ish wavy photon edges const BASE = { substrate: 'cream', showHeader: false, glow: 0.16, vign: 0.12, cubicN: 1, photonCyclesPerUnit: 7, segmentsPerEdge: 12, stroke: 1.3, cubicScale: 1.05, cubicRot: 0, linkCount: 0, e8Count: 0, fields: { cubic: F(0.52, 0.57, 0.6, 0.34, 1.0, 1.3), schlegel: OFF, e8: OFF, ripple: OFF, links: OFF, }, }; const D = Math.PI / 180; const cam = (name, label, c) => ({ name, label, cam: c }); const SWEEP = [ // baseline isometric cam('01_iso-default', 'isometric · default 3/4', { }), // yaw (spin) at iso pitch cam('02_yaw-front', 'yaw 0° · facing a face', { yaw: 0, pitch: 35 * D }), cam('03_yaw-deep', 'yaw -70° · spun round', { yaw: -70 * D, pitch: 35 * D }), // pitch (tip) cam('04_pitch-low', 'pitch 12° · near eye-level', { yaw: -45 * D, pitch: 12 * D }), cam('05_pitch-steep', 'pitch 58° · looking down', { yaw: -45 * D, pitch: 58 * D }), cam('06_pitch-top', 'pitch 78° · near top-down', { yaw: -45 * D, pitch: 78 * D }), // perspective (vanishing point) at the 3/4 angle cam('07_persp-mild', 'perspective 0.45 · gentle depth', { persp: 0.45, dist: 4.2 }), cam('08_persp-strong','perspective 0.9 · dist 2.6 · dramatic', { persp: 0.9, dist: 2.6 }), cam('09_persp-corner','into a corner · 1-pt-ish', { yaw: 30 * D, pitch: 30 * D, persp: 0.7, dist: 3.0 }), // roll / cant cam('10_roll-cant', 'roll 18° · canted + persp 0.4', { roll: 18 * D, persp: 0.4, dist: 3.6 }), // dramatic hero angles cam('11_hero-tunnel', 'low + strong persp · tunnel', { yaw: -45 * D, pitch: 18 * D, persp: 0.95, dist: 2.3 }), cam('12_hero-vault', 'steep + persp · vaulted ceiling', { yaw: -20 * D, pitch: 62 * D, persp: 0.8, dist: 2.8 }), ]; console.log(`Perspective sweep (${SWEEP.length}) → ${OUT}/ seed=${SEED}`); for (const v of SWEEP) { const p = { ...paramsFromSeed(SEED), ...BASE, cubicYaw: v.cam.yaw ?? -45 * D, cubicPitch: v.cam.pitch ?? 35.26 * D, cubicRoll: v.cam.roll ?? 0, cubicPersp: v.cam.persp ?? 0, cubicDist: v.cam.dist ?? 3.4, }; writeFileSync(`${OUT}/${v.name}.svg`, renderQFTSVG(generateQFTScene(p), p, SIZE)); console.log(` ${v.name} — ${v.label}`); } const cap = (v) => `${v.name.replace(/^\d+_/, '').replace(/-/g, ' ')} · ${v.label}`; const m = `