feat: weave speed-reactive terrain and ground roadside signs
Anchor desktop signs beside the road and feather the compact reading edge with measured chapter landings. Add a continuous cyan-to-coral terrain grid and a bounded rear wake that builds with existing drive speed. Preserve approved content, media, soundtrack, and canonical journey contracts.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
type SignViewportProfile,
|
||||
} from '@/journey/sign-routes';
|
||||
import type { SceneResourceBag } from '@/scene/resources';
|
||||
import { roadsideSignMetrics, usesRoadsideSign } from './sign-layout';
|
||||
|
||||
interface ProjectingCamera {
|
||||
position: { x: number; y: number; z: number };
|
||||
@@ -32,6 +33,7 @@ export interface SignWorld {
|
||||
height: number,
|
||||
navigationBottom: number,
|
||||
now: number,
|
||||
carAnchor?: { x: number; z: number },
|
||||
): void;
|
||||
setViewportAspect(aspect: number): void;
|
||||
setViewportSize(width: number, height: number): void;
|
||||
@@ -407,6 +409,11 @@ export function createSign(
|
||||
let screenY = 120;
|
||||
let screenWidth = 340;
|
||||
let initialized = false;
|
||||
let roadsideMix = 0;
|
||||
let previousPhase: SignSnapshot['phase'] | null = null;
|
||||
let dwellDrift = 0;
|
||||
let departureDrift = 0;
|
||||
const physical = roadsideSignMetrics(900);
|
||||
const point = new THREE.Vector3();
|
||||
const ray = new THREE.Vector3();
|
||||
const forward = new THREE.Vector3();
|
||||
@@ -498,9 +505,13 @@ export function createSign(
|
||||
if (!sign) {
|
||||
group.visible = false;
|
||||
initialized = false;
|
||||
previousPhase = null;
|
||||
dwellDrift = 0;
|
||||
return;
|
||||
}
|
||||
if (target !== sign.targetId) {
|
||||
previousPhase = null;
|
||||
dwellDrift = 0;
|
||||
target = sign.targetId;
|
||||
face.userData.textureUpdateCount += 1;
|
||||
face.userData.fontReadyWhenDrawn =
|
||||
@@ -516,7 +527,14 @@ export function createSign(
|
||||
initialized = false;
|
||||
}
|
||||
},
|
||||
compose(camera, width, height, navigationBottom, now) {
|
||||
compose(
|
||||
camera,
|
||||
width,
|
||||
height,
|
||||
navigationBottom,
|
||||
now,
|
||||
carAnchor = { x: 0, z: 0.72 },
|
||||
) {
|
||||
if (!snapshot) return;
|
||||
viewportWidth = width;
|
||||
viewportHeight = height;
|
||||
@@ -546,6 +564,8 @@ export function createSign(
|
||||
const dt = Math.min(0.05, Math.max(0, (now - previousNow) / 1000));
|
||||
previousNow = now;
|
||||
const mix = initialized ? 1 - Math.exp(-dt / 0.18) : 1;
|
||||
const roadside = usesRoadsideSign(width, height);
|
||||
roadsideMix += ((roadside ? 1 : 0) - roadsideMix) * mix;
|
||||
screenX += (targetX - screenX) * mix;
|
||||
screenY += (targetY - screenY) * mix;
|
||||
screenWidth += (targetWidth - screenWidth) * mix;
|
||||
@@ -583,9 +603,43 @@ export function createSign(
|
||||
worldPoint(worldLeft, cx - projectedWidth / 2, cy, depth, camera);
|
||||
worldPoint(worldRight, cx + projectedWidth / 2, cy, depth, camera);
|
||||
if (lowerStrip) point.y = Math.max(1.4, point.y);
|
||||
// A wide-screen sign is a real roadside object: fixed lateral offset,
|
||||
// fixed mast height, constant size, and travel only along the road axis.
|
||||
// Camera movement changes its projection, never its world-space anchor.
|
||||
const desiredPhysical = roadsideSignMetrics(height);
|
||||
for (const key of ['x', 'y', 'scale', 'dwellZ'] as const) {
|
||||
physical[key] += (desiredPhysical[key] - physical[key]) * mix;
|
||||
}
|
||||
// Canonical dwell progress is always 1; visual drift must integrate time.
|
||||
if (snapshot.phase === 'dwell')
|
||||
dwellDrift = Math.min(3, dwellDrift + dt * 1.7);
|
||||
if (snapshot.phase === 'depart' && previousPhase !== 'depart')
|
||||
departureDrift = dwellDrift;
|
||||
previousPhase = snapshot.phase;
|
||||
const roadZ =
|
||||
snapshot.phase === 'depart'
|
||||
? THREE.MathUtils.lerp(physical.dwellZ + departureDrift, 48, p)
|
||||
: THREE.MathUtils.lerp(-230, physical.dwellZ, arrival) + dwellDrift;
|
||||
point.x = THREE.MathUtils.lerp(
|
||||
point.x,
|
||||
carAnchor.x + side * physical.x,
|
||||
roadsideMix,
|
||||
);
|
||||
point.y = THREE.MathUtils.lerp(point.y, physical.y, roadsideMix);
|
||||
point.z = THREE.MathUtils.lerp(point.z, carAnchor.z + roadZ, roadsideMix);
|
||||
group.position.set(point.x, point.y, point.z);
|
||||
group.scale.setScalar(worldRight.distanceTo(worldLeft) / 8.64);
|
||||
group.rotation.y = camera.rotation.y - side * 0.065;
|
||||
group.scale.setScalar(
|
||||
THREE.MathUtils.lerp(
|
||||
worldRight.distanceTo(worldLeft) / 8.64,
|
||||
physical.scale,
|
||||
roadsideMix,
|
||||
),
|
||||
);
|
||||
group.rotation.y = THREE.MathUtils.lerp(
|
||||
camera.rotation.y - side * 0.065,
|
||||
-side * 0.1,
|
||||
roadsideMix,
|
||||
);
|
||||
plantMounting();
|
||||
const opacity = clamp01(snapshot.opacity);
|
||||
for (const [mat, weight] of fadeMaterials) mat.opacity = opacity * weight;
|
||||
@@ -596,6 +650,7 @@ export function createSign(
|
||||
sweepMaterial.uniforms.uOpacity.value =
|
||||
snapshot.phase === 'approach' ? opacity * 0.14 : 0;
|
||||
group.visible = opacity > 0;
|
||||
group.userData.anchoring = roadside ? 'roadside' : 'compact-shelf';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user