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.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 03:32:23 -03:00
1 parent 8d07b8c6bb
commit c9a0b80656
48 files changed
+7210 -1241

No files matched your search

@@ -0,0 +1,82 @@
"use client";
import { type RefObject, useEffect, useState } from "react";
import { isSaveData, MOBILE_QUERY } from "@/lib/device";
import type { Device } from "@/lib/project-showcase";
/**
* Versão de vídeo e pôster que vale para o aparelho: "mobile" abaixo de 768 px, "desktop" acima. É `null`
* até a página montar no navegador (no servidor nenhum arquivo de mídia é escolhido: só o espaço da capa).
*/
export function useViewportDevice(): Device | null {
const [device, setDevice] = useState<Device | null>(null);
useEffect(() => {
if (typeof window.matchMedia !== "function") {
setDevice("desktop");
return;
}
const query = window.matchMedia(MOBILE_QUERY);
const update = () => setDevice(query.matches ? "mobile" : "desktop");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return device;
}
/** Telas em que a vitrine fixa cabe: tablet ou desktop, com altura para o notebook e a legenda (showcase.css usa a mesma conta). */
export const PINNED_QUERY = "(min-width: 768px) and (min-height: 561px)";
/** A vitrine fixa cabe nesta tela? `null` até montar no navegador. */
export function usePinnedViewport(): boolean | null {
const [pinned, setPinned] = useState<boolean | null>(null);
useEffect(() => {
if (typeof window.matchMedia !== "function") {
setPinned(true);
return;
}
const query = window.matchMedia(PINNED_QUERY);
const update = () => setPinned(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return pinned;
}
/** "Economizar dados" ligado (lido no navegador, depois de montar): só pôsteres, nenhum vídeo baixado. */
export function useSaveData(): boolean {
const [saveData, setSaveData] = useState(false);
useEffect(() => {
setSaveData(isSaveData());
}, []);
return saveData;
}
/** Margem em volta da janela dentro da qual uma seção conta como "perto": uma tela acima e abaixo. */
export const NEAR_MARGIN = "100% 0px";
/**
* Verdadeiro quando o elemento chegou a até ~1 tela da janela: só então o pôster dele é pedido. Depois de
* perto, continua verdadeiro (o pôster já baixado não some e volta).
*/
export function useNearViewport(ref: RefObject<Element | null>): boolean {
const [near, setNear] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
if (typeof IntersectionObserver === "undefined") {
setNear(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) setNear(true);
},
{ rootMargin: NEAR_MARGIN },
);
observer.observe(element);
return () => observer.disconnect();
}, [ref]);
return near;
}