pub mod motion;
pub use motion::Motion;
use std::{
fmt::Debug,
time::{Duration, Instant},
};
use crate::{event::Event, Animate};
pub const ESPILON: f32 = 0.005;
pub const MAX_DURATION: Duration = Duration::from_millis(33);
#[derive(Debug, Clone, PartialEq)]
pub struct Spring<T> {
value: T,
target: T,
motion: Motion,
last_update: Instant,
velocity: Vec<f32>,
initial_distance: Vec<f32>,
}
impl<T> Spring<T> {
pub fn with_velocity(mut self, velocity: Vec<f32>) -> Self {
self.velocity = velocity;
self
}
pub fn value(&self) -> &T {
&self.value
}
pub fn target(&self) -> &T {
&self.target
}
pub fn motion(&self) -> Motion {
self.motion
}
pub fn last_update(&self) -> Instant {
self.last_update
}
pub fn set_motion(&mut self, motion: Motion) {
self.motion = motion;
}
pub fn with_motion(mut self, motion: Motion) -> Self {
self.motion = motion;
self
}
}
impl<T> Spring<T>
where
T: Animate,
{
pub fn new(value: T) -> Self {
let motion = Motion::default();
Self {
value: value.clone(),
target: value,
motion,
last_update: Instant::now(),
velocity: vec![0.0; T::components()],
initial_distance: vec![0.0; T::components()],
}
}
pub fn to(mut self, target: T) -> Self {
self.set_target(target);
self
}
pub fn has_energy(&self) -> bool {
self.value != self.target || self.velocity.iter().any(|&v| v != 0.0)
}
pub fn update(&mut self, event: Event<T>) {
match event {
Event::Tick(now) => self.tick(now),
Event::Target(target) => self.set_target(target),
Event::Settle => self.settle(),
Event::SettleAt(target) => self.settle_at(target),
}
}
pub fn tick(&mut self, now: Instant) {
if !self.has_energy() {
return;
}
let dt = now.duration_since(self.last_update).min(MAX_DURATION);
self.last_update = now;
if self.is_near_end() {
self.settle();
return;
}
let velocity: Vec<f32> = self
.target
.distance_to(&self.value)
.into_iter()
.zip(self.velocity.iter().copied())
.map(|(d, v)| self.new_velocity(d, v, dt.as_secs_f32()))
.collect();
self.velocity.clone_from(&velocity);
let mut components = velocity.iter().map(|v| v * dt.as_secs_f32());
self.value.update(&mut components);
}
fn new_velocity(&self, displacement: f32, velocity: f32, dt: f32) -> f32 {
let spring: f32 = displacement * self.motion.applied_stiffness();
let damping = -self.motion.applied_damping() * velocity;
let acceleration = spring + damping;
velocity + acceleration * dt
}
pub fn set_target(&mut self, new_target: T) {
if self.target == new_target {
return;
}
if !self.has_energy() {
self.last_update = Instant::now();
}
self.target = new_target;
self.initial_distance = self.value.distance_to(&self.target);
}
pub fn settle(&mut self) {
self.value = self.target.clone();
self.velocity = vec![0.0; T::components()];
}
pub fn settle_at(&mut self, target: T) {
self.value = target.clone();
self.target = target;
self.velocity = vec![0.0; T::components()];
}
fn is_near_end(&self) -> bool {
self.motion.duration().is_zero()
|| self
.value
.distance_to(&self.target)
.iter()
.zip(&self.initial_distance)
.zip(&self.velocity)
.all(|((d, i), v)| match i {
0.0 => true,
_ => {
let d_percent = (d / i).abs();
let v_percent = (v / i).abs();
d_percent <= ESPILON && v_percent <= ESPILON
}
})
}
}
impl<T> Default for Spring<T>
where
T: Animate + Default,
{
fn default() -> Self {
Self::new(T::default())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn max_duration_time() {
assert_eq!(MAX_DURATION, Duration::from_millis(33));
}
#[test]
fn new_springs_have_no_energy() {
let spring = Spring::new(0.0);
assert!(!spring.has_energy());
}
#[test]
fn has_energy_when_target_is_not_current() {
let spring = Spring::new(0.0).to(5.0);
assert!(spring.has_energy());
}
#[test]
fn has_energy_when_velocity_is_nonzero() {
let spring = Spring::new(0.0).with_velocity(vec![1.0]);
assert!(spring.has_energy());
}
#[test]
fn settle_at() {
let mut spring = Spring::new(0.0).to(3.0);
spring.settle_at(5.0);
assert_eq!(spring.value(), &5.0);
assert_eq!(spring.target(), &5.0);
assert_eq!(spring.velocity, vec![0.0]);
}
#[test]
fn tick_changes_value_and_last_update_time() {
let mut spring = Spring::new(0.0).to(1.0);
let now = Instant::now();
spring.tick(now);
assert!(spring.last_update() == now);
assert!(*spring.value() > 0.0);
}
#[test]
fn set_target_changes_target_and_resets_last_update_time() {
let mut spring = Spring::new(0.0).to(1.0);
let now = Instant::now();
spring.tick(now);
spring.set_target(5.0);
assert_eq!(spring.target, 5.0);
}
#[test]
fn set_target_resets_last_update_when_at_rest() {
let start_time = Instant::now();
let mut spring = Spring::new(0.0);
spring.set_target(5.0);
assert!(spring.last_update > start_time);
}
#[test]
fn set_target_does_not_reset_last_update_with_energy() {
let mut spring = Spring::new(0.0).to(10.0).with_velocity(vec![1.0]);
let update_time = Instant::now();
spring.update(Event::Tick(update_time));
spring.set_target(5.0);
assert_eq!(spring.last_update, update_time);
}
#[test]
fn settle_sets_value_to_target() {
let mut spring = Spring::new(0.0).to(5.0);
spring.settle();
assert_eq!(spring.value(), spring.target());
}
#[test]
fn settle_resets_velocity() {
let mut spring = Spring::new(0.0).to(5.0).with_velocity(vec![1.0]);
spring.settle();
assert_eq!(spring.velocity, vec![0.0]);
}
#[test]
fn default_impl() {
let spring = Spring::<f32>::default();
assert_eq!(spring.value(), &f32::default());
}
#[test]
fn is_near_end_with_zero_duration() {
let spring = Spring::new(0.0).to(1.0).with_motion(Motion {
damping: 0.5,
response: Duration::ZERO,
});
assert!(spring.is_near_end());
}
#[test]
fn update_zero_response() {
let mut spring = Spring::new(0.0).to(1.0);
spring.set_motion(Motion {
response: Duration::ZERO,
damping: 0.5,
});
spring.update(Event::Tick(Instant::now()));
assert_eq!(spring.value(), spring.target());
}
}