# Cómo crear un pulso continuo (loop)

> Animar un valor en ciclo infinito con LoopAnimation, útil para indicadores de carga y efectos de brillo.

Source: https://crossfly.dev/es/docs/cookbook/anim/pulso-loop — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# Cómo crear un pulso continuo (loop)

**Propósito.** Hacer que algo pulse/parpadee indefinidamente (indicador de "cargando", brillo, respiración de un botón). `LoopAnimation` cicla un valor de 0 → 1 repetidamente, con o sin easing, **sin** `onComplete` (es infinita — la detienes con `stop()`).

## APIs usadas
- `new LoopAnimation(float duration [, float fps, boolean easeInOut])` — cada ciclo dura `duration` segundos y se reinicia solo. `fps` por defecto 60; `easeInOut` por defecto `false` (linear). Con `easeInOut=true`, aplica ease-in-out cubic por ciclo.
- `LoopAnimation.onUpdate(Runnable)` — callback en cada frame; lee `getValue()` (0–1, progreso del ciclo actual).
- `LoopAnimation.getValue()` — progreso actual del ciclo.
- `LoopAnimation.start()` / `stop()` — inicia / interrumpe el loop. **No hay `onComplete` ni `cancel()`** — es infinita por naturaleza.
- `Component.alpha(float)` — aplica opacidad (para el efecto de "respiración").
- `Component.repaint()` — estático; obligatorio en el `onUpdate`.

## Ejemplo completo (copiar, pegar y ejecutar)
```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.animation.*;

/** Pantalla principal ejecutable: pulsa un "●" en loop; detén el loop al cerrar la pantalla. */
public class MainWindow extends Window
{
   private LoopAnimation pulse;

   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      Label dot = new Label("●");
      add(dot);

      // ciclo de 1 s, 60 fps, con easing (pulso suave)
      pulse = new LoopAnimation(1f, 60f, true);
      pulse.onUpdate(() ->
         {
            // el valor va 0 → 1 y reinicia; mapea a opacidad 0.3 → 1.0
            float v = 0.3f + 0.7f * pulse.getValue();
            dot.alpha(v);
            Component.repaint();
         })
         .start();

      // botón que finaliza el pulso (ej.: cuando la carga termina)
      add(new Button("Parar").onAction(e -> pulse.stop()));
   }

   @Override
   public void onRemove()
   {
      // Detén al salir de la pantalla para no dejar un loop huérfano animando fuera de la pantalla.
      if (pulse != null) pulse.stop();
      super.onRemove();
   }
}
```

> El fragmento de abajo muestra **solo el núcleo**. Para ejecutarlo, colócalo en el esqueleto `MainWindow` de arriba.

## Fragmento mínimo
```java
// verificado en la fuente: LoopAnimation(duration, fps, easeInOut); sin onComplete; detén con stop()
Label dot = new Label("●");
add(dot);

// ciclo de 1 s, 60 fps, con easing (pulso suave)
LoopAnimation pulse = new LoopAnimation(1f, 60f, true);
pulse.onUpdate(() ->
   {
      // el valor va 0 → 1 y reinicia; mapea a opacidad 0.3 → 1.0
      float v = 0.3f + 0.7f * pulse.getValue();
      dot.alpha(v);
      Component.repaint();
   })
   .start();

// más tarde, para finalizar el pulso (ej.: cuando la carga termina):
pulse.stop();
```

Cuidados importantes:

- **Guarda la referencia** (`pulse`) para poder llamar a `stop()`. Una `LoopAnimation` sin `stop()` corre para siempre y consume CPU/GPU continuamente (cada frame llama a `repaint()`).
- **Detén al salir de la pantalla**: llama a `pulse.stop()` cuando la `Window` se cierre (`onRemove`/`hide`) para no dejar un loop huérfano animando fuera de la pantalla.
- `getValue()` vuelve a 0 al inicio de cada ciclo. Para un pulso "ida y vuelta" (0→1→0), mapea con una función triangular o seno sobre `getValue()`.

## Profundiza
- [Animaciones de valor](/docs/ui/animations/value-animations) · [Press y feedback](/docs/ui/animations/press-and-feedback)
- Receta hermana: [Fade de un componente](/docs/cookbook/anim/fade) · [Mover un componente](/docs/cookbook/anim/mover-componente)
