/* ============================================================ 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 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 = ` `; 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 = `QFT × Bubble Chamber · sketch 01

QFT × Bubble Chamber · sketch 01 — the field that authors the trace, under the trace

Two independently-seeded plates stacked as one image: a QFT field plate UNDERNEATH a bubble-chamber plate, joined with mix-blend-mode (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.
${VARIATIONS.map(v => `
${cap(v)}${v.qftSeed} × ${v.bcSeed} · ${v.blend || 'multiply'}
`).join('\n')}
`; writeFileSync(`${OUT}/index.html`, idx); const m = `
${VARIATIONS.map(v => `
${cap(v)}
`).join('\n')}
`; writeFileSync(`${OUT}/m.html`, m); console.log(`contact sheets -> ${OUT}/index.html , m.html`);