wandres.dev
ONTOLOGÍA · El mapa de la animación web

El árbol de decisión: qué familia usar

Un procedimiento de cuatro preguntas que lleva de un requisito de animación a la familia correcta, con las razones de cada bifurcación.

⏱ 16 min

Elegir herramienta de animación por costumbre produce dos patologías simétricas: proyectos que arrastran una librería de sesenta kilobytes para hacer aparecer un menú, y proyectos que reimplementan una línea de tiempo entera a base de setTimeout porque “CSS es suficiente”. El árbol que sigue no es una opinión: es la formalización de las preguntas que ya te hace el problema, en el orden en que las hace. Cuatro preguntas y estás dentro de una familia.

🎯 Al terminar esta lección sabrás
  • Aplicar el árbol de decisión a un requisito concreto y llegar a una familia justificada.
  • Explicar por qué la primera pregunta es sobre el disparador y no sobre el rendimiento.
  • Reconocer las tres bifurcaciones que obligan a subir de familia.
  • Detectar cuándo una decisión ya tomada estaba en la rama equivocada.

El árbol

flowchart TB
ini[Pregunta 1 que dispara la animacion]
ini -->|Un cambio de estado| p2{Pregunta 2 hace falta secuenciar o medir}
ini -->|La posicion del scroll| scr[Familia 3 lineas de tiempo de scroll]
ini -->|Un gesto continuo del usuario| p4{Pregunta 4 debe conservar velocidad al interrumpir}
p2 -->|No| css[Familia 1 CSS declarativo]
p2 -->|Si| p3{Pregunta 3 mas de diez pasos con solapes}
p3 -->|No| waapi[Familia 2 WAAPI]
p3 -->|Si| lib[Familia 4 libreria]
scr --> ffx{Firefox debe ver lo mismo}
ffx -->|No es critico| scr2[scroll y view con deteccion supports]
ffx -->|Si es critico| lib
p4 -->|No| waapi
p4 -->|Si| lib
style ini fill:#cba6f7,color:#11111b
style css fill:#a6e3a1,color:#11111b
style waapi fill:#89b4fa,color:#11111b
style scr fill:#94e2d5,color:#11111b
style scr2 fill:#94e2d5,color:#11111b
style lib fill:#fab387,color:#11111b
style p2 fill:#f9e2af,color:#11111b
style p3 fill:#f9e2af,color:#11111b
style p4 fill:#f9e2af,color:#11111b
style ffx fill:#f9e2af,color:#11111b

Por qué la primera pregunta es el disparador

La primera bifurcación no pregunta qué se mueve ni cuánto pesa la solución. Pregunta qué evento externo hace que la animación exista, y es la pregunta correcta porque determina qué tiene que saber tu código y qué no.

Si el disparador es un cambio de estado —una clase que aparece, un atributo que cambia, un pseudo-selector que empieza a coincidir— entonces el estado ya está representado en algún sitio y la animación puede derivarse de él. No necesitas guardar nada. Esta es la rama mayoritaria en interfaces de aplicación: menús, paneles, validaciones, cambios de selección, estados de carga.

Si el disparador es la posición del scroll, hay una asimetría fundamental: el scroll puede avanzar sin que tu JavaScript se ejecute. El desplazamiento lo puede procesar el compositor mientras el hilo principal está ocupado, y por tanto cualquier solución que necesite ejecutar código en cada píxel de scroll está estructuralmente condenada a desincronizarse. La familia 3 existe precisamente para eliminar esa ejecución.

Si el disparador es un gesto continuo —arrastrar, deslizar, un puntero que empuja algo— aparece un requisito que ninguna función de suavizado puede cumplir: la continuidad de velocidad. Volveremos a ello en la cuarta pregunta.

Fíjate en que “cuando cargue la página” y “cuando aparezca este elemento” son variantes de cambio de estado, no categorías nuevas. Y “cada fotograma, siempre” —un fondo animado permanente, un simulador— no está en el árbol: eso no es animación de interfaz, es un bucle de render, y tiene otras reglas.

Las tres bifurcaciones que hacen subir de familia

Secuenciar o medir. CSS declarativo no sabe hacer ninguna de las dos. No sabe secuenciar porque no tiene forma de expresar “después de que termine aquello”; lo más cercano es un transition-delay calculado a mano, que es una duplicación de información que se rompe en cuanto tocas una duración. Y no sabe medir porque no tiene acceso a la geometría resuelta: no puedes decir “desplázate tu propia altura hacia arriba” si esa altura la decide el contenido.

Hay una excepción parcial que conviene conocer, porque evita muchos saltos innecesarios de familia: las custom properties permiten pasar un número medido desde JavaScript a CSS una sola vez, y dejar que CSS haga el resto. Si lo único que necesitas medir es un valor que no cambia durante la animación, no hace falta abandonar la familia 1.

// Medir una vez, animar en CSS. Sigue siendo familia 1.
const alto = panel.getBoundingClientRect().height;
panel.style.setProperty('--alto-panel', `${alto}px`);
.panel { transition: translate 260ms ease-out; }
.panel[data-cerrado] { translate: 0 calc(var(--alto-panel) * -1); }

Más de diez pasos con solapes. El número exacto da igual; lo que importa es el punto en el que encadenar promesas deja de ser legible y empieza a ser un grafo implícito. La señal fiable no es la cantidad de pasos, es la necesidad de reproducir el conjunto hacia atrás o de saltar a un punto arbitrario del conjunto. WAAPI da control temporal sobre una animación, no sobre una secuencia de animaciones: no existe una currentTime que abarque a diez animaciones encadenadas. Reconstruir eso a mano es, literalmente, escribir una librería de líneas de tiempo.

Conservar la velocidad al interrumpir. Una función de suavizado es una tabla que va de 0 a 1 en un tiempo fijo. Si la interrumpes a mitad y arrancas otra, la nueva empieza con la velocidad que dicte su curva en el instante cero, que casi siempre es cero. En una animación disparada por estado eso no se percibe. En un gesto continuo se percibe como un tirón: el elemento venía moviéndose a cierta velocidad y de repente se para para volver a acelerar. La solución no es una curva mejor, es un integrador que arrastre el estado físico entre animaciones, y eso lo dan las librerías de muelles.

El árbol tiene una rama que no está dibujada: no animar

Antes de la primera pregunta hay una pregunta cero que el diagrama no muestra porque no reparte territorio, lo recorta: ¿la animación aporta información o solo decora? Una animación que informa —de dónde viene un panel, qué elemento sustituyó a cuál, que un cambio ocurrió aquí y no allí— justifica cualquier complejidad que necesite, porque el usuario obtiene algo a cambio. Una animación que solo decora tiene que ser barata en todos los sentidos: barata en bytes, barata en fotogramas y barata de eliminar. La prueba operativa es brutal y funciona: quítala y mira si alguien echa de menos la información. Si nadie la echa de menos, entonces cualquier decisión que suba de familia por ella está mal tomada por definición, porque estás pagando estado, peso y superficie de fallo por algo que no comunica nada. Esto tiene una consecuencia que cambia el orden en que se escribe el código: la rama de prefers-reduced-motion no es un añadido de accesibilidad que se pega al final, es la versión sin decoración de tu interfaz, y si esa versión no es coherente y utilizable es que la animación estaba tapando un problema de diseño. Escríbela primero. Descubrirás que la mitad de las animaciones que ibas a hacer no eran necesarias, y que la otra mitad merecía más cuidado del que le ibas a dar.

Los cruces que el árbol permite

El árbol asigna una familia principal, no una exclusiva. Hay tres combinaciones legítimas y frecuentes, y conviene reconocerlas para no sentir que estás haciendo trampa.

CSS más WAAPI para el control. Declaras la animación en CSS, la recuperas con getAnimations() y la pausas o la inviertes desde JavaScript. Sigues teniendo el estado en el estilo; solo has tomado prestado el control temporal. Es lo mejor de las dos familias sin duplicar nada.

WAAPI más líneas de tiempo de scroll. Los constructores ScrollTimeline y ViewTimeline existen y se pasan como opción timeline a animate(). Sirven cuando los fotogramas dependen de una medición, pero el tiempo sigue viniendo del scroll.

Librería para una parte, nativo para el resto. Es el patrón más sano con librerías: usar la librería solo donde aporta —una secuencia compleja, un morfeo de SVG, un arrastre con inercia— y dejar el noventa por ciento de las microinteracciones en CSS. Cargar una librería no obliga a canalizar todo por ella.

Lo que no es legítimo es el cruce que duplica estado: una clase CSS con transición y una animación WAAPI sobre la misma propiedad del mismo elemento. Ahí acabas con dos verdades sobre el mismo valor y el resultado depende del orden de composición.

⚔️ Recorre el árbol tres veces
  1. Requisito A: un menú desplegable que aparece al pulsar un botón, con desvanecido y desplazamiento. Recorre el árbol y anota en qué nodo terminas.
  2. Requisito B: una sección horizontal que avanza mientras el usuario hace scroll vertical, con una barra de progreso sincronizada. Recorre el árbol y decide qué haces en Firefox.
  3. Requisito C: una tarjeta arrastrable que al soltarla vuelve a su sitio con rebote y que se puede volver a agarrar a mitad del rebote. Recorre el árbol y justifica en una frase por qué la última condición decide la familia.