feat: sign interlude, window plate and a two-button control bay

The sign closes in over the road on a gantry truss, arcs into a wider
window and hands off to a flat 2D plate that mirrors its face; content
steps aside for the interlude and rises after. The bay keeps sound and
settings; volume and Pause motion move into the settings popover.
This commit is contained in:
2026-10-10 23:30:58 +03:00
parent 5757dd968f
commit bacc035c43
12 changed files with 823 additions and 287 deletions

View File

@@ -154,6 +154,7 @@ export async function createProceduralSceneGeneration(
car.group,
speedWake.group,
sign.group,
sign.truss,
);
const postProcessor = quality.postEffects
@@ -192,6 +193,8 @@ export async function createProceduralSceneGeneration(
// The outro mirrors the intro: on arrival the camera cranes back up toward
// the sun on a long lens. Leaving the destination reverses it, faster.
let outroProgress = 0;
let interludeOn = false;
let riseUntil = 0;
let gantryState = '';
const resize = (): void => {
@@ -373,7 +376,8 @@ export async function createProceduralSceneGeneration(
car.group.position,
width < 1100 || height < 620 ? 2.32 : 2.78,
);
sign.update(snapshot.sign, now);
sign.setResting(snapshot.activeSectionId);
sign.update(snapshot.sign, now, snapshot.driveSpeed);
atmosphere.update(
driveDistance,
visualSpeed,
@@ -568,6 +572,18 @@ export async function createProceduralSceneGeneration(
gantryState = gantry;
document.documentElement.dataset.gantry = gantry;
}
// The interlude: the reading plates step aside while the sign has the
// road to itself (CSS keys off this attribute).
// 'on' while the sign approaches, then 'rise' while the plates return.
const interlude = sign.interlude();
if (interlude !== interludeOn) {
interludeOn = interlude;
riseUntil = interlude ? 0 : now + INTERLUDE_RISE_MS;
document.documentElement.dataset.interlude = interlude ? 'on' : 'rise';
} else if (riseUntil > 0 && now > riseUntil) {
riseUntil = 0;
delete document.documentElement.dataset.interlude;
}
if (TEST_BUILD && snapshot.sign !== null) {
const presentation = sign.inspect(camera, width, height);
if (presentation !== null) {
@@ -603,6 +619,7 @@ export async function createProceduralSceneGeneration(
if (disposed) return;
disposed = true;
delete document.documentElement.dataset.gantry;
delete document.documentElement.dataset.interlude;
// 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();
@@ -617,6 +634,8 @@ const INTRO_SECONDS = 4.2;
const OUTRO_SECONDS = 5.5;
const OUTRO_RETURN_SECONDS = 1.6;
const OUTRO_REACH = 0.62;
/** Plates rise back over 720 ms with up to three 90 ms staggers. */
const INTERLUDE_RISE_MS = 1100;
function easeInOutCubic(t: number): number {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;