feat: daybreak finale turns the contact arrival into a sunrise with a dawn chord

This commit is contained in:
2026-10-11 05:32:08 +03:00
parent c9e6ac0f29
commit b6e22a1299
37 changed files with 756 additions and 44 deletions

View File

@@ -25,6 +25,8 @@ import { CHAPTER_COMPOSITIONS } from '@/scene/world/composition';
import { createCoachworkEnvironment } from '@/scene/world/coachwork-light';
import { createTravelDistance } from '@/scene/world/travel-distance';
import {
blendDaybreak,
engineOffStarScale,
createMutableChapterPalette,
STORM_STAR_SCALE,
STORM_TINT,
@@ -34,6 +36,8 @@ import {
import { createMaintenanceWindow } from '@/scene/world/maintenance-window';
import { createHeadlights } from '@/scene/world/headlights';
import { maintenanceProgress } from '@/journey/maintenance-progress';
import { dawnStep, type DawnStep } from '@/journey/dawn';
import { dawnLightUniforms } from '@/scene/world/dawn-light';
import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks';
declare const __PORTFOLIO_TEST_BUILD__: boolean;
@@ -161,6 +165,10 @@ export async function createProceduralSceneGeneration(
|| (typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)
? 'lite'
: 'full';
// END OF SHIFT: the low tier and touch keep the palette, the sun lift and
// the star drain (uniform writes); the warm rims and the bloom change are
// high/medium fine-pointer only.
const dawnRims = maintenanceTier === 'full';
scene.add(
maintenance.group,
atmosphere.group,
@@ -218,6 +226,7 @@ export async function createProceduralSceneGeneration(
let interludeOn = false;
let riseUntil = 0;
let gantryState = '';
let dawnState: DawnStep | '' = '';
const resize = (): void => {
const nextWidth = Math.max(1, canvas.clientWidth || canvas.width || 1);
@@ -342,14 +351,20 @@ export async function createProceduralSceneGeneration(
targetPalette.starVisibility *= 1 - (1 - STORM_STAR_SCALE) * service.storm;
}
districts.setServiceState(service.dim, service.relight);
// Engine off: parked at the destination the sky drops about 10%, more
// stars come out, and the drive winds down to a stop.
// 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);
blendDaybreak(targetPalette, THREE.MathUtils.smoothstep(dawn, 0.15, 0.95));
// Engine off: parked at the destination the sky drops about 10%, the
// stars come out (then drain as the dawn rises), and the drive winds
// down to a stop.
const engineOff = THREE.MathUtils.smoothstep(outroProgress, 0.12, 0.7);
if (engineOff > 0) {
targetPalette.sky.lerp(ENGINE_OFF_NIGHT, 0.1 * engineOff);
targetPalette.fog.lerp(ENGINE_OFF_NIGHT, 0.1 * engineOff);
targetPalette.starVisibility *= 1 + 0.8 * engineOff;
targetPalette.starVisibility *= engineOffStarScale(engineOff, dawn);
}
dawnLightUniforms.uDawnLight.value = dawnRims ? dawn : 0;
dampPalette(currentPalette, targetPalette, deltaSeconds);
const driveDistance = travelDistance.advance(
deltaSeconds * (1 - engineOff),
@@ -394,7 +409,7 @@ export async function createProceduralSceneGeneration(
);
atmosphericTime += deltaSeconds;
const elapsedSeconds = atmosphericTime;
horizonAir.update(currentPalette, elapsedSeconds);
horizonAir.update(currentPalette, elapsedSeconds, dawn);
skyline.update(currentPalette, elapsedSeconds);
traffic.update(
deltaSeconds,
@@ -407,6 +422,7 @@ export async function createProceduralSceneGeneration(
elapsedSeconds,
Math.min(1, response.bloomEnergy + beat * 0.35),
snapshot.arrived,
dawn,
);
car.update(
visualSpeed,
@@ -437,6 +453,7 @@ export async function createProceduralSceneGeneration(
currentPalette,
atmosphericTime,
overdrive,
dawn,
);
background.copy(currentPalette.sky);
scene.fog.color.copy(currentPalette.fog);
@@ -644,6 +661,12 @@ export async function createProceduralSceneGeneration(
riseUntil = 0;
delete document.documentElement.dataset.interlude;
}
// END OF SHIFT: CSS keys the credit and the letterbox tint off this.
const nextDawnState = dawnStep(dawn);
if (nextDawnState !== dawnState) {
dawnState = nextDawnState;
document.documentElement.dataset.dawn = nextDawnState;
}
if (TEST_BUILD && snapshot.sign !== null) {
const presentation = sign.inspect(camera, width, height);
if (presentation !== null) {
@@ -663,13 +686,14 @@ export async function createProceduralSceneGeneration(
}
}
if (postProcessor) {
postProcessor.setDynamics(response.bloomEnergy);
postProcessor.setDynamics(response.bloomEnergy, dawnRims ? dawn : 0);
postProcessor.setLens({
time: elapsedSeconds,
letterbox: 0.115 * (1 - THREE.MathUtils.smoothstep(introProgress, 0.5, 1)),
energy: overdrive,
rain: service.rain,
rainTime: elapsedSeconds,
dawn,
});
postProcessor.render(scene, camera);
} else {
@@ -683,6 +707,8 @@ export async function createProceduralSceneGeneration(
delete document.documentElement.dataset.gantry;
delete document.documentElement.dataset.interlude;
delete document.documentElement.dataset.interludeArmed;
delete document.documentElement.dataset.dawn;
dawnLightUniforms.uDawnLight.value = 0;
// 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();
@@ -694,7 +720,8 @@ export async function createProceduralSceneGeneration(
}
const INTRO_SECONDS = 4.2;
const OUTRO_SECONDS = 5.5;
/** Long enough for the dawn to read: the sun clears the skyline near 3.8 s. */
const OUTRO_SECONDS = 7.0;
const OUTRO_RETURN_SECONDS = 1.6;
const OUTRO_REACH = 0.62;
const ENGINE_OFF_NIGHT = new THREE.Color(0x000000);