Efeitos ao vivo
Quatro efeitos que mantêm uma página em movimento depois que seu conteúdo está na tela:
- Aurora hero: um campo de luz suave (WebGL) atrás do hero que se curva ao redor do ponteiro.
- Cartas holográficas: uma carta inclina-se em direção ao cursor, levanta-se e capta um brilho e uma borda iridescente.
- Spotlight de grade: a grade de fundo da página acende em um círculo suave ao redor do cursor.
- Texto com glitch e scramble: títulos com divisão RGB ao aparecer, e rótulos de links que decodificam ao passar o mouse.
O padrão é "vivo, nunca atrapalhando". As pessoas devem sentir a página reagir a elas, mas nada pode tremer, deslocar o layout, roubar cliques ou ocultar texto de um leitor de tela.
Exemplo ao vivo: https://www.lamanhtruong.com (passe o mouse sobre os cartões de projeto, mova-se sobre o hero, passe o mouse sobre a navegação).
O que está em assets/fx/
| Arquivo | Função |
|---|---|
ticker.ts | Um loop requestAnimationFrame compartilhado que dorme quando nada solicita frames |
built.ts | Portão de prontidão (isBuilt, buildActive), frozen(), PRNG com semente, media query de ponteiro |
aurora.ts | O shader WebGL, seu escalonador de qualidade e ciclo de vida (createAurora) |
hero-shader.tsx | <HeroShader />, o wrapper React para a aurora |
holo.ts | Inclinação + brilho + brilho em cartas, física de mola, overlay injetado |
spotlight.ts | Spotlight de grade que segue o cursor (apenas transform) |
glitch.ts, glitch-copies.tsx | Agendador de glitch, <GlitchCopies /> e <GlitchHeading /> |
scramble.ts, scramble-text.tsx | Executor de scramble e o hook de marcação <ScrambleText /> |
fx-root.tsx | <FxRoot />: monta holo, spotlight, glitch e scramble uma vez |
fx.css | Todos os estilos, tematizados com propriedades customizadas --fx-* |
O código só precisa de React; nenhuma biblioteca de animação. Os templates são verificados com TypeScript estrito contra os tipos do React 18 e 19.
Instalação
- Copie
assets/fx/para o aplicativo, ex.:src/components/fx/. Mantenha os arquivos juntos, pois eles importam uns aos outros por caminho relativo. - Monte-o. A página em si pode continuar sendo um componente servidor estático; apenas
FxRooteHeroShadersão componentes cliente.
import { FxRoot } from "@/components/fx/fx-root";
import { HeroShader } from "@/components/fx/hero-shader";
import { GlitchHeading } from "@/components/fx/glitch-copies";
import { ScrambleText } from "@/components/fx/scramble-text";
export default function Page() {
return (
<div className="fx-grid"> {/* grade base fixa; pule se você já tiver a sua */}
<main className="relative z-[1]"> {/* conteúdo fica acima da grade + spotlight */}
<section className="relative min-h-[80vh]"> {/* hero deve ser position: relative */}
<HeroShader /> {/* primeiro filho, atrás do conteúdo do hero */}
<h1>…</h1>
</section>
<GlitchHeading as="h2" className="text-5xl font-semibold">Trabalhos selecionados</GlitchHeading>
<article data-holo className="rounded-2xl border p-6">
<a href="/work/x" className="inline-flex items-center gap-2">
<ScrambleText>Leia o estudo de caso</ScrambleText> <ArrowIcon />
</a>
</article>
</main>
<FxRoot /> {/* uma vez por página, dentro do wrapper da grade */}
</div>
);
}
- Tema: edite as variáveis
--fx-*no topo defx.csse passepalette/intensitypara<HeroShader />(veja "Tematização" abaixo). - Verifique com as verificações do Playwright em
references/verification.mdantes de considerar concluído.
Desligue partes com props: <FxRoot holo={false} spotlight={false} />. Aponte a inclinação para cartas existentes sem tocar na marcação delas: <FxRoot holo={{ selector: ".card", within: "main" }} />.
Regras que mantêm a suavidade
Estas regras são o que separa "premium" de "travado". Cada uma existe porque quebrá-la causou um problema visível.
-
Aguarde até que o conteúdo esteja pronto. Nunca incline, glitche ou embaralhe algo que outro script ainda está revelando ou medindo. Uma transformação em um elemento durante a revelação corrompe as medições de quem o está animando, e o resultado parece um travamento.
built.tsimplementa um contrato: enquantohtml[data-build="on"]estiver definido, um elemento só conta depois que seu[data-build-item]e[data-build-section]carregam[data-built]. Sem um mecanismo de build na página, tudo conta como construído imediatamente. Qualquer animação de introdução pode optar por isso definindo esses atributos. Detalhes:references/gating-and-performance.md. -
Use um único loop de frames e deixe-o dormir. Cada efeito assina o
ticker.tsem vez de chamarrequestAnimationFramepor conta própria. Um assinante retornafalsequando se estabiliza, e quando ninguém precisa de frames, o loop para. Uma página ociosa então não custa nada, e os efeitos não podem competir entre si por frames. Glitch não usa frames: são keyframes CSS alternados por um atributo. -
Anime apenas
transform,opacityeclip-path. Eles ficam no compositor. Nada aqui muda tamanho, posição no fluxo ou métricas de fonte, portanto, deslocamento de layout é zero por construção. O scramble mantém o texto real no lugar (transparente) e desenha os glifos em um overlay, então mesmo um rótulo em mudança nunca muda de largura. -
Combine o efeito com a entrada. A inclinação e o spotlight de rastreio só existem em
(hover: hover) and (pointer: fine). No toque, inclinar sob um dedo parece um bug, então o toque recebe um breve fade no ponto de toque e uma lente de shader autodeslocante. -
Deixe o DOM do React em paz quando em repouso. O
<span>do overlay holo e otransforminline são adicionados quando uma carta é ativada e removidos quando ela retorna ao repouso. Em repouso, o DOM é exatamente o que o React renderizou, então re-renderizações, hidratação e os estilos:hoverda própria carta continuam funcionando. As camadas de glitch e os spans de scramble são escritos na marcação (<GlitchCopies>,<ScrambleText>) em vez de inseridos em torno de nós de propriedade do React em tempo de execução; reestruturar a árvore do React de fora quebra a reconciliação. -
Mantenha o texto real. As cópias de glitch são apenas do lado do cliente e
aria-hidden, para que o HTML estático, a pesquisa na página e os leitores de tela vejam cada título uma vez. O overlay de scramble éaria-hiddene o rótulo real permanece o nome acessível durante todo o processo. -
Limpe e sobreviva ao StrictMode. Cada
setup*retorna um descartável que remove listeners, observadores, overlays e estilos inline. O shader libera seu buffer e programa, mas deliberadamente não chamaWEBGL_lose_context, porque o React StrictMode monta, desmonta e remonta no mesmo canvas, e um contexto perdido permanece perdido. -
Orce a GPU. O shader renderiza em resolução reduzida sob um orçamento de pixels, desenha apenas enquanto visível, limita os draws a ~80 por segundo em telas de alta taxa de atualização e reduz a qualidade uma vez se a página rodar abaixo de ~38 fps. Ele se recusa a iniciar em um fallback de software (
failIfMajorPerformanceCaveat) e renderiza nada em vez de algo lento.
Os efeitos
Shader de aurora hero (aurora.ts, hero-shader.tsx)
O shader é ruído de valor com warp de domínio (fbm warpado por fbm), desenhado como um brilho amplo mais fios finos e brilhantes ao longo dos contornos warpados. O ponteiro atua como uma lente: ele amplia e gira o fluxo próximo. Entrar no hero, clicar ou mover-se rapidamente deixa cair um anel de ondulação suave. A saída é alpha pré-multiplicado e esmaecido em cada borda, para que o fundo da página e qualquer grade fixa apareçam, e o hero se misture com a próxima seção em vez de terminar em uma caixa.
Ciclo de vida: o canvas surge em 1,8 s depois que sua seção é construída. Ele renderiza apenas enquanto a seção está na tela (IntersectionObserver com margem de 80 px) e a aba está visível. Se o WebGL estiver ausente, não renderiza nada.
Controles:
palette:{ veil, primary, secondary, accent }como hex. A paleta é compilada no shader, portanto, mudanças exigem uma remontagem.intensity: multiplica a luz. 0.6 é calmo, 1.4 é alto.- Legibilidade do texto: o shader desvia a luz do canto inferior esquerdo, onde o texto do hero geralmente fica. Se o seu texto estiver em outro lugar, mude a linha
bias(veja a referência).
Leia references/aurora-shader.md antes de alterar a matemática do shader, a velocidade ou o escalonador de qualidade.
Cartas holográficas (holo.ts)
pointerover delegado encontra a carta sob um ponteiro fino. Ao engajar, mede a caixa não transformada da carta, injeta um span de overlay aria-hidden e executa uma mola criticamente amortecida (sub-passo a 120 Hz para se comportar igual a 60 ou 144 fps) em direção a uma inclinação apontada para o ponteiro. A borda sob o cursor vem em direção ao visualizador. Ao sair, volta com um leve overshoot e remove tudo depois que se estabiliza.
- A inclinação escala com o tamanho da carta, 2,5° a 7,5°: cartas grandes inclinam menos para que suas bordas distantes não balancem. A perspectiva é 1,6× o lado maior. A elevação é de 3 px.
- O overlay tem um brilho radial no ponteiro, um leve lavagem de cor diagonal e um anel de gradiente cônico mascarado na borda (escuro ao redor, brilhante perto do ponteiro).
- Em uma carta com
overflow: hidden, a caixa da borda de 1 px é cortada, então o anel é desenhado logo dentro (.fx-holo--in, detectado automaticamente). - O overlay fica em
z-index: 3compointer-events: none, então ele pinta sobre controles dentro da carta (um alternador de vídeo, um link) sem bloquear cliques. - Uma carta com
position: staticrecebeposition: relativeapenas enquanto ativa. - Opt-out:
data-holo="off"em uma carta ou qualquer ancestral. Pause toda inclinação comhtml[data-fx-freeze]enquanto algo lê retângulos reais (um inspetor, uma passagem de captura de tela). O alternador de inspeção living-ui-swarm é detectado automaticamente.
Spotlight de grade (spotlight.ts, div.fx-spot)
Uma janela fixa de 560 px, mascarada radialmente, contém uma cópia mais brilhante da grade base. Cada frame move apenas duas transformações: a janela segue o cursor, e a grade dentro é deslocada de volta por floor(x / cell) * cell - x para que suas linhas caiam exatamente no reticulado da grade base. Ambas as grades devem estar ancoradas na origem da viewport, e .fx-grid::before está. A lerp de rastreio se estabiliza e o loop dorme quando o cursor para. Ele se esconde no blur da janela ou quando o ponteiro sai da janela. O toque recebe um fade de 360 ms no ponto de toque.
O tamanho da célula da grade, o espaçamento das linhas finas e o tamanho da janela vêm de --fx-grid-cell, --fx-grid-fine e --fx-spot-size (valores em px), para que o CSS permaneça a única fonte da verdade.
Títulos com glitch (glitch.ts, <GlitchHeading> / <GlitchCopies>)
Marcação: um wrapper .fx-glitch[data-fx-glitch] contendo o título real (.fx-glitch__base) e duas cópias apenas do lado do cliente. GlitchHeading escreve tudo; use GlitchCopies diretamente quando o título já existir e carregar outros atributos.
- Entrada: 560 ms quando um título construído está pelo menos 50% visível. O mesmo título não será reproduzido novamente dentro de 2,5 s.
- Micro-glitch: 260 ms em um título visível aleatório a cada 8–14 s.
- A aleatoriedade é semeada, então o ritmo é o mesmo em todas as visitas e nos testes.
- Apenas um título glitcha por vez; as entradas se enfileiram atrás umas das outras.
- Implementado como keyframes CSS puros alternados por
[data-glitch]: fatie comclip-path: inset(), desloque comtranslate3de divida a cor comtext-shadow. --fx-giescala os deslocamentos por execução.
Texto scramble (scramble.ts, <ScrambleText>)
Envolva apenas o rótulo, nunca o ícone: <a><Icon/> <ScrambleText>{label}</ScrambleText></a>.
- Gatilhos: hover (não toque) ou
:focus-visibledo teclado noa,buttonou[data-fx-scr-host]envolvente. - Duração: 360–520 ms, dependendo do comprimento.
- Resolução: os caracteres resolvem da esquerda para a direita com um pequeno tremor. A cauda não resolvida redesenha a ~18 Hz em vez de a cada frame, então ela é lida como decodificação, não ruído.
- Pools de glifos: correspondência de maiúsculas/minúsculas, e pontuação como
@ . / : -permanece no lugar para que e-mails e caminhos permaneçam legíveis durante o scramble. - Limpeza: o overlay é sempre esvaziado, incluindo um fallback de timer para abas em segundo plano onde o rAF para.
Use inline para links inline simples (mantém o sublinhado). Use quiet em botões preenchidos, junto com --fx-scr-tail: currentColor no botão.
Tematização
| Variável | Usada por | Padrão |
|---|---|---|
--fx-accent | camada de glitch A, glifos de scramble, brilho do spotlight, sombra do holo | #5eead4 |
--fx-hot | camada de glitch B | #f472b6 |
--fx-sheen-1..5 | anel holo, em ordem ao redor do círculo | teal, azul, violeta, rosa, âmbar |
--fx-grid-line, --fx-grid-fine-line | linhas da grade base | slate com alpha baixo |
--fx-grid-cell, --fx-grid-fine | espaçamento da grade (px) | 96px, 24px |
--fx-spot-size | janela do spotlight (px) | 560px |
--fx-mono | fonte dos glifos de scramble | mono do sistema |
--fx-scr-tail | substituição de cor do glifo por elemento | não definido |
As cores se misturam através de color-mix(in srgb, …), suportado em todos os navegadores atuais. O shader recebe sua própria paleta: <HeroShader palette={{ accent: "#22d3ee", secondary: "#f0abfc" }} intensity={0.8} />.
Escolha cores de shader da mesma família do acento da página. O shader é um clima, não uma segunda cor de marca; se um visitante de primeira viagem notar o shader antes do título, ele está muito alto.
Camadas
| Camada | z-index | Notas |
|---|---|---|
Grade base (.fx-grid::before, fixa) | 0 | Ancorada na origem da viewport |
Spotlight (div.fx-spot, fixo) | 0 | Após a grade na ordem da árvore, então pinta acima dela |
Conteúdo (main, footer) | 1 | Deve ser posicionado com z-index: 1 |
Shader hero (canvas.fx-shader) | auto | Absoluto dentro do hero, primeiro filho, para que o conteúdo posterior pinte sobre ele |
| Overlay holo | 3 dentro de sua carta | pointer-events: none |
| Cabeçalho fixo, qualquer canvas de overlay de animação de build | acima do conteúdo | Não afetado |
Se o spotlight aparecer acima do seu texto, o wrapper de conteúdo está sem position: relative; z-index: 1.
Preferências de movimento
Em lamanhtruong.com o proprietário escolheu tocar todos os efeitos para todos, incluindo visitantes com prefers-reduced-motion, porque o movimento é o ponto do portfólio. Essa é uma decisão de produto legítima, mas tome-a com propósito. Para respeitar a configuração:
<FxRoot motion="reduce" />desliga holo, spotlight e o movimento das cores de glitch, mas mantém o shader (ele se move lentamente por conta própria) e o scramble (ele resolve a igual, apenas mais rápido). O shader também reduz sua velocidade de deslocamento.- Passe
motion="reduce"emFxRoote seu componente de página pode lerprefers-reduced-motionno CSS e aplicar--fx-motion-reduce: 1;no corpo. - O shader já escuta
prefers-reduced-motione reduz a amplitude de deslocamento se estiver definido, masmotion="reduce"também o desacelera.
Suporte a build engines
O gating de build é opcional e automático. Se a página carregar qualquer atributo data-build relevante antes do primeiro requestAnimationFrame, o sistema espera até que esses nós estejam prontos antes de anexar efeitos. Caso contrário, ele age imediatamente.
Depuração
- Defina
localStorage.fx_log = "1"para ver logs de ciclo de vida de cada efeito no console. - Defina
localStorage.fx_spam = "1"para registrar cada frame de ticker (alto volume). - Pressione Ctrl+Shift+D três vezes para ativar uma sobreposição de depuração que mostra o estado de cada efeito, contagem de frames e uso de GPU.
Testes
As verificações de regressão visual do Playwright estão em references/verification.md. Execute-as em um ambiente de CI que suporte WebGL (por exemplo, Playwright com --disable-gpu não é suficiente; use chromium com args: ['--use-gl=egl']).
Créditos
Efeitos de anhlamtruong/living-ui-effects. Consulte o repositório original para atualizações e problemas.