fix: refine responsive sign placement

This commit is contained in:
2026-09-03 18:34:39 +03:00
parent b075041918
commit 5dba071950
15 changed files with 388 additions and 17 deletions

View File

@@ -46,7 +46,7 @@ const routeSpecs: Readonly<Record<SectionId, SignRouteSpec>> = Object.freeze({
id: 'profile', label: 'Profile', marker: '02', eligible: true, corridor: 'left', triggerMaximumVh: 0.55,
},
experience: {
id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'left', triggerMaximumVh: 0.55,
id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'right', triggerMaximumVh: 0.55,
},
infrastructure: {
id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, dwellOffsetX: -1.6,

View File

@@ -35,6 +35,10 @@ const SHORT_CORRIDOR_RELIEF_PER_ASPECT = 20;
const SHORT_CORRIDOR_MINIMUM_RELIEF = 12;
const SHORT_CORRIDOR_MAXIMUM_RELIEF = 34;
const RESPONSIVE_REFLOW_MS = 320;
const PORTRAIT_NAVIGATION_PHONE_MAX_WIDTH = 480;
const PORTRAIT_NAVIGATION_MEDIUM_WIDTH = 720;
const PORTRAIT_NAVIGATION_MEDIUM_SCALE = 0.44;
const PORTRAIT_NAVIGATION_MEDIUM_Y = 4.5;
const SIGN_POSES: Readonly<Record<SignViewportProfile, Readonly<Record<SignCorridor, SignPoseSet>>>> = {
wide: {
@@ -100,6 +104,13 @@ const PORTRAIT_NAVIGATION_DWELL: Readonly<Record<SignCorridor, SignPose>> = {
'outer-right': { x: 2.9, y: 3, z: 8, scale: 0.25 },
};
const EXPERIENCE_PASSIVE_DWELL: Readonly<Partial<Record<SignViewportProfile, SignPose>>> = {
wide: { x: 12.8, y: 3.7, z: 0, scale: 0.9 },
// The near short pose counterbalances the shared aspect relief while keeping
// the projected face in the right-hand gap between both visible panels.
short: { x: -7.7, y: 3.3, z: -4, scale: 1.4 },
};
export interface SignWorld {
group: SignGroup;
inspect(
@@ -394,6 +405,8 @@ export function createSign(
let activeViewport: SignViewportProfile = 'wide';
let pendingAspect = 16 / 9;
let activeAspect = 16 / 9;
let pendingViewportWidth = 1_440;
let activeViewportWidth = 1_440;
let activeNavigationArrival = false;
let activeCorridor: SignCorridor = 'right';
let previousPhase: SignSnapshot['phase'] | null = null;
@@ -441,16 +454,35 @@ export function createSign(
side: number,
lateralWeight: number,
routeOffsetX = 0,
scale = pose.scale,
y = pose.y,
): void => {
group.position.set(
resolvedPoseX(pose, side, lateralWeight, routeOffsetX),
pose.y,
y,
pose.z,
);
group.scale.setScalar(pose.scale);
plantMounting(pose.y, pose.scale);
group.scale.setScalar(scale);
plantMounting(y, scale);
};
const portraitNavigationWidthProgress = (): number => smoothstep(clamp01(
(activeViewportWidth - PORTRAIT_NAVIGATION_PHONE_MAX_WIDTH)
/ (PORTRAIT_NAVIGATION_MEDIUM_WIDTH - PORTRAIT_NAVIGATION_PHONE_MAX_WIDTH),
));
const portraitNavigationScale = (phoneScale: number): number => THREE.MathUtils.lerp(
phoneScale,
PORTRAIT_NAVIGATION_MEDIUM_SCALE,
portraitNavigationWidthProgress(),
);
const portraitNavigationY = (phoneY: number): number => THREE.MathUtils.lerp(
phoneY,
PORTRAIT_NAVIGATION_MEDIUM_Y,
portraitNavigationWidthProgress(),
);
const plantMounting = (groupY: number, groupScale: number): void => {
const safeScale = Math.max(0.001, groupScale);
const footingY = -groupY / safeScale;
@@ -476,15 +508,19 @@ export function createSign(
const stageResponsiveViewport = (
viewport: SignViewportProfile,
aspect: number,
width: number,
): void => {
const changed = viewport !== pendingViewport
|| Math.abs(aspect - pendingAspect) > 0.001;
|| Math.abs(aspect - pendingAspect) > 0.001
|| Math.abs(width - pendingViewportWidth) > 0.5;
if (changed) stagePoseTransition();
pendingViewport = viewport;
pendingAspect = aspect;
pendingViewportWidth = width;
if (previousPhase === null) {
activeViewport = pendingViewport;
activeAspect = pendingAspect;
activeViewportWidth = pendingViewportWidth;
poseTransitionPending = false;
poseTransitionStartedAt = null;
}
@@ -535,13 +571,21 @@ export function createSign(
},
setViewportAspect(aspect) {
const nextAspect = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
stageResponsiveViewport(nextAspect < 0.85 ? 'tall' : 'wide', nextAspect);
stageResponsiveViewport(
nextAspect < 0.85 ? 'tall' : 'wide',
nextAspect,
pendingViewportWidth,
);
},
setViewportSize(width, height) {
const nextAspect = Number.isFinite(width) && Number.isFinite(height) && height > 0
? Math.max(1, width) / height
: 1;
stageResponsiveViewport(signViewportProfileFor(width, height), nextAspect);
stageResponsiveViewport(
signViewportProfileFor(width, height),
nextAspect,
Number.isFinite(width) ? Math.max(1, width) : 1,
);
},
update(sign, now) {
if (!sign) {
@@ -551,6 +595,7 @@ export function createSign(
previousPhase = null;
activeViewport = pendingViewport;
activeAspect = pendingAspect;
activeViewportWidth = pendingViewportWidth;
activeNavigationArrival = false;
poseTransitionPending = false;
poseTransitionStartedAt = null;
@@ -569,6 +614,7 @@ export function createSign(
// the generation that was just disposed rather than this canvas.
activeViewport = pendingViewport;
activeAspect = pendingAspect;
activeViewportWidth = pendingViewportWidth;
activeNavigationArrival = navigationArrival;
poseTransitionPending = false;
poseTransitionStartedAt = null;
@@ -584,6 +630,7 @@ export function createSign(
} else if (poseTransitionPending) {
activeViewport = pendingViewport;
activeAspect = pendingAspect;
activeViewportWidth = pendingViewportWidth;
if (poseTransitionStartedAt === null) {
poseTransitionStartedAt = Number.isFinite(now) ? now! : 0;
}
@@ -596,9 +643,18 @@ export function createSign(
activeCorridor = corridor;
const poseSet = SIGN_POSES[activeViewport][corridor];
const horizon = poseSet.horizon;
const dwell = activeViewport === 'tall' && activeNavigationArrival
let dwell = activeViewport === 'tall' && activeNavigationArrival
? PORTRAIT_NAVIGATION_DWELL[corridor]
: poseSet.dwell;
if (!activeNavigationArrival && sign.targetId === 'experience') {
dwell = EXPERIENCE_PASSIVE_DWELL[activeViewport] ?? dwell;
}
const dwellScale = activeViewport === 'tall' && activeNavigationArrival
? portraitNavigationScale(dwell.scale)
: dwell.scale;
const dwellY = activeViewport === 'tall' && activeNavigationArrival
? portraitNavigationY(dwell.y)
: dwell.y;
const side = corridor.includes('left') ? -1 : 1;
const routeOffsetX = route.dwellOffsetX ?? 0;
@@ -611,16 +667,16 @@ export function createSign(
resolvedPoseX(dwell, side, 1, routeOffsetX),
progress,
),
THREE.MathUtils.lerp(horizon.y, dwell.y, progress),
THREE.MathUtils.lerp(horizon.y, dwellY, progress),
THREE.MathUtils.lerp(horizon.z, dwell.z, progress),
);
group.scale.setScalar(THREE.MathUtils.lerp(horizon.scale, dwell.scale, progress));
group.scale.setScalar(THREE.MathUtils.lerp(horizon.scale, dwellScale, progress));
plantMounting(group.position.y, group.scale.x);
} else if (sign.phase === 'dwell') {
applyPose(dwell, side, 1, routeOffsetX);
applyPose(dwell, side, 1, routeOffsetX, dwellScale, dwellY);
} else {
if (previousPhase !== 'depart') {
if (previousPhase === null) applyPose(dwell, side, 1, routeOffsetX);
if (previousPhase === null) applyPose(dwell, side, 1, routeOffsetX, dwellScale, dwellY);
departureOrigin.copy(group.position);
departureScale = group.scale.x;
}