← DEVLOG
Simulator2026.02.159 min read

Recreating SpaceX Starship Mission on the Web

From launch to Mechazilla catch — building a physics simulation and audio synthesis using only Canvas 2D and Web Audio API

canvasweb-audiophysicsanimation

The Question — "Can This Be Built on the Web?"

After watching the Super Heavy booster get caught by the launch tower's mechanical arms in IFT-3, I wanted to recreate that moment using only Canvas 2D API and Web Audio API. No WebGL, no external audio files.

It turned out to be possible. But far more intricate than expected.


Physics Simulation Design

Coordinate System

The rocket's state is driven by a scrollY-based coordinate system.

// Launch→separation: scrollY accumulated value maps to altitude
const altKm = scrollY * SCROLL_TO_KM;
// e.g. SEP_TRIGGER_SCROLL(16000px) × 0.00437 ≈ 70km

As scrollY increases, the camera moves upward — the rocket stays centered while the background (stars, clouds, Earth curvature) scrolls down, creating parallax depth.

Stage Separation

Separation is the most critical physics event. Based on actual IFT data, separation altitude is 67–70km.

if (altKm >= SEP_ALT_KM && st.phase === 'ascent') {
  st.phase = 'separation';
  st.boosterVy = SEP_BOOSTER_VY; // begin retroburn
  st.starshipVy = SEP_STARSHIP_VY; // upper stage continues
}

For 30 frames after separation, thrustLevel ramps down to zero — the engines fade out gradually.


Mechazilla Catch Sequence

This was the trickiest part. The arm Y-coordinate calculation involves two conflicting reference frames.

  • padY: launch tower base Y (ground reference)
  • padYAnim: actual Y where the booster lands (92px offset)
// Gripper position = padY - (92 + s1H - 5)
const gripperY = padY - (92 + BOOSTER_S1H - 5);
// armTopY ratio = (towerH - 208) / towerH ≈ 0.27

Initially I ignored this 92px offset and the arms clipped outside the rocket body. The root cause was mismatched coordinate frames.


Audio — Pure Web Audio Synthesis

Zero external audio files. Every sound is synthesized in real time using OscillatorNode, BiquadFilterNode, and GainNode.

Engine Rumble

Low-frequency noise base with high-frequency harmonics to mimic Raptor engine characteristics.

const noise = createNoiseSource(ctx);
const hpf = ctx.createBiquadFilter();
hpf.type = 'highpass';
hpf.frequency.value = 80; // cut low rumble floor

Reentry Heat Shield Audio

Peak at 52km altitude, with smoothstep transitions from 12–78km for smooth intensity control.

const t = smoothstep(12, 52, altKm) - smoothstep(52, 78, altKm);
setReentryHeatIntensity(t); // 0 → 1 → 0

Performance Optimizations

DPR Cap

On 4K displays, an uncapped Canvas renders at 8K resolution, overwhelming the GPU.

const MAX_AREA = 1920 * 1080;
const area = w * h;
const dpr = Math.min(window.devicePixelRatio, Math.sqrt(MAX_AREA / area));

uiTick Pattern

Calling setState every frame explodes React render cost. Sync once every 6 frames instead.

uiTick++;
if (uiTick >= 6) {
  uiTick = 0;
  setUiSnapshot({ phase: st.phase, altKm: st.altKm });
}

Engine Cluster OffscreenCanvas Cache

The Super Heavy booster's engine cluster draws 31 radialGradient circles per frame — one for each Raptor engine nozzle glow. Since the engine layout only changes when the thrust level or engine configuration changes, this is largely redundant work.

An OffscreenCanvas caches the engine cluster image. A cache key built from ${mode}:${seed}:${thrustBucket} determines whether a redraw is needed. The thrust level is quantized to 3–5 frame buckets, meaning the actual visual difference between adjacent frames is imperceptible.

const cacheKey = `${mode}:${seed}:${Math.round(thrustLevel * 20)}`;
if (cacheKey !== prevKey) {
  drawEngineClusterToOffscreen(offCtx, engines, thrustLevel);
  prevKey = cacheKey;
}
ctx.drawImage(engineOffscreen, dx, dy);

This achieves roughly 97% cache hit rate during sustained burns. The 31 radialGradient fills are replaced with a single drawImage call on cache-hit frames.

Cloud Rendering Optimization

The launch and landing sequences feature a cloud layer that the rocket passes through. Drawing hundreds of gradient circles per cloud is expensive, so three optimizations are applied:

  • Three-mode gradient (white, shadow, warm): instead of uniform shading, each circle uses one of three pre-defined gradient profiles, reducing per-circle computation while improving visual variety.
  • Viewport culling: clouds outside the visible scroll region are skipped entirely. During ascent and descent, at most half the cloud array is actually on screen.
  • Mobile cloud count halving: on viewports narrower than 520px, the cloud particle count is halved. The visual density remains adequate on smaller screens.

Closing Thoughts

From initial idea to completion took about three weeks. Things I thought Canvas 2D couldn't handle turned out to be possible; things I never considered became the real blockers.

The audio synthesis — building engine rumble purely from waveforms — was unexpectedly satisfying. The moment when the noise filter combination started sounding vaguely like a rocket engine was the highlight of the whole build.

Content related to this post

Try it yourself