Saltar al contenido
zabloo

Motion

Una transición por nodo y ninguna lista de disparadores: un valor animable ya resuelto que cambia se interpola. Qué se anima, qué salta y las cuatro curvas de easing en forma cerrada.

Aquí el movimiento es una sola idea: un nodo puede decir cuando cambie mi aspecto, entra en el cambio suavemente en vez de saltar a él. El botón de comprar de la tienda se aclara un tono bajo el puntero; dale una transición y ese tono aparece fundiéndose durante 120 ms en vez de conmutar. No hay ninguna línea de tiempo que autorar ni ninguna lista de disparadores que mantener: si cambia un valor con el que el nodo pinta, y el nodo declaró una transición, se interpola.

Formalmente: un nodo puede declarar una transición, y entonces sus valores animables se interpolan en vez de saltar cada vez que cambian.

{
  "type": "Button",
  "transition": { "duration": "{motion.fast}", "easing": "ease-out" },
  "style":  { "background": "{color.primary}" },
  "states": { "hover": { "style": { "background": "{color.primary.hover}" } } }
}
PropTipoPor defectoDescripción
durationDimMilisegundos. Es un Dim, así que el movimiento se puede tematizar ("{motion.fast}"). Cero o menos es instantáneo.
easing"linear" | "ease-in" | "ease-out" | "ease-in-out""ease-out"Curva de la rampa.

transition vive en el nodo, no en style: style es el qué, transition el cómo. Como duration es un token como cualquier otro, un tema de «reducir movimiento» pone motion.fast a 0 y para en seco la UI entera sin volver a emitir un solo nodo.

Los keyframes y las líneas de tiempo no están en v1. Esta es la pieza del sistema de animación que el formato aterrizó primero, y el resto queda aplazado.

No hay lista de disparadores

Cambió un valor animable ya resuelto ⇒ transiciona. Lo haya causado lo que lo haya causado: entrar o salir de un estado, un SetData sobre una entrada bindeada, un token que cambió un hot-update de tema, un relayout que le dio al nodo un tamaño nuevo.

Esto es lo que mantiene pequeño el modelo. No hay nada que declarar sobre cuándo animar, así que no hay nada que mantener sincronizado con los estados, los bindings o los datos.

Qué se anima

No todo se puede interpolar. Un color tiene un punto intermedio; un salto de línea no, y el texto de un campo tampoco. De qué lado de esa raya cae cada valor es normativo, porque un valor que se suaviza en un target y salta en otro es la misma UI comportándose distinto:

normativeValores animados y valores que saltan

ValorComportamientoPor qué
background, borderColor, colorSe animaSe interpola componente a componente.
opacity, radius, borderWidthSe animaMagnitudes numéricas llanas.
width, height, gap, paddingSe animaSe interpolan como entradas de layout, antes del pase.
fontSizeSaltaEs la clave del atlas de glifos.
textAlign, textAlignY, lineHeight, wrap, overflow, maxLinesSaltaVolver a partir las líneas no tiene intermedio.
grow, direction, justify, alignSaltaEntradas de layout discretas.
visible, disabled, clip, text, open, src, fit, axis, scrollbarSaltaEstructura y contenido, no magnitudes.
El valor de un control — value/min/max/step de Slider, value/placeholder/maxLength de TextInputSaltaEs estado que el jugador está arrastrando o tecleando, no una magnitud visual que tenga que ponerse al día.
modal, z y autoCloseMs de OverlaySaltaz y autoCloseMs son números, pero son orden y temporización.

Los colores se interpolan componente a componente en sRGB directo y con alfa directo.

Las dimensiones de layout se interpolan como entradas, antes del pase de layout, nunca como rects ya calculados. El SDK interpola el width, el height, el gap y el padding declarados y después corre su layout de siempre con los valores interpolados, así que sigue habiendo exactamente un pase de layout por frame y un rect calculado nunca se realimenta a su propia entrada.

Los extremos

  • Los dos extremos tienen que resolver. Un extremo undefined (auto) no tiene nada desde lo que interpolar ni hacia lo que hacerlo, así que el cambio salta. Donde esto aparece es en los tamaños y en los colores declarados; en cambio opacity, radius, borderWidth, gap y padding tienen valores por defecto del renderer, así que siempre resuelven y un estado que introduce uno se anima igualmente.
  • Montar salta, y recargar un envelope también: no hay valor anterior desde el que interpolar.
  • Una interrupción reapunta desde el valor que hay ahora mismo en pantalla, a lo largo de una duración entera — un botón soltado a mitad del fundido de hover continúa desde donde está en vez de reiniciarse.
  • Un borderColor no declarado conserva el último en vez de caer a ningún valor. El borde que se va se está yendo por borderWidth, y un anillo de foco que perdiera su color a medio camino soltaría un destello del magenta de color-ausente al salir.
  • Un nodo fuera del layout olvida su estado de animación; cuando vuelve, salta. No hay ningún valor anterior honesto para un nodo que no estaba en pantalla.

Sin cascada

transition se lee solo del nodo base. Un nodo nunca hereda la de su padre, y una sobrescritura de estado no puede llevar una — una transición asimétrica de entrada y salida es una extensión futura compatible, no superficie de v1.

En la autoría, el movimiento se resuelve como una variante: prop del nodo, luego variante, luego el valor por defecto del tema para ese componente — y la declaración más específica gana entera, ya que es un solo objeto y no un conjunto de campos que mezclar.

El easing

Una curva de easing es lo que hace que un fundido parezca frenar al final en vez de moverse a ritmo constante. Hay cuatro, y sus formas están escritas como aritmética para que un botón en un móvil y ese mismo botón en una consola sean del mismo color en el mismo milisegundo.

normative

Cuatro curvas, definidas como polinomios cúbicos en forma cerrada y no como curvas de Bézier cúbicas de CSS, para que todos los targets calculen el mismo número sin un solver:

Curvaf(t)
lineart
ease-in
ease-out1 − (1 − t)³
ease-in-outt < 0.5 ? 4t³ : 1 − (−2t + 2)³ / 2

Una t fuera de 0..1 se acota. Una curva desconocida —contenido más nuevo sobre un lector más viejo— cae a linear en vez de negarse a animar.

Implementación de referencia: easeProgress en @zabloo/format.

El movimiento del que es dueño un componente

El comportamiento propio de un componente puede mover esta misma maquinaria con extremos que calcula él. Cuando lo hace, la interpolación es parte de la spec de ese componente y no un valor del conjunto animable de arriba — que es por lo que ninguna de estas props aparece en él:

ComponenteQué hace su transición
ProgressBarInterpola el valor y después maqueta el relleno a partir de él. Lo interpolado nunca es el rect del relleno.
SpinnerDa vueltas sin fin sobre period, pulsando la opacidad de sus hijos. Es lo único del formato que se repite sin acabar.
OverlayFunde su presencia de capa entera al entrar y al salir, así que un overlay que se cierra sigue en pantalla exactamente una duración después de que visible se pusiera a falso.
CollapseAnima su propia altura entre cerrado y la altura natural de su contenido.
ToggleFunde cruzados sus dos slots de indicador, que comparten caja.
SliderSe desliza hasta un valor que empujó el juego; un valor que el jugador está arrastrando nunca va por detrás del dedo.
visible no se anima nunca

Un Overlay que se está desvaneciendo es solo píxeles: la entrada, la trampa de foco y los temporizadores leen la capa viva, que él ya ha abandonado.

Relacionado