sequoia_animation/
spring_value.rs1use crate::Spring;
2use sequoia_core::Point;
3
4const SETTLE_EPS: f32 = 1e-3;
5
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct SpringValue {
8 value: f32,
9 target: f32,
10 velocity: f32,
11 spring: Spring,
12}
13
14impl SpringValue {
15 pub fn new(value: f32, spring: Spring) -> Self {
16 Self {
17 value,
18 target: value,
19 velocity: 0.0,
20 spring,
21 }
22 }
23
24 pub fn with_default_spring(value: f32) -> Self {
25 Self::new(value, Spring::new(0.9, 6.0))
26 }
27
28 pub fn value(&self) -> f32 {
29 self.value
30 }
31
32 pub fn target(&self) -> f32 {
33 self.target
34 }
35
36 pub fn velocity(&self) -> f32 {
37 self.velocity
38 }
39
40 pub fn spring(&self) -> Spring {
41 self.spring
42 }
43
44 pub fn is_settled(&self) -> bool {
45 (self.value - self.target).abs() <= SETTLE_EPS && self.velocity.abs() <= SETTLE_EPS
46 }
47
48 pub fn set_target(&mut self, target: f32) {
49 self.target = target;
50 if self.is_settled() {
51 self.value = target;
52 self.velocity = 0.0;
53 }
54 }
55
56 pub fn jump_to(&mut self, value: f32) {
57 self.value = value;
58 self.target = value;
59 self.velocity = 0.0;
60 }
61
62 pub fn reset_value(&mut self, value: f32) {
63 self.value = value;
64 self.velocity = 0.0;
65 }
66
67 pub fn set_spring(&mut self, spring: Spring) {
68 self.spring = spring;
69 }
70
71 pub fn nudge(&mut self, velocity: f32) {
72 self.velocity += velocity;
73 }
74
75 pub fn advance(&mut self, dt: f32) -> bool {
76 let dt = dt.clamp(0.0, 0.25);
77 if dt <= 0.0 {
78 return !self.is_settled();
79 }
80 let w0 = std::f32::consts::TAU * self.spring.frequency.max(0.0001);
81 let k = w0 * w0;
82 let c = 2.0 * self.spring.damping_ratio.max(0.0) * w0;
83
84 let max_step = 1.0 / (w0 * 4.0).max(60.0);
85 let steps = (dt / max_step).ceil().max(1.0) as usize;
86 let h = dt / steps as f32;
87 for _ in 0..steps {
88 let accel = -k * (self.value - self.target) - c * self.velocity;
89 self.velocity += accel * h;
90 self.value += self.velocity * h;
91 }
92 if self.is_settled() {
93 self.value = self.target;
94 self.velocity = 0.0;
95 false
96 } else {
97 true
98 }
99 }
100}
101
102impl Default for SpringValue {
103 fn default() -> Self {
104 Self::with_default_spring(0.0)
105 }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq)]
109pub struct SpringPoint {
110 pub x: SpringValue,
111 pub y: SpringValue,
112}
113
114impl SpringPoint {
115 pub fn new(x: f32, y: f32, spring: Spring) -> Self {
116 Self {
117 x: SpringValue::new(x, spring),
118 y: SpringValue::new(y, spring),
119 }
120 }
121
122 pub fn with_default_spring(x: f32, y: f32) -> Self {
123 Self {
124 x: SpringValue::with_default_spring(x),
125 y: SpringValue::with_default_spring(y),
126 }
127 }
128
129 pub fn point(&self) -> Point {
130 Point::new(self.x.value(), self.y.value())
131 }
132
133 pub fn set_target(&mut self, x: f32, y: f32) {
134 self.x.set_target(x);
135 self.y.set_target(y);
136 }
137
138 pub fn jump_to(&mut self, x: f32, y: f32) {
139 self.x.jump_to(x);
140 self.y.jump_to(y);
141 }
142
143 pub fn set_spring(&mut self, spring: Spring) {
144 self.x.set_spring(spring);
145 self.y.set_spring(spring);
146 }
147
148 pub fn is_settled(&self) -> bool {
149 self.x.is_settled() && self.y.is_settled()
150 }
151
152 pub fn advance(&mut self, dt: f32) -> bool {
153 let flying_x = self.x.advance(dt);
154 let flying_y = self.y.advance(dt);
155 flying_x || flying_y
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn chases_target_then_snaps_exactly() {
165 let mut v = SpringValue::new(0.0, Spring::new(0.8, 5.0));
166 v.set_target(100.0);
167 assert_eq!(v.value(), 0.0, "set_target does not teleport");
168 assert!(v.advance(1.0 / 60.0), "still flying");
169 assert!(v.value() > 0.0 && v.value() < 100.0);
170 let mut frames = 1;
171 while v.advance(1.0 / 60.0) {
172 frames += 1;
173 assert!(frames < 10_000, "spring must settle");
174 }
175 assert_eq!(v.value(), 100.0, "settles exactly on target");
176 assert_eq!(v.velocity(), 0.0);
177 assert!(v.is_settled());
178 }
179
180 #[test]
181 fn retarget_keeps_momentum_and_does_not_jump() {
182 let mut v = SpringValue::new(0.0, Spring::new(0.9, 3.0));
183 v.set_target(100.0);
184 for _ in 0..3 {
185 v.advance(1.0 / 60.0);
186 }
187 let mid = v.value();
188 let vel = v.velocity();
189 assert!(mid > 0.0 && mid < 100.0, "in flight: {mid}");
190 assert!(vel > 0.0, "moving forward");
191
192 v.set_target(0.0);
193 assert_eq!(v.value(), mid, "no teleport on retarget");
194 assert_eq!(v.velocity(), vel, "velocity preserved on retarget");
195
196 for _ in 0..600 {
197 v.advance(1.0 / 120.0);
198 }
199 assert!(v.is_settled());
200 assert_eq!(v.value(), 0.0);
201 }
202
203 #[test]
204 fn overdamped_never_overshoots() {
205 let mut v = SpringValue::new(0.0, Spring::new(1.6, 6.0));
206 v.set_target(50.0);
207 let mut peak = 0.0f32;
208 for _ in 0..600 {
209 v.advance(1.0 / 120.0);
210 peak = peak.max(v.value());
211 }
212 assert!(peak <= 50.0 + 1e-3, "over-damped overshoot: {peak}");
213 }
214
215 #[test]
216 fn underdamped_overshoots() {
217 let mut v = SpringValue::new(0.0, Spring::new(0.25, 4.0));
218 v.set_target(50.0);
219 let mut peak = 0.0f32;
220 for _ in 0..600 {
221 v.advance(1.0 / 120.0);
222 peak = peak.max(v.value());
223 }
224 assert!(peak > 52.0, "under-damped should overshoot: {peak}");
225 }
226
227 #[test]
228 fn low_frame_rate_stays_stable() {
229 let mut v = SpringValue::new(0.0, Spring::new(0.5, 10.0));
230 v.set_target(10.0);
231 for _ in 0..60 {
232 v.advance(0.05);
233 assert!(v.value().is_finite());
234 assert!(v.value() < 100.0, "stable integration: {}", v.value());
235 }
236 assert!(v.is_settled() || (v.value() - 10.0).abs() < 1.0);
237 }
238
239 #[test]
240 fn zero_dt_does_not_move() {
241 let mut v = SpringValue::new(0.0, Spring::new(0.7, 6.0));
242 v.set_target(10.0);
243 v.advance(0.0);
244 assert_eq!(v.value(), 0.0);
245 }
246
247 #[test]
248 fn jump_is_instant_and_no_op_retarget_is_clean() {
249 let mut v = SpringValue::new(0.0, Spring::new(0.7, 6.0));
250 v.jump_to(3.0);
251 assert_eq!(v.value(), 3.0);
252 assert!(v.is_settled());
253 v.set_target(3.0);
254 assert!(v.is_settled());
255 assert_eq!(v.value(), 3.0);
256 }
257
258 #[test]
259 fn nudge_adds_velocity() {
260 let mut v = SpringValue::new(0.0, Spring::new(0.7, 6.0));
261 v.nudge(120.0);
262 v.advance(1.0 / 60.0);
263 assert!(v.value() > 0.0, "nudge pushes the value");
264 }
265
266 #[test]
267 fn spring_point_animates_both_axes() {
268 let mut p = SpringPoint::with_default_spring(0.0, 0.0);
269 p.set_target(10.0, -20.0);
270 for _ in 0..600 {
271 p.advance(1.0 / 120.0);
272 }
273 assert!(p.is_settled());
274 assert_eq!(p.point(), Point::new(10.0, -20.0));
275 }
276}