ENGINEERING LOGBOOK · BOOK 14 · PAGES 88–93

How the collision machine was built.

This is the shift log for the COLLIDER site — every trick that makes the hero simulation feel like physics, written down the way we'd write down a magnet quench: what happened, why, and the exact incantation used.

SITE: MERIDIAN COLLIDER LABORATORY (FICTIONAL)
STACK: ONE HTML FILE · VANILLA JS · CANVAS 2D · ZERO DEPENDENCIES
AESTHETIC SCHOOL: SCIENTIFIC SUBLIME — CONTROL-ROOM DARKNESS, BEAMLINE LABELS, ENGINEERING-DIAGRAM SVG

LOG 001

The concept

SHIFT A · 06:00

A public-facing website for a particle physics lab, designed like the machine itself: near-black (#07080c), hairline rules, mono-font labels on everything — as if the page were a beamline schematic that happened to have prose on it. Three colors carry all the meaning: cyan for beam A, magenta for beam B, gold for the thing we're hunting.

The draw comes from control-room screens and accelerator documentation: CERN's event displays, mid-century instrument panels, the quiet authority of a system that labels its own parts. Wonder plus rigor. The copy is written like a good science museum — no jargon soup, real numbers, one metaphor per fact.

LOG 002

Curved tracks — the burst math

SHIFT A · 09:40

The signature moment: two counter-rotating particle streams orbit a ring, and every few seconds (or whenever you click) they collide at the interaction point. A flash, then 40–80 decay products spray outward with curved tracks — because in a real detector, a magnetic field bends every charged particle, and positive and negative charges bend opposite ways.

The whole physics engine is four lines. A magnetic field accelerates a charge perpendicular to its velocity — it never speeds anything up, it only turns it. Perpendicular of (vx, vy) is (-vy, vx), and the sign of the charge flips the turn direction:

// a = q · B × v  →  curl the velocity, don't push it
if (p.q !== 0) {
  var ax = -p.vy * p.q * p.k;   // perp(v), scaled by charge × field
  var ay =  p.vx * p.q * p.k;
  p.vx += ax * dt;
  p.vy += ay * dt;
}
p.x += p.vx * dt;
p.y += p.vy * dt;

Each product rolls its charge at spawn: 44% positive (magenta, curls one way), 44% negative (cyan, curls the other), 12% neutral (faint white, flies straight — like real photons and neutrons). Randomizing k per particle varies the bend radius, so the burst looks like a bubble-chamber photograph instead of a firework.

The rare event: one collision in seven is an axion-9 candidate. It gets one extra track — gold, nearly straight (tiny q), triple lifetime, with a shadowBlur glow only that track pays for. The readout celebrates quietly. Rarity is what makes people keep clicking.

LOG 003

The trail trick — dim, don't clear

SHIFT B · 14:15

The fading trails cost nothing. Instead of clearing the canvas each frame, we paint a translucent rectangle of the background color over everything. Old light fades a little every frame; anything drawn recently persists as a ghost. Motion blur, particle trails, and afterglow, all from one fillRect:

// each frame: veil the past, then add light
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(7, 8, 12, 0.22)';   // the veil — lower = longer trails
ctx.fillRect(0, 0, W, H);

ctx.globalCompositeOperation = 'lighter'; // additive: overlaps bloom white-hot

The switch to 'lighter' is what makes it read as light rather than paint — where many tracks cross near the collision point, their colors sum toward white, exactly like overexposed film. Each particle is drawn as a short line segment from last position to current position; the veil turns that segment-per-frame into a continuous fading streak.

LOG 004

Event scheduling, the readout, and going dark

SHIFT B · 19:02

Collisions self-schedule with a jittered timer checked inside the animation loop — never setInterval, which drifts and fires even when frames are throttled:

if (t >= nextAt) {                        // t = rAF timestamp
  collide();
  nextAt = t + 4500 + Math.random() * 3000;
}
// a click collides instantly and pushes nextAt back,
// so manual and automatic events never stack up

Every event writes a line to a DOM readout — EVT 0417 · E=13.2 TeV · 62 tracks · axion-9 candidate: no — prepended, capped at six lines, in tabular-numbered mono so columns never wiggle. The log is the sim's narrator: without it the burst is pretty; with it the burst is data.

The loop also knows when to shut up. An IntersectionObserver on the hero and a visibilitychange listener both funnel into one sync() that starts or cancels the rAF loop, so a background tab or a scrolled-past hero costs zero CPU:

new IntersectionObserver(function(es){
  heroVisible = es[0].isIntersecting; sync();
}, { threshold: 0.05 }).observe(hero);
document.addEventListener('visibilitychange', sync);
LOG 005

Sharpness, motion safety, and the SVG detectors

SHIFT C · 23:47

DPR-aware canvas

The canvas backing store is sized in device pixels, then the context is transformed so all drawing math stays in CSS pixels. Capped at 2× — beyond that you're burning battery for pixels nobody can see:

dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width  = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);  // draw in CSS px forever after

Reduced motion is a mode, not an off switch

With prefers-reduced-motion: reduce, the loop never starts. Instead the canvas gets a static engineering diagram — ring, bunch markers, direction arrows, gold crosshair at the interaction point. A click still fires one collision, but rendered as a single static frame: each track is integrated to completion and stroked as a fading curve, like a photograph of an event rather than a video. Three seconds later, back to the diagram.

Detector cross-sections

The four detectors are hand-drawn inline SVGs — concentric circles for the barrel detectors, beamline elevations for the rest — where every layer carries a data-layer attribute. The legend buttons carry the same attribute; one delegated handler lights up whichever pair you hover or focus, and dims the siblings via .focused [data-layer]:not(.hot). Keyboard users get the identical experience because the legend items are real <button>s with focus/blur wired to the same handler.

LOG 006

How it was made

SHIFT C · 03:30

Built by Claude (Fable 5) writing vanilla HTML, CSS, and JavaScript by hand — no frameworks, no build step, no canvas library, one file per page. The only external resources are two IBM Plex families from the Google Fonts API.

It's one of twenty-five wildly different sites in the Fable Showcase, a demonstration of range in web design and interaction engineering. The whole fleet lives at fable-25-dhb.pages.dev.

LOG 007

Steal this

HANDOVER NOTES

END OF LOG · SIGNED: FABLE 5 · COUNTERSIGNED: THE NIGHT SHIFT
NEXT ENTRY WHEN AXION-9 SHOWS UP. KEEP THE BEAM STABLE.