diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/README.md b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/README.md new file mode 100644 index 0000000..1032a06 --- /dev/null +++ b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/README.md @@ -0,0 +1,88 @@ +# Nightdrive sign reliability and legibility evidence + +Status date: 2026-09-03 + +This directory records the bounded Three.js roadside-sign pass. The portfolio content, panels, navigation, HUD, controls, car, road, terrain, sun, atmosphere, camera, and forward-travel direction were not redesigned. + +## Shared causes found + +The supplied Profile failure was not one isolated coordinate defect: + +1. The former candidate gate discarded large deltas and multi-boundary samples before lifecycle reconciliation. Slow threshold crossings and rapid arrival therefore followed different paths, and the latter could skip the relevant destination. +2. Dwell time was derived from the original latch time. A delayed frame could consume the readable interval before the first dwell frame, while no further input could leave another sign frozen indefinitely. +3. Every desktop label inherited one right-side pose. Profile therefore competed directly with its right-side content card, while one immediate responsive remap could move a live sign to a different pose family. +4. The 1024×288 source was stretched onto a 3.93:1 face, could be drawn before the intended font was ready, and was small enough relative to the projected dwell face to look soft after perspective, mip selection, and post-processing. +5. The former departure could advance toward and through the camera plane. A late departure frame consequently expanded instead of receding. +6. A scene rebuild copied the lifecycle fields but omitted the latched responsive profile, so a quality/resize rebuild could silently replace a live composition. + +## Canonical route contract + +Only forward `passive-scroll` samples with motion enabled are eligible. The opening route is deliberately ineligible. A normal sample selects the next route once its heading is within 0.55 viewport heights of the activation line. A sample that crosses a section boundary selects the eligible section actually reached. Large and multi-section jumps coalesce to that latest relevant destination rather than queueing intermediate labels. + +| Route target | Face label | Marker | Corridor | Trigger | Dwell placement intent | +| --- | --- | --- | --- | --- | --- | +| `start` | Start | 01 | none | ineligible | no sign | +| `profile` | Profile | 02 | left | ≤0.55vh or actual arrival | opposite the right Profile card | +| `experience` | Experience | 03 | right | ≤0.55vh or actual arrival | opposite the left Career card | +| `infrastructure` | Infrastructure | 04 | outer-right | ≤0.55vh or actual arrival | outside the broad systems content corridor | +| `selected-builds` | Selected builds | 05 | outer-right | ≤0.55vh or actual arrival | outside the projects content corridor | +| `additional-work` | Additional work | 06 | left | ≤0.55vh or actual arrival | away from the dominant content surface | +| `toolkit` | Toolkit | 07 | outer-left | ≤0.55vh or actual arrival | outside the toolkit content corridor | +| `contact` | Contact | 08 | right | ≤0.55vh or actual arrival | opposite the Contact destination content | + +The controller owns exactly one visible target. A concealed sign can retarget immediately. A visible label is immutable; a newer candidate is coalesced as the pending target. Completed targets are suppressed for the current journey so small oscillations cannot spam them. A stale readable target completes its minimum dwell before departing. Initialization, explicit navigation (including skip links and direct fragments), history restoration, Static mode, and reduced motion remain intentionally suppressed. Visibility loss resets transient lifecycle state. + +## Lifecycle contract + +| Phase | Duration | Behavior | +| --- | ---: | --- | +| Anticipate | 300ms | target/profile latch; sign remains concealed at its horizon pose | +| Approach | 900ms, reduced by at most 250ms with acceleration | elapsed-time interpolation from horizon to dwell; opacity reaches full early in approach | +| Dwell | at least 1,400ms from the first actual dwell sample | fixed readable pose; reverse/passed/stale input cannot shorten the minimum | +| Depart | 700ms | exits laterally and recedes 32 world units; never reverses through the car or crosses the camera | +| Cooldown | 350ms | prevents immediate replay; retains only the latest valid pending target | + +The controller continues sampling without input while a sign is live, so release cannot freeze it. A delayed first dwell frame starts the dwell clock at that observed frame rather than skipping readability. Resize/orientation and scene-quality rebuilds preserve the latched `wide`, `tall`, or `short` profile until the lifecycle is concealed; the newly measured family applies to the next sign. + +## Responsive pose and texture contract + +`short` applies below 560px height in landscape, `tall` below a 1.05 aspect ratio, and `wide` otherwise. Each family has inner left/right and outer left/right roadside poses. Dwell face-width assertions are 120px at 390×844, 130px at 844×390, 150px at 768×1024, 180px at 1440×900, and 240px at 2560×1440. Every readable face must stay at least 8px inside the viewport, 16px clear of active and adjacent opaque content, and 8px clear of fixed navigation, controls, and HUD. + +The face is an exact 4:1 plane backed by a 2048×512 4:1 canvas. It uses sRGB, trilinear mipmaps, linear magnification, up to 8× renderer-supported anisotropy, and a non-tone-mapped face material. The bundled Geist Mono marker and label weights are awaited before the first draw. A target change redraws only while concealed; there is no per-frame canvas redraw or texture replacement. The housing, clipped bevel, backplate, bracket, brace, narrow post, and dynamically compensated footing remain stable render-owned Three.js objects. + +## Measured browser matrix + +All recorded dwell frames reported a 2048×512 texture, 8× anisotropy, and `fontReadyWhenDrawn: true`. + +| Scenario | CSS face width | Renderer ratio | Source texels per rendered face pixel | Content separation | +| --- | ---: | ---: | ---: | --- | +| 390×844 portrait, device DPR 3 / High | 150.64px | 1.25 | 10.87 | pass | +| 844×390 short landscape, DPR 1 / High | 134.49px | 1 | 15.23 | active + adjacent pass | +| 768×1024 tablet, DPR 1 / High | 182.76px | 1 | 11.21 | pass | +| 1440×900 desktop, DPR 1 / High | 195.05px | 1 | 10.50 | pass | +| 1440×900 desktop, device DPR 2 / High | 195.06px | 1.5 | 7.00 | pass | +| 1440×900 desktop, DPR 1 / Low | 195.06px | 1 | 10.50 | pass | +| 2560×1440 ultrawide, DPR 1 / High | 312.09px | 1 | 6.56 | pass | + +The ratios above divide the 2,048 source texels by CSS face width × renderer ratio. They describe source sampling headroom; they are not an invented GPU-inspector measurement. + +## Evidence index + +- Before: [`before/profile-overlap-1280x720-dpr1.25.png`](before/profile-overlap-1280x720-dpr1.25.png) +- Desktop Profile/right-card separation: [`after/profile-desktop-1440x900-dpr1-high.png`](after/profile-desktop-1440x900-dpr1-high.png) +- Desktop Experience/left-card separation: [`after/experience-desktop-1440x900-dpr1-high.png`](after/experience-desktop-1440x900-dpr1-high.png) +- Responsive: [`after/profile-portrait-390x844-dpr3-high.png`](after/profile-portrait-390x844-dpr3-high.png), [`after/profile-short-844x390-dpr1-high.png`](after/profile-short-844x390-dpr1-high.png), [`after/profile-tablet-768x1024-dpr1-high.png`](after/profile-tablet-768x1024-dpr1-high.png), [`after/profile-ultrawide-2560x1440-dpr1-high.png`](after/profile-ultrawide-2560x1440-dpr1-high.png) +- DPR/quality: [`after/profile-desktop-1440x900-dpr2-high.png`](after/profile-desktop-1440x900-dpr2-high.png), [`after/profile-desktop-1440x900-dpr1-low-quality.png`](after/profile-desktop-1440x900-dpr1-low-quality.png), [`after/profile-desktop-1440x900-dpr1-high-quality.png`](after/profile-desktop-1440x900-dpr1-high-quality.png) +- Lifecycle: [`lifecycle/profile-anticipate.png`](lifecycle/profile-anticipate.png), [`lifecycle/profile-approach.png`](lifecycle/profile-approach.png), [`lifecycle/profile-dwell.png`](lifecycle/profile-dwell.png), [`lifecycle/profile-depart.png`](lifecycle/profile-depart.png) +- Adverse input: [`lifecycle/profile-reverse-during-approach.png`](lifecycle/profile-reverse-during-approach.png), [`lifecycle/profile-dwell-after-reverse.png`](lifecycle/profile-dwell-after-reverse.png), [`lifecycle/repeated-tiny-input-dwell.png`](lifecycle/repeated-tiny-input-dwell.png), [`lifecycle/rapid-multi-section-reconciliation.png`](lifecycle/rapid-multi-section-reconciliation.png) +- Visible resize: [`lifecycle/resize-visible-before.png`](lifecycle/resize-visible-before.png), [`lifecycle/resize-visible-after-844x390.png`](lifecycle/resize-visible-after-844x390.png) +- Suppressed modes: [`after/static-mode-1440x900.png`](after/static-mode-1440x900.png), [`after/reduced-motion-390x844-dpr3.png`](after/reduced-motion-390x844-dpr3.png) +- Machine-readable measurements: [`browser-matrix.json`](browser-matrix.json), [`lifecycle/trace.json`](lifecycle/trace.json), [`browser-console.json`](browser-console.json) + +The lifecycle trace records target, phase, progress, opacity, projected face bounds, active section, direction, responsive profile, texture state, renderer ratio, reverse handling, repeated-small-input dwell, rapid multi-section reconciliation, visible resize, and the captured departure frame. Its rapid reconciliation contains only `Selected builds`; the visible Profile label never mutates. + +Three.js DevTools was unavailable for the final local preview (`No Three.js app connected`), so no replacement draw-call or inspector-memory result is claimed here. Allocation and transfer impact are verified by the repository's authenticated runtime-budget and production-build harnesses instead. + +## Asset ownership + +No additional Blender asset was justified. The sign needs dynamic labels, route candidacy, lifecycle motion, responsive placement, and quality-tier integration; lightweight stable Three.js geometry preserves those controls without another GLB or asset transfer. The finite authored car remains Blender-owned and unchanged. Dynamic sign text and behavior, responsive placement, road, terrain, atmosphere, sun, camera, and motion remain Three.js-owned. diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/experience-desktop-1440x900-dpr1-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/experience-desktop-1440x900-dpr1-high.png new file mode 100644 index 0000000..366f01d Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/experience-desktop-1440x900-dpr1-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-high-quality.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-high-quality.png new file mode 100644 index 0000000..9481416 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-high-quality.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-high.png new file mode 100644 index 0000000..3ee7a8e Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-low-quality.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-low-quality.png new file mode 100644 index 0000000..2af268b Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr1-low-quality.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr2-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr2-high.png new file mode 100644 index 0000000..521bc42 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-desktop-1440x900-dpr2-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-portrait-390x844-dpr3-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-portrait-390x844-dpr3-high.png new file mode 100644 index 0000000..6c30e9d Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-portrait-390x844-dpr3-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-short-844x390-dpr1-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-short-844x390-dpr1-high.png new file mode 100644 index 0000000..884de30 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-short-844x390-dpr1-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-tablet-768x1024-dpr1-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-tablet-768x1024-dpr1-high.png new file mode 100644 index 0000000..6c6e69d Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-tablet-768x1024-dpr1-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-ultrawide-2560x1440-dpr1-high.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-ultrawide-2560x1440-dpr1-high.png new file mode 100644 index 0000000..5284577 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/profile-ultrawide-2560x1440-dpr1-high.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/reduced-motion-390x844-dpr3.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/reduced-motion-390x844-dpr3.png new file mode 100644 index 0000000..3c0e56a Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/reduced-motion-390x844-dpr3.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/static-mode-1440x900.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/static-mode-1440x900.png new file mode 100644 index 0000000..4ab4e72 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/after/static-mode-1440x900.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/before/profile-overlap-1280x720-dpr1.25.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/before/profile-overlap-1280x720-dpr1.25.png new file mode 100644 index 0000000..b77a8ce Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/before/profile-overlap-1280x720-dpr1.25.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/browser-console.json b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/browser-console.json new file mode 100644 index 0000000..3186889 --- /dev/null +++ b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/browser-console.json @@ -0,0 +1,3 @@ +{ + "errors": [] +} diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/browser-matrix.json b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/browser-matrix.json new file mode 100644 index 0000000..7c35a7e --- /dev/null +++ b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/browser-matrix.json @@ -0,0 +1,461 @@ +{ + "generatedAt": "2026-09-02T23:06:25.825Z", + "scenarios": [ + { + "name": "desktop Profile beside right panel", + "viewport": { + "width": 1440, + "height": 900 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 3817.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.97990319603, + "top": 421.80306615142877, + "right": 612.0313301500913, + "bottom": 469.98894347500817, + "width": 195.0514269540613, + "height": 48.185877323579405 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 777.59375, + "top": 681, + "right": 1382.390625, + "bottom": 1139.28125 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-desktop-1440x900-dpr1-high.png" + }, + { + "name": "desktop Experience beside left panel", + "viewport": { + "width": 1440, + "height": 900 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "experience", + "frame": { + "now": 3028.7, + "targetId": "experience", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "profile", + "direction": 0, + "visible": true, + "corridor": "right", + "viewportProfile": "wide", + "bounds": { + "left": 862.8430142691166, + "top": 422.07400076838144, + "right": 1055.992547838176, + "bottom": 469.2669788703455, + "width": 193.14953356905949, + "height": 47.19297810196406 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 57.59375, + "top": 680.28125, + "right": 719.984375, + "bottom": 1830.875 + }, + "separatedFromPanel": true, + "screenshot": "after/experience-desktop-1440x900-dpr1-high.png" + }, + { + "name": "portrait Profile", + "viewport": { + "width": 390, + "height": 844 + }, + "deviceScaleFactor": 3, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 2975.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "tall", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 45.214151854185765, + "top": 393.78359453769036, + "right": 195.85026402457592, + "bottom": 431.69356317041803, + "width": 150.63611217039016, + "height": 37.90996863272767 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1.25 + }, + "panel": { + "left": 16, + "top": 651, + "right": 374, + "bottom": 1187.109375 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-portrait-390x844-dpr3-high.png" + }, + { + "name": "short landscape Profile", + "viewport": { + "width": 844, + "height": 390 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 2876.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "short", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 299.66361248256146, + "top": 191.1365135995139, + "right": 434.1535837811257, + "bottom": 225.4414949799874, + "width": 134.48997129856423, + "height": 34.30498138047349 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 303.828125, + "top": 286.03125, + "right": 810.21875, + "bottom": 700.40625 + }, + "adjacentPanel": { + "left": 33.75, + "top": -138, + "right": 523.265625, + "bottom": 174.03125 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-short-844x390-dpr1-high.png" + }, + { + "name": "tablet Profile", + "viewport": { + "width": 768, + "height": 1024 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 2441.6, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "tall", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 202.26939254304233, + "top": 477.7671107266057, + "right": 385.03157389039734, + "bottom": 523.7621386015365, + "width": 182.762181347355, + "height": 45.99502787493083 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 276.46875, + "top": 792.828125, + "right": 737.265625, + "bottom": 1254.640625 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-tablet-768x1024-dpr1-high.png" + }, + { + "name": "ultrawide Profile", + "viewport": { + "width": 2560, + "height": 1440 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 5781.4, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 795.1606507914319, + "top": 674.7812815235544, + "right": 1107.2526954957643, + "bottom": 751.8792940365541, + "width": 312.09204470433247, + "height": 77.09801251299973 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 1382.390625, + "top": 1107, + "right": 2457.578125, + "bottom": 1510.15625 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-ultrawide-2560x1440-dpr1-high.png" + }, + { + "name": "desktop high DPR Profile", + "viewport": { + "width": 1440, + "height": 900 + }, + "deviceScaleFactor": 2, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 4315.7, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.97709613178563, + "top": 421.7626066451545, + "right": 612.0323349380212, + "bottom": 469.94872191078514, + "width": 195.0552388062356, + "height": 48.18611526563063 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1.5 + }, + "panel": { + "left": 777.59375, + "top": 681, + "right": 1382.390625, + "bottom": 1139.28125 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-desktop-1440x900-dpr2-high.png" + }, + { + "name": "desktop low quality", + "viewport": { + "width": 1440, + "height": 900 + }, + "deviceScaleFactor": 1, + "quality": "low", + "targetId": "profile", + "frame": { + "now": 2293.1, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.975431797155, + "top": 421.73866114795436, + "right": 612.032925818174, + "bottom": 469.92491685811837, + "width": 195.057494021019, + "height": 48.186255710164005 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 777.59375, + "top": 681, + "right": 1382.390625, + "bottom": 1139.28125 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-desktop-1440x900-dpr1-low-quality.png" + }, + { + "name": "desktop high quality", + "viewport": { + "width": 1440, + "height": 900 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 3370.4, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.9769832458706, + "top": 421.76098148986165, + "right": 612.0323751296199, + "bottom": 469.9471062962213, + "width": 195.05539188374934, + "height": 48.18612480635966 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 777.59375, + "top": 681, + "right": 1382.390625, + "bottom": 1139.28125 + }, + "separatedFromPanel": true, + "screenshot": "after/profile-desktop-1440x900-dpr1-high-quality.png" + }, + { + "name": "resize before visible phase", + "viewport": { + "width": 1440, + "height": 900 + }, + "deviceScaleFactor": 1, + "quality": "high", + "targetId": "profile", + "frame": { + "now": 4038.1, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.9840276279932, + "top": 421.8626810680434, + "right": 612.0298349796249, + "bottom": 470.0482063315882, + "width": 195.04580735163165, + "height": 48.185525263544775 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "panel": { + "left": 777.59375, + "top": 681, + "right": 1382.390625, + "bottom": 1139.28125 + }, + "separatedFromPanel": true, + "screenshot": "lifecycle/resize-visible-before.png" + } + ] +} diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-anticipate.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-anticipate.png new file mode 100644 index 0000000..8baed9c Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-anticipate.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-approach.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-approach.png new file mode 100644 index 0000000..45f7483 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-approach.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-depart.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-depart.png new file mode 100644 index 0000000..f43a746 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-depart.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-dwell-after-reverse.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-dwell-after-reverse.png new file mode 100644 index 0000000..8ee8e12 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-dwell-after-reverse.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-dwell.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-dwell.png new file mode 100644 index 0000000..b0c67f5 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-dwell.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-reverse-during-approach.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-reverse-during-approach.png new file mode 100644 index 0000000..e003c28 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/profile-reverse-during-approach.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/rapid-multi-section-reconciliation.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/rapid-multi-section-reconciliation.png new file mode 100644 index 0000000..f451443 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/rapid-multi-section-reconciliation.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/repeated-tiny-input-dwell.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/repeated-tiny-input-dwell.png new file mode 100644 index 0000000..0c64d58 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/repeated-tiny-input-dwell.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/resize-visible-after-844x390.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/resize-visible-after-844x390.png new file mode 100644 index 0000000..32ee355 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/resize-visible-after-844x390.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/resize-visible-before.png b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/resize-visible-before.png new file mode 100644 index 0000000..9ca9496 Binary files /dev/null and b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/resize-visible-before.png differ diff --git a/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/trace.json b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/trace.json new file mode 100644 index 0000000..2b7827e --- /dev/null +++ b/docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/lifecycle/trace.json @@ -0,0 +1,3072 @@ +{ + "targetId": "profile", + "dwellFirstObservedAt": 3031, + "frames": [ + { + "now": 1831, + "targetId": "profile", + "phase": "anticipate", + "progress": 0, + "opacity": 0, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 1, + "visible": false, + "corridor": "left", + "bounds": { + "left": 671.1903698130607, + "top": 437.1148386926221, + "right": 677.37309131101, + "bottom": 438.68508590529194, + "width": 6.182721497949274, + "height": 1.5702472126698694 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1947.7, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.3333333333333333, + "opacity": 0, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "bounds": { + "left": 671.1907483216276, + "top": 437.1124209161359, + "right": 677.3734252734644, + "bottom": 438.68249967377534, + "width": 6.182676951836811, + "height": 1.5700787576394646 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2064.4, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.7223333333333335, + "opacity": 0, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "bounds": { + "left": 671.1909683751229, + "top": 437.11101563342, + "right": 677.3736194329961, + "bottom": 438.680996478889, + "width": 6.182651057873159, + "height": 1.569980845469047 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2164.3, + "targetId": "profile", + "phase": "approach", + "progress": 0.03711111111111121, + "opacity": 0.022042626949096655, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 670.9830613774552, + "top": 437.0982084422851, + "right": 677.3169759188186, + "bottom": 438.7065294727903, + "width": 6.333914541363356, + "height": 1.6083210305051807 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2381, + "targetId": "profile", + "phase": "approach", + "progress": 0.1481111111111113, + "opacity": 0.285367372896859, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 667.9883632632608, + "top": 436.9178541577212, + "right": 676.5013170702402, + "bottom": 439.0784790989404, + "width": 8.512953806979453, + "height": 2.1606249412192255 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2497.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.3888888888888889, + "opacity": 0.9843519788650105, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 647.5715194631781, + "top": 435.6881056846114, + "right": 670.9716705950207, + "bottom": 441.6073193366265, + "width": 23.400151131842563, + "height": 5.919213652015117 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2597.7, + "targetId": "profile", + "phase": "approach", + "progress": 0.5184444444444444, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 624.3221673056124, + "top": 434.2972003650973, + "right": 664.7395090708287, + "bottom": 444.48276173370573, + "width": 40.41734176521629, + "height": 10.185561368608433 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2714.3, + "targetId": "profile", + "phase": "approach", + "progress": 0.6296666666666665, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 592.2143461789243, + "top": 432.3877110645775, + "right": 656.2440237853959, + "bottom": 448.44580422132395, + "width": 64.02967760647164, + "height": 16.05809315674645 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2814.3, + "targetId": "profile", + "phase": "approach", + "progress": 0.7592222222222225, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 533.3992271862137, + "top": 428.84324519349866, + "right": 641.0075224306049, + "bottom": 455.6902256747483, + "width": 107.60829524439123, + "height": 26.84698048124966 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2931, + "targetId": "profile", + "phase": "approach", + "progress": 0.8703333333333335, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 465.9703299541794, + "top": 424.764253815125, + "right": 624.0389359798723, + "bottom": 463.9731774170383, + "width": 158.06860602569293, + "height": 39.208923601913284 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3031, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.97859641924333, + "top": 421.78421953518216, + "right": 612.0317991962395, + "bottom": 469.970207795606, + "width": 195.05320277699616, + "height": 48.18598826042381 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3147.7, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.97709744520745, + "top": 421.76262555462296, + "right": 612.0323344702962, + "bottom": 469.94874070923504, + "width": 195.05523702508873, + "height": 48.18611515461208 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3264.3, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9760117402434, + "top": 421.747001388727, + "right": 612.0327203340534, + "bottom": 469.93320821388323, + "width": 195.05670859380996, + "height": 48.186206825156205 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3364.3, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.975431618753, + "top": 421.7386585829298, + "right": 612.0329258813176, + "bottom": 469.92491430812294, + "width": 195.0574942625646, + "height": 48.18625572519312 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3480.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.97540648788066, + "top": 421.7382972605964, + "right": 612.0329347757233, + "bottom": 469.9245551028509, + "width": 195.05752828784267, + "height": 48.18625784225446 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3597.6, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9759384246379, + "top": 421.7459468116359, + "right": 612.0327463352709, + "bottom": 469.9321598199166, + "width": 195.05680791063298, + "height": 48.186213008280674 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3714.3, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9769819728865, + "top": 421.7609631642673, + "right": 612.0323755827548, + "bottom": 469.9470880782028, + "width": 195.05539360986825, + "height": 48.18612491393549 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3831, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.97844817631403, + "top": 421.7820828039612, + "right": 612.0318522640047, + "bottom": 469.96808363081567, + "width": 195.05340408769064, + "height": 48.18600082685447 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3947.6, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9802124330006, + "top": 421.8075289575089, + "right": 612.0312188259885, + "bottom": 469.99337998612356, + "width": 195.05100639298792, + "height": 48.18585102861465 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4047.6, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.98212538466424, + "top": 421.83516098878505, + "right": 612.03052736805, + "bottom": 470.02084899098475, + "width": 195.04840198338576, + "height": 48.1856880021997 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4164.3, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9840257879638, + "top": 421.8626544273715, + "right": 612.0298356516888, + "bottom": 470.0481798486345, + "width": 195.04580986372503, + "height": 48.18552542126298 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4281, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9857541824854, + "top": 421.8876964792042, + "right": 612.0292023758129, + "bottom": 470.0730734925845, + "width": 195.04344819332755, + "height": 48.18537701338033 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4381, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.98716617047444, + "top": 421.90818056802453, + "right": 612.0286820710473, + "bottom": 470.0934359565739, + "width": 195.0415159005729, + "height": 48.185255388549365 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4497.6, + "targetId": "profile", + "phase": "depart", + "progress": 0, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 416.9881442348465, + "top": 421.9223835385011, + "right": 612.0283200981097, + "bottom": 470.1075544755434, + "width": 195.0401758632632, + "height": 48.1851709370423 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4614.3, + "targetId": "profile", + "phase": "depart", + "progress": 0.1665714285714291, + "opacity": 0.9843954976144482, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 417.2929553288712, + "top": 421.9107717833033, + "right": 598.5975422370246, + "bottom": 466.5168151852199, + "width": 181.30458690815345, + "height": 44.60604340191662 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4730.9, + "targetId": "profile", + "phase": "depart", + "progress": 0.3332857142857145, + "opacity": 0.8332672824468997, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 417.911597890741, + "top": 421.87268893920384, + "right": 570.926811655347, + "bottom": 459.1996829188253, + "width": 153.01521376460602, + "height": 37.32699397962148 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4847.6, + "targetId": "profile", + "phase": "depart", + "progress": 0.49985714285714233, + "opacity": 0.5832255451895052, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 418.5036837354567, + "top": 421.8339972921179, + "right": 543.9506185332182, + "bottom": 452.18502410669555, + "width": 125.44693479776146, + "height": 30.351026814577665 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4964.2, + "targetId": "profile", + "phase": "depart", + "progress": 0.666571428571429, + "opacity": 0.31006017665478824, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 418.9425737550785, + "top": 421.8066439134676, + "right": 523.6238914568287, + "bottom": 446.9779829215244, + "width": 104.68131770175017, + "height": 25.171339008056805 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 5080.9, + "targetId": "profile", + "phase": "depart", + "progress": 0.8331428571428569, + "opacity": 0.09037103558930937, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.209100714309, + "top": 421.79227145432026, + "right": 511.118014694258, + "bottom": 443.8090767020726, + "width": 91.90891397994903, + "height": 22.01680524775236 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 5197.6, + "targetId": "profile", + "phase": "depart", + "progress": 0.9998571428571423, + "opacity": 7.557779135858311e-8, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.3018391784916, + "top": 421.7880731869931, + "right": 506.6930609033512, + "bottom": 442.6946234138454, + "width": 87.39122172485958, + "height": 20.906550226852346 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } + ], + "reverseDuringDwell": [ + { + "now": 931.9, + "targetId": "profile", + "phase": "anticipate", + "progress": 0, + "opacity": 0, + "activeSectionId": "start", + "direction": 1, + "visible": false, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 671.1893525421453, + "top": 437.1213403208575, + "right": 677.3721938022838, + "bottom": 438.6920405124378, + "width": 6.1828412601385025, + "height": 1.570700191580329 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1081.9, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.4443333333333332, + "opacity": 0, + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 671.189881605934, + "top": 437.11795827659273, + "right": 677.3726605724944, + "bottom": 438.68842283797625, + "width": 6.182778966560363, + "height": 1.5704645613835169 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1182, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.9443333333333336, + "opacity": 0, + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 671.1903698130607, + "top": 437.1148386926221, + "right": 677.37309131101, + "bottom": 438.68508590529194, + "width": 6.182721497949274, + "height": 1.5702472126698694 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1348.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.09266666666666665, + "opacity": 0.12455839102248775, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 669.9230391209551, + "top": 437.0355671392349, + "right": 677.0280101002622, + "bottom": 438.8394732346223, + "width": 7.104970979307154, + "height": 1.80390609538739 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1548.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.2777777777777777, + "opacity": 0.7336583685230137, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 659.7707176557573, + "top": 436.4208752275615, + "right": 674.2693671353741, + "bottom": 440.0954681249566, + "width": 14.498649479616802, + "height": 3.6745928973950868 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1665.3, + "targetId": "profile", + "phase": "approach", + "progress": 0.4999999999999999, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 628.4092762389746, + "top": 434.5442778660982, + "right": 665.8303189701605, + "bottom": 443.97870417248214, + "width": 37.421042731185935, + "height": 9.434426306383955 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1831.9, + "targetId": "profile", + "phase": "approach", + "progress": 0.6296666666666666, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 592.2164833128992, + "top": 432.3989851812343, + "right": 656.2443532404476, + "bottom": 448.45704052040554, + "width": 64.02786992754841, + "height": 16.058055339171233 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1948.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.8147777777777779, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 500.58716676608555, + "top": 426.9129774703503, + "right": 632.6829631584179, + "bottom": 459.77701565110203, + "width": 132.09579639233237, + "height": 32.86403818075172 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2098.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.9444444444444444, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 427.8066285550828, + "top": 422.538493513257, + "right": 614.6588664957585, + "bottom": 468.7414136969572, + "width": 186.8522379406757, + "height": 46.2029201837002 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2231.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.9841962982466, + "top": 421.8651233125276, + "right": 612.0297733543197, + "bottom": 470.0506341160075, + "width": 195.04557705607306, + "height": 48.18551080347987 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2381.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.98230060383014, + "top": 421.83769412983105, + "right": 612.0304637913953, + "bottom": 470.0233671679684, + "width": 195.04816318756514, + "height": 48.18567303813734 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2531.8, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.9803797537438, + "top": 421.809944142568, + "right": 612.0311585386738, + "bottom": 469.9957809367725, + "width": 195.05077878493, + "height": 48.18583679420453 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2648.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.9785947493282, + "top": 421.7841954640752, + "right": 612.0317997941935, + "bottom": 469.9701838660772, + "width": 195.0532050448653, + "height": 48.18598840200201 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2815.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 416.97709613178563, + "top": 421.7626066451545, + "right": 612.0323349380212, + "bottom": 469.94872191078514, + "width": 195.0552388062356, + "height": 48.18611526563063 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2931.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": -1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.4079190469438, + "top": 421.81908191847043, + "right": 612.1957759555936, + "bottom": 469.93879144596616, + "width": 194.78785690864981, + "height": 48.11970952749573 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3098.6, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.9181593971397, + "top": 421.9007818642459, + "right": 612.3908601725545, + "bottom": 469.9423625725585, + "width": 194.47270077541486, + "height": 48.04158070831261 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3215.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 418.34004385172653, + "top": 421.978209048876, + "right": 612.5523222262453, + "bottom": 469.95536896718124, + "width": 194.2122783745188, + "height": 47.97715991830523 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3365.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 418.63821921955895, + "top": 422.04422599633375, + "right": 612.6662030445774, + "bottom": 469.9759779243101, + "width": 194.02798382501845, + "height": 47.931751927976336 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3498.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 418.81650197118574, + "top": 422.09773961845775, + "right": 612.7338228183786, + "bottom": 470.0024591668771, + "width": 193.91732084719285, + "height": 47.90471954841934 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3648.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 418.91283534570033, + "top": 422.1430818654804, + "right": 612.7697520230464, + "bottom": 470.0333208565338, + "width": 193.85691667734608, + "height": 47.89023899105342 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3815.2, + "targetId": "profile", + "phase": "depart", + "progress": 0, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 418.9511703424436, + "top": 422.18148393865295, + "right": 612.7832704931112, + "bottom": 470.0661117057937, + "width": 193.8321001506676, + "height": 47.88462776714073 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } + ], + "reverseDuringApproach": [ + { + "now": 1826.9, + "targetId": "profile", + "phase": "anticipate", + "progress": 0, + "opacity": 0, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 1, + "visible": false, + "corridor": "left", + "bounds": { + "left": 671.1903693143604, + "top": 437.11484187862794, + "right": 677.3730908710064, + "bottom": 438.68508931327636, + "width": 6.182721556646015, + "height": 1.5702474346484223 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1926.9, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.3890000000000002, + "opacity": 0, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "bounds": { + "left": 671.1907479821303, + "top": 437.11242308439125, + "right": 677.3734249739183, + "bottom": 438.68250199310177, + "width": 6.182676991787957, + "height": 1.5700789087105136 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2010.3, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.7223333333333335, + "opacity": 0, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "bounds": { + "left": 671.190945773132, + "top": 437.11115996039274, + "right": 677.37359949048, + "bottom": 438.6811508617901, + "width": 6.182653717348103, + "height": 1.5699909013973752 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2110.3, + "targetId": "profile", + "phase": "approach", + "progress": 0.00011111111111101006, + "opacity": 2.0992349734508019e-7, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 671.1910084500408, + "top": 437.1107474586091, + "right": 677.3736559541823, + "bottom": 438.6807099784994, + "width": 6.182647504141414, + "height": 1.5699625198902822 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2293.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.11122222222222237, + "opacity": 0.17323938169994454, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 669.3736680124555, + "top": 437.00148826299016, + "right": 676.8784637341063, + "bottom": 438.90666421151445, + "width": 7.504795721650794, + "height": 1.9051759485242883 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2393.5, + "targetId": "profile", + "phase": "approach", + "progress": 0.3148888888888887, + "opacity": 0.8434522760091469, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 656.31231278833, + "top": 436.21380942351215, + "right": 673.332420166488, + "bottom": 440.5253989918913, + "width": 17.020107378157945, + "height": 4.311589568379134 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2493.5, + "targetId": "profile", + "phase": "approach", + "progress": 0.4258888888888888, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 642.1083803512073, + "top": 435.36033707520807, + "right": 669.5009865691351, + "bottom": 442.2838677736304, + "width": 27.392606217927778, + "height": 6.923530698422326 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2593.6, + "targetId": "profile", + "phase": "approach", + "progress": 0.5369999999999999, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 619.8970770409117, + "top": 434.0312477079627, + "right": 663.5610024350055, + "bottom": 445.0283335075434, + "width": 43.663925394093894, + "height": 10.997085799580702 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2710.3, + "targetId": "profile", + "phase": "approach", + "progress": 0.648222222222222, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 585.3637331617608, + "top": 431.97130169167235, + "right": 654.4479589180193, + "bottom": 449.28622946187085, + "width": 69.08422575625843, + "height": 17.314927770198494 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2810.2, + "targetId": "profile", + "phase": "approach", + "progress": 0.7778888888888891, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 522.7886027679796, + "top": 428.19295130644457, + "right": 638.3029063817804, + "bottom": 456.98558146215123, + "width": 115.51430361380085, + "height": 28.79263015570666 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 2910.2, + "targetId": "profile", + "phase": "approach", + "progress": 0.8888888888888886, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 454.93041052889134, + "top": 424.0807200419932, + "right": 621.3105507722115, + "bottom": 465.31279208169343, + "width": 166.38014024332017, + "height": 41.232072039700256 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3026.9, + "targetId": "profile", + "phase": "approach", + "progress": 0.9999999999999998, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": -1, + "visible": true, + "corridor": "left", + "bounds": { + "left": 417.58678264743077, + "top": 421.866993313606, + "right": 612.2623559825164, + "bottom": 469.95924930531714, + "width": 194.67557333508563, + "height": 48.09225599171117 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3143.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 418.30559388532424, + "top": 421.97549015398613, + "right": 612.5374845629046, + "bottom": 469.9576392953458, + "width": 194.2318906775804, + "height": 47.982149141359685 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3260.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 418.92157425748394, + "top": 422.0774001537731, + "right": 612.7737881715844, + "bottom": 469.96543184115484, + "width": 193.8522139141005, + "height": 47.888031687381726 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3360.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.3667876572977, + "top": 422.1610625464937, + "right": 612.9445605841591, + "bottom": 469.9812142553996, + "width": 193.57777292686143, + "height": 47.82015170890588 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3476.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.64680558944184, + "top": 422.2265780803868, + "right": 613.0516642484372, + "bottom": 470.0041698803606, + "width": 193.40485865899535, + "height": 47.777591799973834 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3593.6, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.8064089542614, + "top": 422.2799496057928, + "right": 613.1122170262157, + "bottom": 470.0334261257187, + "width": 193.3058080719543, + "height": 47.75347651992587 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3693.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.8823239147317, + "top": 422.3246973906083, + "right": 613.1403706231341, + "bottom": 470.0668655891519, + "width": 193.2580467084024, + "height": 47.742168198543595 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3810.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.9005837017586, + "top": 422.36196856824523, + "right": 613.1462427076008, + "bottom": 470.1016360000148, + "width": 193.24565900584219, + "height": 47.73966743176959 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 3926.8, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.8768926193578, + "top": 422.3907668312082, + "right": 613.1360264909624, + "bottom": 470.13432100467145, + "width": 193.2591338716046, + "height": 47.743554173463224 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4043.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.8224543299678, + "top": 422.4096131828636, + "right": 613.1141895494163, + "bottom": 470.16171350757173, + "width": 193.29173521944853, + "height": 47.752100324708124 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4160.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.74542628227135, + "top": 422.4171213707468, + "right": 613.0840075107715, + "bottom": 470.18116901760555, + "width": 193.33858122850012, + "height": 47.76404764685873 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4276.9, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.65192328409745, + "top": 422.412430943298, + "right": 613.0479309531419, + "bottom": 470.19088567405436, + "width": 193.39600766904442, + "height": 47.77845473075638 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4393.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.54664930622766, + "top": 422.3954592845458, + "right": 613.0078172569233, + "bottom": 470.19005977444135, + "width": 193.46116795069565, + "height": 47.79460048989557 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4510.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.43331064241113, + "top": 422.36700063451764, + "right": 612.9650858906556, + "bottom": 470.1789214431, + "width": 193.53177524824446, + "height": 47.811920808582386 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4626.9, + "targetId": "profile", + "phase": "depart", + "progress": 0, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.3148716903272, + "top": 422.3286937752194, + "right": 612.9208198412017, + "bottom": 470.1586627914094, + "width": 193.6059481508745, + "height": 47.829969016190034 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 4743.5, + "targetId": "profile", + "phase": "depart", + "progress": 0.16671428571428545, + "opacity": 0.9843301980267711, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 419.5254669544736, + "top": 422.27160775780595, + "right": 599.5297081927516, + "bottom": 466.5561301458065, + "width": 180.00424123827804, + "height": 44.28452238800054 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } + ], + "repeatedTinyInput": [ + { + "now": 18474.2, + "targetId": "profile", + "phase": "anticipate", + "progress": 0, + "opacity": 0, + "activeSectionId": "start", + "direction": 1, + "visible": false, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 671.3213994971967, + "top": 437.2064847437303, + "right": 677.488785579316, + "bottom": 438.7730984402309, + "width": 6.167386082119265, + "height": 1.5666136965006103 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 18607.6, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.5, + "opacity": 0, + "activeSectionId": "start", + "direction": 0, + "visible": false, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 671.3222496635818, + "top": 437.20342828449606, + "right": 677.4895770715596, + "bottom": 438.7698147187421, + "width": 6.167327407977723, + "height": 1.5663864342460556 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 18757.5, + "targetId": "profile", + "phase": "anticipate", + "progress": 0.9446666666666594, + "opacity": 0, + "activeSectionId": "start", + "direction": 1, + "visible": false, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 671.3222419998115, + "top": 437.2014775554174, + "right": 677.4895334400512, + "bottom": 438.76772457715475, + "width": 6.167291440239751, + "height": 1.5662470217373539 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 18924.2, + "targetId": "profile", + "phase": "approach", + "progress": 0.148566007380685, + "opacity": 0.286851932453083, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 668.1086994140715, + "top": 437.00593234335753, + "right": 676.6146922317268, + "bottom": 439.1648690558585, + "width": 8.505992817655397, + "height": 2.1589367125009744 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19107.5, + "targetId": "profile", + "phase": "approach", + "progress": 0.3343571058830138, + "opacity": 0.8922173913037398, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 654.4381990534582, + "top": 436.18134701288494, + "right": 672.9058998270351, + "bottom": 440.8580199297091, + "width": 18.467700773576894, + "height": 4.676672916824145 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19240.8, + "targetId": "profile", + "phase": "approach", + "progress": 0.5386492975775344, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 619.7404778228579, + "top": 434.10863437105303, + "right": 663.6005113376139, + "bottom": 445.1521699357342, + "width": 43.86003351475597, + "height": 11.043535564681179 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19374.2, + "targetId": "profile", + "phase": "approach", + "progress": 0.6872153049582195, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 569.6784948371998, + "top": 431.12479192468504, + "right": 650.4395748764739, + "bottom": 451.337821823113, + "width": 80.76108003927413, + "height": 20.213029898427976 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19524.1, + "targetId": "profile", + "phase": "approach", + "progress": 0.8358927647075346, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 487.91991475018887, + "top": 426.2102377465587, + "right": 629.5924087742484, + "bottom": 461.4165073901034, + "width": 141.67249402405952, + "height": 35.206269643544715 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19657.4, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.6169023790239, + "top": 421.96278739199994, + "right": 612.2782452938375, + "bottom": 470.0526532140833, + "width": 194.6613429148136, + "height": 48.08986582208337 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19807.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.6067100267858, + "top": 421.93216409278085, + "right": 612.2756696627225, + "bottom": 470.0234161827052, + "width": 194.66895963593674, + "height": 48.09125208992435 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 19924.1, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5989567321147, + "top": 421.9042144175956, + "right": 612.2741411984281, + "bottom": 469.9965356832875, + "width": 194.6751844663134, + "height": 48.09232126569191 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20090.8, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5902830525748, + "top": 421.8801021044994, + "right": 612.2718286041624, + "bottom": 469.9736080052955, + "width": 194.68154555158753, + "height": 48.09350590079612 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20190.8, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.58305069149696, + "top": 421.86280653320114, + "right": 612.2699279300717, + "bottom": 469.95734762718536, + "width": 194.68687723857477, + "height": 48.09454109398422 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20357.5, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5749130472187, + "top": 421.852765707697, + "right": 612.2670866843141, + "bottom": 469.9484747770481, + "width": 194.69217363709544, + "height": 48.095709069351074 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20474.2, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5687841126673, + "top": 421.8518065316867, + "right": 612.2647710574771, + "bottom": 469.9484598192206, + "width": 194.69598694480982, + "height": 48.09665328753391 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20624.1, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5617420753833, + "top": 421.8588107193472, + "right": 612.2614800218641, + "bottom": 469.9565466915269, + "width": 194.69973794648087, + "height": 48.09773597217969 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20740.8, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.56370101909147, + "top": 421.8749725514057, + "right": 612.2613229808272, + "bottom": 469.9724694621413, + "width": 194.69762196173576, + "height": 48.09749691073563 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 20890.8, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5546322086364, + "top": 421.8959907741456, + "right": 612.2567016154707, + "bottom": 469.9949852645171, + "width": 194.70206940683426, + "height": 48.09899449037147 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21057.4, + "targetId": "profile", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.5567568282064, + "top": 421.92304655195477, + "right": 612.256196537102, + "bottom": 470.0218574127212, + "width": 194.69943970889562, + "height": 48.098810860766434 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21174.1, + "targetId": "profile", + "phase": "depart", + "progress": 0, + "opacity": 1, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 417.559287887759, + "top": 421.95143479943425, + "right": 612.2554529661433, + "bottom": 470.0499417210186, + "width": 194.69616507838424, + "height": 48.098506921584374 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21324.1, + "targetId": "profile", + "phase": "depart", + "progress": 0.16671428571428157, + "opacity": 0.9843301980267729, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 380.2499378626167, + "top": 419.830101849839, + "right": 596.5344824251388, + "bottom": 472.98873935200874, + "width": 216.28454456252206, + "height": 53.15863750216971 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21440.7, + "targetId": "profile", + "phase": "depart", + "progress": 0.38099999999999584, + "opacity": 0.7684243923182501, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 169.8664492885792, + "top": 407.75199758543874, + "right": 511.38761624652994, + "bottom": 489.42257121850366, + "width": 341.52116695795075, + "height": 81.67057363306492 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21607.4, + "targetId": "profile", + "phase": "depart", + "progress": 0.5475714285714275, + "opacity": 0.5040475797510087, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": -462.9222866810494, + "top": 371.42289722723075, + "right": 286.5507543923785, + "bottom": 538.6459116114337, + "width": 749.4730410734279, + "height": 167.22301438420294 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21724.1, + "targetId": "profile", + "phase": "depart", + "progress": 0.7857142857142857, + "opacity": 0.14307310225677583, + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 5110.016855655407, + "top": -975.0849677965864, + "right": 9595.883019404495, + "bottom": 1439.4834401450216, + "width": 4485.866163749088, + "height": 2414.568407941608 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 21890.7, + "targetId": "profile", + "phase": "depart", + "progress": 0.9524285714285673, + "opacity": 0.008086279883383285, + "activeSectionId": "start", + "direction": 1, + "visible": true, + "corridor": "left", + "viewportProfile": "wide", + "bounds": { + "left": 1900.83302816385, + "top": 258.70966729490567, + "right": 2467.352317272834, + "bottom": 581.7062430851961, + "width": 566.5192891089841, + "height": 322.9965757902904 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } + ], + "rapidJump": { + "labelsDrawn": [ + "Selected builds" + ], + "frames": [ + { + "now": 286.83, + "targetId": "selected-builds", + "phase": "anticipate", + "progress": 0, + "opacity": 0, + "activeSectionId": "selected-builds", + "direction": 1, + "visible": false, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 775.7004894283019, + "top": 436.09066590120074, + "right": 781.5266327728575, + "bottom": 437.5547829649269, + "width": 5.8261433445555895, + "height": 1.464117063726178 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 853.4, + "targetId": "selected-builds", + "phase": "approach", + "progress": 0.3175840362691505, + "opacity": 0.850614280663091, + "activeSectionId": "selected-builds", + "direction": 0, + "visible": true, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 789.8479543272683, + "top": 434.704233694568, + "right": 806.7066375290394, + "bottom": 438.9177922902411, + "width": 16.8586832017711, + "height": 4.213558595673078 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1086.8, + "targetId": "selected-builds", + "phase": "approach", + "progress": 0.5956502630209222, + "opacity": 1, + "activeSectionId": "selected-builds", + "direction": 0, + "visible": true, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 836.5214148426758, + "top": 429.6557871099795, + "right": 890.4039961439952, + "bottom": 442.9207840514762, + "width": 53.88258130131942, + "height": 13.26499694149669 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1203.4, + "targetId": "selected-builds", + "phase": "approach", + "progress": 0.7345642391959719, + "opacity": 1, + "activeSectionId": "selected-builds", + "direction": 0, + "visible": true, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 882.6291198361448, + "top": 424.549558934118, + "right": 974.6992115981232, + "bottom": 446.9301347872729, + "width": 92.07009176197835, + "height": 22.38057585315488 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1320.2, + "targetId": "selected-builds", + "phase": "approach", + "progress": 0.8737164897726939, + "opacity": 1, + "activeSectionId": "selected-builds", + "direction": 0, + "visible": true, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 943.069289335527, + "top": 417.5949157899856, + "right": 1087.657813834245, + "bottom": 452.1703363951296, + "width": 144.58852449871802, + "height": 34.57542060514402 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1436.8, + "targetId": "selected-builds", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "selected-builds", + "direction": 0, + "visible": true, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 974.1407600087241, + "top": 413.95119017011257, + "right": 1146.926084571791, + "bottom": 454.923575042987, + "width": 172.78532456306698, + "height": 40.97238487287444 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + { + "now": 1553.5, + "targetId": "selected-builds", + "phase": "dwell", + "progress": 1, + "opacity": 1, + "activeSectionId": "selected-builds", + "direction": 0, + "visible": true, + "corridor": "outer-right", + "viewportProfile": "wide", + "bounds": { + "left": 973.8481938163675, + "top": 413.99628130300283, + "right": 1146.4285627297875, + "bottom": 454.9197599667291, + "width": 172.58036891342, + "height": 40.92347866372626 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } + ] + }, + "resize": { + "before": { + "now": 1666.4, + "targetId": "profile", + "phase": "approach", + "progress": 0.1666666666666668, + "opacity": 0.34743468227973184, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 667.141288653538, + "top": 436.86659110380236, + "right": 676.2708392019488, + "bottom": 439.1833792093745, + "width": 9.129550548410748, + "height": 2.3167881055721296 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + }, + "after": { + "now": 1949.7, + "targetId": "profile", + "phase": "approach", + "progress": 0.7036666666666668, + "opacity": 1, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 353.46399177086346, + "top": 186.5731026396684, + "right": 390.94026571363025, + "bottom": 195.94631581048841, + "width": 37.476273942766795, + "height": 9.373213170820009 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } + }, + "departureCaptureFrame": { + "now": 4901.6, + "targetId": "profile", + "phase": "depart", + "progress": 0.30942857142857066, + "opacity": 0.8627549100449124, + "viewportProfile": "wide", + "activeSectionId": "start", + "direction": 0, + "visible": true, + "corridor": "left", + "bounds": { + "left": 417.8175603865608, + "top": 421.861593251323, + "right": 575.0895934446095, + "bottom": 460.27561468028296, + "width": 157.2720330580487, + "height": 38.414021428959984 + }, + "texture": { + "width": 2048, + "height": 512, + "anisotropy": 8, + "fontReadyWhenDrawn": true + }, + "rendererPixelRatio": 1 + } +} diff --git a/docs/reviews/visual-qa.md b/docs/reviews/visual-qa.md index 926a0d2..44e5a85 100644 --- a/docs/reviews/visual-qa.md +++ b/docs/reviews/visual-qa.md @@ -108,6 +108,33 @@ Fresh verification produced these results: The unchanged 20-second interaction harness still does not yield a valid long trace on this host, so no interaction latency or frame-cadence result is claimed. Its independent mobile-low run loaded content under the prescribed throttle but stayed in `data-scene-state="loading"` beyond the fixed five-second scene-ready window. Its independent desktop-high run again reached the Toolkit CDP action but exceeded the fixed 75-second test timeout, matching the previously documented host behavior. Neither assertion or budget was weakened. The authenticated allocation trace, deterministic live-uniform trace, responsive screenshots, and browser assertions are the measured evidence for this pass; physical-browser, cross-engine, and audience checks remain outstanding below. +## Nightdrive sign reliability and legibility pass + +The 2026-09-03 sign-only continuation is retained under `docs/reviews/evidence/nightdrive-iteration/sign-reliability-pass/`. Its [`README.md`](evidence/nightdrive-iteration/sign-reliability-pass/README.md) records the exact per-route candidacy/corridor mapping, lifecycle contract, responsive pose families, texture contract, measured face sizes, and evidence index. The supplied Profile-overlap scenario remains in `before/`; corrected left/right content relationships, five viewport families, DPR 1/2/3 states, Low/High tiers, lifecycle/adverse-input states, Static, reduced motion, and empty console evidence are separate and do not overwrite earlier review sets. + +The investigation identified shared controller, projection, and texture causes. Large deltas and multi-boundary samples were previously discarded before reconciliation; dwell was timed from the original latch instead of the first actual readable frame; every desktop sign inherited one right-side pose; a resize/quality rebuild omitted the latched responsive profile; the former departure could cross the camera plane; and the 1024×288 source did not match the face ratio and could be cached before the intended font was ready. These were fixed within the Three.js sign boundary. No content, CSS, navigation, HUD, control, car, road, terrain, sun, atmosphere, camera, or forward-motion redesign was introduced. + +The final controller accepts only forward passive-scroll candidates with motion enabled. A normal next route becomes eligible within 0.55vh; a boundary crossing resolves to the eligible route actually reached. Concealed labels may retarget, visible labels never mutate, jumps retain only the latest relevant pending target, and completed transitions cannot spam on small oscillations. The clock-driven phases are 300ms anticipate, 650–900ms approach, at least 1,400ms dwell measured from the first actual dwell frame, 700ms lateral/receding departure, and 350ms cooldown. A visible approach now completes approach plus the entire dwell even after reversal or passing its target; the refreshed trace observed 1,483.4ms between its first sampled dwell frame and departure after reverse input. Initialization, explicit/skip/direct navigation, history restoration, Static, reduced motion, and hidden-document transitions remain intentionally suppressed or reset. + +Route metadata places Profile, Additional work, and Toolkit in left/outer-left corridors; Experience and Contact in right corridors; and Infrastructure and Selected builds in outer-right corridors. The `wide`, `tall`, and `short` pose families are latched per lifecycle and survive scene-quality rebuilds. Browser projection assertions require the complete readable face to remain inside viewport edges and clear of the active panel, adjacent opening panel, navigation, controls, and HUD. Final measured face widths were 150.64px at 390×844/DPR 3, 134.49px at 844×390, 182.76px at 768×1024, 195.05px at 1440×900, and 312.09px at 2560×1440. The Profile and Experience desktop frames were measured on opposite sides of their corresponding content panels. + +The sign face is now an exact 4:1 plane with a 2048×512 sRGB canvas texture, trilinear mipmaps, linear magnification, and up to 8× available anisotropy. The bundled Geist Mono faces are awaited before the first draw, and the face bypasses tone mapping to avoid post-process color distortion. Every recorded dwell reported `fontReadyWhenDrawn: true`; source sampling headroom ranged from 6.56 to 15.23 texture pixels per rendered face pixel across the recorded viewport/DPR matrix. Label redraw and texture upload occur only when a concealed target changes. Geometry, materials, texture, projection vectors, and mounting remain stable render-owned objects; test-only projection inspection runs only in the authenticated test build. + +Fresh verification produced these final results: + +- The production build gate passed type checking, all 43 unit files / 538 tests, client and SSR builds, prerender verification, and the transfer check. +- The focused controller test passed 30/30. The final focused controller/world/lifecycle/drive/resource group passed 179 tests in 5 files, including the strengthened visible-approach dwell assertions. +- The final complete four-worker browser matrix passed 178 checks with 44 deliberate project skips and no failures. An earlier pre-final run produced one known desktop heading-focus timeout under load; the exact unchanged test passed with one worker in 2.1 seconds, and it also passed in the final four-worker matrix. +- The focused desktop/mobile sign-and-journey run passed 13 checks with the one intentional duplicate responsive-matrix skip. Its responsive loop exercised the automatic Low cap at 390px, Medium cap at 768/844px, and High cap at 1440/2560px. The final complete matrix independently passed the same sign, passive-scroll, explicit-navigation/restoration, keyboard/accessibility, responsive, Static/reduced-motion, fallback, and flash-safety paths. +- Deployment verification passed 9 tests in 3 files. Its sandboxed attempt could not bind the local HTTP fixture (`listen EPERM`); the unchanged suite passed with local-network permission. +- The final authenticated 1440×900 High-tier allocation gate passed with device DPR 2 and renderer ratio 1.5. It measured 5,608,790 texture bytes, 748,476 geometry bytes, 161,344,784 render-target bytes, a 33,554,432-byte allowance, and 201,256,482 total accounted bytes. Peak scene-image concurrency was 1 and video concurrency was 0. +- Transfer verification passed with a 222,897-byte Brotli-accounted first-live-scene payload. The environment chunk measured 701,690 raw / 146,308 transfer bytes, application JavaScript 80,343 transfer bytes, and the unchanged car GLB 410,408 raw / 76,589 transfer bytes. +- `browser-console.json` contains an empty error array, and `git diff --check` passed before final review. + +The independent long interaction harness remains invalid on this host and no frame-cadence or input-latency number is claimed. Its desktop High/DPR run again reached the Toolkit target lookup but exceeded the fixed 75-second timeout; its mobile Low/DPR run remained in scene loading until the fixed five-second deadline and entered the tested static failure path. Neither timeout, assertion, scene-ready deadline, nor budget was changed. Three.js DevTools also reported no connected app for the final preview, so the earlier DevTools draw-call profile remains baseline evidence rather than being relabeled as a new result. + +No new Blender asset was justified. The finite authored car remains Blender-owned and unchanged. Dynamic sign text, route candidacy, lifecycle, responsive placement, road, terrain, atmosphere, sun, camera, and motion remain Three.js-owned. Physical-device, Firefox, Safari/WebKit, Edge, assistive-technology, and audience review remain the honest manual limitations below. + ## Direction review checklist - [x] Information uses near-black opaque panels rather than neon glass. diff --git a/src/composables/useJourneySections.ts b/src/composables/useJourneySections.ts index 1546036..4515d07 100644 --- a/src/composables/useJourneySections.ts +++ b/src/composables/useJourneySections.ts @@ -91,6 +91,7 @@ export function useJourneySections( if (headings.length === 0) return false; const scrollingElement = document.scrollingElement ?? document.documentElement; + const viewportWidth = visualViewport?.width ?? document.documentElement.clientWidth; const viewportHeight = visualViewport?.height ?? document.documentElement.clientHeight; const viewportOffsetTop = visualViewport?.offsetTop ?? 0; const activationY = activationLineDocumentY({ @@ -123,6 +124,7 @@ export function useJourneySections( direction: driveSnapshot.direction, acceleration: driveSnapshot.acceleration, deltaY: driveSnapshot.scrollY - previousScrollY, + viewportWidth, viewportHeight, crossedBoundaries, candidateId, diff --git a/src/journey/sign-controller.ts b/src/journey/sign-controller.ts index 94b87d8..9d2dc77 100644 --- a/src/journey/sign-controller.ts +++ b/src/journey/sign-controller.ts @@ -4,33 +4,21 @@ import type { SignControllerSample, SignSnapshot, } from '@/journey/types'; - -const ANTICIPATE_MS = 300; -const APPROACH_MS = 900; -const APPROACH_REDUCTION_MS = 250; -const DWELL_MS = 1_400; -const DEPART_MS = 700; -const COOLDOWN_MS = 350; -const ELIGIBLE_DISTANCE_VH = 1.2; -const JUMP_DISTANCE_VH = 0.75; -const CONCEALED_OPACITY = 0.02; -const SECTION_ORDER: readonly SectionId[] = [ - 'start', - 'profile', - 'experience', - 'infrastructure', - 'selected-builds', - 'additional-work', - 'toolkit', - 'contact', -]; +import { + SIGN_LIFECYCLE, + SIGN_ROUTE_ORDER, + signRouteFor, + signViewportProfileFor, + type SignViewportProfile, +} from '@/journey/sign-routes'; interface VisibleState { kind: 'visible'; targetId: SectionId; latchedAt: number; approachDuration: number; - departureRequested: boolean; + viewportProfile: SignViewportProfile; + dwellStartedAt: number | null; departStartedAt: number | null; generation: number; } @@ -51,56 +39,44 @@ const isFiniteSample = (input: SignControllerSample): boolean => Number.isFinite(input.now) && Number.isFinite(input.acceleration) && Number.isFinite(input.deltaY) && + (input.viewportWidth === undefined || ( + Number.isFinite(input.viewportWidth) && input.viewportWidth > 0 + )) && Number.isFinite(input.viewportHeight) && Number.isFinite(input.crossedBoundaries) && input.viewportHeight > 0 && (input.candidateDistanceVh === null || Number.isFinite(input.candidateDistanceVh)); -const canLatch = (input: SignControllerSample): input is SignControllerSample & { - candidateId: SectionId; - candidateDistanceVh: number; -} => - input.motionAllowed && - input.source === 'passive-scroll' && - input.direction === 1 && - input.deltaY > 0 && - input.deltaY <= input.viewportHeight * JUMP_DISTANCE_VH && - input.crossedBoundaries <= 1 && - input.candidateId !== null && - input.candidateId !== input.activeSectionId && - input.candidateDistanceVh !== null && - input.candidateDistanceVh > 0 && - input.candidateDistanceVh <= ELIGIBLE_DISTANCE_VH; - export function createSignController(): SignController { let state: State = { kind: 'idle' }; let lastNow: number | null = null; let generation = 0; + let pendingTargetId: SectionId | null = null; + const completedTargets = new Set(); const reset = (now?: number): void => { generation += 1; state = { kind: 'idle' }; + pendingTargetId = null; lastNow = now !== undefined && Number.isFinite(now) ? now : null; }; - const enterCooldown = (now: number): void => { - generation += 1; - state = { kind: 'cooldown', until: now + COOLDOWN_MS }; - }; - const latch = ( - input: SignControllerSample & { - candidateId: SectionId; - candidateDistanceVh: number; - }, + targetId: SectionId, + now: number, + acceleration: number, + viewportWidth: number, + viewportHeight: number, ): VisibleState => { generation += 1; return { kind: 'visible', - targetId: input.candidateId, - latchedAt: input.now, - approachDuration: APPROACH_MS - APPROACH_REDUCTION_MS * clamp01(input.acceleration), - departureRequested: false, + targetId, + latchedAt: now, + approachDuration: SIGN_LIFECYCLE.approachMs + - SIGN_LIFECYCLE.approachReductionMs * clamp01(acceleration), + viewportProfile: signViewportProfileFor(viewportWidth, viewportHeight), + dwellStartedAt: null, departStartedAt: null, generation, }; @@ -115,27 +91,25 @@ export function createSignController(): SignController { const requestDeparture = ( visible: VisibleState, - current: SignSnapshot, now: number, ): void => { - const dwellCompleteAt = visible.latchedAt - + ANTICIPATE_MS - + visible.approachDuration - + DWELL_MS; - if (current.phase === 'dwell' && now < dwellCompleteAt) { - visible.departureRequested = true; - return; - } + if ( + visible.dwellStartedAt === null + || now < visible.dwellStartedAt + SIGN_LIFECYCLE.dwellMs + ) return; beginDeparture(visible, now); }; const snapshotVisible = (visible: VisibleState, now: number): SignSnapshot | null => { if (visible.departStartedAt !== null) { - const progress = clamp01((now - visible.departStartedAt) / DEPART_MS); + const progress = clamp01((now - visible.departStartedAt) / SIGN_LIFECYCLE.departMs); if (progress >= 1) { + if (visible.dwellStartedAt !== null) completedTargets.add(visible.targetId); state = { kind: 'cooldown', - until: visible.departStartedAt + DEPART_MS + COOLDOWN_MS, + until: visible.departStartedAt + + SIGN_LIFECYCLE.departMs + + SIGN_LIFECYCLE.cooldownMs, }; return null; } @@ -144,20 +118,22 @@ export function createSignController(): SignController { phase: 'depart', progress, opacity: 1 - smoothstep(0.1, 1, progress), + viewportProfile: visible.viewportProfile, }; } const elapsed = Math.max(0, now - visible.latchedAt); - if (elapsed < ANTICIPATE_MS) { + if (elapsed < SIGN_LIFECYCLE.anticipateMs) { return { targetId: visible.targetId, phase: 'anticipate', - progress: clamp01(elapsed / ANTICIPATE_MS), + progress: clamp01(elapsed / SIGN_LIFECYCLE.anticipateMs), opacity: 0, + viewportProfile: visible.viewportProfile, }; } - const approachElapsed = elapsed - ANTICIPATE_MS; + const approachElapsed = elapsed - SIGN_LIFECYCLE.anticipateMs; if (approachElapsed < visible.approachDuration) { const progress = clamp01(approachElapsed / visible.approachDuration); return { @@ -165,17 +141,46 @@ export function createSignController(): SignController { phase: 'approach', progress, opacity: smoothstep(0, 0.42, progress), + viewportProfile: visible.viewportProfile, }; } + if (visible.dwellStartedAt === null) visible.dwellStartedAt = now; return { targetId: visible.targetId, phase: 'dwell', progress: 1, opacity: 1, + viewportProfile: visible.viewportProfile, }; }; + const canonicalCandidate = (input: SignControllerSample): SectionId | null => { + if ( + input.direction !== 1 + || input.deltaY <= 0 + || input.source !== 'passive-scroll' + || !input.motionAllowed + ) return null; + + if (input.crossedBoundaries > 0) { + const route = signRouteFor(input.activeSectionId); + return route.eligible && !completedTargets.has(route.id) ? route.id : null; + } + if ( + input.candidateId === null + || input.candidateId === input.activeSectionId + || input.candidateDistanceVh === null + || input.candidateDistanceVh <= 0 + ) return null; + const route = signRouteFor(input.candidateId); + return route.eligible + && input.candidateDistanceVh <= route.triggerMaximumVh + && !completedTargets.has(route.id) + ? route.id + : null; + }; + const sample = (input: SignControllerSample): SignSnapshot | null => { if (!isFiniteSample(input) || (lastNow !== null && input.now < lastNow)) { reset(); @@ -184,85 +189,113 @@ export function createSignController(): SignController { lastNow = input.now; if (!input.motionAllowed || input.source !== 'passive-scroll') { + if (input.source !== 'passive-scroll') { + const route = signRouteFor(input.activeSectionId); + if (route.eligible) completedTargets.add(route.id); + } reset(input.now); return null; } + const candidate = canonicalCandidate(input); const discontinuous = - input.crossedBoundaries > 1 || - Math.abs(input.deltaY) > input.viewportHeight * JUMP_DISTANCE_VH; - if (discontinuous) { - if (state.kind !== 'visible') return null; - const current = snapshotVisible(state, input.now); - if (current === null || current.opacity <= CONCEALED_OPACITY) { - enterCooldown(input.now); - return null; - } - requestDeparture(state, current, input.now); - return snapshotVisible(state, input.now); - } + input.crossedBoundaries > 1 + || Math.abs(input.deltaY) > input.viewportHeight * 0.75; if (state.kind === 'cooldown') { + if (candidate !== null) pendingTargetId = candidate; if (input.now < state.until) return null; state = { kind: 'idle' }; + if (pendingTargetId !== null && !completedTargets.has(pendingTargetId)) { + const targetId = pendingTargetId; + pendingTargetId = null; + state = latch( + targetId, + input.now, + input.acceleration, + input.viewportWidth ?? input.viewportHeight * (16 / 9), + input.viewportHeight, + ); + return snapshotVisible(state, input.now); + } } if (state.kind === 'idle') { - if (!canLatch(input)) return null; - state = latch(input); + if (candidate === null) return null; + state = latch( + candidate, + input.now, + input.acceleration, + input.viewportWidth ?? input.viewportHeight * (16 / 9), + input.viewportHeight, + ); return snapshotVisible(state, input.now); } + if (candidate !== null && candidate !== state.targetId) { + pendingTargetId = candidate; + } const current = snapshotVisible(state, input.now); if (current === null) return null; if ( - current.opacity <= CONCEALED_OPACITY - && canLatch(input) - && input.candidateId !== state.targetId + current.opacity <= SIGN_LIFECYCLE.concealedOpacity + && candidate !== null + && candidate !== state.targetId ) { - state = latch(input); + pendingTargetId = null; + state = latch( + candidate, + input.now, + input.acceleration, + input.viewportWidth ?? input.viewportHeight * (16 / 9), + input.viewportHeight, + ); return snapshotVisible(state, input.now); } - const activeIndex = SECTION_ORDER.indexOf(input.activeSectionId); - const targetIndex = SECTION_ORDER.indexOf(state.targetId); - const targetPassed = activeIndex >= 0 && targetIndex >= 0 && activeIndex > targetIndex; - if (targetPassed && state.departStartedAt === null) { - if (current.opacity <= CONCEALED_OPACITY) { - enterCooldown(input.now); + if (discontinuous && state.departStartedAt === null) { + if (current.opacity <= SIGN_LIFECYCLE.concealedOpacity) { + state = { kind: 'idle' }; return null; } - requestDeparture(state, current, input.now); + requestDeparture(state, input.now); + return snapshotVisible(state, input.now); + } + + const activeIndex = SIGN_ROUTE_ORDER.indexOf(input.activeSectionId); + const targetIndex = SIGN_ROUTE_ORDER.indexOf(state.targetId); + const targetPassed = activeIndex >= 0 && targetIndex >= 0 && activeIndex > targetIndex; + if (targetPassed && state.departStartedAt === null) { + if (current.opacity <= SIGN_LIFECYCLE.concealedOpacity) { + state = { kind: 'idle' }; + return null; + } + requestDeparture(state, input.now); return snapshotVisible(state, input.now); } if (input.direction < 0) { - if (current.opacity <= CONCEALED_OPACITY) { + if (current.opacity <= SIGN_LIFECYCLE.concealedOpacity) { state = { kind: 'idle' }; return null; } - if (state.departStartedAt === null) requestDeparture(state, current, input.now); + if (state.departStartedAt === null) requestDeparture(state, input.now); } if (state.departStartedAt === null) { - const dwellStartedAt = state.latchedAt + ANTICIPATE_MS + state.approachDuration; - const dwellComplete = input.now >= dwellStartedAt + DWELL_MS; - if ( - dwellComplete - && (state.departureRequested || input.activeSectionId === state.targetId) - ) { - beginDeparture(state, input.now); - } + const dwellStartedAt = state.dwellStartedAt; + const dwellComplete = dwellStartedAt !== null + && input.now >= dwellStartedAt + SIGN_LIFECYCLE.dwellMs; + if (dwellComplete) beginDeparture(state, input.now); } return snapshotVisible(state, input.now); }; const needsFrame = (): boolean => { - if (state.kind !== 'visible' || lastNow === null) return false; - if (state.departStartedAt !== null) return true; - return lastNow < state.latchedAt + ANTICIPATE_MS + state.approachDuration + DWELL_MS; + if (state.kind === 'cooldown') return pendingTargetId !== null; + return state.kind === 'visible' && lastNow !== null; }; return { sample, reset, needsFrame }; diff --git a/src/journey/sign-routes.ts b/src/journey/sign-routes.ts new file mode 100644 index 0000000..39886c1 --- /dev/null +++ b/src/journey/sign-routes.ts @@ -0,0 +1,75 @@ +import type { SectionId } from '@/content/types'; + +export type SignCorridor = 'left' | 'right' | 'outer-left' | 'outer-right'; +export type SignViewportProfile = 'wide' | 'tall' | 'short'; + +export interface SignRouteSpec { + id: SectionId; + label: string; + marker: string; + eligible: boolean; + corridor: SignCorridor | null; + triggerMaximumVh: number; +} + +export const SIGN_LIFECYCLE = Object.freeze({ + anticipateMs: 300, + approachMs: 900, + approachReductionMs: 250, + dwellMs: 1_400, + departMs: 700, + cooldownMs: 350, + concealedOpacity: 0.02, +}); + +export const SIGN_ROUTE_ORDER: readonly SectionId[] = [ + 'start', + 'profile', + 'experience', + 'infrastructure', + 'selected-builds', + 'additional-work', + 'toolkit', + 'contact', +]; + +const routeSpecs: Readonly> = Object.freeze({ + start: { + id: 'start', label: 'Start', marker: '01', eligible: false, corridor: null, triggerMaximumVh: 0, + }, + profile: { + id: 'profile', label: 'Profile', marker: '02', eligible: true, corridor: 'left', triggerMaximumVh: 0.55, + }, + experience: { + id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'right', triggerMaximumVh: 0.55, + }, + infrastructure: { + id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, + }, + 'selected-builds': { + id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, + }, + 'additional-work': { + id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'left', triggerMaximumVh: 0.55, + }, + toolkit: { + id: 'toolkit', label: 'Toolkit', marker: '07', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55, + }, + contact: { + id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'right', triggerMaximumVh: 0.55, + }, +}); + +export function signRouteFor(id: SectionId): SignRouteSpec { + return routeSpecs[id]; +} + +export function signViewportProfileFor( + width: number, + height: number, +): SignViewportProfile { + const safeWidth = Number.isFinite(width) ? Math.max(1, width) : 1; + const safeHeight = Number.isFinite(height) ? Math.max(1, height) : 1; + const aspect = safeWidth / safeHeight; + return safeHeight < 560 && aspect >= 1 ? 'short' : aspect < 1.05 ? 'tall' : 'wide'; +} diff --git a/src/journey/types.ts b/src/journey/types.ts index 95100ce..b6e1940 100644 --- a/src/journey/types.ts +++ b/src/journey/types.ts @@ -1,4 +1,5 @@ import type { SectionId } from "@/content/types"; +import type { SignViewportProfile } from '@/journey/sign-routes'; export interface ScrollDriveSample { scrollY: number; @@ -38,6 +39,7 @@ export interface SignSnapshot { phase: SignPhase; progress: number; opacity: number; + viewportProfile?: SignViewportProfile; } export interface SignControllerSample { @@ -46,6 +48,7 @@ export interface SignControllerSample { direction: -1 | 0 | 1; acceleration: number; deltaY: number; + viewportWidth?: number; viewportHeight: number; crossedBoundaries: number; candidateId: SectionId | null; diff --git a/src/scene/create-scene-controller.ts b/src/scene/create-scene-controller.ts index 2dd6b8c..5a59854 100644 --- a/src/scene/create-scene-controller.ts +++ b/src/scene/create-scene-controller.ts @@ -574,6 +574,9 @@ function copyJourneySnapshot(snapshot: JourneySnapshot): JourneySnapshot { phase: snapshot.sign.phase, progress: finite(snapshot.sign.progress), opacity: finite(snapshot.sign.opacity), + ...(snapshot.sign.viewportProfile === undefined + ? {} + : { viewportProfile: snapshot.sign.viewportProfile }), } : null, arrived: Boolean(snapshot.arrived), }; diff --git a/src/scene/world/environment.ts b/src/scene/world/environment.ts index 8a68ba5..639a5e8 100644 --- a/src/scene/world/environment.ts +++ b/src/scene/world/environment.ts @@ -7,7 +7,7 @@ import { createRoad } from '@/scene/world/road'; import { createTerrain } from '@/scene/world/terrain'; import { createSun } from '@/scene/world/sun'; import { createCar, loadGrandTourer } from '@/scene/world/car'; -import { createSign } from '@/scene/world/sign'; +import { createSign, prepareSignTypography } from '@/scene/world/sign'; import { createAtmosphere } from '@/scene/world/atmosphere'; import { createPostProcessor } from '@/scene/post-processing'; import { createDriveDynamics } from '@/scene/drive-dynamics'; @@ -97,7 +97,10 @@ export async function createProceduralSceneGeneration( const authoredCar = await loadGrandTourer(context.signal); const car = createCar(context.resources, authoredCar); car.setDetail(mobileOrTablet ? 'reduced' : 'full'); - const sign = createSign(context.resources); + await prepareSignTypography(context.signal); + const sign = createSign(context.resources, { + maximumAnisotropy: renderer.capabilities.getMaxAnisotropy(), + }); const atmosphere = createAtmosphere(context.resources, quality); scene.add( atmosphere.group, @@ -152,7 +155,7 @@ export async function createProceduralSceneGeneration( } car.setDetail(width < 1_100 || height < 620 ? 'reduced' : 'full'); car.group.visible = height >= 340; - sign.setViewportAspect(camera.aspect); + sign.setViewportSize(width, height); atmosphere.setViewportAspect(camera.aspect); postProcessor?.resize(width, height, quality.pixelRatio * quality.postScale); if (TEST_BUILD && context.quality !== 'medium') { @@ -296,6 +299,21 @@ export async function createProceduralSceneGeneration( camera.fov = cameraState.fov; camera.updateProjectionMatrix(); } + if (TEST_BUILD && snapshot.sign !== null) { + const presentation = sign.inspect(camera, width, height); + if (presentation !== null) { + window.dispatchEvent(new CustomEvent('portfolio-sign-frame', { + detail: { + now, + ...snapshot.sign, + activeSectionId: snapshot.activeSectionId, + direction: snapshot.direction, + ...presentation, + rendererPixelRatio: renderer.getPixelRatio(), + }, + })); + } + } if (postProcessor) { postProcessor.setDynamics(response.bloomEnergy); postProcessor.render(scene, camera); diff --git a/src/scene/world/sign.ts b/src/scene/world/sign.ts index d9afc0f..c69634b 100644 --- a/src/scene/world/sign.ts +++ b/src/scene/world/sign.ts @@ -1,19 +1,15 @@ import * as THREE from 'three'; import type { SectionId } from '@/content/types'; import type { SignSnapshot } from '@/journey/types'; +import { + signRouteFor, + signViewportProfileFor, + type SignCorridor, + type SignRouteSpec, + type SignViewportProfile, +} from '@/journey/sign-routes'; import type { SceneResourceBag } from '@/scene/resources'; -const routeLabels: Readonly> = { - start: 'Start', - profile: 'Profile', - experience: 'Experience', - infrastructure: 'Infrastructure', - 'selected-builds': 'Selected builds', - 'additional-work': 'Additional work', - toolkit: 'Toolkit', - contact: 'Contact', -}; - interface SignPose { x: number; y: number; @@ -21,22 +17,111 @@ interface SignPose { scale: number; } -const NARROW_SIGN_POSES = { - horizon: { x: 5.2, y: 6.8, z: -142, scale: 0.15 }, - dwell: { x: 5.8, y: 4.86, z: -20, scale: 0.82 }, -} as const; +interface SignPoseSet { + horizon: SignPose; + dwell: SignPose; +} -const WIDE_SIGN_POSES = { - horizon: { x: 9.1, y: 7.4, z: -160, scale: 0.18 }, - dwell: { x: 10.7, y: 6.64, z: -14, scale: 1.12 }, -} as const; +interface ProjectingCamera { + updateMatrixWorld(force: boolean): void; +} + +const SIGN_MARKER_FONT = '560 46px "Geist Mono Variable", ui-monospace, monospace'; +const SIGN_LABEL_FONT = '660 164px "Geist Mono Variable", ui-monospace, monospace'; + +const SIGN_POSES: Readonly>>> = { + wide: { + left: { + horizon: { x: -9.6, y: 7.2, z: -156, scale: 0.2 }, + dwell: { x: -11.6, y: 7.9, z: -16, scale: 1.25 }, + }, + right: { + horizon: { x: 10.2, y: 7.2, z: -156, scale: 0.2 }, + dwell: { x: 12.4, y: 7.9, z: -16, scale: 1.25 }, + }, + 'outer-left': { + horizon: { x: -17.5, y: 7.4, z: -168, scale: 0.2 }, + dwell: { x: -21.5, y: 8.55, z: -24, scale: 1.35 }, + }, + 'outer-right': { + horizon: { x: 18.5, y: 7.4, z: -168, scale: 0.2 }, + dwell: { x: 22.5, y: 8.55, z: -24, scale: 1.35 }, + }, + }, + tall: { + left: { + horizon: { x: -4.2, y: 6.4, z: -136, scale: 0.16 }, + dwell: { x: -5.2, y: 8.1, z: -24, scale: 1.3 }, + }, + right: { + horizon: { x: 4.2, y: 6.4, z: -136, scale: 0.16 }, + dwell: { x: 5.2, y: 8.1, z: -24, scale: 1.3 }, + }, + 'outer-left': { + horizon: { x: -5.8, y: 6.7, z: -148, scale: 0.16 }, + dwell: { x: -7.2, y: 8.72, z: -30, scale: 1.4 }, + }, + 'outer-right': { + horizon: { x: 5.8, y: 6.7, z: -148, scale: 0.16 }, + dwell: { x: 7.2, y: 8.72, z: -30, scale: 1.4 }, + }, + }, + short: { + left: { + horizon: { x: -7.8, y: 6.6, z: -144, scale: 0.18 }, + dwell: { x: -8.8, y: 5.45, z: -26, scale: 2.55 }, + }, + right: { + horizon: { x: 7.8, y: 6.6, z: -144, scale: 0.18 }, + dwell: { x: 8.8, y: 5.45, z: -26, scale: 2.55 }, + }, + 'outer-left': { + horizon: { x: -11.2, y: 6.8, z: -154, scale: 0.18 }, + dwell: { x: -12.6, y: 5.8, z: -32, scale: 2.25 }, + }, + 'outer-right': { + horizon: { x: 11.2, y: 6.8, z: -154, scale: 0.18 }, + dwell: { x: 12.6, y: 5.8, z: -32, scale: 2.25 }, + }, + }, +}; export interface SignWorld { group: SignGroup; + inspect( + camera: ProjectingCamera, + width: number, + height: number, + ): SignPresentation | null; setViewportAspect(aspect: number): void; + setViewportSize(width: number, height: number): void; update(sign: SignSnapshot | null): void; } +export interface SignPresentation { + visible: boolean; + corridor: SignCorridor; + viewportProfile: SignViewportProfile; + bounds: { + left: number; + top: number; + right: number; + bottom: number; + width: number; + height: number; + }; + texture: { + width: number; + height: number; + anisotropy: number; + fontReadyWhenDrawn: boolean; + }; +} + +export interface SignRenderOptions { + maximumAnisotropy?: number; +} + interface SignGroup { visible: boolean; position: { @@ -51,13 +136,28 @@ interface SignGroup { scale: { x: number; setScalar(scale: number): void }; add(...objects: unknown[]): void; getObjectByName(name: string): unknown; + updateMatrixWorld(force: boolean): void; } export function routeLabelFor(id: SectionId): string { - return routeLabels[id]; + return signRouteFor(id).label; } -export function createSign(resources: SceneResourceBag): SignWorld { +export async function prepareSignTypography(signal: AbortSignal): Promise { + const fonts = document.fonts; + if (fonts === undefined) return; + await Promise.all([ + fonts.load(SIGN_MARKER_FONT), + fonts.load(SIGN_LABEL_FONT), + fonts.ready, + ]); + if (signal.aborted) throw new DOMException('Scene build aborted', 'AbortError'); +} + +export function createSign( + resources: SceneResourceBag, + options: SignRenderOptions = {}, +): SignWorld { const group = new THREE.Group(); group.name = 'roadside-sign'; group.visible = false; @@ -103,7 +203,7 @@ export function createSign(resources: SceneResourceBag): SignWorld { })); const housing = new THREE.Mesh( - resources.own('geometry', createSignBody(7.5, 2.3, 0.34, 0.2)), + resources.own('geometry', createSignBody(8.55, 2.65, 0.42, 0.22)), housingMaterial, ); housing.name = 'sign-housing'; @@ -111,7 +211,7 @@ export function createSign(resources: SceneResourceBag): SignWorld { group.add(housing); const backplate = new THREE.Mesh( - resources.own('geometry', createSignBody(7.2, 2.04, 0.18, 0.16)), + resources.own('geometry', createSignBody(8.24, 2.34, 0.2, 0.18)), backplateMaterial, ); backplate.name = 'sign-backplate'; @@ -119,15 +219,18 @@ export function createSign(resources: SceneResourceBag): SignWorld { group.add(backplate); const canvas = document.createElement('canvas'); - canvas.width = 1_024; - canvas.height = 288; + canvas.width = 2_048; + canvas.height = 512; const context = canvas.getContext('2d'); const texture = resources.own('texture', new THREE.CanvasTexture(canvas)); texture.colorSpace = THREE.SRGBColorSpace; texture.generateMipmaps = true; texture.minFilter = THREE.LinearMipmapLinearFilter; texture.magFilter = THREE.LinearFilter; - texture.anisotropy = 4; + texture.anisotropy = Math.min( + 8, + Math.max(1, Math.floor(options.maximumAnisotropy ?? 8)), + ); const faceMaterial = resources.own('material', new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, @@ -140,55 +243,56 @@ export function createSign(resources: SceneResourceBag): SignWorld { toneMapped: false, })); const face = new THREE.Mesh( - resources.own('geometry', new THREE.PlaneGeometry(6.92, 1.76)), + resources.own('geometry', new THREE.PlaneGeometry(8, 2)), faceMaterial, ); face.name = 'sign-face'; - face.position.z = 0.545; + face.position.z = 0.625; face.userData.textureUpdateCount = 0; + face.userData.fontReadyWhenDrawn = false; group.add(face); const edgeLight = new THREE.Group(); edgeLight.name = 'sign-edge-light'; - const edgeGeometry = resources.own('geometry', new THREE.BoxGeometry(0.055, 1.42, 0.055)); - const upperEdgeGeometry = resources.own('geometry', new THREE.BoxGeometry(1.16, 0.045, 0.055)); + const edgeGeometry = resources.own('geometry', new THREE.BoxGeometry(0.055, 1.62, 0.055)); + const upperEdgeGeometry = resources.own('geometry', new THREE.BoxGeometry(1.32, 0.045, 0.055)); const leftEdge = new THREE.Mesh(edgeGeometry, edgeMaterial); - leftEdge.position.set(-3.36, 0, 0.585); + leftEdge.position.set(-3.9, 0, 0.665); edgeLight.add(leftEdge); const upperEdge = new THREE.Mesh(upperEdgeGeometry, edgeMaterial); - upperEdge.position.set(-2.81, 0.88, 0.585); + upperEdge.position.set(-3.28, 1, 0.665); edgeLight.add(upperEdge); group.add(edgeLight); const routeAccent = new THREE.Mesh( - resources.own('geometry', new THREE.BoxGeometry(1.05, 0.055, 0.065)), + resources.own('geometry', new THREE.BoxGeometry(1.18, 0.055, 0.065)), routeMaterial, ); routeAccent.name = 'sign-route-accent'; - routeAccent.position.set(2.77, -0.88, 0.585); + routeAccent.position.set(3.35, -1, 0.665); group.add(routeAccent); const post = new THREE.Mesh( - resources.own('geometry', new THREE.CylinderGeometry(0.095, 0.14, 4.85, 8)), + resources.own('geometry', new THREE.CylinderGeometry(0.095, 0.145, 5.05, 8)), supportMaterial, ); post.name = 'sign-post'; - post.position.set(2.35, -3.32, -0.14); + post.position.set(2.75, -3.58, -0.16); group.add(post); const bracket = new THREE.Group(); bracket.name = 'sign-bracket'; const arm = new THREE.Mesh( - resources.own('geometry', new THREE.BoxGeometry(2.5, 0.11, 0.15)), + resources.own('geometry', new THREE.BoxGeometry(2.95, 0.11, 0.15)), supportMaterial, ); - arm.position.set(1.18, -1.08, -0.13); + arm.position.set(1.4, -1.24, -0.15); bracket.add(arm); const brace = new THREE.Mesh( resources.own('geometry', new THREE.BoxGeometry(0.1, 1.25, 0.12)), supportMaterial, ); - brace.position.set(1.72, -1.56, -0.13); + brace.position.set(2.05, -1.74, -0.15); brace.rotation.z = -0.72; bracket.add(brace); group.add(bracket); @@ -198,7 +302,7 @@ export function createSign(resources: SceneResourceBag): SignWorld { supportMaterial, ); footing.name = 'sign-footing'; - footing.position.set(2.35, -5.76, -0.14); + footing.position.set(2.75, -6.18, -0.16); group.add(footing); const fadeMaterials = [ @@ -212,15 +316,24 @@ export function createSign(resources: SceneResourceBag): SignWorld { const departureOrigin = new THREE.Vector3(); let departureScale = 1; let target: SectionId | null = null; - let pendingNarrowViewport = false; - let activeNarrowViewport = false; + let pendingViewport: SignViewportProfile = 'wide'; + let activeViewport: SignViewportProfile = 'wide'; + let activeCorridor: SignCorridor = 'right'; let previousPhase: SignSnapshot['phase'] | null = null; + const faceCornerCoordinates = [ + [-4, -1, 0], + [4, -1, 0], + [4, 1, 0], + [-4, 1, 0], + ] as const; + const projectedCorner = new THREE.Vector3(); const updateTexture = (id: SectionId): void => { if (target === id) return; target = id; face.userData.textureUpdateCount += 1; - if (context) drawLabel(context, routeLabelFor(id), canvas.width, canvas.height); + face.userData.fontReadyWhenDrawn = document.fonts?.check(SIGN_LABEL_FONT) ?? true; + if (context) drawLabel(context, signRouteFor(id), canvas.width, canvas.height); texture.needsUpdate = true; }; @@ -231,34 +344,92 @@ export function createSign(resources: SceneResourceBag): SignWorld { const applyPose = (pose: SignPose): void => { group.position.set(pose.x, pose.y, pose.z); group.scale.setScalar(pose.scale); + plantMounting(pose.y, pose.scale); + }; + + const plantMounting = (groupY: number, groupScale: number): void => { + const safeScale = Math.max(0.001, groupScale); + const footingY = -groupY / safeScale; + const postTopY = -1.36; + const postHeight = Math.max(0.36, postTopY - footingY); + post.position.y = (postTopY + footingY) * 0.5; + post.scale.set(1 / safeScale, postHeight / 5.05, 1 / safeScale); + footing.position.y = footingY; + footing.scale.setScalar(1 / Math.max(1, safeScale)); }; return { group, + inspect(camera, width, height) { + if (target === null || width <= 0 || height <= 0) return null; + group.updateMatrixWorld(true); + camera.updateMatrixWorld(true); + let left = Number.POSITIVE_INFINITY; + let top = Number.POSITIVE_INFINITY; + let right = Number.NEGATIVE_INFINITY; + let bottom = Number.NEGATIVE_INFINITY; + for (const [xPosition, yPosition, zPosition] of faceCornerCoordinates) { + projectedCorner + .set(xPosition, yPosition, zPosition) + .applyMatrix4(face.matrixWorld) + .project(camera); + const x = (projectedCorner.x + 1) * 0.5 * width; + const y = (1 - projectedCorner.y) * 0.5 * height; + left = Math.min(left, x); + top = Math.min(top, y); + right = Math.max(right, x); + bottom = Math.max(bottom, y); + } + return { + visible: group.visible, + corridor: activeCorridor, + viewportProfile: activeViewport, + bounds: { + left, + top, + right, + bottom, + width: right - left, + height: bottom - top, + }, + texture: { + width: canvas.width, + height: canvas.height, + anisotropy: texture.anisotropy, + fontReadyWhenDrawn: face.userData.fontReadyWhenDrawn === true, + }, + }; + }, setViewportAspect(aspect) { - pendingNarrowViewport = !Number.isFinite(aspect) || aspect < 0.85; - if (!group.visible) activeNarrowViewport = pendingNarrowViewport; + pendingViewport = !Number.isFinite(aspect) || aspect < 0.85 ? 'tall' : 'wide'; + if (previousPhase === null) activeViewport = pendingViewport; + }, + setViewportSize(width, height) { + pendingViewport = signViewportProfileFor(width, height); + if (previousPhase === null) activeViewport = pendingViewport; }, update(sign) { if (!sign) { group.visible = false; applyOpacity(0); previousPhase = null; - activeNarrowViewport = pendingNarrowViewport; + activeViewport = pendingViewport; return; } if (target !== null && target !== sign.targetId && group.visible) return; if (target !== sign.targetId) { - activeNarrowViewport = pendingNarrowViewport; + activeViewport = sign.viewportProfile ?? pendingViewport; updateTexture(sign.targetId); } const opacity = clamp01(sign.opacity); const progress = smoothstep(clamp01(sign.progress)); - const { horizon, dwell } = activeNarrowViewport - ? NARROW_SIGN_POSES - : WIDE_SIGN_POSES; + const route = signRouteFor(sign.targetId); + const corridor = route.corridor ?? 'right'; + activeCorridor = corridor; + const { horizon, dwell } = SIGN_POSES[activeViewport][corridor]; + const side = corridor.includes('left') ? -1 : 1; if (sign.phase === 'anticipate') { applyPose(horizon); @@ -269,6 +440,7 @@ export function createSign(resources: SceneResourceBag): SignWorld { THREE.MathUtils.lerp(horizon.z, dwell.z, progress), ); group.scale.setScalar(THREE.MathUtils.lerp(horizon.scale, dwell.scale, progress)); + plantMounting(group.position.y, group.scale.x); } else if (sign.phase === 'dwell') { applyPose(dwell); } else { @@ -277,15 +449,18 @@ export function createSign(resources: SceneResourceBag): SignWorld { departureOrigin.copy(group.position); departureScale = group.scale.x; } + const lateralExit = activeViewport === 'wide' + ? 14 + : activeViewport === 'short' ? 10 : 8; group.position.set( - departureOrigin.x + progress * (activeNarrowViewport ? 2.2 : 4.4), - departureOrigin.y + progress * 0.62, - THREE.MathUtils.lerp(departureOrigin.z, 28, progress), + departureOrigin.x + progress * side * lateralExit, + departureOrigin.y + progress * 0.72, + departureOrigin.z - progress * 32, ); - group.scale.setScalar(departureScale * THREE.MathUtils.lerp(1, 1.06, progress)); + group.scale.setScalar(departureScale * THREE.MathUtils.lerp(1, 0.82, progress)); } - group.rotation.y = activeNarrowViewport ? -0.14 : -0.23; + group.rotation.y = -side * (activeViewport === 'wide' ? 0.2 : 0.12); applyOpacity(opacity); group.visible = opacity > 0; previousPhase = sign.phase; @@ -325,32 +500,32 @@ function createClippedShape(width: number, height: number, cut: number) { function drawLabel( context: CanvasRenderingContext2D, - label: string, + route: SignRouteSpec, width: number, height: number, ): void { context.clearRect(0, 0, width, height); context.fillStyle = '#07111a'; context.fillRect(0, 0, width, height); - context.fillStyle = '#55d4df'; - context.fillRect(42, 36, 8, height - 72); - context.fillStyle = '#91aab3'; - context.font = '500 23px "Geist Mono Variable", ui-monospace, monospace'; + context.fillStyle = '#4cbac4'; + context.fillRect(86, 72, 12, height - 144); + context.fillStyle = '#879da4'; + context.font = SIGN_MARKER_FONT; context.textAlign = 'left'; context.textBaseline = 'middle'; - context.fillText('NEXT / ROUTE', 82, 72); - context.fillStyle = '#eef7f8'; - context.font = '650 74px "Geist Mono Variable", ui-monospace, monospace'; - context.fillText(label, 82, 170, width - 210); - context.strokeStyle = '#d99758'; - context.lineWidth = 7; + context.fillText(`ROUTE ${route.marker} / NEXT`, 150, 126); + context.fillStyle = '#c8d8da'; + context.font = SIGN_LABEL_FONT; + context.fillText(route.label, 150, 326, width - 420); + context.strokeStyle = '#cf8e54'; + context.lineWidth = 12; context.lineCap = 'square'; context.beginPath(); - context.moveTo(width - 116, 156); - context.lineTo(width - 74, 156); - context.lineTo(width - 92, 138); - context.moveTo(width - 74, 156); - context.lineTo(width - 92, 174); + context.moveTo(width - 250, 304); + context.lineTo(width - 158, 304); + context.lineTo(width - 198, 264); + context.moveTo(width - 158, 304); + context.lineTo(width - 198, 344); context.stroke(); } diff --git a/tests/browser/journey-integration.spec.ts b/tests/browser/journey-integration.spec.ts index 3208db0..cf9715a 100644 --- a/tests/browser/journey-integration.spec.ts +++ b/tests/browser/journey-integration.spec.ts @@ -88,7 +88,7 @@ test('draws only the independently eligible next roadside sign during passive fo if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const headingTop = profile.getBoundingClientRect().top + window.scrollY; - const destination = Math.max(1, headingTop - viewportHeight * (0.35 + 0.8)); + const destination = Math.max(1, headingTop - viewportHeight * (0.35 + 0.45)); return { destination, viewportHeight, expectedLabel: 'Profile' }; }); await page.evaluate(() => window.dispatchEvent(new WheelEvent('wheel', { deltaY: 1 }))); @@ -118,7 +118,7 @@ test('keeps a readable sign immutable through reversal, resize, and a multi-sect const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const headingTop = profile.getBoundingClientRect().top + window.scrollY; return { - destination: Math.max(1, headingTop - viewportHeight * (0.35 + 0.8)), + destination: Math.max(1, headingTop - viewportHeight * (0.35 + 0.45)), viewportHeight, }; }); @@ -163,7 +163,159 @@ test('suppresses roadside signs for explicit navigation and history restoration' expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]); }); +test('reconciles an idle rapid jump to the latest active route without flashing intermediates', async ({ + page, + distUrl, +}) => { + await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); + await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); + + await page.mouse.wheel(0, 1); + await page.evaluate(() => { + const target = document.getElementById('selected-builds-heading'); + if (!(target instanceof HTMLElement)) throw new Error('Missing selected builds heading'); + const top = target.getBoundingClientRect().top + window.scrollY; + const viewportHeight = window.visualViewport?.height ?? window.innerHeight; + window.scrollTo(0, Math.max(1, top - viewportHeight * 0.35 + 1)); + }); + + await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn) + .toContain('Selected builds'); + expect([...new Set((await readSceneInstrumentation(page)).routeLabelsDrawn)]) + .toEqual(['Selected builds']); +}); + +test('keeps the readable Profile face outside its destination panel with verified texel density', async ({ + page, + distUrl, +}) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); + await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); + + const destination = await page.evaluate(() => { + const profile = document.getElementById('profile-heading'); + if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); + const viewportHeight = window.visualViewport?.height ?? window.innerHeight; + return Math.max( + 1, + profile.getBoundingClientRect().top + window.scrollY + - viewportHeight * (0.35 + 0.45), + ); + }); + await page.mouse.wheel(0, 1); + await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination); + + await expect.poll(async () => ( + await readSceneInstrumentation(page) + ).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell')).toBe(true); + + const instrumentation = await readSceneInstrumentation(page); + const frame = instrumentation.signFrames.findLast((candidate) => ( + candidate.targetId === 'profile' && candidate.phase === 'dwell' + )); + if (frame === undefined) throw new Error('Missing readable Profile sign frame'); + const panel = await page.locator('#profile').evaluate((element) => { + const bounds = element.getBoundingClientRect(); + return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom }; + }); + expect(frame.bounds.right).toBeLessThanOrEqual(panel.left - 16); + expect(frame.texture).toMatchObject({ + width: 2_048, + height: 512, + fontReadyWhenDrawn: true, + }); + expect(frame.texture.width / (frame.bounds.width * frame.rendererPixelRatio)) + .toBeGreaterThanOrEqual(4); +}); + +test('keeps a fresh readable sign inside responsive world-safe corridors', async ({ + page, + distUrl, +}, testInfo) => { + test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact responsive matrix is sufficient.'); + + for (const viewport of [ + { width: 390, height: 844, profile: 'tall', minimumFaceWidth: 120 }, + { width: 844, height: 390, profile: 'short', minimumFaceWidth: 130 }, + { width: 768, height: 1_024, profile: 'tall', minimumFaceWidth: 150 }, + { width: 1_440, height: 900, profile: 'wide', minimumFaceWidth: 180 }, + { width: 2_560, height: 1_440, profile: 'wide', minimumFaceWidth: 240 }, + ] as const) { + await page.setViewportSize(viewport); + await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); + await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); + const destination = await page.evaluate(() => { + const profile = document.getElementById('profile-heading'); + if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); + const viewportHeight = window.visualViewport?.height ?? window.innerHeight; + return Math.max( + 1, + profile.getBoundingClientRect().top + window.scrollY + - viewportHeight * (0.35 + 0.45), + ); + }); + await page.mouse.wheel(0, 1); + await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination); + await expect.poll(async () => ( + await readSceneInstrumentation(page) + ).signFrames.at(-1)?.phase, { intervals: [16], timeout: 5_000 }).toBe('dwell'); + + const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => ( + candidate.targetId === 'profile' && candidate.phase === 'dwell' + )); + if (frame === undefined) throw new Error(`Missing Profile dwell at ${viewport.width}x${viewport.height}`); + const geometry = await page.evaluate(() => { + const boundsFor = (selector: string) => { + const element = document.querySelector(selector); + if (!(element instanceof HTMLElement)) return null; + const bounds = element.getBoundingClientRect(); + return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom }; + }; + return { + panel: boundsFor('#profile'), + adjacentPanels: [boundsFor('.hero-journey__content')].filter((bounds) => bounds !== null), + fixedZones: [ + boundsFor('.site-navigation'), + boundsFor('.drive-controls'), + boundsFor('.journey-hud'), + ].filter((bounds) => bounds !== null), + }; + }); + if (geometry.panel === null) throw new Error('Missing Profile panel bounds'); + expect(frame.viewportProfile).toBe(viewport.profile); + expect(frame.bounds.left).toBeGreaterThanOrEqual(8); + expect(frame.bounds.right).toBeLessThanOrEqual(viewport.width - 8); + expect(frame.bounds.top).toBeGreaterThanOrEqual(8); + expect(frame.bounds.bottom).toBeLessThanOrEqual(viewport.height - 8); + expect(frame.bounds.width).toBeGreaterThanOrEqual(viewport.minimumFaceWidth); + expect(rectanglesOverlap(frame.bounds, geometry.panel, 16)).toBe(false); + for (const adjacentPanel of geometry.adjacentPanels) { + expect(rectanglesOverlap(frame.bounds, adjacentPanel, 16)).toBe(false); + } + for (const fixedZone of geometry.fixedZones) { + expect(rectanglesOverlap(frame.bounds, fixedZone, 8)).toBe(false); + } + expect(frame.texture.fontReadyWhenDrawn).toBe(true); + expect(frame.texture.width / (frame.bounds.width * frame.rendererPixelRatio)) + .toBeGreaterThanOrEqual(4); + } +}); + function colorEnergy(color: string): number { const value = Number.parseInt(color.slice(1), 16); return ((value >> 16) & 0xff) + ((value >> 8) & 0xff) + (value & 0xff); } + +function rectanglesOverlap( + first: { left: number; top: number; right: number; bottom: number }, + second: { left: number; top: number; right: number; bottom: number }, + gap: number, +): boolean { + return !( + first.right <= second.left - gap + || first.left >= second.right + gap + || first.bottom <= second.top - gap + || first.top >= second.bottom + gap + ); +} diff --git a/tests/helpers/scene-instrumentation.ts b/tests/helpers/scene-instrumentation.ts index 83ead4d..ce6d3ae 100644 --- a/tests/helpers/scene-instrumentation.ts +++ b/tests/helpers/scene-instrumentation.ts @@ -10,6 +10,23 @@ export interface SceneInstrumentationSnapshot { routeLabelsDrawn: string[]; clearColors: string[]; unhandledRejections: string[]; + signFrames: SignFrameInstrumentation[]; +} + +export interface SignFrameInstrumentation { + now: number; + visible: boolean; + targetId: string; + phase: string; + progress: number; + opacity: number; + activeSectionId: string; + direction: number; + corridor: string; + viewportProfile: string; + bounds: { left: number; top: number; right: number; bottom: number; width: number; height: number }; + texture: { width: number; height: number; anisotropy: number; fontReadyWhenDrawn: boolean }; + rendererPixelRatio: number; } type InstrumentedWindow = Window & { @@ -34,6 +51,7 @@ export async function installSceneInstrumentation(page: Page): Promise { routeLabelsDrawn: [], clearColors: [], unhandledRejections: [], + signFrames: [], animationFrameIds: new Set(), audioContexts: [], }; @@ -135,6 +153,11 @@ export async function installSceneInstrumentation(page: Page): Promise { const reason = event.reason; state.unhandledRejections.push(reason instanceof Error ? reason.message : String(reason)); }); + window.addEventListener('portfolio-sign-frame', (event) => { + const detail = (event as CustomEvent).detail; + if (!detail || state.signFrames.length >= 600) return; + state.signFrames.push(structuredClone(detail)); + }); }); } @@ -150,6 +173,7 @@ export async function readSceneInstrumentation(page: Page): Promise structuredClone(frame)), }; }); } diff --git a/tests/unit/scene-lifecycle.test.ts b/tests/unit/scene-lifecycle.test.ts index 35a1afa..25551a6 100644 --- a/tests/unit/scene-lifecycle.test.ts +++ b/tests/unit/scene-lifecycle.test.ts @@ -216,7 +216,13 @@ describe('scene controller lifecycle', () => { await controller.start(); const input = journey({ routeProgress: 0.25, - sign: { targetId: 'selected-builds', phase: 'approach', progress: 0.2, opacity: 0.5 }, + sign: { + targetId: 'selected-builds', + phase: 'approach', + progress: 0.2, + opacity: 0.5, + viewportProfile: 'wide', + }, }); controller.update(input); @@ -225,7 +231,10 @@ describe('scene controller lifecycle', () => { harness.scheduler.flushFrame(16); expect(harness.generations[0]?.render).toHaveBeenCalledWith( - expect.objectContaining({ routeProgress: 0.25, sign: expect.objectContaining({ progress: 0.2 }) }), + expect.objectContaining({ + routeProgress: 0.25, + sign: expect.objectContaining({ progress: 0.2, viewportProfile: 'wide' }), + }), 16, ); }); diff --git a/tests/unit/sign-controller.test.ts b/tests/unit/sign-controller.test.ts index 0b96ec0..5b43dee 100644 --- a/tests/unit/sign-controller.test.ts +++ b/tests/unit/sign-controller.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { createSignController } from '@/journey/sign-controller'; +import { SIGN_ROUTE_ORDER, signRouteFor } from '@/journey/sign-routes'; import type { SignControllerSample } from '@/journey/types'; const sample = ( @@ -10,16 +11,30 @@ const sample = ( direction: 1, acceleration: 0, deltaY: 16, + viewportWidth: 1_440, viewportHeight: 800, crossedBoundaries: 0, candidateId: 'profile', - candidateDistanceVh: 1, + candidateDistanceVh: 0.5, activeSectionId: 'start', motionAllowed: true, ...overrides, }); describe('clock-driven sign lifecycle', () => { + it('defines one explicit route marker and safe corridor for every eligible destination', () => { + expect(SIGN_ROUTE_ORDER.map((id) => signRouteFor(id))).toEqual([ + { id: 'start', label: 'Start', marker: '01', eligible: false, corridor: null, triggerMaximumVh: 0 }, + { id: 'profile', label: 'Profile', marker: '02', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 }, + { id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 }, + { id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55 }, + { id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55 }, + { id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 }, + { id: 'toolkit', label: 'Toolkit', marker: '07', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55 }, + { id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 }, + ]); + }); + it('advances through exact anticipate, approach, dwell, depart, and cooldown timing', () => { const controller = createSignController(); @@ -28,6 +43,7 @@ describe('clock-driven sign lifecycle', () => { phase: 'anticipate', progress: 0, opacity: 0, + viewportProfile: 'wide', }); expect(controller.needsFrame()).toBe(true); expect(controller.sample(sample({ now: 299, direction: 0 }))).toMatchObject({ @@ -87,21 +103,34 @@ describe('clock-driven sign lifecycle', () => { }); }); - it('holds a completed minimum dwell without scheduling idle frames until the target activates', () => { + it('keeps the latched viewport profile immutable across a responsive resize', () => { + const controller = createSignController(); + expect(controller.sample(sample())).toMatchObject({ viewportProfile: 'wide' }); + + expect(controller.sample(sample({ + now: 500, + direction: 0, + deltaY: 0, + viewportWidth: 844, + viewportHeight: 390, + }))).toMatchObject({ targetId: 'profile', viewportProfile: 'wide' }); + }); + + it('departs after the minimum dwell even when scrolling stops before the target activates', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0 })); - expect(controller.sample(sample({ now: 2_600, direction: 0 }))).toMatchObject({ phase: 'dwell' }); - expect(controller.needsFrame()).toBe(false); - expect(controller.sample(sample({ now: 4_000, activeSectionId: 'profile' }))).toMatchObject({ + expect(controller.sample(sample({ now: 2_599, direction: 0, deltaY: 0 }))).toMatchObject({ phase: 'dwell' }); + expect(controller.needsFrame()).toBe(true); + expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }))).toMatchObject({ phase: 'depart', progress: 0, }); expect(controller.needsFrame()).toBe(true); }); - it('cancels a concealed sign on reverse and departs a visible approaching sign cleanly', () => { + it('cancels a concealed sign on reverse but preserves the full dwell for a visible approach', () => { const early = createSignController(); early.sample(sample()); expect(early.sample(sample({ now: 310, direction: -1, deltaY: -20 }))).toBeNull(); @@ -109,10 +138,14 @@ describe('clock-driven sign lifecycle', () => { const readable = createSignController(); readable.sample(sample()); - expect(readable.sample(sample({ now: 700, direction: -1, deltaY: -20 }))).toMatchObject({ - phase: 'depart', - progress: 0, - }); + expect(readable.sample(sample({ now: 700, direction: -1, deltaY: -20 }))) + .toMatchObject({ phase: 'approach' }); + expect(readable.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }))) + .toMatchObject({ phase: 'dwell' }); + expect(readable.sample(sample({ now: 2_599, direction: 0, deltaY: 0 }))) + .toMatchObject({ phase: 'dwell' }); + expect(readable.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }))) + .toMatchObject({ phase: 'depart', progress: 0 }); }); it('preserves the minimum readable dwell when reverse input requests departure', () => { @@ -166,7 +199,7 @@ describe('clock-driven sign lifecycle', () => { }); }); - it('departs when the authoritative section has already passed the latched target', () => { + it('preserves approach and minimum dwell when the authoritative section passes the target', () => { const controller = createSignController(); controller.sample(sample()); @@ -176,9 +209,29 @@ describe('clock-driven sign lifecycle', () => { candidateId: 'infrastructure', }))).toMatchObject({ targetId: 'profile', - phase: 'depart', - progress: 0, + phase: 'approach', }); + expect(controller.sample(sample({ + now: 1_200, + direction: 0, + deltaY: 0, + activeSectionId: 'experience', + candidateId: 'infrastructure', + }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); + expect(controller.sample(sample({ + now: 2_599, + direction: 0, + deltaY: 0, + activeSectionId: 'experience', + candidateId: 'infrastructure', + }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); + expect(controller.sample(sample({ + now: 2_600, + direction: 0, + deltaY: 0, + activeSectionId: 'experience', + candidateId: 'infrastructure', + }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 }); }); it('does not truncate dwell when the authoritative section passes the target', () => { @@ -236,7 +289,7 @@ describe('clock-driven sign lifecycle', () => { it.each([ ['reverse input', { direction: -1 as const, deltaY: -1 }], ['stationary input', { direction: 0 as const, deltaY: 0 }], - ['candidate before the approach window', { candidateDistanceVh: 1.200_001 }], + ['candidate before the approach window', { candidateDistanceVh: 0.550_001 }], ['candidate already passed', { candidateDistanceVh: 0 }], ['candidate is already active', { activeSectionId: 'profile' as const }], ['missing candidate', { candidateId: null }], @@ -265,4 +318,144 @@ describe('clock-driven sign lifecycle', () => { controller.reset(); expect(controller.sample(sample({ now: 100 }))).toMatchObject({ phase: 'anticipate' }); }); + + it('resolves a rapid multi-section jump to the latest active destination', () => { + const controller = createSignController(); + + expect(controller.sample(sample({ + now: 20, + deltaY: 2_400, + crossedBoundaries: 4, + activeSectionId: 'selected-builds', + candidateId: 'additional-work', + candidateDistanceVh: 0.4, + acceleration: 1, + }))).toMatchObject({ + targetId: 'selected-builds', + phase: 'anticipate', + }); + }); + + it('coalesces changing jump targets and reveals only the latest after a readable stale sign departs', () => { + const controller = createSignController(); + controller.sample(sample()); + controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); + + expect(controller.sample(sample({ + now: 1_300, + deltaY: 2_400, + crossedBoundaries: 4, + activeSectionId: 'selected-builds', + candidateId: 'additional-work', + candidateDistanceVh: 0.4, + }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); + expect(controller.sample(sample({ + now: 1_500, + deltaY: 1_200, + crossedBoundaries: 2, + activeSectionId: 'toolkit', + candidateId: 'contact', + candidateDistanceVh: 0.35, + }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); + expect(controller.sample(sample({ + now: 2_600, + direction: 0, + deltaY: 0, + crossedBoundaries: 0, + activeSectionId: 'toolkit', + candidateId: 'contact', + candidateDistanceVh: 0.35, + }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 }); + expect(controller.sample(sample({ + now: 3_300, + direction: 0, + deltaY: 0, + crossedBoundaries: 0, + activeSectionId: 'toolkit', + candidateId: 'contact', + candidateDistanceVh: 0.35, + }))).toBeNull(); + expect(controller.sample(sample({ + now: 3_650, + direction: 0, + deltaY: 0, + crossedBoundaries: 0, + activeSectionId: 'toolkit', + candidateId: 'contact', + candidateDistanceVh: 0.35, + }))).toMatchObject({ targetId: 'toolkit', phase: 'anticipate' }); + }); + + it('retains the latest jump target first observed on the departure-completion frame', () => { + const controller = createSignController(); + controller.sample(sample()); + controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); + controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); + + expect(controller.sample(sample({ + now: 3_300, + deltaY: 2_400, + crossedBoundaries: 4, + activeSectionId: 'selected-builds', + candidateId: 'additional-work', + candidateDistanceVh: 0.4, + }))).toBeNull(); + expect(controller.sample(sample({ + now: 3_650, + direction: 0, + deltaY: 0, + crossedBoundaries: 0, + activeSectionId: 'selected-builds', + candidateId: 'additional-work', + candidateDistanceVh: 0.4, + }))).toMatchObject({ targetId: 'selected-builds', phase: 'anticipate' }); + }); + + it('starts the readable dwell clock on the first rendered dwell frame after a delayed gap', () => { + const controller = createSignController(); + controller.sample(sample()); + + expect(controller.sample(sample({ + now: 5_000, + direction: 0, + deltaY: 0, + activeSectionId: 'profile', + }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); + expect(controller.sample(sample({ + now: 6_399, + direction: 0, + deltaY: 0, + activeSectionId: 'profile', + }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); + expect(controller.sample(sample({ + now: 6_400, + direction: 0, + deltaY: 0, + activeSectionId: 'profile', + }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 }); + }); + + it('does not retrigger a completed route sign during small boundary oscillations', () => { + const controller = createSignController(); + controller.sample(sample()); + controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); + controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); + controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); + + expect(controller.sample(sample({ + now: 3_700, + direction: -1, + deltaY: -16, + crossedBoundaries: 1, + activeSectionId: 'start', + }))).toBeNull(); + expect(controller.sample(sample({ + now: 3_800, + direction: 1, + deltaY: 16, + crossedBoundaries: 1, + activeSectionId: 'profile', + candidateId: 'experience', + }))).toBeNull(); + }); }); diff --git a/tests/unit/world-models.test.ts b/tests/unit/world-models.test.ts index 20fbd5e..660f95e 100644 --- a/tests/unit/world-models.test.ts +++ b/tests/unit/world-models.test.ts @@ -576,8 +576,15 @@ describe("procedural world models", () => { } const face = sign.group.getObjectByName("sign-face") as { userData: { textureUpdateCount: number }; - material: { opacity: number }; - geometry: { getAttribute(name: string): { array: ArrayLike } }; + material: { + opacity: number; + map: { anisotropy: number; image: { width: number; height: number } }; + }; + geometry: { + boundingBox: { min: { x: number; y: number }; max: { x: number; y: number } } | null; + computeBoundingBox(): void; + getAttribute(name: string): { array: ArrayLike }; + }; position: { z: number }; }; const backplate = sign.group.getObjectByName("sign-backplate") as { @@ -593,6 +600,12 @@ describe("procedural world models", () => { const faceUvs = Array.from(face.geometry.getAttribute("uv").array); expect(Math.min(...faceUvs)).toBeGreaterThanOrEqual(0); expect(Math.max(...faceUvs)).toBeLessThanOrEqual(1); + face.geometry.computeBoundingBox(); + const faceWidth = (face.geometry.boundingBox?.max.x ?? 0) - (face.geometry.boundingBox?.min.x ?? 0); + const faceHeight = (face.geometry.boundingBox?.max.y ?? 0) - (face.geometry.boundingBox?.min.y ?? 0); + expect(faceWidth / faceHeight).toBeCloseTo(4, 6); + expect(face.material.map.image).toMatchObject({ width: 2_048, height: 512 }); + expect(face.material.map.anisotropy).toBeGreaterThanOrEqual(8); const allocation = resources.snapshot(); sign.update(null); @@ -667,6 +680,94 @@ describe("procedural world models", () => { expect(sign.group.scale.x).toBeCloseTo(approachScale, 6); resources.dispose(); }); + + it("defers responsive pose changes for the complete lifecycle, including concealed anticipation", () => { + const resources = new SceneResourceBag(); + const sign = createSign(resources); + const camera = createChaseCamera(16 / 9); + sign.setViewportSize(1_440, 900); + sign.update({ targetId: "profile", phase: "anticipate", progress: 0, opacity: 0 }); + + sign.setViewportSize(844, 390); + const concealed = sign.inspect(camera, 844, 390); + expect(concealed?.viewportProfile).toBe("wide"); + sign.update({ targetId: "profile", phase: "approach", progress: 0.5, opacity: 0.7 }); + expect(sign.inspect(camera, 844, 390)?.viewportProfile).toBe("wide"); + + sign.update(null); + sign.update({ targetId: "experience", phase: "anticipate", progress: 0, opacity: 0 }); + expect(sign.inspect(camera, 844, 390)?.viewportProfile).toBe("short"); + resources.dispose(); + + const rebuiltResources = new SceneResourceBag(); + const rebuilt = createSign(rebuiltResources); + rebuilt.setViewportSize(844, 390); + rebuilt.update({ + targetId: "profile", + phase: "dwell", + progress: 1, + opacity: 1, + viewportProfile: "wide", + }); + expect(rebuilt.inspect(camera, 844, 390)?.viewportProfile).toBe("wide"); + rebuiltResources.dispose(); + }); + + it("departs laterally without crossing the chase camera plane or growing toward the car", () => { + const resources = new SceneResourceBag(); + const sign = createSign(resources); + sign.setViewportSize(1_440, 900); + sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); + const dwellX = sign.group.position.x; + const dwellScale = sign.group.scale.x; + + sign.update({ targetId: "profile", phase: "depart", progress: 0.9, opacity: 0.1 }); + + expect(sign.group.position.x).toBeLessThan(dwellX); + expect(sign.group.position.z).toBeLessThanOrEqual(0); + expect(sign.group.scale.x).toBeLessThan(dwellScale); + resources.dispose(); + }); + + it.each([ + [390, 844], + [844, 390], + [1_440, 900], + ])("keeps the responsive sign footing planted at %ix%i", (width, height) => { + const resources = new SceneResourceBag(); + const sign = createSign(resources); + sign.setViewportSize(width, height); + sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); + const footing = sign.group.getObjectByName("sign-footing") as { position: { y: number } }; + const footingWorldY = sign.group.position.y + footing.position.y * sign.group.scale.x; + + expect(footingWorldY).toBeGreaterThanOrEqual(-0.5); + expect(footingWorldY).toBeLessThanOrEqual(0.5); + resources.dispose(); + }); + + it("places route signs in the corridor opposite their destination content", () => { + const resources = new SceneResourceBag(); + const sign = createSign(resources); + sign.setViewportAspect(16 / 9); + + sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); + expect(sign.group.position.x).toBeLessThan(0); + const profileFace = sign.group.getObjectByName("sign-face"); + const camera = createChaseCamera(16 / 9); + camera.position.set(-1.35, 8.6, 22); + camera.lookAt(1.6, 4.8, -128); + sign.group.updateMatrixWorld(true); + const profileBounds = projectedBounds(profileFace, camera); + expect(profileBounds.maxX).toBeLessThan(0.08); + + sign.update(null); + sign.update({ targetId: "experience", phase: "dwell", progress: 1, opacity: 1 }); + sign.group.updateMatrixWorld(true); + const experienceBounds = projectedBounds(profileFace, camera); + expect(experienceBounds.minX).toBeGreaterThan(0); + resources.dispose(); + }); }); function paletteSnapshot(