BUILD LOG · DOC HELIOS-B/13 · CLASSIFICATION: OPEN · REV 3

How HELIOS STATION was made A mission dashboard, reverse-engineered by the machine that built it.

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.

Build
One index.html
Stack
HTML · CSS · vanilla JS
Type faces
Space Grotesk · JetBrains Mono
Assets
0 files — all procedural
Passes
3 critique + fix
Console errors
0
01

Brief & Concept

Mandate

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.

02

Design Decisions

Density that stays legible

Legible density is not one trick — it is a stack of small refusals. Here is what kept the screen from collapsing into noise.

  • Monospace, tabular, everywhere. Every number is 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.
  • A reserved status vocabulary. Four colors — good / warning / serious / critical — mean exactly one thing each and are never borrowed for decoration. The plasma amber of the sun and the cyan of the probe live in a separate register, so a red pill always reads as alarm, never as "brand accent."
  • Hairlines, not boxes. Panels are separated by 6.5%-opacity white rules and a single corner tick, not heavy borders. At this density, borders would build a cage; hairlines let the darkness be the negative space.
  • Type as hierarchy. Space Grotesk carries every label and heading in tight uppercase tracking; JetBrains Mono carries every value. Two faces, two jobs — you can tell a label from a datum from three meters away.
  • One balanced console. The three columns are a CSS grid that stretches to a common baseline, with exactly one flex-growing panel per column. No column floats short, so there is no dead zone — the hardest bug of the whole build (see pass 2).
03

Signature Techniques

4 excerpts

A. The sun — a lit disc on canvas

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:

drawing the photospherecanvas 2d
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:

corona particles (additive)canvas 2d
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.

B. Ticking telemetry — one smoothed random walk

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.

the walkerjavascript
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

C. Sparklines — self-drawing SVG paths

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.

redrawing a sparklinesvg
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

D. The event feed — severity drives station status

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.

status from worst live eventjavascript
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
04

Assets & Provenance

All procedural

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.

ElementHow it existsSource
The sunCanvas 2D — radial gradients, additive particles, quadratic prominencesprocedural
Sparklines & flare historySVG <path> geometry built from rolling data buffersprocedural
Favicon & logo markInline SVG (a rayed sun), data-URI in the <link>procedural
Grid & vignetteCSS layered gradients + box-shadow, no imagecss
All telemetry valuesSmoothed random-walk closures, seeded to plausible bandssynthetic
Copy & event logWritten to sound like a real solar-weather ops deskauthored
05

Three Iteration Passes

Critique → fix

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.

PASS 1 First light
FOUND — a massive dead zone. The three columns had wildly unequal natural heights; the event log ran to ~1100px while the center and left columns ended near 350px, leaving an enormous black void across the lower two-thirds. A faint reticle label also overlapped the stage title.
CHANGED — the sun canvas, telemetry, sparklines and feed all landed clean on the first try; the work was structural. Deferred the layout fix to pass 2 and repositioned the reticle to the sun's center. Upgrade added: the GOES flare-class scale (A→X log strip with a live marker).
PASS 2 Balancing the console
FOUND — the dead zone confirmed as the single worst flaw. Column heights driven by content, not by a shared baseline.
CHANGED — reworked the layout to a CSS grid with 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.
PASS 3 Polish & texture
FOUND — the console was balanced and legible; what remained was the difference between "correct" and "alive." Interactive rows had no feedback; the stage was static between number ticks.
CHANGED — added hover states to every event and subsystem row (background lift + rail glow), a slow cyan sensor-sweep gradient across the stage, and full prefers-reduced-motion handling for the sweep. Verified zero console errors at both breakpoints, no horizontal scroll, columns bottoming out together at the footer.
06

Reproduce This

Checklist

To build a dashboard that survives the "would an art director stop scrolling?" bar, the load-bearing moves in order:

  • Commit to tabular monospace for all data. Non-negotiable — it is what makes live numbers calm instead of frantic.
  • Reserve your status colors. Pick four state colors, give each one meaning, and never spend them on decoration. Keep accents in a separate register.
  • Make the grid stretch, not stack. One flex-growing panel per column; let the rest sit at natural height. This kills the dead-zone failure mode before it appears.
  • Prefer procedural over assets. A canvas star and SVG sparklines keep the whole thing in one file with near-zero network cost — and they animate for free.
  • Drive motion from one closure. A single smoothed random walk, reused and clamped, makes a dozen instruments feel independently alive.
  • Pause on document.hidden; honor reduced motion. A dashboard that runs forever must be a good citizen in a background tab.
  • Screenshot, then read your own work. Three passes at two breakpoints. The dead zone was invisible in code and obvious in a screenshot — you have to look.

Back to the live console: ◂ HELIOS STATION dashboard  ·  browse the full set at the gallery  ·  read the project-wide /guide.