feat: floodlit roadside billboard carries the selected-builds case studies

This commit is contained in:
2026-10-11 07:09:31 +03:00
parent 19f3ce79e6
commit d71b0ecfd4
31 changed files with 1216 additions and 6 deletions

View File

@@ -37,6 +37,8 @@ import { createMaintenanceWindow } from '@/scene/world/maintenance-window';
import { createHeadlights } from '@/scene/world/headlights';
import { maintenanceProgress } from '@/journey/maintenance-progress';
import { createMilestones } from '@/scene/world/milestones';
import { billboardNear, createBillboard, textureSize } from '@/scene/world/billboard';
import { BILLBOARD_NUDGE } from '@/scene/world/composition';
import { isMilestoneSection, sectionLocalProgress } from '@/journey/route-log';
import { dawnStep, type DawnStep } from '@/journey/dawn';
import { dawnLightUniforms } from '@/scene/world/dawn-light';
@@ -174,7 +176,15 @@ export async function createProceduralSceneGeneration(
// MILE MARKERS: kilometre posts roll past through Experience and Profile.
// Low tier and touch draw them without beam, pulse or flash.
const milestones = createMilestones(context.resources, maintenanceTier === 'full');
// DRIVE-IN: the Selected builds billboard. Low tier and touch: one steady
// flood, no pulse, flicker or hover zoom, 512 px screens.
const billboard = createBillboard(context.resources, {
mode: maintenanceTier,
textureSize: textureSize(context.quality),
});
let billboardKey = '';
scene.add(
billboard.mesh,
maintenance.group,
milestones.mesh,
milestones.pool,
@@ -366,6 +376,19 @@ export async function createProceduralSceneGeneration(
musicPulse,
deltaSeconds,
);
const billboardNudge = billboard.update({
section: snapshot.activeSectionId,
p: snapshot.activeSectionId === 'selected-builds'
? sectionLocalProgress(snapshot, 'selected-builds')
: null,
direction: snapshot.direction,
near: billboardNear(snapshot),
pulse: musicPulse,
energy: overdrive,
focus: document.documentElement.dataset.billboardFocus,
narrow: camera.aspect <= 1.18,
deltaSeconds,
});
// END OF SHIFT: parked at the destination, the night ends. One scalar
// drives it all; leaving rewinds it as fast as the outro returns.
const dawn = easeInOutCubic(outroProgress);
@@ -511,7 +534,7 @@ export async function createProceduralSceneGeneration(
baseComposition.targetZ,
upcoming.targetZ,
anticipation,
),
) + BILLBOARD_NUDGE.targetZ * billboardNudge,
fov: THREE.MathUtils.lerp(
baseComposition.fov,
upcoming.fov,
@@ -521,7 +544,7 @@ export async function createProceduralSceneGeneration(
baseComposition.shift,
upcoming.shift,
anticipation,
),
) + BILLBOARD_NUDGE.shift * billboardNudge,
};
// The document stays native. Only the rendered lens eases to the chapter's safe zone.
const cameraX = wideComposition ? composition.x : 0;
@@ -682,6 +705,16 @@ export async function createProceduralSceneGeneration(
dawnState = nextDawnState;
document.documentElement.dataset.dawn = nextDawnState;
}
if (TEST_BUILD) {
const board = billboard.inspect();
const key = `${board.progress.activeProjectId}|${board.shown}|${board.floods.map(Math.round).join()}|${Math.round(board.lit * 10)}|${Math.round(board.rise * 10)}|${Math.round(board.zoom * 10)}|${board.flickers}|${board.shimmers}|${board.textures}`;
if (key !== billboardKey) {
billboardKey = key;
window.dispatchEvent(new CustomEvent('portfolio-billboard-frame', {
detail: { now, activeSectionId: snapshot.activeSectionId, direction: snapshot.direction, ...board },
}));
}
}
if (TEST_BUILD && snapshot.sign !== null) {
const presentation = sign.inspect(camera, width, height);
if (presentation !== null) {