← DEVLOG
Space Science2025.11.297 min read

Stellar Life — Making the HR Diagram Interactive

From protostar to white dwarf — building an interactive simulation where you can manually step through every stage of a star's life

canvasastronomystellar-evolutioninteractive

The Idea — Compressing Billions of Years

Every star in the night sky is evolving. They're just too slow for us to notice. The Sun itself will swell into a red giant in about five billion years, eventually engulfing Earth's orbit.

This simulation compresses that journey into something you can step through in seconds, directly in a browser.


Defining the Stellar Evolution Stages

Each stage's color and physical characteristics are based on real astronomical data.

const STAR_STAGES = {
  protostar: { color: '#aa66ff', tempK: 3000, lumRatio: 0.01 },
  mainSequence: { color: '#ffdd44', tempK: 5778, lumRatio: 1.0 },
  redGiant: { color: '#ff7733', tempK: 3500, lumRatio: 200 },
  supernova: { color: '#ff4422', tempK: 30000, lumRatio: 1e9 },
  neutronStar: { color: '#88aaff', tempK: 600000, lumRatio: 0.001 },
};

The protostar is violet — a dust cloud still collapsing under gravity before nuclear fusion begins. The main sequence represents stable hydrogen fusion, like our Sun. The red giant is the stage where hydrogen runs out and the outer layers expand. The supernova is the shortest and brightest moment of all.


H-R Diagram Canvas Rendering

The Hertzsprung–Russell diagram plots a star's temperature (x-axis) against luminosity (y-axis). The current stage's position on the diagram updates in real time.

// Temperature → x coordinate (high temp on the left)
const tempToX = (tempK: number) => {
  const logT = Math.log10(tempK);
  return W - ((logT - LOG_T_MIN) / (LOG_T_MAX - LOG_T_MIN)) * W;
};

// Luminosity → y coordinate (high luminosity at top)
const lumToY = (lumRatio: number) => {
  const logL = Math.log10(lumRatio);
  return H - ((logL - LOG_L_MIN) / (LOG_L_MAX - LOG_L_MIN)) * H;
};

The main sequence band is drawn as a semi-transparent white diagonal, and the giant branch region is marked with a faint orange background. The current stage marker glides smoothly to its position with a glow effect.


Canvas Star Rendering — Corona and Sunspots

The visual representation of the star is built from three layers.

Layer 1 — Corona Glow: A radialGradient that fades outward from the star's edge, expanding to 1.8× the star's radius.

Layer 2 — Star Surface: The highlight point is offset from the center to create a sense of spherical volume.

const grad = ctx.createRadialGradient(cx - r * 0.25, cy - r * 0.25, 0, cx, cy, r);
grad.addColorStop(0, lightenColor(stage.color, 0.6));
grad.addColorStop(0.5, stage.color);
grad.addColorStop(1, darkenColor(stage.color, 0.4));

Layer 3 — Sunspot Effect: Small dark circles placed randomly on the surface, visible only during main sequence and red giant stages. A seededRandom function keeps positions stable across frames.


Audio — Each Stage Has Its Own Voice

Every stage is given a distinct frequency and timbre, synthesized purely with Web Audio API.

  • Protostar: 40Hz low drone — the weight of gravitational collapse
  • Main Sequence: 220Hz stable sine + harmonics — the quiet burn of a star in balance
  • Red Giant: 80Hz + 130Hz bass — the low rumble of expansion
  • Supernova: White noise burst + high-frequency spike — the chaos of the explosion
  • Neutron Star: 1200Hz pulse — the millisecond rotation of a pulsar
const startStageAudio = (stage: StarStage, ctx: AudioContext) => {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.frequency.value = STAGE_AUDIO[stage].freq;
  osc.type = STAGE_AUDIO[stage].type;
  gain.gain.setValueAtTime(0, ctx.currentTime);
  gain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.5);
  osc.connect(gain).connect(ctx.destination);
  osc.start();
};

Auto-Advance Timer + Manual Stage Switching

In auto mode, each stage holds for a set duration before advancing. Protostars and main sequence stages linger; supernovae are brief.

const STAGE_DURATION_MS = {
  protostar: 4000,
  mainSequence: 6000,
  redGiant: 5000,
  supernova: 1500, // a fleeting moment
  neutronStar: 4000,
};

A click skips the current stage and advances immediately. During transitions, the star's size and color are interpolated with lerp for smooth visual continuity.


Canvas Performance — OffscreenCanvas Blur Batching

The star's corona glow and flare effects originally used Canvas 2D filter = 'blur(N)' on individual draw calls. Each blurred arc or gradient is expensive because the browser must rasterize the shape, allocate a temporary buffer, apply a Gaussian convolution, and composite the result. During the supernova stage, this totaled 438 blur operations per frame.

The solution is to batch all blur-requiring draws onto an OffscreenCanvas, render them without blur, and then draw the entire offscreen buffer onto the main canvas with a single filter = 'blur(...)' applied.

// Draw all glow elements unblurred onto the offscreen canvas
offCtx.clearRect(0, 0, off.width, off.height);
drawCoronaGlow(offCtx, cx, cy, r, stage);
drawFlareRays(offCtx, cx, cy, r, tick);

// Single blurred composite onto the main canvas
ctx.save();
ctx.filter = `blur(${blurRadius}px)`;
ctx.drawImage(off, 0, 0);
ctx.restore();

This reduced blur operations from 438 to 7 per frame — a 98% reduction. The offscreen canvas is allocated once at module level and resized only when the viewport dimensions change, avoiding per-frame allocation overhead.

The visual result is identical. Blur is a convolution operation, and blurring a pre-composited image produces the same soft-glow effect as blurring each element individually — as long as all elements share the same blur radius, which they do in this case.


Closing Thoughts

The Sun is roughly halfway through its 10-billion-year lifespan. That number never quite registered until I built this. Each time you click through a stage and watch the star transform, the scale of it becomes strangely tangible.

Compressing billions of years into a single click — a star's life turns out to be surprisingly short.

Content related to this post

Try it yourself