# `Plushie.Animation.Tween`
[🔗](https://github.com/plushie-ui/plushie-elixir/blob/v0.7.2/lib/plushie/animation/tween.ex#L1)

SDK-side stateful interpolator for model-level animation.

Pure functions operating on structs. No processes, no state
management beyond what lives in your app model. Use this for
complex animations that need frame-by-frame control: canvas
animations, physics simulations, custom interpolation logic.

For simple property animations (fades, slides, scales), prefer
renderer-side transitions via `Plushie.Animation.Transition`
which require zero model state and zero wire traffic.

## Example

    alias Plushie.Animation.Tween

    def init(_opts) do
      %{anim: Tween.new(from: 0.0, to: 1.0, duration: 300, easing: :ease_out)}
    end

    def subscribe(_model) do
      [Plushie.Subscription.on_animation_frame()]
    end

    def update(model, %SystemEvent{type: :animation_frame, value: ts}) do
      anim = model.anim |> Tween.start_once(ts) |> Tween.advance(ts)
      %{model | anim: anim}
    end

    def view(model) do
      opacity = Tween.value(model.anim)
      # use opacity in widget props
    end

## Easing

Uses `Plushie.Animation.Easing` for the full catalogue of 31
named curves plus cubic bezier. Pass easing as an atom:

    Tween.new(from: 0.0, to: 1.0, duration: 300, easing: :ease_out_bounce)

## Interruption

Change the target mid-animation with `redirect/2`. The animation
smoothly continues from its current interpolated value:

    anim = Tween.redirect(model.anim, to: 0.0, at: timestamp)

## Spring mode

For physics-based animation on the SDK side:

    anim = Tween.spring(from: 0.0, to: 1.0, stiffness: 200, damping: 20)

# `t`

```elixir
@type t() :: %Plushie.Animation.Tween{
  auto_reverse: boolean() | nil,
  delay: non_neg_integer() | nil,
  duration: pos_integer() | nil,
  easing: Plushie.Animation.Easing.t(),
  easing_fn: (float() -&gt; float()) | nil,
  finished: boolean(),
  from: number(),
  last_timestamp: integer() | nil,
  repeat: pos_integer() | :forever | nil,
  spring_config: map() | nil,
  started_at: integer() | nil,
  to: number(),
  value: number() | nil
}
```

# `advance`

```elixir
@spec advance(animation :: t(), timestamp :: integer()) :: t()
```

Advances the animation to the given timestamp.

Always returns an updated `%Tween{}` struct. Check
`finished?/1` to detect completion.

If the animation hasn't been started, returns the struct unchanged.

# `finished?`

```elixir
@spec finished?(animation :: t()) :: boolean()
```

Returns true if the animation has completed.

# `looping`

```elixir
@spec looping(
  from :: number(),
  to :: number(),
  duration_ms :: pos_integer(),
  opts :: keyword()
) :: t()
```

Creates a looping tween that repeats forever with auto-reverse.

Convenience for common back-and-forth animations (pulsing,
breathing, oscillating). Equivalent to calling `new/1` with
`repeat: :forever` and `auto_reverse: true`.

## Example

    Tween.looping(0.0, 1.0, 500, easing: :ease_in_out)

# `new`

```elixir
@spec new(opts :: keyword()) :: t()
```

Creates a new timed animation.

## Required options

- `from:` - start value
- `to:` - end value
- `duration:` - duration in milliseconds

## Optional

- `easing:` - easing atom or `{:cubic_bezier, ...}`. Default: `:ease_in_out`
- `delay:` - delay before start in ms. Default: 0
- `repeat:` - repeat count or `:forever`
- `auto_reverse:` - reverse on each repeat cycle

## Example

    Tween.new(from: 0.0, to: 1.0, duration: 300, easing: :ease_out)

# `redirect`

```elixir
@spec redirect(animation :: t(), opts :: keyword()) :: t()
```

Redirects the animation to a new target, starting from the
current interpolated value. Resets the timer.

Use this for smooth interruption: the animation continues
from where it is rather than jumping.

## Options

- `to:` - new target value (required)
- `at:` - current timestamp (required)
- `easing:` - optionally change easing
- `duration:` - optionally change duration

# `running?`

```elixir
@spec running?(animation :: t()) :: boolean()
```

Returns true if the animation is actively running (started and not finished).

# `spring`

```elixir
@spec spring(opts :: keyword()) :: t()
```

Creates a spring-mode animation (SDK-side spring solver).

## Required options

- `from:` - start value
- `to:` - end value

## Optional

- `stiffness:` - spring constant. Default: 100
- `damping:` - friction. Default: 10
- `mass:` - mass. Default: 1.0
- `velocity:` - initial velocity. Default: 0.0

## Example

    Tween.spring(from: 0.0, to: 1.0, stiffness: 200, damping: 20)

# `start`

```elixir
@spec start(animation :: t(), timestamp :: integer()) :: t()
```

Starts the animation at the given timestamp. Resets value to `from`.

If already started, restarts from the beginning.

# `start_once`

```elixir
@spec start_once(animation :: t(), timestamp :: integer()) :: t()
```

Starts the animation only if it hasn't been started yet.

Convenience for the common pattern of starting on the first frame.

# `value`

```elixir
@spec value(animation :: t()) :: number() | nil
```

Returns the current interpolated value.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
