352 lines
18 KiB
Markdown
352 lines
18 KiB
Markdown
# Round 5: The Gantry
|
||
|
||
Brainstormed autonomously (Luca gave "100% leeway" and asked for the
|
||
brainstorming and frontend-design passes). This file records the
|
||
understanding, the options weighed and the decisions. Evidence is appended at
|
||
the end once the work is done.
|
||
|
||
## Understanding
|
||
|
||
What Luca said (round 4 feedback, desktop first, mobile must not break):
|
||
|
||
1. A cinematic outro that mirrors the intro.
|
||
2. Two red lights drift away from the car, faster than it, while scrolling.
|
||
They must stay locked to the car.
|
||
3. Remove the white rectangles that flash on chapter changes. Nothing may
|
||
flash more than 3 times a second; no large bright flashes at all.
|
||
4. The top bar feels unfinished. It should feel ready and distinctive.
|
||
5. and 6. The empty band between the bar and the content (kept clear for
|
||
the 3D signs) is jarring. Keep the signs, make them better, kill the gap.
|
||
7. A lighter car on mobile, a cooler car on desktop (headless Blender only).
|
||
8. The soundtrack and settings buttons get in the way of content.
|
||
9. Card titles sit inside some cards and outside others. One rule.
|
||
10. Much more motion, fluid and professional, reduced motion respected.
|
||
|
||
Assumptions: the night-drive identity, the content and the copy stay. "Signs"
|
||
means the chapter exit signs, not the districts. "Professional" rules out
|
||
gimmicks such as cursor trails, and anything that slows reading.
|
||
|
||
## The big one: where do the signs live? (items 4, 5, 6, 8)
|
||
|
||
The band exists because the signs dock at the top of the frame, and the
|
||
content had to stay out of their way.
|
||
|
||
| Option | For | Against |
|
||
| --- | --- | --- |
|
||
| A. Signs pass at the screen edges | Real roadside feel | Content spans 1280 of 1440 px. Edges are covered for most chapters. Needs gutters, which is the same waste turned sideways. |
|
||
| B. Camera reframes so signs sit beside the content | No new UI | Every chapter layout differs (right rail, left timeline, full diagram). No side is free everywhere. |
|
||
| C. Content panels framed by the sign | Strong idea for openers | The sign is in the canvas, under the DOM. A frame has to be drawn twice and kept in sync on every scroll. Fragile. |
|
||
| D. Sign flies into the HUD (3D to 2D handoff) | Clear story, frees the band | On its own, the 2D twin is just a label. |
|
||
| **E. The top bar becomes an overhead gantry, the sign docks in its window (A + D)** | The bar and the signs become one object. Content gets the full height. The bar gains a reason to exist. | Needs a see-through window in the bar and a clipped reading viewport. |
|
||
|
||
**Decision: E, "The Gantry".**
|
||
|
||
- The top bar is redrawn as a steel overhead gantry spanning the road: a
|
||
full-bleed beam with rivet rails, a route line and the exits.
|
||
- Its centre holds a **sign window**: a real cut-out the WebGL canvas shows
|
||
through. The reading viewport is clipped at the beam's lower edge, so
|
||
content never shows through the window, and content now starts directly
|
||
under the beam. The gap is gone.
|
||
- On each chapter approach, the 3D sign rises out of the road on a small
|
||
overhead truss, grows as it approaches, and locks into the window (the
|
||
handoff). It holds, then passes overhead, out of the top of the frame,
|
||
exactly like driving under a gantry.
|
||
- Between signs, the window shows its 2D twin: the current exit, the next
|
||
exit, route progress and the Bucharest clock (the old bottom-right HUD).
|
||
- The window is interactive: hover tilts the docked sign toward the pointer.
|
||
Clicking it, or pressing Enter, drives to the next exit.
|
||
- The soundtrack and settings controls move into the gantry's right bay as
|
||
compact icon buttons. Nothing floats over the content any more.
|
||
- Mobile keeps its compact route bar and the screen-anchored shelf sign. The
|
||
shelf now docks into the bar too, so mobile also loses its band.
|
||
|
||
## Other decisions
|
||
|
||
- **Car lights (2), root cause first.** See "Car light bug" below. Fixed at
|
||
the cause, not by hiding symptoms.
|
||
- **Flashing (3).** The exit gates (nine near-white frames at about 4.5 Hz
|
||
with a lens flash and a bloom lift) are deleted, along with the flash
|
||
uniform. The chapter change is now carried by DOM motion: the plate
|
||
entrance, the gantry plate swap and the palette drift. Every remaining
|
||
blinking light is audited against the 3 Hz rule.
|
||
- **Outro (1).** It mirrors the intro. On arrival, the camera cranes up and
|
||
back to the sun, a telephoto end shot. Letterbox bars close in on every
|
||
tier (as CSS, so even the low tier gets them). A closing credit line made
|
||
only of real facts runs in the lower bar. Leaving the destination reverses
|
||
it. Under reduced motion, the bars appear without a crane.
|
||
- **Car (7).** Mobile loads a decimated GLB, built by a headless
|
||
`blender -b` script. Desktop gets better coachwork: clearcoat paint, a
|
||
lit tail bar with a soft halo, and a stronger rim from the key light.
|
||
- **Cards (9).** One rule: *chapter titles sit outside, above their plate;
|
||
every card title sits inside its card, first, under its mono plate label.*
|
||
- **Motion (10).** Chapter plates and cards rise and unmask on entry, with a
|
||
stagger. Hover lifts cards with an accent rule. The gantry plate swaps on
|
||
every chapter change, and the exits light in sequence. All of it is
|
||
transform and opacity only, behind `prefers-reduced-motion:
|
||
no-preference`. No cursor effects.
|
||
|
||
## Car light bug (systematic debugging record)
|
||
|
||
- **Symptom.** While scrolling, two red lights near the car move separately
|
||
from it and faster, then drift away.
|
||
- **Evidence.** Frame bursts at 1440, both idle and scrolling, show bodiless
|
||
red lamp pairs with trails and wet-road pools next to the car's flank, at
|
||
the height of its rear diffuser. These are `world/traffic.ts` vehicles:
|
||
they appear only where traffic lanes are, and their speed matches traffic,
|
||
not the car.
|
||
- **Root cause.** Traffic was authored at 1x scale while the hero car renders
|
||
at 2.78x (a half-width of about 2.6 units).
|
||
- The fast lane at x = -2.6 puts one lamp at x = -1.8, inside the car's
|
||
footprint. The truck lane at x = 4.9 runs along its flank.
|
||
- Lamp heights (0.95 to 1.95) and lamp spacing match the car's own tail
|
||
lamps.
|
||
- Traffic moves at its relative speed, up to about 110 units/s while
|
||
scrolling. So a bodiless pair of red lamps slides out of, or past, the
|
||
car. It reads as the car's own taillights coming loose.
|
||
- **Fix.**
|
||
- Lanes are moved clear of the car's envelope.
|
||
- The car's near field (from 18 units ahead of its nose to behind the
|
||
camera) is a hidden band. Traffic fades out before it and wraps inside
|
||
it, so no light ever passes the car.
|
||
- A unit test pins both rules.
|
||
- The car's own lamps and the wake stay parented to the car, as before.
|
||
|
||
## Evidence
|
||
|
||
All captures are 1440x900 unless named otherwise, from the production
|
||
preview build on a headed GPU Chromium. They are in `docs/redesign/round5/`.
|
||
|
||
| Item | Before | After |
|
||
| --- | --- | --- |
|
||
| 4, 5, 6, 8 Gantry | `gantry-before-1440.webp` | `gantry-after-1440-incoming.webp`, `-docked.webp`, `-idle.webp`, `gantry-after-1024.webp`, `gantry-after-390.webp` |
|
||
| 1 Outro | `outro-before-1440.webp` | `outro-after-1440.webp` |
|
||
| 7 Car | left half of `car-before-after-desktop.webp` | right half; `car-lite-390.webp` |
|
||
| 9, 10 Cards and motion | `cards-before-1440.webp` | `cards-after-1440.webp` |
|
||
|
||
- **Gantry docking (headed run, clicking Profile).** idle at 300 ms,
|
||
incoming at 700 ms, docked from 1000 to 2600 ms, passing at 3300 ms, idle
|
||
at 3700 ms. The face lands within 2 px of the window at 1440x900,
|
||
1920x1080, 1024x768, 390x844 and 844x390 (unit tests). Clicking the
|
||
window, or Enter on it, drives to the next exit (Start to Profile, then
|
||
Experience) at 1440 and 390.
|
||
- **A mask bug found on the way.** The window cut-out used `--window-x`
|
||
as a `mask-position`. Percentages there resolve against the box minus the
|
||
layer, so the hole sat 128 px left of the window and the beam hid half of
|
||
the sign. The cut-out now has its own `--window-mask-x`.
|
||
- **Car.** Phones and the low tier load the lite model: 31,288 to 11,027
|
||
faces, 1.19 MB to 0.50 MB raw. The transfer saving is small (149 KB to
|
||
143 KB), because the geometry already compresses well. The real wins are
|
||
parse time and GPU memory.
|
||
- **fps (same machine, same script, Intel Arc iGPU, headed).**
|
||
|
||
| | start | infrastructure | builds | toolkit | contact |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| Before | 70.1 | 82.6 | 83.8 | 81.8 | 87.2 |
|
||
| After | 68.7 | 83.5 | 79.7 | 83.7 | 95.1 |
|
||
|
||
## Design review pass (Fable)
|
||
|
||
A design consultant reviewed the first pass. Its calls were adopted, except
|
||
where noted under "Deviations". Evidence is in `docs/redesign/round5/`.
|
||
|
||
### The sign interlude (items 5, 6)
|
||
|
||
- **Sequence.**
|
||
- The reading plates step aside: 260 ms in-quart, rising 24 px. Headings
|
||
fade only.
|
||
- The road and the sign then have the frame to themselves. The sign
|
||
hangs from a 3D gantry truss, about 600 px wide in the upper third.
|
||
- It lifts off the truss and arcs into the window, landing within 2 px
|
||
(unit tested). The truss passes overhead as it docks.
|
||
- The plates rise back with a 90 ms stagger (720 ms out-expo).
|
||
- The whole sequence is driven by `html[data-interlude]` (`on`, then
|
||
`rise`), which the scene loop owns.
|
||
- **Window.**
|
||
- At 1280 px and wider it is 320x72, with an 8 px bulge in the beam.
|
||
- The rails sit at 40%.
|
||
- An amber cone of light falls below the window.
|
||
- **Docked.**
|
||
- The 3D face hands off in 200 ms to a flat 2D plate. The plate mirrors
|
||
the face: tab, label fitted like the canvas, and lane rule. So the
|
||
handoff lands in place (`window-handoff-1440.webp`).
|
||
- Hover brings back the 3D twin, which leans at most about 4 degrees,
|
||
with a "DRIVE TO EXIT NN →" label (`window-hover-1440.webp`).
|
||
- **Skips.**
|
||
- Fast scrolling: brisk latches skip the interlude.
|
||
- A hash load or back/forward lands docked without replaying.
|
||
- Reduced motion: no scene and no interlude. The plates simply change.
|
||
- **Measured at 1440 (nav click).**
|
||
- Plates hidden from 340 to 1246 ms, rise until 2355 ms, idle at 3388 ms.
|
||
- About 1.9 s with the rise, against the brief's ~1.4 s. This was kept
|
||
so the arc stays readable.
|
||
- A wheel crossing triggers the interlude.
|
||
- A 2500 px jump shows no sign.
|
||
|
||
### Top bar and controls (items 4, 8)
|
||
|
||
- **Layout.** Brand | exits 01–04 | window | exits 05–08 | bay.
|
||
- **Window plate.**
|
||
- It shows the current exit tab in the sign colour, the chapter, a route
|
||
rule, and a chevron breathing at 0.5 Hz.
|
||
- At the destination the chevron reads "AGAIN ↺", and the window drives
|
||
back to the start.
|
||
- **Exit lamps.**
|
||
- The current exit lamp carries an 18 px ghost swatch in the sign colour.
|
||
- The NEXT EXIT plaques and the RESUME chip are retired. Resume now lives
|
||
in the window ("RESUME EXIT NN →").
|
||
- **Bay.**
|
||
- Two lilac tiles: the soundtrack, with a three-bar equaliser that moves
|
||
at most 1 Hz while playing, and settings.
|
||
- Settings opens a popover: 240 ms out-expo, Esc closes it, focus is
|
||
trapped, and it becomes a bottom sheet on phones. Volume and Pause
|
||
motion moved into it.
|
||
- The active state is amber.
|
||
- Tooltips hang from the button's right edge, are hidden while a popover
|
||
is open or on touch, and are kept out of the accessible name.
|
||
- **Title stack.** The chapter title is smaller (5.6vw, up to 100 px), with
|
||
a tighter gap, so the body starts about 50 px higher at 1440.
|
||
|
||
### Cards (items 9, 10)
|
||
|
||
- **One rule.** A single `.plate` with panel, record and grid variants.
|
||
- **Motion.**
|
||
- Arrivals ease out-expo, departures in-quart.
|
||
- Durations are 160, 320 and 720 ms.
|
||
- **Entrance.** A 28 px rise plus a top-down `clip-path` unmask over
|
||
`view()` entry 0–55%. Firefox and other browsers without
|
||
`animation-timeline` get an IntersectionObserver fallback
|
||
(`useCardEntrances`).
|
||
- **Why `translate`.** Entrances animate `translate`, not `transform`. A
|
||
filled animation overrides `transform`, which had silently disabled the
|
||
hover lift.
|
||
- **Focus.** Focus-visible gets the hover accent edge plus an outline.
|
||
- **chapter-settle removed.** It is dropped because the interlude now owns
|
||
the chapter change ("one timeline owns the chapter change").
|
||
|
||
### Outro: Engine off (item 1)
|
||
|
||
- **Car and scene.** On arrival the drive winds down to a stop: travel,
|
||
traffic and wheel spin. The tail and running lamps drop to parking level,
|
||
the sky darkens 10%, stars rise about 80%, and the grid fades.
|
||
- **Sound.** The engine and road noise take a 1.2 s low-pass sweep to idle
|
||
when the final exit is reached. Driving away restores them.
|
||
- **Letterbox.**
|
||
- Each bar stays within 8vh.
|
||
- The credit, built from real identity facts, types into the lower bar
|
||
in mono. It cannot go in the upper bar, which the contact avatar
|
||
shares.
|
||
- The Email CTA and the END OF ROUTE plaque stay clear at 1440 and 390.
|
||
- **Reduced motion.** Bars and poster dimming only; the credit simply
|
||
appears.
|
||
|
||
### Found on the way
|
||
|
||
- **The mouse wheel did not scroll at all.**
|
||
- Cause: `overscroll-behavior: none` on the script-scrolled,
|
||
overflow-hidden reading viewport, introduced in 71959f7, swallowed
|
||
wheel events.
|
||
- Removed. The wheel now scrolls and wheel crossings raise signs.
|
||
- **The docked plate's root was a fragment.** A template comment made it
|
||
one, which dropped `aria-hidden` from the HUD in tests. The comment was
|
||
moved into the script.
|
||
- **Grey slabs on the road.** These are the existing Blender district
|
||
billboards and gantries, not the new truss. They were left as authored.
|
||
|
||
### Deviations from the review
|
||
|
||
- **Bay targets.** The buttons stay 44 px targets with a 36 px drawn tile.
|
||
The keyboard and pointer-target tests require 44x44.
|
||
- **A third recovery button.** It stays in the bay while motion is paused
|
||
or static ("Enable motion"). Reduced-motion users must reach it without
|
||
opening settings.
|
||
- **Interlude length.** It runs about 1.9 s rather than 1.4 s; see above.
|
||
- **The 2D crossfade under reduced motion.** It is a plain swap: with
|
||
reduced motion the scene never starts, so there is no 3D sign to
|
||
crossfade from.
|
||
|
||
### Risks checked
|
||
|
||
- **mask-composite with backdrop-filter:** no backdrop-filter is used
|
||
anywhere.
|
||
- **No `animation-timeline` (Firefox):** covered by the IntersectionObserver
|
||
fallback.
|
||
- **will-change:** none added.
|
||
- **Scene cost:** the engine-off and interlude scene work is uniform writes
|
||
only. It is free on every tier.
|
||
- **Adaptive tiers:** the low tier keeps the CSS bars and plates.
|
||
|
||
### Verification after the review pass
|
||
|
||
- **Title stack (1440x900, Experience).** Below the 72 px beam, the section
|
||
starts at 100 px, the h2 at 148–245 px, and the chapter body at 268 px.
|
||
Evidence: `round5/title-stack-1440.webp`.
|
||
- **Interlude sequencing.** A CDP screencast of a Start → Experience click
|
||
showed the Profile chapter flashing past during the smooth scroll, before
|
||
the sign's approach hid the plates. A long navigator jump (more than 0.6
|
||
viewport) now pre-arms `data-interlude="on"` at click time, while the
|
||
scene runs. The loop takes the attribute over when the interlude starts,
|
||
and a 1.4 s timeout lets the plates rise if no interlude follows.
|
||
Evidence: `round5/interlude-prearm-1440-sequence.webp`.
|
||
- **Found by the browser suite.**
|
||
- The volume `<output>` was an implicit second live region. It is now an
|
||
`aria-hidden` readout, and the slider carries `aria-valuetext`.
|
||
- The bay tiles lost their outline in forced colours, because the
|
||
gradients are dropped. They now get a `ButtonText` border.
|
||
|
||
### Playwright against the round-4 baseline
|
||
|
||
The baseline is 63 failures. The final full run gave 147 passed, 88
|
||
failed and 55 skipped. Of the 27 failures new against the baseline, 25
|
||
were coupled or fixed and re-run:
|
||
|
||
- **Coupled to Volume and Pause moving into settings:** audio-controls
|
||
×3, bfcache, scene-controls and visual-regression audio. These tests
|
||
were updated.
|
||
- **Coupled to the gantry:** title-navigation ×2, and journey-integration
|
||
×4, where the window pre-draws the resting "Start" twin. These tests
|
||
were updated.
|
||
- **Real regressions, fixed:** forced colours (bay borders) and the
|
||
scene-controls status (the `<output>` live region).
|
||
- **Flaky under full-suite load:** contact-refinements reading mask. It
|
||
passes on a re-run.
|
||
|
||
Two remain on both projects and were left as is:
|
||
|
||
- journey-integration "carries a departed sign past the car"
|
||
- journey-integration "keeps the readable Profile face outside its
|
||
destination panel"
|
||
|
||
Both assert the retired roadside choreography: the sign now hands off to
|
||
the window plate. Seven siblings already fail in the baseline. Rewriting
|
||
the geometry oracle is out of scope.
|
||
|
||
Fixed against the baseline: accessibility-keyboard "explicit keyboard
|
||
navigation" (desktop) and audio-controls compact panels (mobile).
|
||
|
||
### Test edits (intentional behaviour changes)
|
||
|
||
- **Unit, `tests/unit/sign-rendering.test.ts`.**
|
||
- The fade test now uses relative texture counts, and the docked face
|
||
is hidden after the handoff.
|
||
- "closes in over the road in the upper third, then arcs up into the
|
||
window".
|
||
- "leaves the window to the flat plate as it departs".
|
||
- The tilt applies only on hover.
|
||
- **Unit, `tests/unit/SceneCanvas.test.ts`.** Volume and Pause motion are
|
||
checked inside the open settings panel.
|
||
- **Browser, settings.**
|
||
- `audio-controls.spec.ts`: opens Drive settings for volume and pause.
|
||
The volume stays visible beside Quality after Static. The interruption
|
||
test lifts its synthetic state before Resume, because Resume and Mute
|
||
now share one slot.
|
||
- `bfcache-lifecycle.spec.ts`, `scene-controls.spec.ts` and
|
||
`visual-regression.spec.ts`: open Drive settings before Pause motion
|
||
or volume.
|
||
- `tests/performance/drive-trace.spec.ts`: Drive settings instead of
|
||
Sound volume. Not run.
|
||
- **Browser, gantry.**
|
||
- `title-navigation.spec.ts`: the gantry is docked and unscaled from the
|
||
first frame. The current exit is its lane lamp or a rounded inset.
|
||
- `journey-integration.spec.ts`: `roadsideLabels()` leaves out the
|
||
resting "Start" twin texture from the roadside label checks.
|