Files
bubblechambersimart/src/qft/scene.js
2026-05-29 15:40:42 -04:00

191 lines
7.9 KiB
JavaScript
Raw 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/scene.js — assemble a QFT plate from params.
Per-field origins (each lattice movable), multi-E8 clusters
scattered around the canvas, and explicit cross-field LINKS
(Feynman propagator symbols connecting a vertex in one field
to a vertex in another — the "interactions" between fields).
Returns:
{ grids: [...], links: [...], hash, plate, exposure, lab }
Each grid: { id, propagator, vertices:[{x,y}], edges:[{a,b}] }
Each link: { a:{x,y}, b:{x,y}, propagator: 'photon'|'scalar'|'gluon' }
============================================================ */
import { makeRng, cyrb53, pick } from '../rng.js';
import { buildCubic, buildSchlegel, buildE8, buildNautilus, buildRipples } from './topology.js';
import { distortVertices, applyWavepackets, applyVortices, applyStandingWaves } from './distortion.js';
const LABS = [
'LATTICE QFT · LANL',
'BNL · ALGEBRAIC GEOMETRY GROUP',
'IHES · BURES-SUR-YVETTE',
'PERIMETER INSTITUTE · WATERLOO',
'INST. THEORETICAL PHYSICS · COPENHAGEN',
];
function rotateScale(verts, angle, scale) {
const c = Math.cos(angle), s = Math.sin(angle);
for (const v of verts) {
const x = v.x * c - v.y * s;
const y = v.x * s + v.y * c;
v.x = x * scale; v.y = y * scale;
}
}
function translateVerts(verts, dx, dy) {
for (const v of verts) { v.x += dx; v.y += dy; }
}
export function generateQFTScene(params) {
// ---- cubic ---- (cubicN controls density: 1=27v, 2=125v, 3=343v)
const cubic = buildCubic(Math.max(1, params.cubicN | 0));
rotateScale(cubic.vertices, params.cubicRot, params.cubicScale);
translateVerts(cubic.vertices, params.cubicOriginX, params.cubicOriginY);
cubic.id = 'cubic'; cubic.propagator = 'photon';
// ---- schlegel ---- (outer/inner radius and 3D rotation now parameterised)
const schl = buildSchlegel(
params.schlegelOuterR ?? 0.78,
params.schlegelInnerR ?? 0.32,
params.schlegelRot3D ?? 0.42,
);
rotateScale(schl.vertices, params.schlegelRot, params.schlegelScale);
translateVerts(schl.vertices, params.schlegelOriginX, params.schlegelOriginY);
schl.id = 'schlegel'; schl.propagator = 'scalar';
// ---- E8 clusters ----
// If params.e8Origins (array of {x,y}) is provided, place clusters exactly
// at those positions (count comes from the array length). Otherwise auto-
// distribute on a ring of radius e8OriginRadius.
const e8Rng = makeRng(params.seed, 'qft-e8');
const e8Instances = [];
let e8Origins;
if (Array.isArray(params.e8Origins) && params.e8Origins.length) {
e8Origins = params.e8Origins;
} else {
const N = Math.max(1, params.e8Count | 0);
e8Origins = [];
const angleJitter = 0.35;
for (let i = 0; i < N; i++) {
const baseAng = (i / N) * Math.PI * 2;
const ang = baseAng + (e8Rng() - 0.5) * angleJitter;
const radius = N === 1 ? 0 : params.e8OriginRadius;
e8Origins.push({ x: Math.cos(ang) * radius, y: Math.sin(ang) * radius });
}
}
for (let i = 0; i < e8Origins.length; i++) {
const o = e8Origins[i];
// per-cluster scale override: e8Origins[i] may include {x, y, scale}
const clusterScale = (typeof o.scale === 'number') ? o.scale : params.e8Scale;
// pick rosette geometry: open NAUTILUS spiral (default for chaotic-growth feel)
// or closed concentric RINGS (the older E8-Coxeter-style mandala).
const style = params.e8Style ?? 'nautilus';
const e = style === 'rings'
? buildE8(4, 18)
: buildNautilus(params.nautilusTurns ?? 2.6,
params.nautilusPerTurn ?? 14,
params.nautilusGrowth ?? 0.21);
rotateScale(e.vertices, params.e8Rot + (e8Rng() - 0.5) * 0.6, clusterScale);
translateVerts(e.vertices, o.x, o.y);
e.id = 'e8';
e.instance = i;
e.propagator = 'gluon';
e8Instances.push(e);
}
// ---- RIPPLES — concentric wavefronts from source points ----
// params.ripples: [{ x, y, count?, r0?, dR?, propagator? }]
const rippleInstances = [];
if (Array.isArray(params.ripples)) {
for (let i = 0; i < params.ripples.length; i++) {
const rs = params.ripples[i];
const r = buildRipples(
{ x: rs.x, y: rs.y },
rs.count ?? 6,
rs.r0 ?? 0.06,
rs.dR ?? 0.09,
rs.segments ?? 40,
);
r.id = 'ripple';
r.instance = i;
r.propagator = rs.propagator ?? 'photon';
rippleInstances.push(r);
}
}
const allFields = [cubic, schl, ...e8Instances, ...rippleInstances];
// ---- FIELD PERTURBATIONS — applied to cubic + schlegel after scale/rotate ----
// wavepackets: Gaussian bulge/dimple (Schrödinger particle as field feature)
if (Array.isArray(params.wavepackets) && params.wavepackets.length) {
applyWavepackets(cubic.vertices, params.wavepackets);
applyWavepackets(schl.vertices, params.wavepackets);
}
// vortices: swirling Gaussian rotation (angular momentum / spin)
if (Array.isArray(params.vortices) && params.vortices.length) {
applyVortices(cubic.vertices, params.vortices);
applyVortices(schl.vertices, params.vortices);
}
// standing waves: global plane-wave sinusoidal modulation (Chladni / nodal lines)
if (Array.isArray(params.standingWaves) && params.standingWaves.length) {
applyStandingWaves(cubic.vertices, params.standingWaves);
applyStandingWaves(schl.vertices, params.standingWaves);
}
// distortion (per-field salt so neighbouring lattices warp differently)
for (const g of allFields) {
const salt = 'dist:' + g.id + (g.instance != null ? ':' + g.instance : '');
distortVertices(g.vertices, params.distRCalm, params.distRMax, params.distStrength,
params.seed, salt);
}
// composition offset — translate the whole scene on the paper
if (params.compositionOffsetX || params.compositionOffsetY) {
for (const g of allFields) translateVerts(g.vertices, params.compositionOffsetX, params.compositionOffsetY);
}
// ---- cross-field links ----
const linkRng = makeRng(params.seed, 'qft-links');
const propChoices = ['photon', 'scalar', 'gluon'];
const linkCount = Math.max(0, params.linkCount | 0);
const links = [];
// pair fields by their INDEX in allFields so multi-E8 clusters can interact
// with cubic/schlegel and with each other
for (let i = 0; i < linkCount && allFields.length >= 2; i++) {
let f1Idx, f2Idx, attempts = 0;
do {
f1Idx = Math.floor(linkRng() * allFields.length);
f2Idx = Math.floor(linkRng() * allFields.length);
attempts++;
} while ((f1Idx === f2Idx || allFields[f1Idx].id === allFields[f2Idx].id) && attempts < 20);
const f1 = allFields[f1Idx], f2 = allFields[f2Idx];
const v1 = f1.vertices[Math.floor(linkRng() * f1.vertices.length)];
const v2 = f2.vertices[Math.floor(linkRng() * f2.vertices.length)];
const prop = params.linkPropagator === 'mixed'
? propChoices[Math.floor(linkRng() * propChoices.length)]
: (params.linkPropagator || 'photon');
// per-link curvature: random sign × random magnitude up to params.linkCurvature.
// a fraction of links stay straight (curvature=0) so the result is mixed-tangle.
let curvature = 0;
const lc = params.linkCurvature ?? 0;
if (lc > 1e-3) {
// 30% straight, 70% curved; signed
if (linkRng() > 0.30) {
const sign = linkRng() < 0.5 ? -1 : 1;
curvature = sign * lc * (0.45 + linkRng() * 0.55);
}
}
links.push({ a: { x: v1.x, y: v1.y }, b: { x: v2.x, y: v2.y }, propagator: prop, curvature });
}
// archival metadata
const hash = cyrb53(params.seed);
const ds = parseInt(hash.slice(0, 8), 16);
const plate = (parseInt(hash.slice(-3), 16) % 999).toString().padStart(3, '0');
const year = 1985 + (ds % 30);
const month = 1 + ((ds >> 4) % 12);
const day = 1 + ((ds >> 8) % 28);
const exposure = `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`;
const lab = pick(makeRng(params.seed, 'qft-lab'), LABS);
return { grids: allFields, links, hash, plate, exposure, lab };
}