1use std::time::Duration;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Easing {
13 Linear,
15 EaseIn,
17 #[default]
19 EaseOut,
20 EaseInOut,
22}
23
24impl Easing {
25 pub const ALL: [Self; 4] = [Self::Linear, Self::EaseIn, Self::EaseOut, Self::EaseInOut];
27
28 #[must_use]
30 pub fn name(self) -> &'static str {
31 match self {
32 Self::Linear => "linear",
33 Self::EaseIn => "ease-in",
34 Self::EaseOut => "ease-out",
35 Self::EaseInOut => "ease-in-out",
36 }
37 }
38
39 #[must_use]
41 pub fn apply(self, t: f32) -> f32 {
42 let t = t.clamp(0.0, 1.0);
43 match self {
44 Self::Linear => t,
45 Self::EaseIn => t * t * t,
46 Self::EaseOut => 1.0 - (1.0 - t).powi(3),
47 Self::EaseInOut => {
48 if t < 0.5 {
49 4.0 * t * t * t
50 } else {
51 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
52 }
53 }
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct Tween {
61 from: f32,
62 to: f32,
63 start: Duration,
64 duration: Duration,
65 easing: Easing,
66}
67
68impl Tween {
69 #[must_use]
71 pub fn settled(value: f32) -> Self {
72 Self { from: value, to: value, start: Duration::ZERO, duration: Duration::ZERO, easing: Easing::Linear }
73 }
74
75 #[must_use]
77 pub fn value(&self, now: Duration) -> f32 {
78 if self.duration.is_zero() || now >= self.end() {
79 return self.to;
80 }
81 let elapsed = now.saturating_sub(self.start).as_secs_f32() / self.duration.as_secs_f32();
82 self.from + (self.to - self.from) * self.easing.apply(elapsed)
83 }
84
85 #[must_use]
87 pub fn target(&self) -> f32 {
88 self.to
89 }
90
91 #[must_use]
93 pub fn is_running(&self, now: Duration) -> bool {
94 now < self.end() && self.from != self.to
95 }
96
97 fn end(&self) -> Duration {
99 self.start.saturating_add(self.duration)
100 }
101
102 pub fn retarget(&mut self, to: f32, now: Duration, duration: Duration, easing: Easing) {
104 let current = self.value(now);
105 *self = Self { from: current, to, start: now, duration, easing };
106 }
107}
108
109#[must_use]
111pub fn steps(progress: f32, count: u16) -> u16 {
112 let position = (progress.clamp(0.0, 1.0) * f32::from(count)).round();
113 position as u16
115}
116
117#[derive(Debug, Default)]
119pub(crate) struct Tweens {
120 values: Vec<(&'static str, Tween)>,
121}
122
123impl Tweens {
124 pub(crate) fn drive(
127 &mut self,
128 name: &'static str,
129 target: f32,
130 now: Duration,
131 duration: Duration,
132 easing: Easing,
133 ) -> Tween {
134 match self.values.iter_mut().find(|(n, _)| *n == name) {
135 Some((_, tween)) => {
136 if (tween.target() - target).abs() > f32::EPSILON {
137 tween.retarget(target, now, duration, easing);
138 }
139 *tween
140 }
141 None => {
142 let tween = Tween::settled(target);
143 self.values.push((name, tween));
144 tween
145 }
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn easings_start_at_zero_and_end_at_one() {
156 for easing in [Easing::Linear, Easing::EaseIn, Easing::EaseOut, Easing::EaseInOut] {
157 assert!(easing.apply(0.0).abs() < 1e-6);
158 assert!((easing.apply(1.0) - 1.0).abs() < 1e-6);
159 }
160 assert!(Easing::EaseOut.apply(0.5) > 0.5);
161 assert!(Easing::EaseIn.apply(0.5) < 0.5);
162 }
163
164 #[test]
165 fn tween_moves_and_retargets_from_current_value() {
166 let mut tween = Tween::settled(0.0);
167 tween.retarget(10.0, Duration::ZERO, Duration::from_millis(100), Easing::Linear);
168 assert!((tween.value(Duration::from_millis(50)) - 5.0).abs() < 1e-4);
169 assert!(tween.is_running(Duration::from_millis(50)));
170 tween.retarget(0.0, Duration::from_millis(50), Duration::from_millis(100), Easing::Linear);
171 assert!((tween.value(Duration::from_millis(50)) - 5.0).abs() < 1e-4);
172 assert_eq!(tween.value(Duration::from_millis(200)), 0.0);
173 assert!(!tween.is_running(Duration::from_millis(200)));
174 }
175
176 #[test]
177 fn endless_tween_does_not_overflow_the_clock() {
178 let mut tween = Tween::settled(0.0);
179 tween.retarget(1.0, Duration::from_secs(5), Duration::MAX, Easing::Linear);
180 assert!(tween.is_running(Duration::from_secs(6)));
181 assert!(tween.value(Duration::from_secs(6)) < 1e-6);
182 }
183
184 #[test]
185 fn steps_round_to_cells() {
186 assert_eq!(steps(0.0, 3), 0);
187 assert_eq!(steps(0.49, 3), 1);
188 assert_eq!(steps(1.0, 3), 3);
189 assert_eq!(steps(7.0, 3), 3);
190 }
191
192 #[test]
193 fn first_sight_does_not_animate() {
194 let mut tweens = Tweens::default();
195 let first = tweens.drive("x", 1.0, Duration::ZERO, Duration::from_millis(100), Easing::Linear);
196 assert!(!first.is_running(Duration::ZERO));
197 let moving = tweens.drive("x", 0.0, Duration::from_millis(10), Duration::from_millis(100), Easing::Linear);
198 assert!(moving.is_running(Duration::from_millis(20)));
199 }
200}