The track map on the home page — the one you drive around by scrolling — runs on a generated dataset. I want to walk through how that dataset gets made, partly because it's a fun little pipeline, and partly because if you put telemetry-looking data on a page, you should be able to say exactly where it came from.
Starting with the real track
The circuit is the Sonoma Raceway Karting Center. The outline comes from OpenStreetMap (ways 230287123 / 127 / 129 / 131 / 133 / 139, ODbL — that's why the footer credits OSM contributors wherever the map shows).
Here's the first wrinkle: OSM doesn't store the kart track as one neat loop. The facility can be run in different configurations, so it's mapped as a network — ten junction nodes connected by fifteen arcs. "The track" is really a path you have to compose out of arcs. I picked the composition that matches the National Circuit layout and confirmed it against the real thing: 1,203.8 m once splined, run clockwise, T1 at the northernmost corner, start/finish on the straight leading into it. All eleven corner labels are verified, which is why they get to render as callouts on the map.
From outline to geometry
scripts/build-lap-data.mjs turns that outline into the dataset. It's
deterministic — same input, byte-identical output — and dependency-free. The
steps:
- Compose the chosen arcs into one ordered centerline.
- Project the GPS coordinates into local meters. Plain equirectangular projection is fine at track scale, with y flipped because SVG's y-axis points down.
- Fit a centripetal Catmull-Rom spline through the loop. The centripetal variant matters: it smooths the raw polyline into a track-shaped curve without the kinks and overshoot a naive fit can produce.
- Resample along arc length every 5 m — 242 samples for this lap. Even spacing makes everything downstream simple: lookups become arithmetic.
- Estimate curvature at each sample by fitting a circle through it and its two neighbors (a three-point circumcircle), lightly smoothed.
Curvature is the star of the show. It's the one number that says how tight the track is at any point, and that's what decides how fast a car can be there. The corners fall out of it too — they're the local curvature maxima. I assigned the T1–T11 numbering by hand and verified it against the confirmed layout.
Making up honest speed data
Now the honest part: the speed and lateral-g traces on the site are generated, not measured. These aren't logged laps. But rather than draw a plausible-looking squiggle, I generate the channels from the track geometry using a model whose assumptions are fully disclosed — a point-mass quasi-steady-state pass:
- In a corner, speed is capped by grip:
v = min(vmax, sqrt(aLat / |k|)). Tighter corner, bigger curvaturek, lower cap. - A forward pass limits how quickly speed can build out of corners (the acceleration limit).
- A backward pass limits how quickly it can shed into them (the braking limit). Running that pass backward is the trick that puts braking zones before corners, where they belong.
- Iterate until nothing changes.
The parameters sit at the top of the script: 105 km/h top speed, 1.9 g lateral, 1.6 g braking, 0.55 g acceleration — roughly kart-shaped numbers. And every place the data renders, it's labeled demo. That's enforced by the test suite, not left to good intentions.
The swap contract
One nice property fell out of building it this way: scripts/import-aim-csv.mjs
can take an AiM RaceStudio CSV export — distance, GPS position, GPS speed,
lateral g — and run it through the same 5 m resample, regenerating the same
file with source: "aim-racestudio" and a real session label. It's proven
against a synthetic fixture in the test flow: the site builds and renders a
swapped dataset with zero component changes. So if I ever want the map running
on real logged laps instead of a model, it's a one-file change.
One last architectural note: everything downstream of the dataset is
hand-authored. The content stations, and the timing sectors derived from
them, live in lap-course.ts — so the sectors follow where the content sits
on the page, not the geometry. The map is scenery that refuses to lie about
its data, and that's the whole trick.