Added Ridgeline Plots and layers output

This commit is contained in:
2026-05-29 17:17:06 -04:00
parent 56c59a1f9c
commit e38f11f71a
79 changed files with 356982 additions and 21 deletions

40
tools/archive-wall.html Normal file
View 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
View 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`);

View 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
View 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
View 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
View 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`);

View 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`);

View 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`);