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

djeedai / bevy_tweening / 14694529280

27 Apr 2025 05:35PM UTC coverage: 81.978% (-9.4%) from 91.4%
14694529280

push

github

web-flow
Fix CI (#145)

373 of 455 relevant lines covered (81.98%)

307731.03 hits per line

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

26.0
/src/plugin.rs
1
use bevy::{ecs::component::Mutable, 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.16.0/bevy/transform/components/struct.Transform.html
31
/// [`TextColor`]: https://docs.rs/bevy/0.16.0/bevy/text/struct.TextColor.html
32
/// [`Node`]: https://docs.rs/bevy/0.16.0/bevy/ui/struct.Node.html
33
/// [`Sprite`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.Sprite.html
34
/// [`ColorMaterial`]: https://docs.rs/bevy/0.16.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::<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,
×
65
            asset_animator_system::<ColorMaterial, MeshMaterial2d<ColorMaterial>>
×
66
                .in_set(AnimationSystem::AnimationUpdate),
×
67
        );
68

69
        #[cfg(feature = "bevy_text")]
70
        app.add_systems(
×
71
            Update,
×
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<Mutability = Mutable>>(
11✔
89
    time: Res<Time>,
90
    mut animator_query: Query<(Entity, &mut Animator<T>)>,
91
    mut target_query: Query<&mut T>,
92
    events: ResMut<Events<TweenCompleted>>,
93
    mut commands: Commands,
94
) {
95
    let mut events: Mut<Events<TweenCompleted>> = events.into();
11✔
96
    for (animator_entity, mut animator) in animator_query.iter_mut() {
22✔
97
        if animator.state != AnimatorState::Paused {
×
98
            let speed = animator.speed();
11✔
99
            let entity = animator.target.unwrap_or(animator_entity);
11✔
100
            let Ok(target) = target_query.get_mut(entity) else {
22✔
101
                continue;
×
102
            };
103
            let mut target = ComponentTarget::new(target);
11✔
104
            animator.tweenable_mut().tick(
11✔
105
                time.delta().mul_f32(speed),
11✔
106
                &mut target,
11✔
107
                entity,
11✔
108
                &mut events,
11✔
109
                &mut commands,
11✔
110
            );
111
        }
112
    }
113
}
114

115
#[cfg(feature = "bevy_asset")]
116
use std::ops::Deref;
117

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

155
#[cfg(test)]
156
mod tests {
157
    use std::{
158
        marker::PhantomData,
159
        ops::DerefMut,
160
        sync::{
161
            atomic::{AtomicBool, Ordering},
162
            Arc,
163
        },
164
    };
165

166
    use bevy::ecs::component::Mutable;
167

168
    use crate::{lens::TransformPositionLens, *};
169

170
    /// A simple isolated test environment with a [`World`] and a single
171
    /// [`Entity`] in it.
172
    struct TestEnv<T: Component> {
173
        world: World,
174
        animator_entity: Entity,
175
        target_entity: Option<Entity>,
176
        _phantom: PhantomData<T>,
177
    }
178

179
    impl<T: Component + Default> TestEnv<T> {
180
        /// Create a new test environment containing a single entity with a
181
        /// [`Transform`], and add the given animator on that same entity.
182
        pub fn new(animator: Animator<T>) -> Self {
183
            let mut world = World::new();
184
            world.init_resource::<Events<TweenCompleted>>();
185
            world.init_resource::<Time>();
186

187
            let entity = world.spawn((T::default(), animator)).id();
188

189
            Self {
190
                world,
191
                animator_entity: entity,
192
                target_entity: None,
193
                _phantom: PhantomData,
194
            }
195
        }
196

197
        /// Like [`TestEnv::new`], but the component is placed on a separate entity.
198
        pub fn new_separated(animator: Animator<T>) -> Self {
199
            let mut world = World::new();
200
            world.init_resource::<Events<TweenCompleted>>();
201
            world.init_resource::<Time>();
202

203
            let target = world.spawn(T::default()).id();
204
            let entity = world.spawn(animator.with_target(target)).id();
205

206
            Self {
207
                world,
208
                animator_entity: entity,
209
                target_entity: Some(target),
210
                _phantom: PhantomData,
211
            }
212
        }
213
    }
214

215
    impl<T: Component<Mutability = Mutable>> TestEnv<T> {
216
        /// Get the test world.
217
        pub fn world_mut(&mut self) -> &mut World {
218
            &mut self.world
219
        }
220

221
        /// Tick the test environment, updating the simulation time and ticking
222
        /// the given system.
223
        pub fn tick(&mut self, duration: Duration, system: &mut dyn System<In = (), Out = ()>) {
224
            // Simulate time passing by updating the simulation time resource
225
            {
226
                let mut time = self.world.resource_mut::<Time>();
227
                time.advance_by(duration);
228
            }
229

230
            // Reset world-related change detection
231
            self.world.clear_trackers();
232
            assert!(!self.component_mut().is_changed());
233

234
            // Tick system
235
            system.run((), &mut self.world);
236

237
            // Update events after system ticked, in case system emitted some events
238
            let mut events = self.world.resource_mut::<Events<TweenCompleted>>();
239
            events.update();
240
        }
241

242
        /// Get the animator for the component.
243
        pub fn animator(&self) -> &Animator<T> {
244
            self.world
245
                .entity(self.animator_entity)
246
                .get::<Animator<T>>()
247
                .unwrap()
248
        }
249

250
        /// Get the component.
251
        pub fn component_mut(&mut self) -> Mut<T> {
252
            self.world
253
                .get_mut::<T>(self.target_entity.unwrap_or(self.animator_entity))
254
                .unwrap()
255
        }
256

257
        /// Get the emitted event count since last tick.
258
        pub fn event_count(&self) -> usize {
259
            let events = self.world.resource::<Events<TweenCompleted>>();
260
            events.get_cursor().len(events)
261
        }
262
    }
263

264
    #[test]
265
    fn custom_target_entity() {
266
        let tween = Tween::new(
267
            EaseMethod::EaseFunction(EaseFunction::Linear),
268
            Duration::from_secs(1),
269
            TransformPositionLens {
270
                start: Vec3::ZERO,
271
                end: Vec3::ONE,
272
            },
273
        )
274
        .with_completed_event(0);
275
        let mut env = TestEnv::new_separated(Animator::new(tween));
276
        let mut system = IntoSystem::into_system(component_animator_system::<Transform>);
277
        system.initialize(env.world_mut());
278

279
        env.tick(Duration::ZERO, &mut system);
280
        let transform = env.component_mut();
281
        assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
282

283
        env.tick(Duration::from_millis(500), &mut system);
284
        let transform = env.component_mut();
285
        assert!(transform.translation.abs_diff_eq(Vec3::splat(0.5), 1e-5));
286
    }
287

288
    #[test]
289
    fn change_detect_component() {
290
        let tween = Tween::new(
291
            EaseMethod::default(),
292
            Duration::from_secs(1),
293
            TransformPositionLens {
294
                start: Vec3::ZERO,
295
                end: Vec3::ONE,
296
            },
297
        )
298
        .with_completed_event(0);
299

300
        let mut env = TestEnv::new(Animator::new(tween));
301

302
        // After being inserted, components are always considered changed
303
        let transform = env.component_mut();
304
        assert!(transform.is_changed());
305

306
        // fn nit() {}
307
        // let mut system = IntoSystem::into_system(nit);
308
        let mut system = IntoSystem::into_system(component_animator_system::<Transform>);
309
        system.initialize(env.world_mut());
310

311
        env.tick(Duration::ZERO, &mut system);
312

313
        let animator = env.animator();
314
        assert_eq!(animator.state, AnimatorState::Playing);
315
        assert_eq!(animator.tweenable().times_completed(), 0);
316
        let transform = env.component_mut();
317
        assert!(transform.is_changed());
318
        assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
319

320
        env.tick(Duration::from_millis(500), &mut system);
321

322
        assert_eq!(env.event_count(), 0);
323
        let animator = env.animator();
324
        assert_eq!(animator.state, AnimatorState::Playing);
325
        assert_eq!(animator.tweenable().times_completed(), 0);
326
        let transform = env.component_mut();
327
        assert!(transform.is_changed());
328
        assert!(transform.translation.abs_diff_eq(Vec3::splat(0.5), 1e-5));
329

330
        env.tick(Duration::from_millis(500), &mut system);
331

332
        assert_eq!(env.event_count(), 1);
333
        let animator = env.animator();
334
        assert_eq!(animator.state, AnimatorState::Playing);
335
        assert_eq!(animator.tweenable().times_completed(), 1);
336
        let transform = env.component_mut();
337
        assert!(transform.is_changed());
338
        assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
339

340
        env.tick(Duration::from_millis(100), &mut system);
341

342
        assert_eq!(env.event_count(), 0);
343
        let animator = env.animator();
344
        assert_eq!(animator.state, AnimatorState::Playing);
345
        assert_eq!(animator.tweenable().times_completed(), 1);
346
        let transform = env.component_mut();
347
        assert!(!transform.is_changed());
348
        assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
349
    }
350

351
    #[derive(Debug, Default, Clone, Copy, Component)]
352
    struct DummyComponent {
353
        value: f32,
354
    }
355

356
    /// Test [`Lens`] which only access mutably the target component if `defer`
357
    /// is `true`.
358
    struct ConditionalDeferLens {
359
        pub defer: Arc<AtomicBool>,
360
    }
361

362
    impl Lens<DummyComponent> for ConditionalDeferLens {
363
        fn lerp(&mut self, target: &mut dyn Targetable<DummyComponent>, ratio: f32) {
364
            if self.defer.load(Ordering::SeqCst) {
365
                target.deref_mut().value += ratio;
366
            }
367
        }
368
    }
369

370
    #[test]
371
    fn change_detect_component_conditional() {
372
        let defer = Arc::new(AtomicBool::new(false));
373
        let tween = Tween::new(
374
            EaseMethod::default(),
375
            Duration::from_secs(1),
376
            ConditionalDeferLens {
377
                defer: Arc::clone(&defer),
378
            },
379
        )
380
        .with_completed_event(0);
381

382
        let mut env = TestEnv::new(Animator::new(tween));
383

384
        // After being inserted, components are always considered changed
385
        let component = env.component_mut();
386
        assert!(component.is_changed());
387

388
        let mut system = IntoSystem::into_system(component_animator_system::<DummyComponent>);
389
        system.initialize(env.world_mut());
390

391
        assert!(!defer.load(Ordering::SeqCst));
392

393
        // Mutation disabled
394
        env.tick(Duration::ZERO, &mut system);
395

396
        let animator = env.animator();
397
        assert_eq!(animator.state, AnimatorState::Playing);
398
        assert_eq!(animator.tweenable().times_completed(), 0);
399
        let component = env.component_mut();
400
        assert!(!component.is_changed());
401
        assert!((component.value - 0.).abs() <= 1e-5);
402

403
        // Zero-length tick should not change the component
404
        env.tick(Duration::from_millis(0), &mut system);
405

406
        let animator = env.animator();
407
        assert_eq!(animator.state, AnimatorState::Playing);
408
        assert_eq!(animator.tweenable().times_completed(), 0);
409
        let component = env.component_mut();
410
        assert!(!component.is_changed());
411
        assert!((component.value - 0.).abs() <= 1e-5);
412

413
        // New tick, but lens mutation still disabled
414
        env.tick(Duration::from_millis(200), &mut system);
415

416
        let animator = env.animator();
417
        assert_eq!(animator.state, AnimatorState::Playing);
418
        assert_eq!(animator.tweenable().times_completed(), 0);
419
        let component = env.component_mut();
420
        assert!(!component.is_changed());
421
        assert!((component.value - 0.).abs() <= 1e-5);
422

423
        // Enable lens mutation
424
        defer.store(true, Ordering::SeqCst);
425

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

431
        let animator = env.animator();
432
        assert_eq!(animator.state, AnimatorState::Playing);
433
        assert_eq!(animator.tweenable().times_completed(), 0);
434
        let component = env.component_mut();
435
        assert!(component.is_changed());
436
        assert!((component.value - 0.2).abs() <= 1e-5);
437

438
        // 0.2s + 0.3s = 0.5s
439
        // t = 0.5s / 1s = 0.5
440
        // value += 0.5
441
        // value == 0.7
442
        env.tick(Duration::from_millis(300), &mut system);
443

444
        let animator = env.animator();
445
        assert_eq!(animator.state, AnimatorState::Playing);
446
        assert_eq!(animator.tweenable().times_completed(), 0);
447
        let component = env.component_mut();
448
        assert!(component.is_changed());
449
        assert!((component.value - 0.7).abs() <= 1e-5);
450
    }
451
}
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