docs/bevy/examples/animation/animated_ui.rs 7.1 K · 209 lines · raw · history

1 //! Shows how to use animation clips to animate UI properties.
2
3 use bevy::{
4 animation::{
5 animated_field, AnimatedBy, AnimationEntityMut, AnimationEvaluationError, AnimationTargetId,
6 },
7 prelude::*,
8 };
9
10 use core::any::TypeId;
11 use core::f32::consts::TAU;
12
13 // Holds information about the animation we programmatically create.
14 struct 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.
26 fn 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
35 impl 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.
121 fn 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)]
181 struct TextColorProperty;
182
183 impl 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 }