Skip to content

Repository files navigation

SLIPSTREAM

ci typescript three.js licence

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.

SLIPSTREAM replaying the 2024 Las Vegas Grand Prix


Table of contents


Overview

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.

The broadcast layer during the Las Vegas Grand Prix


Key features

Auto race director

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.

Analysis lab

The analysis lab

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

AI race engineer

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.

Any circuit, one command

npm run race:build -- 9644

Track 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.

Circuit map with corner numbers and sector overlays

Broadcast layer

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.

Shareable moments

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.


Architecture

Architecture diagram

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.


How it works

The data format

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

Elevation is real

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

Smooth motion

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.


Tech stack

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.


Prerequisites

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.


Getting started

1. Clone and install

git clone https://github.com/lumixed/slipstream.git
cd slipstream && npm install

2. Run it

npm run dev

Open 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-..." > .env

Restart 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 -- 2024
npm run race:build -- 9598

Responses 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.

The race library


Commands

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

Controls

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

Testing

npm test && npm run test:e2e

131 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.


Deploying

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.


Project structure

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

Data sources

  • 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.

About

Interactive 3D Formula 1 race reconstruction from real telemetry, with an automated race director and a grounded AI race engineer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages