1use core::f32::consts::PI;
14
15use crate::curve::{Curve, KeyframeInterp};
16use crate::CurveError;
17
18pub fn spring(damping: f32, stiffness: f32, mass: f32) -> Result<Curve, CurveError> {
29 let mass = mass.max(1e-3);
30 let stiffness = stiffness.max(1e-3);
31 let zeta = damping.clamp(0.0, 1.0);
32 let omega = (stiffness / mass).sqrt();
33 let omega_d = omega * (1.0 - zeta * zeta).max(0.0).sqrt();
34
35 let scale = 3.0 * PI;
39
40 Curve::from_curve_fn(
41 move |t| {
42 let x = t * scale;
43 1.0 - (-zeta * omega * x).exp() * (omega_d * x).cos()
44 },
45 128,
46 KeyframeInterp::Linear,
47 )
48}
49
50pub fn bounce(bounces: u32, decay: f32) -> Result<Curve, CurveError> {
57 let b = bounces.max(1) as f32;
58 let decay = decay.clamp(0.0, 1.0).max(1e-2);
59 let k = 0.5 + 2.0 * decay;
62
63 Curve::from_curve_fn(
64 move |t| {
65 let envelope = (1.0 - t).max(0.0).powf(k);
66 1.0 - (b * PI * t).cos().abs() * envelope
67 },
68 128,
69 KeyframeInterp::Linear,
70 )
71}
72
73pub fn elastic(period: f32, amplitude: f32) -> Result<Curve, CurveError> {
78 let p = period.max(0.05);
79 let a = amplitude.max(1.0);
80 let s = p / (2.0 * PI) * (1.0f32 / a).asin();
82
83 Curve::from_curve_fn(
84 move |t| {
85 if t <= 0.0 {
86 return 0.0;
87 }
88 if t >= 1.0 {
89 return 1.0;
90 }
91 a * (2.0f32).powf(-10.0 * t) * ((t - s) * 2.0 * PI / p).sin() + 1.0
92 },
93 128,
94 KeyframeInterp::Linear,
95 )
96}
97
98pub fn overshoot(strength: f32) -> Result<Curve, CurveError> {
102 let s = strength.max(0.0);
103 let c1 = s + 1.0;
104
105 Curve::from_curve_fn(
106 move |t| {
107 let u = t - 1.0;
108 u * u * (c1 * u + s) + 1.0
109 },
110 96,
111 KeyframeInterp::Linear,
112 )
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::AnimationCurve;
119
120 const EPS: f32 = 1e-3;
121
122 fn endpoints_reach_zero_and_one(curve: &Curve) {
123 assert!(curve.eval(0.0).abs() < EPS, "start != 0: {}", curve.eval(0.0));
124 assert!(
125 (curve.eval(1.0) - 1.0).abs() < EPS,
126 "end != 1: {}",
127 curve.eval(1.0)
128 );
129 }
130
131 #[test]
132 fn spring_endpoints_and_bounded() {
133 let c = spring(0.5, 180.0, 1.0).unwrap();
134 endpoints_reach_zero_and_one(&c);
135 for i in 0..=50 {
137 let t = i as f32 / 50.0;
138 let v = c.eval(t);
139 assert!(v.abs() < 5.0, "spring blew up at t={t}: {v}");
140 }
141 }
142
143 #[test]
144 fn spring_critical_damping_no_overshoot() {
145 let c = spring(1.0, 180.0, 1.0).unwrap();
146 for i in 0..=50 {
147 let t = i as f32 / 50.0;
148 let v = c.eval(t);
149 assert!(v <= 1.01, "critical spring overshot at t={t}: {v}");
150 }
151 }
152
153 #[test]
154 fn bounce_endpoints() {
155 let c = bounce(3, 0.5).unwrap();
156 endpoints_reach_zero_and_one(&c);
157 }
158
159 #[test]
160 fn elastic_endpoints() {
161 let c = elastic(0.3, 1.0).unwrap();
162 endpoints_reach_zero_and_one(&c);
163 }
164
165 #[test]
166 fn overshoot_zero_strength_is_smooth_ease_out_cubic() {
167 let c = overshoot(0.0).unwrap();
168 for i in 0..=10 {
170 let t = i as f32 / 10.0;
171 let expected = {
172 let u = 1.0 - t;
173 1.0 - u * u * u
174 };
175 assert!(
176 (c.eval(t) - expected).abs() < 5e-3,
177 "at t={t}: got {}, want {expected}",
178 c.eval(t)
179 );
180 }
181 }
182
183 #[test]
184 fn overshoot_positive_strength_overshoots_one() {
185 let c = overshoot(1.7).unwrap();
186 let peak = (0..=100)
188 .map(|i| c.eval(i as f32 / 100.0))
189 .fold(f32::MIN, f32::max);
190 assert!(peak > 1.05, "no overshoot detected, peak = {peak}");
191 }
192}