BONEYARD · part · layer
The tube
src/20-tube.js · one module, no third-party code
ParametersFactory
How it works
Each frame the ride hands the tube its three stacked 2D canvases (sky, row, corridor). They are uploaded as textures and composited over the field gradient, then combined with the previous tube frame by keeping the brighter of "now" and "last frame times a decay". The old frame drifts slightly toward the warmer core token first, like a long-persistence phosphor changing colour as it fades. The current frame is never softened or recoloured: anything that moves leaves a distinct optical tail, while anything still stays exactly as crisp and bright as drawn. A 4x4 box downsample to quarter resolution (four bilinear taps) keeps one-pixel lines from falling between samples; a horizontal then a vertical 9-tap blur makes the bloom, which is added back. Two low-energy horizontal samples of that tight bloom make the small lens flare a real bright vector tube produces, without blurring the source. The last pass samples through a barrel of k = 0.04 centered on the vanishing point (so the one vanishing point never moves), scaled so the farthest corner samples its own corner (no black rim), and darkens the corners. The rooms are CSS-transformed canvases in the DOM, animated by the core and by SWITCH. Rebuilding their transforms, opacities and flashes inside WebGL every frame would cost a texture upload per room plus a style read per frame, well past 4 ms, so the rooms sit ABOVE the tube and carry their own static halo (a box-shadow on the frame, rasterized once and then only transformed). Every room already looks right without the tube. The adaptive budget drops it first: below internal scale 1 the bloom goes; at 0.5 the tube goes. It turns itself off under the dial's Still (the layers underneath are the still). Without WebGL it reports enabled: false and the ride keeps using its untouched source canvases. The standalone part instead draws an honest Canvas 2D vector diagnostic: it is a designed fallback, not a claim that the phosphor post-pass or the visitor's GPU was tested.
Provenance
- technique
- WebGL1 phosphor post-pass over the composited 2D layers: decay-tinted persistence behind an untouched current frame, a two-scale bloom with a restrained horizontal lens flare, overdriven white-hot cores, a very slight barrel, a vignette, and a light chromatic fringe only at peak warp or a punch
- lineage
- the vector CRT: the oscilloscope; Asteroids (Atari, 1979); Battlezone (Atari, 1980); Tempest (Atari, 1981); Vectrex (1982)
- original
- one skin over the whole row that no room depends on: it takes the sky, the row and the corridor canvases as textures, and the rooms stay crisp DOM above it with a static CSS halo on their frames
- not
- scanlines (vector tubes drew lines, not rasters, so they had none), a curvature gimmick, a blocking dependency; it does not skin the rooms (see HOW IT WORKS)
- deps
- none · WebGL1 when present; a clean ride no-op and an authored Canvas 2D diagnostic on the standalone part otherwise · 2026-09
- budget
- 0.24 ms/frame JS (3 uploads + 5 passes) @ 2160x1350 internal (1440x900 x1.5), desktop Chromium 149 on an AMD RX 5600M (D3D11), 2026-09-29; A/B over 5 s of scroll, frame work with the tube 4.7 to 5.5 ms vs 4.2 to 5.7 ms without (inside the noise); 390x844 x1: 0.21 ms JS, +0.6 to 1.4 ms frame work. A readPixels-synced upper bound (?tubeprobe=1) reads 10 to 14 ms because it also waits on the layers' own 2D raster; on SwiftShader (software GL) the adaptive budget drops it within seconds; phone TBD
- api
- mount(canvas, params, ctx) -> { enabled, tick(dt, t, sources), resize(w, h, dpr), still(t), destroy(), bloom(on), params(p) }
- license
- MIT, Desert Data Labs LLC
Budget: 0.24 ms/frame JS (3 uploads + 5 passes) @ 2160x1350 internal (1440x900 x1.5), desktop Chromium 149 on an AMD RX 5600M (D3D11), 2026-09-29; A/B over 5 s of scroll, frame work with the tube 4.7 to 5.5 ms vs 4.2 to 5.7 ms without (inside the noise); 390x844 x1: 0.21 ms JS, +0.6 to 1.4 ms frame work. A readPixels-synced upper bound (?tubeprobe=1) reads 10 to 14 ms because it also waits on the layers' own 2D raster; on SwiftShader (software GL) the adaptive budget drops it within seconds; phone TBD