VAPOR Technical supplement
← Back to console

How it's made

One dial. Four regimes.
Everything follows.

VAPOR has no pages to navigate — only a state to change. This supplement documents the canvas engine behind the backdrop, the CSS trick that lets one variable morph an entire instrument panel, and the passes that got it there.

01 Concept

VAPOR is the relay console for Kepler-1649c, an imagined exoplanet whose surface weather is piped home as four discrete regimes — rain, snow, aurora, heat — rather than a continuous forecast. The console doesn't simulate a whole planet; it simulates the feed, and the segmented dial is a channel selector. Every panel — current conditions, a 7-sol forecast, an atmospheric-composition donut, a wind-vector mini-map — reads from the same small data table, so switching regimes never leaves one instrument out of sync with the others.

02 The weather engine

The backdrop is one full-bleed <canvas>, cleared and redrawn every frame — no DOM particles. Each regime is its own draw method (drawRain, drawSnow, drawAurora, drawHeat) working off particle arrays built once from a seeded mulberry32 PRNG, so the field looks the same on every reload instead of re-randomizing into a different composition. A mode switch never hard-cuts: the loop calls the outgoing regime at alpha = 1‑t and the incoming one at alpha = t, where t comes from a cubic-bezier solved by hand with Newton–Raphson iteration — the same easing math the browser runs for CSS cubic-bezier(), so canvas motion and DOM transitions share one feel. Rain gets angled streaks, ground splashes and a full-scene lightning flash with a two-pulse decay envelope; snow drifts across three parallax layers and slowly banks a bumpy accumulation line; aurora is three sine-summed ribbon bands alpha-blended with lighter compositing over a twinkling starfield; heat is the hard one — see below.

03 The heat-shimmer trick

Real refraction is expensive; the illusion doesn't have to be. Heat mode renders a ridge silhouette and a warm gradient once per frame into a tiny offscreen buffer canvas, then blits it back to the visible canvas one thin horizontal slice at a time, each slice offset sideways by amplitude × sin(row·frequency − time·speed). Amplitude grows toward the ground and shrinks to nothing near the top of the haze band, so the distortion reads as rising off hot ground rather than smearing the whole sky. It's a handful of drawImage calls, not a shader — and it earned its keep the hard way: an early pass forgot to clearRect the buffer before redrawing it, so every frame's translucent fill composited on top of the last until heat mode slowly bled into a solid orange screen. Caught on a five-second soak test, fixed with one line.

04 Morphing the instrument

The panels never run a JS color loop. Two root custom properties, --accent and --accent2 (plus RGB-triplet siblings for use inside rgba() glows), get reassigned once, synchronously, on click. Every rule that consumes them — glass glow, dial thumb, donut stroke, wind arrows — declares its own transition on the real property (background, stroke, box-shadow…), and the browser animates the *used value* across that change even though the variable itself jumped in a single frame. That single trick is what makes "everything morphs" affordable: no per-frame style writes for the DOM half of the console. Numbers still get real tweening — the temperature counts up through the same eased curve — and text (the condition tag, the note, stat labels, forecast cards) crossfades via a staggered opacity/translate class-swap timed inside the 1.2s window, so the panel reads as a cascade of small, offset reveals rather than a jump cut. The donut's arcs animate for free: stroke-dasharray and stroke-dashoffset are ordinary transitionable properties once you set them via element.style instead of as bare attributes.

05 Data, voice, and the wind field

Each regime carries its own small dataset — temperature, wind, a third reading that relabels itself (humidity, ionization, particulate), a composition donut where the fourth slice is always the "live" trace gas, and a 7-sol trend baked straight into a sparkline. Copy stays mission-log short: no adjectives that don't carry a number nearby. The wind mini-map skips a real noise library — three summed sine terms (noise2) stand in for gradient noise, cheaply enough to drive twenty-eight arrows and eight flowing tracer particles at sixty frames a second inside a small canvas.

06 Passes

Build pass: engine, panels, and copy went in together, since none of the four regimes is convincing without live data behind it. First screenshot audit at 1440×900 caught a real composition problem — the current-conditions panel spans two grid rows but its content only filled one, leaving a dead gap — fixed with a 7-sol trend sparkline that reuses forecast data instead of inventing a new field. Live interaction testing (clicking through all four modes, then soaking heat mode for several seconds) caught the buffer-accumulation bug described above; a five-second wait a static screenshot would never have taken was what surfaced it. Reduced-motion was verified with an actual emulated browser context, not assumed: mode switches still update every value, but the canvas renders one still frame per regime and the loop never starts.