1use super::curve::Curve;
11use super::simulation::{Simulation, SpringDescription, SpringSimulation, Tolerance};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Status {
16 Dismissed,
18 Forward,
20 Reverse,
22 Completed,
24}
25
26#[derive(Clone, Debug)]
29struct InterpolationSimulation {
30 begin: f32,
31 end: f32,
32 duration: f32,
33 curve: Curve,
34}
35
36impl Simulation for InterpolationSimulation {
37 fn x(&self, time: f32) -> f32 {
38 if self.duration <= 0.0 {
39 return self.end;
40 }
41 let t = (time / self.duration).clamp(0.0, 1.0);
42 let shaped = self.curve.transform(t);
43 self.begin + (self.end - self.begin) * shaped
44 }
45
46 fn dx(&self, time: f32) -> f32 {
47 let h = 1e-3;
48 (self.x(time + h) - self.x(time - h)) / (2.0 * h)
49 }
50
51 fn is_done(&self, time: f32) -> bool {
52 time >= self.duration
53 }
54}
55
56#[derive(Clone)]
59struct Repeat {
60 period: f32,
61 reverse: bool,
62 curve: Curve,
63}
64
65pub struct AnimationController {
67 value: f32,
68 velocity: f32,
69 lower: f32,
70 upper: f32,
71 status: Status,
72 active: Option<(Box<dyn Simulation>, f32)>,
74 heading_up: bool,
77 repeat: Option<Repeat>,
79}
80
81impl AnimationController {
82 pub fn new(lower: f32, upper: f32) -> Self {
84 Self {
85 value: lower,
86 velocity: 0.0,
87 lower,
88 upper,
89 status: Status::Dismissed,
90 active: None,
91 heading_up: false,
92 repeat: None,
93 }
94 }
95
96 pub fn unit() -> Self {
98 Self::new(0.0, 1.0)
99 }
100
101 pub fn bounds(&self) -> (f32, f32) {
103 (self.lower, self.upper)
104 }
105
106 pub fn value(&self) -> f32 {
108 self.value
109 }
110
111 pub fn velocity(&self) -> f32 {
113 self.velocity
114 }
115
116 pub fn status(&self) -> Status {
118 self.status
119 }
120
121 pub fn is_animating(&self) -> bool {
123 self.active.is_some()
124 }
125
126 pub fn set_value(&mut self, value: f32) {
128 self.value = value.clamp(self.lower, self.upper);
129 self.velocity = 0.0;
130 self.active = None;
131 self.repeat = None;
132 self.status = self.rest_status();
133 }
134
135 pub fn reset(&mut self) {
137 self.set_value(self.lower);
138 }
139
140 pub fn stop(&mut self) {
143 self.active = None;
144 self.repeat = None;
145 self.velocity = 0.0;
146 self.status = self.rest_status();
147 }
148
149 pub fn forward(&mut self, duration: f32, curve: Curve) {
151 self.animate_to(self.upper, duration, curve);
152 }
153
154 pub fn reverse(&mut self, duration: f32, curve: Curve) {
156 self.animate_to(self.lower, duration, curve);
157 }
158
159 pub fn animate_to(&mut self, target: f32, duration: f32, curve: Curve) {
162 self.repeat = None;
163 self.start_interpolation(target, duration, curve);
164 }
165
166 fn start_interpolation(&mut self, target: f32, duration: f32, curve: Curve) {
169 let target = target.clamp(self.lower, self.upper);
170 self.heading_up = target >= self.value;
171 self.status = if self.heading_up {
172 Status::Forward
173 } else {
174 Status::Reverse
175 };
176 self.active = Some((
177 Box::new(InterpolationSimulation {
178 begin: self.value,
179 end: target,
180 duration,
181 curve,
182 }),
183 0.0,
184 ));
185 }
186
187 pub fn repeat(&mut self, period: f32, reverse: bool, curve: Curve) {
193 self.repeat = Some(Repeat {
194 period,
195 reverse,
196 curve: curve.clone(),
197 });
198 self.start_interpolation(self.upper, period, curve);
200 }
201
202 pub fn fling(&mut self, velocity: f32) {
205 self.repeat = None;
206 let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 1.0);
208 let target = if velocity < 0.0 {
209 self.lower
210 } else {
211 self.upper
212 };
213 self.heading_up = velocity >= 0.0;
214 self.status = if self.heading_up {
215 Status::Forward
216 } else {
217 Status::Reverse
218 };
219 self.active = Some((
220 Box::new(SpringSimulation::new(
221 spring,
222 self.value,
223 target,
224 velocity,
225 Tolerance::default(),
226 )),
227 0.0,
228 ));
229 }
230
231 pub fn spring_to(&mut self, target: f32, spring: SpringDescription, velocity: f32) {
237 let target = target.clamp(self.lower, self.upper);
238 let heading_up = target >= self.value;
239 let sim = SpringSimulation::new(spring, self.value, target, velocity, Tolerance::default());
240 self.drive(Box::new(sim), heading_up);
241 }
242
243 pub fn drive(&mut self, simulation: Box<dyn Simulation>, heading_up: bool) {
245 self.repeat = None;
246 self.heading_up = heading_up;
247 self.status = if heading_up {
248 Status::Forward
249 } else {
250 Status::Reverse
251 };
252 self.active = Some((simulation, 0.0));
253 }
254
255 fn rest_status(&self) -> Status {
257 if (self.value - self.upper).abs() < 1e-4 {
258 Status::Completed
259 } else if (self.value - self.lower).abs() < 1e-4 {
260 Status::Dismissed
261 } else if self.heading_up {
262 Status::Completed
263 } else {
264 Status::Dismissed
265 }
266 }
267
268 pub fn tick(&mut self, dt: f32) -> bool {
271 let Some((sim, elapsed)) = self.active.as_mut() else {
272 return false;
273 };
274 *elapsed += dt;
275 let time = *elapsed;
276 let raw = sim.x(time);
277 let velocity = sim.dx(time);
278 let done = sim.is_done(time);
279 self.value = raw.clamp(self.lower, self.upper);
281 self.velocity = velocity;
282
283 let out_of_bounds = raw <= self.lower - 1e-4 || raw >= self.upper + 1e-4;
286 if done || out_of_bounds {
287 if let Some(rep) = self.repeat.clone() {
289 let target = if rep.reverse {
290 if self.heading_up {
292 self.lower
293 } else {
294 self.upper
295 }
296 } else {
297 self.value = self.lower;
299 self.upper
300 };
301 self.start_interpolation(target, rep.period, rep.curve);
302 return true;
303 }
304 self.velocity = 0.0;
305 self.active = None;
306 self.status = self.rest_status();
307 false
308 } else {
309 true
310 }
311 }
312}
313
314impl Default for AnimationController {
315 fn default() -> Self {
317 Self::unit()
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn settle(ctrl: &mut AnimationController, max_frames: usize) -> usize {
327 let mut frames = 0;
328 while ctrl.tick(0.016) {
329 frames += 1;
330 if frames >= max_frames {
331 break;
332 }
333 }
334 frames
335 }
336
337 #[test]
338 fn forward_reaches_upper_and_completes() {
339 let mut ctrl = AnimationController::unit();
340 assert_eq!(ctrl.status(), Status::Dismissed);
341 ctrl.forward(0.3, Curve::ease_in_out());
342 assert_eq!(ctrl.status(), Status::Forward);
343 assert!(ctrl.is_animating());
344 settle(&mut ctrl, 1000);
345 assert!(
346 (ctrl.value() - 1.0).abs() < 1e-3,
347 "value = {}",
348 ctrl.value()
349 );
350 assert_eq!(ctrl.status(), Status::Completed);
351 assert!(!ctrl.is_animating());
352 }
353
354 #[test]
355 fn reverse_reaches_lower_and_dismisses() {
356 let mut ctrl = AnimationController::unit();
357 ctrl.set_value(1.0);
358 assert_eq!(ctrl.status(), Status::Completed);
359 ctrl.reverse(0.3, Curve::ease_in_out());
360 assert_eq!(ctrl.status(), Status::Reverse);
361 settle(&mut ctrl, 1000);
362 assert!(ctrl.value().abs() < 1e-3);
363 assert_eq!(ctrl.status(), Status::Dismissed);
364 }
365
366 #[test]
367 fn value_is_monotonic_during_forward() {
368 let mut ctrl = AnimationController::unit();
369 ctrl.forward(0.5, Curve::ease_in_out());
370 let mut prev = ctrl.value();
371 while ctrl.tick(0.016) {
372 assert!(
373 ctrl.value() >= prev - 1e-4,
374 "recule : {} < {}",
375 ctrl.value(),
376 prev
377 );
378 assert!(ctrl.value() <= 1.0 + 1e-4);
379 prev = ctrl.value();
380 }
381 }
382
383 #[test]
384 fn fling_upward_settles_at_upper() {
385 let mut ctrl = AnimationController::unit();
386 ctrl.set_value(0.5);
387 ctrl.fling(3.0); assert_eq!(ctrl.status(), Status::Forward);
389 settle(&mut ctrl, 2000);
390 assert!(
391 (ctrl.value() - 1.0).abs() < 1e-2,
392 "value = {}",
393 ctrl.value()
394 );
395 assert_eq!(ctrl.status(), Status::Completed);
396 }
397
398 #[test]
399 fn fling_downward_settles_at_lower() {
400 let mut ctrl = AnimationController::unit();
401 ctrl.set_value(0.5);
402 ctrl.fling(-3.0);
403 settle(&mut ctrl, 2000);
404 assert!(ctrl.value().abs() < 1e-2, "value = {}", ctrl.value());
405 assert_eq!(ctrl.status(), Status::Dismissed);
406 }
407
408 #[test]
409 fn spring_to_settles_at_target_from_velocity() {
410 let mut ctrl = AnimationController::unit();
411 ctrl.set_value(0.2);
412 let spring = SpringDescription::new(1.0, 220.0, 30.0);
414 ctrl.spring_to(1.0, spring, 5.0);
415 assert_eq!(ctrl.status(), Status::Forward);
416 settle(&mut ctrl, 2000);
417 assert!(
418 (ctrl.value() - 1.0).abs() < 1e-2,
419 "value = {}",
420 ctrl.value()
421 );
422 assert_eq!(ctrl.status(), Status::Completed);
423 }
424
425 #[test]
428 fn repeat_never_settles_and_restarts() {
429 let mut ctrl = AnimationController::unit();
430 ctrl.repeat(0.1, false, Curve::Linear);
431 let (mut max, mut saw_restart, mut prev) = (0.0f32, false, ctrl.value());
432 for _ in 0..100 {
433 ctrl.tick(0.016);
435 assert!(ctrl.is_animating(), "a looping controller always animates");
436 assert!(ctrl.value() >= -1e-4 && ctrl.value() <= 1.0 + 1e-4);
437 max = max.max(ctrl.value());
438 if ctrl.value() + 1e-3 < prev {
439 saw_restart = true; }
441 prev = ctrl.value();
442 }
443 assert!(max > 0.9, "reaches the top (max = {max})");
444 assert!(saw_restart, "it restarts: the value drops back");
445 }
446
447 #[test]
449 fn repeat_reverse_ping_pongs() {
450 let mut ctrl = AnimationController::unit();
451 ctrl.repeat(0.1, true, Curve::Linear);
452 let (mut went_up, mut came_down, mut prev) = (false, false, ctrl.value());
453 for _ in 0..40 {
454 ctrl.tick(0.016);
455 if ctrl.value() > prev + 1e-4 {
456 went_up = true;
457 }
458 if went_up && ctrl.value() < prev - 1e-4 {
459 came_down = true;
460 }
461 prev = ctrl.value();
462 }
463 assert!(went_up && came_down, "a round trip: up then down");
464 assert!(ctrl.is_animating());
465 }
466
467 #[test]
469 fn stop_and_reset_end_a_repeat() {
470 let mut ctrl = AnimationController::unit();
471 ctrl.repeat(0.1, false, Curve::Linear);
472 ctrl.tick(0.05);
473 assert!(ctrl.is_animating());
474 ctrl.stop();
475 assert!(!ctrl.is_animating(), "stop halts the loop");
476 assert!(!ctrl.tick(0.016), "nothing left to animate after stop");
477
478 ctrl.repeat(0.1, false, Curve::Linear);
479 ctrl.tick(0.05);
480 ctrl.reset();
481 assert!(!ctrl.is_animating(), "reset halts the loop");
482 assert_eq!(ctrl.value(), 0.0, "reset returns to the lower bound");
483 }
484
485 #[test]
486 fn zero_duration_snaps_immediately() {
487 let mut ctrl = AnimationController::unit();
488 ctrl.forward(0.0, Curve::Linear);
489 ctrl.tick(0.016);
491 assert!((ctrl.value() - 1.0).abs() < 1e-4);
492 assert!(!ctrl.is_animating());
493 }
494}