web interface

This commit is contained in:
2026-06-02 19:17:19 -04:00
parent 52453fba67
commit 219eb6632c
140 changed files with 2793 additions and 40 deletions

View File

@@ -557,8 +557,11 @@ function generateScene(params) {
const rng = makeRng(params.seed, 'scene');
const tracks = [];
// Foreground event, slightly off-centre — near the focal plane, current trigger
const fgVertex = { x: (rng() - 0.5) * 0.3, y: (rng() - 0.5) * 0.3 };
// Foreground event, slightly off-centre — near the focal plane, current trigger.
// eventX/eventY (in [-1,1] canvas coords) override the seed placement for
// deliberate composition; rng() is still consumed so the rest stays deterministic.
const rvx = (rng() - 0.5) * 0.3, rvy = (rng() - 0.5) * 0.3;
const fgVertex = { x: params.eventX ?? rvx, y: params.eventY ?? rvy };
const fgZ = gauss(rng) * 0.12;
tracks.push(...generateOneEvent(params, fgVertex, 1.0, 'fg', fgZ, 0));
@@ -1199,6 +1202,7 @@ function generateMedia(params, scene, rng) {
/* ---- grease-pencil (chinagraph) hand marks ---- */
if ((params.annotate ?? 0) > 0) {
const full = params.annotate >= 0.66; // restrained vs studied
const aw = params.annoWidth ?? 1; // line-weight multiplier (thinner = <1)
const v = scene.vertex || { x: 0, y: 0 };
// ring a δ-ray curl if there is one, else the vertex region
const deltas = scene.tracks.filter(t => t.kind === 'delta' && t.pts.length > 6);
@@ -1207,19 +1211,22 @@ function generateMedia(params, scene, rng) {
const d = pick(rng, deltas), p = d.pts[Math.floor(d.pts.length * 0.4)];
cx = p.x; cy = p.y; cr = 0.05 + rng() * 0.035;
} else { cx = v.x + gauss(rng) * 0.05; cy = v.y + gauss(rng) * 0.05; cr = 0.11 + rng() * 0.05; }
out.grease.push({ kind: 'ring', pts: wobblyRing(cx, cy, cr, rng), width: 2.4 });
if (params.annoRing !== false) out.grease.push({ kind: 'ring', pts: wobblyRing(cx, cy, cr, rng), width: 2.4 * aw });
// arrow from open space toward the ring
const side = cx <= 0 ? 1 : -1;
const ax = cx + side * (0.34 + rng() * 0.12), ay = cy - (0.30 + rng() * 0.12);
const tipx = cx - side * cr * 1.25, tipy = cy - cr * 1.15;
const ang = Math.atan2(tipy - ay, tipx - ax);
out.grease.push({ kind: 'arrow', shaft: wobblyLine(ax, ay, tipx, tipy, rng), tip: { x: tipx, y: tipy, ang }, width: 2.0 });
// arrow from open space toward the ring/find
if (params.annoArrow !== false) {
const side = cx <= 0 ? 1 : -1;
const ax = cx + side * (0.34 + rng() * 0.12), ay = cy - (0.30 + rng() * 0.12);
const tipx = cx - side * cr * 1.25, tipy = cy - cr * 1.15;
const ang = Math.atan2(tipy - ay, tipx - ax);
out.grease.push({ kind: 'arrow', shaft: wobblyLine(ax, ay, tipx, tipy, rng), tip: { x: tipx, y: tipy, ang }, width: 2.0 * aw });
}
// event number by the ring
out.grease.push({ kind: 'text', x: cx + cr * 1.05, y: cy - cr * 1.05, text: `${scene.plate}`, size: 0.045, rot: -0.05 + gauss(rng) * 0.03 });
// event number by the ring/find
if (params.annoNum !== false)
out.grease.push({ kind: 'text', x: cx + cr * 1.05, y: cy - cr * 1.05, text: params.annoLabel ?? `${scene.plate}`, size: 0.045, rot: -0.05 + gauss(rng) * 0.03 });
if (full) {
if (full && params.annoExtras !== false) {
// angle arc + reading at the vertex
const a0 = rng() * TWO_PI, a1 = a0 + (0.5 + rng() * 0.6);
out.grease.push({ kind: 'arc', x: v.x, y: v.y, r: 0.12 + rng() * 0.05, a0, a1, width: 1.6 });
@@ -2383,6 +2390,9 @@ function renderSVG(scene, params, sizePx = 4800) {
const paperRGB = pal.paper(); // {flat,glowIn,glowOut} rgb arrays
const paperC = { flat: rgbHex(paperRGB.flat), glowIn: rgbHex(paperRGB.glowIn), glowOut: rgbHex(paperRGB.glowOut) };
const ink = rgbHex(pal.feature()); // non-particle marks (optics, disk, damage, header)
// diskPressure: a darker, denser core (compressed gas = higher pressure)
const press = Math.max(0, Math.min(1, params.diskPressure ?? 0));
const coreInk = rgbHex(mix(pal.feature(), [0, 0, 0], press * 0.7));
const featKey = rgbKey(pal.feature());
const colorMap = new Map([[featKey, pal.feature()]]); // distinct bubble colours → gradients
@@ -2391,7 +2401,9 @@ function renderSVG(scene, params, sizePx = 4800) {
content ? `<g id="${id}" inkscape:groupmode="layer" inkscape:label="${attr(label)}" style="display:inline"${extra ? ' ' + extra : ''}>\n${content}\n</g>\n` : '';
/* ---------- Background ---------- */
const bg = `<rect width="${w}" height="${h}" fill="${paperC.flat}"/>\n<rect width="${w}" height="${h}" fill="url(#paper)"/>`;
// transparentPaper: omit paper + vignette so the event can float as an object
// over another layer (e.g. the QFT carpet) without a paper rectangle.
const bg = params.transparentPaper ? '' : `<rect width="${w}" height="${h}" fill="${paperC.flat}"/>\n<rect width="${w}" height="${h}" fill="url(#paper)"/>`;
/* ---------- Chamber optics ---------- */
let optics = '';
@@ -2545,7 +2557,7 @@ function renderSVG(scene, params, sizePx = 4800) {
}
/* ---------- Vignette ---------- */
const vign = params.vign > 0 ? `<rect width="${w}" height="${h}" fill="url(#vign)"/>` : '';
const vign = (params.vign > 0 && !params.transparentPaper) ? `<rect width="${w}" height="${h}" fill="url(#vign)"/>` : '';
/* ---------- Header ---------- */
let header = '';
@@ -2597,7 +2609,8 @@ function renderSVG(scene, params, sizePx = 4800) {
media += `</g>`;
}
if (M.grease && M.grease.length) {
const ch = inv ? '#9c1e1e' : '#f0e296';
// annotateInk overrides the chinagraph colour (e.g. white pencil)
const ch = params.annotateInk || (inv ? '#9c1e1e' : '#f0e296');
let g = `<g stroke="${ch}" fill="${ch}" stroke-linecap="round" stroke-linejoin="round">`;
for (const gm of M.grease) {
if (gm.kind === 'ring' || gm.kind === 'arrow') {
@@ -2627,21 +2640,25 @@ function renderSVG(scene, params, sizePx = 4800) {
let s = `<?xml version="1.0" encoding="UTF-8"?>\n`;
s += `<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n`;
s += `<metadata>Bubble Chamber · seed=${params.seed} · hash=${cyrb53(params.seed)} · palette=${params.palette || 'mono'}</metadata>\n`;
s += defs({ paperC, ink, baseVign: pal.vign(), params, u, colorMap });
s += layer('background', 'Background', bg);
s += layer('optics', 'Chamber optics', optics);
s += layer('shock', 'Shock disk', shock, params.diskSoften > 0 ? 'filter="url(#soften)"' : '');
s += trackLayers;
s += layer('damage', 'Plate damage', damage);
s += layer('fiducials', 'Fiducials', fids);
s += layer('vignette', 'Vignette', vign);
s += layer('header', 'Archival header', header);
s += layer('media', 'Media & hand', media);
s += defs({ paperC, ink, coreInk, press, baseVign: pal.vign(), params, u, colorMap });
// emit: optional array of layer-GROUPS to include, so the plate can be rendered
// as decoupled sub-layers — 'background' | 'disk' | 'bubble' | 'fiduciaries'.
const emit = params.emit;
const keep = (gp) => !emit || emit.includes(gp);
if (keep('background')) s += layer('background', 'Background', bg);
if (keep('bubble')) s += layer('optics', 'Chamber optics', optics);
if (keep('disk')) s += layer('shock', 'Shock disk', shock, params.diskSoften > 0 ? 'filter="url(#soften)"' : '');
if (keep('bubble')) s += trackLayers;
if (keep('bubble')) s += layer('damage', 'Plate damage', damage);
if (keep('fiduciaries')) s += layer('fiducials', 'Fiducials', fids);
if (keep('background')) s += layer('vignette', 'Vignette', vign);
if (keep('fiduciaries')) s += layer('header', 'Archival header', header);
if (keep('fiduciaries')) s += layer('media', 'Media & hand', media);
s += `</svg>\n`;
return s;
}
function defs({ paperC, ink, baseVign, params, u, colorMap }) {
function defs({ paperC, ink, coreInk, press = 0, baseVign, params, u, colorMap }) {
const soften = params.diskSoften > 0
? `<filter id="soften" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="${(params.diskSoften * u).toFixed(2)}"/></filter>`
: '';
@@ -2663,8 +2680,9 @@ function defs({ paperC, ink, baseVign, params, u, colorMap }) {
</radialGradient>
${bubGrads}
<radialGradient id="shockcore" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="${ink}" stop-opacity="0.5"/>
<stop offset="60%" stop-color="${ink}" stop-opacity="0.28"/>
<stop offset="0%" stop-color="${coreInk || ink}" stop-opacity="${(0.5 + press * 0.45).toFixed(2)}"/>
<stop offset="28%" stop-color="${coreInk || ink}" stop-opacity="${(0.32 + press * 0.32).toFixed(2)}"/>
<stop offset="62%" stop-color="${ink}" stop-opacity="0.28"/>
<stop offset="100%" stop-color="${ink}" stop-opacity="0"/>
</radialGradient>
<radialGradient id="shockstain" cx="50%" cy="50%" r="50%">