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