CI / verify (push) Failing after 5m9s
Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
118 lines
3.3 KiB
TypeScript
118 lines
3.3 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_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 ARRIVAL = 0.18;
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function between(value: number, start: number, end: number): number {
|
|
return clamp((value - start) / (end - start), 0, 1);
|
|
}
|
|
|
|
export function getNotebookSeekProgress(
|
|
index: number,
|
|
tabCount: number,
|
|
): number {
|
|
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 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;
|
|
}
|