There is a Formula-style car on this site now. You can pick its colour, take it apart on a workbench, and drive it around Brands Hatch Indy.
Open the playground, choose a colour and select Start driving. The track has a lap timer and a ghost of your best valid lap. A translucent version of your previous attempt is quite enough competition for a small browser game.
This is a build note about the current implementation. There are three parts worth looking at: keeping the game separate from the rest of the site, deciding what counts as a lap, and being honest about the circuit data.
A game inside a small website
The site serves static pages through Astro. The Formula page loads a Three.js runtime for the scene and uses GSAP for transitions between the workbench, tunnel and driving views.
Continuous movement belongs to a single animation loop. Steering, wheel rotation and the following camera update as the car moves. Moving between the workbench and track is a different kind of job: a sequence with a beginning and an end. That belongs to a transition timeline.
Keeping those jobs separate gives each part a clear owner. The driving code moves the car; it does not decide which screen should be visible. The state machine controls which transitions are allowed.
The heavier runtime sits behind the Formula page’s dynamic import. Someone reading an old blog post does not need to download the car game.
Crossing the line is only part of a lap
A timer could start at one line crossing and stop at the next. That would also leave it willing to congratulate someone who drove a shortcut.
The time-trial model checks a sequence of gates around the circuit. A valid attempt has to cross them in order and stay within the permitted track tolerance. Missing checkpoints, sustained backwards travel or leaving the allowed corridor invalidate the attempt.
The next finish-line crossing can still start another lap. A failed attempt should not require reloading the page.
Driving and timing use the same elapsed simulation time. The important line in the runtime is small:
const simulationDelta = this.paused ? 0 : Math.min(delta, 0.05);
That caps a single frame’s contribution at 0.05 seconds and contributes zero while paused. The same value advances the movement and the time trial. This is a timer for simulated driving time, not a wall clock that keeps counting while the tab is hidden or the browser stalls.
The limitation belongs in the explanation. A time on this playground is a result inside this model; it is not a measurement to compare with a real racing lap.
Race a saved attempt
The ghost is a recording of positions and headings. A valid attempt records samples at ten per second, with exact start and finish samples too. Playback fills in the movement between samples.
A new personal best replaces the saved recording. The browser keeps that best time and ghost in local storage. Clearing the site’s stored data can remove them, and opening the page on another device does not bring them along.
There is no account or shared leaderboard in this implementation. It has enough information to let you chase your own previous lap.
A real place with approximate pieces
The circuit uses an OpenStreetMap centreline and Environment Agency elevation data. Those give the scene a connection to the shape and terrain of Brands Hatch Indy.
They do not supply a finished racing simulator. The road uses a constant width, the kerbs have explicitly chosen dimensions, and the mapped barriers have gaps and varying source ages. The car stays grounded through a simplified contact model. It does not calculate a full suspension or tyre-load model.
The visible and drivable surfaces need to agree even when they are approximate. A kerb should not appear in one place while the car reacts to a different surface underneath it. The road and contact code share the height calculation for that reason.
The track view carries the source credits: OpenStreetMap contributors for the map data, and the Environment Agency for the terrain. The imported car model and engine recording also have their own provenance; they are separate assets from the code around them.
A short way to inspect it
After starting a lap, try the Pause control and resume. The attempt should continue from its paused simulation time. Restart gives you a fresh attempt while retaining the saved personal best. Once you have a valid best lap, the ghost gives you something to follow on the next one.
That is a useful scale for this project: a car, a circuit, and another attempt. There is a much older Lego Raspberry Pi case in the archive if you prefer your small projects without a lap timer.