Skip to main content

aura_anim_core/
spring.rs

1//! Spring-based animations.
2
3use crate::{
4    timing::Duration,
5    traits::{Animatable, Animation, AnimationState},
6};
7
8/// Physical parameters used by a [`Spring`].
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct SpringConfig {
11    /// Restoring force applied toward the target.
12    pub stiffness: f32,
13    /// Resistance applied against the current velocity.
14    pub damping: f32,
15    /// Inertial mass used by the spring simulation.
16    pub mass: f32,
17    /// Position and velocity threshold used to detect completion.
18    pub epsilon: f32,
19}
20
21impl Default for SpringConfig {
22    fn default() -> Self {
23        Self {
24            stiffness: 220.0,
25            damping: 24.0,
26            mass: 1.0,
27            epsilon: 0.001,
28        }
29    }
30}
31
32/// An animation driven by a damped spring simulation.
33#[derive(Debug, Clone)]
34pub struct Spring<T: Animatable> {
35    from: T,
36    to: T,
37    current: T,
38    position: f32,
39    velocity: f32,
40    config: SpringConfig,
41    state: AnimationState,
42}
43
44impl<T: Animatable> Spring<T> {
45    /// Creates a running spring animation from `from` to `to`.
46    #[must_use]
47    pub fn new(from: T, to: T, config: SpringConfig) -> Self {
48        Self {
49            current: from.clone(),
50            from,
51            to,
52            position: 0.0,
53            velocity: 0.0,
54            config,
55            state: AnimationState::Running,
56        }
57    }
58
59    /// Restarts the spring from its current value toward `target`.
60    pub fn retarget(&mut self, target: T) {
61        self.from = self.current.clone();
62        self.to = target;
63        self.position = 0.0;
64        self.state = AnimationState::Running;
65    }
66
67    #[allow(clippy::cast_possible_truncation)]
68    fn integrate(&mut self, delta: Duration) {
69        let mut remaining = delta.as_secs().min(0.1) as f32;
70        let step = 1.0 / 120.0;
71        let mass = self.config.mass.max(f32::EPSILON);
72
73        while remaining > 0.0 {
74            let dt = remaining.min(step);
75            let acceleration = (self.config.stiffness * (1.0 - self.position)
76                - self.config.damping * self.velocity)
77                / mass;
78            self.velocity += acceleration * dt;
79            self.position += self.velocity * dt;
80            remaining -= dt;
81        }
82
83        self.current = T::extrapolate(&self.from, &self.to, self.position);
84        if (1.0 - self.position).abs() <= self.config.epsilon
85            && self.velocity.abs() <= self.config.epsilon
86        {
87            self.finish();
88        }
89    }
90}
91
92impl<T: Animatable> Animation<T> for Spring<T> {
93    fn value(&self) -> &T {
94        &self.current
95    }
96
97    fn state(&self) -> AnimationState {
98        self.state
99    }
100
101    fn tick(&mut self, delta: Duration) {
102        if self.state == AnimationState::Running {
103            self.integrate(delta);
104        }
105    }
106
107    fn pause(&mut self) {
108        if self.state == AnimationState::Running {
109            self.state = AnimationState::Paused;
110        }
111    }
112
113    fn resume(&mut self) {
114        if self.state == AnimationState::Paused {
115            self.state = AnimationState::Running;
116        }
117    }
118
119    fn cancel(&mut self) {
120        if matches!(self.state, AnimationState::Running | AnimationState::Paused) {
121            self.state = AnimationState::Canceled;
122        }
123    }
124
125    fn seek(&mut self, progress: f32) {
126        self.position = if progress.is_nan() {
127            0.0
128        } else {
129            progress.clamp(0.0, 1.0)
130        };
131        self.velocity = 0.0;
132        self.current = T::extrapolate(&self.from, &self.to, self.position);
133    }
134
135    fn finish(&mut self) {
136        self.position = 1.0;
137        self.velocity = 0.0;
138        self.current = self.to.clone();
139        self.state = AnimationState::Completed;
140    }
141
142    fn retarget(&mut self, target: &T) -> bool {
143        self.retarget(target.clone());
144        true
145    }
146}