2026-05-20 16:53:23 -04:00
|
|
|
# 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
|
Colour palettes, depth/exposure, disk & halo effects; stop tracking rasters
Generator
- Depth & exposure dynamics: per-track chamber depth (z) + event age drive
opacity, bubble size and defocus (depthFactors); depth/aging dials.
- Palette abstraction (src/render/palette.js) — one registry entry per "feel":
mono, charge, beta, kind, kindlife, kindrise, lifecycle, psychedelic,
cyanotype, magentarise. Per-track ink + per-bubble bubbleInk hooks.
- Global colour controls: saturation, hue shift; paper toning (cream/sepia/
selenium/cool/olive/neutral + brightness + gas-glow), bubble edge softness,
iridescent disk (spectral sunburst), chromatic halo. Ink blend chosen by
ground luminance so light-on-dark chemistries composite correctly.
- Tracks carry charge q; bubbles carry lifecycle position + local beta.
- All effects in raster + layered SVG + CMYK/OCG PDF; B&W remains the default.
Tooling & art
- tools/find-semicircle.mjs; render-svg/pdf --seed mode + k=v overrides.
- Curated vector SVG sets under output/ with browsable index.html.
Repo hygiene
- .gitignore: stop tracking generated rasters/PDFs (reproducible from seeds),
the reference image, and a stray db; keep curated SVGs + code + docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:03 -04:00
|
|
|
palette.js pluggable colour "feels" (registry; mono/charge/β + extensible)
|
2026-05-20 16:53:23 -04:00
|
|
|
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 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.
|
Colour palettes, depth/exposure, disk & halo effects; stop tracking rasters
Generator
- Depth & exposure dynamics: per-track chamber depth (z) + event age drive
opacity, bubble size and defocus (depthFactors); depth/aging dials.
- Palette abstraction (src/render/palette.js) — one registry entry per "feel":
mono, charge, beta, kind, kindlife, kindrise, lifecycle, psychedelic,
cyanotype, magentarise. Per-track ink + per-bubble bubbleInk hooks.
- Global colour controls: saturation, hue shift; paper toning (cream/sepia/
selenium/cool/olive/neutral + brightness + gas-glow), bubble edge softness,
iridescent disk (spectral sunburst), chromatic halo. Ink blend chosen by
ground luminance so light-on-dark chemistries composite correctly.
- Tracks carry charge q; bubbles carry lifecycle position + local beta.
- All effects in raster + layered SVG + CMYK/OCG PDF; B&W remains the default.
Tooling & art
- tools/find-semicircle.mjs; render-svg/pdf --seed mode + k=v overrides.
- Curated vector SVG sets under output/ with browsable index.html.
Repo hygiene
- .gitignore: stop tracking generated rasters/PDFs (reproducible from seeds),
the reference image, and a stray db; keep curated SVGs + code + docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:03 -04:00
|
|
|
- **Depth & exposure** (Phase 1): the chamber is a volume photographed with finite
|
|
|
|
|
depth of field. Every track carries a depth `z` (0 = focal plane) and an `age`
|
|
|
|
|
(0 = current trigger); one interaction's tracks fan out across a range of z.
|
|
|
|
|
`depthFactors(track, params)` maps these — with the `depth` and `aging` dials —
|
|
|
|
|
to **tone** (opacity), **sizeScale**, and **softness**. Softness folds into one
|
|
|
|
|
continuous value with the global **`bubbleSoft`** edge-softness dial and drives
|
|
|
|
|
the raster sprite's gradient (`bubbleStops`, shared with the SVG bubble
|
|
|
|
|
gradient so raster/vector match) and footprint. Variation is depth/time-driven, not noise, so
|
|
|
|
|
it reads as a 3D volume and a temporal palimpsest. Density (ionisation) is left
|
|
|
|
|
to β; depth only touches opacity/size/softness. Applies in raster *and* vector.
|
|
|
|
|
|
|
|
|
|
## Colour palettes (Phase 2)
|
|
|
|
|
|
|
|
|
|
Colour is an optional, swappable **"feel"** layered on the geometry — **B&W is the
|
|
|
|
|
default** (`palette: 'mono'`). `palette.js` is a registry; each entry maps the
|
|
|
|
|
scene's physics to colour via optional hooks: `ink(track,env)` (per-trail colour),
|
|
|
|
|
`paper(env)`, `feature(env)` (non-particle marks), `vign(env)`. Adding a feel is
|
|
|
|
|
one entry. `resolvePalette(id, env)` fills defaults so renderers stay simple.
|
|
|
|
|
Shipped: `mono`, `charge` (duotone by charge sign), `beta` (velocity → spectral),
|
|
|
|
|
and `lifecycle` (per-bubble: colour follows each particle birth→death). Two hook
|
|
|
|
|
levels: `ink(track,env)` for per-track colour, and `bubbleInk({track,life,beta},env)`
|
|
|
|
|
for **per-bubble** colour — `sampleBubbles` tags every bubble with `life` (0=birth
|
|
|
|
|
at the vertex, 1=death/rest) and local `beta`. `resolvePalette` exposes `perBubble`
|
|
|
|
|
so renderers stamp a colour-keyed sprite per-track (fast) or per-bubble (quantised,
|
|
|
|
|
still bounded). SVG emits a gradient per distinct colour, PDF uses RGB for trails
|
|
|
|
|
(mono stays CMYK rich black). Track attributes for mappings: `q` (charge),
|
|
|
|
|
`pts[i].beta`, `kind`, `weight`, `z`, `age`. Colour is render-only and orthogonal
|
|
|
|
|
to geometry, so any seed re-colours without changing its forms.
|
|
|
|
|
|
|
|
|
|
Also shipped: `kind` (one categorical hue per particle type — primary/δ-ray/cosmic/
|
|
|
|
|
sweeper/V-decay), `kindlife` / `kindrise` (type sets the hue, lifecycle sets the
|
|
|
|
|
*intensity* — fading toward, or rising into, the death), `psychedelic` (per-bubble
|
|
|
|
|
hue cycles, `hueCycles`), `cyanotype` (a full chemistry: overrides `paper()` + ink
|
|
|
|
|
for the blueprint look), and `magentarise` (a restrained example bundling a
|
|
|
|
|
magenta-family kind-scheme + rising intensity + a monochrome burnt-orange disk via
|
|
|
|
|
`feature()`). A palette is one registry entry; hooks: `ink`/`bubbleInk`/`paper`/
|
|
|
|
|
`feature`/`vign`.
|
|
|
|
|
|
|
|
|
|
**Saturation** is a global `saturation` dial applied in `resolvePalette` (about
|
|
|
|
|
each colour's own luminance: 0 = grey, 1 = as-authored, >1 boosted), and
|
|
|
|
|
**`hueShift`** rotates every palette's hues (CSS hue-rotate matrix; greys are
|
|
|
|
|
unchanged so mono stays B&W) — both ink-only, substrate untouched.
|
|
|
|
|
|
|
|
|
|
Because a palette may set a dark ground (cyanotype), the raster renderer chooses
|
|
|
|
|
its ink **blend mode from the actual paper luminance** (`pal.darkGround()`) rather
|
|
|
|
|
than the `invert` flag — so light-on-dark chemistries composite correctly. For
|
|
|
|
|
`mono` this is identical to the old invert-based behaviour.
|
|
|
|
|
|
|
|
|
|
**Paper toning & shading** is a *separate* system from the ink palette
|
|
|
|
|
(`paperTone(params, inv)`): a photographic toner (`paperTone` select — cream /
|
|
|
|
|
sepia / selenium / cool / olive / neutral, as tint multipliers relative to cream),
|
|
|
|
|
`toneStrength`, `paperBright`, and `glow` (gas-glow gradient strength). It feeds
|
|
|
|
|
the renderers' base paper + vignette tint, so any ink palette pairs with any paper
|
|
|
|
|
(e.g. sepia substrate under lifecycle trails). Defaults (cream / 1 / 1 / 0.5)
|
|
|
|
|
reproduce the committed look exactly. A palette may still fully override `paper()`
|
|
|
|
|
for a complete "chemistry" feel (e.g. a future cyanotype).
|
2026-05-20 16:53:23 -04:00
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
```
|