1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::Instant;
4
5use reactive_core::{ReadSignal, RwSignal, signal};
6
7use crate::curve::{Curve, Spring, Tween};
8use crate::lerp::Lerp;
9use crate::ticker::{self, Tickable};
10
11const NOOP_EPS_SQ: f32 = 1e-12;
13const MAX_SUBSTEP: f32 = 1.0 / 240.0;
15const MAX_FRAME_DT: f32 = 0.1;
17const MIN_FRAME_DT: f32 = 1.0 / 1000.0;
19const DISP_EPS_SQ: f32 = 1e-6;
21const VEL_EPS_SQ: f32 = 1e-6;
22const MIN_MASS: f32 = 1e-4;
23
24pub(crate) struct AnimInner<T: Lerp + 'static> {
25 signal: RwSignal<T>,
26 current: T,
27 target: T,
28 velocity: T,
30 start: T,
32 elapsed_secs: f32,
33 curve: Curve,
34 settled: bool,
35 last: Option<Instant>,
37}
38
39impl<T: Lerp + 'static> AnimInner<T> {
40 fn integrate(&mut self, now: Instant, scale: f32) -> Option<T> {
42 if self.settled {
43 return None;
44 }
45 if scale <= 0.0 {
47 return Some(self.snap_to_target());
48 }
49 let last = match self.last {
50 Some(last) => last,
51 None => {
52 self.last = Some(now);
53 return None;
54 }
55 };
56 let dt = now.saturating_duration_since(last).as_secs_f32() * scale;
57 if dt < MIN_FRAME_DT {
59 return None;
60 }
61 self.last = Some(now);
62 match self.curve {
63 Curve::Tween(t) => self.step_tween(t, dt),
64 Curve::Spring(s) => self.step_spring(s, dt),
65 }
66 }
67
68 fn step_tween(&mut self, t: Tween, dt: f32) -> Option<T> {
69 self.elapsed_secs += dt;
70 let duration = t.duration.as_secs_f32();
71 if duration <= 0.0 || self.elapsed_secs >= duration {
72 return Some(self.snap_to_target());
73 }
74 let eased = t.easing.apply(self.elapsed_secs / duration);
75 self.current = self.start.lerp(&self.target, eased);
76 Some(self.current.clone())
77 }
78
79 fn step_spring(&mut self, s: Spring, dt: f32) -> Option<T> {
80 let dt = dt.min(MAX_FRAME_DT);
81 let steps = (dt / MAX_SUBSTEP).ceil().max(1.0) as u32;
82 let h = dt / steps as f32;
83 let mass = s.mass.max(MIN_MASS);
84 let before = self.current.clone();
85 for _ in 0..steps {
86 let displacement = self.current.sub(&self.target);
88 let force = displacement
89 .scale(-s.stiffness)
90 .sub(&self.velocity.scale(s.damping));
91 let accel = force.scale(1.0 / mass);
92 self.velocity = self.velocity.add(&accel.scale(h));
93 self.current = self.current.add(&self.velocity.scale(h));
94 }
95 let arrived = self.current.sub(&self.target).magnitude_sq() < DISP_EPS_SQ
96 && self.velocity.magnitude_sq() < VEL_EPS_SQ;
97 if arrived || self.value_is_frozen(&before) {
98 return Some(self.snap_to_target());
99 }
100 Some(self.current.clone())
101 }
102
103 fn value_is_frozen(&self, before: &T) -> bool {
105 self.current.sub(before).magnitude_sq() == 0.0
106 }
107
108 fn snap_to_target(&mut self) -> T {
109 self.current = self.target.clone();
110 self.velocity = T::zero();
111 self.settled = true;
112 self.current.clone()
113 }
114}
115
116impl<T: Lerp + 'static> Tickable for RefCell<AnimInner<T>> {
117 fn tick(&self, now: Instant, scale: f32) {
118 let (signal, value) = {
120 let mut inner = self.borrow_mut();
121 let value = inner.integrate(now, scale);
122 (inner.signal.clone(), value)
123 };
124 if let Some(value) = value {
125 signal.set(value);
126 }
127 }
128
129 fn is_settled(&self) -> bool {
130 self.borrow().settled
131 }
132}
133
134pub struct Animated<T: Lerp + 'static> {
136 inner: Rc<RefCell<AnimInner<T>>>,
137 id: u64,
138}
139
140impl<T: Lerp + 'static> Clone for Animated<T> {
141 fn clone(&self) -> Self {
142 Animated {
143 inner: Rc::clone(&self.inner),
144 id: self.id,
145 }
146 }
147}
148
149impl<T: Lerp + 'static> Animated<T> {
150 pub fn new(initial: T, curve: impl Into<Curve>) -> Self {
152 let signal = signal(initial.clone());
153 let inner = Rc::new(RefCell::new(AnimInner {
154 signal,
155 current: initial.clone(),
156 target: initial.clone(),
157 velocity: T::zero(),
158 start: initial,
159 elapsed_secs: 0.0,
160 curve: curve.into(),
161 settled: true,
162 last: None,
163 }));
164 Animated {
165 inner,
166 id: ticker::next_id(),
167 }
168 }
169
170 pub fn retarget(&self, target: T) {
172 {
173 let mut inner = self.inner.borrow_mut();
174 if target.sub(&inner.target).magnitude_sq() <= NOOP_EPS_SQ {
175 return;
176 }
177 match inner.curve {
178 Curve::Spring(_) => {
179 inner.target = target;
180 }
181 Curve::Tween(_) => {
182 inner.start = inner.current.clone();
183 inner.elapsed_secs = 0.0;
184 inner.target = target;
185 }
186 }
187 inner.settled = false;
188 inner.last = None;
190 }
191 let weak = Rc::downgrade(&self.inner);
193 ticker::register(self.id, weak);
194 }
195
196 pub fn get(&self) -> T {
198 self.inner.borrow().signal.get()
199 }
200
201 pub fn read(&self) -> ReadSignal<T> {
203 self.inner.borrow().signal.read_only()
204 }
205
206 pub fn is_settled(&self) -> bool {
208 self.inner.borrow().settled
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use std::time::Duration;
215
216 use super::*;
217 use crate::curve::{spring, tween};
218 use crate::easing::Easing;
219 use crate::ticker::{has_active, reset, set_scale, tick};
220 use geometry_core::Rect;
221
222 fn fresh() -> Instant {
224 reset();
225 set_scale(1.0);
226 Instant::now()
227 }
228
229 #[test]
230 fn tween_reaches_eased_value_at_half_duration() {
231 let base = fresh();
232 let a = Animated::new(0.0f32, tween(Duration::from_millis(200), Easing::EaseInOut));
233 a.retarget(1.0);
234 tick(base);
235 tick(base + Duration::from_millis(100));
236 let expected = 0.0f32.lerp(&1.0, Easing::EaseInOut.apply(0.5));
237 assert!(
238 (a.get() - expected).abs() < 1e-4,
239 "{} != {expected}",
240 a.get()
241 );
242 }
243
244 #[test]
245 fn tween_settles_at_target_and_goes_inactive() {
246 let base = fresh();
247 let a = Animated::new(0.0f32, tween(Duration::from_millis(200), Easing::Linear));
248 a.retarget(1.0);
249 tick(base);
250 tick(base + Duration::from_millis(200));
251 assert!((a.get() - 1.0).abs() < 1e-6);
252 assert!(a.is_settled());
253 assert!(!has_active());
254 }
255
256 #[test]
257 fn first_tick_does_not_move_the_value() {
258 let base = fresh();
259 let a = Animated::new(0.0f32, tween(Duration::from_millis(200), Easing::Linear));
260 a.retarget(1.0);
261 tick(base);
263 assert_eq!(a.get(), 0.0);
264 assert!(has_active());
265 }
266
267 #[test]
268 fn spring_settles_at_target() {
269 let base = fresh();
270 let a = Animated::new(0.0f32, spring(120.0, 22.0));
271 a.retarget(1.0);
272 tick(base);
273 let mut now = base;
274 for _ in 0..1000 {
275 now += Duration::from_millis(16);
276 tick(now);
277 if !has_active() {
278 break;
279 }
280 }
281 assert!(!has_active(), "spring never settled");
282 assert!((a.get() - 1.0).abs() < 1e-2, "settled at {}", a.get());
283 }
284
285 #[test]
286 fn spring_preserves_velocity_across_retarget() {
287 let base = fresh();
288 let a = Animated::new(0.0f32, spring(120.0, 14.0));
290 a.retarget(1.0);
291 tick(base);
292 tick(base + Duration::from_millis(16));
293 tick(base + Duration::from_millis(32));
294 let value_before = a.get();
295 a.retarget(value_before);
297 tick(base + Duration::from_millis(33));
298 tick(base + Duration::from_millis(37));
299 assert!(
300 a.get() > value_before,
301 "momentum lost: {} !> {value_before}",
302 a.get()
303 );
304 }
305
306 #[test]
307 fn retarget_to_current_goal_is_a_noop() {
308 let _ = fresh();
309 let a = Animated::new(5.0f32, tween(Duration::from_millis(200), Easing::Linear));
310 a.retarget(5.0);
311 assert!(a.is_settled());
312 assert!(!has_active());
313 }
314
315 #[test]
316 fn same_target_retarget_does_not_restart_the_tween() {
317 let base = fresh();
318 let a = Animated::new(0.0f32, tween(Duration::from_millis(200), Easing::Linear));
319 a.retarget(1.0);
320 tick(base);
321 tick(base + Duration::from_millis(100));
322 assert!((a.get() - 0.5).abs() < 1e-4);
323 a.retarget(1.0);
325 tick(base + Duration::from_millis(150));
326 assert!((a.get() - 0.75).abs() < 1e-4, "restarted: {}", a.get());
327 }
328
329 #[test]
330 fn scale_zero_jumps_straight_to_target() {
331 let base = fresh();
332 set_scale(0.0);
333 let a = Animated::new(0.0f32, spring(120.0, 14.0));
334 a.retarget(1.0);
335 tick(base);
336 assert_eq!(a.get(), 1.0);
337 assert!(a.is_settled());
338 assert!(!has_active());
339 set_scale(1.0);
340 }
341
342 #[test]
344 fn spring_on_large_coordinates_stops_being_active() {
345 let base = fresh();
346 let a = Animated::new(Rect::new(1920.0, 1080.0, 240.0, 64.0), spring(180.0, 26.0));
347 a.retarget(Rect::new(2400.0, 1080.0, 240.0, 64.0));
348 tick(base);
349 let mut now = base;
350 for _ in 0..600 {
351 now += Duration::from_micros(16_667);
352 tick(now);
353 if !has_active() {
354 break;
355 }
356 }
357 assert!(!has_active(), "spring never deregistered: {:?}", a.get());
358 assert!(
359 (a.get().x - 2400.0).abs() < 1e-2,
360 "settled at {:?}",
361 a.get()
362 );
363 }
364
365 #[test]
367 fn sub_frame_ticks_do_not_consume_elapsed_time() {
368 let base = fresh();
369 let a = Animated::new(0.0f32, tween(Duration::from_millis(200), Easing::Linear));
370 a.retarget(1.0);
371 tick(base);
372 for extra in 1..7u64 {
373 tick(base + Duration::from_micros(extra * 20));
374 }
375 assert_eq!(a.get(), 0.0, "a sub-frame tick moved the value");
376 tick(base + Duration::from_millis(100));
377 assert!(
378 (a.get() - 0.5).abs() < 1e-3,
379 "elapsed time was lost to the sub-frame ticks: {}",
380 a.get()
381 );
382 }
383
384 #[test]
385 fn dropped_animation_deregisters() {
386 let base = fresh();
387 let a = Animated::new(0.0f32, tween(Duration::from_millis(200), Easing::Linear));
388 a.retarget(1.0);
389 tick(base);
390 assert!(has_active());
391 drop(a);
392 assert!(!has_active());
393 }
394}