Files
Evandro Fonseca Junior 318a764190 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.
2026-10-14 14:15:50 -03:00

61 lines
1.7 KiB
TypeScript

"use client";
import { type CSSProperties, useRef } from "react";
import { useInView } from "@/app/hooks/useInView";
interface TextDiffuseRevealProps {
text: string;
className?: string;
startDelay?: number;
once?: boolean;
amount?: number;
}
/**
* Título que entra palavra por palavra. A animação é CSS (.text-reveal em globals.css) com transparência
* e deslocamento: a placa de vídeo compõe sem redesenhar. A versão anterior animava cada letra com
* desfoque e renderizava o React de novo a cada letra, o que pesava justamente durante as transições.
* Movimento reduzido: o CSS mostra o texto direto, sem animação.
*/
export function TextDiffuseReveal({
text,
className = "",
startDelay = 0,
once = true,
amount = 0.3,
}: Readonly<TextDiffuseRevealProps>) {
const ref = useRef<HTMLSpanElement | null>(null);
const isInView = useInView(ref, { once, amount });
let word = 0;
return (
<span
ref={ref}
className={`text-reveal ${className}`.trim()}
data-shown={isInView ? "" : undefined}
style={{ "--d": `${startDelay}s` } as CSSProperties}
>
<span className="sr-only">{text}</span>
<span aria-hidden="true">
{text
.split(/(\n|\s+)/)
.filter(Boolean)
.map((token, index) => {
const key = `${index}:${token}`;
if (token === "\n") return <br key={key} />;
if (/^\s+$/.test(token)) return " ";
return (
<span
key={key}
className="text-reveal-word"
style={{ "--i": word++ } as CSSProperties}
>
{token}
</span>
);
})}
</span>
</span>
);
}