◀ BACK TO HOME
PROMPT
oops the track title doesn't fit on a mobile screen, adjust accordingly and fade it out quicker
CHANGES
- Responsive title sizing. Measures the track name at the ideal 84 px and shrinks to fit
W · 0.88. Long titles fit cleanly on narrow phones; desktops get the full size
- Snappier intro.
TRACK_INTRO_DURATION 3.6 s → 2.4 s; TRACK_INTRO_HOLD 1.7 s → 1.3 s
PROMPT
actually it's fine, but show the track name more prominently at the start of the race then move into into position at the top in a cool way
(follow-up) always show the track name at the top and fade it out instead of moving it to the top. it should fade out while increasing in size
CHANGES
- Big centred track intro at the start of every track. Two-line readout —
TRACK NN in amber + the track name in white-on-cyan-halo at ~84 px Press Start 2P
- Pop-in (0 → 0.35 s): overshoot bounce scale 0.5 → 1.15 → 1.0 + a brief white screen-flash
- Hold (0.35 → 1.7 s): full size centred at
H · 0.42
- Fade-out + grow (1.7 → 3.6 s): scale 1.0× → 2.0× while alpha 1 → 0 — title expands past the camera
- HUD
#track-label stays visible the whole time; canvas intro plays on top
- Race clock paused while
introT < TRACK_INTRO_DURATION
PROMPT
ok so this is track 1. maintain the random seed for track 1 but choose other seeds for the other tracks, after track 1 comes track 2 and so on, give each track a new name based on boards of canada song titles, these should be random but seeded so procedural. inform the player of which track they're about to play. also if you finish a track, leave the score on a bit longer before going to the next track. after crossing the line the vehicle should slow down so as not to crash
CHANGES
- Track 1 keeps the legacy seed (7341).
trackSeedFor(n) returns 7341 for track 1; tracks 2+ derive a deterministic seed from hash01(n · 13.37 + 17.6) · 1 000 000
- Track names. A
TRACK_NAMES array of band-song-title identifiers used as level labels. Track 1 is hard-coded to the first entry (ROYGBIV); trackNameFor(n) picks the rest via hash01(n · 11.7 + 3.3)
- Multi-track campaign.
race.trackNum / trackName; runScoreAccum carries score across tracks; advanceToNextTrack() regenerates the world with the new seed but preserves score. commitReset() is now the failure path only
- Persistent TRACK label at the top of the HUD with diff-caching
- Finish overlay extended 6 s → 9.5 s with a two-phase crossfade: score breakdown then NEXT TRACK callout
- Vehicle slows down past the line.
throttle=0, brake=1 override during the post-finish hold; off-track crash check skipped so a curve after the flag can't wipe out the celebration
- Crash readout (WIPEOUT / TIME OVER) shows the full accumulated score
PROMPT
ok tweak it so it's only 60 seconds per stage, and make them slightly further apart
CHANGES
- Per-stage duration 90 s → 60 s. Total base budget 270 s → 180 s
- Distances 60 / 140 / 240 k → 65 / 150 / 255 k. Gaps now 65 k / 85 k / 105 k
- Net: time-out back in play even with carry-over. Need ~1500 u/s (~94% MAX_SPEED) to finish on the wire
PROMPT
ok a few things, make the stages further apart say 10 times further apart, add any remaining time at checkpoint to the new countdown. give the player 90 seconds per stage and when you pass a checkpoint the word checkpoint! should be displayed in a cool way and then disappear.
CHANGES
- Stages 10× further apart. Distances 6000 / 14000 / 24000 → 60 000 / 140 000 / 240 000
- 90 seconds per stage across the board (was 22 / 22 / 25)
- Time carries over at checkpoints. Leftover seconds added to the next stage's 90 s — fast play compounds
- CHECKPOINT! banner renders centre-screen on every intermediate cross. Snap-zoom from 1.4× scale to 1.0× while fading in (0.15 s), holds ~0.9 s, then fades out over 0.45 s. Two-pass text — wide amber drop-shadow halo + slightly smaller white-on-amber core
- Final flag still skips the CHECKPOINT! banner — the finish overlay takes over
PROMPT
ok let's make a radical change. it's too easy to get a high score by simply driving slowly. add an outrun style countdown where you must complete 3 stages in a certain time, think of it as 3 checkpoints, when you hit a checkpoint before time runs out the countdown resets after the third stage you get to the chequered flag and any time you had left on the clock is generously factored into your score. maintain the style.
CHANGES
- 3-stage race with countdown clock. Stage distances: 6000 / 14000 / 24000 units. Per-stage durations: 22 / 22 / 25 seconds. Each stage's checkpoint resets the timer to the next stage's duration. Reach the chequered flag at 24000 to finish
- TIME OVER — when
race.timeLeft hits 0, triggerCrash("timeout") fires the existing slow-mo explosion sequence with a "TIME OVER" label instead of "WIPEOUT" (separate crashReason flag distinguishes them)
- Generous bonus at the flag —
race.finalBonus = floor(timeLeft × 250,000) added on top of the base score (speed × distance at flag-cross)
- Top HUD restructured: big Press Start 2P
#timer at top centre with urgency colours — cyan above 10 s, amber 5–10 s, rose under 5 s with a step-blink animation. Below it a pip indicator 1 ✦ 2 ✦ 3 with the active stage glowing amber and completed stages in cyan. Score sits below the pips at a smaller size
- Intermediate checkpoints draw as a bright amber bar across the track; final is a two-row alternating white + cyan chequered banner
- Finish screen —
drawFinishOverlay() renders "FINISH!" plus a score breakdown over a fading backdrop, holds 6 s then auto-resets
PROMPT
explain what the hexagon pick ups do at the start, maybe show an example of the spinning hexagon pickup
CHANGES
- New SHIELD PICKUP tutorial card in the start overlay — a live SVG hexagon that spins (
5s linear infinite) with a pulsing white core (0.9s ease-in-out), matching the in-game magenta colour + glow
- One-line explanation next to the demo: "grab one to charge the side rails for 5 seconds — the track edges push you back harder, so you can lean into corners"
- Styled to match the overlay aesthetic: dashed magenta border, low-opacity magenta tint, Press Start 2P label + VT323 description
- SVG uses
drop-shadow filters for the phosphor glow so the demo reads at the same vector-tube intensity as the in-game pickup
- Fix pass after first cut: the hex was orbiting (translating in a wide circle) rather than spinning in place. Cause:
transform-origin: center on an inner <g> resolves to the SVG viewport's top-left in some browsers because transform-box defaults are inconsistent. Moved the spin to the outer <svg> element (a DOM element where transform-origin: center is well-behaved), kept the core's pulse on the inner circle with transform-box: fill-box so its scale stays pinned to its own centre. Added 6 white vertex phosphor dots that ride along with the rotation — matches the in-game render where each beam() call auto-emits dots at both endpoints
PROMPT
ok lets try adding drift around corners when the brake is applied
CHANGES
- New
car.slipAngle field — radians offset between the car's heading and its actual motion direction. Translate now uses motionDir = heading + slipAngle instead of plain heading, so the world scrolls diagonally during a slide
- Drift trigger: brake held + steer commitment over
DRIFT_STEER_MIN = 0.18 + speed over DRIFT_MIN_SPEED = 220. Below the threshold the brake is just a brake (no slide)
- Yaw boost while drifting (
DRIFT_YAW_BOOST = 1.55) — heading rotates faster than motion. Slip angle eases toward ±DRIFT_MAX_SLIP = 0.50 rad (~29°) opposite the steer at 1/τ = 5.5. When the trigger releases, slip decays back to zero at 1/τ = 6.5 — grip recovers
- Speed dynamics: brake during drift is softened (
DRIFT_BRAKE_MUL = 0.30) so the slide actually continues, plus DRIFT_DRAG = 90 u/s² of bleed so it isn't free speed
- Visual cue: wheels brighten to hot orange (
#ff8d3a) and thicken (1.4 → 1.8 px) while |slipAngle| > 0.06 rad. The diagonal world-scroll already shows the slide; the brighter wheels just emphasise it
commitReset() clears slipAngle alongside the rest of the car state
PROMPT
ok I don't like the ramps, keep this version for reference but rollback and add power ups on the course that make the repellent effect of the side of the course stronger, during this period have the vehicle flash
CHANGES
- Rollback: ramps + airborne physics dropped from
latest/; v015 stays in the snapshot folder as the "we tried ramps" reference. Codebase is back to the v014 baseline before layering the new feature
- Power-ups: sparse pickups every 1500–2600 units (deterministic from
worldSeed). Visual: a magenta wireframe hexagon rotating slowly at phase × 1.2, with a pulsing white phosphor core dot — clearly distinct from track (cyan) and scenery (amber)
- Pickup detection: when the car centre is within
POWERUP_RADIUS = 24 of an unpicked one (along-track + lateral together), the pickup is marked taken and car.boostT = 5 seconds
- Boost effect: while
car.boostT > 0, edge repulsion is multiplied by 3.0× (max push 620 → 1860 u/s) — well above MAX_SPEED, so the edge becomes a near-soft-wall you can't easily punch through. Returns to baseline when the timer expires
- Vehicle flash: while boost is active, body / cockpit / wheel colours switch to a magenta-tinted palette at ~6 Hz via
(Math.floor(phase × 12) & 1). Square wave gives a sharp on/off blink that reads as "powered up" rather than a soft glow
commitReset() clears the powerups array + boost timer; trimWorld() drops pickups (taken or not) once they're well behind
- Fix pass: the "power-ups disappear" bug — four call sites computed track indices as
Math.floor(s / SEG_LEN), but after trimWorld() splices old segments off the front, track[0].s is no longer 0 so that math points at the wrong row (or clamps to the last segment, making the hexagon snap to the end of the visible track). Added a trackIndexAt(s) helper that uses track[0].s as the base offset; rewired scenery placement, power-up gen, power-up render, and distance markers through it
- Hexagon visual radius 14 → 21, pickup hit radius 18 → 24
PROMPT
ok let's try adding occasional ramps to the course, when you hit a ramp the vehicle leaves the ground and the road fades a bit and allows the vehicle to fly over any part of the course but must land on the track again or wipeout
CHANGES
- Ramps: sparse procedural launches placed every ~900–1700 units (deterministic from
worldSeed). Each ramp is a wireframe wedge — a forward-pointing triangle with two perspective cross-bars narrowing toward the apex — drawn in amber so it pops against the cyan track edges. Width clamped to a fraction of the (variable) track half-width so it never blocks the track
- Airborne physics: hitting a ramp footprint (along-track and lateral) launches the car.
altVel = 220 + (speed/MAX_SPEED) · 460, gravity 720 u/s². While airborne: forward velocity preserved (no throttle/brake/coast), edge repulsion off, off-track crash check skipped. Steering still works at ~45% authority. On landing, if the vehicle lands inside the track edges → safe; if not → triggerCrash()
- Visual fade:
worldAlpha = 1 − 0.6 · min(1, altitude/70) applied to only the G_TRACK + G_SCEN beams and dots — track + scenery dim while the car + thrust + intersection sparkles stay full strength. Group tag now travels with each dot too
- Car scale-up: while airborne, render scale
= zoom · (1 + altitude · 0.0065) — the vehicle grows toward the camera as it climbs, gives the lift cue without touching the camera transform
commitReset() clears airborne state + ramps array; trimWorld() drops ramps far behind the player alongside scenery
- Tuning pass: jumps higher + longer airtime, ramps sparser, fade stronger:
GRAVITY 720 → 600 / LAUNCH_BASE 220 → 360 / LAUNCH_BONUS 460 → 680 — full-speed jumps now ~3.5 s airtime peaking ~900 u (was ~1.9 s / ~320 u)
RAMP_GAP_MIN 900 → 2000, RAMP_GAP_RANGE 800 → 1600 — ramps now every 2000–3600 units (≈ half as many)
- World fade:
worldAlpha = 1 − 0.78 · altitudeFactor (was 0.60), saturating at altitude 90 (was 70). Track + scenery drop to ~0.22 alpha at peak (was 0.40)
- Car-scale formula clamped via
Math.min(2.4, 1 + altitude · 0.004) so very high jumps don't make the car fill the screen
PROMPT
the key info on the first screen is a little messy, tidy that up a bit
CHANGES
- Controls list rebuilt as a 4-row grid: keys right-aligned | action left-aligned, with a fixed column gap. Each row covers one direction so the layout is symmetrical (LEFT and RIGHT are now their own rows instead of being lumped together as STEER)
kbd elements get a fixed min-width: 28px + display: inline-flex so arrow-glyph and letter keys all render the same size — no more uneven gaps from glyphs of different widths
- Arrow + letter alternates sit beside each other (no
/ separator); the grouping reads naturally without punctuation noise
- Action labels coloured cyan with a soft glow so they pop next to the white keycaps; old reliance on
for spacing removed
- "OR DRAG THE JOYSTICK" → "OR USE THE JOYSTICK" (covers both touch and mouse)
PROMPT
ok - small change move the joystick up a bit because on mobile it's too close to the edge of the screen causing unintentional gestures
CHANGES
- Joystick centre raised:
cy = H - r - 80 (was H - H · 0.10) — keeps a consistent ~50 px clearance below the BRAKE / STEER labels and out of the iOS / Android swipe-up gesture zone, regardless of viewport height
- Pointer hit-zone updated to follow the joystick (
py ≥ stick.cy - stick.r - 20) so the top of the knob stays grabbable now that the stick sits higher
PROMPT
do 2 (option 2 from the v011 follow-up: rollback)
CHANGES
- WebGPU code path removed from
latest/ — back to single-canvas, canvas-2D-only rendering
- Stripped: WGSL shaders,
initWebGPU, two-canvas stack, ?renderer=webgpu flag, render-loop branches, resize hooks for the GPU canvas
- v011 stays frozen as the "we tried this" reference. The shader work + integration glue is preserved there if we want to revisit when scene density actually outgrows canvas-2D
- Net code delta:
game.js ~1500 lines → ~1200 lines; styles.css back to single canvas rule; index.html back to single canvas element
WHY
- At v010 scene density on a fast Mac, both renderers easily hit the 60 fps cap, so the WebGPU win is invisible — it's headroom for content we don't have yet
- The shader-rendered output looked slightly less crisp than the canvas-2D layered strokes. A second shader pass tightened it but at that point we're maintaining two render paths for parity, not headroom
- Decision: pay the perf cost when there's content that needs it; until then, the simpler single-path canvas-2D code wins on maintainability
PROMPT
I still feel like if we use webgpu we'd free ourselves up to worry less about performance, we can always rollback if it doesn't work
CHANGES
- Optional WebGPU renderer for beams + dots. Activated by
?renderer=webgpu in the URL; falls back automatically to canvas 2D if navigator.gpu is missing or device init fails. Game logic, joystick, crash explosion, and HUD stay on the canvas-2D layer
- Two-canvas stack:
<canvas id="game-gpu"> at z-index 0 (WebGPU output, opaque, full screen) sits behind the existing <canvas id="game"> at z-index 1 (canvas-2D, cleared transparent when GPU is active so the layer below shows through)
- Line shader (WGSL): one instance per beam, vertex shader expands a quad along the line with rounded-cap glow padding, fragment shader computes capsule SDF and outputs
color × (crisp + exp-falloff glow) with additive blend. Single draw call replaces the 2-pass canvas-2D stroke loop
- Dot shader (WGSL): billboarded quad per dot, fragment evaluates halo + mid + hot-core layers from one radial SDF in a single draw call. Replaces the 3-fill-per-dot canvas-2D loop
- Hex-color → RGBA float cache so per-frame instance buffer marshalling is one Map lookup per beam/dot
- Per-frame instance buffers grow on demand (start 256 lines / 512 dots, double when needed) and are reused across frames
device.lost handler clears the webgpu reference so we automatically fall back to canvas 2D if the GPU disappears mid-session
- Canvas-2D crisp beam pass deduplicated (an earlier edit had accidentally left a duplicate copy)
OPENING NOTES
- To opt in: append
?renderer=webgpu to the URL (the PLAY button on this card already does)
- Without the flag, the game runs the same canvas-2D path as v010
- If your browser doesn't support WebGPU (Firefox < 141, Safari < 26, Chromium without GPU), it logs a warning and silently falls back
SHADER CRISPNESS FIX
- First pass used exponential glow falloff (
exp(-d * k)) which bled softer than canvas-2D's hard-edged 2-pass stroke. Switched both shaders to smoothstep with hard edges matching the canvas-2D layers exactly: line crisp inner (α 1) + mid-glow ring (α 0.26) at width × 2.6; dot halo + mid + white core as three hard-edged disks. Smoothstep starts at the disk/line edge so the interior keeps full intensity (no AA softening of thin lines)
- Performance honesty note: at 60 fps cap on a fast Mac, both renderers easily hit vsync — the WebGPU win is headroom, only visible if scene density grows past what canvas-2D can sustain. If that headroom isn't currently valuable, this version can be ignored and v010 remains the active baseline
PROMPT
there seems to be a few performance issues, especially in Firefox, please look into improving performance, maybe we can use webgpu related stuff. also with firefox I left ot on overnight and it slowed to a crawl. memory leak?
CHANGES
- Memory leak fix:
trimWorld() splices old track[] segments + deletes far-behind sceneryChunks entries every ~1s; lastSegIdx is adjusted so the window math stays valid
- Intersection scan capped at 6000 pair checks per frame so heavy scenery can't make it dominate
- Vignette gradient cached and rebuilt only on resize (was being recreated every frame)
visibilitychange handler gates the entire frame body when the tab is hidden and resets last on return
- FPS readout added to the HUD for quick A/B perception checks without DevTools
- WebGPU not used — bottlenecks were JS-side state churn + unbounded arrays; canvas 2D handles this scene fine if we don't fight the rasterizer
- Hotfix: moved
_vignette declaration to the top of the IIFE — initial resize() ran before let _vignette, throwing TDZ ReferenceError on load
- Reverted: the path-batched beam + dot renderers I shipped first turned out to be slower in both browsers (worse in Firefox). Building one big path with hundreds of arc/line subpaths and filling it under
"lighter" hits the complex-path slow path; the state-change savings don't pay back. Back to per-element draws for now
- The actual win — Firefox profile pinpointed it:
drawScanlines was eating 67% of frame time (full-screen fillRect with globalCompositeOperation = "overlay"), and the per-frame radial-gradient vignette another 14%. 81% of CPU on two effects that don't need to be in JS at all. Both moved to body::before / body::after CSS pseudo-elements layered over the canvas — the browser compositor handles them effectively for free. Visual result identical
- Follow-up profile (21× fewer total samples) revealed two smaller spikes — both addressed:
- Joystick labels: re-issuing
ctx.font + 3× fillText every frame triggered Style computation. Pre-baked to three offscreen canvases at updateStickGeom; render now does drawImage
updateHUD: was setting textContent on 4 spans + running toLocaleString + writing to localStorage every frame the score climbed. Now diffed against last-displayed values — DOM only updates on actual change. Best-score writes debounced via setInterval(saveBestScore, 3000) + pagehide save
carTrackInfo() result cached per frame so render() and updateHUD() share one nearest-segment search
- Follow-ups (third profile pass):
- Beam glow reduced to 2 passes. Dropped the wide × 5 / α 0.10 outer-glow pass — widest line caps to rasterize, contributed least to bloom. Mid pass slightly bumped (× 2.4 α 0.22 → × 2.6 α 0.26) so the visual stays close. Kept
- Joystick labels bake made dpr-aware (was rendering at 1× and being upscaled on retina displays). Kept
- Tried + reverted: baked the joystick's static rings / ticks / glows into two offscreen canvases (one source-over, one composited "lighter") so each frame would be 2
drawImage calls. Profile showed it was slower in Firefox — drawJoystick went 5.2% → 8.4%, and drawImage appeared as a brand-new 7.9% entry. Likely cause: the bake bbox is mostly transparent space but Firefox's "lighter"-blend canvas-to-canvas drawImage still pays per-pixel work over the full ~350×350 region, while the original live draws only paid for actually-stroked pixels. Reverted to the previous live drawJoystick. Cardinal ticks now use a single 4-subpath stroked path (one stroke call instead of four) — small consolation
PROMPT
I don't like this track. change the seed
CHANGES
worldSeed 1 → 7341 in both the initial assignment and commitReset()
PROMPT
just do the first thing
CHANGES
- World seed pinned to
1 instead of being randomized on load + on reset — same track every run
- Same fixed seed used after a crash so the world is identical post-reset
PROMPT
ok the edges of the track are slightly repellent, almost antimagnetic but if you go too fast this won't be enought to stop you going out, the closer to the edges the more you feel it though
CHANGES
- Quadratic edge-repulsion velocity field —
proximity²·EDGE_REPEL_MAX (620 u/s) added laterally toward the centerline
- Repel zone
min(75, hw·0.6) so narrow tracks shrink the field with them
- Slow drift is corrected; fast perpendicular runs (lateral component up to 1600 u/s) punch through
- Visualisation: nearest edge thickens with proximity + pulsing inward "field ticks" on every nearby segment
car.edgeProximity + car.edgeSide exposed for the renderer
PROMPT
ok when we crash we should see a spectacular explosion in slow motion
CHANGES
- Crash sequence runs in slow-mo for 2.4 real seconds via a global
timeScale (curve 0.10 + t^2.6 · 0.90)
- Car shatters into ~17 rotating line-shard debris pieces flying outward, each with phosphor-dot endpoints
- 90 radial sparks (red / yellow / orange / white) with halos + hot cores
- 5 staggered shockwave rings, each with halo + mid-glow + crisp ring passes
- Initial radial bloom (white→amber→red), then darkening vignette while debris flies
- WIPEOUT title + frozen final score in Press Start 2P, mid-screen
- HUD score hidden during the crash so it doesn't fight the in-canvas readout
commitReset() regenerates the world after the spectacle ends
PROMPT
ok vary the size of the track more in dimension although we can have lon stretches the same size, include the score in a font reminiscent of asteroids, top centre larger than the distance font, the score should be speed multiplied by distance, use commas if the score gets large.
CHANGES
- Width range expanded to 38–230; double-smoothstep on a slow noise (~5500u wavelength) gives long flat plateaus that snap between bands
- Spawn area clamped to ≥55% of max width so you never start in a pinch
- Press Start 2P (Google Fonts) loaded for the score readout
- Score =
floor(car.speed × distance), formatted via toLocaleString("en-US") for commas
- SCORE element top-center,
clamp(26px, 4.4vw, 56px) with cyan triple-glow
BEST now tracks max score (new infiniracer.bestScore localStorage key) and is also comma-formatted
PROMPT
in the bottom fith of the screen show a joystick I can use to control the ship with the mouse, also change the lettered controls to q up a brake o left and p right
CHANGES
- Vector-styled virtual joystick in the bottom fifth — drag with mouse or touch, knob springs back on release
- Outer halo ring + cardinal phosphor ticks + amber stem + hot core knob;
THROTTLE / BRAKE / ◀ STEER ▶ labels
- Keyboard remap: throttle
Q/↑, brake A/↓, left O/←, right P/→
- Unified analog input pipeline (
readInput()) — keyboard ORed with stick magnitude per axis
- Thrust flame intensity now scales with analog throttle
- Cursor changed back from
none to crosshair; touch-action: none to stop mobile fighting the stick
- Start overlay updated with the new controls + "OR DRAG THE JOYSTICK"
PROMPT
ok allow the player to go much much faster and alter the width of the track randomly. when the player goes off the track reset the distance
CHANGES
MAX_SPEED 620 → 1600, ACCEL 140 → 280, BRAKE_DECEL 320 → 520
- Per-segment
hw (50–160) driven by two slow noise octaves; spawn clamped wide
- Off-track now triggers a full run reset — world seed re-randomized, track + scenery cleared, car teleported home
- Brief red/white WIPEOUT flash with text on reset; best distance preserved
- Distance markers + scenery offsets switched from the old constant to per-segment
seg.hw
PROMPT
ok cool, the vehicle should also alter size when zooming out and make it possible to go faster
CHANGES
- Car geometry scaled by world zoom in
p(x, y) so the ship shrinks as the camera pulls back; line widths stay constant (vector-tube convention)
MAX_SPEED 360 → 620, ACCEL 95 → 140, BRAKE_DECEL 220 → 320
- Off-road drag rebalanced to
BRAKE_DECEL · 0.55 so it still bites at the new top speed
PROMPT
ok in v001 dir create a game using only js html and css, the game is infini racer where you control a vehicle through a landscape that is purposefully minimal, in the vector style of the original asteroid game from the 70s/80s. where lines on landscape object or vehicle intersect there is a more brilliant point or dot. This is a top down racer where the vehicle appears static but the course moves and zooms out the faster you go. You can also look at other vector/retro games for inspiration.
CHANGES
- Vector top-down racer: car drawn at screen-center pointing up; the world rotates + translates around it
- Procedural infinite track from two octaves of smooth noise; deterministic chunked scenery (3–6-sided wireframes)
- 3-pass beam rendering — wide soft glow → medium glow → crisp line — for a phosphor-tube feel
- Phosphor endpoint dots at every line endpoint plus extra-bright cross-group line-intersection dots
- Camera zoom-out as
1.05 / (1 + speed_ratio · 1.35)
- CRT scanline overlay + radial vignette
- Off-road drag, distance + speed HUD, best distance in
localStorage