aura-anim-iced 0.2.1

Iced-first animation primitives.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# aura-anim-iced

Iced-first animation orchestration for applications that need coordinated
property changes, state transitions, and screen-to-screen route motion.

This crate builds on Iced's public animation surface instead of replacing it.
User-facing APIs use Iced types such as `iced::Color`, `iced::Vector`,
`iced::Size`, `iced::Rectangle`, `iced::Shadow`, and
`iced::animation::Easing`.

The foundation layer covers:

- typed visual properties and sampled property snapshots;
- timing primitives that use Iced easing directly;
- property keyframes and timeline orchestration;
- a runtime that can gate Iced subscriptions while animations are active;
- Iced integration helpers for applying snapshots in `view` code.

The v0.2 behavior layer adds:

- `PropertyTransition` and `BehaviorRule` for animating value changes from the
  visual value currently on screen;
- `StateAnimator`, `StateTransition`, and `StateTransitionSet` for mapping
  application state changes to timelines;
- retargeting and interruption helpers that replace active animations without
  jumping back to stale target values;
- `RouteAnimator` and route screen transitions for outgoing and incoming route
  motion on separate targets.

Use Iced's `Animation<T>` for direct single-value animation. Use
`aura-anim-iced` when a UI state change needs coordinated opacity, transform,
size, color, shadow, hold, sequence, parallel, and runtime cleanup behavior.

## Status

v0.2.1

## Installation

Add the crate to an Iced application:

```sh
cargo add aura-anim-iced
```

Enable optional diagnostics when runtime tick events should be visible through
`tracing`:

```sh
cargo add aura-anim-iced --features tracing
cargo add aura-anim-iced --features inspector
```

The same configuration can be written directly in `Cargo.toml`:

```toml
[dependencies]
aura-anim-iced = "0.2.1"
```

```toml
[dependencies]
aura-anim-iced = { version = "0.2.1", features = ["inspector"] }
```

## Minimal Runtime Example

Store an `AnimationRuntime` in application state, register keyframes in `update`,
keep an Iced tick subscription active while the runtime is playing,
and convert tick output into view effects for one target.

```rust
use std::time::Instant;

use aura_anim_iced::{iced_ext, prelude::*};

struct App {
    animations: AnimationRuntime,
    panel: AnimationTargetId,
    panel_effects: EffectSnapshot,
}

#[derive(Debug, Clone)]
enum Message {
    OpenPanel,
    AnimationTick(Instant),
}

fn update(app: &mut App, message: Message) {
    match message {
        Message::OpenPanel => {
            app.animations.register_keyframes(
                app.panel,
                KeyframesBuilder::new()
                    .with_timing(Timing::new(180.0))
                    .opacity(0.0, 0.0)
                    .opacity(1.0, 1.0)
                    .scale(0.0, 0.96)
                    .scale(1.0, 1.0)
                    .finish(),
            );
        }
        Message::AnimationTick(tick) => {
            let output = iced_ext::update_tick(&mut app.animations, tick);
            app.panel_effects = tick_effect_snapshot_for(&output, app.panel);
        }
    }
}

fn subscription(app: &App) -> iced::Subscription<Message> {
    iced_ext::subscription(&app.animations, Message::AnimationTick)
}
```

In `view`, apply the sampled `EffectSnapshot` fields to the widget style,
layout, or wrapper code owned by the application.

## Animatable Values

Public animation inputs use Iced value types wherever possible. The v0.1 value
model covers scalar values, `iced::Vector`, `iced::Size`, `iced::Rectangle`,
`iced::Color`, `iced::Shadow`, and transform-friendly values. Interpolation is
kept internal so application code works with typed properties and sampled
snapshots instead of implementing animation traits.

```rust
use aura_anim_iced::{KeyframesBuilder, Timing, property};
use iced::Color;

let fade_and_color = KeyframesBuilder::new()
    .with_timing(Timing::new(160.0))
    .at(0.0, (property::OPACITY, 0.0))
    .at(1.0, (property::OPACITY, 1.0))
    .at(0.0, (property::BACKGROUND, Color::from_rgb(0.12, 0.14, 0.18)))
    .at(1.0, (property::BACKGROUND, Color::from_rgb(0.20, 0.36, 0.52)))
    .finish();
```

## Property Tracks

Properties are identified by typed `PropertySpec` values. Built-in specs cover
opacity, translation, scale, size, padding, radius, colors, and shadow.
Applications can also define custom specs when an example or widget needs an
extra sampled value, such as a toast offset.

```rust
use aura_anim_iced::{PropertyKey, PropertySpec, property};

const TOAST_Y: PropertySpec<property::Scalar> =
    PropertySpec::new(PropertyKey::new("app", "toast-y"), 21);
```

A `PropertySnapshot` stores sampled values for one target. When snapshots are
merged, later values replace earlier values with the same property spec and the
result is sorted by composition order.

## Keyframes

Use `KeyframesBuilder` to collect property snapshots at normalized offsets from
`0.0` to `1.0`, then call `finish()` to compile them into a `Keyframes` value.
The finished keyframes own a `Timing`, so duration, easing, fill mode,
direction, iterations, and playback rate stay attached to the sampled property
data.

```rust
use aura_anim_iced::{Easing, KeyframesBuilder, Timing, property};

let popup_open = KeyframesBuilder::new()
    .with_timing(Timing::new(280.0).with_easing(Easing::EaseOut))
    .at(0.0, (property::OPACITY, 0.0))
    .at(0.0, (property::SCALE, 0.92))
    .at(0.68, (property::SCALE, 1.07))
    .at(1.0, (property::OPACITY, 1.0))
    .at(1.0, (property::SCALE, 1.0))
    .finish();
```

Duplicate offsets are merged. If the same property appears multiple times at
the same offset, the later value wins.

## Timeline Orchestration

Timelines combine keyframe tracks into sequences, parallel groups, and holds.
Use sequences for lifecycle animation, parallel groups for coordinated property
changes, and holds when a state should remain visible before the next step.

```rust
use aura_anim_iced::{
    Duration, Easing, Hold, KeyframesBuilder, Timeline, Timing, Track, property,
};

let enter = Track::new(
    KeyframesBuilder::new()
        .with_timing(Timing::new(220.0).with_easing(Easing::EaseOut))
        .at(0.0, (property::OPACITY, 0.0))
        .at(1.0, (property::OPACITY, 1.0))
        .finish(),
);

let exit = Track::new(
    KeyframesBuilder::new()
        .with_timing(Timing::new(180.0).with_easing(Easing::EaseIn))
        .at(0.0, (property::OPACITY, 1.0))
        .at(1.0, (property::OPACITY, 0.0))
        .finish(),
);

let toast_lifecycle = Timeline::sequence([
    enter.into(),
    Hold::new(Duration::from_millis(1_200.0)).into(),
    exit.into(),
]);
```

Use `Timeline::parallel` when several tracks should sample at the same time.
Property collisions are resolved by insertion order inside the target snapshot.

## Runtime Ticking

`AnimationRuntime` stores active keyframes and timelines by target ID. Register a
source in `update`, keep the returned handle if completion cleanup matters, and
route tick output back into application state.

```rust
use aura_anim_iced::{
    AnimationRuntime, AnimationTargetId, KeyframesBuilder, Timing, property,
};

let mut runtime = AnimationRuntime::new();
let target = AnimationTargetId::new();

let registration = runtime.register_keyframes(
    target,
    KeyframesBuilder::new()
        .with_timing(Timing::new(120.0))
        .at(0.0, (property::OPACITY, 0.0))
        .at(1.0, (property::OPACITY, 1.0))
        .finish(),
);

let handle = registration.handle();
```

Each runtime tick returns target-scoped snapshots plus completed handles.
Completed entries are removed automatically after their final output is emitted.

## Iced Subscription Wiring

Use `iced_ext::subscription` to produce ticks only while the runtime has active
animations. Use `iced_ext::update_tick` to advance the runtime from an Iced tick
message. The runtime tick interval comes from `TickPolicy`.

```rust
use std::time::Instant;

use aura_anim_iced::{AnimationRuntime, iced_ext};

#[derive(Debug, Clone)]
enum Message {
    AnimationTick(Instant),
}

fn subscription(runtime: &AnimationRuntime) -> iced::Subscription<Message> {
    iced_ext::subscription(runtime, Message::AnimationTick)
}
```

For view code, convert tick output with `tick_effect_snapshot_for` when using
the built-in effect fields, or read `AnimationTick::properties_for` directly
when the application owns custom property specs.

## Property Change Animation

Use `PropertyTransition` when an application value should animate whenever its
target changes. The first observed value seeds the stable baseline and does not
start an animation. Later different values register keyframes from the current
visual result to the new target.

`BehaviorRule` stores reusable property and timing settings. Bind it to one or
more targets to create independent transition trackers.

```rust
use aura_anim_iced::{
    AnimationRuntime, AnimationTargetId, BehaviorRule, Easing, PropertyTransition,
    Timing, WIDTH,
};

struct Panel {
    runtime: AnimationRuntime,
    target: AnimationTargetId,
    width: PropertyTransition<aura_anim_iced::property::Scalar>,
    rendered_width: f32,
}

impl Panel {
    fn new() -> Self {
        let mut runtime = AnimationRuntime::new();
        let target = AnimationTargetId::new();
        let rule = BehaviorRule::new(WIDTH)
            .with_timing(Timing::new(180.0).with_easing(Easing::EaseOut));
        let mut width = rule.bind(target);

        width.transition_to(&mut runtime, 240.0);

        Self {
            runtime,
            target,
            width,
            rendered_width: 240.0,
        }
    }

    fn set_width(&mut self, next_width: f32) {
        self.width.transition_from_visual(
            &mut self.runtime,
            self.rendered_width,
            next_width,
        );
    }
}
```

On each animation tick, merge the target snapshot into the value used by `view`
and let the transition clear its active handle when the runtime finishes:

```rust
use aura_anim_iced::{PropertyValue, WIDTH, iced_ext};

fn update_tick(panel: &mut Panel, tick: std::time::Instant) {
    let output = iced_ext::update_tick(&mut panel.runtime, tick);

    if let Some(snapshot) = output.properties_for(panel.target)
        && let Some(entry) = snapshot.find_property(&WIDTH.raw())
        && let PropertyValue::Scalar(width) = entry.value()
    {
        panel.rendered_width = *width;
    }

    panel.width.handle_completion(&panel.runtime);
}
```

The `examples/behavior_width.rs` example shows the same flow in a runnable Iced
application with controls for repeated value changes.

## State-Driven Animation

Use `StateAnimator` when the application has a small state machine and each
state pair should launch a specific timeline. A `StateTransitionSet` stores the
known pairs and can also provide a fallback timeline for unlisted changes.

```rust
use aura_anim_iced::{
    AnimationRuntime, AnimationTargetId, Duration, OPACITY, StateAnimator,
    StateTransition, StateTransitionSet, Timeline, Track,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PanelState {
    Closed,
    Open,
    Disabled,
}

fn opacity_timeline(from: f32, to: f32, ms: f64) -> Timeline {
    Timeline::track(Track::from(OPACITY, from).to(to).duration(Duration::from_millis(ms)))
}

let mut runtime = AnimationRuntime::new();
let target = AnimationTargetId::new();
let mut animator = StateAnimator::new(target, PanelState::Closed);
let transitions = StateTransitionSet::from_transitions([
    StateTransition::new(
        PanelState::Closed,
        PanelState::Open,
        opacity_timeline(0.0, 1.0, 160.0),
    ),
    StateTransition::new(
        PanelState::Open,
        PanelState::Closed,
        opacity_timeline(1.0, 0.0, 120.0),
    ),
])
.with_fallback(opacity_timeline(0.4, 1.0, 100.0));

let registration = animator.transition_to(&mut runtime, PanelState::Open, &transitions);
assert!(registration.is_some());
```

`StateAnimator::current` is updated as soon as a transition starts, while
`active_transition` and `active_progress_at` expose runtime metadata for loading
indicators, navigation locks, or diagnostics. Call `handle_completion` after
ticks when application code needs the cached active transition to match the
runtime exactly.

## Retargeting And Interruption

Retargeting is for active animations that receive a new destination. The
replacement starts from the active animation's last sampled visual value, not
from the previous target.

```rust
let mut runtime = AnimationRuntime::new();
let target = AnimationTargetId::new();
let mut opacity = PropertyTransition::new(target, aura_anim_iced::OPACITY)
    .with_timing(Timing::new(200.0));

opacity.transition_to(&mut runtime, 0.0);
opacity.transition_to(&mut runtime, 1.0);

// After one or more ticks, continue from the rendered value to the new target.
let retargeted = opacity.retarget_to(&mut runtime, 0.35);
```

Interruption is for cases where application code already knows the rendered
value, such as drag cancellation or repeated user input. It can replace an
active animation even when the destination has not changed.

```rust
let visual_opacity = 0.42;
let interrupted = opacity.interrupt_from_visual(&mut runtime, visual_opacity, 1.0);
```

Both paths cancel the superseded runtime handle after registering the
replacement. That prevents interrupted animations from later reporting
completion or overriding the replacement output.

## Route Transition Guide

Use route transitions when changing screens should animate the leaving and
entering views independently.

1. Store a `RouteAnimator<Route>` in application state.
2. Give the outgoing and incoming screen layers separate `AnimationTargetId`
   values.
3. Build a `RouteScreenTransition` from an outgoing timeline and an incoming
   timeline or `RouteIncomingMotion`.
4. Register it with `transition_screens_with`.
5. On ticks, merge snapshots for both screen targets into the effects used by
   `view`.
6. When the route or incoming handle completes, clear temporary leaving-screen
   state.

```rust
use aura_anim_iced::{
    AnimationRuntime, AnimationTargetId, Duration, OPACITY, RouteAnimator,
    RouteIncomingMotion, RouteScreenTargets, RouteScreenTransition, Timeline, Track,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Route {
    Home,
    Reports,
}

fn outgoing() -> Timeline {
    Timeline::track(Track::from(OPACITY, 1.0).to(0.0).duration(Duration::from_millis(180.0)))
}

let mut runtime = AnimationRuntime::new();
let route_target = AnimationTargetId::new();
let outgoing_target = AnimationTargetId::new();
let incoming_target = AnimationTargetId::new();
let mut animator = RouteAnimator::new(route_target, Route::Home);

let transition = RouteScreenTransition::with_incoming_motion(
    Route::Home,
    Route::Reports,
    outgoing(),
    RouteIncomingMotion::new(
        iced::Vector::new(48.0, 0.0),
        Duration::from_millis(220.0),
    ),
);

let registration = animator.transition_screens_with(
    &mut runtime,
    &transition,
    RouteScreenTargets::new(outgoing_target, incoming_target),
);

assert!(registration.is_some());
```

`RouteIncomingMotion` builds an incoming timeline that fades from `0.0` to `1.0`
and translates from the supplied offset to `iced::Vector::new(0.0, 0.0)`.
Repeated navigation replaces the active route, outgoing, and incoming handles
as a group, so stale screen animations are canceled together.

The `examples/route_transition.rs` example shows a complete Iced flow with
navigation buttons, overlaid screen cards, snapshot merging, and cleanup after
the incoming screen reaches its final state.