Rebuild any Formula 1 race as an interactive 3D reconstruction from real telemetry — in a browser, on an ordinary laptop.
▶ Live demo · eight 2024 races, no sign-in, nothing to install.
- Overview
- Key features
- Architecture
- How it works
- Tech stack
- Prerequisites
- Getting started
- Commands
- Controls
- Testing
- Deploying
- Project structure
- Data sources
Point SLIPSTREAM at an OpenF1 session key and it downloads the race, derives the circuit geometry, reconstructs every car's path from raw GPS, and hands you a scrubbable 3D replay with a broadcast-style timing tower.
Eight 2024 races ship with the repo, so it plays the moment you clone it — Las Vegas, São Paulo, Monaco, Spa, Monza, Suzuka, Singapore and Silverstone. Street circuits and road courses, dry and wet, from 5 m of elevation change to 102 m. Every one was built by the same pipeline with zero per-circuit hand-tuning.
It is not a video player. Every position, gap, tyre age and speed on screen is computed from telemetry at the current clock value, which is what makes the analysis lab and the AI race engineer possible: they read the same replay engine the 3D scene renders from.
Every moment of the race is detected and scored — overtakes judged on-track versus pit-cycle, wheel-to-wheel battles, retirements, safety cars, the start and the finish. The camera cuts to whatever matters most right now, with a lower-third caption and clickable story markers on the timeline.
Four views, all drawn as plain SVG with no charting library:
| Race evolution | gap to leader by lap, selected drivers emphasised over a grey context field |
| Lap times | per-lap traces with tyre compound encoded on each point |
| Fastest lap telemetry | speed, throttle, brake and the running time delta between two drivers, plotted against lap distance |
| Tyre strategy | stints with a fitted degradation slope in seconds per lap |
Ask "why did Hamilton lose the lead?" and Claude answers by querying the replay engine. The serverless route pins the system prompt and tool schemas; the browser executes those tools against the same objects the UI renders from, so every number in the answer is computed, never recalled.
Needs ANTHROPIC_API_KEY — everything else works without it. The route is
same-origin only and rate limited per caller, so a deployed key cannot be
drained by whoever finds the endpoint.
npm run race:build -- 9644Track geometry comes from MultiViewer's geometric centreline, and kerbs are placed from curvature, so nothing is authored per circuit. Races whose geometry MultiViewer does not publish fall back to a derived centreline rather than failing to load.
Sector timing works the way it does on television: every driver's three most recent sector times are graded against the session and against their own best, and coloured purple, green or yellow accordingly — resolved at the current clock, so rewinding shows who held purple at that moment, not who ended up with it.
A live event feed (overtakes, battles, pit stops, retirements, safety cars, fastest laps) with playable team-radio clips, green/red position flashes in the timing tower, a full driver card — headshot, speed in km/h and mph, DRS, tyres, sector splits, pedal traces, follow-cam — and corner-numbered map info.
Every view is a link. ?race=2024-las-vegas&t=3120&follow=44&cam=chase opens the
replay at 52:00 on lap 32, chasing Hamilton. The Share button writes whatever
you are looking at into the address bar and copies it.
The ingest scripts run once per race on your machine and commit a binary bundle.
Everything after that is the browser: a worker decodes the bundle into typed
arrays, prepareRace builds the track model, and a single ReplayEngine
becomes the source every panel reads from.
The reference implementations of this idea ship race data as JSON assigned to a global — around 18 MB per race, which blocks the main thread for seconds while the parser runs.
Here a race is columnar binary. Each positional sample is 8 bytes: a uint16 millisecond delta and three int16 coordinates, quantised against a per-race transform so the format stays exact whatever coordinate range a circuit uses. Decoding is a pass of typed-array views inside a worker.
| reference | SLIPSTREAM | |
|---|---|---|
| bytes per race | ~18 MB | 7.35 MB |
| needed before playback | all of it | 4.3 MB (car telemetry streams after) |
| parse cost | multi-second JSON.parse on the main thread |
typed-array views in a worker |
| round-trip error | — | 1.2 cm median, 2.1 cm max |
OpenF1's z channel is altitude above sea level in decimetres — Las Vegas reads
a median of 606.3 m, which is the city's actual elevation. So the vertical
profile does not need to be invented, only extracted: every driver's samples are
binned to the nearest centreline index and the per-bin median is taken, which
rejects GPS spikes and cars airborne over kerbs.
Nothing about this is tuned per circuit, and the numbers that fall out match the real world — which is the best evidence the extraction is honest:
| Circuit | Relief recovered | |
|---|---|---|
| Spa-Francorchamps | 101.9 m | Eau Rouge into Raidillon |
| Monte Carlo | 41.4 m | the climb to Casino Square |
| Suzuka | 40.0 m | the Esses |
| Las Vegas | 16.1 m | flat street circuit |
| Monza | 12.4 m | famously flat |
| Singapore | 5.1 m | sea-level street circuit |
Raw GPS arrives at ~3.5 Hz and is noisy. Two decisions matter:
One continuous spline per driver, not one per lap. Per-lap splines meet with mismatched tangents, which shows up as a heading flick every time a car crosses the start/finish line. A single time-uniform Catmull-Rom spanning the whole race is C1 everywhere. Because control points are uniform in time, evaluating at a clock value is a division and a cubic — no search, no per-frame projection, no allocation.
Resample with a Hermite, not a lerp. Linearly interpolating raw samples onto the control grid cuts every corner: a 10 m chord on a 500 m radius sits 2.5 cm inside the true arc, and no amount of downstream splining recovers a control point that is already in the wrong place.
| Layer | Choice | Why |
|---|---|---|
| Language | TypeScript, strict + noUncheckedIndexedAccess |
the whole project is index arithmetic over typed arrays |
| UI | React 19 | panels are the only stateful surface; the render loop bypasses it |
| 3D | three.js r180 | WebGL without writing a renderer |
| Build | Vite 7 | worker bundling and route splitting out of the box |
| Ingest | Node + tsx | scripts share the format module with the browser |
| AI | Anthropic SDK, tool use | tools execute client-side against live objects |
| Unit tests | Vitest | 131 tests over the engine, geometry, sectors, moments and tools |
| Browser tests | Playwright | asserts the scene actually renders |
| Hosting | Vercel | static bundle plus one Node function |
No state library, no chart library, no UI kit, no CSS framework.
| Node.js | 22 or newer |
| npm | 10 or newer |
| A WebGL2 browser | Chrome, Edge, Firefox or Safari 16+ |
ANTHROPIC_API_KEY |
optional — only the AI race engineer needs one |
Building a new race also needs network access to the OpenF1 and MultiViewer public APIs. Neither requires a key.
1. Clone and install
git clone https://github.com/lumixed/slipstream.gitcd slipstream && npm install2. Run it
npm run devOpen http://localhost:5173. Two races are already bundled, so there is nothing else to do — pick one and press space.
3. Optional — enable the AI race engineer
echo "ANTHROPIC_API_KEY=sk-ant-..." > .envRestart the dev server. Without this the ENGINEER panel shows a clear "not configured" message and everything else is unaffected.
4. Optional — build another race
npm run race:list -- 2024npm run race:build -- 9598Responses are cached to .cache/, so re-running the build costs zero network
requests. Any race from 2018 onward works. New bundles land in public/races/
and are gitignored unless you add them to the allowlist in .gitignore.
npm run dev |
dev server |
npm run build |
typecheck + production build |
npm run preview |
serve the production build |
npm test |
unit tests |
npm run test:e2e |
browser smoke tests |
npm run lint |
eslint |
npm run typecheck |
tsc, no emit |
npm run race:list -- <year> [filter] |
browse OpenF1 race sessions |
npm run race:build -- <sessionKey> |
build a race bundle |
npm run race:build -- <year> "<circuit>" |
…or resolve by name |
npx tsx scripts/ingest/verify-bundle.ts <key> |
verify a built bundle |
space |
play / pause |
← → |
seek 5 s (shift for 30 s) |
c |
cycle camera: free → chase → broadcast |
d |
auto race director |
a |
analysis lab |
e |
AI race engineer |
l |
compare racing lines |
m |
map info + corner numbers |
esc |
close the top panel, then deselect |
| double-click | re-frame the circuit |
| drag / scroll | orbit / zoom |
| click a car or tower row | follow that driver |
npm test && npm run test:e2e131 unit tests cover the replay engine's state resolution, the geometry pipeline, sector grading, moment detection, lap traces, the engineer tools and the API route's validation and rate limiting.
6 Playwright specs boot the app against a bundled race and assert the scene renders twenty cars, deep links restore, the analysis lab draws, Escape closes panels and focus returns to the trigger.
verify-bundle is a third layer, checking a built race for structural
consistency, per-driver distinctness, round-trip accuracy against the cached raw
JSON, that samples land on the circuit, elevation relief and clock monotonicity.
It caught a real bug during development: delta-encoded records dropped each
driver's absolute start time, silently shifting every trace to begin at t=0.
The repo ships eight race bundles, so a fresh deploy has something to play the
moment it boots. Vercel detects the Vite build and the api/ route with no extra
configuration.
| Environment variable | |
|---|---|
ANTHROPIC_API_KEY |
optional — only the ENGINEER panel needs it |
ALLOWED_ORIGINS |
optional — comma-separated; defaults to same-origin only |
Two notes. The engineer route runs on the Node runtime, not edge — the
Anthropic SDK reaches for node:fs and node:path on its credential paths,
which an edge bundle cannot resolve. And vite preview does not serve api/, so
vercel dev is the only way to exercise that route locally.
og:image in index.html is absolute, pointing at the deployed domain — change
it if you deploy somewhere else, because link previews will not resolve a
relative path.
The route is written as a web handler and adapts itself when the Node runtime
calls it with (req, res) instead. That adapter is not cosmetic: without it a
GET hangs until the function times out and a POST returns
FUNCTION_INVOCATION_FAILED, which is exactly how it first shipped.
api/ serverless route for the AI race engineer
e2e/ Playwright browser tests
scripts/ingest/ OpenF1 + MultiViewer download, packing and verification
shared/ the bundle format, shared by Node and the browser
src/
analysis/ lap and stint models, fastest-lap telemetry traces
audio/ synthesised engine note
data/ bundle loading, race preparation, deep links
engine/ replay engine, splines, moment detection
engineer/ client-side execution of Claude's tool calls
lib/ track geometry, resampling, spatial index
scene/ three.js scene, cameras, overlays
ui/ broadcast layer and the analysis lab
workers/ off-thread binary decode
tests/ unit tests
- OpenF1 — timing, telemetry, race control. Free, keyless, historical (available ~30 min after a session).
- MultiViewer — circuit centrelines and corner markers, in the same coordinate space as OpenF1 positions.
Not affiliated with Formula 1. F1 and related marks belong to Formula One Licensing BV. Released under the MIT licence.





