Flip.getState y Flip.from: el ciclo completo
Qué captura exactamente el estado, cómo se le añaden propiedades, y el catálogo de opciones de Flip.from con las cuatro que de verdad usarás siempre.
Todo el plugin cabe en dos llamadas y una línea de código propio entre ellas. Esa simplicidad aparente esconde decisiones que hay que tomar: qué elementos entran en la captura, qué propiedades se recuerdan además de la geometría, si el cambio de tamaño se hará escalando o redimensionando, y si los elementos deben salir del flujo mientras dura el movimiento. Cada una de esas decisiones tiene un valor por defecto razonable y un caso en el que el valor por defecto es exactamente lo que no quieres.
- Capturar un estado con
Flip.getState()y saber qué guarda por defecto. - Añadir propiedades no geométricas a la captura con
props. - Elegir entre
scaley el redimensionado por defecto según el contenido. - Reconocer cuándo hace falta
absolutey qué efecto secundario tiene.
Los tres pasos
import { gsap } from "gsap";
import { Flip } from "gsap/Flip";
gsap.registerPlugin(Flip);
// 1) Capturar
const estado = Flip.getState(".tarjeta");
// 2) Cambiar lo que sea: clases, orden, contenedor, estilos
document.querySelector(".panel").classList.toggle("expandido");
// 3) Animar desde el estado capturado hasta el actual
Flip.from(estado, { duration: 0.5, ease: "power2.inOut" });
Flip.getState() acepta texto de selector, un elemento, un array o un NodeList. Por defecto captura posición en el viewport, tamaño, rotación, sesgado y opacidad de cada objetivo. No captura color, ni fondo, ni radio de borde, ni nada más.
Hay un matiz importante: si en el momento de capturar hay un FLIP activo sobre alguno de esos elementos, getState() lo fuerza al final antes de medir, para no capturar una posición intermedia. Es lo que permite encadenar transiciones rápidas sin que se acumulen errores.
El paso intermedio no tiene que pasar por el plugin. Puedes hacer lo que quieras: mover nodos, alternar clases, cambiar estilos en línea, reordenar. La única condición es que el cambio esté aplicado cuando llames a Flip.from(), lo cual en frameworks que renderizan de forma diferida requiere esperar al siguiente ciclo.
Flip.from() devuelve una timeline, así que puedes controlarla, pausarla, invertirla o añadirle otras animaciones con add().
const tl = Flip.from(estado, { duration: 0.5 });
tl.from(".detalle", { autoAlpha: 0, y: 20 }, "-=0.2");
props: más allá de la geometría
Por defecto, un FLIP no anima el color de fondo aunque cambie: el elemento salta a su color nuevo mientras se desplaza suavemente. Para incluir propiedades adicionales hay que declararlas en la captura, porque si no se guardó el valor anterior no hay nada desde donde animar.
const estado = Flip.getState(".tarjeta", {
props: "backgroundColor,color,borderRadius",
});
tarjeta.classList.toggle("destacada");
Flip.from(estado, { duration: 0.5 });
La lista va separada por comas y en formato camelCase, no con guiones. Flip.from() acepta también una opción props, pero su función es distinta: sirve para limitar la animación a un subconjunto de las capturadas, no para añadir. Si no la pones, se animan todas las que estén en el estado.
getState() acepta además simple: true, que le dice al plugin que no hay contenedores rotados, escalados ni sesgados en el camino y que puede saltarse los cálculos de compensación. Es una promesa que haces tú; si mientes, las posiciones salen mal. En la mayoría de las páginas la diferencia de rendimiento es imperceptible, y solo compensa activarlo con muchos elementos.
scale: la decisión que cambia el resultado
Por defecto, Flip anima width y height para cambiar el tamaño. Con scale: true, usa scaleX y scaleY.
Las dos opciones tienen ventajas opuestas y elegir mal se nota mucho.
Redimensionar —el valor por defecto— reflowea el contenido en cada frame: el texto se recompone, las imágenes se recortan, los hijos se reposicionan. El resultado es correcto en todo momento y cuesta layout en cada frame.
Escalar deforma: el contenido se estira o se comprime junto con la caja. Es mucho más barato porque lo resuelve el compositor, pero un texto escalado en horizontal a 1,8 durante medio segundo se ve mal.
// Tarjeta con texto: redimensionar, aunque cueste mas
Flip.from(estado, { duration: 0.6 });
// Miniatura de imagen que crece a pantalla completa: escalar
Flip.from(estado, { duration: 0.6, scale: true });
La regla práctica: escala cuando el contenido sea una imagen o un color plano, y redimensiona cuando haya texto. Si tienes texto y necesitas escalar por rendimiento, el truco habitual es escalar el contenedor y aplicar la escala inversa al texto para compensar, aunque eso ya es un montaje a medida.
La documentación recomienda explícitamente box-sizing: border-box en los elementos que vayan a participar en un FLIP. Con el modelo de caja por defecto, el width que Flip mide y anima no incluye relleno ni borde, y cualquier diferencia de relleno entre el estado inicial y el final produce un salto al final de la animación.
absolute: cuando el layout se colapsa
Este es el ajuste que más veces salva una transición y el que más gente descubre por accidente.
El problema aparece con contenedores flex y grid. Mientras un elemento se desplaza de una posición a otra, sus hermanos siguen ocupando el espacio del layout nuevo, así que el contenedor ya está reorganizado desde el frame uno. Eso suele estar bien, pero produce artefactos cuando el elemento que se mueve cambiaba el tamaño del contenedor, o cuando varios elementos se cruzan.
absolute: true pone todos los objetivos en position: absolute durante la animación, sacándolos del flujo. Así se mueven libremente sin afectar ni ser afectados por el layout de los demás.
El efecto secundario es inmediato: al salir del flujo, dejan de ocupar espacio, y si eran los que daban altura a su contenedor, ese contenedor se colapsa. Para eso absolute acepta también un selector o un array, de modo que puedes sacar del flujo solo un subconjunto y dejar dentro al que sostiene el layout.
// Todos fuera del flujo
Flip.from(estado, { absolute: true, duration: 0.5 });
// Solo las tarjetas; el contenedor se queda en el flujo sosteniendo la altura
Flip.from(estado, { absolute: ".tarjeta", duration: 0.5 });
Un aviso de la documentación que conviene tener presente: con absolute: true, las coordenadas se calculan respecto al viewport actual, así que si el usuario hace scroll o redimensiona durante la animación, la posición puede desviarse. Al acabar, todo queda donde debe.
El resto de opciones, por orden de utilidad
Flip.from() acepta además cualquier propiedad de tween normal: duration, ease, stagger, onComplete, repeat. Y estas propias:
| Opción | Para qué |
|---|---|
nested: true |
Si hay padres e hijos en los objetivos, evita que los desplazamientos se acumulen |
prune: true |
Descarta los objetivos que no se han movido, para ahorrar y para que el stagger no cuente huecos |
toggleClass |
Añade una clase mientras dura la animación, y la retira al acabar |
zIndex |
Fija un zIndex durante la animación para que los objetivos queden por encima |
spin |
Añade vueltas completas. Acepta booleano, número o función por objetivo |
targets |
Anima un subconjunto del estado, o unos elementos nuevos que se correlacionan por identificador |
prune merece un comentario porque su efecto sobre el stagger es el que lo hace útil de verdad. Al filtrar una lista de veinte tarjetas y quedarte con seis, las catorce que no se mueven seguirían contando para el escalonado, produciendo huecos temporales sin nada visible. Con prune: true desaparecen del cálculo y el escalonado se aplica solo a las que de verdad se mueven.
Flip.from(estado, {
duration: 0.5,
ease: "power2.inOut",
absolute: ".tarjeta",
prune: true,
stagger: 0.04,
toggleClass: "en-movimiento",
});
La confusión más costosa con este plugin no es sintáctica, es conceptual, y sale a la luz en cuanto lo metes en un framework. El objeto que devuelve getState() parece una referencia a unos elementos, y en realidad es un registro con dos partes: los datos geométricos de un instante, y las identidades de los elementos a los que corresponden esos datos. Mientras la fase Last se limite a mover o restilar los mismos nodos, ambas partes siguen siendo válidas y todo funciona sin pensar. Pero en cuanto la fase Last consiste en un rerenderizado —React, Vue, Solid— los nodos viejos se han eliminado del DOM y hay nodos nuevos con el mismo aspecto y distinta identidad. El estado sigue teniendo la geometría correcta y ya no tiene a quién aplicársela, así que Flip no anima nada, y no lanza ningún error porque desde su punto de vista no ha pasado nada anómalo. La solución oficial es exactamente la que se deduce del párrafo anterior y no es un parche: pasar targets para señalar a los nodos nuevos, y data-flip-id para que Flip pueda casar cada nodo nuevo con la geometría del viejo. Es decir, reponer a mano la parte de identidad que el rerenderizado destruyó. La lección va más allá de Flip. Cada vez que una biblioteca te da un objeto que “captura” algo del DOM, pregúntate qué parte de esa captura son datos y qué parte son referencias, y qué operaciones de tu aplicación invalidan cada parte. Los datos sobreviven a casi todo; las referencias no sobreviven a un rerenderizado, a un innerHTML, a una navegación ni a un replaceChildren. Casi todos los bugs de integración de bibliotecas de animación con frameworks reactivos son exactamente esta confusión, con distintos nombres.
- Monta un FLIP básico que alterne una clase de expansión y comprueba que la timeline devuelta se puede invertir con
reverse(). - Añade un cambio de color de fondo y observa que salta. Captúralo con
propsy compáralo. - Prueba la misma transición con
scale: truey sin él sobre una tarjeta con texto. Decide cuál usarías. - Provoca el colapso del contenedor con
absolute: truey arréglalo restringiendo el selector. - Filtra una lista de veinte elementos a seis con
staggery sinprune, y luego conprune: true. Describe la diferencia en el escalonado.