Skip to main content

animated_ui/
animated_ui.rs

1//! Shows how to use animation clips to animate UI properties.
2
3use bevy::{
4    animation::{
5        animated_field, AnimatedBy, AnimationEntityMut, AnimationEvaluationError, AnimationTargetId,
6    },
7    prelude::*,
8};
9
10use core::any::TypeId;
11use core::f32::consts::TAU;
12
13// Holds information about the animation we programmatically create.
14struct AnimationInfo {
15    // The name of the animation target (in this case, the text).
16    target_name: Name,
17    // The ID of the animation target, derived from the name.
18    target_id: AnimationTargetId,
19    // The animation graph asset.
20    graph: Handle<AnimationGraph>,
21    // The index of the node within that graph.
22    node_index: AnimationNodeIndex,
23}
24
25// The entry point.
26fn main() {
27    App::new()
28        .add_plugins(DefaultPlugins)
29        // Note that we don't need any systems other than the setup system,
30        // because Bevy automatically updates animations every frame.
31        .add_systems(Startup, setup)
32        .run();
33}
34
35impl AnimationInfo {
36    // Programmatically creates the UI animation.
37    fn create(
38        animation_graphs: &mut Assets<AnimationGraph>,
39        animation_clips: &mut Assets<AnimationClip>,
40    ) -> AnimationInfo {
41        // Create an ID that identifies the text node we're going to animate.
42        let animation_target_name = Name::new("Text");
43        let animation_target_id = AnimationTargetId::from_name(&animation_target_name);
44
45        // Allocate an animation clip.
46        let mut animation_clip = AnimationClip::default();
47
48        // Create a curve that animates `UiTransform::scale`.
49        animation_clip.add_curve_to_target(
50            animation_target_id,
51            AnimatableCurve::new(
52                animated_field!(UiTransform::scale),
53                AnimatableKeyframeCurve::new(
54                    [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
55                        .into_iter()
56                        .zip([0.3, 1.0, 0.3, 1.0, 0.3, 1.0, 0.3].map(Vec2::splat)),
57                )
58                .expect(
59                    "should be able to build translation curve because we pass in valid samples",
60                ),
61            ),
62        );
63
64        // Create a curve that animates font color. Note that this should have
65        // the same time duration as the previous curve.
66        //
67        // This time we use a "custom property", which in this case animates TextColor under the assumption
68        // that it is in the "srgba" format.
69        animation_clip.add_curve_to_target(
70            animation_target_id,
71            AnimatableCurve::new(
72                TextColorProperty,
73                AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0].into_iter().zip([
74                    Srgba::RED,
75                    Srgba::GREEN,
76                    Srgba::BLUE,
77                    Srgba::RED,
78                ]))
79                .expect(
80                    "should be able to build translation curve because we pass in valid samples",
81                ),
82            ),
83        );
84
85        // Create a curve that animates `UiTransform::rotation`.
86        //
87        // This animates the 2D rotation of the UI element using `Rot2`.
88        // Like other `Animatable` types, it uses shortest-path interpolation (slerp)
89        // to ensure smooth movement between keyframes.
90        animation_clip.add_curve_to_target(
91            animation_target_id,
92            AnimatableCurve::new(
93                animated_field!(UiTransform::rotation),
94                AnimatableKeyframeCurve::new(
95                    [0.0, 1.0, 2.0, 3.0]
96                        .into_iter()
97                        .zip([0., TAU / 3., TAU / 1.5, TAU].map(Rot2::radians)),
98                )
99                .expect("should be able to build rotation curve because we pass in valid samples"),
100            ),
101        );
102
103        // Save our animation clip as an asset.
104        let animation_clip_handle = animation_clips.add(animation_clip);
105
106        // Create an animation graph with that clip.
107        let (animation_graph, animation_node_index) =
108            AnimationGraph::from_clip(animation_clip_handle);
109        let animation_graph_handle = animation_graphs.add(animation_graph);
110
111        AnimationInfo {
112            target_name: animation_target_name,
113            target_id: animation_target_id,
114            graph: animation_graph_handle,
115            node_index: animation_node_index,
116        }
117    }
118}
119
120// Creates all the entities in the scene.
121fn setup(
122    mut commands: Commands,
123    asset_server: Res<AssetServer>,
124    mut animation_graphs: ResMut<Assets<AnimationGraph>>,
125    mut animation_clips: ResMut<Assets<AnimationClip>>,
126) {
127    // Create the animation.
128    let AnimationInfo {
129        target_name: animation_target_name,
130        target_id: animation_target_id,
131        graph: animation_graph,
132        node_index: animation_node_index,
133    } = AnimationInfo::create(animation_graphs.as_mut(), animation_clips.as_mut());
134
135    // Build an animation player that automatically plays the UI animation.
136    let mut animation_player = AnimationPlayer::default();
137    animation_player.play(animation_node_index).repeat();
138
139    // Add a camera.
140    commands.spawn(Camera2d);
141
142    // Build the UI. We have a parent node that covers the whole screen and
143    // contains the `AnimationPlayer`, as well as a child node that contains the
144    // text to be animated.
145    let mut entity = commands.spawn((
146        // Cover the whole screen, and center contents.
147        Node {
148            position_type: PositionType::Absolute,
149            top: px(0),
150            left: px(0),
151            right: px(0),
152            bottom: px(0),
153            justify_content: JustifyContent::Center,
154            align_items: AlignItems::Center,
155            ..default()
156        },
157        animation_player,
158        AnimationGraphHandle(animation_graph),
159    ));
160
161    let player = entity.id();
162    entity.with_child((
163        Text::new("Bevy"),
164        TextFont {
165            font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
166            font_size: FontSize::Px(80.),
167            ..default()
168        },
169        TextColor(Color::Srgba(Srgba::RED)),
170        TextLayout::justify(Justify::Center),
171        animation_target_id,
172        AnimatedBy(player),
173        animation_target_name,
174    ));
175}
176
177// A type that represents the color of the first text section.
178//
179// We implement `AnimatableProperty` on this to define custom property accessor logic
180#[derive(Clone)]
181struct TextColorProperty;
182
183impl AnimatableProperty for TextColorProperty {
184    type Property = Srgba;
185
186    fn evaluator_id(&self) -> EvaluatorId<'_> {
187        EvaluatorId::Type(TypeId::of::<Self>())
188    }
189
190    fn get_mut<'a>(
191        &self,
192        entity: &'a mut AnimationEntityMut,
193    ) -> Result<&'a mut Self::Property, AnimationEvaluationError> {
194        let text_color = entity
195            .get_mut::<TextColor>()
196            .ok_or(AnimationEvaluationError::ComponentNotPresent(TypeId::of::<
197                TextColor,
198            >(
199            )))?
200            .into_inner();
201        match text_color.0 {
202            Color::Srgba(ref mut color) => Ok(color),
203            _ => Err(AnimationEvaluationError::PropertyNotPresent(TypeId::of::<
204                Srgba,
205            >(
206            ))),
207        }
208    }
209}