Efectos vivos
Cuatro efectos que mantienen una página en movimiento una vez que su contenido está en pantalla:
- Héroe aurora: un campo WebGL de luz suave detrás del héroe que se curva alrededor del puntero.
- Tarjetas holograma: una tarjeta se inclina hacia el cursor, se eleva y captura un deslumbramiento y un borde iridiscente.
- Foco de cuadrícula: la cuadrícula de fondo de la página se ilumina en un círculo suave alrededor del cursor.
- Texto con fallo y revuelto: los encabezados se dividen en RGB cuando aparecen, y las etiquetas de enlace se decodifican al pasar el ratón.
El estándar es "vivo, nunca molesto". Las personas deben sentir que la página reacciona a ellas, pero nada puede tironear, cambiar el diseño, robar clics u ocultar texto de un lector de pantalla.
Ejemplo en vivo: https://www.lamanhtruong.com (pasa el ratón sobre las tarjetas de proyecto, muévete sobre el héroe, pasa el ratón sobre la navegación).
Qué hay en assets/fx/
| Archivo | Rol |
|---|---|
ticker.ts | Un bucle requestAnimationFrame compartido que se duerme cuando nadie pide fotogramas |
built.ts | Compuerta de preparación (isBuilt, buildActive), frozen(), PRNG con semilla, consulta de medios para puntero |
aurora.ts | El shader WebGL, su escalador de calidad y ciclo de vida (createAurora) |
hero-shader.tsx | <HeroShader />, el envoltorio React para la aurora |
holo.ts | Inclinación + deslumbramiento + brillo en tarjetas, física de muelle, superposición inyectada |
spotlight.ts | Foco de cuadrícula que sigue al cursor (solo transform) |
glitch.ts, glitch-copies.tsx | Planificador de fallo, <GlitchCopies /> y <GlitchHeading /> |
scramble.ts, scramble-text.tsx | Ejecutor de revuelto y el enganche de marcado <ScrambleText /> |
fx-root.tsx | <FxRoot />: monta holo, foco, fallo y revuelto una vez |
fx.css | Todos los estilos, tematizados con propiedades personalizadas --fx-* |
El código solo necesita React; ninguna biblioteca de animación. Las plantillas verifican tipos bajo TypeScript estricto contra los tipos de React 18 y 19.
Instalar
- Copia
assets/fx/en la aplicación, p.ej.src/components/fx/. Mantén los archivos juntos, ya que se importan entre sí por ruta relativa. - Móntalo. La página en sí puede seguir siendo un componente servidor estático; solo
FxRootyHeroShaderson 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"> {/* cuadrícula base fija; omite si tienes la tuya propia */}
<main className="relative z-[1]"> {/* el contenido está sobre la cuadrícula + el foco */}
<section className="relative min-h-[80vh]"> {/* el héroe debe tener position: relative */}
<HeroShader /> {/* primer hijo, detrás del contenido del héroe */}
<h1>…</h1>
</section>
<GlitchHeading as="h2" className="text-5xl font-semibold">Trabajo seleccionado</GlitchHeading>
<article data-holo className="rounded-2xl border p-6">
<a href="/work/x" className="inline-flex items-center gap-2">
<ScrambleText>Leer el caso de estudio</ScrambleText> <ArrowIcon />
</a>
</article>
</main>
<FxRoot /> {/* una vez por página, dentro del envoltorio de cuadrícula */}
</div>
);
}
- Tematízalo: edita las variables
--fx-*en la parte superior defx.css, y pasapalette/intensitya<HeroShader />(consulta "Tematización" más abajo). - Verifica con las comprobaciones de Playwright en
references/verification.mdantes de darlo por terminado.
Apaga piezas con props: <FxRoot holo={false} spotlight={false} />. Apunta la inclinación a tarjetas existentes sin tocar su marcado: <FxRoot holo={{ selector: ".card", within: "main" }} />.
Reglas que lo mantienen fluido
Estas reglas son lo que separa "premium" de "con tirones". Cada una existe porque romperla causó un problema visible.
-
Espera hasta que el contenido esté listo. Nunca inclines, hagas fallo o revuelvas algo que otro script todavía está revelando o midiendo. Una transformación en un elemento a medio revelar corrompe las mediciones de quien lo está animando, y el resultado parece un tartamudeo.
built.tsimplementa un contrato: mientrashtml[data-build="on"]esté establecido, un elemento solo cuenta una vez que su[data-build-item]y[data-build-section]llevan[data-built]. Sin un motor de construcción en la página, todo cuenta como construido de inmediato. Cualquier animación de entrada puede optar por establecer esos atributos. Detalles:references/gating-and-performance.md. -
Usa un bucle de fotogramas, y déjalo dormir. Cada efecto se suscribe a
ticker.tsen lugar de llamar arequestAnimationFramepor su cuenta. Un suscriptor devuelvefalsecuando se ha estabilizado, y cuando nadie necesita fotogramas, el bucle se detiene. Una página inactiva entonces no cuesta nada, y los efectos no pueden pelear entre sí por los fotogramas. El fallo no usa fotogramas en absoluto: son keyframes de CSS activados por un atributo. -
Anima solo
transform,opacityyclip-path. Estos se quedan en el compositor. Nada aquí cambia de tamaño, posición en el flujo o métricas de fuente, por lo que el cambio de diseño es cero por construcción. El revuelto mantiene el texto real en su lugar (transparente) y dibuja los glifos en una superposición, por lo que incluso una etiqueta que cambia nunca cambia de ancho. -
Adecúa el efecto a la entrada. La inclinación y el foco que sigue solo existen en
(hover: hover) and (pointer: fine). En táctil, inclinarse bajo un dedo se lee como un error, por lo que el táctil obtiene un breve desvanecimiento en el punto de toque y una lente shader autodesplazante en su lugar. -
Deja el DOM de React en reposo. El
<span>de superposición de holo y latransformen línea se añaden cuando una tarjeta se activa y se eliminan cuando ha vuelto a su estado de reposo. En reposo, el DOM es exactamente lo que React renderizó, por lo que los rerenderizados, la hidratación y los estilos:hoverde la tarjeta siguen funcionando. Las capas de fallo y los span de revuelto están escritos en el marcado (<GlitchCopies>,<ScrambleText>) en lugar de insertarse alrededor de nodos propiedad de React en tiempo de ejecución; reestructurar el árbol de React desde fuera rompe la reconciliación. -
Mantén el texto real. Las copias de fallo son solo cliente y
aria-hidden, por lo que el HTML estático, la búsqueda en página y los lectores de pantalla ven cada encabezado una vez. La superposición de revuelto esaria-hiddeny la etiqueta real sigue siendo el nombre accesible durante todo el proceso. -
Limpia y sobrevive al StrictMode. Cada
setup*devuelve un eliminador que elimina listeners, observadores, superposiciones y estilos en línea. El shader libera su búfer y programa, pero deliberadamente no llama aWEBGL_lose_context, porque React StrictMode monta, desmonta y vuelve a montar en el mismo canvas, y un contexto perdido se queda perdido. -
Presupuesta la GPU. El shader renderiza a resolución reducida bajo un presupuesto de píxeles, dibuja solo mientras es visible, limita los dibujos a ~80 por segundo en pantallas de alta frecuencia de actualización, y reduce la calidad una vez si la página funciona por debajo de ~38 FPS. Se niega a iniciar en una alternativa de software (
failIfMajorPerformanceCaveat) y no renderiza nada en lugar de algo lento.
Los efectos
Shader héroe aurora (aurora.ts, hero-shader.tsx)
El shader es ruido de valor con deformación de dominio (fbm deformado por fbm), dibujado como un amplio resplandor más hebras brillantes y delgadas a lo largo de los contornos deformados. El puntero actúa como una lente: magnifica y arremolina el flujo cercano. Entrar en el héroe, hacer clic o moverse rápido deja caer un anillo de ondulación suave. La salida es alfa premultiplicado y se desvanece en cada borde, por lo que el fondo de la página y cualquier cuadrícula fija se ven a través, y el héroe se mezcla con la siguiente sección en lugar de terminar en una caja.
Ciclo de vida: el canvas aparece gradualmente en 1.8 s una vez que su sección está construida. Renderiza solo mientras la sección está en pantalla (IntersectionObserver con un margen de 80 px) y la pestaña es visible. Si WebGL no está disponible, no renderiza nada.
Perillas:
palette:{ veil, primary, secondary, accent }como hex. La paleta se compila en el shader, por lo que los cambios necesitan un remontaje.intensity: multiplica la luz. 0.6 es calmado, 1.4 es intenso.- Legibilidad del texto: el shader sesga la luz lejos de la esquina inferior izquierda, donde suele estar el texto del héroe. Si tu texto está en otro lugar, cambia la línea
bias(consulta la referencia).
Lee references/aurora-shader.md antes de cambiar las matemáticas del shader, la velocidad o el escalador de calidad.
Tarjetas holograma (holo.ts)
Un pointerover delegado encuentra la tarjeta bajo un puntero fino. Al activarse, mide la caja sin transformar de la tarjeta, inyecta un span de superposición aria-hidden, y ejecuta un muelle con amortiguación crítica (subdividido a 120 Hz para que se comporte igual a 60 o 144 fps) hacia una inclinación apuntada al puntero. El borde bajo el cursor viene hacia el espectador. Al salir, vuelve con un ligero rebasamiento y elimina todo una vez que se ha estabilizado.
- La inclinación escala con el tamaño de la tarjeta, 2.5° a 7.5°: las tarjetas grandes se inclinan menos para que sus bordes lejanos no oscilen. La perspectiva es 1.6 veces el lado más grande. La elevación es de 3 px.
- La superposición tiene un deslumbramiento radial en el puntero, un lavado de color diagonal tenue y un anillo de gradiente cónico enmascarado al borde (tenue en todas partes, brillante cerca del puntero).
- En una tarjeta con
overflow: hiddenla caja del borde de 1 px está recortada, por lo que el anillo se dibuja justo en el interior (.fx-holo--in, detectado automáticamente). - La superposición está en
z-index: 3conpointer-events: none, por lo que pinta sobre los controles dentro de la tarjeta (un alternador de video, un enlace) sin bloquear los clics. - Una tarjeta con
position: staticobtieneposition: relativesolo mientras está activa. - Opt-out:
data-holo="off"en una tarjeta o cualquier ancestro. Pausa toda inclinación conhtml[data-fx-freeze]mientras algo lee rectángulos reales (un inspector, una pasada de captura de pantalla). El alternador de inspección living-ui-swarm se detecta automáticamente.
Foco de cuadrícula (spotlight.ts, div.fx-spot)
Una ventana fija de 560 px, enmascarada radialmente, contiene una copia más brillante de la cuadrícula base. Cada fotograma mueve solo dos transformaciones: la ventana sigue al cursor, y la cuadrícula interior se desplaza hacia atrás por floor(x / cell) * cell - x para que sus líneas caigan exactamente sobre el entramado de la cuadrícula base. Ambas cuadrículas deben estar ancladas al origen de la ventana gráfica, y .fx-grid::before lo está. La interpolación rezagada se estabiliza y el bucle se duerme cuando el cursor se detiene. Se oculta al perder el foco de la ventana o cuando el puntero sale de la ventana. El táctil obtiene un desvanecimiento de 360 ms en el punto de toque.
El tamaño de celda de la cuadrícula, el espaciado de líneas finas y el tamaño de la ventana provienen de --fx-grid-cell, --fx-grid-fine y --fx-spot-size (valores en px), por lo que CSS sigue siendo la fuente única de verdad.
Encabezados con fallo (glitch.ts, <GlitchHeading> / <GlitchCopies>)
Marcado: un envoltorio .fx-glitch[data-fx-glitch] que contiene el encabezado real (.fx-glitch__base) y dos copias solo cliente. GlitchHeading escribe todo; usa GlitchCopies directamente cuando el encabezado ya existe y lleva otros atributos.
- Entrada: 560 ms cuando un encabezado construido está al menos al 50% visible. El mismo encabezado no se reproducirá de nuevo dentro de 2.5 s.
- Micro-fallo: 260 ms en un encabezado visible aleatorio cada 8–14 s.
- La aleatoriedad tiene semilla, por lo que el ritmo es el mismo en cada visita y en las pruebas.
- Solo un encabezado falla a la vez; las entradas se ponen en cola detrás de la otra.
- Implementado como keyframes CSS puros activados por
[data-glitch]: corta conclip-path: inset(), desplaza contranslate3d, y divide el color context-shadow. --fx-giescala los desplazamientos por ejecución.
Texto revuelto (scramble.ts, <ScrambleText>)
Envuelve solo la etiqueta, nunca el icono: <a><Icon/> <ScrambleText>{etiqueta}</ScrambleText></a>.
- Disparadores: pasar el ratón (no táctil) o
:focus-visibledel teclado en ela,buttono[data-fx-scr-host]que lo envuelve. - Duración: 360–520 ms, dependiendo de la longitud.
- Resolución: los caracteres se resuelven de izquierda a derecha con un poco de fluctuación. La cola no resuelta se redibuja a ~18 Hz en lugar de cada fotograma, por lo que se lee como decodificación, no como ruido.
- Conjuntos de glifos: coinciden con mayúsculas/minúsculas, y la puntuación como
@ . / : -se queda quieta para que los correos electrónicos y las rutas sigan siendo legibles durante el revuelto. - Limpieza: la superposición siempre se vacía, incluido un resguardo de temporizador para pestañas en segundo plano donde rAF se detiene.
Usa inline para enlaces en línea simples (mantiene el subrayado). Usa quiet en botones rellenos, junto con --fx-scr-tail: currentColor en el botón.
Tematización
| Variable | Usada por | Por defecto |
|---|---|---|
--fx-accent | capa de fallo A, glifos de revuelto, resplandor del foco, sombra de holo | #5eead4 |
--fx-hot | capa de fallo B | #f472b6 |
--fx-sheen-1..5 | anillo holo, en orden alrededor del círculo | verde azulado, azul, violeta, rosa, ámbar |
--fx-grid-line, --fx-grid-fine-line | líneas de cuadrícula base | pizarra de baja opacidad |
--fx-grid-cell, --fx-grid-fine | espaciado de cuadrícula (px) | 96px, 24px |
--fx-spot-size | ventana de foco (px) | 560px |
--fx-mono | fuente de glifos de revuelto | mono del sistema |
--fx-scr-tail | anulación de color de glifo por elemento | sin establecer |
Los colores se mezclan a través de color-mix(in srgb, …), compatible con todos los navegadores actuales. El shader toma su propia paleta: <HeroShader palette={{ accent: "#22d3ee", secondary: "#f0abfc" }} intensity={0.8} />.
Elige colores para el shader de la misma familia que el acento de la página. El shader es un ambiente, no un segundo color de marca; si un visitante primerizo nota el shader antes que el titular, es demasiado fuerte.
Capas
| Capa | z-index | Notas |
|---|---|---|
Cuadrícula base (.fx-grid::before, fija) | 0 | Anclada al origen de la ventana gráfica |
Foco (div.fx-spot, fijo) | 0 | Después de la cuadrícula en orden de árbol, por lo que pinta sobre ella |
Contenido (main, pie de página) | 1 | Debe estar posicionado con z-index: 1 |
Shader del héroe (canvas.fx-shader) | auto | Absoluto dentro del héroe, primer hijo, por lo que el contenido posterior pinta sobre él |
| Superposición holo | 3 dentro de su tarjeta | pointer-events: none |
| Encabezado fijo, cualquier canvas de superposición de animación de construcción | sobre el contenido | No afectado |
Si el foco se muestra sobre tu texto, al envoltorio de contenido le falta position: relative; z-index: 1.
Preferencias de movimiento
En lamanhtruong.com, el propietario eligió reproducir todos los efectos para todos, incluidos los visitantes con prefers-reduced-motion, porque el movimiento es el punto del portafolio. Esa es una decisión de producto legítima, pero tómala a propósito. Para respetar la preferencia en su lugar:
<FxRoot motion="reduce" />: respectaprefers-reduced-motiony desactiva los efectos de parpadeo y movimiento (fallo, revuelto, inclinación de tarjetas y foco de cuadrícula). La aurora se mantiene como una animación de fondo sutil y suave. El foco de cuadrícula se convierte en un simple realce de opacidad estático.<FxRoot motion="off" />: desactiva todos los efectos animados. La página se renderiza exactamente como lo haría sin el skill.- El valor por defecto es
"on", que reproduce todos los efectos independientemente de la preferencia del usuario.
La detección de prefers-reduced-motion se realiza a través de una consulta de medios CSS y se pasa al contexto de FxRoot. Los efectos individuales pueden optar por no participar consultando la misma variable CSS o el contexto de React.
Referencias
Se proporcionan documentos de referencia adicionales en el directorio references/:
references/aurora-shader.md: Explicación detallada de las matemáticas del shader, la deformación de dominio, la función de ruido y el modelo de iluminación. Léelo antes de modificar el shader.references/gating-and-performance.md: Explicación del sistema de compuerta de construcción, cómo funcionabuilt.tsy cómo los efectos esperan a que el contenido esté listo. También cubre las optimizaciones de rendimiento y el bucle de fotogramas.references/verification.md: Lista de verificación de Playwright para asegurar que los efectos funcionan correctamente, no causan problemas de accesibilidad y mantienen el rendimiento.
Solución de problemas
| Problema | Causa probable | Solución |
|---|---|---|
| El shader no se muestra | WebGL no disponible o falló | Verifica la consola del navegador para mensajes de error de WebGL. Asegúrate de que el canvas esté dentro de una sección con position: relative. |
| Las tarjetas no se inclinan | Selector incorrecto o tarjeta envuelta | Usa <FxRoot holo={{ selector: ".mi-tarjeta" }} /> o asegúrate de que la tarjeta tenga el atributo data-holo. |
| El foco de cuadrícula no aparece | El envoltorio de contenido carece de z-index | Añade position: relative; z-index: 1 al <main> o contenedor de contenido. |
| El texto revuelto no se decodifica | El elemento no es a o button | Envuelve el texto en <ScrambleText> y asegúrate de que el elemento padre sea un enlace o botón. |
| El fallo de encabezado no se reproduce | El encabezado no está en vista | Desplázate para que el encabezado esté al menos al 50% visible. La reproducción está limitada a una vez cada 2.5 s. |
| El rendimiento es lento | Demasiados efectos o GPU débil | Reduce la calidad del shader con intensity más bajo o desactiva efectos no esenciales con props en FxRoot. |
Licencia
El código proporcionado en este skill se ofrece como ejemplo y guía. Siéntete libre de adaptarlo y usarlo en tus propios proyectos. Se agradece la atribución, pero no es obligatoria.
Documentación generada para el skill anhlamtruong/living-ui-effects. Para la documentación más reciente y ejemplos, visita el repositorio original.