Misbehaved Race Cars

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.

Navigation
Arrival radius
144px
Scatter radius
1.40×
Slip angle
Stiffness
34
Damping
3.0
Scale
1.0×
Shadows
Blur
4.5px
Opacity
0.40
Exhaust
Pop interval
0.9s

Programmatic API

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

Car options

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)

CarDriver options

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


Steering — the bicycle model

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.

Bézier paths

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.

Speed & alignment scaling

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.

Speed & braking

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 targetbrakes too low — increase it or reduce maxSpeed
Stops too abruptly, no characterbrakes too high; or lower grip to soften effective decel
Skids at very low speedgrip too low — it multiplies skidThreshold down; raise grip or skidThreshold
No skids at allmaxSpeed below skidThreshold × grip; lower grip or skidThreshold
Dramatic rear oversteer / driftLow grip weakens the slip spring; pair with high slipScale for wider marks
Car won't reach top speedAlignment scaling is capping it — large heading error; reduce aggression
Acceleration feels sluggishacceleration too low; or maxSpeed very high making the ramp long

Braking & arrival

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.

Orbit detection & escape

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.

Collision resolution

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.

Proximity boost

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.

Rear slip angle

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.

Avoidance steering

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.

Target scatter & assignment

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.

Skidmarks

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.

Shadows

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.

Sprites & exhaust

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.

click anywhere to drive