Skip to main content

argui_animation/
inertia.rs

1use crate::{Decay, DecayConfig, Duration, PhysicsError, Spring, SpringConfig};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct InertiaConfig {
5    pub decay: DecayConfig,
6    pub bounce: SpringConfig,
7    pub bounds: Option<(f32, f32)>,
8}
9
10impl Default for InertiaConfig {
11    fn default() -> Self {
12        Self {
13            decay: DecayConfig::default(),
14            bounce: SpringConfig {
15                stiffness: 240.0,
16                damping: 28.0,
17                ..SpringConfig::default()
18            },
19            bounds: None,
20        }
21    }
22}
23
24impl InertiaConfig {
25    pub fn validate(self) -> Result<Self, PhysicsError> {
26        self.decay.validate()?;
27        self.bounce.validate()?;
28        if self.bounds.is_some_and(|(minimum, maximum)| {
29            !minimum.is_finite() || !maximum.is_finite() || minimum > maximum
30        }) {
31            Err(PhysicsError::InvalidBounds)
32        } else {
33            Ok(self)
34        }
35    }
36}
37
38#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
39pub enum InertiaState {
40    #[default]
41    Settled,
42    Decaying,
43    Bouncing,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct Inertia {
48    decay: Decay<f32>,
49    spring: Option<Spring<f32>>,
50    config: InertiaConfig,
51    state: InertiaState,
52}
53
54impl Inertia {
55    pub fn new(value: f32, velocity: f32, config: InertiaConfig) -> Result<Self, PhysicsError> {
56        let config = config.validate()?;
57        let decay = Decay::new(value, velocity, config.decay)?;
58        let bound = bound_target(config.bounds, value);
59        let spring = bound.map(|target| {
60            Spring::new(value, target, velocity, config.bounce)
61                .expect("validated inertia contains a valid bounce spring")
62        });
63        let state = if spring.is_some() {
64            InertiaState::Bouncing
65        } else if decay.is_active() {
66            InertiaState::Decaying
67        } else {
68            InertiaState::Settled
69        };
70        Ok(Self {
71            decay,
72            spring,
73            config,
74            state,
75        })
76    }
77
78    #[must_use]
79    pub fn value(&self) -> f32 {
80        self.spring
81            .map_or_else(|| self.decay.value(), |spring| spring.value())
82    }
83
84    #[must_use]
85    pub fn velocity(&self) -> f32 {
86        self.spring
87            .map_or_else(|| self.decay.velocity(), |spring| spring.velocity())
88    }
89
90    #[must_use]
91    pub const fn state(&self) -> InertiaState {
92        self.state
93    }
94
95    #[must_use]
96    pub fn is_active(&self) -> bool {
97        self.state != InertiaState::Settled
98    }
99
100    pub fn advance(&mut self, elapsed: Duration) -> bool {
101        match &mut self.spring {
102            Some(spring) => {
103                let changed = spring.advance(elapsed);
104                if !spring.is_active() {
105                    self.state = InertiaState::Settled;
106                }
107                changed
108            }
109            None => {
110                let changed = self.decay.advance(elapsed);
111                if let Some(target) = self.bound_target() {
112                    self.spring = Some(
113                        Spring::new(
114                            self.decay.value(),
115                            target,
116                            self.decay.velocity(),
117                            self.config.bounce,
118                        )
119                        .expect("validated inertia contains a valid bounce spring"),
120                    );
121                    self.state = InertiaState::Bouncing;
122                } else if !self.decay.is_active() {
123                    self.state = InertiaState::Settled;
124                }
125                changed
126            }
127        }
128    }
129
130    pub fn launch(&mut self, value: f32, velocity: f32) {
131        *self = Self::new(value, velocity, self.config)
132            .expect("an existing inertia always retains valid configuration");
133    }
134
135    fn bound_target(&self) -> Option<f32> {
136        bound_target(self.config.bounds, self.decay.value())
137    }
138}
139
140fn bound_target(bounds: Option<(f32, f32)>, value: f32) -> Option<f32> {
141    let (minimum, maximum) = bounds?;
142    if value < minimum {
143        Some(minimum)
144    } else if value > maximum {
145        Some(maximum)
146    } else {
147        None
148    }
149}