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.
44 lines
1.4 KiB
TypeScript
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" />;
|
|
}
|