fix: make nightdrive signage reliable and legible
This commit is contained in:
@@ -9,6 +9,7 @@ export interface NavigationState {
|
||||
epoch: number;
|
||||
source: JourneySource;
|
||||
destinationId: SectionId | null;
|
||||
signDestinationId: SectionId | null;
|
||||
}
|
||||
|
||||
export interface NavigationRuntime {
|
||||
@@ -28,7 +29,11 @@ export interface NavigationRuntime {
|
||||
}
|
||||
|
||||
export interface NavigationController {
|
||||
navigate(id: SectionId, target: HTMLElement): number;
|
||||
navigate(
|
||||
id: SectionId,
|
||||
target: HTMLElement,
|
||||
options?: { requestSign?: boolean },
|
||||
): number;
|
||||
noteUserScrollIntent(): number;
|
||||
notePassiveSection(id: SectionId): void;
|
||||
initialize(id: SectionId, target: HTMLElement): number;
|
||||
@@ -118,6 +123,7 @@ export function createNavigationController(
|
||||
epoch: 0,
|
||||
source: "initial",
|
||||
destinationId: null,
|
||||
signDestinationId: null,
|
||||
};
|
||||
const subscribers = new Set<(state: NavigationState) => void>();
|
||||
let frameId: number | undefined;
|
||||
@@ -159,12 +165,14 @@ export function createNavigationController(
|
||||
const beginEpoch = (
|
||||
source: JourneySource,
|
||||
destinationId: SectionId | null,
|
||||
signDestinationId: SectionId | null = null,
|
||||
): number => {
|
||||
cancelPending();
|
||||
currentState = {
|
||||
epoch: currentState.epoch + 1,
|
||||
source,
|
||||
destinationId,
|
||||
signDestinationId,
|
||||
};
|
||||
if (TEST_BUILD)
|
||||
window.__PORTFOLIO_PERFORMANCE__?.noteNavigationEpoch(currentState.epoch);
|
||||
@@ -292,9 +300,13 @@ export function createNavigationController(
|
||||
};
|
||||
|
||||
return {
|
||||
navigate(id, target) {
|
||||
navigate(id, target, options) {
|
||||
requireTarget(id, target);
|
||||
const epoch = beginEpoch("explicit-navigation", id);
|
||||
const epoch = beginEpoch(
|
||||
"explicit-navigation",
|
||||
id,
|
||||
options?.requestSign === true ? id : null,
|
||||
);
|
||||
if (canonicalSectionIdFromHash(runtime.getHash(), canonicalIds) !== id) {
|
||||
runtime.pushHash(`#${id}`);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SIGN_LIFECYCLE,
|
||||
SIGN_ROUTE_ORDER,
|
||||
signRouteFor,
|
||||
signTriggerMaximumVhFor,
|
||||
signViewportProfileFor,
|
||||
type SignViewportProfile,
|
||||
} from '@/journey/sign-routes';
|
||||
@@ -18,15 +19,27 @@ interface VisibleState {
|
||||
latchedAt: number;
|
||||
approachDuration: number;
|
||||
viewportProfile: SignViewportProfile;
|
||||
navigationArrival: boolean;
|
||||
dwellStartedAt: number | null;
|
||||
departStartedAt: number | null;
|
||||
traversalGeneration: number;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
interface PendingTarget {
|
||||
targetId: SectionId;
|
||||
navigationArrival: boolean;
|
||||
}
|
||||
|
||||
type State =
|
||||
| { kind: 'idle' }
|
||||
| VisibleState
|
||||
| { kind: 'cooldown'; until: number };
|
||||
| {
|
||||
kind: 'cooldown';
|
||||
targetId: SectionId;
|
||||
traversalGeneration: number;
|
||||
until: number;
|
||||
};
|
||||
|
||||
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
|
||||
|
||||
@@ -35,8 +48,17 @@ const smoothstep = (minimum: number, maximum: number, value: number): number =>
|
||||
return progress * progress * (3 - 2 * progress);
|
||||
};
|
||||
|
||||
const withNavigationArrival = (
|
||||
snapshot: SignSnapshot,
|
||||
navigationArrival: boolean,
|
||||
): SignSnapshot => {
|
||||
if (navigationArrival) snapshot.navigationArrival = true;
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
const isFiniteSample = (input: SignControllerSample): boolean =>
|
||||
Number.isFinite(input.now) &&
|
||||
Number.isFinite(input.navigationEpoch) &&
|
||||
Number.isFinite(input.acceleration) &&
|
||||
Number.isFinite(input.deltaY) &&
|
||||
(input.viewportWidth === undefined || (
|
||||
@@ -51,13 +73,17 @@ export function createSignController(): SignController {
|
||||
let state: State = { kind: 'idle' };
|
||||
let lastNow: number | null = null;
|
||||
let generation = 0;
|
||||
let pendingTargetId: SectionId | null = null;
|
||||
let pendingTarget: PendingTarget | null = null;
|
||||
let lastNavigationEpoch = 0;
|
||||
let lastNavigationTargetId: SectionId | null = null;
|
||||
let traversalGeneration = 0;
|
||||
const completedTraversalTargets = new Set<SectionId>();
|
||||
|
||||
const reset = (now?: number): void => {
|
||||
generation += 1;
|
||||
state = { kind: 'idle' };
|
||||
pendingTargetId = null;
|
||||
pendingTarget = null;
|
||||
lastNavigationTargetId = null;
|
||||
lastNow = now !== undefined && Number.isFinite(now) ? now : null;
|
||||
};
|
||||
|
||||
@@ -67,6 +93,7 @@ export function createSignController(): SignController {
|
||||
acceleration: number,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
navigationArrival = false,
|
||||
): VisibleState => {
|
||||
generation += 1;
|
||||
return {
|
||||
@@ -76,8 +103,10 @@ export function createSignController(): SignController {
|
||||
approachDuration: SIGN_LIFECYCLE.approachMs
|
||||
- SIGN_LIFECYCLE.approachReductionMs * clamp01(acceleration),
|
||||
viewportProfile: signViewportProfileFor(viewportWidth, viewportHeight),
|
||||
navigationArrival,
|
||||
dwellStartedAt: null,
|
||||
departStartedAt: null,
|
||||
traversalGeneration,
|
||||
generation,
|
||||
};
|
||||
};
|
||||
@@ -104,58 +133,74 @@ export function createSignController(): SignController {
|
||||
if (visible.departStartedAt !== null) {
|
||||
const progress = clamp01((now - visible.departStartedAt) / SIGN_LIFECYCLE.departMs);
|
||||
if (progress >= 1) {
|
||||
if (visible.dwellStartedAt !== null) completedTraversalTargets.add(visible.targetId);
|
||||
if (
|
||||
visible.dwellStartedAt !== null
|
||||
&& visible.traversalGeneration === traversalGeneration
|
||||
) {
|
||||
completedTraversalTargets.add(visible.targetId);
|
||||
}
|
||||
state = {
|
||||
kind: 'cooldown',
|
||||
targetId: visible.targetId,
|
||||
traversalGeneration: visible.traversalGeneration,
|
||||
until: visible.departStartedAt
|
||||
+ SIGN_LIFECYCLE.departMs
|
||||
+ SIGN_LIFECYCLE.cooldownMs,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
return withNavigationArrival({
|
||||
targetId: visible.targetId,
|
||||
phase: 'depart',
|
||||
progress,
|
||||
opacity: 1 - smoothstep(0.1, 1, progress),
|
||||
opacity: 1 - smoothstep(0.58, 0.94, progress),
|
||||
viewportProfile: visible.viewportProfile,
|
||||
};
|
||||
}, visible.navigationArrival);
|
||||
}
|
||||
|
||||
const elapsed = Math.max(0, now - visible.latchedAt);
|
||||
if (elapsed < SIGN_LIFECYCLE.anticipateMs) {
|
||||
return {
|
||||
return withNavigationArrival({
|
||||
targetId: visible.targetId,
|
||||
phase: 'anticipate',
|
||||
progress: clamp01(elapsed / SIGN_LIFECYCLE.anticipateMs),
|
||||
opacity: 0,
|
||||
viewportProfile: visible.viewportProfile,
|
||||
};
|
||||
}, visible.navigationArrival);
|
||||
}
|
||||
|
||||
const approachElapsed = elapsed - SIGN_LIFECYCLE.anticipateMs;
|
||||
if (approachElapsed < visible.approachDuration) {
|
||||
const progress = clamp01(approachElapsed / visible.approachDuration);
|
||||
return {
|
||||
return withNavigationArrival({
|
||||
targetId: visible.targetId,
|
||||
phase: 'approach',
|
||||
progress,
|
||||
opacity: smoothstep(0, 0.42, progress),
|
||||
viewportProfile: visible.viewportProfile,
|
||||
};
|
||||
}, visible.navigationArrival);
|
||||
}
|
||||
|
||||
if (visible.dwellStartedAt === null) visible.dwellStartedAt = now;
|
||||
return {
|
||||
return withNavigationArrival({
|
||||
targetId: visible.targetId,
|
||||
phase: 'dwell',
|
||||
progress: 1,
|
||||
opacity: 1,
|
||||
viewportProfile: visible.viewportProfile,
|
||||
};
|
||||
}, visible.navigationArrival);
|
||||
};
|
||||
|
||||
const canonicalCandidate = (input: SignControllerSample): SectionId | null => {
|
||||
const rearmRoutesFrom = (targetId: SectionId): void => {
|
||||
const targetIndex = SIGN_ROUTE_ORDER.indexOf(targetId);
|
||||
if (targetIndex < 0) return;
|
||||
for (let index = targetIndex; index < SIGN_ROUTE_ORDER.length; index += 1) {
|
||||
const routeId = SIGN_ROUTE_ORDER[index];
|
||||
if (routeId !== undefined) completedTraversalTargets.delete(routeId);
|
||||
}
|
||||
};
|
||||
|
||||
const passiveCandidate = (input: SignControllerSample): SectionId | null => {
|
||||
if (
|
||||
input.direction !== 1
|
||||
|| input.deltaY <= 0
|
||||
@@ -175,7 +220,11 @@ export function createSignController(): SignController {
|
||||
) return null;
|
||||
const route = signRouteFor(input.candidateId);
|
||||
return route.eligible
|
||||
&& input.candidateDistanceVh <= route.triggerMaximumVh
|
||||
&& input.candidateDistanceVh <= signTriggerMaximumVhFor(
|
||||
route,
|
||||
input.viewportWidth ?? input.viewportHeight * (16 / 9),
|
||||
input.viewportHeight,
|
||||
)
|
||||
&& !completedTraversalTargets.has(route.id)
|
||||
? route.id
|
||||
: null;
|
||||
@@ -188,32 +237,130 @@ export function createSignController(): SignController {
|
||||
}
|
||||
lastNow = input.now;
|
||||
|
||||
if (!input.motionAllowed || input.source !== 'passive-scroll') {
|
||||
if (input.source !== 'passive-scroll') {
|
||||
const receivedNavigationEpoch = input.source !== 'passive-scroll'
|
||||
&& input.navigationEpoch > lastNavigationEpoch;
|
||||
const receivedNavigationIntent = input.source === 'explicit-navigation'
|
||||
&& receivedNavigationEpoch;
|
||||
let navigationCandidate: SectionId | null = null;
|
||||
let repeatedNavigationIntent = false;
|
||||
if (receivedNavigationEpoch) {
|
||||
lastNavigationEpoch = input.navigationEpoch;
|
||||
if (receivedNavigationIntent) {
|
||||
const destinationId = input.navigationDestinationId;
|
||||
repeatedNavigationIntent = destinationId !== null
|
||||
&& destinationId === lastNavigationTargetId
|
||||
&& (
|
||||
input.activeSectionId === destinationId
|
||||
|| (state.kind === 'visible' && state.targetId === destinationId)
|
||||
);
|
||||
lastNavigationTargetId = destinationId;
|
||||
} else {
|
||||
lastNavigationTargetId = null;
|
||||
}
|
||||
if (!repeatedNavigationIntent) {
|
||||
traversalGeneration += 1;
|
||||
completedTraversalTargets.clear();
|
||||
}
|
||||
const destinationId = input.navigationDestinationId;
|
||||
if (
|
||||
receivedNavigationIntent
|
||||
&& destinationId !== null
|
||||
&& !repeatedNavigationIntent
|
||||
) {
|
||||
const route = signRouteFor(destinationId);
|
||||
if (route.eligible) navigationCandidate = route.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.motionAllowed) {
|
||||
reset(input.now);
|
||||
return null;
|
||||
}
|
||||
if (input.source === 'initial' || input.source === 'history-restoration') {
|
||||
reset(input.now);
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = canonicalCandidate(input);
|
||||
const discontinuous =
|
||||
if (
|
||||
receivedNavigationIntent
|
||||
&& navigationCandidate === null
|
||||
&& !repeatedNavigationIntent
|
||||
) {
|
||||
pendingTarget = null;
|
||||
if (state.kind === 'idle' || state.kind === 'cooldown') {
|
||||
state = { kind: 'idle' };
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (input.source === 'passive-scroll' && input.direction < 0) {
|
||||
pendingTarget = null;
|
||||
if (input.candidateId !== null && input.candidateDistanceVh !== null) {
|
||||
const retreatRoute = signRouteFor(input.candidateId);
|
||||
if (
|
||||
retreatRoute.eligible
|
||||
&& input.candidateDistanceVh
|
||||
>= signTriggerMaximumVhFor(
|
||||
retreatRoute,
|
||||
input.viewportWidth ?? input.viewportHeight * (16 / 9),
|
||||
input.viewportHeight,
|
||||
) + SIGN_LIFECYCLE.rearmMarginVh
|
||||
) {
|
||||
traversalGeneration += 1;
|
||||
rearmRoutesFrom(retreatRoute.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const candidate = receivedNavigationIntent
|
||||
? navigationCandidate
|
||||
: input.source === 'passive-scroll'
|
||||
? passiveCandidate(input)
|
||||
: null;
|
||||
const discontinuous = input.source === 'passive-scroll' && (
|
||||
input.crossedBoundaries > 1
|
||||
|| Math.abs(input.deltaY) > input.viewportHeight * 0.75;
|
||||
|| Math.abs(input.deltaY) > input.viewportHeight * 0.75
|
||||
);
|
||||
|
||||
if (state.kind === 'cooldown') {
|
||||
if (candidate !== null) pendingTargetId = candidate;
|
||||
if (
|
||||
receivedNavigationIntent
|
||||
&& input.navigationDestinationId === state.targetId
|
||||
&& pendingTarget?.targetId === state.targetId
|
||||
) {
|
||||
pendingTarget.navigationArrival = true;
|
||||
}
|
||||
if (
|
||||
candidate !== null
|
||||
&& (
|
||||
candidate !== state.targetId
|
||||
|| receivedNavigationIntent
|
||||
|| state.traversalGeneration !== traversalGeneration
|
||||
)
|
||||
) {
|
||||
pendingTarget = {
|
||||
targetId: candidate,
|
||||
navigationArrival: receivedNavigationIntent,
|
||||
};
|
||||
}
|
||||
if (input.now < state.until) return null;
|
||||
state = { kind: 'idle' };
|
||||
if (pendingTargetId !== null && !completedTraversalTargets.has(pendingTargetId)) {
|
||||
const targetId = pendingTargetId;
|
||||
pendingTargetId = null;
|
||||
const queued = pendingTarget;
|
||||
pendingTarget = null;
|
||||
if (
|
||||
queued !== null
|
||||
&& (
|
||||
queued.navigationArrival
|
||||
|| !completedTraversalTargets.has(queued.targetId)
|
||||
)
|
||||
) {
|
||||
state = latch(
|
||||
targetId,
|
||||
queued.targetId,
|
||||
input.now,
|
||||
input.acceleration,
|
||||
input.viewportWidth ?? input.viewportHeight * (16 / 9),
|
||||
input.viewportHeight,
|
||||
queued.navigationArrival,
|
||||
);
|
||||
return snapshotVisible(state, input.now);
|
||||
}
|
||||
@@ -227,28 +374,66 @@ export function createSignController(): SignController {
|
||||
input.acceleration,
|
||||
input.viewportWidth ?? input.viewportHeight * (16 / 9),
|
||||
input.viewportHeight,
|
||||
receivedNavigationIntent,
|
||||
);
|
||||
return snapshotVisible(state, input.now);
|
||||
}
|
||||
|
||||
if (candidate !== null && candidate !== state.targetId) {
|
||||
pendingTargetId = candidate;
|
||||
const navigationTargetsVisibleSign = receivedNavigationIntent
|
||||
&& input.navigationDestinationId === state.targetId;
|
||||
if (navigationTargetsVisibleSign) {
|
||||
pendingTarget = null;
|
||||
if (!repeatedNavigationIntent) {
|
||||
state.traversalGeneration = traversalGeneration;
|
||||
}
|
||||
if (
|
||||
!state.navigationArrival
|
||||
&& state.dwellStartedAt !== null
|
||||
&& state.departStartedAt === null
|
||||
) {
|
||||
state.dwellStartedAt = input.now;
|
||||
}
|
||||
state.navigationArrival = true;
|
||||
}
|
||||
const candidateStartsNewPresentation = candidate !== null && (
|
||||
candidate !== state.targetId
|
||||
|| (
|
||||
input.source === 'passive-scroll'
|
||||
&& state.traversalGeneration !== traversalGeneration
|
||||
)
|
||||
);
|
||||
if (candidateStartsNewPresentation && candidate !== null) {
|
||||
pendingTarget = {
|
||||
targetId: candidate,
|
||||
navigationArrival: receivedNavigationIntent,
|
||||
};
|
||||
}
|
||||
const current = snapshotVisible(state, input.now);
|
||||
if (current === null) return null;
|
||||
|
||||
if (
|
||||
current.opacity <= SIGN_LIFECYCLE.concealedOpacity
|
||||
&& candidate !== null
|
||||
&& candidate !== state.targetId
|
||||
receivedNavigationIntent
|
||||
&& navigationCandidate === null
|
||||
&& !repeatedNavigationIntent
|
||||
&& current.opacity <= SIGN_LIFECYCLE.concealedOpacity
|
||||
) {
|
||||
pendingTargetId = null;
|
||||
state = { kind: 'idle' };
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
current.opacity <= SIGN_LIFECYCLE.concealedOpacity
|
||||
&& candidateStartsNewPresentation
|
||||
&& candidate !== null
|
||||
) {
|
||||
pendingTarget = null;
|
||||
state = latch(
|
||||
candidate,
|
||||
input.now,
|
||||
input.acceleration,
|
||||
input.viewportWidth ?? input.viewportHeight * (16 / 9),
|
||||
input.viewportHeight,
|
||||
receivedNavigationIntent,
|
||||
);
|
||||
return snapshotVisible(state, input.now);
|
||||
}
|
||||
@@ -264,7 +449,10 @@ export function createSignController(): SignController {
|
||||
|
||||
const activeIndex = SIGN_ROUTE_ORDER.indexOf(input.activeSectionId);
|
||||
const targetIndex = SIGN_ROUTE_ORDER.indexOf(state.targetId);
|
||||
const targetPassed = activeIndex >= 0 && targetIndex >= 0 && activeIndex > targetIndex;
|
||||
const targetPassed = input.source === 'passive-scroll'
|
||||
&& activeIndex >= 0
|
||||
&& targetIndex >= 0
|
||||
&& activeIndex > targetIndex;
|
||||
if (targetPassed && state.departStartedAt === null) {
|
||||
if (current.opacity <= SIGN_LIFECYCLE.concealedOpacity) {
|
||||
state = { kind: 'idle' };
|
||||
@@ -274,7 +462,7 @@ export function createSignController(): SignController {
|
||||
return snapshotVisible(state, input.now);
|
||||
}
|
||||
|
||||
if (input.direction < 0) {
|
||||
if (input.source === 'passive-scroll' && input.direction < 0) {
|
||||
if (current.opacity <= SIGN_LIFECYCLE.concealedOpacity) {
|
||||
state = { kind: 'idle' };
|
||||
return null;
|
||||
@@ -293,7 +481,7 @@ export function createSignController(): SignController {
|
||||
};
|
||||
|
||||
const needsFrame = (): boolean => {
|
||||
if (state.kind === 'cooldown') return pendingTargetId !== null;
|
||||
if (state.kind === 'cooldown') return pendingTarget !== null;
|
||||
return state.kind === 'visible' && lastNow !== null;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import type { SectionId } from '@/content/types';
|
||||
export type SignCorridor = 'left' | 'right' | 'outer-left' | 'outer-right';
|
||||
export type SignViewportProfile = 'wide' | 'tall' | 'short';
|
||||
|
||||
export const PORTRAIT_SIGN_TRIGGER_MAXIMUM_VH = 0.23;
|
||||
export const SHORT_LANDSCAPE_SIGN_TRIGGER_MAXIMUM_VH = 0.38;
|
||||
|
||||
export interface SignRouteSpec {
|
||||
id: SectionId;
|
||||
label: string;
|
||||
@@ -10,6 +13,7 @@ export interface SignRouteSpec {
|
||||
eligible: boolean;
|
||||
corridor: SignCorridor | null;
|
||||
triggerMaximumVh: number;
|
||||
dwellOffsetX?: number;
|
||||
}
|
||||
|
||||
export const SIGN_LIFECYCLE = Object.freeze({
|
||||
@@ -20,6 +24,7 @@ export const SIGN_LIFECYCLE = Object.freeze({
|
||||
departMs: 700,
|
||||
cooldownMs: 350,
|
||||
concealedOpacity: 0.02,
|
||||
rearmMarginVh: 0.2,
|
||||
});
|
||||
|
||||
export const SIGN_ROUTE_ORDER: readonly SectionId[] = [
|
||||
@@ -41,22 +46,22 @@ 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: 'right', triggerMaximumVh: 0.55,
|
||||
id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'left', triggerMaximumVh: 0.55,
|
||||
},
|
||||
infrastructure: {
|
||||
id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55,
|
||||
id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, dwellOffsetX: -1.6,
|
||||
},
|
||||
'selected-builds': {
|
||||
id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55,
|
||||
id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'right', triggerMaximumVh: 0.55,
|
||||
},
|
||||
'additional-work': {
|
||||
id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'left', triggerMaximumVh: 0.55,
|
||||
id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55,
|
||||
},
|
||||
toolkit: {
|
||||
id: 'toolkit', label: 'Toolkit', marker: '07', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55,
|
||||
},
|
||||
contact: {
|
||||
id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'right', triggerMaximumVh: 0.55,
|
||||
id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,3 +78,18 @@ export function signViewportProfileFor(
|
||||
const aspect = safeWidth / safeHeight;
|
||||
return safeHeight < 560 && aspect >= 1 ? 'short' : aspect < 1.05 ? 'tall' : 'wide';
|
||||
}
|
||||
|
||||
export function signTriggerMaximumVhFor(
|
||||
route: SignRouteSpec,
|
||||
width: number,
|
||||
height: number,
|
||||
): number {
|
||||
const profile = signViewportProfileFor(width, height);
|
||||
if (profile === 'tall') {
|
||||
return Math.min(route.triggerMaximumVh, PORTRAIT_SIGN_TRIGGER_MAXIMUM_VH);
|
||||
}
|
||||
if (profile === 'short') {
|
||||
return Math.min(route.triggerMaximumVh, SHORT_LANDSCAPE_SIGN_TRIGGER_MAXIMUM_VH);
|
||||
}
|
||||
return route.triggerMaximumVh;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,14 @@ export interface SignSnapshot {
|
||||
progress: number;
|
||||
opacity: number;
|
||||
viewportProfile?: SignViewportProfile;
|
||||
navigationArrival?: true;
|
||||
}
|
||||
|
||||
export interface SignControllerSample {
|
||||
now: number;
|
||||
source: JourneySource;
|
||||
navigationEpoch: number;
|
||||
navigationDestinationId: SectionId | null;
|
||||
direction: -1 | 0 | 1;
|
||||
acceleration: number;
|
||||
deltaY: number;
|
||||
|
||||
Reference in New Issue
Block a user