1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::{Duration, Instant};
4
5use reactive_core::{ReadSignal, RwSignal, signal};
6
7use crate::easing::Easing;
8use crate::lerp::Lerp;
9use crate::ticker::{self, Tickable};
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum Repeat {
14 Once,
15 Loop,
16 PingPong,
17}
18
19struct Step<T: Lerp> {
22 start: T,
23 end: T,
24 duration: Duration,
25 easing: Easing,
26}
27
28#[derive(Clone, Copy, PartialEq)]
30enum Direction {
31 Forward,
32 Backward,
33}
34
35fn locate<T: Lerp>(steps: &[Step<T>], t: f32) -> (usize, f32, f32) {
38 let mut acc = 0.0;
39 let last_idx = steps.len() - 1;
40 for (i, step) in steps.iter().enumerate() {
41 let end_cum = acc + step.duration.as_secs_f32();
42 if t <= end_cum || i == last_idx {
43 return (i, acc, end_cum);
44 }
45 acc = end_cum;
46 }
47 unreachable!("steps is never empty: KeyframesBuilder::start() guarantees at least one step")
48}
49
50fn value_at<T: Lerp>(steps: &[Step<T>], t: f32) -> T {
53 let (idx, start_cum, end_cum) = locate(steps, t);
54 let step = &steps[idx];
55 let dur = end_cum - start_cum;
56 let local = if dur <= 0.0 {
57 1.0
58 } else {
59 ((t - start_cum) / dur).clamp(0.0, 1.0)
60 };
61 step.start.lerp(&step.end, step.easing.apply(local))
62}
63
64pub(crate) struct KeyframesInner<T: Lerp + 'static> {
65 signal: RwSignal<T>,
66 steps: Vec<Step<T>>,
67 total_duration: f32,
68 initial: T,
69 repeat: Repeat,
70 direction: Direction,
71 elapsed_secs: f32,
73 current: T,
74 settled: bool,
76 completed_once: bool,
78 last: Option<Instant>,
79}
80
81impl<T: Lerp + 'static> KeyframesInner<T> {
82 fn integrate(&mut self, now: Instant, scale: f32) -> Option<T> {
83 if self.settled {
84 return None;
85 }
86 if scale <= 0.0 {
87 return Some(self.snap_scale_zero());
88 }
89 let last = match self.last {
90 Some(last) => last,
91 None => {
92 self.last = Some(now);
93 return None;
94 }
95 };
96 self.last = Some(now);
97 let dt = now.saturating_duration_since(last).as_secs_f32() * scale;
98 if dt <= 0.0 {
99 return None;
100 }
101 if self.total_duration <= 0.0 {
103 return matches!(self.repeat, Repeat::Once).then(|| self.finish_once());
104 }
105 self.advance(dt);
106 if matches!(self.repeat, Repeat::Once) && self.elapsed_secs >= self.total_duration {
107 return Some(self.finish_once());
108 }
109 self.current = value_at(&self.steps, self.elapsed_secs);
110 Some(self.current.clone())
111 }
112
113 fn advance(&mut self, dt: f32) {
114 match self.repeat {
115 Repeat::Once => self.elapsed_secs = (self.elapsed_secs + dt).min(self.total_duration),
116 Repeat::Loop => self.elapsed_secs = (self.elapsed_secs + dt) % self.total_duration,
119 Repeat::PingPong => {
120 let mut remaining = dt;
121 while remaining > 0.0 {
122 match self.direction {
123 Direction::Forward => {
124 let to_edge = self.total_duration - self.elapsed_secs;
125 if remaining < to_edge {
126 self.elapsed_secs += remaining;
127 remaining = 0.0;
128 } else {
129 self.elapsed_secs = self.total_duration;
130 remaining -= to_edge;
131 self.direction = Direction::Backward;
132 }
133 }
134 Direction::Backward => {
135 let to_edge = self.elapsed_secs;
136 if remaining < to_edge {
137 self.elapsed_secs -= remaining;
138 remaining = 0.0;
139 } else {
140 self.elapsed_secs = 0.0;
141 remaining -= to_edge;
142 self.direction = Direction::Forward;
143 }
144 }
145 }
146 }
147 }
148 }
149 }
150
151 fn finish_once(&mut self) -> T {
152 self.elapsed_secs = self.total_duration;
153 self.current = self.steps.last().expect("steps is never empty").end.clone();
154 self.completed_once = true;
155 self.settled = true;
156 self.current.clone()
157 }
158
159 fn snap_scale_zero(&mut self) -> T {
163 if matches!(self.repeat, Repeat::Once) {
164 return self.finish_once();
165 }
166 let (_, start_cum, end_cum) = locate(&self.steps, self.elapsed_secs);
167 self.elapsed_secs = match self.direction {
168 Direction::Forward => end_cum,
169 Direction::Backward => start_cum,
170 };
171 self.current = value_at(&self.steps, self.elapsed_secs);
172 self.current.clone()
173 }
174}
175
176impl<T: Lerp + 'static> Tickable for RefCell<KeyframesInner<T>> {
177 fn tick(&self, now: Instant, scale: f32) {
178 let (signal, value) = {
180 let mut inner = self.borrow_mut();
181 let value = inner.integrate(now, scale);
182 (inner.signal.clone(), value)
183 };
184 if let Some(value) = value {
185 signal.set(value);
186 }
187 }
188
189 fn is_settled(&self) -> bool {
190 self.borrow().settled
191 }
192}
193
194pub struct Keyframes<T: Lerp + 'static> {
197 inner: Rc<RefCell<KeyframesInner<T>>>,
198 id: u64,
199}
200
201impl<T: Lerp + 'static> Clone for Keyframes<T> {
202 fn clone(&self) -> Self {
203 Keyframes {
204 inner: Rc::clone(&self.inner),
205 id: self.id,
206 }
207 }
208}
209
210impl<T: Lerp + 'static> Keyframes<T> {
211 #[allow(clippy::new_ret_no_self)]
214 pub fn new(initial: T) -> KeyframesBuilder<T> {
215 KeyframesBuilder {
216 initial: initial.clone(),
217 cursor: initial,
218 steps: Vec::new(),
219 }
220 }
221
222 pub fn get(&self) -> T {
224 self.inner.borrow().signal.get()
225 }
226
227 pub fn read(&self) -> ReadSignal<T> {
229 self.inner.borrow().signal.read_only()
230 }
231
232 pub fn restart(&self) {
234 let (signal, initial) = {
236 let mut inner = self.inner.borrow_mut();
237 inner.elapsed_secs = 0.0;
238 inner.direction = Direction::Forward;
239 inner.current = inner.initial.clone();
240 inner.settled = false;
241 inner.completed_once = false;
242 inner.last = None;
244 (inner.signal.clone(), inner.current.clone())
245 };
246 let weak = Rc::downgrade(&self.inner);
248 ticker::register(self.id, weak);
249 signal.set(initial);
250 }
251
252 pub fn stop(&self) {
254 let mut inner = self.inner.borrow_mut();
255 inner.settled = true;
256 inner.last = None;
257 }
258
259 pub fn is_finished(&self) -> bool {
261 self.inner.borrow().completed_once
262 }
263}
264
265pub struct KeyframesBuilder<T: Lerp + 'static> {
267 initial: T,
268 cursor: T,
270 steps: Vec<Step<T>>,
271}
272
273impl<T: Lerp + 'static> KeyframesBuilder<T> {
274 pub fn then(mut self, value: T, duration: Duration, easing: Easing) -> Self {
276 self.steps.push(Step {
277 start: self.cursor,
278 end: value.clone(),
279 duration,
280 easing,
281 });
282 self.cursor = value;
283 self
284 }
285
286 pub fn hold(mut self, duration: Duration) -> Self {
288 self.steps.push(Step {
289 start: self.cursor.clone(),
290 end: self.cursor.clone(),
291 duration,
292 easing: Easing::Linear,
293 });
294 self
295 }
296
297 pub fn start(self, repeat: Repeat) -> Keyframes<T> {
299 let steps = if self.steps.is_empty() {
300 vec![Step {
302 start: self.initial.clone(),
303 end: self.initial.clone(),
304 duration: Duration::ZERO,
305 easing: Easing::Linear,
306 }]
307 } else {
308 self.steps
309 };
310 let total_duration = steps.iter().map(|s| s.duration.as_secs_f32()).sum();
311 let signal = signal(self.initial.clone());
312 let inner = Rc::new(RefCell::new(KeyframesInner {
313 signal,
314 steps,
315 total_duration,
316 initial: self.initial.clone(),
317 repeat,
318 direction: Direction::Forward,
319 elapsed_secs: 0.0,
320 current: self.initial,
321 settled: false,
322 completed_once: false,
323 last: None,
324 }));
325 let kf = Keyframes {
326 inner,
327 id: ticker::next_id(),
328 };
329 let weak = Rc::downgrade(&kf.inner);
330 ticker::register(kf.id, weak);
331 kf
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::curve::{spring, tween};
339 use crate::ticker::{has_active, reset, set_scale, tick};
340 use crate::{Animated, Easing};
341
342 fn fresh() -> Instant {
343 reset();
344 set_scale(1.0);
345 Instant::now()
346 }
347
348 #[test]
350 fn restart_with_subscribed_effect_does_not_reentrantly_panic() {
351 use std::cell::Cell;
352 use std::rc::Rc;
353 let base = fresh();
354 let kf = Keyframes::new(0.0f32)
355 .then(10.0, Duration::from_millis(100), Easing::Linear)
356 .start(Repeat::Once);
357 let seen = Rc::new(Cell::new(-1.0f32));
358 let seen_c = Rc::clone(&seen);
359 let kf_read = kf.clone();
360 let _e = reactive_core::effect(move || seen_c.set(kf_read.get()));
361 tick(base);
362 tick(base + Duration::from_millis(200));
363 assert!(kf.is_finished());
364 assert_eq!(seen.get(), 10.0);
365
366 kf.restart();
367 assert_eq!(
368 seen.get(),
369 0.0,
370 "effect observes the reset value during restart's flush"
371 );
372 assert!(has_active());
373 tick(base + Duration::from_millis(300));
374 tick(base + Duration::from_millis(350));
375 assert_eq!(seen.get(), 5.0, "sequence replays after restart");
376 }
377
378 #[test]
379 fn once_respects_easing_mid_step_then_chains_then_holds_then_settles() {
380 let base = fresh();
381 let kf = Keyframes::new(0.0f32)
382 .then(1.0, Duration::from_millis(200), Easing::EaseInOut)
383 .then(2.0, Duration::from_millis(100), Easing::Linear)
384 .hold(Duration::from_millis(50))
385 .start(Repeat::Once);
386 tick(base); assert_eq!(kf.get(), 0.0);
388
389 tick(base + Duration::from_millis(100));
391 let expected = 0.0f32.lerp(&1.0, Easing::EaseInOut.apply(0.5));
392 assert!((kf.get() - expected).abs() < 1e-4, "{}", kf.get());
393
394 tick(base + Duration::from_millis(250));
396 assert!((kf.get() - 1.5).abs() < 1e-4, "{}", kf.get());
397
398 tick(base + Duration::from_millis(320));
400 assert!((kf.get() - 2.0).abs() < 1e-4, "{}", kf.get());
401 assert!(has_active());
402 assert!(!kf.is_finished());
403
404 tick(base + Duration::from_millis(400));
406 assert!((kf.get() - 2.0).abs() < 1e-6);
407 assert!(kf.is_finished());
408 assert!(!has_active());
409 }
410
411 #[test]
412 fn loop_wraps_with_a_discrete_jump_and_stays_active() {
413 let base = fresh();
414 let kf = Keyframes::new(0.0f32)
416 .then(1.0, Duration::from_millis(100), Easing::Linear)
417 .start(Repeat::Loop);
418 tick(base);
419 tick(base + Duration::from_millis(250));
421 assert!((kf.get() - 0.5).abs() < 1e-4, "{}", kf.get());
422 assert!(has_active(), "Loop must stay registered indefinitely");
423 }
424
425 #[test]
426 fn pingpong_reverses_and_decreases_on_the_way_back() {
427 let base = fresh();
428 let kf = Keyframes::new(0.0f32)
429 .then(1.0, Duration::from_millis(100), Easing::Linear)
430 .start(Repeat::PingPong);
431 tick(base);
432 tick(base + Duration::from_millis(100));
434 assert!((kf.get() - 1.0).abs() < 1e-4, "{}", kf.get());
435 tick(base + Duration::from_millis(120));
437 assert!(kf.get() < 1.0, "did not decrease: {}", kf.get());
438 assert!((kf.get() - 0.8).abs() < 1e-4, "{}", kf.get());
439 assert!(has_active(), "PingPong must stay registered indefinitely");
440 }
441
442 #[test]
443 fn restart_after_finished_resets_and_reregisters() {
444 let base = fresh();
445 let kf = Keyframes::new(0.0f32)
446 .then(1.0, Duration::from_millis(100), Easing::Linear)
447 .start(Repeat::Once);
448 tick(base);
449 tick(base + Duration::from_millis(100));
450 assert!(kf.is_finished());
451 assert!(!has_active());
452
453 kf.restart();
454 assert_eq!(kf.get(), 0.0);
455 assert!(!kf.is_finished());
456 assert!(has_active());
457
458 tick(base + Duration::from_millis(200)); tick(base + Duration::from_millis(250));
460 assert!((kf.get() - 0.5).abs() < 1e-4, "{}", kf.get());
461 }
462
463 #[test]
464 fn stop_deregisters_and_freezes_without_marking_finished() {
465 let base = fresh();
466 let kf = Keyframes::new(0.0f32)
467 .then(1.0, Duration::from_millis(200), Easing::Linear)
468 .start(Repeat::Once);
469 tick(base);
470 tick(base + Duration::from_millis(100));
471 assert!((kf.get() - 0.5).abs() < 1e-4);
472
473 kf.stop();
474 assert!(!has_active());
475 assert!(!kf.is_finished(), "stop() is not natural completion");
476 assert!((kf.get() - 0.5).abs() < 1e-4, "value moved after stop");
477 }
478
479 #[test]
480 fn spring_presets_build_expected_values() {
481 assert_eq!(crate::Spring::gentle(), spring(120.0, 14.0));
482 assert_eq!(crate::Spring::snappy(), spring(210.0, 20.0));
483 assert_eq!(crate::Spring::bouncy(), spring(180.0, 12.0));
484 }
485
486 #[test]
487 fn ticker_tracks_animated_and_keyframes_together() {
488 let base = fresh();
489 let anim = Animated::new(0.0f32, tween(Duration::from_millis(100), Easing::Linear));
490 anim.retarget(1.0);
491 let kf = Keyframes::new(0.0f32)
492 .then(1.0, Duration::from_millis(100), Easing::Linear)
493 .start(Repeat::Loop);
494
495 tick(base);
496 assert!(has_active());
497 tick(base + Duration::from_millis(50));
498 assert!((anim.get() - 0.5).abs() < 1e-4);
499 assert!((kf.get() - 0.5).abs() < 1e-4);
500 assert!(has_active());
501
502 tick(base + Duration::from_millis(100));
504 assert!((anim.get() - 1.0).abs() < 1e-6);
505 assert!(anim.is_settled());
506 assert!(has_active(), "Keyframes loop must keep the ticker active");
507
508 kf.stop();
509 assert!(!has_active());
510 }
511}