onedrop_engine/engine/
transition_state.rs

1//! Engine-side preset transition state.
2//!
3//! A small delta-time-driven timer that the engine advances each frame to
4//! crossfade the outgoing preset (rendered on the renderer's secondary
5//! chain) into the incoming preset (primary). Distinct from the
6//! `Transition` / `TransitionManager` in `crate::transition` which were
7//! built earlier on top of `std::time::Instant` and never wired up — this
8//! variant takes `delta_time` so the engine's tick (and our tests) drive
9//! the progress without touching the wall clock.
10
11/// EaseInOut S-curve used for the default transition. Slower at the
12/// endpoints, faster in the middle — softer to the eye than a strict
13/// linear crossfade.
14fn ease_in_out(t: f32) -> f32 {
15    let t = t.clamp(0.0, 1.0);
16    if t < 0.5 {
17        2.0 * t * t
18    } else {
19        1.0 - 2.0 * (1.0 - t) * (1.0 - t)
20    }
21}
22
23/// One in-flight transition. `total_s` is the configured duration
24/// (typically 2.7 s, mirror of MD2's default `f_transition_time`).
25#[derive(Debug, Clone, Copy)]
26pub(crate) struct TransitionState {
27    pub(crate) elapsed_s: f32,
28    pub(crate) total_s: f32,
29}
30
31impl TransitionState {
32    pub(crate) fn new(total_s: f32) -> Self {
33        Self {
34            elapsed_s: 0.0,
35            total_s: total_s.max(1e-3),
36        }
37    }
38
39    pub(crate) fn tick(&mut self, delta_s: f32) {
40        self.elapsed_s = (self.elapsed_s + delta_s).max(0.0);
41    }
42
43    /// Raw `[0, 1]` progress with no easing.
44    pub(crate) fn linear(&self) -> f32 {
45        (self.elapsed_s / self.total_s).clamp(0.0, 1.0)
46    }
47
48    /// Eased progress in `[0, 1]` — feeds the renderer's blend uniform.
49    pub(crate) fn eased(&self) -> f32 {
50        ease_in_out(self.linear())
51    }
52
53    /// `true` once the timer has reached or passed the configured
54    /// duration. The engine drops `fading_out` and clears the renderer's
55    /// secondary chain at this point.
56    pub(crate) fn is_complete(&self) -> bool {
57        self.elapsed_s >= self.total_s
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn linear_progresses_with_delta_time() {
67        let mut t = TransitionState::new(1.0);
68        assert_eq!(t.linear(), 0.0);
69        t.tick(0.25);
70        assert!((t.linear() - 0.25).abs() < 1e-6);
71        t.tick(0.25);
72        assert!((t.linear() - 0.5).abs() < 1e-6);
73        t.tick(1.0);
74        assert_eq!(t.linear(), 1.0);
75        assert!(t.is_complete());
76    }
77
78    #[test]
79    fn ease_in_out_endpoints_and_midpoint() {
80        assert_eq!(ease_in_out(0.0), 0.0);
81        assert_eq!(ease_in_out(1.0), 1.0);
82        let mid = ease_in_out(0.5);
83        assert!((mid - 0.5).abs() < 1e-6, "midpoint should still be 0.5");
84        // The curve is below the diagonal before 0.5, above after.
85        assert!(ease_in_out(0.25) < 0.25);
86        assert!(ease_in_out(0.75) > 0.75);
87    }
88
89    #[test]
90    fn clamp_at_one_after_overshoot() {
91        let mut t = TransitionState::new(0.5);
92        t.tick(10.0);
93        assert_eq!(t.linear(), 1.0);
94        assert_eq!(t.eased(), 1.0);
95    }
96}