6 releases

new 0.2.0 Jan 1, 2025
0.1.4 Oct 18, 2024
0.1.2 Sep 29, 2024

#148 in GUI

Download history 334/week @ 2024-09-20 268/week @ 2024-09-27 34/week @ 2024-10-04 126/week @ 2024-10-11 209/week @ 2024-10-18 7/week @ 2024-10-25 8/week @ 2024-11-01 9/week @ 2024-11-08 5/week @ 2024-11-15 10/week @ 2024-11-22 12/week @ 2024-11-29 17/week @ 2024-12-06 15/week @ 2024-12-13 1/week @ 2024-12-20 33/week @ 2024-12-27

66 downloads per month

MIT license

160KB
3K SLoC

Iced Animations

This package is designed to make it easy to animate between values using the Iced framework.

Overview

Animations are powered by the Animate trait. You can pick between spring or transition animations. Controlled animations using the Animation widget have a few parts:

  • Storing an Animated<T> value in your app state
  • Including an update message, e.g. Message::UpdateSize(iced_anim::Event<T>)
  • Piping animation events to your animated value
  • Wrapping the UI you want to animate in an Animation widget

Let's follow an example where you have a size in your app state and want to animate changes to it:

use iced_anim::{Animated, Event};

#[derive(Default)]
struct State {
    // Your animated value, wrapped in an `Animated` type.
    size: Animated<f32>,
}

#[derive(Clone)]
enum Message {
    // A message that lets you (and animation widgets) update the size.
    UpdateSize(Event<f32>),
}

```rust
impl State {
    fn update(&mut self, message: Message) {
        match message {
            // Let your animated value process the event
            Message::UpdateSize(event) => self.size.update(event),
        }
    }
}

Next, let's add a default impl using an ease transition that sets the initial size at 50px:

use iced_anim::transition::Easing;

impl Default for State {
    fn default() -> Self {
        Self {
            size: Animated::transition(50.0, Easing::EASE),
        }
    }
}

Or, if you're feeling spicy, you can use a spring animation:

use iced_anim::spring::Motion;

impl Default for State {
    fn default() -> Self {
        Self {
            size: Animated::spring(50.0, Motion::BOUNCY),
        }
    }
}

Next, you need to use your animated size in a view. You can use the Animation widget to make your UI animate when the size changes. You'll also want a way to change the size so you can see the animation, so let's add some buttons too.

use iced::{
    widget::{button, column, container, row, text},
    Color, Element, Length,
};
use iced_anim::{animation::animation, transition::Easing};

impl State {
    pub fn view(&self) -> Element<Message> {
        let buttons = row![
            button(text("-50")).on_press(Message::UpdateSize(Event::Target(self.size.target() - 50.0))),
            button(text(" 50")).on_press(Message::UpdateSize(Event::Target(self.size.target()   50.0))),
        ]
        .spacing(8);

        let animated_box = animation(
            &self.size,
            container(text(*self.size.value() as isize))
                .style(|_| container::background(Color::from_rgb(1.0, 0.0, 0.0)))
                .center(*self.size.value()),
        )
        .on_update(Message::UpdateSize);

        column![buttons, animated_box]
            .spacing(8)
            .padding(8)
            .width(Length::Shrink)
            .into()
    }
}

Or, if you don't want to type of Event::Target, you can replace it with .into() since the From impl for Event is the Event::Target case.

let buttons = row![
    button(text("-50")).on_press(Message::UpdateSize((self.size.target() - 50.0).into())),
    button(text(" 50")).on_press(Message::UpdateSize((self.size.target()   50.0).into())),
]
.spacing(8);

The Animation widget works by watching your animated size and sending updates through the message you pass to .on_update to trigger rebuilds. Anytime you set a new target value for your animation, the Animation widget will pick up the change and emit messages to update the animated value.

In the above example, you can set a new target by emitting the message we created, e.g. Message::UpdateSize(150.0.into()). If you need to access the current animated value or the target value (the value being animated towards), you can use the .value() and .target() functions respectively.

If you have a very simple value you'd like to animate but don't want to store in your app state, then consider the AnimationBuilder widget.

AnimationBuilder widget

The AnimationBuilder widget lets you have uncontrolled animations that takes some sort of value that implements Animate and a closure to build out a UI based on the current interpolated value. The benefit is that you don't have to emit messages for very simple animations but has a couple limitations mentioned below. Typical usage might look something like this, where self.size is an f32 in your app state:

AnimationBuilder::new(self.size, |size| {
    container(text(size as isize))
        .center(size)
        .into()
})
.animates_layout(true)

NOTE: .animates_layout(true) is what allows re-rendering certain properties like the app layout or text. This is off by default to avoid unnecessarily invalidating the app layout, but will be necessary in certain situations.

The UI will automatically re-render when self.size is changed. The closure provides the current animated value and will be called to generate the next view as appropriate. This means you don't need to store an Animated value in your app state or have a message dedicated to updating it. To see more for this particular example, refer to the animated_size example.

Limitations

It might not be easy/possible to pass in non-clonable content like custom elements to AnimationBuilder's closure to due the closure being having to be invoked multiple times to animate between values. Making reusable functions that use this widget and also take a generic element might be difficult.

Nested animations also don't work if both properties are actively being animated. One animation at a time will function correctly, but trying to adjust both at the same time leads to the inner property skipping to the final value. Use the Animation widget if you need any of these properties.

Should I use Animation or AnimationBuilder?

Generally, if you're animating a tiny value that might not be directly within your state and the element won't contain any nested animated values, then use AnimationBuilder. Otherwise, use the state-driven Animation to avoid the limitations of widget-driven animations. Also, use Animation anytime you want your state to contain the animated value.

Should I use a Spring or a Transition?

It's largely personal preference, but springs are typically better at interactive animations because they're momentum-based and keep their existing speed even when the target changes. This makes animations whose target value is always changing to appear much more fluid. See the animated_bubble example for a constantly-changing animation target.

If your animation isn't going to be constantly changing or it's something minor like a color, then you can use a transition with an easing curve.

Animated widgets

A subset of the standard iced widgets are exported under a widgets feature flag. You can use these as drop-in replacements for the existing widgets and control the animation mode via the .animation() builder function on them:

use iced::widget::text;
use iced_anim::widget::button;

let my_button = button(text("Animated button"))
    .on_press(Message::DoSomething);

Types that implement Animate

Several types implement Animate by default, such as f32, iced::Color, and iced::Theme, with support for others being added in the future. You can also derive Animate on your own structs if you enable the derive feature and all inner properties already implement Animate:

use iced_anim::Animate;

#[derive(Animate)]
struct CustomRectangle {
    width: f32,
    height: f32,
    color: iced::Color,
}

You can also animate multiple values at once by providing a tuple up to a length of four:

AnimationBuilder::new((self.size, self.color), |(size, color)| {
    container(text(size as isize).color(color))
        .center(size)
        .into()
})
.animates_layout(true)

Controlling the animation

You can customize the animation by passing in different values to the .animation function. You can pass anything that can be converted to an animated::Mode, namely a spring::Motion or transition::Easing. There are defaults like Easing::EASE_OUT and Motion::BOUNCY, but you can create your own as well.

use std::time::Duration;
use iced::widget::{container, text};
use iced_anim::{AnimationBuilder, spring::Motion};

AnimationBuilder::new(self.size, |size| {
    container(text(size as isize))
        .center(size)
        .into()
})
.animates_layout(true)
.animation(Motion { 
    damping: 0.5,
    response: Duration::from_millis(500),
})

Examples

Refer to the examples directory for a variety of ways to use this crate. You can also run these examples locally with cargo run --example <package>, e.g. cargo run --example animated_color.

Breaking Changes

0.1 -> 0.2

  • Introduced the Animated API, which encompasses both springs and transitions
  • Animation widget now takes an Animated value instead of a Spring
  • Renamed SpringMotion to Motion under the iced_anim::spring module
  • Renamed SpringEvent to Event and added a new SettleAt(T) case
  • Moved AnimatedState to the top-level module instead of the widget module
  • Replaced .motion() functions with .animation() functions that take a new iced_anim::animated::Mode that allows configuring springs or transitions
  • Add lerp function to Animate trait

Dependencies

~38–74MB
~1.5M SLoC