feat: expose stable journey intent signals
This commit is contained in:
@@ -5,11 +5,14 @@ declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
const TEST_BUILD =
|
||||
typeof __PORTFOLIO_TEST_BUILD__ !== "undefined" && __PORTFOLIO_TEST_BUILD__;
|
||||
|
||||
export type NavigationPhase = "idle" | "travelling" | "settled";
|
||||
|
||||
export interface NavigationState {
|
||||
epoch: number;
|
||||
source: JourneySource;
|
||||
destinationId: SectionId | null;
|
||||
signDestinationId: SectionId | null;
|
||||
readonly epoch: number;
|
||||
readonly source: JourneySource;
|
||||
readonly phase: NavigationPhase;
|
||||
readonly destinationId: SectionId | null;
|
||||
readonly signDestinationId: SectionId | null;
|
||||
}
|
||||
|
||||
export interface NavigationRuntime {
|
||||
@@ -122,6 +125,7 @@ export function createNavigationController(
|
||||
let currentState: NavigationState = {
|
||||
epoch: 0,
|
||||
source: "initial",
|
||||
phase: "idle",
|
||||
destinationId: null,
|
||||
signDestinationId: null,
|
||||
};
|
||||
@@ -171,6 +175,7 @@ export function createNavigationController(
|
||||
currentState = {
|
||||
epoch: currentState.epoch + 1,
|
||||
source,
|
||||
phase: source === "passive-scroll" ? "idle" : "travelling",
|
||||
destinationId,
|
||||
signDestinationId,
|
||||
};
|
||||
@@ -211,6 +216,8 @@ export function createNavigationController(
|
||||
)
|
||||
return;
|
||||
cancelSettle();
|
||||
currentState = { ...currentState, phase: "settled" };
|
||||
emit();
|
||||
frameId = runtime.requestFrame(() => {
|
||||
frameId = undefined;
|
||||
if (
|
||||
@@ -284,6 +291,14 @@ export function createNavigationController(
|
||||
const epoch = beginEpoch(source, id);
|
||||
if (source === "initial") {
|
||||
runtime.scrollTarget(target, "instant");
|
||||
if (
|
||||
currentState.epoch === epoch &&
|
||||
currentState.source === source &&
|
||||
currentState.destinationId === id
|
||||
) {
|
||||
currentState = { ...currentState, phase: "settled" };
|
||||
emit();
|
||||
}
|
||||
} else {
|
||||
frameId = runtime.requestFrame(() => {
|
||||
frameId = undefined;
|
||||
@@ -293,6 +308,8 @@ export function createNavigationController(
|
||||
currentState.destinationId === id
|
||||
) {
|
||||
runtime.scrollTarget(target, "instant");
|
||||
currentState = { ...currentState, phase: "settled" };
|
||||
emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SectionId } from "@/content/types";
|
||||
import type { EnvironmentChapter } from "./types";
|
||||
import type { EnvironmentChapter, SectionActivationSnapshot } from "./types";
|
||||
|
||||
export interface SectionHeadingGeometry {
|
||||
id: SectionId;
|
||||
@@ -12,6 +12,14 @@ export interface ActiveSectionInput {
|
||||
atDocumentEnd: boolean;
|
||||
}
|
||||
|
||||
export interface SectionActivationInput {
|
||||
readonly headings: readonly SectionHeadingGeometry[];
|
||||
readonly activeSectionId: SectionId;
|
||||
readonly activationY: number;
|
||||
readonly viewportHeight: number;
|
||||
readonly layoutRevision: number;
|
||||
}
|
||||
|
||||
export interface DerivedSectionState {
|
||||
activeSectionId: SectionId;
|
||||
chapter: EnvironmentChapter;
|
||||
@@ -56,6 +64,31 @@ export function selectActiveSection(input: ActiveSectionInput): SectionId {
|
||||
return active;
|
||||
}
|
||||
|
||||
export function deriveSectionActivationSnapshot(
|
||||
input: SectionActivationInput,
|
||||
): SectionActivationSnapshot {
|
||||
const activeIndex = input.headings.findIndex(
|
||||
(heading) => heading.id === input.activeSectionId,
|
||||
);
|
||||
const activeHeading = input.headings[activeIndex];
|
||||
if (activeHeading === undefined || input.viewportHeight <= 0) {
|
||||
return {
|
||||
layoutRevision: input.layoutRevision,
|
||||
activeHeadingOffsetVh: 0,
|
||||
nextHeadingOffsetVh: null,
|
||||
};
|
||||
}
|
||||
const nextHeading = input.headings[activeIndex + 1];
|
||||
return {
|
||||
layoutRevision: input.layoutRevision,
|
||||
activeHeadingOffsetVh:
|
||||
(activeHeading.top - input.activationY) / input.viewportHeight,
|
||||
nextHeadingOffsetVh: nextHeading === undefined
|
||||
? null
|
||||
: (nextHeading.top - input.activationY) / input.viewportHeight,
|
||||
};
|
||||
}
|
||||
|
||||
const chapters: Readonly<Record<SectionId, EnvironmentChapter>> = {
|
||||
start: "deep-night",
|
||||
profile: "deep-night",
|
||||
|
||||
@@ -66,9 +66,16 @@ export interface SignController {
|
||||
needsFrame(): boolean;
|
||||
}
|
||||
|
||||
export interface SectionActivationSnapshot {
|
||||
readonly layoutRevision: number;
|
||||
readonly activeHeadingOffsetVh: number;
|
||||
readonly nextHeadingOffsetVh: number | null;
|
||||
}
|
||||
|
||||
export interface JourneySnapshot extends ScrollDriveSnapshot {
|
||||
activeSectionId: SectionId;
|
||||
chapter: EnvironmentChapter;
|
||||
sign: SignSnapshot | null;
|
||||
arrived: boolean;
|
||||
sectionActivation: SectionActivationSnapshot;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user