Files
portifolio-evandrodevbr/app/components/cinematic/FramePulse.tsx
T
Evandro Fonseca Junior 318a764190 perf(movimento): sem tela vazia, passagens em CSS e vitrine leve
Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
2026-10-14 14:15:50 -03:00

66 lines
1.9 KiB
TypeScript

"use client";
import { useThree } from "@react-three/fiber";
import { useEffect } from "react";
/** Gestos que mostram alguém olhando para a página: qualquer um deles religa a batida. */
const ACTIVITY = [
"pointermove",
"pointerdown",
"scroll",
"wheel",
"touchmove",
"keydown",
] as const;
/**
* Batida das cenas WebGL em `frameloop="demand"`: enquanto `active`, pede um quadro a no máximo `fps` por
* segundo (fundo ambiente, sem gastar 60 quadros por segundo); com `active` falso, nenhum quadro é desenhado.
* Depois de `idleMs` sem cursor, rolagem nem teclado, a batida para de vez (nenhum quadro, nenhum
* requestAnimationFrame) e o último quadro fica na tela; qualquer movimento a religa.
* Quem precisa de mais (entrada, rolagem, cursor) pede o próximo quadro com `invalidate()` dentro do useFrame.
*/
export default function FramePulse({
active,
fps = 30,
idleMs = 5000,
}: {
active: boolean;
fps?: number;
idleMs?: number;
}) {
const invalidate = useThree((state) => state.invalidate);
useEffect(() => {
if (!active) return;
const gap = 1000 / fps - 2;
let id = 0;
let last = 0;
let lastActivity = performance.now();
const beat = (now: number) => {
// parada por inatividade: sem laço até o próximo movimento
if (now - lastActivity > idleMs) {
id = 0;
return;
}
id = requestAnimationFrame(beat);
if (now - last < gap) return;
last = now;
invalidate();
};
const wake = () => {
lastActivity = performance.now();
if (!id) id = requestAnimationFrame(beat);
};
for (const type of ACTIVITY)
window.addEventListener(type, wake, { passive: true });
id = requestAnimationFrame(beat);
return () => {
for (const type of ACTIVITY) window.removeEventListener(type, wake);
cancelAnimationFrame(id);
};
}, [active, fps, idleMs, invalidate]);
return null;
}