"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { ArrowUpRight, ChevronLeft, ChevronRight, CloudRain, FlagTriangleRight, Pause, Play, RotateCcw } from "lucide-react"; import rawData from "~/lib/race-replay-data.json"; type Driver = { id: string; number: number; code: string; name: string; team: string; color: string; grid: number; finish: number; status: string; }; type Timing = { id: string; position: number; seconds: number }; type RaceData = { race: { title: string; year: number; date: string; circuit: string; location: string; laps: number }; drivers: Driver[]; laps: { number: number; timings: Timing[] }[]; pitStops: { id: string; lap: number; seconds: number }[]; events: { lap: number; kind: string; title: string; description: string }[]; track: [number, number][]; corners: { number: number; x: number; y: number }[]; drsZones: { number: number; track: [number, number][] }[]; sources: Record; }; const data = rawData as unknown as RaceData; const driverById = new Map(data.drivers.map((driver) => [driver.id, driver])); const trackSegments = data.track.slice(1).map((point, index) => Math.hypot(point[0] - data.track[index]![0], point[1] - data.track[index]![1])); const trackDistances = [0]; for (const segment of trackSegments) trackDistances.push(trackDistances.at(-1)! + segment); const trackLength = trackDistances.at(-1)!; const trackPath = data.track.map(([x, y], index) => `${index ? "L" : "M"}${x} ${y}`).join(" "); const drsPaths = data.drsZones.map((zone) => ({ number: zone.number, path: zone.track.map(([x, y], index) => `${index ? "L" : "M"}${x} ${y}`).join(" "), middle: zone.track[Math.floor(zone.track.length / 2)]!, })); const elapsedByDriver = new Map(); for (const driver of data.drivers) elapsedByDriver.set(driver.id, [0]); for (const lap of data.laps) { for (const timing of lap.timings) { const elapsed = elapsedByDriver.get(timing.id)!; elapsed.push(Math.round((elapsed.at(-1)! + timing.seconds) * 1000) / 1000); } } const leaderCrossings = [0]; for (let lap = 1; lap <= data.race.laps; lap++) { leaderCrossings.push(Math.min(...[...elapsedByDriver.values()].map((times) => times[lap] ?? Infinity))); } function pointOnTrack(progress: number): [number, number] { const distance = (((progress % 1) + 1) % 1) * trackLength; let low = 0; let high = trackSegments.length - 1; while (low < high) { const middle = (low + high) >> 1; if (trackDistances[middle + 1]! < distance) low = middle + 1; else high = middle; } const fraction = (distance - trackDistances[low]!) / (trackSegments[low] ?? 1); const start = data.track[low]!; const end = data.track[low + 1]!; return [start[0] + (end[0] - start[0]) * fraction, start[1] + (end[1] - start[1]) * fraction]; } function formatTime(seconds: number) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return `${hours ? `${hours}:` : ""}${String(minutes).padStart(hours ? 2 : 1, "0")}:${String(secs).padStart(2, "0")}`; } function formatLapTime(seconds: number) { return `${Math.floor(seconds / 60)}:${(seconds % 60).toFixed(3).padStart(6, "0")}`; } function inactiveLabel(driver: Driver) { if (driver.status === "Did not start") return "DNS"; if (driver.status === "Disqualified") return "DSQ"; return "OUT"; } function positionAtLap(driverId: string, lap: number) { if (lap === 0) { const driver = driverById.get(driverId); return driver?.status === "Did not start" ? null : driver?.grid ?? null; } return data.laps[lap - 1]?.timings.find((timing) => timing.id === driverId)?.position ?? null; } function carProgress(driverId: string, seconds: number, grid: number) { const times = elapsedByDriver.get(driverId)!; if (seconds > times.at(-1)! + 2) return null; let low = 0; let high = times.length - 1; while (low < high) { const middle = Math.ceil((low + high) / 2); if (times[middle]! <= seconds) low = middle; else high = middle - 1; } if (low >= times.length - 1) return seconds >= times.at(-1)! ? 0 : null; const fraction = (seconds - times[low]!) / (times[low + 1]! - times[low]!); const gridOffset = Math.max(0, 1 - seconds / 12) * grid * 0.008; return fraction - gridOffset; } function PositionChart({ driver }: { driver: Driver }) { const positions = [driver.grid, ...data.laps.map((lap) => lap.timings.find((timing) => timing.id === driver.id)?.position).filter((position): position is number => position !== undefined)]; if (positions.length === 1) return

No race laps recorded.

; const points = positions.map((position, index) => `${(index / Math.max(positions.length - 1, 1)) * 284 + 8},${((position - 1) / 19) * 73 + 8}`).join(" "); return
P1P20
STARTFINISH
; } export function RaceReplay() { const [progress, setProgress] = useState(0); const [playing, setPlaying] = useState(false); const [speed, setSpeed] = useState(1); const [selectedId, setSelectedId] = useState("max_verstappen"); const [showAllCars, setShowAllCars] = useState(true); const progressRef = useRef(0); useEffect(() => { if (!playing) return; let frame = 0; let last = 0; function tick(now: number) { if (last && now - last >= 35) { const elapsed = Math.min(now - last, 100); progressRef.current = Math.min(data.race.laps, progressRef.current + elapsed / 1000 * 0.8 * speed); setProgress(progressRef.current); if (progressRef.current >= data.race.laps) setPlaying(false); last = now; } else if (!last) last = now; frame = requestAnimationFrame(tick); } frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); }, [playing, speed]); const completedLap = Math.min(data.race.laps, Math.floor(progress)); const displayLap = Math.min(data.race.laps, completedLap + (progress < data.race.laps ? 1 : 0)); const lapFraction = progress - completedLap; const elapsed = completedLap === data.race.laps ? leaderCrossings.at(-1)! : leaderCrossings[completedLap]! + (leaderCrossings[completedLap + 1]! - leaderCrossings[completedLap]!) * lapFraction; const selected = driverById.get(selectedId)!; const selectedStatus = inactiveLabel(selected); const selectedPosition = positionAtLap(selectedId, completedLap); const activeEvent = [...data.events].reverse().find((event) => event.lap <= displayLap); const lapTimings = completedLap ? data.laps[completedLap - 1]!.timings : []; const timingById = new Map(lapTimings.map((timing) => [timing.id, timing])); const orderedDrivers = [...data.drivers].sort((a, b) => { if (!completedLap && a.status === "Did not start") return b.status === "Did not start" ? a.grid - b.grid : 1; if (!completedLap && b.status === "Did not start") return -1; const aPosition = timingById.get(a.id)?.position; const bPosition = timingById.get(b.id)?.position; if (aPosition !== undefined && bPosition !== undefined) return aPosition - bPosition; if (aPosition !== undefined) return -1; if (bPosition !== undefined) return 1; return completedLap ? a.finish - b.finish : a.grid - b.grid; }); const visibleCars = useMemo(() => data.drivers.filter((driver) => driver.status !== "Did not start" && (showAllCars || driver.id === selectedId)), [showAllCars, selectedId]); const selectedPits = data.pitStops.filter((pit) => pit.id === selectedId); const validLaps = data.laps.flatMap((lap) => lap.timings.filter((timing) => timing.id === selectedId && timing.seconds < 200).map((timing) => timing.seconds)); const bestLap = validLaps.length ? Math.min(...validLaps) : null; function jumpTo(lap: number) { setPlaying(false); progressRef.current = Math.max(0, Math.min(data.race.laps, lap)); setProgress(progressRef.current); } return
RACE REPLAY / 2024 ARCHIVE / ROUND 21

São Paulo ’24.

One very wet Sunday at Interlagos. Scrub the race, follow a driver, and watch the order change lap by lap.

INTERLAGOS, BRAZIL WET RACE03 NOV 2024
THE COMEBACKP17 P1

Max Verstappen · Red Bull Racing

01 / CIRCUIT VIEW

Autódromo José Carlos Pace

4.309 KM · ANTI-CLOCKWISE
INTERLAGOS / SÃO PAULO24° 42′ S   46° 41′ W
{drsPaths.map((zone) => DRS {zone.number} )} {data.corners.filter((corner) => [1, 3, 4, 6, 8, 12, 13, 15].includes(corner.number)).map((corner) => T{corner.number})} {visibleCars.map((driver) => { const onTrack = carProgress(driver.id, elapsed, driver.grid); if (onTrack === null) return null; const [x, y] = pointOnTrack(onTrack); const focused = driver.id === selectedId; return {focused && } {focused && {driver.code}} ; })} START / FINISH
SELECTED DRIVER DRS ZONES · DISABLED IN WET RACE
{activeEvent &&
LAP {String(activeEvent.lap).padStart(2, "0")}{activeEvent.title}
}
RACE CLOCK{formatTime(elapsed)}
CURRENT LAP{String(displayLap).padStart(2, "0")} / {data.race.laps}
{ setPlaying(false); progressRef.current = Number(event.target.value); setProgress(progressRef.current); }} aria-label="Replay lap" style={{ "--replay-progress": `${progress / data.race.laps * 100}%` } as React.CSSProperties} />
{[1, 2, 4].map((option) => )}

Cars are interpolated from recorded lap times on an OpenF1 circuit trace. Order updates at each timing line; dots are not live GPS.

03 / RACE CONTROL

The moments that moved it.

SELECT A MOMENT TO JUMP
{data.events.map((event) => )}
RACE REPLAY / AN INDEPENDENT EXPERIMENT BY SEAN O’CONNOR
; }