feat: refine portfolio showcase, services and quality lab
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.
This commit is contained in:
Evandro Fonseca Junior committed 2026-09-05 14:20:54 -03:00
1 parent e2b20bc699
commit 8b12e51bc5
35 files changed
+2359 -1299

No files matched your search

+79 -177
View File
@@ -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;
}