Project · Live in the browser

Orbit Explorer

Solo project, from the Rust engine to the interface

A digital twin of Earth orbit that runs in the browser, and flies out to every planet, the Moon, and the Oort cloud. It propagates all 32,583 objects in the public catalog live, routes packets across Starlink’s laser mesh, screens the whole catalog for close approaches, and maps coverage. Beyond Earth, every known moon of Mars and the giant planets and every spacecraft orbiting another world fly on JPL’s trajectories. I wrote the orbital mechanics in Rust from the published model definitions, compiled them to WebAssembly, and checked each result against a published reference.

What is SGP4?

SGP4 is the standard model for predicting where a tracked object will be. The US Space Force publishes every object’s orbit as a few mean elements, and SGP4 turns them back into a position and velocity at any time, accounting for Earth’s flattening, atmospheric drag, and, on high orbits, the pull of the Sun and Moon. The elements are only good to about a kilometer, so an implementation is judged by how exactly it reproduces the reference code.

  • Rust
  • WebAssembly
  • TypeScript
  • React
  • Three.js
  • WebGPU
  • D3
  • Zustand

Results

  • 32,583objects propagated live

    The full public catalog from Space-Track, at 60 fps on an M1 MacBook Air. Each frame’s JavaScript takes about 2.6 ms, and moving every object takes 0.22 ms of it.

  • 0.10 mmworst error on 666 reference states

    My SGP4 against the published verification output for the model, deep-space orbits and the cases built to fail included.

  • 423×faster close-approach sieve

    A sorted spatial grid finds every pair within 10 km in 1.5 ms, where checking all 530 million pairs takes 638 ms. At 100,000 objects it is 1,522× faster.

  • 21 msLondon to New York by laser

    About 6 ms faster than the best fiber possible, a straight cable along the 5,570 km great circle, routed over the satellites’ real positions.

Explore · the opening view

Every tracked object,
live in the browser.

Satellite trackers like Stuff in Space put everything in orbit on a globe and stop there. I wanted a twin that answers the questions an engineer would ask, worked out from the orbit model up and checked against a published reference. Here is the app redrawn in dots, its catalog sampled to fit your screen, at ten minutes a second.

  1. It opens over North America with all 32,583 objects of the October 9 catalog live, around an Earth lit by the real Sun.
  2. Nearly nine in ten circle within 2,000 km of the surface, the bright halo hugging the globe.
  3. Pull back, and the navigation satellites near 20,000 km and the geostationary ring at 35,786 km come into view.
  4. Color is kept for data. A third of the catalog is Starlink, in blue, and two in five objects are debris or spent rocket bodies, in red.
  5. Layers switches the reference frame. Fixed to the stars, the orbital planes hold still and the Earth turns beneath them.
tracked objects
32,583
dots drawn
–
frame
Earth

Follow · Starlink by hardware generation

Click a dot,
and ride along with it.

Selecting an object opens a card with its live state and draws its orbit, faint behind it and bright ahead. Follow flies the camera down along a great-circle arc and rides beside it. Up close, every Starlink is drawn as the hardware it carries. Its NORAD ID says which launch it came from, so a table built from Jonathan McDowell’s launch list tells a V1.5 from a V2 Mini or a V3.

  1. Select a Starlink in the 53° shell. Its card reads out altitude, speed, and period, and its orbit is drawn ahead of it.
  2. Follow flies the camera there along a great-circle arc, slowing as it closes in.
  3. 3,143 are V1.0 or V1.5, a flat-pack bus with one solar array about 8 m long.
  4. 7,962 are V2 Minis, two long panels on one boom, about 30 m end to end.
  5. 26 are V3s, launched on Starship flight 14 in September 2026, about 60 m across. Each close-up is framed to fit, so the three aren’t to scale.
V1.0 and V1.5
3,143
V2 Mini
7,962
V3
26

Laser routing · +Grid · Dijkstra

London to New York,
faster than fiber.

SpaceX doesn’t publish how its satellites link to one another, so the twin models the +Grid from the research literature. Four times a second it recovers shells and orbital planes from the elements alone, links each satellite to its neighbors, and runs Dijkstra between two ground terminals. The 53° shell here runs live in this page on a port of the engine’s code, and at the snapshot it finds the engine’s own route, to the microsecond.

  1. Starlink’s 53° shell, 4,640 satellites at 464 km, flying from their October 9 positions at a minute a second.
  2. Nothing is labeled. Gaps in inclination, altitude, and node split the catalog into seven shells, and this shell into 100 planes.
  3. +Grid links each satellite to the ones ahead and behind in its plane, and to the nearest one in the next plane east.
  4. Dijkstra finds the shortest chain of laser hops from London to New York, entering and leaving through satellites at least 25° above each city.
  5. Light in vacuum beats light in fiber. About 21 ms one way, against 27.3 ms through an ideal cable on the great circle.
laser links
–
one-way latency
–
ideal fiber
27.3 ms

Conjunctions · the hour after the snapshot

Starlink’s biggest hazard
is Starlink.

The Conjunctions mode screens every tracked object against every other for close approaches, up to a day ahead, and marks each on the timeline. The hour after the snapshot holds 2,981 pairs passing within 5 km. Most meet where the planes of Starlink’s dense shells cross, nearly head-on. TLEs are only good to about a kilometer, so these are screening hits to look at more closely, not collision probabilities.

  1. Each dot is one encounter under 5 km in the hour after the snapshot, placed where it happens and colored by miss distance.
  2. Sorted by how fast each pair closes, 68% meet faster than 10 km/s, crossing almost head-on.
  3. 55% are Starlink meeting Starlink.
  4. 30% involve debris or a spent rocket body, which only the full catalog shows.
  5. The closest, STARLINK-2063 and FLOCK 4G 24, pass 43 m apart at 14.5 km/s. Watch it happen jumps the clock to 90 s before, with both orbits drawn.
encounters under 5 km
2,981
Starlink with Starlink
55%
faster than 10 km/s
68%

Coverage · the engine’s 2° grid

Covered everywhere,
but not evenly.

Coverage stamps each linked satellite’s visibility cap onto a 2° grid of the Earth, so its cost grows with the satellites and the size of a cap rather than with every cell. A test holds the stamp to the brute-force check on every cell. At the snapshot, no place on Earth is without a Starlink above 25°.

  1. Each dot of the globe is colored by how many Starlinks its 2° cell sees above 25°, from the engine’s grid at the snapshot.
  2. Every cell sees at least four, and 46 on average, weighted by area.
  3. Averaged around each latitude, the count peaks near 47° north and south, where the 53° shells turn around and bunch up.
  4. The equator sees about 38, and near the poles only the 70° and 97° shells pass, a dozen at a time.
fewest
–
mean
–
most
–

Catalog · Space-Track · October 9, 2026

Unfold the sky,
and the structure shows.

Placing each object by its mean altitude and its inclination, the tilt of its orbit against the equator, turns the cloud into a map of how orbit is used. Starlink’s shells are nearly straight lines, because SpaceX flies thousands of satellites at one height and one tilt. Old breakups show up as smears, and above 2,000 km the navigation constellations and the geostationary belt each keep to their own corner.

  1. The app’s Catalog mode counts what is up there, and at which altitude, one row of histograms per group, each on its own scale.
  2. Unfold the sky. Each object moves to its mean altitude, on a log scale, and its inclination.
  3. Starlink flies in shells, thousands of satellites at one height and one tilt, at 43°, 53°, 70°, and 97°.
  4. Fengyun-1C, destroyed in a 2007 anti-satellite test, still fills 98.8° at 824 km with 1,979 pieces.
  5. 85% of everything between 700 and 1,000 km is debris, including what the 2009 collision of Iridium 33 and Cosmos 2251 left at 86° and 74°.
  6. Higher up, navigation constellations fly near 55° and 20,000 km, and the geostationary belt sits at 0°.
Starlink
11,131
debris & rocket bodies
13,744
other satellites
7,708

Flying between worlds · planned in log-distance

Two and a half seconds
to the Moon.

The Fly to list holds every planet, the Moon, and the Oort cloud, each with how far it is right now. Distances there span five orders of magnitude, so a flight is planned in log-distance. The camera rises off one world, its distance growing geometrically, then falls toward the next just as fast. Splitting the time by each half’s log-distance keeps the speed continuous where they meet, and the camera never passes through either world. The flight takes about 2.6 s, slowed down here.

  1. Click the Moon in the Fly to list, 384,796 km away at the snapshot.
  2. The camera rises off the Earth, its distance growing geometrically while it swings around to the side facing the Moon.
  3. Halfway, 192,000 km from each, it has already turned to look at where it is going.
  4. It falls toward the Moon and swings around to arrive 50° off the Sun, so the terminator shows the relief of LRO’s map.
  5. Five spacecraft orbit the Moon on their missions’ own trajectories, from JPL Horizons. LRO, Chandrayaan-2, and Danuri circle the poles every two hours, and the two ARTEMIS probes loop out to 16,000 km.
camera from the Earth
–
from the Moon
–
flight
0.0 s

Link to Earth · relay passes over a landing site

When a rover
can call home.

A lander reaches the Earth two ways, directly while the Earth is above its horizon, or through an orbiter passing overhead, which stores the data and forwards it. Link to Earth samples the elevation of the Earth, the Sun, and every orbiter from a site over the next 24 hours, and turns the samples into passes, each rise and set refined to the second.

  1. At Tranquility Base, where Apollo 11 landed, the Earth never sets. The Moon keeps one face toward us.
  2. At Chang’e 4’s site on the far side it never rises, so every signal has to go through a relay.
  3. At Shackleton crater on the south pole, an Artemis target, LRO, Chandrayaan-2, and Danuri each pass overhead 12 times a day.
  4. From the Moon a signal takes 1.28 s to reach the Earth. From Jezero crater on Mars, Perseverance waits 13 min 20 s, with MRO, Mars Odyssey, and the Trace Gas Orbiter among its relays.
site
Apollo 11
one-way light time
–
relay passes, 24 h
–

The giant planets · every known moon, on JPL’s trajectories

115 moons around Jupiter,
290 around Saturn.

Past Mars, VSOP87 places Mercury, Venus, and the giant planets within a few arcseconds of JPL Horizons, each with every known moon. The fast ones follow trajectories sampled from Horizons. Each of the hundreds of slow irregular moons gets one set of osculating elements instead, and two-body motion keeps it within 0.1% of Horizons over the snapshot’s window.

  1. Jupiter, from Hubble’s 2025 map, with Io, Europa, Ganymede, and Callisto close in, and Juno looping over its poles every 33 days.
  2. Pull back and the other moons appear, mostly small captured irregulars on wide, tilted orbits up to 25 million km out. 92 of the 115 orbit backward.
  3. Saturn’s rings are built from their measured edges and optical depths, the Cassini Division a dark gap between the dense B ring and the A ring.
  4. In October 2026 they open only 7° toward the Earth, a year and a half after they turned edge-on in 2025.
  5. Its 290 moons reach 28 million km out, Titan the largest among them.
world
Jupiter
moons
–
light time from Earth
–

The Oort cloud · the solar system from outside

From a satellite’s hull
to 100,000 au.

One scene spans 10¹⁴ km, from a followed spacecraft’s hull to the far side of the Oort cloud. Anything beyond 700,000 km is pulled toward the camera along its line of sight, which keeps its place on screen and its depth order, so even the cloud’s far side lands inside the view. Each part of the outer solar system then fades in at the scale where it means something.

  1. The planets’ orbits, from Mercury out to Neptune at 30 au, four light-hours from the Sun.
  2. The Kuiper belt is real data. All 7,299 known trans-Neptunian objects sit on their own orbits, from JPL’s Small-Body Database.
  3. The Voyagers fly their actual paths since 1977, bent by the giants. Voyager 1 is now 172 au out and Voyager 2 is 144 au out.
  4. Both have crossed the heliopause, where the Sun’s wind gives way to interstellar space, Voyager 1 at 122 au in 2012 and Voyager 2 at 119 au in 2018.
  5. Beyond lies the Oort cloud, a model rather than data. 50,000 points follow the usual picture, from 2,000 au out to 100,000 au, a third of the way to the nearest star.
camera from the Sun
–
light time
–
dots drawn
–

Validation · Vallado et al., AIAA 2006-6753

Written from the paper,
checked to 0.1 mm.

SGP4 dates from 1980, and decades of fixes live only in reference code, so reimplementations quietly drift from it. I wrote mine in Rust from the model as Vallado, Crawford, Hujsak, and Kelso define it in Revisiting Spacetrack Report #3, deep-space resonance and every sgp4fix included, then held it to the paper’s published verification output. To check it on real orbits too, I compared it with satellite.js across the live catalog.

  1. Each dot is one of the 666 reference states, placed by how far my position lands from the published one.
  2. 92% are closer than the reference file’s own print rounding, 0.01 mm. The worst, 0.10 mm, is a deep-space orbit 1,280 days past its epoch.
  3. Seven objects are built to fail by decaying or escaping. Mine fail at the same step with the same error codes.
  4. Against satellite.js on 2,507 catalog objects, 89.7% of 17,549 states agree within a micrometer. Each dot here is five states.
  5. The reference computes angles it only needs the sine and cosine of. Exact identities instead made propagation 25% faster, for a median difference of 2 nm, a billionth of a TLE’s own error.
reference states matched
666 / 666
worst error
0.097 mm
within 0.001 mm of satellite.js
89.7%

Architecture · SharedArrayBuffer · Atomics

SGP4 at 12 Hz,
frames at 60.

A frame gets 16.7 ms, and running SGP4 on the whole catalog takes 8.6 ms natively, about 6 ms a keyframe even split across four WebAssembly workers. So SGP4 never runs per frame. The workers write keyframes, every object’s position and velocity at one instant, into shared memory, and each frame interpolates between two of them. None of this state lives in React. A plain render-loop class owns the workers and the GPU, and React only reads the summary it publishes five times a second.

  1. The render loop asks for a keyframe by writing a Julian date into shared memory and calling Atomics.notify.
  2. Four workers wake from Atomics.wait, run SGP4 on their quarter of the catalog, and write into one of four keyframe slots. Nothing is posted or copied.
  3. Each frame, a cubic Hermite spline on position and velocity carries all 32,583 objects between two keyframes in 0.22 ms.
  4. Keyframes 60 s apart are off by 2.5 m at worst. When time runs an hour a second they are 300 s apart, off by 0.28 km, and SGP4 runs 12 times a second.
  5. Five times a second the loop publishes a summary to a Zustand store, and only the panels that read it re-render.
propagate.worker.ts · one of four
for (;;) {
  Atomics.wait(ctrl, REQ_SEQ, seen)             // sleep until asked
  seen = Atomics.load(ctrl, REQ_SEQ)
  const slot = Atomics.load(ctrl, REQ_SLOT)
  prop.propagate(reqJd[0])                      // SGP4, Rust → WASM
  pos.set(wasmPositions, slot * 3 * n + 3 * start)
  Atomics.add(ctrl, DONE + slot, 1)             // no postMessage
}
JavaScript per frame
2.6 ms
moving every object
0.22 ms
messages per keyframe
0

Precision · camera-relative rendering

The jitter wasn’t
in the physics.

Following a satellite from a few dozen meters, its model shook. The positions were fine, because JavaScript numbers are already 64-bit. The GPU’s are 32-bit, and 7,000 km from Earth’s center a float32 can only step in half meters. So every layer is placed at the camera, and each object stores its position minus the camera’s, subtracted on the CPU in float64. Near the camera those offsets are small, and float32 holds them to a few micrometers.

  1. The camera rides 40 m from a Starlink V2 Mini. Both are 6,842 km from Earth’s center.
  2. In float32, coordinates that large come in steps of 0.49 m. Every vertex snaps to its own step each frame, and the model shakes.
  3. Subtracting the camera on the CPU, in float64, leaves offsets of tens of meters, which float32 holds to a few micrometers.
  4. The cost is re-uploading the satellites’ offsets every frame, 0.38 MB for the whole catalog, measured.
sprites.ts · every instanced layer
// the layer sits at the camera
commit(count, camera) {
  this.object.position.copy(camera)
}
setOffset(i, x, y, z, cam) {                    // float64 in
  o[3 * i] = x - cam.x                          // float32 out, now small
  o[3 * i + 1] = y - cam.y
}
float32 step at 6,842 km
0.49 m
worst vertex error
–
uploaded per frame
–

Spatial grid · conjunction screening

Half a billion pairs,
sieved in 1.5 ms.

Screening for close approaches asks, at every time step, which of 530 million pairs of objects are near each other. Checking them all takes 638 ms a step, and a day of screening at 60 s steps has 1,441 of them. A spatial grid sorts objects into cubic cells. A pair closer than one cell must share a cell or sit in neighboring ones, so each object only looks around itself and finds exactly the same pairs.

  1. Low Earth orbit from the sample, seen from above the North Pole, at two minutes a second.
  2. Checking all pairs tests every object against every other, like this one against all the rest.
  3. A spatial grid buckets every object by its cell, drawn here 485 km across, the size a 60 s screening step uses.
  4. Only the 26 cells around an object can hold a partner. Radix-sorted by cell, each column is one run of memory, and a cell need only check itself and the 13 neighbors ahead of it, so every pair is met exactly once.
  5. It finds the same pairs, pair for pair. Finding every pair within 10 km at one instant takes 1.5 ms instead of 638 ms, and at 100,000 objects the gap is 1,522×.
pairs checked a step
530.7 M
share of all pairs
100%

Sampling bound · the engine’s regression test

Nothing slips through
between samples.

Screening samples the catalog once a step, so two objects can pass close between samples and look far apart at both. A bound closes the gap. If a pair passes within d at closest approach, then at the nearest sample it is at most d + v·Δt/2 apart, where v is the speed at which they close. Search that far around every object and no encounter can be missed. A single 16 km/s bound once let a transfer orbit slip past a satellite at 16.8 km/s, and a test now pins a pass like it.

  1. A satellite in low orbit and a rocket body on a transfer orbit meet head-on at 18 km/s, closest midway between two samples 120 s apart.
  2. At the nearest sample a pair is at most d + v·Δt/2 apart, so a search radius that size can’t miss it.
  3. One radius once served every pair, sized for 16 km/s, twice the top speed of 95% of the catalog. At 18 km/s both samples sit outside it.
  4. Now the 1,741 objects that can pass 8 km/s each search farther, by their own top speed plus the fastest object’s, and the pass is caught.
  5. Newton’s method on full SGP4 states then finds the closest approach. Over 24 hours, 10, 20, 60, and 120 s steps find the same 60,756 encounters.
closing speed
18 km/s
search radius
–
at the nearest sample
–

What I learned

Lessons I’ll keep.

  1. 01

    Check against the reference, not yourself

    SGP4 has a published verification set. Matching it to a tenth of a millimeter, error codes included, is what made every later number worth trusting. An independent Rust implementation then agreed to 0.7 µm.

    666 / 666reference states matched
  2. 02

    Exact to the model, not to the bit

    Bit-for-bit agreement with satellite.js held only while I kept the reference code’s slow steps. Exact identities in their place made propagation a quarter faster and moved positions by a billionth of a TLE’s own error, so the model is what to match, not its rounding.

    2 nmmedian difference from satellite.js
  3. 03

    Keep the simulation out of React

    The render loop is a plain class that owns the workers and the GPU. React reads a summary of it five times a second, so a busy panel can never cost the globe a frame.

    0.22 msto move all 32,583 objects a frame
  4. 04

    Prove a bound, then test the case that broke it

    A single speed bound once let a transfer orbit slip past a satellite between samples. Sizing each object’s search from its own top speed closed the gap, and a test now pins that pass.

    60,756encounters at every step size
GitHubLinkedIn