Por qué animate devuelve un objeto Animation
La decisión de diseño que separa a WAAPI de todo lo anterior: una animación es una entidad con identidad y estado, no un efecto secundario que se dispara y se olvida.
setTimeout devuelve un número opaco. jQuery.animate devolvía la colección para poder encadenar, y el control real vivía en una cola global indexada por elemento. Una animación CSS no devuelve nada porque no es una llamada. WAAPI rompe con las tres: devuelve un objeto que es la animación, con identidad, estado consultable y una superficie de control completa. Esa decisión es lo que convierte la animación en algo que se puede programar en vez de algo que se dispara.
- Argumentar por qué la API expone un objeto y no un identificador opaco.
- Describir el grafo de objetos que cuelga de una
Animation. - Recuperar animaciones que no guardaste con
getAnimations(). - Explicar por qué las animaciones CSS son también objetos
Animation.
El problema del disparo y olvido
Antes de WAAPI, todas las formas de animar en la web compartían una limitación: una vez lanzada la animación, no tenías un asa sobre ella. Con CSS podías cambiar propiedades del elemento y esperar que el motor hiciera lo correcto, pero no había forma de preguntar “¿por dónde va?” ni de decir “ve al 40% y quédate ahí”. Con temporizadores tenías un identificador que solo servía para cancelar. Con librerías tenías una cola global que la propia librería gestionaba y a la que accedías por el elemento, no por la animación.
La consecuencia práctica de no tener asa es que componer se vuelve imposible. No puedes escribir una función que reciba una animación cualquiera y la reproduzca al revés, ni un depurador que ralentice todo lo que se esté moviendo, ni un control deslizante que recorra una animación de otro. Todo eso requiere que la animación sea un valor que se pueda pasar, y para eso tiene que ser un objeto.
const anim = el.animate(kf, 800);
// Cosas que solo son posibles porque hay un objeto
anim.pause();
anim.currentTime = 400;
anim.playbackRate = -1;
anim.play();
await anim.finished;
Ninguna de esas cinco líneas tiene equivalente en CSS. Y no es por falta de sintaxis: es que el modelo declarativo no tiene dónde ponerlas, porque en CSS la animación no es una entidad a la que puedas referirte.
El grafo de objetos
Una Animation no es una caja opaca. Cuelga de ella un grafo pequeño y bien definido:
const anim = el.animate({ opacity: [0, 1] }, { duration: 600, id: 'entrada' });
anim.effect; // el KeyframeEffect
anim.effect.target; // el elemento
anim.effect.pseudoElement; // null, o '::before'
anim.effect.getKeyframes(); // los keyframes normalizados y con offsets resueltos
anim.effect.getComputedTiming();
anim.timeline; // la DocumentTimeline
anim.id; // 'entrada'
anim.playState; // 'running'
anim.effect.getKeyframes() merece atención: no devuelve lo que escribiste, devuelve la forma canónica con los offsets calculados, el easing resuelto y el composite explícito en cada keyframe. Es la mejor herramienta de depuración de la API, porque te enseña exactamente qué entendió el motor de tus dos sintaxis.
el.animate({ opacity: [0, 1], transform: ['none', 'scale(2)', 'scale(1)'] }, 600)
.effect.getKeyframes();
// [ {offset: 0, opacity: '0', transform: 'none', computedOffset: 0, easing: 'linear', composite: 'auto'},
// {offset: null, transform: 'scale(2)', computedOffset: 0.5, easing: 'linear', composite: 'auto'},
// {offset: 1, opacity: '1', transform: 'scale(1)', computedOffset: 1, easing: 'linear', composite: 'auto'} ]
Ahí se ve de un vistazo lo que explicamos en las dos sintaxis: opacity solo tiene dos paradas y transform tres, y el keyframe intermedio no lleva opacidad ninguna.
Los tres niveles del grafo tienen responsabilidades separadas y eso es lo que permite recombinarlos: un mismo KeyframeEffect puede reasignarse a otro elemento, y una misma Animation puede cambiar de línea de tiempo. Ninguna de esas dos cosas tendría sentido si animate() devolviera un identificador.
Recuperar lo que no guardaste
No hace falta guardar la referencia. El documento sabe qué animaciones tiene vivas y te las devuelve:
el.getAnimations(); // las del elemento
el.getAnimations({ subtree: true }); // y las de sus descendientes
document.getAnimations(); // todas las del documento
Esto cierra el modelo. Aunque una animación la haya creado otro módulo, o una librería, o una regla CSS que no controlas, sigue siendo un objeto Animation accesible. La API no tiene puntos ciegos: todo lo que se mueve en el documento está en esa lista.
// Cancelar solo la animacion de entrada, sea quien sea quien la creo
for (const a of el.getAnimations()) {
if (a.id === 'entrada') a.cancel();
}
Es también la forma correcta de comprobar si algo está animándose antes de tocarlo. Un if (el.getAnimations().length) responde a una pregunta que con clases y temporizadores solo se puede responder llevando la contabilidad a mano.
Las animaciones CSS y las transiciones CSS son objetos Animation, y esa es la parte del diseño que más gente desconoce. Concretamente son instancias de CSSAnimation y CSSTransition, dos subclases que añaden un campo de identificación —animationName en una, transitionProperty en la otra— y por lo demás tienen la superficie completa: pause(), currentTime, playbackRate, reverse(), finished. Eso significa que puedes pausar una animación declarativa, invertirla, o llevarla a un instante concreto sin tocar el CSS que la define. Y significa que la especificación de animaciones CSS está redefinida encima del modelo de WAAPI: animation-play-state: paused no es un mecanismo aparte, es la forma declarativa de llamar a pause() sobre el objeto correspondiente. Hay un matiz que muerde: cuando modificas una CSSAnimation desde JavaScript, algunas de sus propiedades quedan “desligadas” de la cascada y dejan de responder a cambios en el CSS; cambiar currentTime no rompe el vínculo, pero cambiar playbackRate o startTime sí desacopla la reproducción de animation-play-state. Si necesitas control total, cancela la animación CSS y crea la tuya.
Lo que viene después
El objeto Animation que acabas de recibir tiene una superficie que hasta ahora hemos usado de refilón. Los métodos de reproducción, las dos propiedades de tiempo, la velocidad y las dos promesas forman un sistema con reglas propias, y varias de ellas —qué pasa exactamente al llamar a reverse() sobre una animación terminada, qué diferencia hay entre pausar y poner la velocidad a cero— no son deducibles de los nombres.
Lo que sí puedes dar por establecido desde aquí: animate() no es un atajo para escribir CSS con otra sintaxis. Es la forma de obtener una referencia a algo que se mueve. Todo lo demás cuelga de tener esa referencia.
Aplica una animación CSS infinita a un elemento y desde la consola ejecuta const a = el.getAnimations()[0]. Comprueba a.constructor.name y a.animationName. Después haz a.playbackRate = 0.2 y observa que sigue corriendo, a cámara lenta, sin que el CSS haya cambiado. Por último añade una clase que ponga animation-play-state: paused y comprueba si la animación se detiene: descubrirás en qué momento exacto se desacopló del CSS.