Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
This commit is contained in:
1 parent
e2b20bc699
commit
8b12e51bc5
35 files changed
+2359
-1299
No files matched your search
@@ -19,197 +19,99 @@ export type NotebookFrame = {
|
||||
cursorClicking: boolean;
|
||||
};
|
||||
|
||||
const OPEN_START = 0.06;
|
||||
const BOOT_START = 0.18;
|
||||
const CURSOR_START = 0.23;
|
||||
const BROWSE_START = 0.3;
|
||||
const BROWSE_END = 0.84;
|
||||
const CLOSE_START = 0.9;
|
||||
const CLOSE_LID_START = 0.93;
|
||||
const OPEN_ANGLE = 112;
|
||||
const BROWSE_START = 0.44;
|
||||
const BROWSE_END = 0.78;
|
||||
const HOLD_END = 0.87;
|
||||
const CLOSE_LID_START = 0.9;
|
||||
const TAB_CLICK_Y = 0.3;
|
||||
const PARK = { x: 0.5, y: 0.95 };
|
||||
const ARRIVAL = 0.18;
|
||||
|
||||
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 between(value: number, start: number, end: number): number {
|
||||
return clamp((value - start) / (end - start), 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,
|
||||
export function getNotebookSeekProgress(
|
||||
index: 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 tabCenterX(index: number, tabCount: number): number {
|
||||
if (tabCount < 1) return PARK.x;
|
||||
return (index + 0.5) / tabCount;
|
||||
}
|
||||
|
||||
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(PARK.x, tabCenterX(0, tabCount), cursorProgress),
|
||||
y: interpolate(PARK.y, TAB_CLICK_Y, cursorProgress),
|
||||
};
|
||||
}
|
||||
|
||||
if (activeTabIndex < 0) {
|
||||
return { ...PARK };
|
||||
}
|
||||
|
||||
const target = { x: tabCenterX(activeTabIndex, tabCount), y: TAB_CLICK_Y };
|
||||
if (phase !== "browse" || activeTabIndex === 0) return target;
|
||||
|
||||
const browseProgress =
|
||||
(progress - BROWSE_START) / (BROWSE_END - BROWSE_START);
|
||||
const slotProgress = browseProgress * tabCount - activeTabIndex;
|
||||
const previous = {
|
||||
x: tabCenterX(activeTabIndex - 1, tabCount),
|
||||
y: TAB_CLICK_Y,
|
||||
};
|
||||
const moveProgress = clamp(slotProgress / 0.36, 0, 1);
|
||||
|
||||
return {
|
||||
x: interpolate(previous.x, target.x, moveProgress),
|
||||
y: target.y,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
const count = Number.isFinite(tabCount)
|
||||
? Math.max(0, Math.floor(tabCount))
|
||||
: 0;
|
||||
if (!count) return BROWSE_START;
|
||||
const selected = clamp(
|
||||
Number.isFinite(index) ? Math.floor(index) : 0,
|
||||
0,
|
||||
count - 1,
|
||||
);
|
||||
return (
|
||||
BROWSE_START + ((BROWSE_END - BROWSE_START) * (selected + 0.6)) / count
|
||||
);
|
||||
}
|
||||
|
||||
/** One reversible clock. Each tab arrives/clicks before its long reading hold. */
|
||||
export function getNotebookFrame(
|
||||
progress: number,
|
||||
tabCount: number,
|
||||
target?: NotebookFrame,
|
||||
): 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
|
||||
? normalizedProgress < CLOSE_LID_START
|
||||
? OPEN_ANGLE
|
||||
: interpolate(
|
||||
OPEN_ANGLE,
|
||||
0,
|
||||
(normalizedProgress - CLOSE_LID_START) / (1 - CLOSE_LID_START),
|
||||
)
|
||||
: 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) / (CLOSE_LID_START - 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),
|
||||
};
|
||||
const p = clamp(Number.isFinite(progress) ? progress : 0, 0, 1);
|
||||
const count = Number.isFinite(tabCount)
|
||||
? Math.max(0, Math.floor(tabCount))
|
||||
: 0;
|
||||
const phase: NotebookPhase =
|
||||
p < OPEN_START
|
||||
? "approach"
|
||||
: p < BOOT_START
|
||||
? "open"
|
||||
: p < CURSOR_START
|
||||
? "boot"
|
||||
: p < BROWSE_START
|
||||
? "cursor-in"
|
||||
: p < BROWSE_END
|
||||
? "browse"
|
||||
: p < CLOSE_START
|
||||
? "hold"
|
||||
: "close";
|
||||
const slot = between(p, BROWSE_START, BROWSE_END) * count;
|
||||
const destination = clamp(Math.floor(slot), 0, Math.max(0, count - 1));
|
||||
const local = slot - destination;
|
||||
const moving = phase === "browse" && destination > 0 && local < ARRIVAL;
|
||||
const activeTabIndex = !count ? -1 : moving ? destination - 1 : destination;
|
||||
const travel = between(local, 0, ARRIVAL);
|
||||
const cursorTab = moving
|
||||
? destination - 1 + travel * travel * (3 - 2 * travel)
|
||||
: destination;
|
||||
const entry = between(p, CURSOR_START, BROWSE_START);
|
||||
const frame = target ?? ({} as NotebookFrame);
|
||||
frame.phase = phase;
|
||||
frame.lidAngle =
|
||||
OPEN_ANGLE *
|
||||
between(p, OPEN_START, BOOT_START) *
|
||||
(1 - between(p, CLOSE_LID_START, 1));
|
||||
frame.deviceOpacity = between(p, 0, OPEN_START);
|
||||
frame.screenOpacity =
|
||||
between(p, BOOT_START, CURSOR_START) *
|
||||
(1 - between(p, CLOSE_START, CLOSE_LID_START));
|
||||
frame.activeTabIndex = activeTabIndex;
|
||||
frame.cursorVisible = count > 0 && p >= CURSOR_START && p < CLOSE_START;
|
||||
frame.cursorX = count
|
||||
? phase === "cursor-in"
|
||||
? 0.5 + (0.5 / count - 0.5) * entry
|
||||
: (cursorTab + 0.5) / count
|
||||
: 0.5;
|
||||
frame.cursorY = phase === "cursor-in" ? 0.95 - 0.65 * entry : 0.3;
|
||||
frame.cursorClicking =
|
||||
count > 0 &&
|
||||
phase === "browse" &&
|
||||
(destination === 0
|
||||
? local < 0.07
|
||||
: local >= ARRIVAL && local < ARRIVAL + 0.07);
|
||||
return frame;
|
||||
}
|
||||
Reference in new issue
Block a user