feat: refine nightdrive world and authored car

Integrate the Blender grand tourer with a cohesive shader-driven road, terrain, atmosphere, lighting, and post-processing pass. Refine responsive drive dynamics and sign lifecycle behavior, and add durable visual, accessibility, and performance verification evidence.
This commit is contained in:
2026-09-02 23:16:25 +03:00
parent 7dae835ac5
commit bc18bc2043
73 changed files with 1555 additions and 293 deletions

View File

@@ -4,12 +4,14 @@ import type {
ScrollDriveSnapshot,
} from './types';
const MAX_RAW_SPEED = 2.5;
const RISE_TIME_CONSTANT_MS = 90;
const DECAY_TIME_CONSTANT_MS = 420;
const MAX_VIEWPORTS_PER_SECOND = 3;
const RISE_TIME_CONSTANT_MS = 180;
const DECAY_TIME_CONSTANT_MS = 1_050;
const CRUISE_SPEED = 0.18;
const ACCELERATION_SPEED_RANGE = 0.82;
const BACKGROUND_GAP_MS = 250;
const MAX_INTEGRATION_INTERVAL_MS = 120;
const DEFAULT_VIEWPORT_HEIGHT = 800;
const DECAY_SETTLE_EPSILON = 0.001;
const clamp = (value: number, minimum: number, maximum: number): number =>
@@ -18,7 +20,10 @@ const clamp = (value: number, minimum: number, maximum: number): number =>
const isFiniteSample = (input: ScrollDriveSample): boolean =>
Number.isFinite(input.scrollY) &&
Number.isFinite(input.now) &&
Number.isFinite(input.maxScrollY);
Number.isFinite(input.maxScrollY) &&
(input.viewportHeight === undefined || (
Number.isFinite(input.viewportHeight) && input.viewportHeight > 0
));
const initialSnapshot = (): ScrollDriveSnapshot => ({
scrollY: 0,
@@ -76,18 +81,25 @@ export function createScrollDriveController(): ScrollDriveController {
const deltaY = input.scrollY - previousSample.scrollY;
const direction: -1 | 0 | 1 = deltaY > 0 ? 1 : deltaY < 0 ? -1 : 0;
if (elapsedMs > BACKGROUND_GAP_MS) {
previousSample = { ...input };
smoothedAcceleration = 0;
return { ...cruiseSnapshot(input), direction };
}
const rawSpeed = Math.abs(deltaY) / elapsedMs;
const impulse = clamp(rawSpeed / MAX_RAW_SPEED, 0, 1);
const integrationMs = elapsedMs > BACKGROUND_GAP_MS
? MAX_INTEGRATION_INTERVAL_MS
: elapsedMs;
const viewportHeight = Math.max(
1,
input.viewportHeight ?? previousSample.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT,
);
const viewportVelocity = elapsedMs > BACKGROUND_GAP_MS
? 0
: (Math.abs(deltaY) / viewportHeight) / (elapsedMs / 1_000);
const impulse = clamp(
viewportVelocity / MAX_VIEWPORTS_PER_SECOND,
0,
1,
);
const timeConstantMs = impulse > smoothedAcceleration
? RISE_TIME_CONSTANT_MS
: DECAY_TIME_CONSTANT_MS;
const alpha = 1 - Math.exp(-elapsedMs / timeConstantMs);
const alpha = 1 - Math.exp(-integrationMs / timeConstantMs);
smoothedAcceleration += (impulse - smoothedAcceleration) * alpha;
if (impulse === 0 && smoothedAcceleration < DECAY_SETTLE_EPSILON) {
smoothedAcceleration = 0;

View File

@@ -13,6 +13,17 @@ const DEPART_MS = 700;
const COOLDOWN_MS = 350;
const ELIGIBLE_DISTANCE_VH = 1.2;
const JUMP_DISTANCE_VH = 0.75;
const CONCEALED_OPACITY = 0.02;
const SECTION_ORDER: readonly SectionId[] = [
'start',
'profile',
'experience',
'infrastructure',
'selected-builds',
'additional-work',
'toolkit',
'contact',
];
interface VisibleState {
kind: 'visible';
@@ -20,6 +31,7 @@ interface VisibleState {
latchedAt: number;
approachDuration: number;
departStartedAt: number | null;
generation: number;
}
type State =
@@ -62,16 +74,43 @@ const canLatch = (input: SignControllerSample): input is SignControllerSample &
export function createSignController(): SignController {
let state: State = { kind: 'idle' };
let lastNow: number | null = null;
let generation = 0;
const reset = (now?: number): void => {
generation += 1;
state = { kind: 'idle' };
lastNow = now !== undefined && Number.isFinite(now) ? now : null;
};
const enterCooldown = (now: number): void => {
generation += 1;
state = { kind: 'cooldown', until: now + COOLDOWN_MS };
};
const latch = (
input: SignControllerSample & {
candidateId: SectionId;
candidateDistanceVh: number;
},
): VisibleState => {
generation += 1;
return {
kind: 'visible',
targetId: input.candidateId,
latchedAt: input.now,
approachDuration: APPROACH_MS - APPROACH_REDUCTION_MS * clamp01(input.acceleration),
departStartedAt: null,
generation,
};
};
const beginDeparture = (visible: VisibleState, now: number): void => {
if (visible.departStartedAt !== null) return;
generation += 1;
visible.generation = generation;
visible.departStartedAt = now;
};
const snapshotVisible = (visible: VisibleState, now: number): SignSnapshot | null => {
if (visible.departStartedAt !== null) {
const progress = clamp01((now - visible.departStartedAt) / DEPART_MS);
@@ -126,15 +165,23 @@ export function createSignController(): SignController {
}
lastNow = input.now;
const suppressed =
!input.motionAllowed ||
input.source !== 'passive-scroll' ||
if (!input.motionAllowed || input.source !== 'passive-scroll') {
reset(input.now);
return null;
}
const discontinuous =
input.crossedBoundaries > 1 ||
Math.abs(input.deltaY) > input.viewportHeight * JUMP_DISTANCE_VH;
if (suppressed) {
if (!input.motionAllowed) reset(input.now);
else if (state.kind === 'visible') enterCooldown(input.now);
return null;
if (discontinuous) {
if (state.kind !== 'visible') return null;
const current = snapshotVisible(state, input.now);
if (current === null || current.opacity <= CONCEALED_OPACITY) {
enterCooldown(input.now);
return null;
}
beginDeparture(state, input.now);
return snapshotVisible(state, input.now);
}
if (state.kind === 'cooldown') {
@@ -144,13 +191,31 @@ export function createSignController(): SignController {
if (state.kind === 'idle') {
if (!canLatch(input)) return null;
state = {
kind: 'visible',
targetId: input.candidateId,
latchedAt: input.now,
approachDuration: APPROACH_MS - APPROACH_REDUCTION_MS * clamp01(input.acceleration),
departStartedAt: null,
};
state = latch(input);
return snapshotVisible(state, input.now);
}
const current = snapshotVisible(state, input.now);
if (current === null) return null;
if (
current.opacity <= CONCEALED_OPACITY
&& canLatch(input)
&& input.candidateId !== state.targetId
) {
state = latch(input);
return snapshotVisible(state, input.now);
}
const activeIndex = SECTION_ORDER.indexOf(input.activeSectionId);
const targetIndex = SECTION_ORDER.indexOf(state.targetId);
const targetPassed = activeIndex >= 0 && targetIndex >= 0 && activeIndex > targetIndex;
if (targetPassed && state.departStartedAt === null) {
if (current.opacity <= CONCEALED_OPACITY) {
enterCooldown(input.now);
return null;
}
beginDeparture(state, input.now);
return snapshotVisible(state, input.now);
}
@@ -162,7 +227,7 @@ export function createSignController(): SignController {
return null;
}
if (state.departStartedAt === null && input.now >= dwellStartedAt) {
state.departStartedAt = input.now;
beginDeparture(state, input.now);
}
}
@@ -170,7 +235,7 @@ export function createSignController(): SignController {
const dwellStartedAt = state.latchedAt + ANTICIPATE_MS + state.approachDuration;
const dwellComplete = input.now >= dwellStartedAt + DWELL_MS;
if (dwellComplete && input.activeSectionId === state.targetId) {
state.departStartedAt = input.now;
beginDeparture(state, input.now);
}
}

View File

@@ -4,6 +4,7 @@ export interface ScrollDriveSample {
scrollY: number;
now: number;
maxScrollY: number;
viewportHeight?: number;
}
export interface ScrollDriveSnapshot {