Click anywhere on the page and watch the cars race to your cursor. Each car uses a realistic rear-axle bicycle model — it steers, accelerates, brakes, drifts, leaves skidmarks, and backfires. Press Ctrl+Shift+Space to toggle the debug overlay.
Open the browser console and try:
driver.driveTo(400, 300) — moves all cars
driver.cars[0].driveTo(200, 200) — moves only car 0
driver.addCar({ color: '#ff0', maxSpeed: 600 }) — adds a car
driver.removeCar(driver.cars[0]) — removes a car
driver.debug = true — toggle debug overlay
driver.skidOpacity = 0.15 — adjust skidmark darkness
driver.shadow = false — disable shadows
Pass any of these to driver.addCar({ … }):
// Physics
maxSpeed: 320 — px/s (±20% random variation per car)
acceleration: 220 — px/s²
brakes: 0.5 — 0 = poor (~60 px/s²), 0.5 = default (~480 px/s²), 1 = ABS (~900 px/s²)
wheelbase: 32 — px; also scales visual body length
maxSteering: 35 — ± degrees from centre (lock-to-lock = maxSteering × 2)
steeringRate: 120 — degrees/s the wheel can turn
twitchiness: 0.4 — 0 = super stable (rate drops to zero at top speed), 1 = very twitchy (full rate always)
arrivalRadius: 144 — px; brake-to-stop zone around target
skidThreshold: 150 — px/s; speed above which braking produces a skid
grip: 1.0 — 0–1 tire grip; scales skidThreshold, slipStiffness, slipScale offset, and brakeDecel (min 30%)
slipStiffness: 34 — rear grip spring; ω_n = √k ≈ 5.8 rad/s
slipDamping: 3 — ζ ≈ 0.34; underdamped, visible overshoot on corner exit
slipScale: 1.0 — mark offset multiplier; increase to exaggerate the wiggle
driveBias: 1.0 — 0 = FWD, 1 = RWD, 0–1 = AWD (affects accel skidmarks)
aggression: 0.3 — 0 = careful (aims straight at target while braking), 1 = committed (follows bezier all the way in)
// Appearance
color: '#e63946' — any CSS color
height: 24 — body height in px
tireWidth: 4 — skidmark line width in px
sprite: null — URL string or HTMLImageElement
shadowCornerRadius: 4 — shadow rect corner radius in px (0 = square)
// Exhaust afterfire
exhaustPosition: null — 'left' | 'right' | 'bothSides' | 'rear' | null (null = disabled)
exhaustOffset: 0.5 — 0–1 along the chosen edge (0 = front/left corner, 1 = rear/right corner)
exhaustRadius: 6 — base flame radius in px; scales length and width proportionally
exhaustInterval: 0.9 — min seconds between events (actual = interval × 1–2×); 40% chance double pop, 15% triple
exhaustAngle: 90 — side exhausts only: 90 = perpendicular to car side, up to 170 = swept back toward tail
exhaustInset: 0 — px inboard from the car edge (side: toward centre; rear: tucked into bodywork)
// Behaviour flags
orbitDetection: true — detect and escape infinite-circle situations
proximityBoost: true — lead car gets a speed boost to pull away from a trailer
// Initial state
x, y — spawn position (default: random on canvas)
heading — initial heading in radians (default: random)
count: 3 — cars spawned on init
zIndex: 9999 — canvas z-index
clickTarget: document — element to bind clicks on; null to disable
debug: false — show debug overlay (Ctrl+Shift+Space toggles at runtime)
skidOpacity: 0.08 — global skidmark opacity multiplier
shadow: true
shadowOpacity: 0.40
shadowBlur: 4 — px
shadowOffsetX: 4 — page-space offset (not car-space)
shadowOffsetY: 6
Each car is a kinematic rear-axle bicycle model: the two rear wheels are collapsed into a single pivot point, and only the front axle steers. The heading changes at a rate determined by vehicle speed, wheelbase, and steering angle:
ω = (v / L) · tan(δ)
where v is speed in px/s, L is the wheelbase, and δ is the front steering angle (clamped to ±maxSteering°). Each frame the heading is updated by ω · dt, then the rear axle moves forward along the new heading. There is no lateral slip — the rear wheels always track exactly along the heading vector. The visual body length is derived as wheelbase × 1.5 unless overridden.
When a target is assigned, a cubic Bézier curve is generated from the car's current position to the destination. The two control points are offset perpendicular to the straight line by random amounts (up to 60% and 40% of the distance respectively), producing arcs and S-curves. The car tracks a lookahead point 8% ahead of its current progress along the curve, giving smooth anticipatory steering rather than reactive correction. The lookahead is speed-adaptive: 6% of the path at rest, rising to 20% at top speed, so fast cars anticipate direction changes earlier.
A car pointing away from its target would overshoot if it ran at full speed, so target speed is scaled by how well the car is aligned:
veff = vmax · clamp(floor + (1 − floor) · cos(θerr), floor, 1)
The floor is normally 0.3 (so even a fully sideways car keeps 30% speed to help it turn), but rises to 0.7 during an active collision so the car is never artificially slowed by a heading knocked out of alignment by a bump.
Three props set the speed envelope; grip then scales two of them:
brakeDecel = 60 + brakes × 840
effectiveBrakeDecel = brakeDecel × (0.3 + 0.7 × grip)
brakingDist = v² / (2 × effectiveBrakeDecel)
skidding = speed > skidThreshold × grip
maxSpeed is the per-car ceiling, with ±20% random variation at
construction. Alignment scaling reduces it proportionally to heading error
(floor 0.3), so a sideways car still keeps 30% speed to aid turning.
Proximity boost can push a lead car briefly above it.
acceleration is a constant px/s² ramp with no traction limit —
it runs at the same rate regardless of speed or steering angle.
brakes maps 0–1 onto a deceleration range:
0 ≈ 60 px/s² (barely slows),
0.5 ≈ 480 px/s² (default),
1 ≈ 900 px/s² (near-instant).
Use raw brakeDecel (px/s²) for precise control.
grip is a single 0–1 knob that touches four values simultaneously:
effective braking force, skid trigger threshold, rear slip spring stiffness,
and skidmark fan width. Lowering grip makes the car skid sooner, oversteer
more, leave wider marks, and stop in a longer distance.
| Scenario | What dominates |
|---|---|
| Car slides past target | brakes too low — increase it or reduce maxSpeed |
| Stops too abruptly, no character | brakes too high; or lower grip to soften effective decel |
| Skids at very low speed | grip too low — it multiplies skidThreshold down; raise grip or skidThreshold |
| No skids at all | maxSpeed below skidThreshold × grip; lower grip or skidThreshold |
| Dramatic rear oversteer / drift | Low grip weakens the slip spring; pair with high slipScale for wider marks |
| Car won't reach top speed | Alignment scaling is capping it — large heading error; reduce aggression |
| Acceleration feels sluggish | acceleration too low; or maxSpeed very high making the ramp long |
The stopping distance at any given speed is:
dstop = v² / (2 · effectiveBrakeDecel)
The car begins braking when its distance to the target falls inside dstop × 1.2 — the 20% margin absorbs the fact that the car is still turning while decelerating. Once inside the arrival radius (default 144 px, adjustable via the slider), the car brakes to a dead stop while avoidance steering remains active — cars nudge each other apart even while parking. Speed below 10 px/s clears the target.
A car near its target can sometimes end up circling indefinitely — particularly one with a wide turning radius arriving at an awkward angle. To detect this, cumulative angular displacement is tracked whenever the car is within 5 × width of the target:
Σ|ω · dt| > 2π → orbiting = true
Once flagged, the car brakes at 60% of normal deceleration until it stops, then clears its target. The accumulator resets whenever the car leaves the near-finish zone.
After every update tick, each overlapping pair of cars is hard-separated along the axis between their centres by exactly the overlap distance:
a.pos -= n̂ · overlap/2 b.pos += n̂ · overlap/2
A rotational impulse is also applied proportional to the cross product of the push direction and each car's forward axis — a side-on hit spins the car, a head-on hit doesn't. Moving cars receive 15% of the torque that a parked car would, keeping the fleet from spinning wildly mid-race.
When two cars are within 1.5 car-widths of each other, the one that is closer to its target (the lead car) receives a forward speed boost to pull it clear:
boost = urgency × (0.7 if colliding, else 0.3)
where urgency scales linearly from 0 at 1.5 widths to 1 at contact. The trailing car is never slowed — this is a push-forward, not a push-back. The higher multiplier during an active collision ensures the lead car escapes cleanly rather than being dragged along.
Turn skidmarks are driven by a rear slip angle α modelled as a 2nd-order damped oscillator:
α̈ = ω − k·α − c·α̇
where ω is the yaw rate from the bicycle model (the cornering
forcing), k is slipStiffness, and c is
slipDamping. At the defaults, the damping ratio
ζ = c / (2√k) ≈ 0.34 — clearly underdamped, so when the car
exits a corner the rear steps out and oscillates back through zero before settling.
That single overshoot is the snap-back wiggle you see in the turn
skidmarks. Higher ζ eliminates the overshoot entirely; lower
values produce multiple oscillations.
Turn marks fire when |α| > 0.05 rad (~3°). The rear contact-patch positions are offset laterally by sin(α), so marks fan outward mid-corner and trace the oscillation arc on exit — rather than a geometrically perfect curve.
Each car computes a repulsion vector from neighbours within 0.66 × car-width:
f = Σ (n̂away · strength) where strength = (rmin − d) / rmin
This force is blended into the desired heading with a weight of 2.5 normally, reduced to 0.8 while braking (so parked cars near the target don't push an arriving car off-course), and kept active inside the arrival radius so cars can nudge each other apart while parking. The steering rate then clamps actual wheel movement to ±120°/s so the car can never snap direction instantly.
Clicking the page doesn't send all cars to the same point — targets are scattered around the click within a radius that grows with car count. Points are placed via rejection sampling with a minimum separation of 2.5 car-widths, so targets are spread far enough apart that cars don't all enter arrival mode simultaneously. Cars are then sorted by maxSpeed and assigned targets sorted by distance from centre — fastest car gets the furthest target, so the fleet fans out rather than converging.
Four types of skidmark are classified each frame, in priority order. Each type activates a 50/50 coin flip per event — half of all skid events produce no marks at all, giving the organic variation you see on a real track surface.
Stop — braking above
skidThreshold × grip px/s. All four wheels — a random lock bias
is chosen per event: 50% balanced (all equal), 25% front-heavy
(front full, rear at 25% opacity), 25% rear-heavy (rear full, front at 25%
opacity). Simulates unpredictable brake balance under hard stops.
Accel — wheel-spin on launch while
actually gaining speed (10–100 px/s). Driven wheels only: rear marks at
driveBias weight, front marks at 1 − driveBias. RWD
leaves rear marks only; FWD leaves front marks only; AWD blends both.
Opacity scales as 1 − (v − 10) / 90 — darkest at launch,
invisible at 100 px/s.
Turn — rear slip angle above 0.05 rad
at over 60 px/s (see slip angle section above). Opacity scales with slip
magnitude. The inner tyre starts 10 frames later and ends 10 frames earlier
than the outer — a naturally shorter inner streak — and renders at 20% of
the outer's opacity. Contact patches are offset by the slip angle so marks
deviate from a perfect arc.
Bump — any collision while not
hard-braking, at any speed. All four wheels, half opacity. A fast hit
produces a long scrub; a slow nudge produces a short scuff.
Marks are drawn once to a dedicated persistent canvas behind the cars and never redrawn, so they accumulate indefinitely at zero per-frame cost. The global skidOpacity multiplier scales all marks uniformly; in debug mode it is bypassed so individual baked alphas are visible. The tireWidth per car controls the line width of its marks.
Each car casts a soft blurred shadow drawn in a separate canvas transform
before the body, so the offset is in page-space — not car-local
space. A car rotated 180° still casts its shadow in the same screen
direction, simulating a fixed overhead light source. Set
shadowOffsetX: 0, shadowOffsetY: 0
for a shadow directly underneath with no directional component.
Any car can display a sprite image instead of the default colored rectangle.
Pass a URL string or an HTMLImageElement as sprite;
the image is drawn with high-quality smoothing enabled so it stays crisp at
any rotation. Exhaust flames render under the car body so they
appear to emerge from behind bodywork, wings, and diffusers.
When exhaustPosition is set, a teardrop-shaped afterfire flame
fires periodically while the car is moving above 50 px/s. Each event has a
40% chance of a double pop and a 15% chance of a triple — each pop in a burst
re-rolls its size and shape independently. The flame is white-hot at the base,
transitioning through bright yellow and deep orange to a transparent tip.
Use exhaustAngle (90–170°) to sweep side exhaust tips rearward,
and exhaustInset to tuck the emission point inboard from the car
edge — useful for exhausts that exit beneath a wing or body panel. Setting
exhaustPosition: null (the default) disables afterfire entirely
with no overhead.