1use core::f32::consts::PI;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Easing {
12 Linear,
13 QuadIn, QuadOut, QuadInOut,
14 CubicIn, CubicOut, CubicInOut,
15 QuartOut,
16 SineInOut,
17 ExpoOut,
18 BackOut,
19 ElasticOut,
20 BounceOut,
21}
22
23impl Easing {
24 pub fn from_name(s: &str) -> Easing {
26 match s.to_ascii_lowercase().replace('-', "_").as_str() {
27 "linear" => Easing::Linear,
28 "quad_in" | "ease_in" => Easing::QuadIn,
29 "quad_out" | "ease_out" => Easing::QuadOut,
30 "quad_inout" | "ease" | "smooth" => Easing::QuadInOut,
31 "cubic_in" => Easing::CubicIn,
32 "cubic_out" => Easing::CubicOut,
33 "cubic_inout" => Easing::CubicInOut,
34 "quart_out" | "quartout" => Easing::QuartOut,
35 "sine" | "sine_inout" => Easing::SineInOut,
36 "expo" | "expo_out" => Easing::ExpoOut,
37 "back" | "back_out" => Easing::BackOut,
38 "elastic" | "elastic_out"=> Easing::ElasticOut,
39 "bounce" | "bounce_out" => Easing::BounceOut,
40 _ => Easing::QuadInOut,
41 }
42 }
43
44 pub fn apply(self, t: f32) -> f32 {
46 let t = t.clamp(0.0, 1.0);
47 match self {
48 Easing::Linear => t,
49 Easing::QuadIn => t * t,
50 Easing::QuadOut => 1.0 - (1.0 - t) * (1.0 - t),
51 Easing::QuadInOut => if t < 0.5 { 2.0*t*t } else { 1.0 - (-2.0*t+2.0).powi(2)/2.0 },
52 Easing::CubicIn => t*t*t,
53 Easing::CubicOut => 1.0 - (1.0 - t).powi(3),
54 Easing::CubicInOut => if t < 0.5 { 4.0*t*t*t } else { 1.0 - (-2.0*t+2.0).powi(3)/2.0 },
55 Easing::QuartOut => 1.0 - (1.0 - t).powi(4),
56 Easing::SineInOut => -(PI * t).cos() * 0.5 + 0.5,
57 Easing::ExpoOut => if t >= 1.0 { 1.0 } else { 1.0 - 2.0_f32.powf(-10.0 * t) },
58 Easing::BackOut => {
59 let c1 = 1.70158; let c3 = c1 + 1.0;
60 1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
61 }
62 Easing::ElasticOut => {
63 if t == 0.0 || t == 1.0 { return t; }
64 let c4 = (2.0 * PI) / 3.0;
65 2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
66 }
67 Easing::BounceOut => bounce_out(t),
68 }
69 }
70}
71
72fn bounce_out(t: f32) -> f32 {
73 let n1 = 7.5625; let d1 = 2.75;
74 if t < 1.0 / d1 { n1 * t * t }
75 else if t < 2.0 / d1 { let t = t - 1.5/d1; n1 * t * t + 0.75 }
76 else if t < 2.5 / d1 { let t = t - 2.25/d1; n1 * t * t + 0.9375 }
77 else { let t = t - 2.625/d1; n1 * t * t + 0.984375 }
78}
79
80pub fn ease(curve: Easing, a: f32, b: f32, t: f32) -> f32 {
82 a + (b - a) * curve.apply(t)
83}
84
85#[derive(Clone, Copy, Debug)]
90pub struct Spring {
91 pub value: f32,
92 pub target: f32,
93 pub velocity: f32,
94 pub stiffness: f32,
95 pub damping: f32,
96}
97
98impl Spring {
99 pub fn new(value: f32, stiffness: f32, damping: f32) -> Self {
100 Self { value, target: value, velocity: 0.0, stiffness, damping }
101 }
102 pub fn ui(value: f32) -> Self { Self::new(value, 180.0, 18.0) }
104
105 pub fn set_target(&mut self, target: f32) { self.target = target; }
106
107 pub fn update(&mut self, dt: f32) -> f32 {
109 let steps = (dt / 0.004).ceil().max(1.0) as u32;
110 let h = dt / steps as f32;
111 for _ in 0..steps {
112 let force = (self.target - self.value) * self.stiffness - self.velocity * self.damping;
113 self.velocity += force * h;
114 self.value += self.velocity * h;
115 }
116 self.value
117 }
118
119 pub fn is_settled(&self) -> bool {
120 (self.target - self.value).abs() < 1e-3 && self.velocity.abs() < 1e-3
121 }
122}
123
124#[derive(Clone, Copy, Debug)]
126pub struct Tween {
127 pub from: f32,
128 pub to: f32,
129 pub duration: f32,
130 pub elapsed: f32,
131 pub curve: Easing,
132}
133
134impl Tween {
135 pub fn new(from: f32, to: f32, duration: f32, curve: Easing) -> Self {
136 Self { from, to, duration: duration.max(1e-4), elapsed: 0.0, curve }
137 }
138 pub fn update(&mut self, dt: f32) -> f32 {
140 self.elapsed = (self.elapsed + dt).min(self.duration);
141 self.value()
142 }
143 pub fn value(&self) -> f32 {
144 let t = self.elapsed / self.duration;
145 ease(self.curve, self.from, self.to, t)
146 }
147 pub fn done(&self) -> bool { self.elapsed >= self.duration }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn easings_pin_endpoints() {
156 for c in [Easing::Linear, Easing::QuadInOut, Easing::CubicOut, Easing::ExpoOut,
157 Easing::BackOut, Easing::ElasticOut, Easing::BounceOut, Easing::SineInOut] {
158 assert!((c.apply(0.0) - 0.0).abs() < 1e-3, "{c:?} f(0)≠0");
159 assert!((c.apply(1.0) - 1.0).abs() < 1e-3, "{c:?} f(1)≠1");
160 }
161 }
162
163 #[test]
164 fn from_name_aliases() {
165 assert_eq!(Easing::from_name("ease-out"), Easing::QuadOut);
166 assert_eq!(Easing::from_name("ELASTIC"), Easing::ElasticOut);
167 assert_eq!(Easing::from_name("nonsense"), Easing::QuadInOut);
168 }
169
170 #[test]
171 fn spring_settles_at_target() {
172 let mut s = Spring::ui(0.0);
173 s.set_target(1.0);
174 for _ in 0..600 { s.update(1.0/60.0); }
175 assert!(s.is_settled(), "value={} vel={}", s.value, s.velocity);
176 assert!((s.value - 1.0).abs() < 1e-2);
177 }
178
179 #[test]
180 fn tween_runs_to_completion() {
181 let mut tw = Tween::new(10.0, 20.0, 0.5, Easing::CubicOut);
182 assert!(!tw.done());
183 for _ in 0..60 { tw.update(1.0/60.0); }
184 assert!(tw.done());
185 assert!((tw.value() - 20.0).abs() < 1e-3);
186 }
187}