# Bubble Chamber — Design Notes A parametric generator for artificial bubble-chamber plates, aimed at large-format B&W print. Deterministic from a seed string; tweakable in the browser; exportable as photographic raster (full analog texture) or clean vector (SVG/PDF) for print. The north star is a worn CERN-era plate: a photographic *positive* (dark ink on milky paper), busy with multi-scale tracks, dozens of tight δ-ray spirals, a dramatic pressure/shock disk, and the analog remnants of a real plate — film grain, uneven development, scratches, dust, water rings. ## Core architecture — one scene, many renderers ``` seed ──► generateScene(params) ──┬─► renderCanvasPhoto() photographic raster (preview + hi-res PNG) (pure data, no DOM) ├─► renderSVG() clean vector for print └─► buildPDF() vector PDF (24" page) ``` `generateScene()` returns a **renderer-agnostic data model** — tracks (as polylines of `{x,y,beta,theta}`), the shock disk, plate artifacts, plus derived metadata (hash, plate number). Every renderer consumes exactly this. This is the seam to preserve: physics/geometry never touches a drawing API. Bubble *positions* are sampled by the shared `sampleBubbles()` using a salted RNG (`makeRng(seed,'bubbles')`), so the raster and vector outputs place bubbles identically. Only the *look* differs between renderers. Coordinates are a logical `[-1,1]` square; renderers map to whatever pixel size they need. Effect radii in the raster renderer scale with output size, so the live preview (1000px) and the print render (5400px+) look the same. ## Files ``` src/ rng.js cyrb53 hash, mulberry32 PRNG, salted streams, distributions scene/ scene.js generateScene() — assembles events, cosmics, shock, artifacts track.js trajectory integrator (B-field + energy loss), momentum sampling, cosmics delta.js δ-ray log-spirals (the curly bits) vdecay.js neutral-decay "V" daughters shock.js pressure/shock disk model instrument.js chamber optics: structural fan lines, chords, wall arcs artifacts.js plate damage: scratches, hairs, dust, rings, fingerprints params.js derive a full tasteful parameter set from a seed (archetypes) bubbles.js polyline → bubbles (shared by all renderers) render/ canvasPhoto.js photographic multi-pass raster compositor svgVector.js clean vector renderer pdf.js minimal vector PDF writer noise.js value-noise / fbm for grain + tonal mottle ui/controls.js declarative control + preset config (single source) main.js panel build, state, render loop, exports style.css index.html dev entry (ES modules; needs a static server) build.mjs zero-dep bundler → single-file bubble_chamber.html (file://-openable) tools/ preview.html headless render harness (artwork only, params via URL query) shoot.sh build+screenshot helper for the visual compare loop render-svg.mjs CLI SVG render (--seed mode or legacy preset; shares scene module) render-pdf.mjs CLI PDF render (print-ready CMYK; --seed mode) inspire.sh randomize seeds → seed-named thumbnails + HTML contact sheet render.sh one seed → print masters (SVG + PDF + hi-res PNG) gallery.sh render the curated preset set ``` ## Seed-driven workflow (large format) `paramsFromSeed(seed)` makes the seed the complete fingerprint of a plate, so a thumbnail and its print master are identical. Browse `tools/inspire.sh` output, then `tools/render.sh ` for masters. The full large-format strategy (vector geometry + raster texture layers, resolution math, assembly) is in **PRINT.md**. Headless PNG ceiling ≈ 14000 px; SVG/PDF are resolution-independent. ## Physics & geometry decisions - **Trajectory**: charged particle in a uniform out-of-page B field curves with radius `r = p/(qB)`. Energy loss `dp/ds ∝ 1/β²` (simplified Bethe-Bloch) shrinks `p`, tightening the radius toward end-of-range — the inward spiral. The integrator *shortens its step in tight curvature* (`MAX_DTHETA`) so terminal spirals stay smooth instead of going polygonal. - **β as the master variable**: tracked along every polyline; drives both bubble density (`∝1/β²`) and bubble radius (fatter as β falls). Get the β statistics right and the image reads as authentic regardless of rendering tricks. - **Momentum** is log-normal (heavy-tailed): most tracks moderate, a real tail of very stiff (long, near-straight) and very soft (tight spirals). - **δ-rays** are modelled directly as **logarithmic spirals** (radius decays exponentially with wind angle) rather than integrated down to sub-percent radii. This converges cleanly to a point and reliably reads as the textbook curl. β falls toward the centre so the curl gets denser and fatter inward. - **Cosmics/transients**: very stiff tracks entering from the frame edge, crossing as near-straight diagonals (the long lines in real plates that ignore the vertex). - **Shock disk**: not physics — a chamber/window pressure artifact. Modelled as a dark annulus body with a bright textured centre, a hard rim, and fine striations that burst *outward* from a ring. The strongest compositional anchor. ## The analog (raster) layer `renderCanvasPhoto` is a multi-pass compositor: 1. paper fill + radial gas-glow gradient 2. low-frequency tonal mottle (fbm noise, multiply) — uneven development/illumination 3. ink layer (offscreen): faint continuity under-stroke per track + soft blooming bubbles stamped from a cached radial-gradient sprite (overlaps merge into lines) 4. halation/bloom: a blurred copy of the ink composited back for soft spread 5. sharp ink composited onto paper (`multiply` for the positive look) 6. plate damage (dark via multiply; some scratches *lift* via screen) 7. fiducials + chamber boundary 8. vignette 9. structured film grain (supra-pixel clumps, overlay blend) Grain/mottle/bloom are intentionally **raster-only**. The vector renderer is the clean graphic version (soft bubbles via a shared radial-gradient fill, no grain). ## Reproducibility Everything is seeded. New stochastic subsystems must follow the salt pattern: `makeRng(seed, 'subsystem')`. That's what lets a parameter tweak change one thing without reshuffling the rest of the plate. ## Build / run - **Tweak interactively**: `node build.mjs` then open `bubble_chamber.html` directly (no server needed — it's a single self-contained file). Or for live module editing, serve the folder (`python3 -m http.server`) and open `index.html`. - **Visual compare loop**: `tools/shoot.sh /tmp/out.png "preset=BEBC%20Archival&size_px=1200"` renders the artwork headlessly via Chrome. Any param can be set in the query. - **Offline SVG / PDF**: `node tools/render-svg.mjs "BEBC Archival" SEED out.svg` and `node tools/render-pdf.mjs "BEBC Archival" SEED out.pdf`. > Tip for iteration: drop the target reference image into `reference/` so it can > be opened side-by-side with renders when tuning toward it. ## Print pipeline (done) - **PDF**: DeviceCMYK (warm cream + rich black), per-element alpha via ExtGState (`/ca`,`/CA`), Helvetica archival header. 24″ page (1728pt). Validated against macOS's native Quartz renderer. - **PNG**: 7200px (24″ @ 300 DPI), full film texture; effect radii scale with size. - **SVG**: 4800px clean vector with header + soft-bubble gradients. - **Archival metadata** (lab, plate no., exposure date) derives deterministically from the seed, so any export is reproducible and self-labelling. ## Open threads / next ideas - **Tune toward the reference** — once it's in `reference/`, a side-by-side pass on density / curl abundance / track weight / tonality. - **Stereo pairs** — two offset views from one seed, as a diptych. - **Plate series** — deterministic MUON-001…024 grid (archival-sheet aesthetic). - **16-bit / TIFF** export for the highest-end print path (browser PNG is 8-bit). - **Colour stretch goal** — map charge or β to a channel for a stylized variant. - **Performance** — the 7200px PNG does heavy per-bubble stamping + blur; a WebGL post stage (grain/bloom/blur) would make hi-res renders near-instant. ```