133 lines
5.4 KiB
JavaScript
133 lines
5.4 KiB
JavaScript
|
|
/* ============================================================
|
||
|
|
scene.js — the single source of truth.
|
||
|
|
generateScene(params) returns a pure, renderer-agnostic data
|
||
|
|
model. Every renderer (photographic raster, vector SVG/PDF)
|
||
|
|
consumes exactly this. Deterministic from params.seed.
|
||
|
|
============================================================ */
|
||
|
|
import { makeRng, gauss, chance, pick } from '../rng.js';
|
||
|
|
import { integrateTrack, sampleMomentum, cosmicTrack, sweeperTrack } from './track.js';
|
||
|
|
import { spawnDeltaSpiral } from './delta.js';
|
||
|
|
import { spawnVDecay } from './vdecay.js';
|
||
|
|
import { generateShock } from './shock.js';
|
||
|
|
import { generateArtifacts } from './artifacts.js';
|
||
|
|
import { generateInstrument } from './instrument.js';
|
||
|
|
import { cyrb53 } from '../rng.js';
|
||
|
|
|
||
|
|
const LABS = ['BEBC · CERN', 'GARGAMELLE · CERN', '2m HBC · CERN', '82" HBC · SLAC', 'MIRABELLE · IHEP'];
|
||
|
|
|
||
|
|
/* One event: a vertex with primaries (+ δ-rays, V-decays for bright events). */
|
||
|
|
function generateOneEvent(params, vertex, intensity, salt) {
|
||
|
|
const rng = makeRng(params.seed, 'event:' + salt);
|
||
|
|
const tracks = [];
|
||
|
|
const N = Math.max(2, Math.round(params.primaries * (0.45 + intensity * 0.55)));
|
||
|
|
const burstConc = 0.3 + params.burst * 0.7;
|
||
|
|
const bright = intensity > 0.6;
|
||
|
|
|
||
|
|
for (let i = 0; i < N; i++) {
|
||
|
|
const baseAngle = (i / N) * Math.PI * 2;
|
||
|
|
const angle = baseAngle + gauss(rng) * 0.4 * (1 - burstConc);
|
||
|
|
const p = sampleMomentum(rng, params.pspread);
|
||
|
|
const q = chance(rng, 0.5) ? +1 : -1;
|
||
|
|
const pts = integrateTrack(
|
||
|
|
{ x: vertex.x, y: vertex.y, theta: angle, p, q }, params
|
||
|
|
);
|
||
|
|
tracks.push({ pts, kind: 'primary', weight: intensity });
|
||
|
|
|
||
|
|
// δ-rays along bright primaries — abundant, true spirals
|
||
|
|
if (bright) {
|
||
|
|
const dRng = makeRng(params.seed, salt + ':delta' + i);
|
||
|
|
for (let j = 6; j < pts.length; j += 2) {
|
||
|
|
if (dRng() < params.deltaRate * 0.12) {
|
||
|
|
const dpts = spawnDeltaSpiral(pts[j], params, dRng);
|
||
|
|
if (dpts.length > 6) {
|
||
|
|
tracks.push({ pts: dpts, kind: 'delta', weight: intensity * 0.9 });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// dense interaction "star": stubby tracks bursting from the vertex, with a
|
||
|
|
// few medium prongs reaching further out for a richer burst.
|
||
|
|
if (bright && params.burst > 0.15) {
|
||
|
|
const sRng = makeRng(params.seed, salt + ':star');
|
||
|
|
const nStar = Math.floor(params.burst * 26);
|
||
|
|
for (let i = 0; i < nStar; i++) {
|
||
|
|
const a = sRng() * Math.PI * 2;
|
||
|
|
const medium = sRng() < 0.25;
|
||
|
|
const p = medium ? (0.5 + sRng() * 0.7) : (0.13 + sRng() * 0.4);
|
||
|
|
const q = chance(sRng, 0.5) ? 1 : -1;
|
||
|
|
const pts = integrateTrack(
|
||
|
|
{ x: vertex.x + gauss(sRng) * 0.012, y: vertex.y + gauss(sRng) * 0.012, theta: a, p, q },
|
||
|
|
params, { maxTravel: medium ? 2.6 : 1.1 }
|
||
|
|
);
|
||
|
|
tracks.push({ pts, kind: 'primary', weight: intensity });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// V-decays only for the brightest (foreground) events
|
||
|
|
if (intensity > 0.8) {
|
||
|
|
for (let i = 0; i < params.vdecay; i++) {
|
||
|
|
const daughters = spawnVDecay(vertex, params, makeRng(params.seed, salt + ':vdecay' + i));
|
||
|
|
daughters.forEach(d => {
|
||
|
|
if (d.length > 10) tracks.push({ pts: d, kind: 'vdecay', weight: intensity * 0.95 });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return tracks;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function generateScene(params) {
|
||
|
|
const rng = makeRng(params.seed, 'scene');
|
||
|
|
const tracks = [];
|
||
|
|
|
||
|
|
// Foreground event, slightly off-centre
|
||
|
|
const fgVertex = { x: (rng() - 0.5) * 0.3, y: (rng() - 0.5) * 0.3 };
|
||
|
|
tracks.push(...generateOneEvent(params, fgVertex, 1.0, 'fg'));
|
||
|
|
|
||
|
|
// Background "history" events
|
||
|
|
const nBg = params.bgEvents || 0;
|
||
|
|
for (let i = 0; i < nBg; i++) {
|
||
|
|
const bgRng = makeRng(params.seed, 'bg' + i);
|
||
|
|
const vx = (bgRng() - 0.5) * 1.7, vy = (bgRng() - 0.5) * 1.7;
|
||
|
|
const intensity = params.bgIntensity * (0.5 + bgRng() * 0.5);
|
||
|
|
tracks.push(...generateOneEvent(
|
||
|
|
{ ...params, primaries: Math.round(params.primaries * 0.6), vdecay: 0 },
|
||
|
|
{ x: vx, y: vy }, intensity, 'bg' + i
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cosmic/transient straight tracks crossing the frame
|
||
|
|
const nCosmic = Math.round(params.cosmics || 0);
|
||
|
|
for (let i = 0; i < nCosmic; i++) {
|
||
|
|
const cRng = makeRng(params.seed, 'cosmic' + i);
|
||
|
|
const pts = cosmicTrack(params, cRng);
|
||
|
|
if (pts.length > 8) tracks.push({ pts, kind: 'cosmic', weight: 0.7 + cRng() * 0.3 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sweepers — big gentle arcs across the frame
|
||
|
|
const nSweep = Math.round(params.sweepers || 0);
|
||
|
|
for (let i = 0; i < nSweep; i++) {
|
||
|
|
const sRng = makeRng(params.seed, 'sweep' + i);
|
||
|
|
const pts = sweeperTrack(params, sRng);
|
||
|
|
if (pts.length > 8) tracks.push({ pts, kind: 'sweep', weight: 0.75 + sRng() * 0.25 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const shock = generateShock(params, makeRng(params.seed, 'shock'));
|
||
|
|
const artifacts = generateArtifacts(params, makeRng(params.seed, 'artifacts'));
|
||
|
|
const instrument = generateInstrument(params, makeRng(params.seed, 'instrument'));
|
||
|
|
|
||
|
|
// deterministic archival metadata (so exports are reproducible from the seed)
|
||
|
|
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 = 1971 + (ds % 11);
|
||
|
|
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, 'lab'), LABS);
|
||
|
|
|
||
|
|
return { tracks, vertex: fgVertex, shock, artifacts, instrument, hash, plate, exposure, lab };
|
||
|
|
}
|