wandres.dev
WAAPI III · Composición y efectos

replace, add y accumulate

Los tres modos de composición, la diferencia exacta entre sumar y acumular una lista de transformaciones, y dónde se declara cada uno.

⏱ 19 min

composite tiene tres valores y dos de ellos parecen sinónimos. add y accumulate producen el mismo resultado para la mayoría de los tipos, y precisamente por eso la diferencia se aprende mal: solo se manifiesta en listas de transformación, listas de sombras y listas de filtros, que resulta que son justo los valores que más se animan. Entender la diferencia es entender que sumar dos escalas no es lo mismo que combinarlas.

🎯 Al terminar esta lección sabrás
  • Definir el valor subyacente y cómo lo transforma cada modo.
  • Calcular a mano el resultado de sumar y de acumular dos listas de transformación.
  • Declarar composite por efecto, por keyframe y desde CSS.
  • Elegir el modo correcto según el tipo de valor que se anima.

Los tres modos

Los tres describen qué hace un keyframe con el valor que le llega de abajo: ignorarlo, sumarle el suyo, o combinarlo con el suyo según lo que ese tipo de valor signifique.

replace: el valor por defecto

Con composite: 'replace', cada keyframe define el valor completo y el subyacente se descarta. Es lo que has estado usando todo este tiempo y lo que hace que la mayoría de las animaciones sean fáciles de razonar: lo que escribes en el keyframe es lo que se ve.

El valor inicial real no es 'replace' sino 'auto', que en un keyframe significa “usa el composite del efecto” y en el efecto significa 'replace'. Por eso getKeyframes() devuelve composite: 'auto' en los keyframes que no lo declaran.

add: la operación de suma del tipo

Con 'add', el valor del keyframe se combina con el subyacente usando la operación de adición definida para ese tipo de valor. Para números y longitudes es la suma aritmética:

// opacidad base 0.5, keyframe 0.3 con add  ->  0.8
el.animate({ opacity: [0, 0.3] }, { duration: 400, composite: 'add', fill: 'forwards' });

Para las listas de transformación, la adición está definida como concatenación. No se suman los componentes: se pegan las dos listas una detrás de otra.

subyacente: translateX(10px)
keyframe:   translateX(20px)   con add
resultado:  translateX(10px) translateX(20px)

Visualmente eso son treinta píxeles, porque dos traslaciones consecutivas se acumulan. Pero el valor no es translateX(30px), es una lista de dos funciones. Con traslaciones da igual; con escalas, no:

subyacente: scale(2)
keyframe:   scale(3)   con add
resultado:  scale(2) scale(3)   que es una escala efectiva de 6

Ahí está el problema. Si el elemento estaba al doble de tamaño y una segunda animación quiere ampliarlo un cincuenta por ciento más, escribir scale(1.5) con add produce el triple, no el triple de la mitad. La concatenación de escalas multiplica, y sumar intenciones de escala rara vez es lo que se quiere decir.

accumulate: la operación de acumulación

'accumulate' usa una operación distinta, definida para combinar valores que representan el mismo tipo de magnitud aplicada dos veces. Para números y longitudes coincide con la adición, así que no se distingue. Para listas de transformación, no coincide.

La regla es: si las dos listas tienen el mismo número de componentes y cada par de componentes correspondientes es del mismo tipo, se acumulan componente a componente. Si no, se cae a la concatenación de add.

subyacente: translateX(10px)
keyframe:   translateX(20px)   con accumulate
resultado:  translateX(30px)          -- un solo componente

Y para la escala, la acumulación tiene en cuenta que su elemento neutro es 1, no 0:

subyacente: scale(2)
keyframe:   scale(3)   con accumulate
resultado:  scale(4)                   -- 2 + 3 - 1

Eso es lo que la mayoría de la gente quiere decir al componer escalas: “estaba al doble, súbelo el equivalente a un triple respecto de lo normal, y quédate en cuatro”. La operación resta el neutro para no contarlo dos veces, exactamente igual que sumar dos incrementos porcentuales.

Lo mismo pasa con las rotaciones, donde el neutro es cero grados y por tanto la acumulación coincide con la suma; con box-shadow, donde dos listas de la misma longitud acumulan desplazamiento y radio componente a componente en vez de dibujar el doble de sombras; y con filter, donde dos listas de blur() acumulan el radio en vez de encadenar dos desenfoques.

La tabla corta que hay que recordar:

tipo de valor add accumulate
número, longitud, ángulo suma suma
lista de transformación compatible concatena combina componente a componente
lista de transformación incompatible concatena concatena
scale multiplica el efecto a + b - 1
lista de sombras o filtros compatible concatena combina componente a componente

Dónde se declara

Tres sitios, con precedencia de más específico a más general.

En un keyframe individual, lo que permite que unos tramos compongan y otros no:

el.animate([
  { transform: 'translateY(0)',     composite: 'add' },
  { transform: 'translateY(-20px)', composite: 'add' },
], { duration: 240, fill: 'forwards' });

En las opciones del efecto, como valor por defecto de todos sus keyframes:

el.animate(
  { transform: ['translateY(0)', 'translateY(-20px)'] },
  { duration: 240, fill: 'forwards', composite: 'add' }
);

En CSS, con animation-composition, que acepta los mismos tres valores y también listas separadas por comas para varias animaciones:

.tarjeta {
  animation: flotar 3s ease-in-out infinite;
}
.tarjeta:hover {
  animation: flotar 3s ease-in-out infinite, alzar 240ms forwards;
  animation-composition: replace, add;
}

animation-composition está en los tres motores desde 2023, así que es utilizable sin reservas. También se puede declarar dentro de un bloque de keyframe, junto a animation-timing-function, para cambiar el modo en un tramo concreto.

Nivel dios

La operación de composición se aplica contra el valor subyacente en cada frame, no una vez al empezar. Eso significa que una animación con add sobre una propiedad que también está animada por debajo produce un valor que cambia por dos motivos a la vez, y que si la de abajo se cancela a mitad, la de arriba salta. Pero la consecuencia interesante es otra: composite: 'add' con un solo keyframe convierte una animación en un desplazamiento relativo permanente, algo que no tiene ninguna otra forma de expresarse. Un el.animate({transform: 'translateY(-4px)'}, {duration: 1, fill: 'forwards', composite: 'add'}) deja el elemento cuatro píxeles más arriba de donde esté, sea donde sea, aunque otras animaciones lo muevan después. Es un desfase que viaja con el elemento y que se quita cancelando esa animación concreta, sin tocar ni el estilo en línea ni ninguna clase. Como mecanismo de ajuste puntual —corregir la posición de un icono dentro de un sistema de diseño que no puedes tocar, compensar un desalineamiento que solo ocurre en un navegador— no tiene competencia. Es también la forma más rápida de volver loco a quien depure ese elemento después, así que ponle un id descriptivo.

iterationComposite

Hay un cuarto ajuste, en el efecto y no en los keyframes: iterationComposite. Con el valor 'accumulate', cada iteración parte del resultado de la anterior en vez de reiniciarse.

el.animate(
  { transform: ['rotate(0deg)', 'rotate(90deg)'] },
  { duration: 500, iterations: 4, iterationComposite: 'accumulate' }
);

En vez de girar noventa grados cuatro veces volviendo al origen cada vez, gira noventa, luego ciento ochenta, luego doscientos setenta y termina en trescientos sesenta. Es la forma de expresar un avance acumulativo sin recalcular los keyframes.

El soporte de iterationComposite es más estrecho que el del resto de la API y no tiene equivalente en CSS. Compruébalo en tus objetivos antes de depender de él; para el caso de la rotación acumulativa siempre puedes escribir el ángulo total en el keyframe y ajustar la duración, que funciona en todas partes.

Cómo elegir

La regla corta: usa add para desplazamientos y accumulate para todo lo demás.

Las traslaciones dan el mismo resultado visual con los dos modos, así que add es suficiente y produce una lista más literal cuando la inspeccionas. Las escalas, las rotaciones acumuladas, las sombras y los filtros necesitan accumulate para componer con sentido. Y cuando una animación mezcla varios tipos de función en la misma lista, comprueba el resultado: la compatibilidad componente a componente es más frágil de lo que parece, y basta con que una animación escriba translateY(0) y la otra translate(0, 0) para que la acumulación caiga a concatenación sin avisar.

⚔️ Reto práctico

Aplica a un elemento transform: scale(2) desde el estilo en línea. Anímalo con {transform: ['scale(1)', 'scale(3)']} y fill: 'forwards', probando los tres modos, y mide el ancho resultante con getBoundingClientRect() en cada caso. Deberías obtener el triple con replace, el séxtuple con add y el cuádruple con accumulate. Después inspecciona getComputedStyle(el).transform en los tres y comprueba que la matriz final refleja exactamente esos factores.