Communitygithub.com

anhlamtruong/living-ui-swarm

Construye animaciones de "enjambre de agentes" que ensamblan secciones al hacer scroll en React / Next.js. Un equipo de cursores con roles de colores (planificador, codificadores, diseñadores, maquetador, tester, revisor, desplegador) construye cada sección a medida que aparece en la ventana.

¿Qué es living-ui-swarm?

Este skill proporciona un motor completo para crear animaciones de página en las que un enjambre de cursores con roles de colores construye visualmente cada sección a medida que el usuario hace scroll. Está diseñado para React/Next.js y utiliza una capa de lienzo fija superpuesta junto con GSAP para las animaciones y Lenis para el desplazamiento suave. El contenido es HTML renderizado en el servidor que permanece accesible todo el tiempo; el motor solo oculta elementos brevemente para reproducir coreografías. Es ideal para páginas de presentación, portafolios o cualquier sitio donde la narrativa visual de construcción sea fundamental. El motor recuerda las construcciones completadas por pestaña (usando sessionStorage) para evitar repeticiones en recargas. Soporta personalización mediante atributos de datos en el HTML, variables CSS, y ofrece controles como omisión, repetición e inspección. También gestiona escenarios como rendimiento en móviles, movimiento reducido, y convive con otros efectos visuales.

Compatible con~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/anhlamtruong/living-ui-skills/tree/HEAD/skills/living-ui-swarm

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Vista previa

Del README del skill

living-ui-swarm preview

Documentación

Animación de construcción por enjambre de agentes

Una capa de lienzo fija dibuja un pequeño equipo de cursores que "construyen" cada sección de una página cuando aparece en la ventana. El contenido es HTML real, renderizado en el servidor todo el tiempo. El motor solo oculta elementos durante unos segundos y reproduce una breve coreografía para cada uno.

Ejemplo en vivo: https://www.lamanhtruong.com. Su código fuente está anclado en portfolio-anh@a5d1651. assets/swarm/ contiene el mismo motor, limpiado para reutilización.

Cuándo usarlo y cuándo no

Úsalo cuando la página en sí sea la propuesta: un portafolio, una página de lanzamiento o historia de producto, un caso de estudio, una página "sobre nuestra IA". La gente visita una vez, hace scroll de arriba a abajo, y la construcción les dice algo ("esta persona lanza con agentes").

Evítalo para cualquier cosa que la gente use a diario: paneles de control, documentación, configuraciones, pago, lectura extensa. La construcción cuesta unos segundos por sección, y un usuario que regresa no tiene razón para verla dos veces. Por eso el motor recuerda una construcción completada por pestaña (sessionStorage) y nunca la reproduce al recargar.

Lo que cuesta:

  • Aproximadamente 5000 líneas de TypeScript. La capa de construcción se carga con next/dynamic después de la hidratación, por lo que la primera pintura nunca la espera; las piezas de desplazamiento suave y paralaje (Lenis, GSAP, ScrollTrigger) se cargan con la página. Una aplicación nueva de Next.js 15 con las cuatro habilidades living-ui resultó en aproximadamente 67 kB de JS de ruta (170 kB en la primera carga).
  • Una pequeña puntuación de cambio de diseño mientras se llena el registro en vivo (aproximadamente 0.03 en una página de prueba, muy por debajo de la línea "buena" de 0.1): las nuevas líneas de registro se mueven dentro de su cuadro de altura fija. El resto de la página nunca se desplaza.
  • Un lienzo fijo adicional y un bucle gsap.ticker. El bucle se detiene cuando no hay nada en movimiento.
  • GSAP 3.13+ (gratuito, incluido ScrollTrigger), @gsap/react, Lenis.

Cómo funciona en un párrafo

Un pequeño script en línea en <head> establece html[data-build="on"] antes de la primera pintura, y CSS oculta todos los [data-build-item] no construidos con opacity: 0. La opacidad en lugar de display o visibilidad significa que no hay cambio de diseño y nada se elimina del árbol de accesibilidad. Después de la hidratación, el motor escanea la página en un grafo de tareas por [data-build-section], arma un ScrollTrigger para cada sección, y cuando una entra en la ventana, ejecuta planificar → construir → revisar:

  • Planificar: un planificador dibuja guías y distribuye tareas.
  • Construir: los trabajadores reclaman tareas por distancia, rol y la ruta crítica, y cada tarea reproduce una línea de tiempo de GSAP.
  • Revisar: un revisor delinea la sección, marca una lista de verificación y la sella.

Los elementos y secciones terminados obtienen [data-built]. Si el motor nunca se inicia, el script en la cabecera elimina la bandera después de 3 s para que nada permanezca oculto.

Arquitectura, flujo de datos y cómo extenderlo: references/architecture.md.

Instalación (Next.js App Router)

  1. Dependencias. Probado con Next.js 15.1 + React 18.3 (el ejemplo trabajado) y Next.js 15.5 + React 19.1 + Tailwind 4 (un create-next-app nuevo), TypeScript 5.8–5.9, [email protected]–3.15, @gsap/[email protected], [email protected]:

    npm i gsap@^3.13 @gsap/react@^2.1 lenis@^1.3
    
  2. Copia el motor. Copia assets/swarm/ a src/components/swarm/. Las importaciones dentro de él son relativas, por lo que cualquier carpeta funciona.

  3. Añade el script de bandera al <head> del diseño raíz. Establece un atributo en <html> antes de que React hidrate, así que añade suppressHydrationWarning a <html>:

    // app/layout.tsx
    import { BuildFlagScript } from "@/components/swarm/build-flag-script";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en" suppressHydrationWarning>
          <head>
            <BuildFlagScript />
          </head>
          <body>{children}</body>
        </html>
      );
    }
    
  4. Monta el motor una vez, en las páginas que lo usen, después del contenido:

    // app/page.tsx (un componente de servidor; permanece estático)
    import "@/components/swarm/swarm.css";
    import { SwarmRoot } from "@/components/swarm/swarm-root";
    
    export default function Page() {
      return (
        <>
          <main>{/* secciones marcadas con data-build-* */}</main>
          <SwarmRoot />
        </>
      );
    }
    

    SwarmRoot también monta el desplazamiento suave de Lenis impulsado por gsap.ticker. Si la aplicación ya ejecuta Lenis, elimina <SmoothScroll /> de swarm-root.tsx. build-layer.tsx lee la instancia a través de useLenis(), que necesita una instancia raíz de ReactLenis en alguna parte de la página (<ReactLenis root />, como monta smooth-scroll.tsx). No tiene que ser un padre.

  5. Mantén la ruta estática. El motor solo decora HTML que ya existe. No llames a cookies(), headers() o auth() en el árbol de diseño de la página solo para leer una sesión. Obtén eso en el cliente en su lugar, y la página se puede prerenderizar y servir desde el CDN. En páginas que no monten SwarmRoot, el script de bandera es inofensivo: ningún elemento lleva data-build-item, y el mecanismo de seguridad elimina la bandera.

  6. Personaliza el tema. Establece variables CSS (--swarm-font-mono, --swarm-accent, --swarm-text, --swarm-muted, --swarm-dim, --swarm-line, --swarm-neutral-text|fill|stroke), luego edita COLORS y ROLE_COLOR en palette.ts. El lienzo no puede leer variables CSS de manera eficiente en cada fotograma, por lo que su paleta reside en TS.

Marca la página

Todo se controla mediante atributos de datos en el marcado renderizado en el servidor. La referencia completa, con valores predeterminados y comportamiento en teléfonos, está en references/dom-contract.md. Recetas de copiar y pegar para un héroe, cuadrícula de tarjetas, lista, imagen o video, tira de métricas y final están en references/recipes.md.

AtributoEnSignificado
data-build-section="nombre"envoltorio de secciónUna unidad de construcción con su propio plan, equipo y revisión
data-build-start="load"secciónConstruir justo después del inicio en lugar de al hacer scroll, con el presupuesto de héroe más grande
data-build-item + data-build="frame|type|reveal|image|dup"elementoUna tarea de ese tipo
data-build-listcontenedor de listaSu primer elemento es el maestro (frame); los hermanos dup se copian de él
data-build-gridcontenedor o la secciónUn agente de maquetación dibuja la cuadrícula y encuadra cada celda primero
data-build-font / -weight / -scaleelementos typeBorrador mono y luego cambio de fuente; barrido de peso; empezar pequeño, luego redimensionar y mover
data-build-color="text|fill|stroke"cualquier elementoLa rueda de colores elige el color real después de que aparece el elemento
data-build-tuneframeAbre el inspector de radio y relleno
data-build-from="left|right|top|bottom"imageBorde desde el que se arrastra la imagen
data-build-keep / ="mobile"cualquier elementoNunca oculto (solo en teléfonos con mobile), aún anotado
data-build-label, -order, -rolecualquier elementoTexto de etiqueta, sugerencia de orden, qué rol lo toma
data-swarm-spawn="role"<g> SVG en el héroeNodos de malla de los que el equipo sale al inicio (≥1024 px)
data-swarm-logcontenedorLas líneas de respaldo renderizadas en el servidor se intercambian por un registro en vivo
data-build-finale + data-deploy-target + data-deploy-textúltima sección, su CTAEl desplegador "lanza" la página después de la última revisión
?build=off / ?build=forceURLDeshabilitar para esta vista / repetir incluso si esta pestaña ya lo vio

El motor establece: html[data-build="on"] mientras está activo, [data-built] en elementos y secciones terminados, [data-lit] en nodos de malla durante el inicio, y [data-deployed] en el objetivo del final.

Reglas que mantienen su funcionamiento:

  • Tamaño de la sección. Aproximadamente una ventana de elementos por sección. Divide páginas largas: una sección por trabajo en una línea de tiempo, una por tarjeta en una cuadrícula grande, una sección separada para un encabezado de sección. Las secciones pequeñas terminan dentro de su presupuesto y se ven deliberadas; las enormes recurren a la velocidad de recuperación.
  • Anidamiento. Un elemento anidado dentro de otro elemento espera a su padre. Marca la tarjeta como frame y su título, texto y chips dentro de ella.
  • Tipo. Usa type solo para texto corto (menos de aproximadamente 120 caracteres). El texto más largo recurre automáticamente a reveal. Escribir lee los cuadros de glifos una vez, por lo que el texto gestionado por React que cambia después de la hidratación (contadores, vistas ) debe ser un frame, no type.
  • LCP. Mantén el encabezado principal visible en teléfonos con data-build-keep="mobile". En teléfonos, el encabezado del héroe es el Largest Contentful Paint, y ocultarlo cuesta Core Web Vitals. El escritorio aún obtiene la construcción completa de cambio de fuente.
  • Registro en vivo. El contenedor data-swarm-log debe renderizarse en el servidor con líneas de respaldo reales y una altura fija (.swarm-log). El motor las intercambia por líneas en vivo y las devuelve al omitir o desmontar.

Héroe mínimo:

<section data-build-section="hero" data-build-start="load">
  <p data-build-item data-build="type">tu línea de cejas</p>
  <h1
    data-build-item data-build="type" data-build-font data-build-weight
    data-build-keep="mobile" data-build-order="-1" data-build-role="coder"
  >
    Tu Nombre <span className="serif">(Acento)</span>
  </h1>
  <div data-build-item data-build="image" data-build-from="right" className="avatar">
    <img src="/me.jpg" alt="Retrato" />
  </div>
  {/* next/image también funciona (renderiza un <img> interno que el motor lee). */}
  <div data-build-list className="buttons">
    <a data-build-item data-build="frame" href="#">GitHub</a>
    <a data-build-item data-build="dup" href="#">LinkedIn</a>
    <a data-build-item data-build="dup" data-build-color="fill" href="#">Currículum</a>
  </div>
</section>

Reglas de suavidad

Estas surgieron al observar construcciones reales. Cada una solucionó un problema visible.

  • Nunca forzar finalización en una fecha límite. Una versión temprana saltaba cada línea de tiempo en ejecución a 5× de velocidad y colocaba los elementos no reclamados en la fecha límite de cada sección. La gente lo veía como un tirón al final de cada sección. Los presupuestos ahora son objetivos de planificación.
    • estimate() escala las duraciones de las tareas para que el plan encaje.
    • Cuando una sección se retrasa, pace() aumenta un factor de recuperación suavemente, hasta un máximo de 1.7× durante 0.7 s.
    • Cada interpolación permanece al menos 120 ms incluso a máxima recuperación, y los pasos de escritura permanecen al menos 16 ms. Nada aparece de repente.
    • Si lo necesitas más rápido, haz los cursores más rápidos (speed en palette.ts). No acortes las animaciones.
  • Animar solo opacidad, clip-path, filter y transform. Sin escrituras de diseño. Los borradores de cambio de fuente son clones en una capa fuera del flujo, y la tarea de escala elimina su transformación en línea al final.
  • Un reloj. La simulación y el dibujo del lienzo se ejecutan en un único callback de gsap.ticker. El estado de React solo cambia cuando la construcción comienza o se detiene, nunca por fotograma. El ticker se detiene cuando no hay agente en pantalla.
  • Medir con antelación. Las secciones hasta aproximadamente dos ventanas debajo del pliegue se miden en fragmentos de requestIdleCallback. Una sección que comienza nunca causa una gran ráfaga de lectura de diseño en el fotograma de desplazamiento.
  • Respetar al lector.
    • Enfocar cualquier cosa dentro de una sección no construida finaliza esa sección de inmediato, para que los usuarios de teclado nunca esperen.
    • Un cambio de tamaño durante la construcción finaliza las secciones en ejecución, porque sus rectángulos medidos están desactualizados.
    • Una pestaña oculta pausa todo.
    • El HUD siempre ofrece Omitir, Repetir e Inspeccionar.
  • Los teléfonos obtienen menos. Cada perfil tiene su propio presupuesto, equipo y detalle (detectProfile() en palette.ts).
    • Teléfonos: 8 agentes, aproximadamente 2.2 s por sección, un cambio de fuente por página, una selección de color por sección, un barrido de duplicados por lista, sin paneles de inspección, sin tarea de escala, sin desplegador.
    • Tabletas: 11 agentes.
    • Escritorio: 15 agentes, aproximadamente 3.9 s por sección, 5.25 s para el héroe.

Perillas para "más rápido", "más tranquilo" y "más agentes": references/tuning.md.

Movimiento reducido

El motor se reproduce para cada visitante, incluidas las personas cuyo sistema operativo solicita movimiento reducido. El propietario del sitio decidió que la construcción es el punto de la página, y el HUD siempre ofrece Omitir, por lo que esta es una elección deliberada, no un valor predeterminado para copiar ciegamente. Para respetar la configuración en su lugar, realiza estos tres cambios:

// build-flag-script.tsx: dentro de CODE, antes de d.setAttribute('data-build','on')
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;

// motion.ts
export function offByUrl(): boolean {
  return OFF_RE.test(window.location.search) ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

// smooth-scroll.tsx
respectReducedMotion: true,

Prueba ambas formas: algunos navegadores (y algunas configuraciones de automatización) informan reduce incluso cuando nadie lo eligió, que es una de las razones por las que el valor predeterminado aquí es "activado".

Trabajar con otros efectos

  • Puerta en [data-built]. El motor lee rectángulos de elementos y alterna opacidad y clip-path durante la construcción. Tilt, glitch, scramble, paralaje en contenido, o cualquier otra cosa que transforme un elemento corromperá esas mediciones si se ejecuta temprano. Inicia tales efectos solo cuando el elemento y su sección llevan [data-built], o cuando html[data-build] no es "on". Un ayudante listo:

    export function isBuilt(el: Element): boolean {
      if (document.documentElement.getAttribute("data-build") !== "on") return true;
      const item = el.closest("[data-build-item]");
      if (item && !item.hasAttribute("data-built")) return false;
      const section = el.closest("[data-build-section]");
      return !section || section.hasAttribute("data-built");
    }
    
  • Videos. Un elemento image que envuelve un <video poster> se arrastra usando su fotograma de póster como miniatura. Dale al envoltorio una data-build-label como clip.mp4, porque no hay <img> para leer un nombre de archivo.

  • Paralaje decorativo está bien en cualquier lugar donde mueva solo decoración. Nunca lo pongas en algo que el motor mida.

Hazlo tuyo

El motor aquí es una implementación original, y el mismo estándar se aplica a cualquier cosa que construyas a partir de él. Si un sitio te inspiró, escribe qué hace reconocible a ese sitio y aléjate exactamente de esas cosas. Tomar prestado …

… [truncado para traducción]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills relacionados