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

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