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

djeedai / bevy_tweening / 12214786502

07 Dec 2024 05:27PM UTC coverage: 91.498% (-1.0%) from 92.536%
12214786502

push

github

web-flow
Bevy 0.15 support (#138)

41 of 47 new or added lines in 4 files covered. (87.23%)

20 existing lines in 2 files now uncovered.

1539 of 1682 relevant lines covered (91.5%)

1.45 hits per line

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

77.27
/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
/// - [`TextColor`]
13
/// - [`Node`]
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.15.0/bevy/transform/components/struct.Transform.html
31
/// [`TextColor`]: https://docs.rs/bevy/0.15.0/bevy/text/struct.TextColor.html
32
/// [`Node`]: https://docs.rs/bevy/0.15.0/bevy/ui/struct.Node.html
33
/// [`Sprite`]: https://docs.rs/bevy/0.15.0/bevy/sprite/struct.Sprite.html
34
/// [`ColorMaterial`]: https://docs.rs/bevy/0.15.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,
NEW
48
            component_animator_system::<Node>.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,
NEW
65
            asset_animator_system::<ColorMaterial, MeshMaterial2d<ColorMaterial>>
×
66
                .in_set(AnimationSystem::AnimationUpdate),
67
        );
68

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

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

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

110
#[cfg(feature = "bevy_asset")]
111
use std::ops::Deref;
112

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

UNCOV
150
#[cfg(test)]
×
151
mod tests {
152
    use std::{
153
        marker::PhantomData,
154
        ops::DerefMut,
155
        sync::{
156
            atomic::{AtomicBool, Ordering},
157
            Arc,
158
        },
159
    };
160

161
    use crate::{lens::TransformPositionLens, *};
162

163
    /// A simple isolated test environment with a [`World`] and a single
164
    /// [`Entity`] in it.
165
    struct TestEnv<T: Component> {
166
        world: World,
167
        entity: Entity,
168
        _phantom: PhantomData<T>,
169
    }
170

171
    impl<T: Component + Default> TestEnv<T> {
172
        /// Create a new test environment containing a single entity with a
173
        /// [`Transform`], and add the given animator on that same entity.
174
        pub fn new(animator: Animator<T>) -> Self {
2✔
175
            let mut world = World::new();
2✔
176
            world.init_resource::<Events<TweenCompleted>>();
2✔
177
            world.init_resource::<Time>();
2✔
178

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

181
            Self {
182
                world,
183
                entity,
184
                _phantom: PhantomData,
185
            }
186
        }
187
    }
188

189
    impl<T: Component> TestEnv<T> {
190
        /// Get the test world.
191
        pub fn world_mut(&mut self) -> &mut World {
2✔
192
            &mut self.world
×
193
        }
194

195
        /// Tick the test environment, updating the simulation time and ticking
196
        /// the given system.
197
        pub fn tick(&mut self, duration: Duration, system: &mut dyn System<In = (), Out = ()>) {
2✔
198
            // Simulate time passing by updating the simulation time resource
199
            {
200
                let mut time = self.world.resource_mut::<Time>();
2✔
201
                time.advance_by(duration);
2✔
202
            }
203

204
            // Reset world-related change detection
205
            self.world.clear_trackers();
2✔
206
            assert!(!self.component_mut().is_changed());
2✔
207

208
            // Tick system
209
            system.run((), &mut self.world);
2✔
210

211
            // Update events after system ticked, in case system emitted some events
212
            let mut events = self.world.resource_mut::<Events<TweenCompleted>>();
2✔
213
            events.update();
2✔
214
        }
215

216
        /// Get the animator for the component.
217
        pub fn animator(&self) -> &Animator<T> {
2✔
218
            self.world.entity(self.entity).get::<Animator<T>>().unwrap()
2✔
219
        }
220

221
        /// Get the component.
222
        pub fn component_mut(&mut self) -> Mut<T> {
2✔
223
            self.world.get_mut::<T>(self.entity).unwrap()
2✔
224
        }
225

226
        /// Get the emitted event count since last tick.
227
        pub fn event_count(&self) -> usize {
1✔
228
            let events = self.world.resource::<Events<TweenCompleted>>();
1✔
229
            events.get_cursor().len(events)
1✔
230
        }
231
    }
232

233
    #[test]
234
    fn change_detect_component() {
3✔
235
        let tween = Tween::new(
236
            EaseMethod::default(),
1✔
237
            Duration::from_secs(1),
1✔
238
            TransformPositionLens {
1✔
239
                start: Vec3::ZERO,
240
                end: Vec3::ONE,
241
            },
242
        )
243
        .with_completed_event(0);
244

245
        let mut env = TestEnv::new(Animator::new(tween));
1✔
246

247
        // After being inserted, components are always considered changed
248
        let transform = env.component_mut();
1✔
249
        assert!(transform.is_changed());
1✔
250

251
        // fn nit() {}
252
        // let mut system = IntoSystem::into_system(nit);
253
        let mut system = IntoSystem::into_system(component_animator_system::<Transform>);
1✔
254
        system.initialize(env.world_mut());
2✔
255

256
        env.tick(Duration::ZERO, &mut system);
1✔
257

258
        let animator = env.animator();
1✔
259
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
260
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
261
        let transform = env.component_mut();
1✔
262
        assert!(transform.is_changed());
1✔
263
        assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
1✔
264

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

267
        assert_eq!(env.event_count(), 0);
1✔
268
        let animator = env.animator();
1✔
269
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
270
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
271
        let transform = env.component_mut();
1✔
272
        assert!(transform.is_changed());
1✔
273
        assert!(transform.translation.abs_diff_eq(Vec3::splat(0.5), 1e-5));
1✔
274

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

277
        assert_eq!(env.event_count(), 1);
1✔
278
        let animator = env.animator();
1✔
279
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
280
        assert_eq!(animator.tweenable().times_completed(), 1);
1✔
281
        let transform = env.component_mut();
1✔
282
        assert!(transform.is_changed());
1✔
283
        assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
1✔
284

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

287
        assert_eq!(env.event_count(), 0);
1✔
288
        let animator = env.animator();
1✔
289
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
290
        assert_eq!(animator.tweenable().times_completed(), 1);
1✔
291
        let transform = env.component_mut();
1✔
292
        assert!(!transform.is_changed());
1✔
293
        assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
2✔
294
    }
295

296
    #[derive(Debug, Default, Clone, Copy, Component)]
297
    struct DummyComponent {
298
        value: f32,
299
    }
300

301
    /// Test [`Lens`] which only access mutably the target component if `defer`
302
    /// is `true`.
303
    struct ConditionalDeferLens {
304
        pub defer: Arc<AtomicBool>,
305
    }
306

307
    impl Lens<DummyComponent> for ConditionalDeferLens {
308
        fn lerp(&mut self, target: &mut dyn Targetable<DummyComponent>, ratio: f32) {
1✔
309
            if self.defer.load(Ordering::SeqCst) {
2✔
310
                target.deref_mut().value += ratio;
1✔
311
            }
312
        }
313
    }
314

315
    #[test]
316
    fn change_detect_component_conditional() {
3✔
317
        let defer = Arc::new(AtomicBool::new(false));
1✔
318
        let tween = Tween::new(
319
            EaseMethod::default(),
1✔
320
            Duration::from_secs(1),
1✔
321
            ConditionalDeferLens {
322
                defer: Arc::clone(&defer),
1✔
323
            },
324
        )
325
        .with_completed_event(0);
326

327
        let mut env = TestEnv::new(Animator::new(tween));
1✔
328

329
        // After being inserted, components are always considered changed
330
        let component = env.component_mut();
1✔
331
        assert!(component.is_changed());
1✔
332

333
        let mut system = IntoSystem::into_system(component_animator_system::<DummyComponent>);
1✔
334
        system.initialize(env.world_mut());
2✔
335

336
        assert!(!defer.load(Ordering::SeqCst));
1✔
337

338
        // Mutation disabled
339
        env.tick(Duration::ZERO, &mut system);
1✔
340

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

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

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

358
        // New tick, but lens mutation still disabled
359
        env.tick(Duration::from_millis(200), &mut system);
1✔
360

361
        let animator = env.animator();
1✔
362
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
363
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
364
        let component = env.component_mut();
1✔
365
        assert!(!component.is_changed());
1✔
366
        assert!((component.value - 0.).abs() <= 1e-5);
2✔
367

368
        // Enable lens mutation
369
        defer.store(true, Ordering::SeqCst);
1✔
370

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

376
        let animator = env.animator();
1✔
377
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
378
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
379
        let component = env.component_mut();
1✔
380
        assert!(component.is_changed());
1✔
381
        assert!((component.value - 0.2).abs() <= 1e-5);
1✔
382

383
        // 0.2s + 0.3s = 0.5s
384
        // t = 0.5s / 1s = 0.5
385
        // value += 0.5
386
        // value == 0.7
387
        env.tick(Duration::from_millis(300), &mut system);
1✔
388

389
        let animator = env.animator();
1✔
390
        assert_eq!(animator.state, AnimatorState::Playing);
1✔
391
        assert_eq!(animator.tweenable().times_completed(), 0);
1✔
392
        let component = env.component_mut();
1✔
393
        assert!(component.is_changed());
1✔
394
        assert!((component.value - 0.7).abs() <= 1e-5);
1✔
395
    }
396
}
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