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)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct Tween<T: Animatable> {
41 pub start: T,
43 pub end: T,
45 pub duration: f32,
47 pub easing: Easing,
49 pub delay: f32,
51 pub time_scale: f32,
53 pub looping: Loop,
55
56 elapsed: f32,
58 delay_elapsed: f32,
59 state: TweenState,
60 loop_count: u32,
61 ping_pong_reverse: bool,
63}
64
65impl<T: Animatable> Tween<T> {
66 #[doc(hidden)]
70 pub(crate) fn from_builder(
71 start: T,
72 end: T,
73 duration: f32,
74 easing: Easing,
75 delay: f32,
76 time_scale: f32,
77 looping: Loop,
78 ) -> Self {
79 let initial_state = if delay > 0.0 {
80 TweenState::Idle
81 } else {
82 TweenState::Running
83 };
84 Self {
85 start,
86 end,
87 duration: duration.max(0.0),
88 easing,
89 delay: delay.max(0.0),
90 time_scale: time_scale.max(0.0),
91 looping,
92 elapsed: 0.0,
93 delay_elapsed: 0.0,
94 state: initial_state,
95 loop_count: 0,
96 ping_pong_reverse: false,
97 }
98 }
99
100 pub fn value(&self) -> T {
114 if self.duration == 0.0 {
115 return self.end.clone();
116 }
117 let raw_t = (self.elapsed / self.duration).clamp(0.0, 1.0);
118 let curved_t = self.easing.apply(raw_t);
119 if self.ping_pong_reverse {
120 self.end.lerp(&self.start, curved_t)
121 } else {
122 self.start.lerp(&self.end, curved_t)
123 }
124 }
125
126 pub fn progress(&self) -> f32 {
128 if self.duration == 0.0 {
129 return 1.0;
130 }
131 (self.elapsed / self.duration).clamp(0.0, 1.0)
132 }
133
134 pub fn eased_progress(&self) -> f32 {
136 self.easing.apply(self.progress())
137 }
138
139 pub fn is_complete(&self) -> bool {
141 self.state == TweenState::Completed
142 }
143
144 pub fn state(&self) -> &TweenState {
146 &self.state
147 }
148
149 pub fn reset(&mut self) {
151 self.elapsed = 0.0;
152 self.delay_elapsed = 0.0;
153 self.loop_count = 0;
154 self.ping_pong_reverse = false;
155 self.state = if self.delay > 0.0 {
156 TweenState::Idle
157 } else {
158 TweenState::Running
159 };
160 }
161
162 pub fn seek(&mut self, t: f32) {
166 self.elapsed = (t.clamp(0.0, 1.0) * self.duration).max(0.0);
167 if self.state == TweenState::Completed {
168 self.state = TweenState::Running;
169 }
170 }
171
172 pub fn reverse(&mut self) {
197 core::mem::swap(&mut self.start, &mut self.end);
198 self.elapsed = (self.duration - self.elapsed).clamp(0.0, self.duration);
201 if self.state == TweenState::Completed {
202 self.state = TweenState::Running;
203 }
204 }
205
206 pub fn pause(&mut self) {
208 if self.state == TweenState::Running {
209 self.state = TweenState::Paused;
210 }
211 }
212
213 pub fn resume(&mut self) {
215 if self.state == TweenState::Paused {
216 self.state = TweenState::Running;
217 }
218 }
219
220 #[inline]
221 fn playback_duration(&self) -> f32 {
222 match self.looping {
223 Loop::Once => self.delay + self.duration,
224 Loop::Times(n) => self.delay + self.duration * n.max(1) as f32,
225 Loop::Forever | Loop::PingPong => f32::INFINITY,
226 }
227 }
228}
229
230impl<T: Animatable> Update for Tween<T> {
231 fn update(&mut self, dt: f32) -> bool {
236 let dt = dt.max(0.0);
237
238 match self.state {
239 TweenState::Completed => return false,
240 TweenState::Paused => return true,
241 TweenState::Idle => {
242 self.delay_elapsed += dt;
244 if self.delay_elapsed < self.delay {
245 return true;
246 }
247 let overflow = self.delay_elapsed - self.delay;
249 self.state = TweenState::Running;
250 self.elapsed += overflow * self.time_scale;
251 }
252 TweenState::Running => {
253 self.elapsed += dt * self.time_scale;
254 }
255 }
256
257 if self.duration == 0.0 {
259 self.state = TweenState::Completed;
260 return false;
261 }
262
263 while self.elapsed >= self.duration {
265 match &self.looping {
266 Loop::Once => {
267 self.elapsed = self.duration;
268 self.state = TweenState::Completed;
269 return false;
270 }
271 Loop::Times(n) => {
272 self.loop_count += 1;
273 if self.loop_count >= *n {
274 self.elapsed = self.duration;
275 self.state = TweenState::Completed;
276 return false;
277 }
278 self.elapsed -= self.duration;
279 }
280 Loop::Forever => {
281 self.elapsed -= self.duration;
282 }
283 Loop::PingPong => {
284 self.elapsed -= self.duration;
285 self.ping_pong_reverse = !self.ping_pong_reverse;
286 }
287 }
288 }
289
290 true
291 }
292}
293
294impl<T: Animatable> Playable for Tween<T> {
295 fn duration(&self) -> f32 {
296 self.playback_duration()
297 }
298
299 fn reset(&mut self) {
300 Tween::reset(self);
301 }
302
303 fn seek_to(&mut self, progress: f32) {
304 let progress = progress.clamp(0.0, 1.0);
305 let total = self.playback_duration();
306 let finite_total = if total.is_finite() {
307 total
308 } else {
309 self.delay + self.duration
310 };
311
312 Tween::reset(self);
313
314 if finite_total == 0.0 {
315 self.elapsed = self.duration;
316 self.state = TweenState::Completed;
317 return;
318 }
319
320 let secs = finite_total * progress;
321 if secs < self.delay {
322 self.delay_elapsed = secs;
323 self.state = if self.delay > 0.0 {
324 TweenState::Idle
325 } else {
326 TweenState::Running
327 };
328 return;
329 }
330
331 let anim_secs = (secs - self.delay).max(0.0);
332 if self.duration == 0.0 {
333 self.elapsed = 0.0;
334 self.state = TweenState::Completed;
335 return;
336 }
337
338 match self.looping {
339 Loop::Once => {
340 self.elapsed = anim_secs.min(self.duration);
341 self.state = if progress >= 1.0 {
342 TweenState::Completed
343 } else {
344 TweenState::Running
345 };
346 }
347 Loop::Times(n) => {
348 let plays = n.max(1);
349 let total_anim = self.duration * plays as f32;
350 if anim_secs >= total_anim || progress >= 1.0 {
351 self.loop_count = plays;
352 self.elapsed = self.duration;
353 self.state = TweenState::Completed;
354 } else {
355 self.loop_count = (anim_secs / self.duration) as u32;
356 self.elapsed = anim_secs - self.duration * self.loop_count as f32;
357 self.state = TweenState::Running;
358 }
359 }
360 Loop::Forever => {
361 self.elapsed = anim_secs % self.duration;
362 self.state = TweenState::Running;
363 }
364 Loop::PingPong => {
365 let cycle = anim_secs % (self.duration * 2.0);
366 self.ping_pong_reverse = cycle >= self.duration;
367 self.elapsed = if self.ping_pong_reverse {
368 cycle - self.duration
369 } else {
370 cycle
371 };
372 self.state = TweenState::Running;
373 }
374 }
375 }
376
377 fn is_complete(&self) -> bool {
378 Tween::is_complete(self)
379 }
380
381 fn as_any(&self) -> &dyn core::any::Any {
382 self
383 }
384
385 fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
386 self
387 }
388}
389
390#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::loop_mode::Loop;
398 use animato_core::Easing;
399
400 fn make(start: f32, end: f32, duration: f32) -> Tween<f32> {
401 Tween::new(start, end).duration(duration).build()
402 }
403
404 #[test]
405 fn value_at_start_equals_start() {
406 let t = make(10.0, 90.0, 2.0);
407 assert_eq!(t.value(), 10.0);
408 }
409
410 #[test]
411 fn value_at_end_equals_end() {
412 let mut t = make(10.0, 90.0, 1.0);
413 t.update(1.0);
414 assert_eq!(t.value(), 90.0);
415 }
416
417 #[test]
418 fn is_complete_after_full_duration() {
419 let mut t = make(0.0, 1.0, 1.0);
420 t.update(1.0);
421 assert!(t.is_complete());
422 }
423
424 #[test]
425 fn large_dt_completes_cleanly() {
426 let mut t = make(0.0, 1.0, 1.0);
427 t.update(100.0);
428 assert!(t.is_complete());
429 assert_eq!(t.value(), 1.0);
430 }
431
432 #[test]
433 fn no_update_after_complete() {
434 let mut t = make(0.0, 1.0, 0.5);
435 t.update(1.0);
436 assert!(!t.update(1.0)); }
438
439 #[test]
440 fn delay_holds_at_start() {
441 let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
442 t.update(0.25); assert_eq!(t.value(), 0.0);
444 assert_eq!(t.state(), &TweenState::Idle);
445 }
446
447 #[test]
448 fn delay_transitions_to_running() {
449 let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
450 t.update(0.5); assert_eq!(t.state(), &TweenState::Running);
452 }
453
454 #[test]
455 fn seek_jumps_to_midpoint() {
456 let mut t = make(0.0, 100.0, 1.0);
457 t.seek(0.5);
458 let t2 = Tween::new(0.0_f32, 100.0)
460 .duration(1.0)
461 .easing(Easing::Linear)
462 .build();
463 let mut t2 = t2;
464 t2.seek(0.5);
465 assert!((t2.value() - 50.0).abs() < 0.01);
466 }
467
468 #[test]
469 fn reverse_swaps_direction() {
470 let mut t = Tween::new(0.0_f32, 100.0)
471 .duration(1.0)
472 .easing(Easing::Linear)
473 .build();
474 t.update(0.4);
476 let before = t.value(); t.reverse();
479 assert!(
481 (t.value() - before).abs() < 1.0,
482 "visual position should be preserved: before={} after={}",
483 before,
484 t.value()
485 );
486 t.update(0.1);
488 assert!(t.value() < before, "value should decrease after reverse");
489 }
490
491 #[test]
492 fn pause_stops_progress() {
493 let mut t = make(0.0, 1.0, 2.0);
494 t.update(0.5);
495 let v_before = t.value();
496 t.pause();
497 t.update(0.5); assert_eq!(t.value(), v_before);
499 }
500
501 #[test]
502 fn resume_continues_progress() {
503 let mut t = make(0.0, 1.0, 2.0);
504 t.update(0.5);
505 t.pause();
506 t.update(0.5); let v_paused = t.value();
508 t.resume();
509 t.update(0.5); assert!(
511 t.value() > v_paused,
512 "resumed tween must advance past v_paused={}",
513 v_paused
514 );
515 }
516
517 #[test]
518 fn loop_times_completes_after_n() {
519 let mut t = Tween::new(0.0_f32, 1.0)
520 .duration(1.0)
521 .looping(Loop::Times(3))
522 .build();
523 t.update(3.0 + f32::EPSILON);
525 assert!(t.is_complete());
526 }
527
528 #[test]
529 fn loop_forever_never_completes() {
530 let mut t = Tween::new(0.0_f32, 1.0)
531 .duration(1.0)
532 .looping(Loop::Forever)
533 .build();
534 for _ in 0..1000 {
535 t.update(0.1);
536 }
537 assert!(!t.is_complete());
538 }
539
540 #[test]
541 fn pingpong_reverses_direction() {
542 let mut t = Tween::new(0.0_f32, 100.0)
543 .duration(1.0)
544 .easing(Easing::Linear)
545 .looping(Loop::PingPong)
546 .build();
547 t.update(1.0);
549 t.update(0.5);
551 let v = t.value();
552 assert!(v > 40.0 && v < 60.0, "pingpong mid-reverse = {}", v);
553 }
554
555 #[test]
556 fn reset_returns_to_idle_with_delay() {
557 let mut t = Tween::new(0.0_f32, 1.0).duration(1.0).delay(0.5).build();
558 t.update(2.0); t.reset();
560 assert_eq!(t.state(), &TweenState::Idle);
561 assert_eq!(t.value(), 0.0);
562 }
563
564 #[test]
565 fn zero_duration_completes_immediately() {
566 let mut t = make(0.0, 100.0, 0.0);
567 t.update(0.0);
568 assert!(t.is_complete());
569 assert_eq!(t.value(), 100.0);
570 }
571
572 #[test]
573 fn negative_dt_is_noop() {
574 let mut t = make(0.0, 100.0, 1.0);
575 t.update(-5.0);
576 assert_eq!(t.value(), 0.0);
577 }
578}