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