use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpringConfig {
pub period: f32,
pub damping_ratio: f32,
pub epsilon: f32,
}
impl SpringConfig {
pub fn snappy() -> Self {
Self {
period: 0.15,
damping_ratio: 1.0,
epsilon: 0.001,
}
}
pub fn smooth() -> Self {
Self {
period: 0.30,
damping_ratio: 1.0,
epsilon: 0.001,
}
}
pub fn bouncy() -> Self {
Self {
period: 0.40,
damping_ratio: 0.7,
epsilon: 0.005,
}
}
}
impl Default for SpringConfig {
fn default() -> Self {
Self::snappy()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Animated<T: SpringValue> {
pub value: T,
target: T,
velocity: T,
config: SpringConfig,
settled: bool,
}
pub trait SpringValue:
Copy + Default + std::ops::Add<Output = Self> + std::ops::Sub<Output = Self>
{
fn mul_scalar(self, s: f32) -> Self;
fn abs(self) -> Self;
fn max_scalar(self, s: f32) -> Self;
fn is_near_zero(self, epsilon: f32) -> bool;
fn lerp(self, other: Self, t: f32) -> Self;
}
impl SpringValue for f32 {
fn mul_scalar(self, s: f32) -> Self {
self * s
}
fn abs(self) -> Self {
self.abs()
}
fn max_scalar(self, s: f32) -> Self {
self.max(s)
}
fn is_near_zero(self, epsilon: f32) -> bool {
self.abs() < epsilon
}
fn lerp(self, other: Self, t: f32) -> Self {
self + (other - self) * t
}
}
impl SpringValue for glam::Vec2 {
fn mul_scalar(self, s: f32) -> Self {
self * s
}
fn abs(self) -> Self {
self.abs()
}
fn max_scalar(self, s: f32) -> Self {
Self::new(self.x.max(s), self.y.max(s))
}
fn is_near_zero(self, epsilon: f32) -> bool {
self.x.abs() < epsilon && self.y.abs() < epsilon
}
fn lerp(self, other: Self, t: f32) -> Self {
self + (other - self) * t
}
}
impl SpringValue for glam::Vec4 {
fn mul_scalar(self, s: f32) -> Self {
self * s
}
fn abs(self) -> Self {
self.abs()
}
fn max_scalar(self, s: f32) -> Self {
Self::new(self.x.max(s), self.y.max(s), self.z.max(s), self.w.max(s))
}
fn is_near_zero(self, epsilon: f32) -> bool {
self.x.abs() < epsilon
&& self.y.abs() < epsilon
&& self.z.abs() < epsilon
&& self.w.abs() < epsilon
}
fn lerp(self, other: Self, t: f32) -> Self {
self + (other - self) * t
}
}
impl<T: SpringValue + std::cmp::PartialEq> Animated<T> {
pub fn new(initial: T, config: SpringConfig) -> Self {
Self {
value: initial,
target: initial,
velocity: T::default(),
config,
settled: true,
}
}
pub fn default(initial: T) -> Self {
Self::new(initial, SpringConfig::default())
}
pub fn set_target(&mut self, target: T) {
if self.target != target {
self.target = target;
self.settled = false;
}
}
pub fn target(&self) -> T {
self.target
}
pub fn is_settled(&self) -> bool {
self.settled
}
pub fn update(&mut self, dt: f32) {
if self.settled {
return;
}
let omega = 2.0 * std::f32::consts::PI / self.config.period;
let zeta = self.config.damping_ratio;
let displacement = self.value - self.target;
let acceleration =
displacement.mul_scalar(-omega * omega) + self.velocity.mul_scalar(-2.0 * zeta * omega);
self.velocity = self.velocity + acceleration.mul_scalar(dt);
self.value = self.value + self.velocity.mul_scalar(dt);
if displacement.is_near_zero(self.config.epsilon)
&& self.velocity.is_near_zero(self.config.epsilon)
{
self.value = self.target;
self.velocity = T::default();
self.settled = true;
}
}
pub fn update_duration(&mut self, dt: Duration) {
self.update(dt.as_secs_f32());
}
pub fn snap_to_target(&mut self) {
self.value = self.target;
self.velocity = T::default();
self.settled = true;
}
pub fn reset(&mut self, value: T) {
self.value = value;
self.target = value;
self.velocity = T::default();
self.settled = true;
}
pub fn config(&self) -> SpringConfig {
self.config
}
pub fn set_config(&mut self, config: SpringConfig) {
self.config = config;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spring_converges_to_target() {
let mut anim = Animated::new(0.0f32, SpringConfig::snappy());
anim.set_target(1.0);
for _ in 0..100 {
anim.update(0.016);
}
assert!(anim.is_settled());
assert!((anim.value - 1.0).abs() < 0.01);
}
#[test]
fn spring_is_initially_settled() {
let anim = Animated::new(5.0f32, SpringConfig::default());
assert!(anim.is_settled());
}
#[test]
fn spring_snap_to_target() {
let mut anim = Animated::new(0.0f32, SpringConfig::snappy());
anim.set_target(10.0);
anim.snap_to_target();
assert!(anim.is_settled());
assert_eq!(anim.value, 10.0);
}
#[test]
fn spring_reset() {
let mut anim = Animated::new(0.0f32, SpringConfig::snappy());
anim.set_target(10.0);
anim.update(0.016);
anim.reset(5.0);
assert!(anim.is_settled());
assert_eq!(anim.value, 5.0);
assert_eq!(anim.target(), 5.0);
}
#[test]
fn spring_vec2_converges() {
let mut anim = Animated::new(glam::Vec2::ZERO, SpringConfig::smooth());
anim.set_target(glam::Vec2::new(100.0, 200.0));
for _ in 0..200 {
anim.update(0.016);
}
assert!(anim.is_settled());
assert!((anim.value.x - 100.0).abs() < 0.01);
assert!((anim.value.y - 200.0).abs() < 0.01);
}
#[test]
fn spring_does_not_overshoot_critically_damped() {
let mut anim = Animated::new(0.0f32, SpringConfig::snappy());
anim.set_target(1.0);
let mut max_val = 0.0f32;
for _ in 0..200 {
anim.update(0.016);
if anim.value > max_val {
max_val = anim.value;
}
}
assert!(max_val < 1.05, "overshoot: {}", max_val);
}
}