191 lines
7.9 KiB
JavaScript
191 lines
7.9 KiB
JavaScript
|
|
/* ============================================================
|
|||
|
|
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 };
|
|||
|
|
}
|