Mission appendix · How this was built
The math, the lie, and the switch that confesses.
HELIOS is a data-visualization love letter to the solar system: an animated SVG orrery, a scroll journey from the Sun to Pluto, and one deliberately uncomfortable toggle. This appendix documents the working parts.
01The concept
Every solar-system diagram ever printed is a compromise — at true scale, the page is empty. HELIOS leans into that: the interface defaults to a readable lie (log-compressed distances, inflated planets) and offers a single switch to the uncomfortable truth, deadpanning the consequences in a caption. The toggle is the lesson.
Aesthetically it borrows from precision instruments and mission-control UI: deep space ink (#080a12), hairline orbit strokes at rgba(255,255,255,.18), a starfield speckle, and every number set in crisp monospace. Two typefaces only: Archivo (black weights, tight tracking) for display, IBM Plex Mono for data readouts. Each planet keeps its true-ish color character — Mars #c1583c, Neptune #4a6fd4 — against a committed 7-value palette.
The figures are real, rounded to survey precision. Orbits are drawn circular and coplanar (real ones are ellipses — Mercury's eccentricity is 0.21), which is why the orrery footer rates itself "~96% honest."
02The techniques
The orrery math: one line per planet
Each planet stores its real orbital period and its true heliocentric mean longitude at the J2000 epoch (noon UTC, January 1, 2000). Position is pure arithmetic — no physics engine:
// mean longitude at J2000 + mean motion — circles, no Kepler
const angle = p.L0 + 360 * (simYears / p.periodYears);
const x = cx + r * Math.cos(angle * Math.PI / 180);
const y = cy - r * Math.sin(angle * Math.PI / 180); // minus: SVG y points down
Because the starting angles are the real J2000 longitudes, pausing the clock at t = 0 shows where the planets actually were that day — which is exactly what the reduced-motion experience does.
One shared clock, correct relative speeds
There is a single simulation clock, advanced by requestAnimationFrame. Every planet derives its angle from it, so relative speeds are automatically correct — Mercury (P = 0.241 yr) laps about 4.15 times while Earth orbits once. The speed slider maps non-linearly from paused to one year per ten seconds:
simYears += yearsPerSecond * dt; // the one clock
speed = 0.1 * Math.pow(slider / 100, 2.2); // 0 → 1 yr / 10 s, eased
The honest scale toggle
The trick is to precompute two complete layouts per planet — readable (log-spaced orbit radius, inflated dot) and true (linear px-per-AU, true-size dot) — then interpolate one blend parameter between them over 1.2 s:
const t = easeInOutCubic(Math.min(1, (now - t0) / 1200));
const r = lerp(p.radiusReadable, p.au * PX_PER_AU, t); // orbits explode outward
const s = lerp(p.dotReadable, Math.max(0.7, p.truePx), t); // planets collapse to dots
At true scale Earth is genuinely sub-pixel — about 0.001 px wide at this render size — so the code clamps dots to a minimum radius and the caption admits the clamp out loud, computing the honest number live from the SVG's rendered width. Clamping without confessing would just be a smaller lie.
The distance ribbon
Each journey section carries a data-au attribute. On scroll, a reference line at 55% of the viewport finds which two sections bracket it and linearly interpolates their AU values — so the tally accumulates smoothly between planets rather than jumping:
const t = (y - stops[i].top) / (stops[i+1].top - stops[i].top);
const au = lerp(stops[i].au, stops[i+1].au, t);
1,321 Earths, one canvas
Jupiter's volume stat renders 1,321 dots. That many DOM nodes would be wasteful, so it's a 2D canvas that draws only the newly revealed dots each frame, with an eased count-up — total work stays constant no matter how long the animation runs.
Everything else
Planet portraits are hand-authored inline SVG: radial gradients for limb shading, Saturn's rings as ellipses using a stroke-dasharray equal to half the circumference so only the front arc overlaps the planet, Jupiter's bands as clipped rects, Neptune's storm as one dark ellipse. Scroll reveals use a single IntersectionObserver adding .in-view, with stagger via a --d transition-delay custom property. The starfield is a fixed canvas painted once per resize. prefers-reduced-motion parks the orrery at J2000, disables the clock, makes the toggle instant, and shows every stat at its final value.
03How it was made
Built by Claude (Fable 5) writing vanilla HTML, CSS, and JavaScript by hand — no frameworks, no build step, no dependencies beyond the Google Fonts CSS API. Two files, one canvas, one SVG, one clock.
HELIOS is one room in the Fable Showcase — 25 wildly different demo sites exploring what one model can do in web design. The planetary data is real; the survey publishing it is fiction.
04Steal this
- One clock, many derivatives. Never animate related things independently. Advance a single time value and derive every element from it — correct relative motion becomes free, and pause/speed controls touch one variable.
- Lerp between two complete layouts, not between keyframes. Precompute both end states per element, then blend one eased parameter. State transitions of any complexity collapse into one number.
- When you must cheat, confess in the UI. Clamps, compressions, and simplifications become features when the interface states them plainly. Deadpan honesty is a design voice.
- IntersectionObserver + a
--dcustom property gives you choreographed, staggered scroll reveals in ~10 lines, with per-element delays set right in the markup. - Canvas past ~200 elements. For dot grids and starfields, draw incrementally on a canvas instead of minting DOM nodes — and scale it by
devicePixelRatioso it stays crisp.
HELIOS · mission appendix · a fictional survey for the Fable Showcase
Built by Claude Fable 5 · Return to the orrery →