use super::{Easing, Repeat};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpringConfig {
pub stiffness: f64,
pub damping: f64,
pub mass: f64,
pub initial_velocity: f64,
}
impl Default for SpringConfig {
fn default() -> Self {
Self {
stiffness: 100.0,
damping: 10.0,
mass: 1.0,
initial_velocity: 0.0,
}
}
}
impl SpringConfig {
pub fn gentle() -> Self {
Self {
stiffness: 120.0,
damping: 14.0,
..Self::default()
}
}
pub fn bouncy() -> Self {
Self {
stiffness: 300.0,
damping: 10.0,
..Self::default()
}
}
pub fn stiff() -> Self {
Self {
stiffness: 400.0,
damping: 30.0,
..Self::default()
}
}
pub fn snappy() -> Self {
Self {
stiffness: 200.0,
damping: 20.0,
..Self::default()
}
}
pub fn molasses() -> Self {
Self {
stiffness: 60.0,
damping: 12.0,
..Self::default()
}
}
pub fn stiffness(mut self, v: f64) -> Self {
self.stiffness = v;
self
}
pub fn damping(mut self, v: f64) -> Self {
self.damping = v;
self
}
pub fn mass(mut self, v: f64) -> Self {
self.mass = v;
self
}
pub fn initial_velocity(mut self, v: f64) -> Self {
self.initial_velocity = v;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct RedirectOpts {
pub easing: Option<Easing>,
pub duration: Option<u64>,
}
impl RedirectOpts {
pub fn easing(mut self, e: Easing) -> Self {
self.easing = Some(e);
self
}
pub fn duration(mut self, d: u64) -> Self {
self.duration = Some(d);
self
}
}
#[derive(Debug, Clone)]
enum TweenMode {
Timed {
duration_ms: u64,
easing: Easing,
delay_ms: u64,
},
Spring {
config: SpringConfig,
velocity: f64,
},
}
#[derive(Debug, Clone)]
pub struct Tween {
pub from: f64,
pub to: f64,
mode: TweenMode,
pub repeat: Option<Repeat>,
pub auto_reverse: bool,
started_at: Option<u64>,
last_timestamp: Option<u64>,
value: Option<f64>,
finished: bool,
}
impl Tween {
pub fn new(from: f64, to: f64, duration_ms: u64) -> Self {
Self {
from,
to,
mode: TweenMode::Timed {
duration_ms,
easing: Easing::EaseInOut,
delay_ms: 0,
},
repeat: None,
auto_reverse: false,
started_at: None,
last_timestamp: None,
value: None,
finished: false,
}
}
pub fn easing(mut self, e: Easing) -> Self {
if let TweenMode::Timed { easing, .. } = &mut self.mode {
*easing = e;
}
self
}
pub fn delay(mut self, ms: u64) -> Self {
if let TweenMode::Timed { delay_ms, .. } = &mut self.mode {
*delay_ms = ms;
}
self
}
pub fn duration(mut self, ms: u64) -> Self {
if let TweenMode::Timed { duration_ms, .. } = &mut self.mode {
*duration_ms = ms;
}
self
}
pub fn repeat(mut self, n: u32) -> Self {
self.repeat = Some(Repeat::Times(n));
self
}
pub fn repeat_forever(mut self) -> Self {
self.repeat = Some(Repeat::Forever);
self
}
pub fn auto_reverse(mut self, v: bool) -> Self {
self.auto_reverse = v;
self
}
pub fn looping(from: f64, to: f64, duration_ms: u64) -> Self {
Self::new(from, to, duration_ms)
.repeat_forever()
.auto_reverse(true)
}
pub fn spring(from: f64, to: f64, config: SpringConfig) -> Self {
assert!(
config.mass.is_finite() && config.mass > 0.0,
"spring mass must be a positive finite number, got {}",
config.mass
);
let velocity = config.initial_velocity;
Self {
from,
to,
mode: TweenMode::Spring { config, velocity },
repeat: None,
auto_reverse: false,
started_at: None,
last_timestamp: None,
value: None,
finished: false,
}
}
pub fn start(&mut self, timestamp: u64) {
self.started_at = Some(timestamp);
self.last_timestamp = Some(timestamp);
self.value = Some(self.from);
self.finished = false;
}
pub fn start_once(&mut self, timestamp: u64) {
if self.started_at.is_none() {
self.start(timestamp);
}
}
pub fn advance(&mut self, timestamp: u64) {
if self.started_at.is_none() || self.finished {
return;
}
match &self.mode {
TweenMode::Timed { .. } => self.advance_timed(timestamp),
TweenMode::Spring { .. } => self.advance_spring(timestamp),
}
}
pub fn redirect(&mut self, to: f64, timestamp: u64) {
self.redirect_inner(to, timestamp, None);
}
pub fn redirect_with(&mut self, to: f64, timestamp: u64, opts: RedirectOpts) {
self.redirect_inner(to, timestamp, Some(opts));
}
fn redirect_inner(&mut self, to: f64, timestamp: u64, opts: Option<RedirectOpts>) {
let current = self.value.unwrap_or(self.from);
self.from = current;
self.to = to;
if let Some(opts) = opts
&& let TweenMode::Timed {
easing,
duration_ms,
..
} = &mut self.mode
{
if let Some(e) = opts.easing {
*easing = e;
}
if let Some(d) = opts.duration {
*duration_ms = d;
}
}
self.start(timestamp);
}
pub fn value(&self) -> Option<f64> {
self.value
}
pub fn finished(&self) -> bool {
self.finished
}
pub fn running(&self) -> bool {
self.started_at.is_some() && !self.finished
}
pub fn is_spring(&self) -> bool {
matches!(self.mode, TweenMode::Spring { .. })
}
fn advance_timed(&mut self, timestamp: u64) {
let started = self.started_at.unwrap();
let TweenMode::Timed {
duration_ms,
easing,
delay_ms,
} = &self.mode
else {
return;
};
let duration_ms = *duration_ms;
let delay_ms = *delay_ms;
let easing = *easing;
let elapsed = timestamp.saturating_sub(started);
if elapsed < delay_ms {
self.value = Some(self.from);
return;
}
let active_elapsed = elapsed - delay_ms;
if active_elapsed >= duration_ms {
self.handle_cycle_end();
return;
}
let t = active_elapsed as f64 / duration_ms as f64;
let eased_t = apply_easing(t, &easing);
self.value = Some(self.from + (self.to - self.from) * eased_t);
}
fn handle_cycle_end(&mut self) {
match self.repeat {
None => {
self.value = Some(self.to);
self.finished = true;
}
Some(Repeat::Forever) => {
self.restart_cycle();
}
Some(Repeat::Times(n)) if n > 1 => {
self.repeat = Some(Repeat::Times(n - 1));
self.restart_cycle();
}
Some(Repeat::Times(_)) => {
self.value = Some(self.to);
self.finished = true;
}
}
}
fn restart_cycle(&mut self) {
if let Some(started) = self.started_at
&& let TweenMode::Timed { duration_ms, .. } = &self.mode
{
self.started_at = Some(started + duration_ms);
}
if self.auto_reverse {
std::mem::swap(&mut self.from, &mut self.to);
} else {
self.value = Some(self.from);
}
}
fn advance_spring(&mut self, timestamp: u64) {
let last = self.last_timestamp.unwrap_or(timestamp);
let elapsed_ms = timestamp.saturating_sub(last);
self.last_timestamp = Some(timestamp);
if elapsed_ms == 0 {
return;
}
let TweenMode::Spring { config, velocity } = &mut self.mode else {
return;
};
let mut pos = self.value.unwrap_or(self.from);
let mut vel = *velocity;
let dt: f64 = 0.001;
let steps = elapsed_ms.min(1000);
for _ in 0..steps {
let force = -config.stiffness * (pos - self.to) - config.damping * vel;
let acc = force / config.mass;
vel += acc * dt;
pos += vel * dt;
if !pos.is_finite() || !vel.is_finite() {
pos = self.to;
vel = 0.0;
self.finished = true;
break;
}
}
if vel.abs() < 0.01 && (pos - self.to).abs() < 0.001 {
pos = self.to;
vel = 0.0;
self.finished = true;
}
self.value = Some(pos);
*velocity = vel;
}
}
fn apply_easing(t: f64, easing: &Easing) -> f64 {
const C1: f64 = 1.70158;
const C2: f64 = C1 * 1.525;
const C3: f64 = C1 + 1.0;
const C4: f64 = 2.0 * PI / 3.0;
const C5: f64 = 2.0 * PI / 4.5;
const N1: f64 = 7.5625;
const D1: f64 = 2.75;
match easing {
Easing::Linear => t,
Easing::EaseIn => 1.0 - (t * PI / 2.0).cos(),
Easing::EaseOut => (t * PI / 2.0).sin(),
Easing::EaseInOut => -(PI * t).cos().mul_add(0.5, -0.5),
Easing::EaseInQuad => t * t,
Easing::EaseOutQuad => 1.0 - (1.0 - t) * (1.0 - t),
Easing::EaseInOutQuad => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
Easing::EaseInCubic => t * t * t,
Easing::EaseOutCubic => 1.0 - (1.0 - t).powi(3),
Easing::EaseInOutCubic => {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
Easing::EaseInQuart => t * t * t * t,
Easing::EaseOutQuart => 1.0 - (1.0 - t).powi(4),
Easing::EaseInOutQuart => {
if t < 0.5 {
8.0 * t * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(4) / 2.0
}
}
Easing::EaseInQuint => t * t * t * t * t,
Easing::EaseOutQuint => 1.0 - (1.0 - t).powi(5),
Easing::EaseInOutQuint => {
if t < 0.5 {
16.0 * t * t * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(5) / 2.0
}
}
Easing::EaseInExpo => {
if t == 0.0 {
0.0
} else {
2.0_f64.powf(10.0 * t - 10.0)
}
}
Easing::EaseOutExpo => {
if t == 1.0 {
1.0
} else {
1.0 - 2.0_f64.powf(-10.0 * t)
}
}
Easing::EaseInOutExpo => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else if t < 0.5 {
2.0_f64.powf(20.0 * t - 10.0) / 2.0
} else {
(2.0 - 2.0_f64.powf(-20.0 * t + 10.0)) / 2.0
}
}
Easing::EaseInCirc => 1.0 - (1.0 - t * t).sqrt(),
Easing::EaseOutCirc => (1.0 - (t - 1.0) * (t - 1.0)).sqrt(),
Easing::EaseInOutCirc => {
if t < 0.5 {
(1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0
} else {
(1.0 + (1.0 - (-2.0 * t + 2.0).powi(2)).sqrt()) / 2.0
}
}
Easing::EaseInBack => C3 * t * t * t - C1 * t * t,
Easing::EaseOutBack => 1.0 + C3 * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2),
Easing::EaseInOutBack => {
if t < 0.5 {
(2.0 * t).powi(2) * ((C2 + 1.0) * 2.0 * t - C2) / 2.0
} else {
((2.0 * t - 2.0).powi(2) * ((C2 + 1.0) * (2.0 * t - 2.0) + C2) + 2.0) / 2.0
}
}
Easing::EaseInElastic => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
-(2.0_f64.powf(10.0 * t - 10.0) * ((10.0 * t - 10.75) * C4).sin())
}
}
Easing::EaseOutElastic => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
2.0_f64.powf(-10.0 * t) * ((10.0 * t - 0.75) * C4).sin() + 1.0
}
}
Easing::EaseInOutElastic => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else if t < 0.5 {
-(2.0_f64.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * C5).sin()) / 2.0
} else {
2.0_f64.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * C5).sin() / 2.0 + 1.0
}
}
Easing::EaseInBounce => 1.0 - ease_out_bounce(1.0 - t, N1, D1),
Easing::EaseOutBounce => ease_out_bounce(t, N1, D1),
Easing::EaseInOutBounce => {
if t < 0.5 {
(1.0 - ease_out_bounce(1.0 - 2.0 * t, N1, D1)) / 2.0
} else {
(1.0 + ease_out_bounce(2.0 * t - 1.0, N1, D1)) / 2.0
}
}
Easing::CubicBezier(x1, y1, x2, y2) => {
cubic_bezier(t, *x1 as f64, *y1 as f64, *x2 as f64, *y2 as f64)
}
}
}
fn ease_out_bounce(t: f64, n1: f64, d1: f64) -> f64 {
if t < 1.0 / d1 {
n1 * t * t
} else if t < 2.0 / d1 {
let t2 = t - 1.5 / d1;
n1 * t2 * t2 + 0.75
} else if t < 2.5 / d1 {
let t2 = t - 2.25 / d1;
n1 * t2 * t2 + 0.9375
} else {
let t2 = t - 2.625 / d1;
n1 * t2 * t2 + 0.984375
}
}
fn cubic_bezier(t: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let s = newton_raphson_solve(t, x1, x2, t, 8);
bezier_eval(s, y1, y2)
}
fn bezier_eval(s: f64, p1: f64, p2: f64) -> f64 {
let s2 = s * s;
let s3 = s2 * s;
3.0 * (1.0 - s) * (1.0 - s) * s * p1 + 3.0 * (1.0 - s) * s2 * p2 + s3
}
fn bezier_derivative(s: f64, p1: f64, p2: f64) -> f64 {
3.0 * (1.0 - s) * (1.0 - s) * p1 + 6.0 * (1.0 - s) * s * (p2 - p1) + 3.0 * s * s * (1.0 - p2)
}
fn newton_raphson_solve(target_x: f64, x1: f64, x2: f64, mut guess: f64, max_iter: u32) -> f64 {
for _ in 0..max_iter {
let x = bezier_eval(guess, x1, x2);
let dx = bezier_derivative(guess, x1, x2);
if (x - target_x).abs() < 1.0e-7 || dx.abs() < 1.0e-7 {
break;
}
guess = (guess - (x - target_x) / dx).clamp(0.0, 1.0);
}
guess
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_easing_is_identity() {
for i in 0..=10 {
let t = i as f64 / 10.0;
let result = apply_easing(t, &Easing::Linear);
assert!((result - t).abs() < 1.0e-10);
}
}
#[test]
fn ease_in_out_boundaries() {
assert!((apply_easing(0.0, &Easing::EaseInOut)).abs() < 1.0e-10);
assert!((apply_easing(1.0, &Easing::EaseInOut) - 1.0).abs() < 1.0e-10);
}
#[test]
fn all_easings_reach_endpoints() {
let easings = [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
Easing::EaseInQuad,
Easing::EaseOutQuad,
Easing::EaseInOutQuad,
Easing::EaseInCubic,
Easing::EaseOutCubic,
Easing::EaseInOutCubic,
Easing::EaseInQuart,
Easing::EaseOutQuart,
Easing::EaseInOutQuart,
Easing::EaseInQuint,
Easing::EaseOutQuint,
Easing::EaseInOutQuint,
Easing::EaseInExpo,
Easing::EaseOutExpo,
Easing::EaseInOutExpo,
Easing::EaseInCirc,
Easing::EaseOutCirc,
Easing::EaseInOutCirc,
Easing::EaseInBack,
Easing::EaseOutBack,
Easing::EaseInOutBack,
Easing::EaseInElastic,
Easing::EaseOutElastic,
Easing::EaseInOutElastic,
Easing::EaseInBounce,
Easing::EaseOutBounce,
Easing::EaseInOutBounce,
];
for e in &easings {
let at_zero = apply_easing(0.0, e);
let at_one = apply_easing(1.0, e);
assert!(at_zero.abs() < 1.0e-10, "{:?} at 0: {}", e, at_zero);
assert!((at_one - 1.0).abs() < 1.0e-10, "{:?} at 1: {}", e, at_one);
}
}
#[test]
fn back_family_overshoots_endpoints_in_middle() {
let near_one = apply_easing(0.95, &Easing::EaseOutBack);
assert!(
near_one > 1.0,
"EaseOutBack should overshoot before settling, got {near_one}",
);
let near_zero = apply_easing(0.05, &Easing::EaseInBack);
assert!(
near_zero < 0.0,
"EaseInBack should undershoot before pulling forward, got {near_zero}",
);
}
#[test]
fn elastic_family_oscillates_around_endpoints() {
let dip = apply_easing(0.1, &Easing::EaseInElastic);
assert!(
dip < 0.05,
"EaseInElastic near 0 should be very small or negative, got {dip}",
);
let early = apply_easing(0.1, &Easing::EaseOutElastic);
assert!(
!(early - 0.1).abs().is_finite() || (early > 0.0),
"EaseOutElastic should produce a non-trivial intermediate, got {early}",
);
}
#[test]
fn cubic_bezier_easing_linear() {
let e = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
for i in 0..=10 {
let t = i as f64 / 10.0;
let result = apply_easing(t, &e);
assert!((result - t).abs() < 0.01, "t={}: {}", t, result);
}
}
#[test]
fn cubic_bezier_endpoints_are_exact() {
for (x1, y1, x2, y2) in [
(0.25_f32, 0.1, 0.25, 1.0), (0.42, 0.0, 1.0, 1.0), (0.0, 0.0, 0.58, 1.0), (0.42, 0.0, 0.58, 1.0), ] {
let e = Easing::CubicBezier(x1, y1, x2, y2);
assert!(apply_easing(0.0, &e).abs() < 1.0e-10);
assert!((apply_easing(1.0, &e) - 1.0).abs() < 1.0e-10);
}
}
#[test]
fn cubic_bezier_ease_curve_resembles_css_ease() {
let e = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
let mid = apply_easing(0.5, &e);
assert!(
(mid - 0.80).abs() < 0.03,
"css `ease` at t=0.5 should be near 0.80, got {mid}",
);
}
#[test]
fn cubic_bezier_ease_in_starts_slow_and_finishes_fast() {
let e = Easing::CubicBezier(0.42, 0.0, 1.0, 1.0);
let early = apply_easing(0.25, &e);
assert!(
early < 0.10,
"css `ease-in` at t=0.25 should still be near 0, got {early}",
);
let late = apply_easing(0.85, &e);
assert!(
late > 0.7,
"css `ease-in` at t=0.85 should be approaching 1, got {late}",
);
}
#[test]
fn cubic_bezier_ease_out_starts_fast_and_finishes_slow() {
let e = Easing::CubicBezier(0.0, 0.0, 0.58, 1.0);
let early = apply_easing(0.25, &e);
assert!(
early > 0.30,
"css `ease-out` at t=0.25 should pull ahead of linear, got {early}",
);
let late = apply_easing(0.85, &e);
assert!(
late > 0.95,
"css `ease-out` at t=0.85 should be very close to 1, got {late}",
);
}
#[test]
fn cubic_bezier_solver_handles_near_degenerate_curve() {
let e = Easing::CubicBezier(0.0, 0.0, 0.0, 1.0);
let mut last = 0.0;
for i in 0..=10 {
let t = i as f64 / 10.0;
let v = apply_easing(t, &e);
assert!(v.is_finite(), "non-finite at t={t}: {v}");
assert!(
v + 1.0e-9 >= last,
"non-monotonic: at t={t} got {v} after {last}",
);
last = v;
}
assert!(apply_easing(0.0, &e).abs() < 1.0e-10);
assert!((apply_easing(1.0, &e) - 1.0).abs() < 1.0e-10);
}
#[test]
fn advance_uses_easing() {
let mut tween = Tween::new(0.0, 100.0, 1000).easing(Easing::EaseInQuad);
tween.start(0);
tween.advance(500);
let v = tween.value().unwrap();
assert!((v - 25.0).abs() < 0.01, "got: {}", v);
}
#[test]
fn repeat_restarts() {
let mut tween = Tween::new(0.0, 100.0, 100).easing(Easing::Linear).repeat(2);
tween.start(0);
tween.advance(100); assert!(!tween.finished());
assert_eq!(tween.repeat, Some(Repeat::Times(1)));
tween.advance(200); assert!(tween.finished());
}
#[test]
fn auto_reverse_swaps_direction() {
let mut tween = Tween::new(0.0, 100.0, 100)
.easing(Easing::Linear)
.repeat_forever()
.auto_reverse(true);
tween.start(0);
tween.advance(100); assert_eq!(tween.from, 100.0);
assert_eq!(tween.to, 0.0);
tween.advance(150); let v = tween.value().unwrap();
assert!((v - 50.0).abs() < 1.0, "got: {}", v);
}
#[test]
fn spring_config_presets() {
let gentle = SpringConfig::gentle();
assert_eq!(gentle.stiffness, 120.0);
assert_eq!(gentle.damping, 14.0);
let bouncy = SpringConfig::bouncy();
assert_eq!(bouncy.stiffness, 300.0);
assert_eq!(bouncy.damping, 10.0);
let stiff = SpringConfig::stiff();
assert_eq!(stiff.stiffness, 400.0);
assert_eq!(stiff.damping, 30.0);
let snappy = SpringConfig::snappy();
assert_eq!(snappy.stiffness, 200.0);
assert_eq!(snappy.damping, 20.0);
let molasses = SpringConfig::molasses();
assert_eq!(molasses.stiffness, 60.0);
assert_eq!(molasses.damping, 12.0);
}
#[test]
fn spring_config_builder() {
let config = SpringConfig::default()
.stiffness(250.0)
.damping(18.0)
.mass(2.0)
.initial_velocity(5.0);
assert_eq!(config.stiffness, 250.0);
assert_eq!(config.damping, 18.0);
assert_eq!(config.mass, 2.0);
assert_eq!(config.initial_velocity, 5.0);
}
#[test]
fn spring_approaches_target() {
let mut tween = Tween::spring(0.0, 100.0, SpringConfig::default());
tween.start(0);
tween.advance(100);
let v = tween.value().unwrap();
assert!(v > 0.0, "spring should move toward target, got: {}", v);
assert!(
v < 100.0,
"spring shouldn't overshoot this early with default damping"
);
assert!(!tween.finished());
}
#[test]
fn spring_settles_at_target() {
let mut tween = Tween::spring(0.0, 100.0, SpringConfig::stiff());
tween.start(0);
for t in (100..=5000).step_by(100) {
tween.advance(t);
if tween.finished() {
break;
}
}
assert!(tween.finished(), "stiff spring should settle within 5s");
assert!(
(tween.value().unwrap() - 100.0).abs() < 0.01,
"settled value should be at target"
);
}
#[test]
fn bouncy_spring_overshoots() {
let mut tween = Tween::spring(0.0, 100.0, SpringConfig::bouncy());
tween.start(0);
let mut max_value: f64 = 0.0;
for t in (1..=2000).step_by(1) {
tween.advance(t);
if let Some(v) = tween.value() {
max_value = max_value.max(v);
}
}
assert!(
max_value > 100.0,
"bouncy spring should overshoot target, max was: {}",
max_value
);
}
#[test]
fn spring_with_initial_velocity() {
let config = SpringConfig::default().initial_velocity(50.0);
let mut tween = Tween::spring(0.0, 100.0, config);
tween.start(0);
tween.advance(50);
let with_velocity = tween.value().unwrap();
let mut tween2 = Tween::spring(0.0, 100.0, SpringConfig::default());
tween2.start(0);
tween2.advance(50);
let without_velocity = tween2.value().unwrap();
assert!(
with_velocity > without_velocity,
"initial velocity should make the spring move faster: {} vs {}",
with_velocity,
without_velocity
);
}
#[test]
#[should_panic(expected = "spring mass must be a positive finite number")]
fn spring_rejects_zero_mass() {
let _ = Tween::spring(0.0, 100.0, SpringConfig::default().mass(0.0));
}
#[test]
#[should_panic(expected = "spring mass must be a positive finite number")]
fn spring_rejects_negative_mass() {
let _ = Tween::spring(0.0, 100.0, SpringConfig::default().mass(-1.0));
}
#[test]
#[should_panic(expected = "spring mass must be a positive finite number")]
fn spring_rejects_infinite_mass() {
let _ = Tween::spring(0.0, 100.0, SpringConfig::default().mass(f64::INFINITY));
}
#[test]
#[should_panic(expected = "spring mass must be a positive finite number")]
fn spring_rejects_nan_mass() {
let _ = Tween::spring(0.0, 100.0, SpringConfig::default().mass(f64::NAN));
}
#[test]
fn spring_is_spring() {
let tween = Tween::spring(0.0, 100.0, SpringConfig::default());
assert!(tween.is_spring());
let tween2 = Tween::new(0.0, 100.0, 500);
assert!(!tween2.is_spring());
}
#[test]
fn redirect_changes_target() {
let mut tween = Tween::new(0.0, 100.0, 1000).easing(Easing::Linear);
tween.start(0);
tween.advance(500);
let mid = tween.value().unwrap();
tween.redirect(50.0, 500);
assert_eq!(tween.from, mid);
assert_eq!(tween.to, 50.0);
assert!(tween.running());
}
#[test]
fn redirect_with_easing_override() {
let mut tween = Tween::new(0.0, 100.0, 1000).easing(Easing::Linear);
tween.start(0);
tween.advance(500);
tween.redirect_with(
200.0,
500,
RedirectOpts::default()
.easing(Easing::EaseInQuad)
.duration(2000),
);
tween.advance(1500);
let v = tween.value().unwrap();
assert!((v - 87.5).abs() < 1.0, "got: {}", v);
}
#[test]
fn spring_redirect_preserves_velocity() {
let mut tween = Tween::spring(0.0, 100.0, SpringConfig::stiff());
tween.start(0);
tween.advance(50);
let value_before = tween.value().unwrap();
assert!(value_before > 0.0);
tween.redirect(200.0, 50);
assert_eq!(tween.from, value_before);
assert_eq!(tween.to, 200.0);
tween.advance(51);
let value_after = tween.value().unwrap();
assert!(
value_after > value_before,
"spring should keep moving after redirect: {} vs {}",
value_after,
value_before
);
}
#[test]
fn spring_large_time_delta_is_capped() {
let mut tween = Tween::spring(0.0, 100.0, SpringConfig::stiff());
tween.start(0);
tween.advance(1_000_000); let v = tween.value().unwrap();
assert!(
v.is_finite(),
"value should be finite after large delta: {}",
v
);
}
#[test]
fn spring_overflow_stops_with_finite_value() {
let config = SpringConfig::default()
.stiffness(f64::MAX)
.damping(0.0)
.mass(f64::MIN_POSITIVE);
let mut tween = Tween::spring(0.0, 100.0, config);
tween.start(0);
tween.advance(1);
assert!(tween.finished());
assert_eq!(tween.value(), Some(100.0));
}
}