Files
bubblechambersimart/tools/qft-bc-variations.mjs

287 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
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`);