1use crate::loop_mode::Loop;
4use animato_core::{Animatable, Easing, Playable, Update};
5
6#[derive(Clone, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum TweenState {
10 Idle,
12 Running,
14 Paused,
16 Completed,
18}
19
20#[derive(Clone, Debug, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct TweenSnapshot {
27 pub elapsed: f32,
29 pub delay_elapsed: f32,
31 pub loop_count: u32,
33 pub ping_pong_reverse: bool,
35 pub state: TweenState,
37}
38
39#[derive(Clone, Debug)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct Tween<T: Animatable> {
60 pub start: T,
62 pub end: T,
64 pub duration: f32,
66 pub easing: Easing,
68 pub delay: f32,
70 pub time_scale: f32,
72 pub looping: Loop,
74
75 elapsed: f32,
77 delay_elapsed: f32,
78 state: TweenState,
79 loop_count: u32,
80 ping_pong_reverse: bool,
82}
83
84impl<T: Animatable> Tween<T> {
85 #[doc(hidden)]
89 pub(crate) fn from_builder(
90 start: T,
91 end: T,
92 duration: f32,
93 easing: Easing,
94 delay: f32,
95 time_scale: f32,
96 looping: Loop,
97 ) -> Self {
98 let initial_state = if delay > 0.0 {
99 TweenState::Idle
100 } else {
101 TweenState::Running
102 };
103 Self {
104 start,
105 end,
106 duration: duration.max(0.0),
107 easing,
108 delay: delay.max(0.0),
109 time_scale: time_scale.max(0.0),
110 looping,
111 elapsed: 0.0,
112 delay_elapsed: 0.0,
113 state: initial_state,
114 loop_count: 0,
115 ping_pong_reverse: false,
116 }
117 }
118
119 pub fn value(&self) -> T {
133 if self.duration == 0.0 {
134 return self.end.clone();
135 }
136 let raw_t = (self.elapsed / self.duration).clamp(0.0, 1.0);
137 let curved_t = self.easing.apply(raw_t);
138 if self.ping_pong_reverse {
139 self.end.lerp(&self.start, curved_t)
140 } else {
141 self.start.lerp(&self.end, curved_t)
142 }
143 }
144
145 pub fn progress(&self) -> f32 {
147 if self.duration == 0.0 {
148 return 1.0;
149 }
150 (self.elapsed / self.duration).clamp(0.0, 1.0)
151 }
152
153 pub fn eased_progress(&self) -> f32 {
155 self.easing.apply(self.progress())
156 }
157
158 pub fn is_complete(&self) -> bool {
160 self.state == TweenState::Completed
161 }
162
163 pub fn state(&self) -> &TweenState {
165 &self.state
166 }
167
168 pub fn elapsed(&self) -> f32 {
170 self.elapsed
171 }
172
173 pub fn delay_elapsed(&self) -> f32 {
175 self.delay_elapsed
176 }
177
178 pub fn loop_count(&self) -> u32 {
180 self.loop_count
181 }
182
183 pub fn is_ping_pong_reversed(&self) -> bool {
185 self.ping_pong_reverse
186 }
187
188 pub fn snapshot(&self) -> TweenSnapshot {
190 TweenSnapshot {
191 elapsed: self.elapsed,
192 delay_elapsed: self.delay_elapsed,
193 loop_count: self.loop_count,
194 ping_pong_reverse: self.ping_pong_reverse,
195 state: self.state.clone(),
196 }
197 }
198
199 pub fn reset(&mut self) {
201 self.elapsed = 0.0;
202 self.delay_elapsed = 0.0;
203 self.loop_count = 0;
204 self.ping_pong_reverse = false;
205 self.state = if self.delay > 0.0 {
206 TweenState::Idle
207 } else {
208 TweenState::Running
209 };
210 }
211
212 pub fn seek(&mut self, t: f32) {
216 self.elapsed = (t.clamp(0.0, 1.0) * self.duration).max(0.0);
217 if self.state == TweenState::Completed {
218 self.state = TweenState::Running;
219 }
220 }
221
222 pub fn reverse(&mut self) {
247 core::mem::swap(&mut self.start, &mut self.end);
248 self.elapsed = (self.duration - self.elapsed).clamp(0.0, self.duration);
251 if self.state == TweenState::Completed {
252 self.state = TweenState::Running;
253 }
254 }
255
256 pub fn pause(&mut self) {
258 if self.state == TweenState::Running {
259 self.state = TweenState::Paused;
260 }
261 }
262
263 pub fn resume(&mut self) {
265 if self.state == TweenState::Paused {
266 self.state = TweenState::Running;
267 }
268 }
269
270 #[inline]
271 fn playback_duration(&self) -> f32 {
272 match self.looping {
273 Loop::Once => self.delay + self.duration,
274 Loop::Times(n) => self.delay + self.duration * n.max(1) as f32,
275 Loop::PingPongTimes(n) => self.delay + self.duration * n.max(1) as f32,
276 Loop::Forever | Loop::PingPong => f32::INFINITY,
277 }
278 }
279
280 #[inline]
281 fn complete_ping_pong_times(&mut self, passes: u32) -> bool {
282 self.loop_count = passes;
283 self.elapsed = self.duration;
284 self.ping_pong_reverse = passes.is_multiple_of(2);
285 self.state = TweenState::Completed;
286 false
287 }
288}
289
290impl<T: Animatable> Update for Tween<T> {
291 fn update(&mut self, dt: f32) -> bool {
296 let dt = dt.max(0.0);
297
298 match self.state {
299 TweenState::Completed => return false,
300 TweenState::Paused => return true,
301 TweenState::Idle => {
302 self.delay_elapsed += dt;
304 if self.delay_elapsed < self.delay {
305 return true;
306 }
307 let overflow = self.delay_elapsed - self.delay;
309 self.state = TweenState::Running;
310 self.elapsed += overflow * self.time_scale;
311 }
312 TweenState::Running => {
313 self.elapsed += dt * self.time_scale;
314 }
315 }
316
317 if self.duration == 0.0 {
319 self.state = TweenState::Completed;
320 return false;
321 }
322
323 while self.elapsed >= self.duration {
325 match &self.looping {
326 Loop::Once => {
327 self.elapsed = self.duration;
328 self.state = TweenState::Completed;
329 return false;
330 }
331 Loop::Times(n) => {
332 self.loop_count += 1;
333 if self.loop_count >= *n {
334 self.elapsed = self.duration;
335 self.state = TweenState::Completed;
336 return false;
337 }
338 self.elapsed -= self.duration;
339 }
340 Loop::Forever => {
341 self.elapsed -= self.duration;
342 }
343 Loop::PingPong => {
344 self.elapsed -= self.duration;
345 self.ping_pong_reverse = !self.ping_pong_reverse;
346 }
347 Loop::PingPongTimes(n) => {
348 let passes = (*n).max(1);
349 self.loop_count += 1;
350 if self.loop_count >= passes {
351 return self.complete_ping_pong_times(passes);
352 }
353 self.elapsed -= self.duration;
354 self.ping_pong_reverse = !self.ping_pong_reverse;
355 }
356 }
357 }
358
359 true
360 }
361}
362
363impl<T: Animatable> Playable for Tween<T> {
364 fn duration(&self) -> f32 {
365 self.playback_duration()
366 }
367
368 fn reset(&mut self) {
369 Tween::reset(self);
370 }
371
372 fn seek_to(&mut self, progress: f32) {
373 let progress = progress.clamp(0.0, 1.0);
374 let total = self.playback_duration();
375 let finite_total = if total.is_finite() {
376 total
377 } else {
378 self.delay + self.duration
379 };
380
381 Tween::reset(self);
382
383 if finite_total == 0.0 {
384 self.elapsed = self.duration;
385 self.state = TweenState::Completed;
386 return;
387 }
388
389 let secs = finite_total * progress;
390 if secs < self.delay {
391 self.delay_elapsed = secs;
392 self.state = if self.delay > 0.0 {
393 TweenState::Idle
394 } else {
395 TweenState::Running
396 };
397 return;
398 }
399
400 let anim_secs = (secs - self.delay).max(0.0);
401 if self.duration == 0.0 {
402 self.elapsed = 0.0;
403 self.state = TweenState::Completed;
404 return;
405 }
406
407 match self.looping {
408 Loop::Once => {
409 self.elapsed = anim_secs.min(self.duration);
410 self.state = if progress >= 1.0 {
411 TweenState::Completed
412 } else {
413 TweenState::Running
414 };
415 }
416 Loop::Times(n) => {
417 let plays = n.max(1);
418 let total_anim = self.duration * plays as f32;
419 if anim_secs >= total_anim || progress >= 1.0 {
420 self.loop_count = plays;
421 self.elapsed = self.duration;
422 self.state = TweenState::Completed;
423 } else {
424 self.loop_count = (anim_secs / self.duration) as u32;
425 self.elapsed = anim_secs - self.duration * self.loop_count as f32;
426 self.state = TweenState::Running;
427 }
428 }
429 Loop::Forever => {
430 self.elapsed = anim_secs % self.duration;
431 self.state = TweenState::Running;
432 }
433 Loop::PingPong => {
434 let cycle = anim_secs % (self.duration * 2.0);
435 self.ping_pong_reverse = cycle >= self.duration;
436 self.elapsed = if self.ping_pong_reverse {
437 cycle - self.duration
438 } else {
439 cycle
440 };
441 self.state = TweenState::Running;
442 }
443 Loop::PingPongTimes(n) => {
444 let passes = n.max(1);
445 let total_anim = self.duration * passes as f32;
446 if anim_secs >= total_anim || progress >= 1.0 {
447 self.complete_ping_pong_times(passes);
448 } else {
449 self.loop_count = (anim_secs / self.duration) as u32;
450 self.ping_pong_reverse = self.loop_count % 2 == 1;
451 self.elapsed = anim_secs - self.duration * self.loop_count as f32;
452 self.state = TweenState::Running;
453 }
454 }
455 }
456 }
457
458 fn is_complete(&self) -> bool {
459 Tween::is_complete(self)
460 }
461
462 fn as_any(&self) -> &dyn core::any::Any {
463 self
464 }
465
466 fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
467 self
468 }
469}
470
471#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::loop_mode::Loop;
479 use animato_core::Easing;
480
481 fn make(start: f32, end: f32, duration: f32) -> Tween<f32> {
482 Tween::new(start, end).duration(duration).build()
483 }
484
485 #[test]
486 fn value_at_start_equals_start() {
487 let t = make(10.0, 90.0, 2.0);
488 assert_eq!(t.value(), 10.0);
489 }
490
491 #[test]
492 fn value_at_end_equals_end() {
493 let mut t = make(10.0, 90.0, 1.0);
494 t.update(1.0);
495 assert_eq!(t.value(), 90.0);
496 }
497
498 #[test]
499 fn is_complete_after_full_duration() {
500 let mut t = make(0.0, 1.0, 1.0);
501 t.update(1.0);
502 assert!(t.is_complete());
503 }
504
505 #[test]
506 fn large_dt_completes_cleanly() {
507 let mut t = make(0.0, 1.0, 1.0);
508 t.update(100.0);
509 assert!(t.is_complete());
510 assert_eq!(t.value(), 1.0);
511 }
512
513 #[test]
514 fn no_update_after_complete() {
515 let mut t = make(0.0, 1.0, 0.5);
516 t.update(1.0);
517 assert!(!t.update(1.0)); }
519
520 #[test]
521 fn delay_holds_at_start() {
522 let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
523 t.update(0.25); assert_eq!(t.value(), 0.0);
525 assert_eq!(t.state(), &TweenState::Idle);
526 }
527
528 #[test]
529 fn delay_transitions_to_running() {
530 let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
531 t.update(0.5); assert_eq!(t.state(), &TweenState::Running);
533 }
534
535 #[test]
536 fn seek_jumps_to_midpoint() {
537 let mut t = make(0.0, 100.0, 1.0);
538 t.seek(0.5);
539 let t2 = Tween::new(0.0_f32, 100.0)
541 .duration(1.0)
542 .easing(Easing::Linear)
543 .build();
544 let mut t2 = t2;
545 t2.seek(0.5);
546 assert!((t2.value() - 50.0).abs() < 0.01);
547 }
548
549 #[test]
550 fn reverse_swaps_direction() {
551 let mut t = Tween::new(0.0_f32, 100.0)
552 .duration(1.0)
553 .easing(Easing::Linear)
554 .build();
555 t.update(0.4);
557 let before = t.value(); t.reverse();
560 assert!(
562 (t.value() - before).abs() < 1.0,
563 "visual position should be preserved: before={} after={}",
564 before,
565 t.value()
566 );
567 t.update(0.1);
569 assert!(t.value() < before, "value should decrease after reverse");
570 }
571
572 #[test]
573 fn pause_stops_progress() {
574 let mut t = make(0.0, 1.0, 2.0);
575 t.update(0.5);
576 let v_before = t.value();
577 t.pause();
578 t.update(0.5); assert_eq!(t.value(), v_before);
580 }
581
582 #[test]
583 fn resume_continues_progress() {
584 let mut t = make(0.0, 1.0, 2.0);
585 t.update(0.5);
586 t.pause();
587 t.update(0.5); let v_paused = t.value();
589 t.resume();
590 t.update(0.5); assert!(
592 t.value() > v_paused,
593 "resumed tween must advance past v_paused={}",
594 v_paused
595 );
596 }
597
598 #[test]
599 fn loop_times_completes_after_n() {
600 let mut t = Tween::new(0.0_f32, 1.0)
601 .duration(1.0)
602 .looping(Loop::Times(3))
603 .build();
604 t.update(3.0 + f32::EPSILON);
606 assert!(t.is_complete());
607 }
608
609 #[test]
610 fn loop_forever_never_completes() {
611 let mut t = Tween::new(0.0_f32, 1.0)
612 .duration(1.0)
613 .looping(Loop::Forever)
614 .build();
615 for _ in 0..1000 {
616 t.update(0.1);
617 }
618 assert!(!t.is_complete());
619 }
620
621 #[test]
622 fn pingpong_reverses_direction() {
623 let mut t = Tween::new(0.0_f32, 100.0)
624 .duration(1.0)
625 .easing(Easing::Linear)
626 .looping(Loop::PingPong)
627 .build();
628 t.update(1.0);
630 t.update(0.5);
632 let v = t.value();
633 assert!(v > 40.0 && v < 60.0, "pingpong mid-reverse = {}", v);
634 }
635
636 #[test]
637 fn ping_pong_times_even_passes_complete_at_start() {
638 let mut t = Tween::new(0.0_f32, 100.0)
639 .duration(1.0)
640 .easing(Easing::Linear)
641 .looping(Loop::PingPongTimes(2))
642 .build();
643
644 assert!(t.update(1.5));
645 assert_eq!(t.value(), 50.0);
646 assert!(!t.update(0.5));
647 assert!(t.is_complete());
648 assert_eq!(t.value(), 0.0);
649 }
650
651 #[test]
652 fn ping_pong_times_odd_passes_complete_at_end() {
653 let mut t = Tween::new(0.0_f32, 100.0)
654 .duration(1.0)
655 .easing(Easing::Linear)
656 .looping(Loop::PingPongTimes(3))
657 .build();
658
659 assert!(t.update(2.5));
660 assert_eq!(t.value(), 50.0);
661 assert!(!t.update(0.5));
662 assert!(t.is_complete());
663 assert_eq!(t.value(), 100.0);
664 }
665
666 #[test]
667 fn reset_returns_to_idle_with_delay() {
668 let mut t = Tween::new(0.0_f32, 1.0).duration(1.0).delay(0.5).build();
669 t.update(2.0); t.reset();
671 assert_eq!(t.state(), &TweenState::Idle);
672 assert_eq!(t.value(), 0.0);
673 }
674
675 #[test]
676 fn zero_duration_completes_immediately() {
677 let mut t = make(0.0, 100.0, 0.0);
678 t.update(0.0);
679 assert!(t.is_complete());
680 assert_eq!(t.value(), 100.0);
681 }
682
683 #[test]
684 fn negative_dt_is_noop() {
685 let mut t = make(0.0, 100.0, 1.0);
686 t.update(-5.0);
687 assert_eq!(t.value(), 0.0);
688 }
689
690 #[test]
691 fn accessors_snapshot_and_eased_progress_are_current() {
692 let mut t = Tween::new(0.0_f32, 100.0)
693 .duration(2.0)
694 .delay(0.5)
695 .easing(Easing::EaseInQuad)
696 .build();
697
698 t.update(0.25);
699 assert_eq!(t.progress(), 0.0);
700 assert_eq!(t.eased_progress(), 0.0);
701 assert_eq!(t.elapsed(), 0.0);
702 assert_eq!(t.delay_elapsed(), 0.25);
703 assert_eq!(t.loop_count(), 0);
704 assert!(!t.is_ping_pong_reversed());
705
706 let snapshot = t.snapshot();
707 assert_eq!(snapshot.delay_elapsed, 0.25);
708 assert_eq!(snapshot.state, TweenState::Idle);
709 }
710
711 #[test]
712 fn seek_and_reverse_restart_completed_tween() {
713 let mut t = make(0.0, 100.0, 1.0);
714
715 t.update(1.0);
716 assert!(t.is_complete());
717 t.seek(0.25);
718 assert_eq!(t.state(), &TweenState::Running);
719 assert_eq!(t.value(), 25.0);
720
721 t.update(1.0);
722 t.reverse();
723 assert_eq!(t.state(), &TweenState::Running);
724 assert_eq!(t.value(), 100.0);
725 }
726
727 #[test]
728 fn playables_seek_cover_delay_looping_and_downcast() {
729 let mut delayed = Tween::new(0.0_f32, 100.0).duration(1.0).delay(1.0).build();
730 Playable::seek_to(&mut delayed, 0.25);
731 assert_eq!(delayed.state(), &TweenState::Idle);
732 assert_eq!(delayed.delay_elapsed(), 0.5);
733
734 let mut times = Tween::new(0.0_f32, 10.0)
735 .duration(1.0)
736 .looping(Loop::Times(3))
737 .build();
738 Playable::seek_to(&mut times, 0.5);
739 assert_eq!(times.loop_count(), 1);
740 assert_eq!(times.state(), &TweenState::Running);
741 Playable::seek_to(&mut times, 1.0);
742 assert!(Playable::is_complete(×));
743
744 let mut forever = Tween::new(0.0_f32, 10.0)
745 .duration(1.0)
746 .looping(Loop::Forever)
747 .build();
748 Playable::seek_to(&mut forever, 0.75);
749 assert_eq!(forever.value(), 7.5);
750
751 let mut ping_pong = Tween::new(0.0_f32, 10.0)
752 .duration(1.0)
753 .looping(Loop::PingPong)
754 .build();
755 Playable::seek_to(&mut ping_pong, 1.0);
756 assert!(ping_pong.is_ping_pong_reversed());
757 assert_eq!(ping_pong.value(), 10.0);
758
759 let mut ping_pong_times = Tween::new(0.0_f32, 10.0)
760 .duration(1.0)
761 .looping(Loop::PingPongTimes(2))
762 .build();
763 Playable::seek_to(&mut ping_pong_times, 1.0);
764 assert!(Playable::is_complete(&ping_pong_times));
765 assert_eq!(ping_pong_times.value(), 0.0);
766
767 assert_eq!(Playable::duration(×), 3.0);
768 assert_eq!(Playable::duration(&ping_pong_times), 2.0);
769 assert!(Playable::as_any(×).is::<Tween<f32>>());
770 assert!(Playable::as_any_mut(&mut times).is::<Tween<f32>>());
771 Playable::reset(&mut times);
772 assert_eq!(times.state(), &TweenState::Running);
773 }
774
775 #[test]
776 fn playable_seek_handles_zero_duration_and_delay_boundary() {
777 let mut zero = make(0.0, 1.0, 0.0);
778 Playable::seek_to(&mut zero, 0.5);
779 assert_eq!(zero.state(), &TweenState::Completed);
780 assert_eq!(zero.value(), 1.0);
781
782 let mut delayed = Tween::new(0.0_f32, 1.0).duration(1.0).delay(1.0).build();
783 Playable::seek_to(&mut delayed, 0.5);
784 assert_eq!(delayed.state(), &TweenState::Running);
785 assert_eq!(delayed.value(), 0.0);
786 }
787}