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

djeedai / bevy_tweening / 15848700322

24 Jun 2025 11:03AM UTC coverage: 79.787% (-1.8%) from 81.619%
15848700322

Pull #151

github

web-flow
Merge ff3c87617 into 8483d9404
Pull Request #151: Add a param generic to Animator to allow multiple tweens over the same component

3 of 19 new or added lines in 2 files covered. (15.79%)

44 existing lines in 2 files now uncovered.

375 of 470 relevant lines covered (79.79%)

297909.91 hits per line

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

25.49
/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,
×
NEW
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,
×
NEW
53
            component_animator_system::<BackgroundColor, ()>
×
NEW
54
                .in_set(AnimationSystem::AnimationUpdate),
×
55
        );
56

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

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

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

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

85
/// Animator system for components.
86
///
87
/// This system extracts all components of type `T` with an [`Animator<T>`]
88
/// attached to the same entity, and tick the animator to animate the component.
89
pub fn component_animator_system<T: Component<Mutability = Mutable>, M: Send + Sync + 'static>(
11✔
90
    time: Res<Time>,
91
    mut animator_query: Query<(Entity, &mut Animator<T, M>)>,
92
    mut target_query: Query<&mut T>,
93
    events: ResMut<Events<TweenCompleted>>,
94
    mut commands: Commands,
95
) {
96
    let mut events: Mut<Events<TweenCompleted>> = events.into();
11✔
97
    for (animator_entity, mut animator) in animator_query.iter_mut() {
22✔
98
        if animator.state != AnimatorState::Paused {
×
99
            let speed = animator.speed();
11✔
100
            let entity = animator.target.unwrap_or(animator_entity);
11✔
101
            let Ok(target) = target_query.get_mut(entity) else {
22✔
102
                continue;
×
103
            };
104
            let mut target = ComponentTarget::new(target);
11✔
105
            animator.tweenable_mut().tick(
11✔
106
                time.delta().mul_f32(speed),
11✔
107
                &mut target,
11✔
108
                entity,
11✔
109
                &mut events,
11✔
110
                &mut commands,
11✔
111
            );
112
        }
113
    }
114
}
115

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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