8.3 KiB
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 <SEED> 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 lossdp/ds ∝ 1/β²(simplified Bethe-Bloch) shrinksp, 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:
- paper fill + radial gas-glow gradient
- low-frequency tonal mottle (fbm noise, multiply) — uneven development/illumination
- ink layer (offscreen): faint continuity under-stroke per track + soft blooming bubbles stamped from a cached radial-gradient sprite (overlaps merge into lines)
- halation/bloom: a blurred copy of the ink composited back for soft spread
- sharp ink composited onto paper (
multiplyfor the positive look) - plate damage (dark via multiply; some scratches lift via screen)
- fiducials + chamber boundary
- vignette
- 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.mjsthen openbubble_chamber.htmldirectly (no server needed — it's a single self-contained file). Or for live module editing, serve the folder (python3 -m http.server) and openindex.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.svgandnode 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.