Files
portifolio-evandrodevbr/app/components/story/notebookTimeline.ts
T

205 lines
5.0 KiB
TypeScript

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,
tabCount: 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 target = CURSOR_POINTS[activeTabIndex % CURSOR_POINTS.length];
if (phase !== "browse" || activeTabIndex === 0) return target;
const browseProgress =
(progress - BROWSE_START) / (BROWSE_END - BROWSE_START);
const slotProgress = browseProgress * tabCount - activeTabIndex;
const previous = CURSOR_POINTS[(activeTabIndex - 1) % CURSOR_POINTS.length];
const moveProgress = clamp(slotProgress / 0.36, 0, 1);
return {
x: interpolate(previous.x, target.x, moveProgress),
y: interpolate(previous.y, target.y, moveProgress),
};
}
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.36 && slotProgress < 0.5;
}
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,
normalizedTabCount,
);
return {
phase,
lidAngle,
deviceOpacity,
screenOpacity,
activeTabIndex,
cursorVisible,
cursorX,
cursorY,
cursorClicking:
cursorVisible &&
isCursorClicking(normalizedProgress, phase, normalizedTabCount),
};
}