diff --git a/src/journey/navigation-controller.ts b/src/journey/navigation-controller.ts index 1062eae..bd8d6dd 100644 --- a/src/journey/navigation-controller.ts +++ b/src/journey/navigation-controller.ts @@ -97,7 +97,7 @@ export function validateSectionAnchor( /** How long a pre-armed interlude waits for the scene loop to take over. */ const PRE_ARM_TIMEOUT_MS = 1400; /** Matches INTERLUDE_RISE_MS in the environment loop. */ -const PRE_ARM_RISE_MS = 1100; +const PRE_ARM_RISE_MS = 800; /** * A long navigator scroll passes the chapters in between. While the scene runs diff --git a/src/scene/world/environment.ts b/src/scene/world/environment.ts index be6cbb0..e2c5831 100644 --- a/src/scene/world/environment.ts +++ b/src/scene/world/environment.ts @@ -649,7 +649,7 @@ const OUTRO_RETURN_SECONDS = 1.6; const OUTRO_REACH = 0.62; const ENGINE_OFF_NIGHT = new THREE.Color(0x000000); /** Plates rise back over 720 ms with up to three 90 ms staggers. */ -const INTERLUDE_RISE_MS = 1100; +const INTERLUDE_RISE_MS = 800; function easeInOutCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; diff --git a/src/scene/world/sign.ts b/src/scene/world/sign.ts index 690569f..c910ec2 100644 --- a/src/scene/world/sign.ts +++ b/src/scene/world/sign.ts @@ -47,8 +47,12 @@ const TILT_RADIANS = 0.07; const HANDOFF_MS = 200; /** Above this scroll drive speed a passive crossing skips the interlude. */ const BRISK_DRIVE_SPEED = 0.5; -/** Share of the approach spent on the truss before the arc into the window. */ -const ON_TRUSS = 0.7; +/** Share of the approach spent on the truss before the arc into the window. + * Round 6 trimmed the hold so the arc carries more of the approach. */ +const ON_TRUSS = 0.62; +/** The reading plates start to rise about 180 ms (of a 900 ms approach) + * before the sign docks, so the interlude closes in about 1.5 s. */ +const PLATE_RISE_PROGRESS = 0.8; const SIGN_MARKER_FONT = '600 60px "Geist Mono Variable", ui-monospace, monospace'; @@ -524,7 +528,8 @@ export function createSign( return state; }, interlude() { - return !brisk && state === 'incoming'; + if (brisk || state !== 'incoming') return false; + return !(snapshot?.phase === 'approach' && snapshot.progress >= PLATE_RISE_PROGRESS); }, compose(camera, width, height, dock, now, tilt = REST_TILT) { const dt = Math.min(0.05, Math.max(0, (now - previousNow) / 1000)); diff --git a/src/styles/gantry.css b/src/styles/gantry.css index d3e6b7f..d5a8760 100644 --- a/src/styles/gantry.css +++ b/src/styles/gantry.css @@ -708,45 +708,47 @@ html .drive-controls__audio-status { /* ---------- The interlude under the gantry ---------- */ /* While a sign approaches (environment.ts sets data-interlude='on'), the - reading plates step up and out in 260 ms, so the road and the sign have - the frame to themselves; then ('rise') they come back up from below in - sequence. Headings only fade: journey and audio measure them. */ + reading plates step up and out in 180 ms, so the road and the sign have + the frame to themselves; the headings follow once the plates are gone, so + a departing plate never ghosts over an arriving title. Then ('rise', about + 180 ms before the sign docks) they come back up from below in sequence. + Headings only fade: journey and audio measure them. */ @media (prefers-reduced-motion: no-preference) { html[data-interlude='on'] .reading-stage .portfolio-main > * > * { opacity: 0; - transition: opacity 260ms cubic-bezier(0.5, 0, 0.75, 0); + transition: opacity 200ms cubic-bezier(0.5, 0, 0.75, 0) 160ms; } html[data-interlude='on'] .reading-stage .portfolio-main > * > :not(h1, h2, :has(h1, h2)) { transform: translateY(-24px); transition: - opacity 260ms cubic-bezier(0.5, 0, 0.75, 0), - transform 260ms cubic-bezier(0.5, 0, 0.75, 0); + opacity 180ms cubic-bezier(0.5, 0, 0.75, 0), + transform 180ms cubic-bezier(0.5, 0, 0.75, 0); } /* The exit tag's strike-on animation restarts as the plates return. */ html[data-interlude='on'] .reading-stage .portfolio-section::before, html[data-interlude='on'] .reading-stage .portfolio-section[data-exit='08']::after { opacity: 0; animation: none; - transition: opacity 260ms cubic-bezier(0.5, 0, 0.75, 0); + transition: opacity 180ms cubic-bezier(0.5, 0, 0.75, 0); } html[data-interlude='rise'] .reading-stage .portfolio-section[data-exit='08']::after, html[data-interlude='rise'] .reading-stage .portfolio-main > .portfolio-section > *, html[data-interlude='rise'] .reading-stage .portfolio-main > .hero-journey > * { - animation: plate-fade 720ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation: plate-fade 400ms cubic-bezier(0.22, 1, 0.36, 1) both; } html[data-interlude='rise'] .reading-stage .portfolio-main > .portfolio-section > :not(h1, h2, :has(h1, h2)), html[data-interlude='rise'] .reading-stage .portfolio-main > .hero-journey > :not(h1, h2, :has(h1, h2)) { - animation: plate-rise 720ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation: plate-rise 400ms cubic-bezier(0.22, 1, 0.36, 1) both; } /* The chapter body keeps its light-edge sweep alongside the rise. */ html[data-interlude='rise'] .reading-stage .portfolio-main > .portfolio-section[data-current] > .chapter-body { animation: - plate-rise 720ms cubic-bezier(0.22, 1, 0.36, 1) both, + plate-rise 400ms cubic-bezier(0.22, 1, 0.36, 1) both, exit-panel-light 1.6s cubic-bezier(0.2, 0.75, 0.2, 1) both; } - html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(2) { animation-delay: 90ms; } - html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(3) { animation-delay: 180ms; } - html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(n + 4) { animation-delay: 270ms; } + html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(2) { animation-delay: 40ms; } + html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(3) { animation-delay: 80ms; } + html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(n + 4) { animation-delay: 120ms; } } @keyframes plate-rise { from { opacity: 0; transform: translateY(28px); } diff --git a/tests/unit/sign-rendering.test.ts b/tests/unit/sign-rendering.test.ts index 530ad7c..ba1120c 100644 --- a/tests/unit/sign-rendering.test.ts +++ b/tests/unit/sign-rendering.test.ts @@ -164,7 +164,7 @@ describe('composed route signs', () => { const h = setup(); h.render({ ...profile, phase: 'approach', progress: 0, opacity: 1 }, 30); const early = h.render({ ...profile, phase: 'approach', progress: 0.2, opacity: 1 })!.bounds; - const big = h.render({ ...profile, phase: 'approach', progress: 0.69, opacity: 1 })!.bounds; + const big = h.render({ ...profile, phase: 'approach', progress: 0.61, opacity: 1 })!.bounds; const arcing = h.render({ ...profile, phase: 'approach', progress: 0.85, opacity: 1 })!.bounds; const docked = h.render(profile)!.bounds; expect(h.sign.dockState()).toBe('docked');