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.
This commit is contained in:
1 parent
10f170abf4
commit
318a764190
77 files changed
+4293
-4951
No files matched your search
@@ -5,6 +5,48 @@ 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);
|
||||
@@ -12,31 +54,10 @@ export default function FilmGrain() {
|
||||
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
|
||||
);
|
||||
},
|
||||
);
|
||||
return watchProjectScreens((over) => {
|
||||
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" />;
|
||||
|
||||
Reference in new issue
Block a user