Files
portifolio-evandrodevbr/app/components/cinematic/FilmGrain.tsx
T
Evandro Fonseca Junior c9a0b80656 perf(movimento): uma transição, 3D leve e vídeo certo por aparelho
Passagens com fade e 32 px, cena anterior termina em opacidade 0, sem visibility nem clip-path. 3D do hero como fundo ambiente, sob demanda e desligado em toque, aparelho fraco, saveData e movimento reduzido. Lenis fora do toque e salto direto em âncoras distantes. Vitrine só de clientes, até 3 capítulos, fixa só no desktop e carrossel no celular; vídeo só do projeto em cena com preload metadata. Rótulos mono a partir de 12 px e limão só no contato.
2026-10-14 03:32:23 -03:00

44 lines
1.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
/** Telas dos projetos (vitrine e capas): nenhum efeito pode ficar por cima delas. */
const PROJECT_SCREENS = ".ds-reel, .ds-frame, .ds-bframe, .ds-phone";
/** Grão de filme sobre a página. Some enquanto qualquer tela de projeto está à vista (data-over-projects). */
export default function FilmGrain() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const grain = ref.current;
if (!grain) return;
let frame = 0;
const update = () => {
frame = 0;
const over = Array.from(document.querySelectorAll(PROJECT_SCREENS)).some(
(screen) => {
const rect = screen.getBoundingClientRect();
return (
rect.height > 0 && rect.bottom > 0 && rect.top < window.innerHeight
);
},
);
if (over) grain.dataset.overProjects = "";
else delete grain.dataset.overProjects;
};
const queue = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener("scroll", queue, { passive: true });
window.addEventListener("resize", queue);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("scroll", queue);
window.removeEventListener("resize", queue);
};
}, []);
return <div ref={ref} aria-hidden="true" className="film-grain" />;
}