Files
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

65 lines
2.1 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";
/**
* Avisa `onChange(true)` enquanto qualquer tela de projeto está à vista. Usa IntersectionObserver (nada mede a
* página a cada quadro de rolagem) e um MutationObserver só para pegar as telas que a página cria ou remove
* depois (a vitrine fixa troca a lista ao hidratar). Devolve a função que desfaz tudo.
*/
function watchProjectScreens(onChange: (over: boolean) => void) {
if (typeof IntersectionObserver === "undefined") return () => {};
const watched = new Set<Element>();
const inView = new Set<Element>();
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting && entry.boundingClientRect.height > 0)
inView.add(entry.target);
else inView.delete(entry.target);
}
onChange(inView.size > 0);
});
const scan = () => {
for (const screen of document.querySelectorAll(PROJECT_SCREENS)) {
if (watched.has(screen)) continue;
watched.add(screen);
observer.observe(screen);
}
for (const screen of watched) {
if (screen.isConnected) continue;
observer.unobserve(screen);
watched.delete(screen);
inView.delete(screen);
}
onChange(inView.size > 0);
};
scan();
const mutations = new MutationObserver(scan);
mutations.observe(document.body, { childList: true, subtree: true });
return () => {
observer.disconnect();
mutations.disconnect();
};
}
/** 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;
return watchProjectScreens((over) => {
if (over) grain.dataset.overProjects = "";
else delete grain.dataset.overProjects;
});
}, []);
return <div ref={ref} aria-hidden="true" className="film-grain" />;
}