• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_tweening / 9845293287

08 Jul 2024 06:52PM UTC coverage: 92.536% (-0.2%) from 92.723%
9845293287

push

github

web-flow
Upgrade to Bevy 0.14 (#130)

30 of 31 new or added lines in 2 files covered. (96.77%)

16 existing lines in 3 files now uncovered.

1587 of 1715 relevant lines covered (92.54%)

1.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.42
/src/plugin.rs
1
use bevy::prelude::*;
2

3
#[cfg(feature = "bevy_asset")]
4
use crate::{tweenable::AssetTarget, AssetAnimator};
5
use crate::{tweenable::ComponentTarget, Animator, AnimatorState, TweenCompleted};
6

7
/// Plugin to add systems related to tweening of common components and assets.
8
///
9
/// This plugin adds systems for a predefined set of components and assets, to
10
/// allow their respective animators to be updated each frame:
11
/// - [`Transform`]
12
/// - [`Text`]
13
/// - [`Style`]
14
/// - [`Sprite`]
15
/// - [`ColorMaterial`]
16
///
17
/// This ensures that all predefined lenses work as intended, as well as any
18
/// custom lens animating the same component or asset type.
19
///
20
/// For other components and assets, including custom ones, the relevant system
21
/// needs to be added manually by the application:
22
/// - For components, add [`component_animator_system::<T>`] where `T:
23
///   Component`
24
/// - For assets, add [`asset_animator_system::<T>`] where `T: Asset`
25
///
26
/// This plugin is entirely optional. If you want more control, you can instead
27
/// add manually the relevant systems for the exact set of components and assets
28
/// actually animated.
29
///
30
/// [`Transform`]: https://docs.rs/bevy/0.12.0/bevy/transform/components/struct.Transform.html
31
/// [`Text`]: https://docs.rs/bevy/0.12.0/bevy/text/struct.Text.html
32
/// [`Style`]: https://docs.rs/bevy/0.12.0/bevy/ui/struct.Style.html
33
/// [`Sprite`]: https://docs.rs/bevy/0.12.0/bevy/sprite/struct.Sprite.html
34
/// [`ColorMaterial`]: https://docs.rs/bevy/0.12.0/bevy/sprite/struct.ColorMaterial.html
35
#[derive(Debug, Clone, Copy)]
36
pub struct TweeningPlugin;
37

38
impl Plugin for TweeningPlugin {
39
    fn build(&self, app: &mut App) {
×
40
        app.add_event::<TweenCompleted>().add_systems(
×
41
            Update,
42
            component_animator_system::<Transform>.in_set(AnimationSystem::AnimationUpdate),
×
43
        );
44

45
        #[cfg(feature = "bevy_ui")]
46
        app.add_systems(
×
47
            Update,
48
            component_animator_system::<Style>.in_set(AnimationSystem::AnimationUpdate),
×
49
        );
50
        #[cfg(feature = "bevy_ui")]
51
        app.add_systems(
×
52
            Update,
53
            component_animator_system::<BackgroundColor>.in_set(AnimationSystem::AnimationUpdate),
×
54
        );
55

56
        #[cfg(feature = "bevy_sprite")]
57
        app.add_systems(
×
58
            Update,
59
            component_animator_system::<Sprite>.in_set(AnimationSystem::AnimationUpdate),
×
60
        );
61

62
        #[cfg(all(feature = "bevy_sprite", feature = "bevy_asset"))]
63
        app.add_systems(
×
64
            Update,
65
            asset_animator_system::<ColorMaterial>.in_set(AnimationSystem::AnimationUpdate),
×
66
        );
67

68
        #[cfg(feature = "bevy_text")]
69
        app.add_systems(
×
70
            Update,
71
            component_animator_system::<Text>.in_set(AnimationSystem::AnimationUpdate),
×
72
        );
73
    }
74
}
75

76
/// Label enum for the systems relating to animations
77
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, SystemSet)]
78
pub enum AnimationSystem {
79
    /// Ticks animations
80
    AnimationUpdate,
81
}
82

83
/// Animator system for components.
84
///
85
/// This system extracts all components of type `T` with an [`Animator<T>`]
86
/// attached to the same entity, and tick the animator to animate the component.
87
pub fn component_animator_system<T: Component>(
2✔
88
    time: Res<Time>,
89
    mut query: Query<(Entity, &mut T, &mut Animator<T>)>,
90
    events: ResMut<Events<TweenCompleted>>,
91
    mut commands: Commands,
92
) {
93
    let mut events: Mut<Events<TweenCompleted>> = events.into();
2✔
94
    for (entity, target, mut animator) in query.iter_mut() {
6✔
95
        if animator.state != AnimatorState::Paused {
2✔
96
            let speed = animator.speed();
2✔
97
            let mut target = ComponentTarget::new(target);
2✔
98
            animator.tweenable_mut().tick(
4✔
99
                time.delta().mul_f32(speed),
2✔
100
                &mut target,
×
101
                entity,
×
102
                &mut events,
×
103
                &mut commands,
×
104
            );
105
        }
106
    }
107
}
108

109
/// Animator system for assets.
110
///
111
/// This system ticks all [`AssetAnimator<T>`] components to animate their
112
/// associated asset.
113
///
114
/// This requires the `bevy_asset` feature (enabled by default).
115
#[cfg(feature = "bevy_asset")]
116
pub fn asset_animator_system<T: Asset>(
×
117
    time: Res<Time>,
118
    mut assets: ResMut<Assets<T>>,
119
    mut query: Query<(Entity, &Handle<T>, &mut AssetAnimator<T>)>,
120
    events: ResMut<Events<TweenCompleted>>,
121
    mut commands: Commands,
122
) {
123
    let mut events: Mut<Events<TweenCompleted>> = events.into();
×
124
    let mut target = AssetTarget::new(assets.reborrow());
×
125
    for (entity, handle, mut animator) in query.iter_mut() {
×
126
        if animator.state != AnimatorState::Paused {
×
127
            target.handle = handle.clone();
×
128
            if !target.is_valid() {
×
129
                continue;
×
130
            }
131
            let speed = animator.speed();
×
132
            animator.tweenable_mut().tick(
×
133
                time.delta().mul_f32(speed),
×
134
                &mut target,
×
135
                entity,
×
136
                &mut events,
×
137
                &mut commands,
×
138
            );
139
        }
140
    }
141
}
142

143
#[cfg(test)]
144
mod tests {
145
    use std::{
146
        marker::PhantomData,
147
        ops::DerefMut,
148
        sync::{
149
            atomic::{AtomicBool, Ordering},
150
            Arc,
151
        },
152
    };
153

154
    use crate::{lens::TransformPositionLens, *};
155

156
    /// A simple isolated test environment with a [`World`] and a single
157
    /// [`Entity`] in it.
158
    struct TestEnv<T: Component> {
159
        world: World,
160
        entity: Entity,
161
        _phantom: PhantomData<T>,
162
    }
163

UNCOV
164
    impl<T: Component + Default> TestEnv<T> {
×
165
        /// Create a new test environment containing a single entity with a
166
        /// [`Transform`], and add the given animator on that same entity.
167
        pub fn new(animator: Animator<T>) -> Self {
2✔
168
            let mut world = World::new();
2✔
169
            world.init_resource::<Events<TweenCompleted>>();
2✔
170
            world.init_resource::<Time>();
2✔
171

172
            let entity = world.spawn((T::default(), animator)).id();
2✔
173

174
            Self {
175
                world,
176
                entity,
177
                _phantom: PhantomData,
178
            }
179
        }
180
    }
181

UNCOV
182
    impl<T: Component> TestEnv<T> {
×
183
        /// Get the test world.
184
        pub fn world_mut(&mut self) -> &mut World {
2✔
185
            &mut self.world
×
186
        }
187

188
        /// Tick the test environment, updating the simulation time and ticking
189
        /// the given system.
190
        pub fn tick(&mut self, duration: Duration, system: &mut dyn System<In = (), Out = ()>) {
2✔
191
            // Simulate time passing by updating the simulation time resource
192
            {
193
                let mut time = self.world.resource_mut::<Time>();
2✔
194
                time.advance_by(duration);
2✔
195
            }
196

197
            // Reset world-related change detection
198
            self.world.clear_trackers();
2✔
199
            assert!(!self.component_mut().is_changed());
2✔
200

201
            // Tick system
202
            system.run((), &mut self.world);
2✔
203

204
            // Update events after system ticked, in case system emitted some events
205
            let mut events = self.world.resource_mut::<Events<TweenCompleted>>();
2✔
206
            events.update();
2✔
207
        }
208

209
        /// Get the animator for the component.
210
        pub fn animator(&self) -> &Animator<T> {
2✔
211
            self.world.entity(self.entity).get::<Animator<T>>().unwrap()
2✔
212
        }
213

214
        /// Get the component.
215
        pub fn component_mut(&mut self) -> Mut<T> {
2✔
216
            self.world.get_mut::<T>(self.entity).unwrap()
2✔
217
        }
218

219
        /// Get the emitted event count since last tick.
220
        pub fn event_count(&self) -> usize {
1✔
221
            let events = self.world.resource::<Events<TweenCompleted>>();
1✔
222
            events.get_reader().len(events)
1✔
223
        }
224
    }
225

226
    #[test]
227
    fn change_detect_component() {
3✔
228
        let tween = Tween::new(
229
            EaseMethod::Linear,
1✔
230
            Duration::from_secs(1),
1✔
231
            TransformPositionLens {
1✔
232
                start: Vec3::ZERO,
233
                end: Vec3::ONE,
234
            },
235
        )
236
        .with_completed_event(0);
237

238
        let mut env = TestEnv::new(Animator::new(tween));
1✔
239

240
        // After being inserted, components are always considered changed
241
        let transform = env.component_mut();
1✔
242
        assert!(transform.is_changed());
1✔
243

244
        // fn nit() {}
245
        // let mut system = IntoSystem::into_system(nit);
246
        let mut system = IntoSystem::into_system(component_animator_system::<Transform>);
1✔
247
        system.initialize(env.world_mut());
2✔
248

249
        env.tick(Duration::ZERO, &mut system);
1✔
250

251
        let animator = env.animator();
1✔
252
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
253
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
254
        let transform = env.component_mut();
1✔
255
        assert!(transform.is_changed());
1✔
256
        assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
1✔
257

258
        env.tick(Duration::from_millis(500), &mut system);
1✔
259

260
        assert_eq!(env.event_count(), 0);
1✔
261
        let animator = env.animator();
1✔
262
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
263
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
264
        let transform = env.component_mut();
1✔
265
        assert!(transform.is_changed());
1✔
266
        assert!(transform.translation.abs_diff_eq(Vec3::splat(0.5), 1e-5));
1✔
267

268
        env.tick(Duration::from_millis(500), &mut system);
1✔
269

270
        assert_eq!(env.event_count(), 1);
1✔
271
        let animator = env.animator();
1✔
272
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
273
        assert_eq!(animator.tweenable().times_completed(), 1);
1✔
274
        let transform = env.component_mut();
1✔
275
        assert!(transform.is_changed());
1✔
276
        assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
1✔
277

278
        env.tick(Duration::from_millis(100), &mut system);
1✔
279

280
        assert_eq!(env.event_count(), 0);
1✔
281
        let animator = env.animator();
1✔
282
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
283
        assert_eq!(animator.tweenable().times_completed(), 1);
1✔
284
        let transform = env.component_mut();
1✔
285
        assert!(!transform.is_changed());
1✔
286
        assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
2✔
287
    }
288

289
    #[derive(Debug, Default, Clone, Copy, Component)]
290
    struct DummyComponent {
291
        value: f32,
292
    }
293

294
    /// Test [`Lens`] which only access mutably the target component if `defer`
295
    /// is `true`.
296
    struct ConditionalDeferLens {
297
        pub defer: Arc<AtomicBool>,
298
    }
299

300
    impl Lens<DummyComponent> for ConditionalDeferLens {
301
        fn lerp(&mut self, target: &mut dyn Targetable<DummyComponent>, ratio: f32) {
1✔
302
            if self.defer.load(Ordering::SeqCst) {
2✔
303
                target.deref_mut().value += ratio;
1✔
304
            }
305
        }
306
    }
307

308
    #[test]
309
    fn change_detect_component_conditional() {
3✔
310
        let defer = Arc::new(AtomicBool::new(false));
1✔
311
        let tween = Tween::new(
312
            EaseMethod::Linear,
1✔
313
            Duration::from_secs(1),
1✔
314
            ConditionalDeferLens {
1✔
315
                defer: Arc::clone(&defer),
1✔
316
            },
317
        )
318
        .with_completed_event(0);
319

320
        let mut env = TestEnv::new(Animator::new(tween));
1✔
321

322
        // After being inserted, components are always considered changed
323
        let component = env.component_mut();
1✔
324
        assert!(component.is_changed());
1✔
325

326
        let mut system = IntoSystem::into_system(component_animator_system::<DummyComponent>);
1✔
327
        system.initialize(env.world_mut());
2✔
328

329
        assert!(!defer.load(Ordering::SeqCst));
1✔
330

331
        // Mutation disabled
332
        env.tick(Duration::ZERO, &mut system);
1✔
333

334
        let animator = env.animator();
1✔
335
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
336
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
337
        let component = env.component_mut();
1✔
338
        assert!(!component.is_changed());
1✔
339
        assert!((component.value - 0.).abs() <= 1e-5);
2✔
340

341
        // Zero-length tick should not change the component
342
        env.tick(Duration::from_millis(0), &mut system);
1✔
343

344
        let animator = env.animator();
1✔
345
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
346
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
347
        let component = env.component_mut();
1✔
348
        assert!(!component.is_changed());
1✔
349
        assert!((component.value - 0.).abs() <= 1e-5);
2✔
350

351
        // New tick, but lens mutation still disabled
352
        env.tick(Duration::from_millis(200), &mut system);
1✔
353

354
        let animator = env.animator();
1✔
355
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
356
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
357
        let component = env.component_mut();
1✔
358
        assert!(!component.is_changed());
1✔
359
        assert!((component.value - 0.).abs() <= 1e-5);
2✔
360

361
        // Enable lens mutation
362
        defer.store(true, Ordering::SeqCst);
1✔
363

364
        // The current time is already at t=0.2s, so even if we don't increment it, for
365
        // a tween duration of 1s the ratio is t=0.2, so the lens will actually
366
        // increment the component's value.
367
        env.tick(Duration::from_millis(0), &mut system);
1✔
368

369
        let animator = env.animator();
1✔
370
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
371
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
372
        let component = env.component_mut();
1✔
373
        assert!(component.is_changed());
1✔
374
        assert!((component.value - 0.2).abs() <= 1e-5);
1✔
375

376
        // 0.2s + 0.3s = 0.5s
377
        // t = 0.5s / 1s = 0.5
378
        // value += 0.5
379
        // value == 0.7
380
        env.tick(Duration::from_millis(300), &mut system);
1✔
381

382
        let animator = env.animator();
1✔
383
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
384
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
385
        let component = env.component_mut();
1✔
386
        assert!(component.is_changed());
1✔
387
        assert!((component.value - 0.7).abs() <= 1e-5);
1✔
388
    }
389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc