Saltar al contenido
zabloo

Spinner

El indicador para un trabajo sin final medible: dice que algo está pasando, sin afirmar cuánto lleva. Sus hijos laten en una onda que viaja — no gira nada, porque la v1 no tiene transform.

primitivedesde v1sin focus

Un Spinner dice algo está pasando, y no te puedo decir cuánto lleva. Usas uno cuando no hay ninguna fracción que enseñar —conectar con un servidor, buscar partida, cargar un nivel— y usas una ProgressBar en cuanto la haya. Piensa en un panel de emparejamiento: tres puntos debajo de la palabra Searching, iluminándose por turnos. Se iluminan en vez de girar, y la sección siguiente va de por qué eso es una decisión y no un atajo.

spinner-loop.viewIR v1
Two activity indicators side by side: three purple dots under the label Loading world, and five purple bars of rising and falling height under the label Syncing. Each bead is painted at its own brightness — the wave that travels through them.
ESTADO
  • hover — inactivo
  • pressed — inactivo
  • focused — inactivo
  • selected — inactivo
  • disabled — inactivo

Se lee del último frame dibujado

Parado: pulsa Ejecutar para dibujarlo en la GPU.

import { Column, Container, Row, Spinner, Text } from "@zabloo/react";


const BAR = {
  background: "{color.brand}",
  radius: "{radius.sm}",
} as const;

export default function SpinnerLoop() {
  return (
    <Column
      layout={{ grow: 1, justify: "center", align: "center", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Row layout={{ gap: "{space.4}", align: "stretch" }}>
        <Column
          layout={{
            width: 200,
            height: 120,
            padding: "{space.4}",
            gap: "{space.3}",
            justify: "center",
            align: "center",
          }}
          style={{
            background: "{color.surface}",
            radius: "{radius.lg}",
            borderWidth: "{border.hairline}",
            borderColor: "{color.line}",
          }}
        >
          {/* No children: the component builds `dots` round beads for you. */}
          <Spinner
            id="dots"
            dots={3}
            size={10}
            period="{motion.loop}"
            dot={{ background: "{color.brand}", radius: "{radius.pill}" }}
            layout={{ gap: "{space.2}", align: "center" }}
          />
          <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>Loading world</Text>
        </Column>

        <Column
          layout={{
            width: 200,
            height: 120,
            padding: "{space.4}",
            gap: "{space.3}",
            justify: "center",
            align: "center",
          }}
          style={{
            background: "{color.surface}",
            radius: "{radius.lg}",
            borderWidth: "{border.hairline}",
            borderColor: "{color.line}",
          }}
        >
          {/* Your own beads, in wave order. They are ordinary children in every
              respect but the opacity the loop multiplies into them. */}
          <Spinner id="bars" period="{motion.loop}" min={0.15} layout={{ gap: 5, align: "center" }}>
            <Container layout={{ width: 5, height: 14 }} style={BAR} />
            <Container layout={{ width: 5, height: 22 }} style={BAR} />
            <Container layout={{ width: 5, height: 30 }} style={BAR} />
            <Container layout={{ width: 5, height: 22 }} style={BAR} />
            <Container layout={{ width: 5, height: 14 }} style={BAR} />
          </Spinner>
          <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>Syncing</Text>
        </Column>
      </Row>
    </Column>
  );
}
Dos indicadores, un solo tipo de nodo: tres puntos generados y cinco cuentas tuyas. Pulsa Run y sigue una sola cuenta en vez del grupo — no se mueve nunca, y no gira nada. Lo único que cambia es la opacidad, que es la única magnitud que todos los targets calculan hasta el mismo número exacto.

No gira

La v1 no tiene transform —ni translate, ni rotate, ni scale—, así que un arco girando no se puede expresar. Lo que se puede expresar, y es portable hasta el último decimal, es una modulación periódica de la opacity.

Queda la pregunta de por qué es un tipo de nodo siquiera, y la respuesta es el bucle: una animación infinita es comportamiento del que es dueño el SDK e indexado por la identidad del componente, y esa identidad tiene que existir en la IR. No hay nada más en el formato que se repita para siempre.

Las dos tablas de abajo son la distancia más grande del catálogo después de la de Toggle. dots, size y dot son instrucciones para construir cuentas, y ya no existen cuando se publica nada: lo que recibe el juego son las cuentas mismas, como nodos hijos normales.

Props de autoría

Lo que escribes en @zabloo/react.

PropTipoPor defectoDescripción
dotsnumber3Cuántas cuentas construir cuando no pasas hijos propios.
sizenumber8Diámetro de la cuenta en px (solo para las generadas).
periodDim900Ciclo completo en ms. Es un Dim, así que el bucle es tematizable.
minnumber0.25Multiplicador de opacidad en lo más apagado de la onda, 0..1.
easingEasing"ease-in-out"Curva de la subida y la vuelta abajo.
dotStyleabsentEstilo de cada cuenta generada, mezclado sobre el de por defecto.
childrenReactNodeabsentTus propias cuentas, en orden de onda — sustituyen del todo a los puntos generados.

Además de estas, todo componente acepta las props base del nodo —id, visible, disabled, layout, style, states, transition, autofocus, clip— más variant, que el theme resuelve al exportar y que nunca aparece en la IR.

Props de IR

Lo que llega al juego, cuando ya no queda capa de autoría.

PropTipoPor defectoDescripción
periodDim900Ciclo completo en ms. <= 0 o no finito lo congela.
minnumber0.25El valle de la onda: el multiplicador de opacidad en lo más apagado.
easingEasing"ease-in-out"Curva de la rampa.
childrenZNode[][]Las cuentas, en orden de onda — hijos normales en todo lo demás.

Por eso pasar hijos propios sustituye a los puntos generados en vez de configurarlos: cuando se publica algo, entre los dos nunca hubo diferencia.

La onda

Con n hijos, el hijo i lleva la fase frac(elapsed / period − i / n), y el SDK multiplica su opacidad ya resuelta por min + (1 − min) · spinnerPulse(fase, easing).

Multiplicativo, como cualquier otra opacidad del sistema — así que una cuenta escrita con opacity: 0.5 sigue latiendo, solo que más apagada. spinnerPulse es una rampa simétrica: sube durante la primera mitad del ciclo y vuelve a bajar durante la segunda, así que el bucle no tiene costura, y las fases fuera de 0..1 dan la vuelta, incluidas las negativas que produce el desfase de una cuenta. Está construida sobre el mismo easing en forma cerrada que usa el resto del motor de movimiento, por la razón por la que ese existe: la aritmética es lo que mantiene a todos los targets en el mismo número.

Las cuentas conservan su layout normal: la direction, el gap y el align del propio nodo las colocan como a los hijos de cualquier contenedor.

Un theme de movimiento reducido lo congela moviendo un token

period es un Dim, así que puede ser "{motion.loop}". Un theme que ponga a cero los tokens de movimiento detiene este spinner sin que el renderer necesite ningún interruptor — que es exactamente como la figura de arriba respeta el ajuste de movimiento reducido de tu sistema: cambia el diccionario antes de montar, y el bucle es uno de sus valores.

Comportamiento

Estados

Solo disabled, heredado — y sigue corriendo mientras está deshabilitado. disabled va de input, y un spinner no acepta ninguno de todas formas, así que apagar el panel de alrededor no congela la noticia de que algo se sigue cargando.

Focusable

No, y no hay nada que el jugador pueda hacerle. Informa de que algo está pasando; no hay nada que activar.

El bucle

Es del SDK e indexado por la identidad de este nodo, que es la razón de que sobreviva a un relayout y de que dos spinners de una lista no compartan fase. Un period de <= 0, o uno no finito, congela la onda en vez de dividir por cero.

Actions

Ninguna, en ninguno de los dos sentidos: el juego no oye nada de un spinner, y tampoco tiene que alimentarlo. Escóndelo con visible cuando el trabajo termine.

Degradación

En un SDK más antiguo

En un SDK más antiguo se ven los mismos puntos en los mismos sitios — simplemente no se mueven nunca.

Como Container: las cuentas se ven, quietas. La degradación es la ausencia del bucle, nunca un cambio de layout — un SDK más antiguo dibuja exactamente los mismos tres puntos, y no respiran.

Composición

Las cuentas son nodos normales, así que la forma de un indicador es una cuestión de layout: puntos redondos, barras de distintas alturas, una fila de iconos. Lo que añade el nodo es la onda que las recorre.

  • El <Spinner> pelado — cuando quieras el indicador de la casa y no tengas ninguna razón para pensar en él.
  • Cuentas generadas y ajustadas — usa dots, size y min para encajar un indicador en una esquina justa de un HUD.
  • Tus propias cuentas — pasa hijos cuando la forma signifique algo: barras de ecualizador, una fila de iconos de palo, cualquier cosa que no sea un punto.
  • Una ProgressBar en su lugar — en cuanto el juego sepa cuánto lleva, este nodo es el equivocado.
// Generated beads: three round dots, the default.
<Spinner />

// More of them, dimmer trough, and a themeable loop.
<Spinner dots={5} size={6} period="{motion.loop}" min={0.15} />

// Your own beads, in wave order. They replace the generated dots entirely.
<Spinner>
<Container layout={{ width: 4, height: 16 }} style={{ background: "{color.dot}" }} />
<Container layout={{ width: 4, height: 22 }} style={{ background: "{color.dot}" }} />
</Spinner>

// A determinate amount is not a Spinner. A fraction is a ProgressBar.
<ProgressBar value={{ bind: "download.progress" }} />

Relacionado