aura-anim
Typed animation primitives and Iced integration for Rust desktop interfaces.
The application stores lightweight Motion<T> handles while MotionRuntime
owns and advances the actual animation sources. Animated values remain ordinary
Rust structs, and #[derive(Animatable)] generates field-by-field
interpolation.
Application
├── explicit UI state
├── Motion<T> handles
└── event-driven transition_to / play calls
MotionRuntime
├── owns type-erased animation slots
├── ticks active slots only
├── per-motion and batch pause / resume / seek / cancel / finish
├── generation-checked handle reuse
└── completion compaction and optional auto-removal
Animation<T>
├── Tween<T>
├── Spring<T>
├── Keyframes<T>
├── Sequence<T>
├── Parallel<T>
└── Hold<T>
Workspace Crates
aura-anim-core: runtime, handles, interpolation, animation sources and timeline composition.aura-anim-iced: Iced value integration and frame subscriptions.aura-anim: convenience facade re-exporting core and Iced APIs.aura-anim-macros:Animatablederive implementation.
Installation
For Iced applications:
[]
= "0.3.0"
= "0.14"
Use aura-anim-core directly when no Iced integration is required.
Typed Motion
use *;
let mut runtime = new;
let button = runtime.motion_with;
button.transition_to
.unwrap;
runtime.tick;
let visual = button.value.unwrap;
transition_to retargets from the currently sampled value, so interrupted
hover, press, menu and route animations do not jump back to a stale origin.
Motion access and mutation return Result<_, MotionError> so removed, stale,
out-of-bounds, and type-mismatched handles remain distinguishable.
Timing::linear, Timing::ease_in, Timing::ease_out, and
Timing::ease_in_out cover the common duration/easing combinations while
remaining normal Timing values that can be extended with delay, iterations,
or direction.
Use a deferred target factory when replacing the current animation:
button.play?;
# Ok::
tween_to and spring_to sample the motion's current value when playback
starts, so callers do not need to read or clone it manually.
Runtime-wide commands are available for application lifecycle and accessibility policies:
runtime.command_all;
runtime.command_all;
runtime.command_all;
command_all applies to every stored motion, including paused and idle
motions. Completion, cancellation, and DropWhenSettled removal events use the
same semantics as commands sent through an individual Motion<T>.
Independent Field Animations
#[derive(Animatable)] also generates typed field descriptors. A struct can
remain one Motion<T> while each selected field uses a different animation:
let mut runtime = new;
let position = runtime.motion;
position.play?;
# Ok::
Target factories receive the field's current sampled value when play is
called. Custom |from| ... factories remain supported. Interrupted field
animations therefore continue from the visible value, while fields not
included in the plan retain their current values.
For a named struct, the derive generates PositionFields::x,
PositionFields::y, and equivalent field!(Position::x) descriptors. Tuple
struct descriptors use _0, _1, and so on, while field!(Offset::0) uses
the tuple index directly. A generated descriptor type can be renamed when
necessary:
Animation Events
MotionRuntime queues structured lifecycle events when playbacks complete,
cancel, are interrupted, or leave runtime storage. Events are emitted once per
state transition and remain queued until the application takes or clears them:
frame;
for event in runtime.take_events
Matching a Motion<T> is sufficient for simple cleanup. Multi-stage flows
should track the exact playback so an older queued event cannot complete a
newer animation on the same motion:
let exit = motion.play_tracked?;
// In a later frame update:
for event in runtime.take_events
# Ok::
play_tracked and transition_to_tracked return a PlaybackId. Existing
play and transition_to calls remain unchanged when playback identity is not
needed.
Event kinds include:
CompletedCanceledInterrupted(Replaced | Retargeted | Removed)Removed(Explicit | Settled)
DropWhenSettled motions emit their terminal event before their removal event,
so completion remains observable after the handle becomes invalid.
Presence::handle_event uses the current exit playback ID to avoid stale exit
events unmounting content that has already been shown again:
for event in runtime.take_events
Presence::sync remains available for polling-based integrations.
For boolean application state, set_visible avoids restarting an animation
when the requested target is unchanged:
menu.set_visible?;
menu.toggle?;
# Ok::
Both methods return whether a new transition was started. Explicit
show/hide and custom show_with/hide_with calls remain available when a
transition should be replayed intentionally.
Iced Integration
Store the runtime and typed handles in application state:
use Instant;
use *;
use ;
When no animation is active, the subscription returns Subscription::none()
and does not continue waking the application.
TickPolicy supports:
Frames
fps
interval
Motion Binding
MotionBinding<S, T> maps reusable business states to visual targets and
transition factories. The binding is immutable configuration; each button,
menu item, or route owns a small MotionBindingState<S> that records its last
successfully applied state.
let button_binding = new
.when
.when
.transition
.transition
.fallback;
let motion = runtime.motion;
let mut binding_state = button_binding.state;
let playback = button_binding
.set_state_tracked?
.expect;
// In a later update, after the runtime has been ticked:
for event in runtime.take_events
On each state change the binding resolves the target, samples the motion's
current value, selects the exact transition or fallback factory, constructs
the animation, calls motion.play(...), and records the new business state
only after playback succeeds. Factories can return concrete Tween, Spring,
Keyframes, Timeline, or any custom Animation<T>; the binding handles type
erasure internally.
set_state returns whether a transition was started.
set_state_tracked returns None for an unchanged state or the exact
PlaybackId for a newly started transition, allowing completion and
interruption events to be matched without polling the motion.
One binding configuration can be cloned or shared and reused with independent
MotionBindingState values.
Iced Animatable Types
With the core iced integration enabled, these types can be fields in an
Animatable struct:
iced::Vector<T>iced::Point<T>iced::Size<T>iced::Rectangle<T>iced::Paddingiced::border::Radius
The active rgba or oklaba color feature additionally enables:
iced::Coloriced::Shadowiced::Border
Color Interpolation
RGBA component interpolation is enabled by default:
= "0.3.0"
For Oklab RGB interpolation with independently interpolated alpha:
= {
version = "0.3.0",
= false,
= ["oklaba"]
}
rgba and oklaba are mutually exclusive. Oklaba conversion follows:
Iced sRGB
→ palette sRGB
→ Oklab interpolation
→ display sRGB
Tracing
Enable the optional tracing feature to emit runtime diagnostics without
installing or configuring a subscriber inside the library:
= {
version = "0.3.0",
= ["tracing"]
}
The aura_anim::runtime, aura_anim::binding, and aura_anim::presence
targets report motion allocation and reuse, playback commands, lifecycle
changes, invalid handles, binding transition selection, and presence mounting.
Per-tick diagnostics use the TRACE level; lifecycle and error diagnostics use
DEBUG. Applications remain responsible for installing a compatible
tracing subscriber.
Animation Sources
Tween
motion.play;
Timing supports delay, easing, finite or infinite iterations, and playback
direction. Animation::rate directly scales stored durations: 2.0 halves
duration and 0.5 doubles it. It recursively updates existing Timeline
children, while Spring ignores rate because its motion is physics-based.
Keyframes
motion.play;
Spring
motion.play;
Spring interpolation may overshoot and can be retargeted while active.
For values whose fields need different physical responses, create independent spring channels and explicitly compose their outputs:
let movement = new;
let fade = new
.with_mass
.with_epsilon;
let spring = with_channels;
Each channel owns its own position, velocity and SpringConfig. Spring
advancement uses the analytic damped-oscillator solution, so long frame
intervals are fully consumed instead of being truncated.
Timeline Composition
Sequence, Parallel and Hold all implement Animation<T>, so composition
is recursive:
Sequence(
Parallel(
Sequence(Hold, Tween),
Sequence(Tween, Tween),
),
Tween,
)
Parallel branches produce complete T values. A compositor explicitly selects
which fields each branch owns:
let parallel = new
.with
.with;
Sequence propagates unused frame time into following children. Parallel completes when its longest branch completes.
Concrete animations can start a sequence directly through AnimationExt:
let timeline = between
.delay
.then
.then;
delay inserts a Hold before the animation. Both combinators return the
existing Sequence<T> type, so lifecycle, seeking, rate changes, and overflow
propagation retain the same behavior as manually constructing a sequence.
Lifecycle
Normal motions retain their final value:
let motion = runtime.motion;
Completed sources are compacted to the final value, releasing keyframe and timeline trees while keeping the handle valid.
Transient animations can remove their slot automatically:
let transient = runtime.play_once;
Slots are reused with generation counters, preventing stale handles from accessing a newly allocated motion.
Examples
Run the command-line architecture example:
Run the Iced showcase:
Run the focused visual examples:
Run the interactive UI examples:
Run the showcase with perceptual color interpolation: