Skip to main content

bevy_react/animations/
runner.rs

1//! The stateful driver runtime: [`Runner`] evaluates a [`Driver`] over time.
2//!
3//! Split out of `lib.rs` so the crate root keeps only the ECS orchestration
4//! (systems, `SharedValues`, binding evaluation) while the pure time-stepping
5//! machinery lives here. Shared with `bevy-react`'s CSS-like `transition`
6//! engine, which holds a `Runner` per channel rather than re-implementing
7//! easing/spring integration.
8
9use super::protocol::{Driver, Easing};
10
11const SPRING_REST_DELTA: f32 = 0.01;
12const SPRING_REST_SPEED: f32 = 0.01;
13const SPRING_SUBSTEP: f32 = 0.001;
14const SPRING_MAX_SUBSTEPS: u32 = 64;
15
16fn ease(easing: Easing, t: f32) -> f32 {
17    let t = t.clamp(0.0, 1.0);
18    match easing {
19        Easing::Linear => t,
20        Easing::EaseIn => t * t * t,
21        Easing::EaseOut => {
22            let u = 1.0 - t;
23            1.0 - u * u * u
24        }
25        Easing::EaseInOut => {
26            if t < 0.5 {
27                4.0 * t * t * t
28            } else {
29                let u = -2.0 * t + 2.0;
30                1.0 - u * u * u / 2.0
31            }
32        }
33    }
34}
35
36/// The stateful evaluation of a [`Driver`] over time. Built from a driver + the
37/// value's live reading; advanced by [`Runner::step`].
38///
39/// Public so `bevy-react`'s CSS-like `transition` engine can reuse the exact same
40/// driver runtime (the per-entity transition state holds a `Runner` per channel),
41/// rather than re-implementing easing/spring integration.
42pub enum Runner {
43    /// Degenerate (empty sequence / zero-count repeat): already settled.
44    Done(f32),
45    Timing {
46        from: f32,
47        to: f32,
48        duration: f32,
49        easing: Easing,
50        elapsed: f32,
51    },
52    Spring {
53        to: f32,
54        stiffness: f32,
55        damping: f32,
56        mass: f32,
57        pos: f32,
58        vel: f32,
59    },
60    Repeat {
61        template: Box<Driver>,
62        remaining: i32,
63        reverse: bool,
64        iteration: u32,
65        a: f32,
66        b: f32,
67        child: Box<Runner>,
68    },
69    Sequence {
70        steps: Vec<Driver>,
71        index: usize,
72        child: Box<Runner>,
73    },
74    Delay {
75        remaining: f32,
76        animation: Box<Driver>,
77        from: f32,
78        child: Option<Box<Runner>>,
79    },
80}
81
82/// Build a [`Runner`] for `driver` starting from the value `from`. Public for the
83/// `bevy-react` transition engine (see [`Runner`]).
84pub fn build_runner(driver: &Driver, from: f32) -> Runner {
85    build_runner_with_target(driver, from, None)
86}
87
88/// Build a runner; `target` overrides the natural endpoint for scalar drivers
89/// (used by reverse-repeat to ping-pong). Composite drivers ignore it.
90fn build_runner_with_target(driver: &Driver, from: f32, target: Option<f32>) -> Runner {
91    match driver {
92        Driver::Timing {
93            to,
94            duration,
95            easing,
96        } => Runner::Timing {
97            from,
98            to: target.unwrap_or(*to),
99            duration: duration.max(0.0),
100            easing: *easing,
101            elapsed: 0.0,
102        },
103        Driver::Spring {
104            to,
105            stiffness,
106            damping,
107            mass,
108        } => Runner::Spring {
109            to: target.unwrap_or(*to),
110            // Like `mass` below: clamp to a positive floor so a zero/negative/NaN
111            // JS value can't make the integrator diverge to NaN (negative
112            // stiffness repels, negative damping injects energy) or oscillate
113            // forever without settling. `f32::max` also maps NaN to the floor.
114            stiffness: stiffness.max(1e-4),
115            damping: damping.max(1e-4),
116            mass: mass.max(1e-4),
117            pos: from,
118            vel: 0.0,
119        },
120        Driver::Repeat {
121            animation,
122            count,
123            reverse,
124        } => {
125            if *count == 0 {
126                return Runner::Done(from);
127            }
128            Runner::Repeat {
129                template: animation.clone(),
130                remaining: *count,
131                reverse: *reverse,
132                iteration: 0,
133                a: from,
134                b: terminal_value(animation, from),
135                child: Box::new(build_runner(animation, from)),
136            }
137        }
138        Driver::Sequence { steps } => {
139            if steps.is_empty() {
140                return Runner::Done(from);
141            }
142            Runner::Sequence {
143                steps: steps.clone(),
144                index: 0,
145                child: Box::new(build_runner(&steps[0], from)),
146            }
147        }
148        Driver::Delay { delay, animation } => Runner::Delay {
149            remaining: delay.max(0.0),
150            animation: animation.clone(),
151            from,
152            child: None,
153        },
154    }
155}
156
157/// The value a driver settles on if run to completion from `from` (used to derive
158/// repeat endpoints).
159fn terminal_value(driver: &Driver, from: f32) -> f32 {
160    match driver {
161        Driver::Timing { to, .. } => *to,
162        Driver::Spring { to, .. } => *to,
163        Driver::Sequence { steps } => steps.iter().fold(from, |acc, s| terminal_value(s, acc)),
164        Driver::Delay { animation, .. } => terminal_value(animation, from),
165        Driver::Repeat {
166            animation,
167            count,
168            reverse,
169        } => {
170            let a = from;
171            let b = terminal_value(animation, from);
172            if *count <= 0 {
173                b
174            } else if *reverse && count % 2 == 0 {
175                a
176            } else {
177                b
178            }
179        }
180    }
181}
182
183impl Runner {
184    /// Advance by `dt` seconds, returning `(value, finished)`.
185    pub fn step(&mut self, dt: f32) -> (f32, bool) {
186        match self {
187            Runner::Done(v) => (*v, true),
188            Runner::Timing {
189                from,
190                to,
191                duration,
192                easing,
193                elapsed,
194            } => {
195                *elapsed += dt;
196                if *duration <= 0.0 {
197                    return (*to, true);
198                }
199                let t = (*elapsed / *duration).clamp(0.0, 1.0);
200                let v = *from + (*to - *from) * ease(*easing, t);
201                (v, *elapsed >= *duration)
202            }
203            Runner::Spring {
204                to,
205                stiffness,
206                damping,
207                mass,
208                pos,
209                vel,
210            } => {
211                let n = ((dt / SPRING_SUBSTEP).ceil() as u32).clamp(1, SPRING_MAX_SUBSTEPS);
212                let h = dt / n as f32;
213                for _ in 0..n {
214                    let force = -*stiffness * (*pos - *to) - *damping * *vel;
215                    let acc = force / *mass;
216                    *vel += acc * h;
217                    *pos += *vel * h;
218                }
219                let settled =
220                    (*pos - *to).abs() < SPRING_REST_DELTA && vel.abs() < SPRING_REST_SPEED;
221                if settled {
222                    *pos = *to;
223                    *vel = 0.0;
224                }
225                (*pos, settled)
226            }
227            Runner::Repeat {
228                template,
229                remaining,
230                reverse,
231                iteration,
232                a,
233                b,
234                child,
235            } => {
236                let (v, done) = child.step(dt);
237                if !done {
238                    return (v, false);
239                }
240                if *remaining > 0 {
241                    *remaining -= 1;
242                    if *remaining == 0 {
243                        return (v, true);
244                    }
245                }
246                *iteration += 1;
247                let (from, target) = if *reverse {
248                    if *iteration % 2 == 0 {
249                        (*a, *b)
250                    } else {
251                        (*b, *a)
252                    }
253                } else {
254                    (*a, *b)
255                };
256                **child = build_runner_with_target(template, from, Some(target));
257                (v, false)
258            }
259            Runner::Sequence {
260                steps,
261                index,
262                child,
263            } => {
264                let (v, done) = child.step(dt);
265                if !done {
266                    return (v, false);
267                }
268                if *index + 1 >= steps.len() {
269                    return (v, true);
270                }
271                *index += 1;
272                **child = build_runner(&steps[*index], v);
273                (v, false)
274            }
275            Runner::Delay {
276                remaining,
277                animation,
278                from,
279                child,
280            } => {
281                if let Some(child) = child {
282                    return child.step(dt);
283                }
284                *remaining -= dt;
285                if *remaining > 0.0 {
286                    return (*from, false);
287                }
288                // Delay elapsed: build the child and run it with the leftover time
289                // (`-remaining`) so no time is lost crossing the boundary.
290                let leftover = -*remaining;
291                let mut runner = build_runner(animation, *from);
292                let result = runner.step(leftover);
293                *child = Some(Box::new(runner));
294                result
295            }
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    fn timing(to: f32, duration: f32) -> Driver {
305        Driver::Timing {
306            to,
307            duration,
308            easing: Easing::Linear,
309        }
310    }
311
312    /// Drive a runner to completion in fixed `dt` ticks, returning the final value.
313    fn run_to_end(driver: &Driver, from: f32, dt: f32, max_ticks: usize) -> (f32, usize) {
314        let mut r = build_runner(driver, from);
315        for i in 0..max_ticks {
316            let (v, done) = r.step(dt);
317            if done {
318                return (v, i + 1);
319            }
320        }
321        panic!("runner did not finish in {max_ticks} ticks");
322    }
323
324    #[test]
325    fn easing_endpoints_and_midpoint() {
326        for e in [
327            Easing::Linear,
328            Easing::EaseIn,
329            Easing::EaseOut,
330            Easing::EaseInOut,
331        ] {
332            assert!((ease(e, 0.0) - 0.0).abs() < 1e-6, "{e:?} at 0");
333            assert!((ease(e, 1.0) - 1.0).abs() < 1e-6, "{e:?} at 1");
334        }
335        assert!((ease(Easing::Linear, 0.5) - 0.5).abs() < 1e-6);
336        // EaseIn is below the diagonal, EaseOut above, at the midpoint.
337        assert!(ease(Easing::EaseIn, 0.5) < 0.5);
338        assert!(ease(Easing::EaseOut, 0.5) > 0.5);
339    }
340
341    /// Hostile JS spring params (zero/negative/NaN stiffness, damping, mass) are
342    /// clamped to a positive floor, so the integrator stays finite and settles
343    /// instead of diverging to NaN (and being driven forever).
344    #[test]
345    fn spring_survives_hostile_params() {
346        for (stiffness, damping, mass) in [
347            (-1.0, -5.0, 0.0),
348            (0.0, 0.0, -1.0),
349            (f32::NAN, f32::NAN, f32::NAN),
350        ] {
351            let driver = Driver::Spring {
352                to: 100.0,
353                stiffness,
354                damping,
355                mass,
356            };
357            let mut r = build_runner(&driver, 0.0);
358            let mut settled = false;
359            for _ in 0..10_000 {
360                let (v, done) = r.step(1.0 / 60.0);
361                assert!(
362                    v.is_finite(),
363                    "diverged with k={stiffness} c={damping} m={mass}"
364                );
365                if done {
366                    settled = true;
367                    break;
368                }
369            }
370            assert!(
371                settled,
372                "never settled with k={stiffness} c={damping} m={mass}"
373            );
374        }
375    }
376
377    #[test]
378    fn timing_runs_from_current_to_target() {
379        let mut r = build_runner(&timing(100.0, 1.0), 0.0);
380        let (v1, done1) = r.step(0.5);
381        assert!(!done1);
382        assert!((v1 - 50.0).abs() < 1e-3, "halfway expected ~50, got {v1}");
383        let (v2, done2) = r.step(0.5);
384        assert!(done2);
385        assert!((v2 - 100.0).abs() < 1e-3, "end expected 100, got {v2}");
386    }
387
388    #[test]
389    fn zero_duration_timing_snaps() {
390        let mut r = build_runner(&timing(42.0, 0.0), 0.0);
391        let (v, done) = r.step(0.016);
392        assert!(done);
393        assert_eq!(v, 42.0);
394    }
395
396    #[test]
397    fn spring_settles_on_target() {
398        let driver = Driver::Spring {
399            to: 100.0,
400            stiffness: 120.0,
401            damping: 14.0,
402            mass: 1.0,
403        };
404        let (v, ticks) = run_to_end(&driver, 0.0, 1.0 / 60.0, 2000);
405        assert!(
406            (v - 100.0).abs() < 0.1,
407            "spring should settle near 100, got {v}"
408        );
409        assert!(ticks > 1, "spring should take multiple ticks");
410    }
411
412    #[test]
413    fn delay_holds_then_runs_child() {
414        // Hold at the start value for 0.5s, then time 10 -> 30 over 1s.
415        let driver = Driver::Delay {
416            delay: 0.5,
417            animation: Box::new(timing(30.0, 1.0)),
418        };
419        let mut r = build_runner(&driver, 10.0);
420        let (v1, d1) = r.step(0.25);
421        assert!(!d1);
422        assert!(
423            (v1 - 10.0).abs() < 1e-6,
424            "still holding, expected 10, got {v1}"
425        );
426        let (v2, d2) = r.step(0.25); // delay exactly elapses, child starts (0 leftover)
427        assert!(!d2);
428        assert!(
429            (v2 - 10.0).abs() < 1e-3,
430            "child at t=0 expected 10, got {v2}"
431        );
432        let (v3, _d3) = r.step(0.5); // halfway through the 1s timing
433        assert!((v3 - 20.0).abs() < 1e-3, "halfway expected 20, got {v3}");
434        let (v4, d4) = r.step(0.5);
435        assert!(d4);
436        assert!((v4 - 30.0).abs() < 1e-3, "end expected 30, got {v4}");
437    }
438
439    #[test]
440    fn delay_carries_leftover_time_across_the_boundary() {
441        // 0.1s delay, then a 1s timing 0 -> 100. A single 0.6s tick should burn
442        // the delay and advance 0.5s into the timing (≈50), losing no time.
443        let driver = Driver::Delay {
444            delay: 0.1,
445            animation: Box::new(timing(100.0, 1.0)),
446        };
447        let mut r = build_runner(&driver, 0.0);
448        let (v, done) = r.step(0.6);
449        assert!(!done);
450        assert!(
451            (v - 50.0).abs() < 1e-3,
452            "expected ~50 after leftover, got {v}"
453        );
454    }
455
456    #[test]
457    fn zero_delay_runs_child_immediately() {
458        // Covers the i = 0 stagger case: no hold, behaves like the bare child.
459        let driver = Driver::Delay {
460            delay: 0.0,
461            animation: Box::new(timing(100.0, 1.0)),
462        };
463        let mut r = build_runner(&driver, 0.0);
464        let (v, done) = r.step(0.5);
465        assert!(!done);
466        assert!(
467            (v - 50.0).abs() < 1e-3,
468            "zero delay should not hold, got {v}"
469        );
470    }
471
472    #[test]
473    fn delay_inside_repeated_sequence_composes() {
474        // The exact demo shape: bounce -A -> +A with a stop at each end, looped.
475        let amp = 100.0;
476        let bounce = Driver::Repeat {
477            animation: Box::new(Driver::Sequence {
478                steps: vec![
479                    Driver::Delay {
480                        delay: 0.2,
481                        animation: Box::new(timing(amp, 0.5)),
482                    },
483                    Driver::Delay {
484                        delay: 0.2,
485                        animation: Box::new(timing(-amp, 0.5)),
486                    },
487                ],
488            }),
489            count: -1,
490            reverse: false,
491        };
492        let mut r = build_runner(&bounce, -amp);
493        // Run a couple of seconds; it must stay bounded in [-amp, amp] and never finish.
494        let mut min = f32::INFINITY;
495        let mut max = f32::NEG_INFINITY;
496        for _ in 0..240 {
497            let (v, done) = r.step(1.0 / 60.0);
498            assert!(!done, "infinite bounce must never finish");
499            min = min.min(v);
500            max = max.max(v);
501        }
502        assert!(
503            min <= -amp + 1.0,
504            "should reach the left extreme, min={min}"
505        );
506        assert!(
507            max >= amp - 1.0,
508            "should reach the right extreme, max={max}"
509        );
510        assert!(min >= -amp - 1e-3 && max <= amp + 1e-3, "must stay bounded");
511    }
512
513    #[test]
514    fn sequence_chains_steps_from_previous_end() {
515        // 0 -> 50 -> 120, each over 1s.
516        let driver = Driver::Sequence {
517            steps: vec![timing(50.0, 1.0), timing(120.0, 1.0)],
518        };
519        let mut r = build_runner(&driver, 0.0);
520        let (v1, d1) = r.step(1.0); // first step done
521        assert!(!d1);
522        assert!(
523            (v1 - 50.0).abs() < 1e-3,
524            "after step 1 expected 50, got {v1}"
525        );
526        let (v2, _d2) = r.step(0.5); // halfway through second step: 50 -> 120
527        assert!(
528            (v2 - 85.0).abs() < 1.0,
529            "midway second step expected ~85, got {v2}"
530        );
531        let (v3, d3) = r.step(0.5);
532        assert!(d3);
533        assert!(
534            (v3 - 120.0).abs() < 1e-3,
535            "sequence end expected 120, got {v3}"
536        );
537    }
538
539    #[test]
540    fn finite_repeat_finishes_after_count_cycles() {
541        // Repeat a 1s timing twice, no reverse: each cycle 0 -> 10.
542        let driver = Driver::Repeat {
543            animation: Box::new(timing(10.0, 1.0)),
544            count: 2,
545            reverse: false,
546        };
547        let (v, _ticks) = run_to_end(&driver, 0.0, 0.25, 1000);
548        assert!(
549            (v - 10.0).abs() < 1e-3,
550            "finite repeat ends at target, got {v}"
551        );
552    }
553
554    #[test]
555    fn reverse_repeat_ping_pongs_endpoints() {
556        // 0 -> 10 then (reverse) 10 -> 0 over two cycles: ends back at 0.
557        let driver = Driver::Repeat {
558            animation: Box::new(timing(10.0, 1.0)),
559            count: 2,
560            reverse: true,
561        };
562        let (v, _ticks) = run_to_end(&driver, 0.0, 0.25, 1000);
563        assert!(
564            (v - 0.0).abs() < 1e-3,
565            "reverse repeat returns to start, got {v}"
566        );
567        assert_eq!(terminal_value(&driver, 0.0), 0.0);
568    }
569
570    #[test]
571    fn infinite_repeat_never_finishes() {
572        let driver = Driver::Repeat {
573            animation: Box::new(timing(10.0, 1.0)),
574            count: -1,
575            reverse: true,
576        };
577        let mut r = build_runner(&driver, 0.0);
578        for _ in 0..1000 {
579            let (_v, done) = r.step(0.1);
580            assert!(!done, "infinite repeat must never report finished");
581        }
582    }
583}