7.0 KiB
Round 3: The Route
Concept
Round 2 gave the page one sign language. It still read as one page: eight identical glass cards laid over a world that never changed. Round 3 makes the drive a route, with places along it.
- The world changes per chapter. A Blender-authored kit of roadside districts rises out of the ground for each chapter and sinks again behind you.
- The page changes its form. Each chapter opens with its exit tab and a large heading standing over the road, then a reading panel, then a "next exit" sign. Open road separates chapters, so the districts can be seen.
- Navigation is a map of the route. You can see where you are, which exits you have passed, and where you stopped last time.
Changes
World
- Roadside districts (
assets/blender/districts/,src/scene/world/districts.ts).-
Eight original pieces, 5,424 triangles in total. The GLB is 84 KB raw, or 13 KB with brotli.
-
The kit is authored by a headless script,
author-districts.py. It writesnightdrive-districts.blendwith its own "Roadside Districts" scene. -
export-districts.pywrites the GLB and the provenance file. -
Which pieces rise in which chapter:
Pieces Chapters Gantries Profile to Toolkit Billboards Profile, Career Server rows and radio masts Infra Workshop and cranes Builds Container yard More work Destination arch Contact -
Pieces are instanced and wrapped by the same distance shader as the expressway. Flat normals are derived per fragment, so the GLB carries positions only.
-
Lights take the chapter accent, catch with a neon stutter, and dim close to the camera.
-
Pieces at rest are hidden, so they cost no draw calls.
-
The low tier halves the instance counts.
-
Shaders compile at the on-ramp, not mid-drive.
-
Page
- Reading stage (fixed in
71959f7, before this round's UI work).- Sticky failed because the viewport used
overflow: clip. - The viewport is now a script-scrolled
overflow: hiddencontainer. - Browser-initiated inner scrolls (find-in-page, focus) are forwarded to the document.
- Navigation re-lands before focus if the stage switches on or off mid-journey.
- Sticky failed because the viewport used
- Chapter openers (
PortfolioSection.vue,route.css).- Each chapter starts with a
clamp(54px, 7.2vw, 124px)heading over the road, with a dark halo instead of a plate and an accent bar. - The panel chrome moves to a new
.chapter-body. - Each chapter ends with a
NEXT EXIT 04 / Infrastructure →sign. - Phones get the same opener, scaled to the column.
- Each chapter starts with a
- Case studies (≥1100 px).
- Evidence sits beside a sticky media column. This was impossible before the stage fix.
- Evidence reads as spec rows: a lettered stop, then the text.
- Destination.
- A 168 px heading and larger copy and CTA.
- The avatar stands beside the heading.
- The plate reads
END OF ROUTE · 7 OF 8 EXITS · 3 MIN ON THE ROAD. This comes from the real visit and is rendered as CSS generated content.
- Route-map navigation.
- A road line under the exits fills up to the current one. It changes on chapter change only, never per scroll.
- Nodes are hollow ahead, filled once passed, and shown as a car marker where you are.
- On phones the closed bar reads
EXIT 05 · Builds, and the open list is a vertical route.
- Persisted progress (
useRouteMemory.ts).- Exits passed and the last stop are kept in localStorage.
- Storage is read only after mount, and failures are silent.
- A returning visitor gets a "Resume · Exit 05 · Builds →" chip in the hero. The chip has an aria-label and a CSS label, so the hydrated text is identical to the server document.
- Keyboard. Alt+↓ and Alt+↑ go to the next and previous exit.
Decisions
| Brief item | Decision | Why |
|---|---|---|
| Per-chapter Blender props and districts | Adopted | The biggest missing piece: the world now tells you which chapter you are in. |
| Gantries | Adopted | They are part of the district kit, and their height clears the chase camera. |
| Better sign geometry | Adapted | Round 2's sign faces read well at 1280, 1440 and 1920. The budget went to the districts instead. |
| Atmosphere and time progression | Kept | The chapter palettes already run from deep night to pre-dawn. District accents now follow them. |
| Rain or wet road | Rejected | A new full-screen pass plus reflections costs a GPU tier, and it fights the synthwave identity. |
| Better hero car lighting | Deferred | The car GLB was not touched this round (see the Blender note below). |
| Draco or meshopt | Rejected | The districts are 13 KB brotli, and the decoder would cost more than it saves. |
| Reading panels and typography | Adopted | Chapter openers, .chapter-body panels and next-exit signs. |
| Case studies | Adapted | Sticky two-column at ≥1100 px. Long galleries stay in normal flow when taller than the window. |
| Route map or minimap | Adopted | The navigation itself is the map. No extra widget, and no per-scroll work. |
| Scroll choreography | Adapted | Rising districts and the exit-tab strike carry it. The view-timeline reveals stay off inside the live stage to avoid fighting the reading mask. |
| Mobile first-class | Adapted | Openers, the route bar and the vertical route list on phones. No horizontal scroll at 320 or 390 px. |
| Contact as a destination and a strong ending | Adopted | The arch district, the giant heading and the trip log. |
| Sticky layout | Fixed | See "Reading stage" above. |
| Keyboard journey | Adapted | Alt+arrows, not digits or [ ]: single-character shortcuts fail WCAG 2.1.4 without a way to turn them off. |
| Deep links | Kept | Hash links already land correctly and are now more robust. |
| Persisted progress | Adopted | localStorage, client-only. |
| Honest loading states | Kept | Districts stream in silently. A failed fetch leaves the old road. |
| Sound coupling | Deferred | Round 2's music pulse already couples the world to the soundtrack. A pass-by on exit arrival needed audio design time this round did not have. |
| Stylesheet consolidation | Deferred | route.css is one new file. Merging the eleven existing files safely needs a cascade-order audit, and its risk is out of proportion to its value. |
Blender note
An early attempt drove the user's live Blender session through MCP. Calling
bpy.data.libraries.write with a scene crashed that Blender. The open file
reported no unsaved changes beforehand. The crash log is at
/tmp/nightdrive-grand-tourer.crash.txt.
All later work ran headless (blender -b --factory-startup). The author script
refuses to run anywhere else.
Run
source ~/.nvm/nvm.sh && nvm use 22.22.1
npm run build:preview # budgets: "violations": []
python3 -m http.server 5175 -d dist
blender -b --factory-startup --python assets/blender/districts/author-districts.py
blender -b assets/blender/districts/nightdrive-districts.blend --python assets/blender/districts/export-districts.py