Files
Portfolio/docs/redesign/round5.md

352 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.