style: trim the interlude to about 1.5 s

This commit is contained in:
2026-10-11 00:43:25 +03:00
parent fc897e534c
commit c8bc75cdcc
5 changed files with 26 additions and 19 deletions

View File

@@ -97,7 +97,7 @@ export function validateSectionAnchor(
/** How long a pre-armed interlude waits for the scene loop to take over. */ /** How long a pre-armed interlude waits for the scene loop to take over. */
const PRE_ARM_TIMEOUT_MS = 1400; const PRE_ARM_TIMEOUT_MS = 1400;
/** Matches INTERLUDE_RISE_MS in the environment loop. */ /** 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 * A long navigator scroll passes the chapters in between. While the scene runs

View File

@@ -649,7 +649,7 @@ const OUTRO_RETURN_SECONDS = 1.6;
const OUTRO_REACH = 0.62; const OUTRO_REACH = 0.62;
const ENGINE_OFF_NIGHT = new THREE.Color(0x000000); const ENGINE_OFF_NIGHT = new THREE.Color(0x000000);
/** Plates rise back over 720 ms with up to three 90 ms staggers. */ /** 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 { function easeInOutCubic(t: number): number {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

View File

@@ -47,8 +47,12 @@ const TILT_RADIANS = 0.07;
const HANDOFF_MS = 200; const HANDOFF_MS = 200;
/** Above this scroll drive speed a passive crossing skips the interlude. */ /** Above this scroll drive speed a passive crossing skips the interlude. */
const BRISK_DRIVE_SPEED = 0.5; const BRISK_DRIVE_SPEED = 0.5;
/** Share of the approach spent on the truss before the arc into the window. */ /** Share of the approach spent on the truss before the arc into the window.
const ON_TRUSS = 0.7; * 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 = const SIGN_MARKER_FONT =
'600 60px "Geist Mono Variable", ui-monospace, monospace'; '600 60px "Geist Mono Variable", ui-monospace, monospace';
@@ -524,7 +528,8 @@ export function createSign(
return state; return state;
}, },
interlude() { 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) { compose(camera, width, height, dock, now, tilt = REST_TILT) {
const dt = Math.min(0.05, Math.max(0, (now - previousNow) / 1000)); const dt = Math.min(0.05, Math.max(0, (now - previousNow) / 1000));

View File

@@ -708,45 +708,47 @@ html .drive-controls__audio-status {
/* ---------- The interlude under the gantry ---------- */ /* ---------- The interlude under the gantry ---------- */
/* While a sign approaches (environment.ts sets data-interlude='on'), the /* 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 reading plates step up and out in 180 ms, so the road and the sign have
the frame to themselves; then ('rise') they come back up from below in the frame to themselves; the headings follow once the plates are gone, so
sequence. Headings only fade: journey and audio measure them. */ 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) { @media (prefers-reduced-motion: no-preference) {
html[data-interlude='on'] .reading-stage .portfolio-main > * > * { html[data-interlude='on'] .reading-stage .portfolio-main > * > * {
opacity: 0; 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)) { html[data-interlude='on'] .reading-stage .portfolio-main > * > :not(h1, h2, :has(h1, h2)) {
transform: translateY(-24px); transform: translateY(-24px);
transition: transition:
opacity 260ms cubic-bezier(0.5, 0, 0.75, 0), opacity 180ms cubic-bezier(0.5, 0, 0.75, 0),
transform 260ms 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. */ /* 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::before,
html[data-interlude='on'] .reading-stage .portfolio-section[data-exit='08']::after { html[data-interlude='on'] .reading-stage .portfolio-section[data-exit='08']::after {
opacity: 0; opacity: 0;
animation: none; 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-section[data-exit='08']::after,
html[data-interlude='rise'] .reading-stage .portfolio-main > .portfolio-section > *, html[data-interlude='rise'] .reading-stage .portfolio-main > .portfolio-section > *,
html[data-interlude='rise'] .reading-stage .portfolio-main > .hero-journey > * { 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 > .portfolio-section > :not(h1, h2, :has(h1, h2)),
html[data-interlude='rise'] .reading-stage .portfolio-main > .hero-journey > :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. */ /* 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 { html[data-interlude='rise'] .reading-stage .portfolio-main > .portfolio-section[data-current] > .chapter-body {
animation: 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; 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(2) { animation-delay: 40ms; }
html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(3) { animation-delay: 180ms; } 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: 270ms; } html[data-interlude='rise'] .reading-stage .portfolio-main > * > :nth-child(n + 4) { animation-delay: 120ms; }
} }
@keyframes plate-rise { @keyframes plate-rise {
from { opacity: 0; transform: translateY(28px); } from { opacity: 0; transform: translateY(28px); }

View File

@@ -164,7 +164,7 @@ describe('composed route signs', () => {
const h = setup(); const h = setup();
h.render({ ...profile, phase: 'approach', progress: 0, opacity: 1 }, 30); h.render({ ...profile, phase: 'approach', progress: 0, opacity: 1 }, 30);
const early = h.render({ ...profile, phase: 'approach', progress: 0.2, opacity: 1 })!.bounds; 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 arcing = h.render({ ...profile, phase: 'approach', progress: 0.85, opacity: 1 })!.bounds;
const docked = h.render(profile)!.bounds; const docked = h.render(profile)!.bounds;
expect(h.sign.dockState()).toBe('docked'); expect(h.sign.dockState()).toBe('docked');