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
@@ -4,36 +4,44 @@ import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
|
||||
import type { ShowcaseAssets } from "@/lib/project-showcase";
|
||||
import { useSaveData } from "./showcase/useShowcaseMedia";
|
||||
|
||||
/**
|
||||
* Vídeo de rolagem na capa do card. Quem decide se ele toca é a vitrine (`active`: só um card por vez, o que
|
||||
* está sob o mouse ou o mais perto do centro da tela). Nada do vídeo é baixado antes da primeira vez que o
|
||||
* card fica ativo; fora de cena ele pausa. Com prefers-reduced-motion só o poster aparece.
|
||||
* O poster também fica por baixo (fundo da tela da moldura): o vídeo aparece em fade quando começa a tocar.
|
||||
* Vídeo de rolagem na capa do card. `video` já é a versão do aparelho (quem chama escolhe: celular abaixo de
|
||||
* 768 px; no servidor, nenhuma). Quem decide se ele toca é a vitrine (`active`: só um card por vez, o que está
|
||||
* sob o mouse ou o mais perto do centro da tela). Nada do vídeo é baixado antes da primeira vez que o card
|
||||
* fica ativo (`preload="metadata"`, sem fontes até lá); fora de cena ele pausa. Com prefers-reduced-motion ou
|
||||
* "economizar dados" só o pôster aparece. `showPoster` falso (seção longe da tela) não pede nem o pôster.
|
||||
* O pôster também fica por baixo (fundo da tela da moldura): o vídeo aparece em fade quando começa a tocar.
|
||||
*/
|
||||
export function ProjectVideo({
|
||||
video,
|
||||
title,
|
||||
active,
|
||||
showPoster = true,
|
||||
}: {
|
||||
video: ShowcaseAssets;
|
||||
title: string;
|
||||
active: boolean;
|
||||
showPoster?: boolean;
|
||||
}) {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const saveData = useSaveData();
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const [attached, setAttached] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
const playing = active && !reducedMotion;
|
||||
const posterOnly = reducedMotion || saveData;
|
||||
const playing = active && !posterOnly;
|
||||
const firstSource = video.sources[0]?.src;
|
||||
|
||||
useEffect(() => {
|
||||
if (playing) setAttached(true);
|
||||
}, [playing]);
|
||||
|
||||
useEffect(() => {
|
||||
// as fontes entram depois do primeiro render: o elemento precisa ler a lista nova
|
||||
if (attached) ref.current?.load();
|
||||
}, [attached]);
|
||||
// as fontes entram depois do primeiro render (ou mudam com o aparelho): o elemento precisa ler a lista nova
|
||||
if (attached && firstSource) ref.current?.load();
|
||||
}, [attached, firstSource]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
@@ -46,7 +54,8 @@ export function ProjectVideo({
|
||||
}
|
||||
}, [attached, playing]);
|
||||
|
||||
if (reducedMotion) {
|
||||
if (posterOnly) {
|
||||
if (!showPoster) return null;
|
||||
return (
|
||||
<Image
|
||||
src={video.poster}
|
||||
@@ -65,8 +74,8 @@ export function ProjectVideo({
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={video.poster}
|
||||
preload="metadata"
|
||||
poster={showPoster ? video.poster : undefined}
|
||||
width={video.width}
|
||||
height={video.height}
|
||||
data-ready={ready}
|
||||
|
||||
Reference in new issue
Block a user