1#![forbid(unsafe_code)]
2
3use std::time::Duration;
30
31use super::group::AnimationGroup;
32use super::stagger::{StaggerMode, stagger_offsets};
33use super::{Animation, EasingFn, Fade, Sequence, Slide, delay, ease_in, ease_out, sequence};
34
35#[must_use]
46pub fn cascade_in(
47 count: usize,
48 item_duration: Duration,
49 stagger_delay: Duration,
50 mode: StaggerMode,
51) -> AnimationGroup {
52 let offsets = stagger_offsets(count, stagger_delay, mode);
53 let mut group = AnimationGroup::new();
54 for (i, offset) in offsets.into_iter().enumerate() {
55 let anim = delay(offset, Fade::new(item_duration).easing(ease_out));
56 group.insert(&format!("item_{i}"), Box::new(anim));
57 }
58 group
59}
60
61#[must_use]
66pub fn cascade_out(
67 count: usize,
68 item_duration: Duration,
69 stagger_delay: Duration,
70 mode: StaggerMode,
71) -> AnimationGroup {
72 let offsets = stagger_offsets(count, stagger_delay, mode);
73 let mut group = AnimationGroup::new();
74 for (i, offset) in offsets.into_iter().enumerate() {
75 let anim = delay(
76 offset,
77 InvertedFade(Fade::new(item_duration).easing(ease_in)),
78 );
79 group.insert(&format!("item_{i}"), Box::new(anim));
80 }
81 group
82}
83
84#[must_use]
95pub fn fan_out(count: usize, item_duration: Duration, total_spread: Duration) -> AnimationGroup {
96 if count == 0 {
97 return AnimationGroup::new();
98 }
99
100 let mut group = AnimationGroup::new();
101
102 for i in 0..count {
103 let center = (count as f64 - 1.0) / 2.0;
105 let dist = if count <= 1 {
106 0.0
107 } else {
108 ((i as f64 - center).abs() / center).min(1.0)
109 };
110
111 let eased = 1.0 - (1.0 - dist) * (1.0 - dist);
113 let offset = total_spread.mul_f64(eased);
114
115 let anim = delay(offset, Fade::new(item_duration).easing(ease_out));
116 group.insert(&format!("item_{i}"), Box::new(anim));
117 }
118
119 group
120}
121
122#[must_use]
131pub fn typewriter(char_count: usize, total_duration: Duration) -> TypewriterAnim {
132 TypewriterAnim {
133 char_count,
134 fade: Fade::new(total_duration),
135 }
136}
137
138#[derive(Debug, Clone)]
143pub struct TypewriterAnim {
144 char_count: usize,
145 fade: Fade,
146}
147
148impl TypewriterAnim {
149 pub fn visible_chars(&self) -> usize {
151 let t = self.fade.value();
152 let count = (t * self.char_count as f32).round() as usize;
153 count.min(self.char_count)
154 }
155}
156
157impl Animation for TypewriterAnim {
158 fn tick(&mut self, dt: Duration) {
159 self.fade.tick(dt);
160 }
161
162 fn is_complete(&self) -> bool {
163 self.fade.is_complete()
164 }
165
166 fn value(&self) -> f32 {
167 self.fade.value()
168 }
169
170 fn reset(&mut self) {
171 self.fade.reset();
172 }
173
174 fn overshoot(&self) -> Duration {
175 self.fade.overshoot()
176 }
177}
178
179#[must_use]
191pub fn pulse_sequence(
192 count: usize,
193 pulse_duration: Duration,
194 stagger_delay: Duration,
195) -> AnimationGroup {
196 let offsets = stagger_offsets(count, stagger_delay, StaggerMode::Linear);
197 let mut group = AnimationGroup::new();
198 for (i, offset) in offsets.into_iter().enumerate() {
199 let anim = delay(offset, PulseOnce::new(pulse_duration));
200 group.insert(&format!("pulse_{i}"), Box::new(anim));
201 }
202 group
203}
204
205#[derive(Debug, Clone, Copy)]
207struct PulseOnce {
208 elapsed: Duration,
209 duration: Duration,
210}
211
212impl PulseOnce {
213 fn new(duration: Duration) -> Self {
214 Self {
215 elapsed: Duration::ZERO,
216 duration: if duration.is_zero() {
217 Duration::from_nanos(1)
218 } else {
219 duration
220 },
221 }
222 }
223}
224
225impl Animation for PulseOnce {
226 fn tick(&mut self, dt: Duration) {
227 self.elapsed = self.elapsed.saturating_add(dt);
228 }
229
230 fn is_complete(&self) -> bool {
231 self.elapsed >= self.duration
232 }
233
234 fn value(&self) -> f32 {
235 let t = (self.elapsed.as_secs_f64() / self.duration.as_secs_f64()).min(1.0) as f32;
236 (t * std::f32::consts::PI).sin()
237 }
238
239 fn reset(&mut self) {
240 self.elapsed = Duration::ZERO;
241 }
242
243 fn overshoot(&self) -> Duration {
244 self.elapsed.saturating_sub(self.duration)
245 }
246}
247
248#[must_use]
257pub fn slide_in_left(distance: i16, duration: Duration) -> Slide {
258 Slide::new(-distance, 0, duration).easing(ease_out)
259}
260
261#[must_use]
266pub fn slide_in_right(distance: i16, duration: Duration) -> Slide {
267 Slide::new(distance, 0, duration).easing(ease_out)
268}
269
270#[must_use]
279pub fn fade_through(half_duration: Duration) -> Sequence<InvertedFade, Fade> {
280 let out = InvertedFade(Fade::new(half_duration).easing(ease_in));
281 let into = Fade::new(half_duration).easing(ease_out);
282 sequence(out, into)
283}
284
285#[derive(Debug, Clone, Copy)]
291pub struct InvertedFade(Fade);
292
293impl InvertedFade {
294 pub fn new(duration: Duration) -> Self {
296 Self(Fade::new(duration))
297 }
298
299 pub fn easing(mut self, easing: EasingFn) -> Self {
301 self.0 = self.0.easing(easing);
302 self
303 }
304}
305
306impl Animation for InvertedFade {
307 fn tick(&mut self, dt: Duration) {
308 self.0.tick(dt);
309 }
310
311 fn is_complete(&self) -> bool {
312 self.0.is_complete()
313 }
314
315 fn value(&self) -> f32 {
316 1.0 - self.0.value()
317 }
318
319 fn reset(&mut self) {
320 self.0.reset();
321 }
322
323 fn overshoot(&self) -> Duration {
324 self.0.overshoot()
325 }
326}
327
328#[cfg(test)]
333mod tests {
334 use super::*;
335
336 const MS50: Duration = Duration::from_millis(50);
337 const MS100: Duration = Duration::from_millis(100);
338 const MS200: Duration = Duration::from_millis(200);
339 const MS500: Duration = Duration::from_millis(500);
340
341 #[test]
344 fn cascade_in_empty() {
345 let group = cascade_in(0, MS200, MS50, StaggerMode::Linear);
346 assert!(group.is_empty());
347 assert!(group.all_complete());
348 }
349
350 #[test]
351 fn cascade_in_single_item() {
352 let mut group = cascade_in(1, MS200, MS50, StaggerMode::Linear);
353 assert_eq!(group.len(), 1);
354 assert!(!group.all_complete());
355
356 group.tick(MS200);
358 assert!(group.all_complete());
359 }
360
361 #[test]
362 fn cascade_in_multiple_items_staggered() {
363 let mut group = cascade_in(3, MS200, MS100, StaggerMode::Linear);
364 assert_eq!(group.len(), 3);
365
366 assert!(group.get("item_0").unwrap().value() == 0.0);
368
369 group.tick(MS100);
371 let v0 = group.get("item_0").unwrap().value();
372 let v1 = group.get("item_1").unwrap().value();
373 let v2 = group.get("item_2").unwrap().value();
374 assert!(v0 > 0.0, "item_0 should have progressed");
375 assert!(v1 == 0.0, "item_1 just started (delay elapsed)");
376 assert!(v2 == 0.0, "item_2 hasn't started yet");
377
378 group.tick(Duration::from_millis(300));
380 assert!(group.all_complete());
381 }
382
383 #[test]
384 fn cascade_in_values_increase() {
385 let mut group = cascade_in(5, MS500, MS100, StaggerMode::EaseOut);
386 let mut prev = 0.0f32;
387 for _ in 0..20 {
388 group.tick(MS50);
389 let val = group.overall_progress();
390 assert!(val >= prev, "overall progress should not decrease");
391 prev = val;
392 }
393 }
394
395 #[test]
398 fn cascade_out_starts_near_one() {
399 let mut group = cascade_out(3, MS200, MS50, StaggerMode::Linear);
400 group.tick(Duration::from_nanos(1));
403 let v0 = group.get("item_0").unwrap().value();
404 assert!(
405 (v0 - 1.0).abs() < 0.01,
406 "cascade_out should start near 1.0, got {v0}"
407 );
408 }
409
410 #[test]
411 fn cascade_out_ends_at_zero() {
412 let mut group = cascade_out(3, MS200, MS50, StaggerMode::Linear);
413 group.tick(Duration::from_secs(1));
415 for i in 0..3 {
416 let v = group.get(&format!("item_{i}")).unwrap().value();
417 assert!(
418 v < 0.01,
419 "item_{i} should be near 0.0 after completion, got {v}"
420 );
421 }
422 }
423
424 #[test]
427 fn fan_out_empty() {
428 let group = fan_out(0, MS200, MS200);
429 assert!(group.is_empty());
430 }
431
432 #[test]
433 fn fan_out_single() {
434 let group = fan_out(1, MS200, MS200);
435 assert_eq!(group.len(), 1);
436 let v = group.get("item_0").unwrap().value();
438 assert!((v - 0.0).abs() < 0.01);
439 }
440
441 #[test]
442 fn fan_out_center_starts_first() {
443 let mut group = fan_out(5, MS200, MS200);
444 group.tick(Duration::from_millis(10));
446 let center = group.get("item_2").unwrap().value();
447 let edge = group.get("item_0").unwrap().value();
448 assert!(
449 center >= edge,
450 "center ({center}) should start before edges ({edge})"
451 );
452 }
453
454 #[test]
455 fn fan_out_symmetric() {
456 let group = fan_out(5, MS200, MS200);
457 let v0 = group.get("item_0").unwrap().value();
459 let v4 = group.get("item_4").unwrap().value();
460 assert!(
461 (v0 - v4).abs() < 0.01,
462 "symmetric items should match: {v0} vs {v4}"
463 );
464
465 let v1 = group.get("item_1").unwrap().value();
466 let v3 = group.get("item_3").unwrap().value();
467 assert!(
468 (v1 - v3).abs() < 0.01,
469 "symmetric items should match: {v1} vs {v3}"
470 );
471 }
472
473 #[test]
476 fn typewriter_starts_at_zero() {
477 let tw = typewriter(100, MS500);
478 assert_eq!(tw.visible_chars(), 0);
479 }
480
481 #[test]
482 fn typewriter_ends_at_full() {
483 let mut tw = typewriter(100, MS500);
484 tw.tick(MS500);
485 assert_eq!(tw.visible_chars(), 100);
486 assert!(tw.is_complete());
487 }
488
489 #[test]
490 fn typewriter_progresses_monotonically() {
491 let mut tw = typewriter(50, MS500);
492 let mut prev = 0;
493 for _ in 0..20 {
494 tw.tick(Duration::from_millis(25));
495 let chars = tw.visible_chars();
496 assert!(
497 chars >= prev,
498 "visible chars should not decrease: {prev} -> {chars}"
499 );
500 prev = chars;
501 }
502 }
503
504 #[test]
505 fn typewriter_zero_chars() {
506 let mut tw = typewriter(0, MS200);
507 assert_eq!(tw.visible_chars(), 0);
508 tw.tick(MS200);
509 assert_eq!(tw.visible_chars(), 0);
510 assert!(tw.is_complete());
511 }
512
513 #[test]
516 fn pulse_sequence_empty() {
517 let group = pulse_sequence(0, MS200, MS100);
518 assert!(group.is_empty());
519 }
520
521 #[test]
522 fn pulse_sequence_peaks_then_returns() {
523 let mut group = pulse_sequence(1, MS200, MS100);
524 assert!(group.get("pulse_0").unwrap().value() < 0.01);
526
527 group.tick(MS100);
529 let mid = group.get("pulse_0").unwrap().value();
530 assert!(mid > 0.9, "pulse midpoint should be near 1.0, got {mid}");
531
532 group.tick(MS100);
534 let end = group.get("pulse_0").unwrap().value();
535 assert!(end < 0.1, "pulse end should be near 0.0, got {end}");
536 }
537
538 #[test]
539 fn pulse_sequence_items_staggered() {
540 let mut group = pulse_sequence(3, MS200, MS200);
541 group.tick(MS100);
543 let p0 = group.get("pulse_0").unwrap().value();
544 let p1 = group.get("pulse_1").unwrap().value();
545 assert!(p0 > 0.9, "pulse_0 should be at peak");
546 assert!(p1 < 0.01, "pulse_1 should not have started");
547 }
548
549 #[test]
552 fn slide_in_left_starts_offscreen() {
553 let slide = slide_in_left(20, MS200);
554 assert_eq!(slide.position(), -20);
555 }
556
557 #[test]
558 fn slide_in_left_ends_at_zero() {
559 let mut slide = slide_in_left(20, MS200);
560 slide.tick(MS200);
561 assert_eq!(slide.position(), 0);
562 assert!(slide.is_complete());
563 }
564
565 #[test]
566 fn slide_in_right_starts_offscreen() {
567 let slide = slide_in_right(20, MS200);
568 assert_eq!(slide.position(), 20);
569 }
570
571 #[test]
572 fn slide_in_right_ends_at_zero() {
573 let mut slide = slide_in_right(20, MS200);
574 slide.tick(MS200);
575 assert_eq!(slide.position(), 0);
576 }
577
578 #[test]
581 fn fade_through_starts_at_one() {
582 let ft = fade_through(MS200);
583 assert!((ft.value() - 1.0).abs() < 0.01, "should start at 1.0");
584 }
585
586 #[test]
587 fn fade_through_midpoint_near_zero() {
588 let mut ft = fade_through(MS200);
589 ft.tick(MS200);
590 assert!(
593 ft.value() < 0.1,
594 "midpoint should be near 0.0, got {}",
595 ft.value()
596 );
597 }
598
599 #[test]
600 fn fade_through_ends_at_one() {
601 let mut ft = fade_through(MS200);
602 ft.tick(Duration::from_millis(400));
603 assert!(ft.is_complete());
604 assert!(
605 (ft.value() - 1.0).abs() < 0.01,
606 "should end at 1.0, got {}",
607 ft.value()
608 );
609 }
610
611 #[test]
614 fn inverted_fade_starts_at_one() {
615 let f = InvertedFade::new(MS200);
616 assert!((f.value() - 1.0).abs() < 0.001);
617 }
618
619 #[test]
620 fn inverted_fade_ends_at_zero() {
621 let mut f = InvertedFade::new(MS200);
622 f.tick(MS200);
623 assert!(f.value() < 0.001);
624 assert!(f.is_complete());
625 }
626
627 #[test]
628 fn inverted_fade_reset() {
629 let mut f = InvertedFade::new(MS200);
630 f.tick(MS200);
631 assert!(f.is_complete());
632 f.reset();
633 assert!(!f.is_complete());
634 assert!((f.value() - 1.0).abs() < 0.001);
635 }
636
637 #[test]
640 fn cascade_in_deterministic() {
641 let run = || {
642 let mut group = cascade_in(5, MS200, MS50, StaggerMode::EaseInOut);
643 let mut values = Vec::new();
644 for _ in 0..10 {
645 group.tick(MS50);
646 values.push(group.overall_progress());
647 }
648 values
649 };
650 assert_eq!(run(), run(), "cascade_in must be deterministic");
651 }
652
653 #[test]
654 fn typewriter_deterministic() {
655 let run = || {
656 let mut tw = typewriter(100, MS500);
657 let mut counts = Vec::new();
658 for _ in 0..20 {
659 tw.tick(Duration::from_millis(25));
660 counts.push(tw.visible_chars());
661 }
662 counts
663 };
664 assert_eq!(run(), run(), "typewriter must be deterministic");
665 }
666
667 #[test]
668 fn fan_out_deterministic() {
669 let run = || {
670 let mut group = fan_out(7, MS200, MS200);
671 let mut values = Vec::new();
672 for _ in 0..10 {
673 group.tick(MS50);
674 values.push(group.overall_progress());
675 }
676 values
677 };
678 assert_eq!(run(), run(), "fan_out must be deterministic");
679 }
680}