fix: refine responsive sign placement
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user