export type NotebookPhase = | "approach" | "open" | "boot" | "cursor-in" | "browse" | "hold" | "close"; export type NotebookFrame = { phase: NotebookPhase; lidAngle: number; deviceOpacity: number; screenOpacity: number; activeTabIndex: number; cursorVisible: boolean; cursorX: number; cursorY: number; cursorClicking: boolean; }; const OPEN_ANGLE = 112; const BROWSE_START = 0.44; const BROWSE_END = 0.78; const HOLD_END = 0.87; const CURSOR_POINTS = [ { x: 0.26, y: 0.68 }, { x: 0.58, y: 0.32 }, { x: 0.74, y: 0.72 }, { x: 0.38, y: 0.28 }, ]; function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } function normalizeProgress(progress: number): number { return clamp(Number.isFinite(progress) ? progress : 0, 0, 1); } function interpolate(from: number, to: number, progress: number): number { return from + (to - from) * clamp(progress, 0, 1); } function getPhase(progress: number): NotebookPhase { if (progress < 0.1) return "approach"; if (progress < 0.25) return "open"; if (progress < 0.34) return "boot"; if (progress < BROWSE_START) return "cursor-in"; if (progress < BROWSE_END) return "browse"; if (progress < HOLD_END) return "hold"; return "close"; } function getActiveTabIndex( progress: number, tabCount: number, phase: NotebookPhase, ): number { if ( tabCount < 1 || phase === "approach" || phase === "open" || phase === "boot" || phase === "cursor-in" || phase === "close" ) { return -1; } if (phase === "hold") { return tabCount - 1; } const browseProgress = (progress - BROWSE_START) / (BROWSE_END - BROWSE_START); return clamp(Math.floor(browseProgress * tabCount), 0, tabCount - 1); } function getCursorPoint( progress: number, phase: NotebookPhase, activeTabIndex: number, ): { x: number; y: number } { if (phase === "cursor-in") { const cursorProgress = (progress - 0.34) / 0.1; return { x: interpolate(0.06, 0.26, cursorProgress), y: interpolate(0.88, 0.68, cursorProgress), }; } if (activeTabIndex < 0) { return { x: 0.06, y: 0.88 }; } const point = CURSOR_POINTS[activeTabIndex % CURSOR_POINTS.length]; return point; } function isCursorClicking( progress: number, phase: NotebookPhase, tabCount: number, ): boolean { if ( tabCount < 1 || phase === "approach" || phase === "open" || phase === "boot" || phase === "close" ) { return false; } if (phase === "cursor-in") { return progress >= 0.43; } if (phase === "hold") { return false; } const browseProgress = (progress - BROWSE_START) / (BROWSE_END - BROWSE_START); const slotProgress = (browseProgress * tabCount) % 1; return slotProgress < 0.12; } export function getNotebookFrame( progress: number, tabCount: number, ): NotebookFrame { const normalizedProgress = normalizeProgress(progress); const normalizedTabCount = Math.max(0, Math.floor(tabCount)); const phase = getPhase(normalizedProgress); const activeTabIndex = getActiveTabIndex( normalizedProgress, normalizedTabCount, phase, ); const isApproach = normalizedProgress < 0.1; const isOpening = phase === "open"; const isBooting = phase === "boot"; const isClosing = phase === "close"; const lidAngle = isOpening ? interpolate(0, OPEN_ANGLE, (normalizedProgress - 0.1) / 0.15) : isClosing ? interpolate( OPEN_ANGLE, 0, (normalizedProgress - HOLD_END) / (1 - HOLD_END), ) : normalizedProgress >= 0.25 ? OPEN_ANGLE : 0; const screenOpacity = isBooting ? interpolate(0, 1, (normalizedProgress - 0.25) / 0.09) : normalizedProgress >= 0.34 && !isClosing ? 1 : isClosing ? interpolate(1, 0, (normalizedProgress - HOLD_END) / (1 - HOLD_END)) : 0; const deviceOpacity = isApproach ? interpolate(0, 1, normalizedProgress / 0.1) : isClosing ? interpolate(1, 0.92, (normalizedProgress - HOLD_END) / (1 - HOLD_END)) : 1; const cursorVisible = (phase === "cursor-in" || phase === "browse" || phase === "hold") && normalizedTabCount > 0; const { x: cursorX, y: cursorY } = getCursorPoint( normalizedProgress, phase, activeTabIndex, ); return { phase, lidAngle, deviceOpacity, screenOpacity, activeTabIndex, cursorVisible, cursorX, cursorY, cursorClicking: cursorVisible && isCursorClicking(normalizedProgress, phase, normalizedTabCount), }; }