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:
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;
|
||||
}
|
||||
Reference in new issue
Block a user