use super::curve::Curve;
use super::simulation::{Simulation, SpringDescription, SpringSimulation, Tolerance};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Status {
Dismissed,
Forward,
Reverse,
Completed,
}
#[derive(Clone, Debug)]
struct InterpolationSimulation {
begin: f32,
end: f32,
duration: f32,
curve: Curve,
}
impl Simulation for InterpolationSimulation {
fn x(&self, time: f32) -> f32 {
if self.duration <= 0.0 {
return self.end;
}
let t = (time / self.duration).clamp(0.0, 1.0);
let shaped = self.curve.transform(t);
self.begin + (self.end - self.begin) * shaped
}
fn dx(&self, time: f32) -> f32 {
let h = 1e-3;
(self.x(time + h) - self.x(time - h)) / (2.0 * h)
}
fn is_done(&self, time: f32) -> bool {
time >= self.duration
}
}
#[derive(Clone)]
struct Repeat {
period: f32,
reverse: bool,
curve: Curve,
}
pub struct AnimationController {
value: f32,
velocity: f32,
lower: f32,
upper: f32,
status: Status,
active: Option<(Box<dyn Simulation>, f32)>,
heading_up: bool,
repeat: Option<Repeat>,
}
impl AnimationController {
pub fn new(lower: f32, upper: f32) -> Self {
Self {
value: lower,
velocity: 0.0,
lower,
upper,
status: Status::Dismissed,
active: None,
heading_up: false,
repeat: None,
}
}
pub fn unit() -> Self {
Self::new(0.0, 1.0)
}
pub fn bounds(&self) -> (f32, f32) {
(self.lower, self.upper)
}
pub fn value(&self) -> f32 {
self.value
}
pub fn velocity(&self) -> f32 {
self.velocity
}
pub fn status(&self) -> Status {
self.status
}
pub fn is_animating(&self) -> bool {
self.active.is_some()
}
pub fn set_value(&mut self, value: f32) {
self.value = value.clamp(self.lower, self.upper);
self.velocity = 0.0;
self.active = None;
self.repeat = None;
self.status = self.rest_status();
}
pub fn reset(&mut self) {
self.set_value(self.lower);
}
pub fn stop(&mut self) {
self.active = None;
self.repeat = None;
self.velocity = 0.0;
self.status = self.rest_status();
}
pub fn forward(&mut self, duration: f32, curve: Curve) {
self.animate_to(self.upper, duration, curve);
}
pub fn reverse(&mut self, duration: f32, curve: Curve) {
self.animate_to(self.lower, duration, curve);
}
pub fn animate_to(&mut self, target: f32, duration: f32, curve: Curve) {
self.repeat = None;
self.start_interpolation(target, duration, curve);
}
fn start_interpolation(&mut self, target: f32, duration: f32, curve: Curve) {
let target = target.clamp(self.lower, self.upper);
self.heading_up = target >= self.value;
self.status = if self.heading_up {
Status::Forward
} else {
Status::Reverse
};
self.active = Some((
Box::new(InterpolationSimulation {
begin: self.value,
end: target,
duration,
curve,
}),
0.0,
));
}
pub fn repeat(&mut self, period: f32, reverse: bool, curve: Curve) {
self.repeat = Some(Repeat {
period,
reverse,
curve: curve.clone(),
});
self.start_interpolation(self.upper, period, curve);
}
pub fn fling(&mut self, velocity: f32) {
self.repeat = None;
let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 1.0);
let target = if velocity < 0.0 {
self.lower
} else {
self.upper
};
self.heading_up = velocity >= 0.0;
self.status = if self.heading_up {
Status::Forward
} else {
Status::Reverse
};
self.active = Some((
Box::new(SpringSimulation::new(
spring,
self.value,
target,
velocity,
Tolerance::default(),
)),
0.0,
));
}
pub fn spring_to(&mut self, target: f32, spring: SpringDescription, velocity: f32) {
let target = target.clamp(self.lower, self.upper);
let heading_up = target >= self.value;
let sim = SpringSimulation::new(spring, self.value, target, velocity, Tolerance::default());
self.drive(Box::new(sim), heading_up);
}
pub fn drive(&mut self, simulation: Box<dyn Simulation>, heading_up: bool) {
self.repeat = None;
self.heading_up = heading_up;
self.status = if heading_up {
Status::Forward
} else {
Status::Reverse
};
self.active = Some((simulation, 0.0));
}
fn rest_status(&self) -> Status {
if (self.value - self.upper).abs() < 1e-4 {
Status::Completed
} else if (self.value - self.lower).abs() < 1e-4 {
Status::Dismissed
} else if self.heading_up {
Status::Completed
} else {
Status::Dismissed
}
}
pub fn tick(&mut self, dt: f32) -> bool {
let Some((sim, elapsed)) = self.active.as_mut() else {
return false;
};
*elapsed += dt;
let time = *elapsed;
let raw = sim.x(time);
let velocity = sim.dx(time);
let done = sim.is_done(time);
self.value = raw.clamp(self.lower, self.upper);
self.velocity = velocity;
let out_of_bounds = raw <= self.lower - 1e-4 || raw >= self.upper + 1e-4;
if done || out_of_bounds {
if let Some(rep) = self.repeat.clone() {
let target = if rep.reverse {
if self.heading_up {
self.lower
} else {
self.upper
}
} else {
self.value = self.lower;
self.upper
};
self.start_interpolation(target, rep.period, rep.curve);
return true;
}
self.velocity = 0.0;
self.active = None;
self.status = self.rest_status();
false
} else {
true
}
}
}
impl Default for AnimationController {
fn default() -> Self {
Self::unit()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn settle(ctrl: &mut AnimationController, max_frames: usize) -> usize {
let mut frames = 0;
while ctrl.tick(0.016) {
frames += 1;
if frames >= max_frames {
break;
}
}
frames
}
#[test]
fn forward_reaches_upper_and_completes() {
let mut ctrl = AnimationController::unit();
assert_eq!(ctrl.status(), Status::Dismissed);
ctrl.forward(0.3, Curve::ease_in_out());
assert_eq!(ctrl.status(), Status::Forward);
assert!(ctrl.is_animating());
settle(&mut ctrl, 1000);
assert!(
(ctrl.value() - 1.0).abs() < 1e-3,
"value = {}",
ctrl.value()
);
assert_eq!(ctrl.status(), Status::Completed);
assert!(!ctrl.is_animating());
}
#[test]
fn reverse_reaches_lower_and_dismisses() {
let mut ctrl = AnimationController::unit();
ctrl.set_value(1.0);
assert_eq!(ctrl.status(), Status::Completed);
ctrl.reverse(0.3, Curve::ease_in_out());
assert_eq!(ctrl.status(), Status::Reverse);
settle(&mut ctrl, 1000);
assert!(ctrl.value().abs() < 1e-3);
assert_eq!(ctrl.status(), Status::Dismissed);
}
#[test]
fn value_is_monotonic_during_forward() {
let mut ctrl = AnimationController::unit();
ctrl.forward(0.5, Curve::ease_in_out());
let mut prev = ctrl.value();
while ctrl.tick(0.016) {
assert!(
ctrl.value() >= prev - 1e-4,
"recule : {} < {}",
ctrl.value(),
prev
);
assert!(ctrl.value() <= 1.0 + 1e-4);
prev = ctrl.value();
}
}
#[test]
fn fling_upward_settles_at_upper() {
let mut ctrl = AnimationController::unit();
ctrl.set_value(0.5);
ctrl.fling(3.0); assert_eq!(ctrl.status(), Status::Forward);
settle(&mut ctrl, 2000);
assert!(
(ctrl.value() - 1.0).abs() < 1e-2,
"value = {}",
ctrl.value()
);
assert_eq!(ctrl.status(), Status::Completed);
}
#[test]
fn fling_downward_settles_at_lower() {
let mut ctrl = AnimationController::unit();
ctrl.set_value(0.5);
ctrl.fling(-3.0);
settle(&mut ctrl, 2000);
assert!(ctrl.value().abs() < 1e-2, "value = {}", ctrl.value());
assert_eq!(ctrl.status(), Status::Dismissed);
}
#[test]
fn spring_to_settles_at_target_from_velocity() {
let mut ctrl = AnimationController::unit();
ctrl.set_value(0.2);
let spring = SpringDescription::new(1.0, 220.0, 30.0);
ctrl.spring_to(1.0, spring, 5.0);
assert_eq!(ctrl.status(), Status::Forward);
settle(&mut ctrl, 2000);
assert!(
(ctrl.value() - 1.0).abs() < 1e-2,
"value = {}",
ctrl.value()
);
assert_eq!(ctrl.status(), Status::Completed);
}
#[test]
fn repeat_never_settles_and_restarts() {
let mut ctrl = AnimationController::unit();
ctrl.repeat(0.1, false, Curve::Linear);
let (mut max, mut saw_restart, mut prev) = (0.0f32, false, ctrl.value());
for _ in 0..100 {
ctrl.tick(0.016);
assert!(ctrl.is_animating(), "a looping controller always animates");
assert!(ctrl.value() >= -1e-4 && ctrl.value() <= 1.0 + 1e-4);
max = max.max(ctrl.value());
if ctrl.value() + 1e-3 < prev {
saw_restart = true; }
prev = ctrl.value();
}
assert!(max > 0.9, "reaches the top (max = {max})");
assert!(saw_restart, "it restarts: the value drops back");
}
#[test]
fn repeat_reverse_ping_pongs() {
let mut ctrl = AnimationController::unit();
ctrl.repeat(0.1, true, Curve::Linear);
let (mut went_up, mut came_down, mut prev) = (false, false, ctrl.value());
for _ in 0..40 {
ctrl.tick(0.016);
if ctrl.value() > prev + 1e-4 {
went_up = true;
}
if went_up && ctrl.value() < prev - 1e-4 {
came_down = true;
}
prev = ctrl.value();
}
assert!(went_up && came_down, "a round trip: up then down");
assert!(ctrl.is_animating());
}
#[test]
fn stop_and_reset_end_a_repeat() {
let mut ctrl = AnimationController::unit();
ctrl.repeat(0.1, false, Curve::Linear);
ctrl.tick(0.05);
assert!(ctrl.is_animating());
ctrl.stop();
assert!(!ctrl.is_animating(), "stop halts the loop");
assert!(!ctrl.tick(0.016), "nothing left to animate after stop");
ctrl.repeat(0.1, false, Curve::Linear);
ctrl.tick(0.05);
ctrl.reset();
assert!(!ctrl.is_animating(), "reset halts the loop");
assert_eq!(ctrl.value(), 0.0, "reset returns to the lower bound");
}
#[test]
fn zero_duration_snaps_immediately() {
let mut ctrl = AnimationController::unit();
ctrl.forward(0.0, Curve::Linear);
ctrl.tick(0.016);
assert!((ctrl.value() - 1.0).abs() < 1e-4);
assert!(!ctrl.is_animating());
}
}