From 2cbf42f7f869e1bde362c0e302b8d484798b1d5e Mon Sep 17 00:00:00 2001 From: LucaP Date: Sun, 11 Oct 2026 00:12:37 +0300 Subject: [PATCH] fix: step the reading plates aside at click time on long navigator jumps --- src/journey/navigation-controller.ts | 37 +++++++++++++++++++++++++--- src/scene/world/environment.ts | 3 +++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/journey/navigation-controller.ts b/src/journey/navigation-controller.ts index 462dd2f..1062eae 100644 --- a/src/journey/navigation-controller.ts +++ b/src/journey/navigation-controller.ts @@ -94,6 +94,35 @@ export function validateSectionAnchor( return heading === null ? null : { id, target }; } +/** 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; + +/** + * A long navigator scroll passes the chapters in between. While the scene runs + * (the loop has set data-gantry), the reading plates step aside at click time + * instead of when the sign starts its approach, so those chapters do not flash + * past. The loop takes the attribute over as soon as the sign's interlude + * starts; if no interlude follows, the plates rise again after a timeout. + */ +const preArmInterlude = (): void => { + const root = document.documentElement; + if (root.dataset.gantry === undefined || root.dataset.interlude === "on") return; + root.dataset.interlude = "on"; + root.dataset.interludeArmed = "nav"; + window.setTimeout(() => { + if (root.dataset.interludeArmed !== "nav") return; + root.dataset.interlude = "rise"; + root.dataset.interludeArmed = "rise"; + window.setTimeout(() => { + if (root.dataset.interludeArmed !== "rise") return; + delete root.dataset.interlude; + delete root.dataset.interludeArmed; + }, PRE_ARM_RISE_MS); + }, PRE_ARM_TIMEOUT_MS); +}; + const browserRuntime = (): NavigationRuntime => ({ getHash: () => window.location.hash, pushHash: (hash) => window.history.pushState(window.history.state, "", hash), @@ -107,10 +136,10 @@ const browserRuntime = (): NavigationRuntime => ({ return; } const margin = parseFloat(getComputedStyle(target).scrollMarginTop) || 0; - window.scrollTo({ - top: window.scrollY + target.getBoundingClientRect().top - margin, - behavior, - }); + const delta = target.getBoundingClientRect().top - margin; + if (behavior === "smooth" && Math.abs(delta) > window.innerHeight * 0.6) + preArmInterlude(); + window.scrollTo({ top: window.scrollY + delta, behavior }); }, focusHeading: (target) => { // The live reading stage can start or stop while a long journey is still diff --git a/src/scene/world/environment.ts b/src/scene/world/environment.ts index 352428e..be6cbb0 100644 --- a/src/scene/world/environment.ts +++ b/src/scene/world/environment.ts @@ -590,6 +590,8 @@ export async function createProceduralSceneGeneration( interludeOn = interlude; riseUntil = interlude ? 0 : now + INTERLUDE_RISE_MS; document.documentElement.dataset.interlude = interlude ? 'on' : 'rise'; + // A navigator click may have pre-armed the interlude; the loop owns it now. + delete document.documentElement.dataset.interludeArmed; } else if (riseUntil > 0 && now > riseUntil) { riseUntil = 0; delete document.documentElement.dataset.interlude; @@ -630,6 +632,7 @@ export async function createProceduralSceneGeneration( disposed = true; delete document.documentElement.dataset.gantry; delete document.documentElement.dataset.interlude; + delete document.documentElement.dataset.interludeArmed; // Quality changes reuse the canvas context. Leave texture upload flags at // defaults before the next renderer creates its empty array textures. const gl = renderer.getContext();