Files
bubblechambersimart/tools/qft-perspective-sweep.mjs

72 lines
3.9 KiB
JavaScript

/* ============================================================
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 = `<!DOCTYPE html><html><head><meta charset="utf-8">
<style>html,body{margin:0;background:#222}.grid{display:grid;grid-template-columns:repeat(3,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">
${SWEEP.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}</figcaption></figure>`).join('\n')}
</div></body></html>`;
writeFileSync(`${OUT}/m.html`, m);
console.log(`contact sheet -> ${OUT}/m.html`);