El patrón FLIP desde cero
Qué significan las cuatro letras, por qué invertir es más barato que animar el layout, y cómo se implementa a mano antes de usar ninguna biblioteca.
Hay una clase de animación que el navegador no sabe hacer y que la gente intenta forzar durante años antes de descubrir que existe una técnica para ella: animar un elemento entre dos posiciones que no controlas, decididas por el motor de layout. Un elemento que pasa de una celda de un grid a otra, de un contenedor a otro, de una lista ordenada por fecha a la misma lista ordenada por precio. CSS no puede interpolar entre dos layouts porque el layout no es un valor. FLIP resuelve exactamente eso, y lo resuelve dando la vuelta al problema de una forma que, una vez la ves, resulta obvia.
- Explicar qué hace cada una de las cuatro fases del patrón FLIP.
- Justificar por qué invertir con transformaciones es órdenes de magnitud más barato que animar el layout.
- Implementar FLIP a mano con
getBoundingClientRectpara un caso sencillo. - Enumerar los tres problemas de la implementación manual que resuelve el plugin.
Las cuatro letras
El nombre es un acrónimo de las cuatro fases, acuñado por Paul Lewis, y describe literalmente el algoritmo.
First. Mides dónde está el elemento ahora mismo. Posición, tamaño, y si hace falta rotación y otras propiedades.
Last. Aplicas el cambio real y definitivo: mueves el nodo, cambias la clase, reordenas la lista, lo que sea. El navegador recalcula el layout y el elemento salta instantáneamente a su posición final. Durante un frame, el usuario vería el salto, pero no llega a verlo porque la siguiente fase ocurre antes de pintar.
Invert. Mides dónde ha quedado, calculas la diferencia con la medida de la fase First, y aplicas una transformación que cancela exactamente esa diferencia. El elemento está en su posición final según el layout, pero se ve en su posición inicial.
Play. Animas esa transformación hasta cero. A medida que la transformación se reduce, el elemento parece viajar desde donde estaba hasta donde está.
flowchart TB a[First mide el rectangulo inicial] --> b[Last aplica el cambio de layout real] b --> c[Invert aplica un transform que cancela la diferencia] c --> d[Play anima ese transform hasta cero] d --> e[El layout final ya estaba puesto desde el paso Last] style a fill:#89b4fa,color:#11111b style b fill:#f9e2af,color:#11111b style c fill:#cba6f7,color:#11111b style d fill:#a6e3a1,color:#11111b style e fill:#94e2d5,color:#11111b
El nodo final del diagrama es la clave de todo y merece leerse despacio. El layout definitivo se aplica al principio, no al final. Durante toda la animación, el DOM ya está en su estado final; lo único que cambia es una transformación visual. Eso significa que si el usuario interrumpe la animación, si la pestaña se congela, si algo falla a mitad, el estado real ya es el correcto. La animación es puramente decorativa sobre un estado que ya está bien.
Por qué invertir es más barato
Aquí está la razón de que FLIP exista, y no es estética: es de coste.
Animar el layout significa que en cada frame el navegador tiene que recalcular la geometría de todo lo afectado. Cambiar el width de un elemento obliga a recalcular la posición de sus hijos, la de sus hermanos siguientes, la del contenedor si su tamaño depende del contenido, y así hacia arriba. Después hay que repintar todas esas zonas. Sesenta veces por segundo.
Animar una transformación no hace nada de eso. transform y opacity son las dos propiedades que el navegador puede resolver en la fase de composición, sin volver a calcular layout ni repintar. El elemento ya está pintado en una textura; moverla o escalarla es trabajo de la GPU, y el hilo principal ni se entera.
La diferencia no es de un veinte por ciento. Es la diferencia entre una animación que se cae a veinte frames por segundo con veinte elementos y una que aguanta doscientos elementos a sesenta.
Y hay un segundo ahorro menos obvio. En FLIP, el recálculo de layout ocurre exactamente una vez, en la fase Last. Una animación de layout lo provoca en cada frame. Cambiar un coste por frame por un coste único es la misma jugada que hace un compilador cuando saca un cálculo invariante fuera de un bucle.
Hacerlo a mano
Merece la pena escribirlo una vez sin biblioteca, porque entonces entiendes qué automatiza el plugin.
function flip(elemento, cambiarLayout) {
// First: medir el estado actual
const primero = elemento.getBoundingClientRect();
// Last: aplicar el cambio real
cambiarLayout();
// Medir el estado final
const ultimo = elemento.getBoundingClientRect();
// Invert: calcular la diferencia y cancelarla
const dx = primero.left - ultimo.left;
const dy = primero.top - ultimo.top;
const sx = primero.width / ultimo.width;
const sy = primero.height / ultimo.height;
// Play: animar la transformacion hasta cero
return elemento.animate(
[
{ transformOrigin: "top left", transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` },
{ transformOrigin: "top left", transform: "none" },
],
{ duration: 400, easing: "cubic-bezier(0.4, 0, 0.2, 1)" }
);
}
// Uso: mover una tarjeta de un contenedor a otro
const tarjeta = document.querySelector(".tarjeta");
const destino = document.querySelector(".columna-derecha");
document.querySelector("#mover").addEventListener("click", () => {
flip(tarjeta, () => destino.appendChild(tarjeta));
});
Ese código funciona pegado tal cual y hace FLIP de verdad. Fíjate en el transformOrigin: "top left": sin él, la escala se aplica desde el centro y las cuentas de la traslación dejan de cuadrar, porque getBoundingClientRect mide desde la esquina superior izquierda.
Los tres problemas que quedan
La versión manual funciona para el caso sencillo y se rompe en cuanto sales de él. Estos son los tres agujeros, y son exactamente lo que el plugin de GSAP resuelve.
Contenedores transformados. getBoundingClientRect devuelve coordenadas del viewport, ya afectadas por cualquier transformación de los ancestros. Si el contenedor de origen está escalado a 0,8 y el de destino no, la diferencia que calculas incluye ese factor y la transformación que aplicas es incorrecta. Con rotaciones el problema se vuelve intratable a mano, porque una diferencia de posición ya no se puede expresar como una traslación en los ejes del elemento.
Escalar frente a redimensionar. El código de arriba usa scale, que es barato pero deforma el contenido: si la tarjeta crece al doble, su texto también, y se ve estirado durante la animación. La alternativa es animar width y height, que no deforma pero cuesta layout. Elegir entre las dos, y poder cambiar de opinión, requiere dos implementaciones distintas.
Elementos que entran y salen. Si el cambio de layout hace aparecer un elemento nuevo, no hay medida First para él. Si hace desaparecer uno, no hay medida Last. Ambos casos son frecuentes —filtrar una lista los produce constantemente— y ninguno cabe en la función de arriba.
A eso se suman los detalles: interrupciones a mitad de animación, elementos anidados cuyas transformaciones se acumulan, y la necesidad de sacar elementos del flujo con position: absolute para que los layouts de tipo flex y grid no compriman todo mientras dura el movimiento.
// Lo mismo que la funcion manual, con el plugin
import { gsap } from "gsap";
import { Flip } from "gsap/Flip";
gsap.registerPlugin(Flip);
const estado = Flip.getState(".tarjeta");
destino.appendChild(tarjeta);
Flip.from(estado, { duration: 0.4, ease: "power2.inOut" });
La técnica es del dominio público y hay otras implementaciones: las View Transitions del navegador resuelven una clase parecida de problemas por otra vía, y auto-animate automatiza el caso sencillo con una sola línea. El plugin de GSAP gana cuando necesitas control fino sobre el timing, cuando hay transformaciones anidadas de por medio, o cuando la transición tiene que coordinarse con otras animaciones en la misma timeline.
Casi todas las animaciones que escribes tienen esta forma: el estado cambia al final, cuando la animación termina. Animas una tarjeta hacia su nueva posición y, cuando llega, actualizas el modelo. Esa forma es intuitiva y es frágil por una razón concreta: durante la animación tu aplicación está en un estado que no es ni el antiguo ni el nuevo, y todo lo que ocurra en esa ventana temporal —un clic, una consulta de geometría, un rerenderizado del framework, una navegación— encuentra el sistema a medias. De ahí salen los bugs que aparecen solo cuando el usuario hace clic rápido dos veces. FLIP invierte el orden: el estado cambia primero, de golpe, en la fase Last, y la animación es una mentira visual que se aplica encima de un estado que ya es correcto. Las consecuencias son notables. Si interrumpes la animación a mitad, no hay que deshacer nada, porque el DOM ya estaba bien desde el frame uno. Si el usuario hace clic tres veces seguidas, cada FLIP parte de una realidad consistente. Si consultas el DOM durante la animación, obtienes la verdad y no una posición intermedia. Y si la animación no llega a ejecutarse —porque el usuario prefiere movimiento reducido, porque el dispositivo va justo, porque hubo un error— lo único que se pierde es la decoración; la funcionalidad está intacta. Eso convierte a FLIP en el raro caso de una técnica de animación que también es una técnica de robustez, y explica por qué la misma idea reaparece con nombres distintos por todas partes: es exactamente el mismo razonamiento que hay detrás de las View Transitions del navegador, que capturan el estado antiguo, aplican el nuevo y animan entre las capturas. Si te llevas una sola idea de este nivel, que sea esta: cuando puedas elegir, aplica el estado real primero y miente encima.
- Implementa la función
flipdel ejemplo y muévete una tarjeta entre dos contenedores. - Quita el
transformOrigin: "top left"y explica por qué la animación deja de cuadrar. - Anima la misma transición con
leftytopen lugar de con transformaciones, y compara ambas en el panel de rendimiento con veinte tarjetas. - Mete el contenedor de destino dentro de un ancestro con
transform: scale(0.8)y comprueba cómo se rompe la versión manual. - Sustituye tu función por
Flip.getStateyFlip.fromy verifica que el caso escalado ya funciona.