1use crate::{
4 timing::Duration,
5 traits::{Animatable, Animation, AnimationState},
6};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct SpringConfig {
11 pub stiffness: f32,
13 pub damping: f32,
15 pub mass: f32,
17 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#[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 #[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 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}