Added Ridgeline Plots and layers output
This commit is contained in:
40
tools/archive-wall.html
Normal file
40
tools/archive-wall.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<title>The Archive — typology wall mock</title>
|
||||
<style>
|
||||
html,body{margin:0;background:#16140f;color:#cabfa6}
|
||||
/* warm gallery wall, even hang, consistent frames + museum labels */
|
||||
body{padding:64px 56px 80px;font:13px/1.5 ui-monospace,"SFMono-Regular",Menlo,monospace}
|
||||
.head{letter-spacing:.34em;text-transform:uppercase;font-size:13px;color:#9fb7af;margin:0 0 4px}
|
||||
.sub{color:#6f6a5c;font-size:12px;margin:0 0 40px;letter-spacing:.06em}
|
||||
.wall{display:grid;grid-template-columns:repeat(4,1fr);gap:44px 40px}
|
||||
figure{margin:0}
|
||||
.mat{background:#f3ecdb;padding:16px;box-shadow:0 2px 0 #0008,0 18px 40px -18px #000c;border:1px solid #000}
|
||||
.mat img{display:block;width:100%;aspect-ratio:1/1;object-fit:cover;background:#fff}
|
||||
figcaption{margin-top:11px;display:flex;justify-content:space-between;gap:10px;align-items:baseline}
|
||||
.cat{color:#9fb7af;letter-spacing:.12em}
|
||||
.ttl{color:#cabfa6;flex:1;text-align:center}
|
||||
.kind{color:#6f6a5c;text-transform:uppercase;letter-spacing:.1em;font-size:11px}
|
||||
</style></head><body>
|
||||
<p class="head">Traces of the Invisible · a catalogue</p>
|
||||
<p class="sub">one fabricated archive · evidence (bubble chamber) · cause (field) · synthesis · A3 plates, seeds as catalogue numbers</p>
|
||||
<div class="wall" id=w></div>
|
||||
<script>
|
||||
const items=[
|
||||
["output/iterations-claude-craft/78_final-lively-magenta-cream.svg","BC·001","Lively magenta event","evidence"],
|
||||
["output/qft/sketch07/02_single-huge-nautilus.svg","QF·011","Single E8 nautilus","cause"],
|
||||
["output/iterations-claude-craft/03_boron-bluered-cream.svg","BC·014","Boron · blue/red","evidence"],
|
||||
["output/qft/sketch05/10_concentric-rosettes-pale-rose.svg","QF·024","Concentric rosettes","cause"],
|
||||
["output/qft-bc/sketch01/05_scale-rhyme-nautilus.svg","SY·002","Scale rhyme · nautilus","synthesis"],
|
||||
["output/iterations-claude-craft/22_kind-warm-relic.svg","BC·022","Kind · warm relic","evidence"],
|
||||
["output/qft/sketch04/06_bone-radial-gradient.svg","QF·046","Bone radial field","cause"],
|
||||
["output/iterations-claude-craft/26_kind-selenium-soft.svg","BC·026","Kind · selenium","evidence"],
|
||||
["output/qft/sketch05/02_dense-radial-cream.svg","QF·052","Dense radial lattice","cause"],
|
||||
["output/iterations-claude-craft/10_boron-gold-olive.svg","BC·010","Boron · gold/olive","evidence"],
|
||||
["output/qft-bc/sketch02/10_trace-family-communion.svg","SY·010","Communion","synthesis"],
|
||||
["output/iterations-claude-craft/16_warm-halo-nocturne.svg","BC·016","Warm halo nocturne","evidence"],
|
||||
];
|
||||
w.innerHTML=items.map(([src,cat,ttl,kind])=>
|
||||
`<figure><div class="mat"><img src="../${src}"></div>
|
||||
<figcaption><span class="cat">${cat}</span><span class="ttl">${ttl}</span></figcaption>
|
||||
<div style="text-align:right" class="kind">${kind}</div></figure>`).join("");
|
||||
</script></body></html>
|
||||
140
tools/layering.mjs
Normal file
140
tools/layering.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
/* ============================================================
|
||||
layering.mjs — the assembled piece: FILM/diffusion (back) + a QFT
|
||||
vacuum-carpet DECK (middle, 3 spaced plate sheets) + a BUBBLE-CHAMBER
|
||||
event (front), composited as one image (and the literal plexi stack).
|
||||
Produces variations into output/layering/.
|
||||
Usage: node tools/layering.mjs [size]
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1500);
|
||||
const OUT = 'output/layering';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = SIZE / 1000;
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
// ---- FILM / diffusion: milky clouds (light, soft) the light diffuses through ----
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
// ---- film GRAIN veil (fine, dark, low opacity) on top ----
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
// ---- bubble-chamber event ----
|
||||
function bcSVG(seed, over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(seed) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
p.invert = true; p.showHeader = false;
|
||||
Object.assign(p, over);
|
||||
return renderSVG(generateScene(p), p, SIZE);
|
||||
}
|
||||
|
||||
// build a 3-sheet carpet deck for a variation (hue gradient + chaos rising to front)
|
||||
function deck(c) {
|
||||
const base = { mode: 'plate', rows: 46, horizon: c.horizon ?? 0.37, wFar: 0.58, wNear: 0.7,
|
||||
overlap: c.overlap ?? 1.7, mound: c.mound ?? 0.35, sat: c.sat ?? 0.58, lightNear: 0.33, lightFar: 0.56, blips: c.blips ?? 1.0 };
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2; // 0 back → 1 front
|
||||
return carpetSVG(SIZE, { ...base, salt: 'field' + i,
|
||||
hue: lerp(c.hueBack, c.hueFront, t), hue2: lerp(c.hueBack, c.hueFront, t) + 0.035,
|
||||
chaos: lerp((c.chaos ?? 0.6) * 0.8, c.chaos ?? 0.6, t) });
|
||||
});
|
||||
}
|
||||
|
||||
function compose(v) {
|
||||
const film = dataUri(filmSVG(v.film));
|
||||
const sheets = deck(v.carpet).map(dataUri);
|
||||
const bc = dataUri(bcSVG(v.bcSeed, v.bcOver));
|
||||
const grain = dataUri(grainSVG(v.grain));
|
||||
const base = v.base || 'rgb(226,219,199)';
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="b3" x="-5%" y="-5%" width="110%" height="110%"><feGaussianBlur stdDeviation="${(2.4 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-5%" y="-5%" width="110%" height="110%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
</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"/>
|
||||
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${bc}" 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"/>
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} (bc=${v.bcSeed})`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Variations — film + carpet + event, ranging mood & "loudness".
|
||||
// Guiding thesis: calm sea, loud event.
|
||||
// ============================================================
|
||||
const VARIATIONS = [
|
||||
{ name: '01_mono-calm-sea', base: 'rgb(228,221,201)',
|
||||
film: { seed: 3, density: 0.5, tone: [236, 228, 208] }, grain: { amount: 0.4 },
|
||||
carpet: { hueBack: 0.58, hueFront: 0.50, chaos: 0.35, blips: 0.7, mound: 0.32 },
|
||||
bcSeed: 'LAMBDA-2648', bcOver: { palette: 'mono' } },
|
||||
|
||||
{ name: '02_magenta-over-teal', base: 'rgb(228,221,201)',
|
||||
film: { seed: 8, density: 0.5 }, grain: { amount: 0.4 },
|
||||
carpet: { hueBack: 0.54, hueFront: 0.47, chaos: 0.45, blips: 1.0, mound: 0.35 },
|
||||
bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.05 } },
|
||||
|
||||
{ name: '03_quiet-vast', base: 'rgb(230,224,206)',
|
||||
film: { seed: 12, density: 0.42, tone: [238, 231, 212] }, grain: { amount: 0.32 },
|
||||
carpet: { hueBack: 0.57, hueFront: 0.52, chaos: 0.22, blips: 0.5, mound: 0.28, overlap: 1.5 },
|
||||
bcSeed: 'NUCLEON-2131', bcOver: { palette: 'mono', primaries: 7, burst: 0.4, cosmics: 2, deltaRate: 0.45 } },
|
||||
|
||||
{ name: '04_kind-verdigris', base: 'rgb(226,221,205)',
|
||||
film: { seed: 21, density: 0.5 }, grain: { amount: 0.4 },
|
||||
carpet: { hueBack: 0.45, hueFront: 0.40, chaos: 0.5, blips: 1.1, sat: 0.42, mound: 0.35 },
|
||||
bcSeed: 'HYPERON-8444', bcOver: { palette: 'kind', saturation: 1.0 } },
|
||||
|
||||
{ name: '05_ember-warm', base: 'rgb(230,222,202)',
|
||||
film: { seed: 30, density: 0.52, tone: [238, 226, 204] }, grain: { amount: 0.42 },
|
||||
carpet: { hueBack: 0.10, hueFront: 0.07, chaos: 0.5, blips: 1.0, sat: 0.5, mound: 0.34 },
|
||||
bcSeed: 'CASCADE-2755', bcOver: { palette: 'kindrise', saturation: 1.0 } },
|
||||
|
||||
{ name: '06_seethe-bold', base: 'rgb(227,220,200)',
|
||||
film: { seed: 41, density: 0.55 }, grain: { amount: 0.46 },
|
||||
carpet: { hueBack: 0.55, hueFront: 0.47, chaos: 0.8, blips: 1.4, mound: 0.4, overlap: 1.9 },
|
||||
bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.08 } },
|
||||
];
|
||||
|
||||
console.log(`Layered piece — ${VARIATIONS.length} variations → ${OUT}/`);
|
||||
for (const v of VARIATIONS) compose(v);
|
||||
|
||||
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Layered piece · film + QFT carpet + bubble chamber</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,Menlo,monospace;padding:26px;max-width:1900px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#a5d4c9}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:20px;margin-top:18px}
|
||||
figure{margin:0;background:#fff;border:1px solid #262626;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:8px 10px;color:#e8e4d8;background:#111}small{color:#777;display:block}
|
||||
.notes{color:#999;background:#0e1816;padding:14px 18px;border-left:3px solid #a5d4c9;margin:18px 0}</style></head><body>
|
||||
<h1>Layered piece — film · QFT vacuum carpet · bubble-chamber event</h1>
|
||||
<div class="notes">back→front: cream ground · milky film/diffusion · 3 spaced QFT carpet sheets (back two blurred = air-gap depth of field) · bubble-chamber event (multiply) · film grain. The literal plexi stack, previewed as one image.</div>
|
||||
<div class=grid>${VARIATIONS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}<small>${v.bcSeed} · ${v.bcOver?.palette || 'mono'}</small></figcaption></figure>`).join('\n')}</div></body></html>`);
|
||||
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: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 sheets -> ${OUT}/index.html , m.html`);
|
||||
59
tools/qft-bc-composite.mjs
Normal file
59
tools/qft-bc-composite.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
/* ============================================================
|
||||
qft-bc-composite.mjs — quick play: a QFT field plate UNDERNEATH
|
||||
a bubble-chamber plate. Both render to standalone SVG; we embed
|
||||
each as a self-contained data-URI <image> in one outer SVG (no
|
||||
defs/id collisions), and put the bubble chamber on top with
|
||||
mix-blend-mode:multiply — the lightbox metaphor: the field glows
|
||||
underneath, the dark ink darkens it, the light paper drops out.
|
||||
|
||||
Usage:
|
||||
node tools/qft-bc-composite.mjs [qftSeed] [bcSeed] [out.html] [size]
|
||||
============================================================ */
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
// --- QFT side ---
|
||||
import { generateQFTScene } from '../src/qft/scene.js';
|
||||
import { paramsFromSeed as qftParams } from '../src/qft/params.js';
|
||||
import { renderQFTSVG } from '../src/qft/renderer.js';
|
||||
|
||||
// --- bubble chamber side ---
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const qftSeed = argv[0] || 'VACUUM-5113';
|
||||
const bcSeed = argv[1] || 'LAMBDA-2648';
|
||||
const out = argv[2] || '/tmp/qft-bc.html';
|
||||
const SIZE = +(argv[3] || 1600);
|
||||
|
||||
// pale QFT substrate so it reads as a luminous ground under the ink
|
||||
const qp = qftParams(qftSeed);
|
||||
qp.substrate = 'cream'; // force a light ground so the multiply'd BC ink reads
|
||||
qp.glow = 0.6;
|
||||
|
||||
// bubble chamber: default mono, light cream paper → multiply lets the field through
|
||||
const bp = { ...FIXED, ...bcParams(bcSeed) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in bp)) bp[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in bp)) bp[t.id] = t.value;
|
||||
|
||||
const qftSvg = renderQFTSVG(generateQFTScene(qp), qp, SIZE);
|
||||
const bcSvg = renderSVG(generateScene(bp), bp, SIZE);
|
||||
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
const composite =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${dataUri(qftSvg)}"/>
|
||||
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${dataUri(bcSvg)}" style="mix-blend-mode:multiply"/>
|
||||
</svg>`;
|
||||
|
||||
const html =
|
||||
`<!doctype html><meta charset="utf-8">
|
||||
<style>html,body{margin:0;background:#222}img,svg{display:block}</style>
|
||||
${composite}`;
|
||||
|
||||
writeFileSync(out, html);
|
||||
writeFileSync(out.replace(/\.html$/, '.svg'), composite);
|
||||
console.log(`composite -> ${out} (qft=${qftSeed} ${qp.archetype}, bc=${bcSeed}, ${SIZE}px)`);
|
||||
177
tools/qft-bc-magenta.mjs
Normal file
177
tools/qft-bc-magenta.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
/* ============================================================
|
||||
qft-bc-magenta.mjs — sketch 02 of the JOINED series.
|
||||
Built on 78_final-lively-magenta-cream (seed MESON-5113,
|
||||
palette magentarise, cream paper). A THIN-LINE but present
|
||||
QFT field is slipped BETWEEN the cream ground and the bubble
|
||||
chamber: cream paper → thin QFT lattice → magentarise event
|
||||
(multiply on top). The bubble-chamber layer is constant; each
|
||||
frame varies the QFT geometry AND its hue family, chosen to
|
||||
converse with the magenta tracks + burnt-orange disk + cream.
|
||||
Usage: node tools/qft-bc-magenta.mjs [size]
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
|
||||
import { generateQFTScene } from '../src/qft/scene.js';
|
||||
import { paramsFromSeed as qftParams } from '../src/qft/params.js';
|
||||
import { renderQFTSVG } from '../src/qft/renderer.js';
|
||||
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1700);
|
||||
const OUT = 'output/qft-bc/sketch02';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
// F(hueStart,hueEnd,sat,light,opacity[,stroke]) — thin strokes throughout.
|
||||
const F = (h0, h1, s, l, o, st = 0.6) => ({ hueStart: h0, hueEnd: h1, saturation: s, lightness: l, opacity: o, stroke: st });
|
||||
const OFF = F(0, 0, 0, 0.5, 0);
|
||||
const paper = (flat, gi = [10, 9, 8], go = [-16, -16, -16]) => ({
|
||||
flat, glowIn: [flat[0] + gi[0], flat[1] + gi[1], flat[2] + gi[2]], glowOut: [flat[0] + go[0], flat[1] + go[1], flat[2] + go[2]],
|
||||
});
|
||||
const W = (x, y, amplitude, sigma) => ({ x, y, amplitude, sigma });
|
||||
const VX = (x, y, strength, sigma) => ({ x, y, strength, sigma });
|
||||
const SW = (kx, ky, amplitude, phase = 0) => ({ kx, ky, amplitude, phase });
|
||||
const R = (x, y, count = 6, r0 = 0.08, dR = 0.10, propagator = 'photon') => ({ x, y, count, r0, dR, propagator });
|
||||
|
||||
const CREAM = [236, 228, 208]; // pale cream so the BC's own paper carries the tone
|
||||
|
||||
// thin-line QFT base: light vignette/glow so the ground stays flat under the BC.
|
||||
const QBASE = {
|
||||
substrate: 'cream', paperOverride: paper(CREAM), vignOverride: [120, 110, 90],
|
||||
showHeader: false, glow: 0.18, vign: 0.10, stroke: 0.7, segmentsPerEdge: 8,
|
||||
photonCyclesPerUnit: 12, linkCurvature: 0.18,
|
||||
};
|
||||
|
||||
// constant bubble-chamber layer — the 78 piece (lively magenta on cream).
|
||||
function bcLayer() {
|
||||
const p = { ...FIXED, ...bcParams('MESON-5113') };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
p.palette = 'magentarise'; p.paperTone = 'cream'; p.invert = true;
|
||||
p.showHeader = false; p.saturation = 1.05; // a touch livelier
|
||||
return p;
|
||||
}
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
function composite(v) {
|
||||
const qp = { ...qftParams(v.qftSeed), ...QBASE, ...(v.qftOver || {}) };
|
||||
const bp = bcLayer();
|
||||
const qftSvg = renderQFTSVG(generateQFTScene(qp), qp, SIZE);
|
||||
const bcSvg = renderSVG(generateScene(bp), bp, SIZE);
|
||||
const s = v.qScale ?? 1, qw = SIZE * s, qh = SIZE * s;
|
||||
const qx = (SIZE - qw) / 2 + (v.qDx || 0), qy = (SIZE - qh) / 2 + (v.qDy || 0);
|
||||
const out =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<rect width="${SIZE}" height="${SIZE}" fill="rgb(${CREAM.join(',')})"/>
|
||||
<image x="${qx.toFixed(0)}" y="${qy.toFixed(0)}" width="${qw.toFixed(0)}" height="${qh.toFixed(0)}" href="${dataUri(qftSvg)}" opacity="${v.qOpacity ?? 1}"/>
|
||||
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${dataUri(bcSvg)}" style="mix-blend-mode:multiply"/>
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, out);
|
||||
console.log(` ${v.name} (qft=${v.qftSeed}/${qp.archetype})`);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 10 hue families × QFT geometries, all under the same magenta event.
|
||||
// ============================================================
|
||||
const VARIATIONS = [
|
||||
// 1 — TONAL ECHO. Field in the same magenta family, paler & desaturated:
|
||||
// the trace's colour, whispered, as its own substrate.
|
||||
{ name: '01_tonal-magenta-echo', qftSeed: 'FEYNMAN-7167',
|
||||
qftOver: { cubicN: 2, e8Count: 3, e8OriginRadius: 0.6, linkCount: 6,
|
||||
fields: { cubic: F(0.86, 0.90, 0.46, 0.50, 0.60, 0.55), schlegel: F(0.80, 0.84, 0.42, 0.48, 0.52, 0.55),
|
||||
e8: F(0.90, 0.94, 0.52, 0.50, 0.65, 0.6), ripple: OFF, links: F(0.92, 0.88, 0.58, 0.48, 0.66, 0.8) } } },
|
||||
|
||||
// 2 — THE COMPLEMENT. Teal/viridian — magenta's opposite — for maximum
|
||||
// vibrance under the warm event. A swirling vortex lattice.
|
||||
{ name: '02_magenta-teal-duotone', qftSeed: 'GAUGE-2046',
|
||||
qftOver: { cubicN: 2, e8Count: 1, e8OriginRadius: 0.64, linkCount: 6, vortices: [VX(0, 0, 0.8, 0.3)],
|
||||
fields: { cubic: F(0.47, 0.50, 0.55, 0.46, 0.58, 0.55), schlegel: F(0.50, 0.46, 0.50, 0.44, 0.50, 0.6),
|
||||
e8: F(0.45, 0.48, 0.55, 0.48, 0.55, 0.6), ripple: OFF, links: F(0.48, 0.44, 0.60, 0.46, 0.62, 0.8) } } },
|
||||
|
||||
// 3 — EMBER UNDERGLOW. Gold→copper field, picking up the burnt-orange disk:
|
||||
// the event smouldering on a bed of its own embers. Nautilus rosettes.
|
||||
{ name: '03_ember-gold-underglow', qftSeed: 'LATTICE-1003',
|
||||
qftOver: { cubicN: 2, e8Style: 'nautilus', e8Count: 3, nautilusTurns: 2.6, nautilusPerTurn: 14, nautilusGrowth: 0.2, linkCount: 6,
|
||||
fields: { cubic: F(0.10, 0.08, 0.62, 0.46, 0.62, 0.55), schlegel: F(0.08, 0.06, 0.55, 0.44, 0.52, 0.55),
|
||||
e8: F(0.12, 0.07, 0.72, 0.46, 0.70, 0.6), ripple: OFF, links: F(0.07, 0.05, 0.82, 0.44, 0.72, 0.8) } } },
|
||||
|
||||
// 4 — COOL RECEDE. Violet→indigo, the cool neighbour of magenta — the field
|
||||
// steps back into shadow while the warm trace advances. Big tesseract.
|
||||
{ name: '04_violet-indigo-recede', qftSeed: 'PROPAGATOR-2755',
|
||||
qftOver: { cubicN: 1, schlegelScale: 1.3, schlegelOuterR: 0.86, e8Count: 2, linkCount: 5,
|
||||
fields: { cubic: F(0.70, 0.74, 0.45, 0.50, 0.48, 0.5), schlegel: F(0.74, 0.78, 0.50, 0.46, 0.55, 0.6),
|
||||
e8: F(0.68, 0.72, 0.45, 0.50, 0.50, 0.6), ripple: OFF, links: F(0.76, 0.72, 0.50, 0.48, 0.55, 0.8) } } },
|
||||
|
||||
// 5 — VERDIGRIS. Desaturated sea-green patina — aged copper plate — under a
|
||||
// Chladni standing-wave membrane. Quiet, oxidised, archival.
|
||||
{ name: '05_verdigris-chladni', qftSeed: 'VACUUM-5113',
|
||||
qftOver: { cubicN: 2, e8Count: 0, linkCount: 3, standingWaves: [SW(6, 1, 0.04), SW(1, 6, 0.04)],
|
||||
fields: { cubic: F(0.44, 0.48, 0.30, 0.54, 0.55, 0.55), schlegel: F(0.46, 0.50, 0.26, 0.52, 0.45, 0.55),
|
||||
e8: OFF, ripple: OFF, links: F(0.42, 0.46, 0.35, 0.52, 0.50, 0.75) } } },
|
||||
|
||||
// 6 — SPLIT-COMPLEMENT. Teal lattice, GOLD propagator links (tied to the
|
||||
// disk accent) — a two-colour field that brackets the magenta on both sides.
|
||||
{ name: '06_teal-lattice-gold-links', qftSeed: 'FEYNMAN-7167',
|
||||
qftOver: { cubicN: 2, e8Count: 3, e8OriginRadius: 0.58, linkCount: 9, linkCurvature: 0.3,
|
||||
fields: { cubic: F(0.49, 0.52, 0.45, 0.50, 0.52, 0.55), schlegel: F(0.50, 0.47, 0.42, 0.48, 0.45, 0.55),
|
||||
e8: F(0.11, 0.08, 0.55, 0.55, 0.55, 0.6), ripple: OFF, links: F(0.12, 0.07, 0.70, 0.55, 0.7, 0.85) } } },
|
||||
|
||||
// 7 — ROSE GHOST. The thinnest, palest field — a barely-there dusty pink
|
||||
// lattice, present only as a breath. Sparse. Contemplative.
|
||||
{ name: '07_rose-ghost-quiet', qftSeed: 'GAUGE-2046',
|
||||
qftOver: { cubicN: 1, e8Count: 1, e8OriginRadius: 0.66, linkCount: 4,
|
||||
fields: { cubic: F(0.93, 0.90, 0.28, 0.66, 0.40, 0.45), schlegel: F(0.90, 0.94, 0.24, 0.64, 0.34, 0.45),
|
||||
e8: F(0.92, 0.96, 0.30, 0.66, 0.42, 0.5), ripple: OFF, links: F(0.94, 0.90, 0.34, 0.64, 0.42, 0.65) } } },
|
||||
|
||||
// 8 — PALE SPECTRAL. A faint rainbow lattice — cubic cool-blue, schlegel
|
||||
// violet, e8 gold, links rose: an earned-colour field, softened to a haze.
|
||||
{ name: '08_pale-spectral-lattice', qftSeed: 'LATTICE-1003',
|
||||
qftOver: { cubicN: 2, e8Count: 3, e8OriginRadius: 0.6, linkCount: 7,
|
||||
fields: { cubic: F(0.55, 0.60, 0.40, 0.58, 0.46, 0.5), schlegel: F(0.72, 0.78, 0.42, 0.54, 0.46, 0.55),
|
||||
e8: F(0.11, 0.14, 0.50, 0.58, 0.50, 0.6), ripple: OFF, links: F(0.93, 0.88, 0.45, 0.58, 0.5, 0.8) } } },
|
||||
|
||||
// 9 — CYAN BLUEPRINT. Pale cyan/blue draughtsman's lines with expanding
|
||||
// ripple wavefronts — the schematic under the photograph.
|
||||
{ name: '09_cyan-blueprint-ripples', qftSeed: 'PROPAGATOR-2755',
|
||||
qftOver: { cubicN: 2, e8Count: 0, linkCount: 4, ripples: [R(0, 0, 5, 0.1, 0.11)],
|
||||
fields: { cubic: F(0.54, 0.57, 0.45, 0.52, 0.52, 0.5), schlegel: F(0.56, 0.53, 0.42, 0.50, 0.45, 0.55),
|
||||
e8: OFF, ripple: F(0.55, 0.58, 0.50, 0.54, 0.55, 0.6), links: F(0.54, 0.57, 0.50, 0.50, 0.55, 0.8) } } },
|
||||
|
||||
// 10 — COMMUNION. The field sweeps the EXACT purple→magenta→pink band of the
|
||||
// trace family, at lowest saturation — field and trace are one colour,
|
||||
// one chemistry; only the burnt-orange disk stands apart. ★
|
||||
{ name: '10_trace-family-communion', qftSeed: 'FEYNMAN-7167',
|
||||
qftOver: { cubicN: 2, e8Style: 'nautilus', e8Count: 3, nautilusTurns: 2.8, nautilusPerTurn: 16, nautilusGrowth: 0.2, linkCount: 7,
|
||||
fields: { cubic: F(0.82, 0.90, 0.40, 0.50, 0.60, 0.55), schlegel: F(0.79, 0.86, 0.38, 0.48, 0.54, 0.55),
|
||||
e8: F(0.88, 0.95, 0.44, 0.50, 0.64, 0.6), ripple: OFF, links: F(0.90, 0.82, 0.50, 0.48, 0.66, 0.8) } } },
|
||||
];
|
||||
|
||||
console.log(`Compositing ${VARIATIONS.length} magenta-cream QFT×BC plates → ${OUT}/`);
|
||||
for (const v of VARIATIONS) composite(v);
|
||||
|
||||
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
|
||||
const idx = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>QFT × Bubble Chamber · sketch 02 · magenta-cream</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,Menlo,monospace;padding:26px;max-width:1900px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d6a5c4}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:20px;margin-top:18px}
|
||||
figure{margin:0;background:#fff;border:1px solid #262626;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:8px 10px;color:#e8e4d8;background:#111}small{color:#777;display:block}
|
||||
.notes{color:#999;background:#180e16;padding:14px 18px;border-left:3px solid #d6a5c4;margin:18px 0}</style></head><body>
|
||||
<h1>QFT × Bubble Chamber · sketch 02 — thin field between cream & the magenta event</h1>
|
||||
<div class="notes">Built on <b>78_final-lively-magenta-cream</b> (MESON-5113 · magentarise · cream). A thin-line but
|
||||
present QFT lattice is slipped between the cream ground and the bubble chamber (multiply on top). The event is constant;
|
||||
each frame varies the QFT geometry and a hue family chosen to converse with the magenta tracks + burnt-orange disk.</div>
|
||||
<div class=grid>
|
||||
${VARIATIONS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}<small>field: ${v.qftSeed}</small></figcaption></figure>`).join('\n')}
|
||||
</div></body></html>`;
|
||||
writeFileSync(`${OUT}/index.html`, idx);
|
||||
const m = `<!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">
|
||||
${VARIATIONS.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 sheets -> ${OUT}/index.html , m.html`);
|
||||
286
tools/qft-bc-variations.mjs
Normal file
286
tools/qft-bc-variations.mjs
Normal file
@@ -0,0 +1,286 @@
|
||||
/* ============================================================
|
||||
qft-bc-variations.mjs — sketch 01 of the JOINED series.
|
||||
A QFT field plate UNDERNEATH a bubble-chamber plate, composited
|
||||
as two self-contained data-URI <image>s in one outer SVG (no
|
||||
defs/id collision). The bubble chamber sits on top with a chosen
|
||||
mix-blend-mode — the lightbox metaphor: the field is the ground
|
||||
that authors the trace; the trace is the evidence on top.
|
||||
|
||||
Levers (per variation):
|
||||
blend 'multiply'|'screen'|'darken'|'normal'|'hard-light'
|
||||
bg outer background (matters for screen on dark)
|
||||
qScale QFT image scale about centre (>1 bleeds off-frame)
|
||||
qDx,qDy QFT pixel offset
|
||||
qOpacity, bcOpacity
|
||||
qftOver / bcOver param overrides merged into each side
|
||||
Furniture is deduplicated: by default the QFT archival header is
|
||||
OFF and the bubble chamber is the "studied / labelled" top plate.
|
||||
|
||||
Usage: node tools/qft-bc-variations.mjs [size]
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
|
||||
import { generateQFTScene } from '../src/qft/scene.js';
|
||||
import { paramsFromSeed as qftParams } from '../src/qft/params.js';
|
||||
import { renderQFTSVG } from '../src/qft/renderer.js';
|
||||
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1700);
|
||||
const OUT = 'output/qft-bc/sketch01';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
// --- helpers -------------------------------------------------
|
||||
// F(hueStart,hueEnd,sat,light,opacity[,stroke]) — one QFT field's look.
|
||||
const F = (hueStart, hueEnd, saturation, lightness, opacity, stroke) => {
|
||||
const f = { hueStart, hueEnd, saturation, lightness, opacity };
|
||||
if (stroke != null) f.stroke = stroke;
|
||||
return f;
|
||||
};
|
||||
const OFF = F(0, 0, 0, 0.5, 0);
|
||||
const paper = (flat, gi = [16, 14, 12], go = [-22, -20, -18]) => ({
|
||||
flat,
|
||||
glowIn: [flat[0] + gi[0], flat[1] + gi[1], flat[2] + gi[2]],
|
||||
glowOut: [flat[0] + go[0], flat[1] + go[1], flat[2] + go[2]],
|
||||
});
|
||||
const W = (x, y, amplitude, sigma) => ({ x, y, amplitude, sigma });
|
||||
const VX = (x, y, strength, sigma) => ({ x, y, strength, sigma });
|
||||
const SW = (kx, ky, amplitude, phase = 0) => ({ kx, ky, amplitude, phase });
|
||||
|
||||
function bcBase(seed) {
|
||||
const p = { ...FIXED, ...bcParams(seed) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
p.showHeader = false; // off unless a variation opts in
|
||||
return p;
|
||||
}
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
function composite(v) {
|
||||
const qp = { ...qftParams(v.qftSeed), showHeader: false, glow: 0.6, ...(v.qftOver || {}) };
|
||||
const bp = { ...bcBase(v.bcSeed), ...(v.bcOver || {}) };
|
||||
|
||||
// Tie BC polarity to the ground: positive (dark ink on light) for multiply/
|
||||
// darken so the field shows through; negative (light tracks on black) for
|
||||
// screen. Some seeds (e.g. NUCLEON) resolve to a negative by archetype, which
|
||||
// would crush to black under multiply — override unless set explicitly.
|
||||
const blend = v.blend || 'multiply';
|
||||
if (!(v.bcOver && 'invert' in v.bcOver)) bp.invert = (blend !== 'screen');
|
||||
|
||||
const qftSvg = renderQFTSVG(generateQFTScene(qp), qp, SIZE);
|
||||
const bcSvg = renderSVG(generateScene(bp), bp, SIZE);
|
||||
|
||||
const s = v.qScale ?? 1;
|
||||
const qw = SIZE * s, qh = SIZE * s;
|
||||
const qx = (SIZE - qw) / 2 + (v.qDx || 0);
|
||||
const qy = (SIZE - qh) / 2 + (v.qDy || 0);
|
||||
|
||||
const composite =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<rect width="${SIZE}" height="${SIZE}" fill="${v.bg || '#ffffff'}"/>
|
||||
<image x="${qx.toFixed(0)}" y="${qy.toFixed(0)}" width="${qw.toFixed(0)}" height="${qh.toFixed(0)}" href="${dataUri(qftSvg)}" opacity="${v.qOpacity ?? 1}"/>
|
||||
<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${dataUri(bcSvg)}" opacity="${v.bcOpacity ?? 1}" style="mix-blend-mode:${blend}"/>
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, composite);
|
||||
console.log(` ${v.name} (qft=${v.qftSeed}/${qp.archetype} × bc=${v.bcSeed}, ${blend})`);
|
||||
return composite;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 10 variations — each chases one feeling from the roadmap.
|
||||
// ============================================================
|
||||
const VARIATIONS = [
|
||||
// 1 — AWE, plainly stated. The lightbox positive: faint cubic+links
|
||||
// field under a clean mono event. The thesis image.
|
||||
{
|
||||
name: '01_lightbox-positive', blend: 'multiply', bg: '#efe9da',
|
||||
qftSeed: 'VACUUM-5113', bcSeed: 'LAMBDA-2648',
|
||||
qftOver: {
|
||||
substrate: 'cream', cubicN: 1, e8Count: 1, e8OriginRadius: 0.62, linkCount: 6,
|
||||
fields: {
|
||||
cubic: F(0.55, 0.50, 0.30, 0.62, 0.42, 1.1), schlegel: F(0.90, 0.95, 0.22, 0.58, 0.34, 1.0),
|
||||
e8: F(0.10, 0.14, 0.45, 0.60, 0.55, 1.2), ripple: OFF, links: F(0.10, 0.05, 0.80, 0.62, 0.70, 1.6),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono', showHeader: true },
|
||||
},
|
||||
|
||||
// 2 — THE NUMINOUS / deep-sky. Negative void: a luminous field glows
|
||||
// through a photographic NEGATIVE (light tracks on black) via screen.
|
||||
{
|
||||
name: '02_negative-void', blend: 'screen', bg: '#070709',
|
||||
qftSeed: 'GAUGE-2046', bcSeed: 'HYPERON-8444',
|
||||
qftOver: {
|
||||
substrate: 'void', cubicN: 2, e8Count: 2, glow: 0.7,
|
||||
fields: {
|
||||
cubic: F(0.55, 0.62, 0.55, 0.50, 0.55, 1.0), schlegel: F(0.50, 0.58, 0.50, 0.46, 0.50, 1.2),
|
||||
e8: F(0.08, 0.14, 0.65, 0.55, 0.70, 1.3), ripple: OFF, links: F(0.95, 0.88, 0.85, 0.58, 0.85, 1.8),
|
||||
},
|
||||
},
|
||||
bcOver: { invert: false, palette: 'mono', glow: 0.6 },
|
||||
},
|
||||
|
||||
// 3 — DEVOTION as a single chemistry. Cyanotype across BOTH layers; the
|
||||
// seeds share a true name (·2755) — one event, two readings.
|
||||
{
|
||||
name: '03_cyanotype-communion', blend: 'screen', bg: '#0a1b33',
|
||||
qftSeed: 'PROPAGATOR-2755', bcSeed: 'CASCADE-2755',
|
||||
qftOver: {
|
||||
substrate: 'cyanotype', cubicN: 2, e8Count: 3, e8OriginRadius: 0.55, glow: 0.55,
|
||||
fields: {
|
||||
cubic: F(0.58, 0.55, 0.30, 0.78, 0.50, 1.0), schlegel: F(0.58, 0.55, 0.28, 0.72, 0.45, 1.1),
|
||||
e8: F(0.55, 0.52, 0.25, 0.85, 0.55, 1.1), ripple: OFF, links: F(0.55, 0.52, 0.20, 0.92, 0.65, 1.5),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'cyanotype', invert: false },
|
||||
},
|
||||
|
||||
// 4 — VERTIGO of scale, soft. The field swollen to 1.5× and bled off
|
||||
// every edge, faint — atmosphere/fog the event floats in.
|
||||
{
|
||||
name: '04_field-as-atmosphere', blend: 'multiply', bg: '#ece6d6',
|
||||
qftSeed: 'FEYNMAN-7167', bcSeed: 'NUCLEON-2131',
|
||||
qScale: 1.55, qOpacity: 0.62,
|
||||
qftOver: {
|
||||
substrate: 'cream', cubicN: 2, e8Count: 4, e8Style: 'nautilus',
|
||||
nautilusTurns: 2.6, nautilusPerTurn: 14, nautilusGrowth: 0.22, e8OriginRadius: 0.7,
|
||||
fields: {
|
||||
cubic: F(0.55, 0.50, 0.22, 0.66, 0.40, 0.9), schlegel: F(0.90, 0.95, 0.18, 0.60, 0.30, 0.9),
|
||||
e8: F(0.10, 0.14, 0.40, 0.62, 0.50, 1.0), ripple: OFF, links: F(0.10, 0.05, 0.60, 0.64, 0.45, 1.2),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono' },
|
||||
},
|
||||
|
||||
// 5 — UNCANNY RECOGNITION. A single nautilus rosette placed LOW, directly
|
||||
// behind the shock disk: the field's spiral and the chamber's mandala
|
||||
// become the same shape. The rhyme across scales, literalised. ★
|
||||
{
|
||||
name: '05_scale-rhyme-nautilus', blend: 'multiply', bg: '#efe9da',
|
||||
qftSeed: 'LATTICE-1003', bcSeed: 'HYPERON-8444',
|
||||
qftOver: {
|
||||
substrate: 'cream', e8Style: 'nautilus', e8Origins: [{ x: 0.0, y: 0.34 }],
|
||||
e8Scale: 0.5, nautilusTurns: 3.2, nautilusPerTurn: 18, nautilusGrowth: 0.20,
|
||||
cubicN: 1, linkCount: 0,
|
||||
fields: {
|
||||
cubic: F(0.55, 0.50, 0.20, 0.66, 0.26, 0.9), schlegel: OFF,
|
||||
e8: F(0.08, 0.13, 0.55, 0.58, 0.78, 1.5), ripple: OFF, links: OFF,
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono', shockY: 0.62, burst: 0.85 },
|
||||
},
|
||||
|
||||
// 6 — MELANCHOLY / the archive. Sepia throughout; the chamber is the
|
||||
// studied plate — grease-pencil marks, KODAK film edge, header.
|
||||
{
|
||||
name: '06_sepia-archive', blend: 'multiply', bg: '#e7dcc6',
|
||||
qftSeed: 'VACUUM-5113', bcSeed: 'LAMBDA-2648',
|
||||
qftOver: {
|
||||
substrate: 'cream', paperOverride: paper([231, 220, 198]),
|
||||
vignOverride: [120, 100, 70], cubicN: 1, e8Count: 2, linkCount: 5,
|
||||
fields: {
|
||||
cubic: F(0.09, 0.07, 0.35, 0.55, 0.40, 1.0), schlegel: F(0.08, 0.06, 0.30, 0.52, 0.32, 1.0),
|
||||
e8: F(0.09, 0.07, 0.45, 0.50, 0.55, 1.1), ripple: OFF, links: F(0.06, 0.04, 0.55, 0.55, 0.55, 1.4),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono', paperTone: 'sepia', toneStrength: 0.8, showHeader: true, annotate: 0.85, filmEdge: true, reseau: 0.4 },
|
||||
},
|
||||
|
||||
// 7 — TRANSCENDENCE earned. The chamber inked by particle TYPE (a physics
|
||||
// legend); the field's links + rosettes tuned to the same warm/cool
|
||||
// families, so colour means the same thing on both plates.
|
||||
{
|
||||
name: '07_earned-colour-kind', blend: 'multiply', bg: '#ece7d8',
|
||||
qftSeed: 'GAUGE-2046', bcSeed: 'HYPERON-8444',
|
||||
qftOver: {
|
||||
substrate: 'cream', cubicN: 2, e8Count: 3, e8OriginRadius: 0.58,
|
||||
fields: {
|
||||
cubic: F(0.55, 0.60, 0.45, 0.58, 0.45, 1.0), schlegel: F(0.83, 0.88, 0.40, 0.55, 0.40, 1.1),
|
||||
e8: F(0.12, 0.16, 0.55, 0.56, 0.62, 1.2), ripple: OFF, links: F(0.90, 0.82, 0.70, 0.58, 0.70, 1.6),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'kind', saturation: 1.05 },
|
||||
},
|
||||
|
||||
// 8 — CONTEMPLATIVE STILLNESS. A standing-wave (Chladni) field — the
|
||||
// drum membrane of space — under a quiet, sparse event.
|
||||
{
|
||||
name: '08_chladni-quiet', blend: 'multiply', bg: '#eae6da',
|
||||
qftSeed: 'FEYNMAN-7167', bcSeed: 'NUCLEON-2131',
|
||||
qftOver: {
|
||||
substrate: 'cream', cubicN: 2, e8Count: 0, linkCount: 3, linkCurvature: 0.2,
|
||||
standingWaves: [SW(6.0, 1.0, 0.040), SW(1.0, 6.0, 0.040)],
|
||||
fields: {
|
||||
cubic: F(0.52, 0.56, 0.30, 0.60, 0.55, 1.0), schlegel: F(0.55, 0.58, 0.26, 0.56, 0.42, 1.0),
|
||||
e8: OFF, ripple: OFF, links: F(0.10, 0.06, 0.55, 0.60, 0.55, 1.3),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono', primaries: 6, burst: 0.32, cosmics: 2, sweepers: 1, deltaRate: 0.4, vdecay: 1 },
|
||||
},
|
||||
|
||||
// 9 — ORDER vs CHAOS. A dense, ordered lattice swirled by a vortex is the
|
||||
// true subject; a single rare event punctuates it. Cool grey plate.
|
||||
{
|
||||
name: '09_dense-lattice-rare-event', blend: 'multiply', bg: '#dadcdd',
|
||||
qftSeed: 'LATTICE-1003', bcSeed: 'NUCLEON-2131',
|
||||
qftOver: {
|
||||
substrate: 'cream', paperOverride: paper([216, 219, 222]), vignOverride: [70, 75, 85],
|
||||
cubicN: 2, cubicScale: 1.2, e8Count: 1, e8OriginRadius: 0.66, linkCount: 6,
|
||||
vortices: [VX(0, 0, 0.9, 0.28)],
|
||||
fields: {
|
||||
cubic: F(0.56, 0.60, 0.30, 0.50, 0.62, 0.9), schlegel: F(0.58, 0.62, 0.26, 0.48, 0.50, 1.0),
|
||||
e8: F(0.10, 0.14, 0.45, 0.52, 0.55, 1.1), ripple: OFF, links: F(0.55, 0.06, 0.70, 0.55, 0.62, 1.4),
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono', primaries: 5, burst: 0.5, cosmics: 1, sweepers: 1, deltaRate: 0.5, vdecay: 1 },
|
||||
},
|
||||
|
||||
// 10 — VERTIGO, hard. A huge 4D tesseract bleeding past every edge behind
|
||||
// a tight, dense burst: atom and hypercube in one frame.
|
||||
{
|
||||
name: '10_vertigo-tesseract', blend: 'multiply', bg: '#eceadf',
|
||||
qftSeed: 'PROPAGATOR-2755', bcSeed: 'HYPERON-8444',
|
||||
qScale: 1.7, qDx: 60, qDy: -40,
|
||||
qftOver: {
|
||||
substrate: 'cream', cubicN: 1, e8Count: 0, linkCount: 0,
|
||||
schlegelScale: 1.45, schlegelOuterR: 0.9, schlegelInnerR: 0.28, schlegelRot3D: 0.55,
|
||||
fields: {
|
||||
cubic: F(0.55, 0.50, 0.18, 0.66, 0.22, 0.9), schlegel: F(0.90, 0.96, 0.42, 0.54, 0.62, 1.3),
|
||||
e8: OFF, ripple: OFF, links: OFF,
|
||||
},
|
||||
},
|
||||
bcOver: { palette: 'mono', primaries: 22, burst: 0.92, deltaRate: 0.7 },
|
||||
},
|
||||
];
|
||||
|
||||
console.log(`Compositing ${VARIATIONS.length} QFT×BC plates → ${OUT}/`);
|
||||
for (const v of VARIATIONS) composite(v);
|
||||
|
||||
// ---- contact sheets ----
|
||||
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
|
||||
const idx = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>QFT × Bubble Chamber · sketch 01</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,Menlo,monospace;padding:26px;max-width:1900px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#a5d4c9}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:20px;margin-top:18px}
|
||||
figure{margin:0;background:#fff;border:1px solid #262626;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:8px 10px;color:#e8e4d8;background:#111}small{color:#777;display:block}
|
||||
.notes{color:#999;background:#0e1816;padding:14px 18px;border-left:3px solid #a5d4c9;margin:18px 0}</style></head><body>
|
||||
<h1>QFT × Bubble Chamber · sketch 01 — the field that authors the trace, under the trace</h1>
|
||||
<div class="notes">Two independently-seeded plates stacked as one image: a QFT field plate UNDERNEATH a bubble-chamber plate,
|
||||
joined with <code>mix-blend-mode</code> (multiply on light grounds = lightbox; screen on dark = luminous negative).
|
||||
Furniture deduplicated — the bubble chamber is the studied, labelled top plate. Each frame chases one feeling.</div>
|
||||
<div class=grid>
|
||||
${VARIATIONS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)}<small>${v.qftSeed} × ${v.bcSeed} · ${v.blend || 'multiply'}</small></figcaption></figure>`).join('\n')}
|
||||
</div></body></html>`;
|
||||
writeFileSync(`${OUT}/index.html`, idx);
|
||||
|
||||
const m = `<!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">
|
||||
${VARIATIONS.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 sheets -> ${OUT}/index.html , m.html`);
|
||||
49
tools/qft-carpet.mjs
Normal file
49
tools/qft-carpet.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
/* ============================================================
|
||||
qft-carpet.mjs — render VACUUM CARPET studies + a plexi deck.
|
||||
Logic lives in src/qft/carpet.js (shared with tools/layering.mjs).
|
||||
Usage: node tools/qft-carpet.mjs [size]
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1500);
|
||||
const ROOT = 'output/qft/carpet';
|
||||
mkdirSync(`${ROOT}/explore`, { recursive: true });
|
||||
mkdirSync(`${ROOT}/layers`, { recursive: true });
|
||||
|
||||
// EXPLORE — soft sinusoidal/spiralling blips across the chaos range
|
||||
const EXPLORE = [
|
||||
{ name: '01_calm-ridgeline', label: 'calm · low-q swells, few blips', o: { chaos: 0.22, blips: 0.5, rows: 44, overlap: 1.6 } },
|
||||
{ name: '02_vacuum-seethe', label: 'vacuum seethe · many spiralling blips', o: { chaos: 0.8, blips: 1.4, rows: 50, overlap: 1.8, salt: 'seethe' } },
|
||||
{ name: '03_dense-fine', label: 'dense fine weave', o: { chaos: 0.5, blips: 1.0, rows: 70, overlap: 1.5, strokeNear: 1.2, salt: 'fine' } },
|
||||
{ name: '04_sparse-bold', label: 'sparse · bold soft swells', o: { chaos: 0.55, blips: 0.9, rows: 30, overlap: 2.2, strokeNear: 2.2, salt: 'bold' } },
|
||||
{ name: '05_deep-horizon', label: 'deep horizon · more sky above the mound', o: { chaos: 0.5, blips: 0.9, rows: 56, horizon: 0.44, wFar: 0.58, wNear: 0.74, salt: 'deep' } },
|
||||
{ name: '06_verdigris-seethe', label: 'verdigris vacuum · oxidised', o: { chaos: 0.7, blips: 1.2, rows: 48, hue: 0.40, hue2: 0.47, sat: 0.42, salt: 'verd' } },
|
||||
];
|
||||
console.log(`carpet · explore (${EXPLORE.length}) → ${ROOT}/explore/`);
|
||||
for (const v of EXPLORE) {
|
||||
writeFileSync(`${ROOT}/explore/${v.name}.svg`, carpetSVG(SIZE, { ...v.o, mode: 'solid' }));
|
||||
console.log(` ${v.name} — ${v.label}`);
|
||||
}
|
||||
{
|
||||
const cap = (v) => `${v.name.replace(/^\d+_/, '').replace(/-/g, ' ')} · ${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:2000px}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>${cap(v)}</figcaption></figure>`).join('')}</div></body></html>`);
|
||||
}
|
||||
|
||||
// LAYERS — three transparent PLATE sheets for a spaced plexi deck
|
||||
const DECK = [
|
||||
{ name: 'L3_back', salt: 'fieldA', hue: 0.58, hue2: 0.62, chaos: 0.55, blips: 0.8 },
|
||||
{ name: 'L2_mid', salt: 'fieldB', hue: 0.52, hue2: 0.56, chaos: 0.65, blips: 1.0 },
|
||||
{ name: 'L1_front', salt: 'fieldC', hue: 0.47, hue2: 0.50, chaos: 0.75, blips: 1.2 },
|
||||
];
|
||||
const deckBase = { mode: 'plate', rows: 46, horizon: 0.36, wFar: 0.58, wNear: 0.7, overlap: 1.7, mound: 0.4, sat: 0.6, lightNear: 0.33, lightFar: 0.55 };
|
||||
console.log(`carpet · plexi deck (${DECK.length}) → ${ROOT}/layers/`);
|
||||
for (const v of DECK) { writeFileSync(`${ROOT}/layers/${v.name}.svg`, carpetSVG(SIZE, { ...deckBase, ...v })); console.log(` ${v.name}`); }
|
||||
|
||||
writeFileSync(`${ROOT}/stack.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>vacuum carpet · plexi deck</title>
|
||||
<style>html,body{margin:0;background:#0c0c0c}.stage{position:relative;width:${SIZE}px;height:${SIZE}px;margin:30px auto;background:rgb(226,219,199);box-shadow:0 0 120px #000 inset}.stage img{position:absolute;inset:0;width:100%;height:100%}.L3{filter:blur(2.2px);opacity:.5;transform:translateY(-6px) scale(1.01)}.L2{filter:blur(1px);opacity:.72;transform:translateY(-2px)}.L1{opacity:.95}.cap{max-width:${SIZE}px;margin:0 auto;color:#888;font:12px ui-monospace,monospace;padding:0 4px}</style></head><body>
|
||||
<div class="stage"><img class="L3" src="layers/L3_back.svg"><img class="L2" src="layers/L2_mid.svg"><img class="L1" src="layers/L1_front.svg"></div>
|
||||
<p class="cap">deck (back→front): film/diffusion · L3 · L2 · L1 · [bubble chamber]. back sheets blurred+dimmed = air-gap depth of field.</p></body></html>`);
|
||||
console.log(`stack -> ${ROOT}/stack.html ; explore -> ${ROOT}/explore/m.html`);
|
||||
71
tools/qft-infinite-sweep.mjs
Normal file
71
tools/qft-infinite-sweep.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
/* ============================================================
|
||||
qft-infinite-sweep.mjs — workshop the cartesian grid as INFINITE
|
||||
space: wide/deep/shallow slabs + camera pushed into the lattice +
|
||||
exaggerated perspective, so the field bleeds off every edge and
|
||||
rushes to a vanishing point rather than reading as a closed cube.
|
||||
Usage: node tools/qft-infinite-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] || 1200);
|
||||
const OUT = 'output/qft/infinite';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const SEED = 'LATTICE-1003';
|
||||
const D = Math.PI / 180;
|
||||
|
||||
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);
|
||||
|
||||
const BASE = {
|
||||
substrate: 'cream', showHeader: false, glow: 0.16, vign: 0.14,
|
||||
photonCyclesPerUnit: 6, segmentsPerEdge: 10, stroke: 1.0,
|
||||
cubicRot: 0, linkCount: 0, e8Count: 0,
|
||||
fields: { cubic: F(0.52, 0.57, 0.6, 0.38, 1.0, 1.0), schlegel: OFF, e8: OFF, ripple: OFF, links: OFF },
|
||||
};
|
||||
|
||||
// each: nx/ny/nz extents, camera, scale, originY (vertical placement)
|
||||
const V = (name, label, o) => ({ name, label, o });
|
||||
const SWEEP = [
|
||||
V('01_floor-to-horizon', 'flat floor · low pitch · rushes to a horizon',
|
||||
{ nx: 9, ny: 0, nz: 20, yaw: 0, pitch: 13 * D, persp: 1.4, dist: 1.6, scale: 1.0, oy: 0.28 }),
|
||||
V('02_floor-three-quarter', 'floor · 3/4 view · vanishing to the side',
|
||||
{ nx: 11, ny: 0, nz: 18, yaw: -26 * D, pitch: 18 * D, persp: 1.25, dist: 1.9, scale: 0.9, oy: 0.22 }),
|
||||
V('03_tunnel-inside', 'camera INSIDE · walls/floor/ceiling rush inward',
|
||||
{ nx: 4, ny: 4, nz: 18, yaw: 0, pitch: 0, persp: 1.5, dist: 1.5, zShift: 7, scale: 0.62, oy: 0 }),
|
||||
V('04_corridor-3q', 'corridor · slight angle · one-point-ish',
|
||||
{ nx: 5, ny: 3, nz: 18, yaw: -18 * D, pitch: 7 * D, persp: 1.4, dist: 1.7, zShift: 3, scale: 0.72, oy: 0.05 }),
|
||||
V('05_exaggerated-bleed', 'full lattice · strong persp · near corner blows off-page',
|
||||
{ nx: 5, ny: 5, nz: 6, yaw: -40 * D, pitch: 30 * D, persp: 1.7, dist: 1.25, scale: 0.8, oy: 0 }),
|
||||
V('06_diagonal-rush', 'diagonal rush across the frame',
|
||||
{ nx: 9, ny: 2, nz: 18, yaw: 33 * D, pitch: 22 * D, persp: 1.5, dist: 1.55, scale: 0.82, oy: 0.05 }),
|
||||
V('07_vaulted-ceiling', 'looking UP · ceiling lattice vaulting away',
|
||||
{ nx: 9, ny: 0, nz: 18, yaw: 0, pitch: -15 * D, persp: 1.35, dist: 1.7, scale: 1.0, oy: -0.26 }),
|
||||
V('08_deep-floor-strong', 'widest, deepest floor · strongest vanishing',
|
||||
{ nx: 13, ny: 0, nz: 24, yaw: 0, pitch: 10 * D, persp: 1.6, dist: 1.35, scale: 1.0, oy: 0.3 }),
|
||||
];
|
||||
|
||||
console.log(`Infinite-field sweep (${SWEEP.length}) → ${OUT}/ seed=${SEED}`);
|
||||
for (const v of SWEEP) {
|
||||
const o = v.o;
|
||||
const p = {
|
||||
...paramsFromSeed(SEED), ...BASE,
|
||||
cubicNx: o.nx, cubicNy: o.ny, cubicNz: o.nz,
|
||||
cubicYaw: o.yaw ?? 0, cubicPitch: o.pitch ?? 0, cubicRoll: o.roll ?? 0,
|
||||
cubicPersp: o.persp ?? 0, cubicDist: o.dist ?? 3.4, cubicZShift: o.zShift ?? 0,
|
||||
cubicScale: o.scale ?? 1.0, cubicOriginX: o.ox ?? 0, cubicOriginY: o.oy ?? 0,
|
||||
};
|
||||
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(2,1fr);gap:8px;padding:10px;width:2000px}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`);
|
||||
71
tools/qft-perspective-sweep.mjs
Normal file
71
tools/qft-perspective-sweep.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
/* ============================================================
|
||||
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`);
|
||||
Reference in New Issue
Block a user