Bonsai

Cómo funciona

Arrastrar y animar

Todas las librerías del laboratorio resuelven los mismos pasos, cada una a su manera. Entender esos pasos explica por qué Motion se siente mejor en una lista, por qué dnd-kit gana en un Kanban y por qué View Transitions no deja pulsar mientras anima. Las demos usan el código real del laboratorio.

1. Las piezas de una interacción

Arrastrar una tarjeta de una columna a otra parece un solo gesto, pero son cinco problemas distintos:

  1. Empezar: distinguir un clic de un arrastre (un umbral de unos píxeles o una espera).
  2. Seguir al puntero: mover algo que se vea como el elemento, a 60 fotogramas por segundo.
  3. Calcular el destino: sobre qué lista y en qué posición caería si soltaras ahora.
  4. Mostrarlo: un hueco que se abre, una línea, o los demás elementos apartándose.
  5. Terminar: cambiar los datos y animar el aterrizaje (o volver al sitio si se cancela).

Las librerías de arrastre (dnd-kit, Pragmatic) resuelven sobre todo 1, 3 y 5. Las de animación (Motion, AutoAnimate) resuelven 2 y 4. Por eso en el Kanban con Motion hubo que escribir a mano los pasos 1, 3 y 5: 337 líneas frente a las 93 de dnd-kit.

2. Dibujado o nativo

Hay dos formas de arrastrar en la web, y de ahí sale la diferencia de sensación más grande:

Dibujado (dnd-kit, Motion)

La librería escucha los eventos del puntero y mueve el elemento con transform. Controla todo: puede inclinarlo, apartar a los demás en vivo y animar el aterrizaje.

Nativo (Pragmatic, React Aria)

El navegador hace una imagen fija del elemento y la mueve él. Es rápido y funciona con archivos y entre ventanas, pero la imagen no se anima y la página no cambia hasta soltar.

3. Dónde cae

Calcular el destino es el corazón de cualquier arrastre. En el laboratorio aparecen dos estrategias: contar cuántos centros de tarjeta quedan por encima del puntero (el hueco se mueve en vivo), o mirar la tarjeta bajo el puntero y en qué mitad está (una línea en ese borde).

A
B(la que arrastras)
C
D
E

Mueve el ratón sobre la columna. La raya roja es el puntero; las punteadas, el centro de cada tarjeta.

Por centros

Hay 2 centros por encima del puntero: el hueco va en la posición 2 (barra verde).

indexFromMidpoints. Lo usa el Kanban de Motion, y es la idea de dnd-kit: el hueco se mueve en vivo.

Por el borde más cercano

Puntero entre tarjetas: no hay destino.

destinationFromEdge. Lo usa Pragmatic: una línea en el borde y nada se mueve hasta soltar. Si B va hacia abajo, se descuenta su propio hueco.

Un detalle que costó en el Kanban de Motion: los centros se miden con offsetTop y no con getBoundingClientRect. Mientras las tarjetas se deslizan llevan un transform; si se midiera la posición animada, el destino oscilaría entre dos huecos.

4. FLIP: cómo se anima un cambio de diseño

Cuando React reordena una lista, los elementos saltan a su sitio nuevo en un solo fotograma. Para que se deslicen, Motion (layout) y AutoAnimate usan la misma técnica: First, Last, Invert, Play. Hazla aquí paso a paso:

1
2
3
4
5
6

Pulsa «Barajar». En paso a paso, avanza con «Siguiente paso».

engine/flip.ts
export function invert(first: Rect, last: Rect): Invert {
  return {
    x: first.left - last.left,
    y: first.top - last.top,
    scaleX: first.width / last.width,
    scaleY: first.height / last.height,
  };
}
// Play: animar desde el transform inverso hasta "none".
el.animate([{ transform: invertTransform(inv) }, { transform: "none" }], { duration: 350 });

AutoAnimate aplica FLIP solo cuando cambian los hijos de un contenedor (entran, salen o cambian de orden). Por eso no animó el cambio de cuadrícula a lista: los hijos son los mismos, solo cambió su estilo. Motion mide cada elemento con layout en cada render y anima cualquier cambio.

5. Resortes e interrupciones

Una animación con duración fija (300 ms, curva ease-out) tiene un problema: si llega otro cambio a mitad de camino, hay que cortarla y empezar otra desde cero, y se nota el salto. Un resorte no tiene duración: tiene rigidez, amortiguación y masa, y en cada fotograma calcula la fuerza hacia el destino a partir de la posición y la velocidad actuales. Si el destino cambia, sigue desde donde va, con la inercia que llevaba.

La configuración de la galería con Motion
const SPRING = { type: "spring", stiffness: 380, damping: 34, mass: 0.9 };

Esa es la diferencia que se nota al pulsar filtros seguidos en la galería, y la razón principal de que Motion se sienta «vivo».

6. View Transitions: fotos en vez de elementos

La View Transitions API no anima los elementos reales. El navegador toma una captura de la página antes del cambio, deja que React aplique el cambio, toma otra captura y anima entre las dos con pseudoelementos (::view-transition-old y ::view-transition-new). Cada <ViewTransition name> es una capa propia que se puede mover y transformar.

Eso explica sus tres rasgos en la galería:

  • No responde a los clics mientras anima: lo que ves son capturas encima de la página, no la página.
  • No se interrumpe: una transición nueva espera a la anterior o la salta al final.
  • Solo se activa con startTransition: React necesita poder retrasar el cambio hasta tener la primera captura. Las actualizaciones de un store externo (Zustand, con useSyncExternalStore) siempre son inmediatas, así que la galería guarda su estado en useReducer.

Donde brilla es en la navegación entre páginas de Next.js: cada navegación ya es una transición y una miniatura puede convertirse en la imagen de la página de detalle sin librería.

7. Masonry

Pinterest no usa columnas de CSS: calcula la posición de cada tarjeta. La galería hace lo mismo, y eso hace justa la comparación: las tres técnicas reciben las mismas posiciones y solo deciden cómo animar el paso de unas a otras.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

La tarjeta 1 va a la primera columna; cada siguiente, a la columna que en ese momento termina más arriba. Las posiciones se calculan antes de pintar, así las tres técnicas de la galería animan exactamente el mismo cambio.

8. Cómo se midió

  • Peso: pnpm measure:interacciones empaqueta cada implementación por separado con esbuild, minifica y comprime con gzip. React, Zustand, los iconos y el código compartido quedan fuera: el número es lo que añade elegir esa librería.
  • Código propio: líneas no vacías de cada implementación.
  • Fluidez: el medidor del laboratorio cuenta el tiempo entre fotogramas (requestAnimationFrame) mientras dura la interacción. Depende del equipo, así que se mide en el tuyo: abre el laboratorio, elige 500 elementos y arrastra o anima.

9. Mapa de archivos

Todo vive en src/features/interacciones/:

engine/reorder.tsMover en una lista, destino por borde, acciones de menú
engine/board.tsTablero Kanban: mover tarjetas y columnas, destino por centros, desplazamiento automático
engine/gallery.tsGalería: datos, reductor, masonry y lista
engine/flip.tsLa inversión de FLIP
engine/frameStats.tsEstadísticas del medidor de fotogramas
state/Stores de la lista y del Kanban (Zustand), medidor de fotogramas
scenarios/<escenario>/Una implementación por librería sobre los mismos datos
data/Fichas de librerías y mediciones (measurements.json)
scripts/measure-interacciones.mjsEmpaqueta cada implementación con esbuild y mide KB y líneas