HELIOS STATION is a fictional solar-observatory mission control — a single dark screen packed with live telemetry, a shader-lit sun rendered on canvas, and an alarm feed that reprioritizes itself. This log records what it is, the decisions behind the density, the four techniques that carry it, and the three critique passes that got it there. No images were used; every pixel is code.
The mandate was one line: a live mission dashboard for a fictional solar observatory — dark UI done right, with the flex being information density that stays legible. That last clause is the whole design problem. Anyone can make a dark screen busy; the craft is making a busy dark screen readable.
So the concept locked early: this is a working console, not a landing page. It has no scroll narrative, no hero pitch, no marketing. It behaves like the screen a flight director actually stares at — a fixed three-column console that fills the viewport, everything ticking, the sun burning in the middle of it all. The fiction fills in the edges: HELIOS-IV in an L1 halo orbit, a probe named Sentinel-9 skimming the corona, principal investigator Dr. M. Okonkwo-Vance, solar cycle 26 on the ascent. Every string is written as if a real ops team typed it.
Legible density is not one trick — it is a stack of small refusals. Here is what kept the screen from collapsing into noise.
font-variant-numeric: tabular-nums in JetBrains Mono. When a value ticks from 417 to 538, the digits swap in place — nothing reflows, nothing jitters. That single choice is what lets numbers update every 1.4s without the eye flinching.No shader, no WebGL, no texture file. The star is a stack of createRadialGradient fills on a 2D canvas: a bright core-to-limb disc, additive corona particles streaming outward, quadratic-curve prominence arcs off the limb, and a limb-darkening ring painted last. The disc alone is five color stops:
const disc = ctx.createRadialGradient(cx-R*.25, cy-R*.25, R*.1, cx, cy, R); disc.addColorStop(0, '#fff2c8'); // hot core disc.addColorStop(.72, '#ff9a3c'); // mid photosphere disc.addColorStop(1, '#c23c10'); // cool limb ctx.beginPath(); ctx.arc(cx, cy, R, 0, 6.283); ctx.fillStyle = disc; ctx.fill();
The corona is a particle pool set to globalCompositeOperation = 'lighter' so overlapping specks bloom instead of stacking flat. Each particle rides outward on its own life cycle and respawns at a random angle:
ctx.globalCompositeOperation = 'lighter'; for (const p of particles) { p.life += .006 * p.speed * dt; if (p.life > 1) { p.life = 0; p.a = Math.random()*6.283; } const rr = R + (p.maxr - R) * p.life; // stream outward const fade = Math.sin(p.life * Math.PI); // in, then out ctx.fillStyle = `hsla(${p.hue},100%,${60+fade*10}%,${fade*.5})`; ctx.beginPath(); ctx.arc(px, py, p.size*(1+fade), 0, 6.283); ctx.fill(); }
The whole render loop early-returns on document.hidden, so a backgrounded tab burns no CPU — and it honors prefers-reduced-motion.
Every live number on the screen is driven by the same tiny closure. It holds a current value and a drifting target, occasionally re-aims the target, and eases toward it — so readings wander like real instruments instead of flickering like Math.random(). One factory, clamped to a plausible band, reused for temperature, solar wind, flux, velocity, everything.
function walker(v, vol, min, max) { let cur = v, target = v; return function () { if (Math.random() < .08) // occasionally re-aim target = clamp(cur + rand(-vol, vol) * 4, min, max); cur += (target - cur) * .12 // ease toward target + rand(-vol, vol) * .25; // + a little jitter return (cur = clamp(cur, min, max)); }; } const wWind = walker(417, 4, 360, 540); // solar wind km/s
Each flux channel is an SVG that redraws itself from a rolling 40-sample buffer: push a new walker value, drop the oldest, rescale to the current min/max, and rewrite the <path> d string plus a filled area beneath it. No chart library — just string-building the geometry.
sp.data.push(sp.walk()); sp.data.shift(); // roll the window const mn = Math.min(...sp.data), mx = Math.max(...sp.data), r = (mx-mn)||1; const X = i => i/(n-1)*96, Y = v => 32 - ((v-mn)/r)*28 - 2; let d = ''; sp.data.forEach((v,i) => d += (i?'L':'M') + X(i) + ' ' + Y(v) + ' '); sp.line.setAttribute('d', d); sp.fill.setAttribute('d', d + 'L96 34 L0 34 Z'); // close the area
New alarms prepend into a masked, scrolling column. The clever bit: the station's headline status isn't hardcoded — it's derived from the worst severity currently live in the feed. Fold over the alarm rows, take the minimum severity rank, and repaint the top-bar status pill to match. Push an X-flare and the whole station goes to ALERT on its own.
const worst = [...feed.children].reduce((w, c) => { const s = [...c.classList].find(x => sevOrder[x] !== undefined); return Math.min(w, sevOrder[s] ?? 3); }, 3); // 0=critical … 3=good const [cls, txt] = statusMap[worst]; opDot.className = 'dot pulse ' + cls; opState.textContent = txt; // NOMINAL → ALERT · X-FLARE
There is no assets/ payload. Every visual is generated at runtime from code, which is why the whole site is a single file with no network requests beyond the two Google Fonts.
| Element | How it exists | Source |
|---|---|---|
| The sun | Canvas 2D — radial gradients, additive particles, quadratic prominences | procedural |
| Sparklines & flare history | SVG <path> geometry built from rolling data buffers | procedural |
| Favicon & logo mark | Inline SVG (a rayed sun), data-URI in the <link> | procedural |
| Grid & vignette | CSS layered gradients + box-shadow, no image | css |
| All telemetry values | Smoothed random-walk closures, seeded to plausible bands | synthetic |
| Copy & event log | Written to sound like a real solar-weather ops desk | authored |
The build ran the required protocol: screenshot top / mid / bottom at 1440×900 and 390×844, read every frame like a hostile design director, fix everything, and add one deliberate complexity upgrade per pass. Here is the honest record.
align-items: stretch and grid-template-rows: 1fr, then designated exactly one flex-growing panel per column — the feed, the stage, the flare panel — so every column fills to a common bottom edge. Capped the event log with an internal, mask-faded scroll. Upgrade added: a 24-hour X-ray background history chart with M/X threshold guides inside the flare panel, so it reads as full, not floating.prefers-reduced-motion handling for the sweep. Verified zero console errors at both breakpoints, no horizontal scroll, columns bottoming out together at the footer.To build a dashboard that survives the "would an art director stop scrolling?" bar, the load-bearing moves in order:
document.hidden; honor reduced motion. A dashboard that runs forever must be a good citizen in a background tab.Back to the live console: ◂ HELIOS STATION dashboard · browse the full set at the gallery · read the project-wide /guide.