Skip to main content

oxigdal_wasm/
animation.rs

1//! Animation and easing utilities for smooth transitions
2//!
3//! This module provides animation support for the WASM viewer, including:
4
5// Allow unused code in this module as it provides a comprehensive animation API
6// that may not be fully utilized initially
7#![allow(dead_code)]
8//! - Easing functions (linear, quadratic, cubic, quartic, exponential, etc.)
9//! - Animation state management
10//! - Smooth pan and zoom transitions
11//! - Spring physics for natural motion
12//! - Animation sequencing and composition
13//!
14//! # Overview
15//!
16//! Animations are essential for creating smooth, professional user experiences
17//! in interactive map viewers. This module provides tools for:
18//!
19//! ## Easing Functions
20//!
21//! Easing functions control the rate of change over time. Available functions:
22//! - Linear: constant speed
23//! - Quadratic: gradual acceleration/deceleration
24//! - Cubic: more pronounced curves
25//! - Quartic: even smoother motion
26//! - Quintic: very smooth, natural feel
27//! - Sine: smooth start and end
28//! - Exponential: dramatic acceleration
29//! - Circular: arc-based motion
30//! - Elastic: spring-like bounce
31//! - Back: slight overshoot
32//! - Bounce: bouncing effect
33//!
34//! Each has "in", "out", and "in-out" variants for different effects.
35//!
36//! ## Animation State
37//!
38//! The `Animation` struct manages the state of an ongoing animation:
39//! - Start and end values
40//! - Duration and progress
41//! - Easing function
42//! - Current value calculation
43//!
44//! ## Pan and Zoom Animations
45//!
46//! Specialized animation types for common map operations:
47//! - `PanAnimation`: smooth camera panning
48//! - `ZoomAnimation`: smooth zoom in/out with focal point
49//! - `FitBoundsAnimation`: animate to fit a bounding box
50//!
51//! ## Spring Physics
52//!
53//! The `SpringAnimation` provides physics-based motion that feels natural:
54//! - Configurable stiffness and damping
55//! - Automatic settling detection
56//! - Velocity-based motion
57//!
58//! ## Animation Sequencing
59//!
60//! Combine multiple animations:
61//! - Sequential: play animations one after another
62//! - Parallel: play multiple animations simultaneously
63//! - Delayed: start animations after a delay
64//!
65//! # Examples
66//!
67//! ## Basic Easing
68//!
69//! ```rust
70//! use oxigdal_wasm::{Easing, EasingFunction};
71//!
72//! let easing = Easing::QuadraticInOut;
73//! let t = 0.5; // Halfway through
74//! let value = easing.apply(t);
75//! ```
76//!
77//! ## Simple Animation
78//!
79//! ```rust
80//! use oxigdal_wasm::{Animation, Easing};
81//!
82//! let mut anim = Animation::new(0.0, 100.0, 1000.0, Easing::QuadraticOut);
83//!
84//! // Update at each frame
85//! anim.update(16.67); // 60 FPS
86//! let current_value = anim.current_value();
87//! ```
88//!
89//! ## Pan Animation
90//!
91//! ```rust
92//! use oxigdal_wasm::{PanAnimation, Easing};
93//!
94//! let mut pan = PanAnimation::new(
95//!     (0.0, 0.0),    // Start position
96//!     (100.0, 50.0), // End position
97//!     500.0,         // Duration (ms)
98//!     Easing::CubicOut,
99//! );
100//!
101//! while !pan.is_complete() {
102//!     pan.update(16.67);
103//!     let (x, y) = pan.current_position();
104//!     // Update camera position
105//! }
106//! ```
107//!
108//! ## Spring Animation
109//!
110//! ```rust
111//! use oxigdal_wasm::SpringAnimation;
112//!
113//! let mut spring = SpringAnimation::new(0.0, 100.0, 0.5, 0.8);
114//! // stiffness: 0.5, damping: 0.8
115//!
116//! while !spring.is_settled() {
117//!     spring.update(16.67);
118//!     let value = spring.current_value();
119//! }
120//! ```
121//!
122//! # Performance Considerations
123//!
124//! - Animations use `f64` for precision
125//! - Easing functions are pure and fast
126//! - No allocations during updates
127//! - Can run hundreds of animations simultaneously
128//!
129//! # Browser Compatibility
130//!
131//! Works in all modern browsers. For best performance, sync animations
132//! with `requestAnimationFrame`.
133
134use crate::WasmError;
135
136/// Easing function types
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum Easing {
139    /// Constant speed (no easing)
140    Linear,
141
142    /// Quadratic easing in (accelerating from zero)
143    QuadraticIn,
144    /// Quadratic easing out (decelerating to zero)
145    QuadraticOut,
146    /// Quadratic easing in-out (acceleration then deceleration)
147    QuadraticInOut,
148
149    /// Cubic easing in
150    CubicIn,
151    /// Cubic easing out
152    CubicOut,
153    /// Cubic easing in-out
154    CubicInOut,
155
156    /// Quartic easing in
157    QuarticIn,
158    /// Quartic easing out
159    QuarticOut,
160    /// Quartic easing in-out
161    QuarticInOut,
162
163    /// Quintic easing in
164    QuinticIn,
165    /// Quintic easing out
166    QuinticOut,
167    /// Quintic easing in-out
168    QuinticInOut,
169
170    /// Sine easing in
171    SineIn,
172    /// Sine easing out
173    SineOut,
174    /// Sine easing in-out
175    SineInOut,
176
177    /// Exponential easing in
178    ExponentialIn,
179    /// Exponential easing out
180    ExponentialOut,
181    /// Exponential easing in-out
182    ExponentialInOut,
183
184    /// Circular easing in
185    CircularIn,
186    /// Circular easing out
187    CircularOut,
188    /// Circular easing in-out
189    CircularInOut,
190
191    /// Elastic easing in (spring effect)
192    ElasticIn,
193    /// Elastic easing out (spring effect)
194    ElasticOut,
195    /// Elastic easing in-out (spring effect)
196    ElasticInOut,
197
198    /// Back easing in (slight overshoot)
199    BackIn,
200    /// Back easing out (slight overshoot)
201    BackOut,
202    /// Back easing in-out (slight overshoot)
203    BackInOut,
204
205    /// Bounce easing in (bouncing effect)
206    BounceIn,
207    /// Bounce easing out (bouncing effect)
208    BounceOut,
209    /// Bounce easing in-out (bouncing effect)
210    BounceInOut,
211}
212
213/// Trait for applying easing functions
214pub trait EasingFunction {
215    /// Apply easing to a normalized time value (0.0 to 1.0)
216    fn apply(&self, t: f64) -> f64;
217}
218
219impl EasingFunction for Easing {
220    fn apply(&self, t: f64) -> f64 {
221        let t = t.clamp(0.0, 1.0);
222
223        match self {
224            Easing::Linear => t,
225
226            Easing::QuadraticIn => t * t,
227            Easing::QuadraticOut => t * (2.0 - t),
228            Easing::QuadraticInOut => {
229                if t < 0.5 {
230                    2.0 * t * t
231                } else {
232                    -1.0 + (4.0 - 2.0 * t) * t
233                }
234            }
235
236            Easing::CubicIn => t * t * t,
237            Easing::CubicOut => {
238                let t = t - 1.0;
239                t * t * t + 1.0
240            }
241            Easing::CubicInOut => {
242                if t < 0.5 {
243                    4.0 * t * t * t
244                } else {
245                    let t = 2.0 * t - 2.0;
246                    1.0 + t * t * t / 2.0
247                }
248            }
249
250            Easing::QuarticIn => t * t * t * t,
251            Easing::QuarticOut => {
252                let t = t - 1.0;
253                1.0 - t * t * t * t
254            }
255            Easing::QuarticInOut => {
256                if t < 0.5 {
257                    8.0 * t * t * t * t
258                } else {
259                    let t = 2.0 * t - 2.0;
260                    1.0 - t * t * t * t / 2.0
261                }
262            }
263
264            Easing::QuinticIn => t * t * t * t * t,
265            Easing::QuinticOut => {
266                let t = t - 1.0;
267                t * t * t * t * t + 1.0
268            }
269            Easing::QuinticInOut => {
270                if t < 0.5 {
271                    16.0 * t * t * t * t * t
272                } else {
273                    let t = 2.0 * t - 2.0;
274                    1.0 + t * t * t * t * t / 2.0
275                }
276            }
277
278            Easing::SineIn => 1.0 - (t * std::f64::consts::PI / 2.0).cos(),
279            Easing::SineOut => (t * std::f64::consts::PI / 2.0).sin(),
280            Easing::SineInOut => -0.5 * ((std::f64::consts::PI * t).cos() - 1.0),
281
282            Easing::ExponentialIn => {
283                if t == 0.0 {
284                    0.0
285                } else {
286                    2.0_f64.powf(10.0 * (t - 1.0))
287                }
288            }
289            Easing::ExponentialOut => {
290                if t == 1.0 {
291                    1.0
292                } else {
293                    1.0 - 2.0_f64.powf(-10.0 * t)
294                }
295            }
296            Easing::ExponentialInOut => {
297                if t == 0.0 || t == 1.0 {
298                    t
299                } else if t < 0.5 {
300                    0.5 * 2.0_f64.powf(20.0 * t - 10.0)
301                } else {
302                    1.0 - 0.5 * 2.0_f64.powf(-20.0 * t + 10.0)
303                }
304            }
305
306            Easing::CircularIn => 1.0 - (1.0 - t * t).sqrt(),
307            Easing::CircularOut => (2.0 * t - t * t).sqrt(),
308            Easing::CircularInOut => {
309                if t < 0.5 {
310                    0.5 * (1.0 - (1.0 - 4.0 * t * t).sqrt())
311                } else {
312                    0.5 * ((2.0 * t - 1.0) * (3.0 - 2.0 * t) * 4.0).sqrt() + 0.5
313                }
314            }
315
316            Easing::ElasticIn => {
317                if t == 0.0 || t == 1.0 {
318                    t
319                } else {
320                    let p = 0.3;
321                    let s = p / 4.0;
322                    let t = t - 1.0;
323                    -(2.0_f64.powf(10.0 * t)) * ((t - s) * (2.0 * std::f64::consts::PI) / p).sin()
324                }
325            }
326            Easing::ElasticOut => {
327                if t == 0.0 || t == 1.0 {
328                    t
329                } else {
330                    let p = 0.3;
331                    let s = p / 4.0;
332                    2.0_f64.powf(-10.0 * t) * ((t - s) * (2.0 * std::f64::consts::PI) / p).sin()
333                        + 1.0
334                }
335            }
336            Easing::ElasticInOut => {
337                if t == 0.0 || t == 1.0 {
338                    t
339                } else {
340                    let p = 0.45;
341                    let s = p / 4.0;
342
343                    if t < 0.5 {
344                        let t = 2.0 * t - 1.0;
345                        -0.5 * 2.0_f64.powf(10.0 * t)
346                            * ((t - s) * (2.0 * std::f64::consts::PI) / p).sin()
347                    } else {
348                        let t = 2.0 * t - 1.0;
349                        0.5 * 2.0_f64.powf(-10.0 * t)
350                            * ((t - s) * (2.0 * std::f64::consts::PI) / p).sin()
351                            + 1.0
352                    }
353                }
354            }
355
356            Easing::BackIn => {
357                let c1 = 1.70158;
358                let c3 = c1 + 1.0;
359                c3 * t * t * t - c1 * t * t
360            }
361            Easing::BackOut => {
362                let c1 = 1.70158;
363                let c3 = c1 + 1.0;
364                let t = t - 1.0;
365                1.0 + c3 * t * t * t + c1 * t * t
366            }
367            Easing::BackInOut => {
368                let c1 = 1.70158;
369                let c2 = c1 * 1.525;
370
371                if t < 0.5 {
372                    let t = 2.0 * t;
373                    (t * t * ((c2 + 1.0) * t - c2)) / 2.0
374                } else {
375                    let t = 2.0 * t - 2.0;
376                    (t * t * ((c2 + 1.0) * t + c2) + 2.0) / 2.0
377                }
378            }
379
380            Easing::BounceIn => 1.0 - Easing::BounceOut.apply(1.0 - t),
381            Easing::BounceOut => {
382                let n1 = 7.5625;
383                let d1 = 2.75;
384
385                if t < 1.0 / d1 {
386                    n1 * t * t
387                } else if t < 2.0 / d1 {
388                    let t = t - 1.5 / d1;
389                    n1 * t * t + 0.75
390                } else if t < 2.5 / d1 {
391                    let t = t - 2.25 / d1;
392                    n1 * t * t + 0.9375
393                } else {
394                    let t = t - 2.625 / d1;
395                    n1 * t * t + 0.984375
396                }
397            }
398            Easing::BounceInOut => {
399                if t < 0.5 {
400                    0.5 * Easing::BounceIn.apply(2.0 * t)
401                } else {
402                    0.5 * Easing::BounceOut.apply(2.0 * t - 1.0) + 0.5
403                }
404            }
405        }
406    }
407}
408
409/// Basic animation state
410#[derive(Debug, Clone)]
411pub struct Animation {
412    start_value: f64,
413    end_value: f64,
414    duration_ms: f64,
415    elapsed_ms: f64,
416    easing: Easing,
417}
418
419impl Animation {
420    /// Create a new animation
421    pub fn new(start_value: f64, end_value: f64, duration_ms: f64, easing: Easing) -> Self {
422        Self {
423            start_value,
424            end_value,
425            duration_ms,
426            elapsed_ms: 0.0,
427            easing,
428        }
429    }
430
431    /// Update animation with time delta
432    pub fn update(&mut self, delta_ms: f64) {
433        self.elapsed_ms += delta_ms;
434        if self.elapsed_ms > self.duration_ms {
435            self.elapsed_ms = self.duration_ms;
436        }
437    }
438
439    /// Get current value
440    pub fn current_value(&self) -> f64 {
441        let t = if self.duration_ms > 0.0 {
442            self.elapsed_ms / self.duration_ms
443        } else {
444            1.0
445        };
446
447        let eased_t = self.easing.apply(t);
448        self.start_value + (self.end_value - self.start_value) * eased_t
449    }
450
451    /// Check if animation is complete
452    pub fn is_complete(&self) -> bool {
453        self.elapsed_ms >= self.duration_ms
454    }
455
456    /// Get progress (0.0 to 1.0)
457    pub fn progress(&self) -> f64 {
458        if self.duration_ms > 0.0 {
459            (self.elapsed_ms / self.duration_ms).min(1.0)
460        } else {
461            1.0
462        }
463    }
464
465    /// Reset animation to beginning
466    pub fn reset(&mut self) {
467        self.elapsed_ms = 0.0;
468    }
469
470    /// Reverse the animation
471    pub fn reverse(&mut self) {
472        std::mem::swap(&mut self.start_value, &mut self.end_value);
473        self.elapsed_ms = 0.0;
474    }
475}
476
477/// Animation for panning the viewport
478#[derive(Debug, Clone)]
479pub struct PanAnimation {
480    start_pos: (f64, f64),
481    end_pos: (f64, f64),
482    duration_ms: f64,
483    elapsed_ms: f64,
484    easing: Easing,
485}
486
487impl PanAnimation {
488    /// Create a new pan animation
489    pub fn new(
490        start_pos: (f64, f64),
491        end_pos: (f64, f64),
492        duration_ms: f64,
493        easing: Easing,
494    ) -> Self {
495        Self {
496            start_pos,
497            end_pos,
498            duration_ms,
499            elapsed_ms: 0.0,
500            easing,
501        }
502    }
503
504    /// Update animation
505    pub fn update(&mut self, delta_ms: f64) {
506        self.elapsed_ms += delta_ms;
507        if self.elapsed_ms > self.duration_ms {
508            self.elapsed_ms = self.duration_ms;
509        }
510    }
511
512    /// Get current position
513    pub fn current_position(&self) -> (f64, f64) {
514        let t = if self.duration_ms > 0.0 {
515            self.elapsed_ms / self.duration_ms
516        } else {
517            1.0
518        };
519
520        let eased_t = self.easing.apply(t);
521        let x = self.start_pos.0 + (self.end_pos.0 - self.start_pos.0) * eased_t;
522        let y = self.start_pos.1 + (self.end_pos.1 - self.start_pos.1) * eased_t;
523
524        (x, y)
525    }
526
527    /// Check if complete
528    pub fn is_complete(&self) -> bool {
529        self.elapsed_ms >= self.duration_ms
530    }
531
532    /// Get progress
533    pub fn progress(&self) -> f64 {
534        if self.duration_ms > 0.0 {
535            (self.elapsed_ms / self.duration_ms).min(1.0)
536        } else {
537            1.0
538        }
539    }
540}
541
542/// Animation for zooming the viewport
543#[derive(Debug, Clone)]
544pub struct ZoomAnimation {
545    start_zoom: f64,
546    end_zoom: f64,
547    focal_point: (f64, f64),
548    duration_ms: f64,
549    elapsed_ms: f64,
550    easing: Easing,
551}
552
553impl ZoomAnimation {
554    /// Create a new zoom animation
555    pub fn new(
556        start_zoom: f64,
557        end_zoom: f64,
558        focal_point: (f64, f64),
559        duration_ms: f64,
560        easing: Easing,
561    ) -> Self {
562        Self {
563            start_zoom,
564            end_zoom,
565            focal_point,
566            duration_ms,
567            elapsed_ms: 0.0,
568            easing,
569        }
570    }
571
572    /// Update animation
573    pub fn update(&mut self, delta_ms: f64) {
574        self.elapsed_ms += delta_ms;
575        if self.elapsed_ms > self.duration_ms {
576            self.elapsed_ms = self.duration_ms;
577        }
578    }
579
580    /// Get current zoom level
581    pub fn current_zoom(&self) -> f64 {
582        let t = if self.duration_ms > 0.0 {
583            self.elapsed_ms / self.duration_ms
584        } else {
585            1.0
586        };
587
588        let eased_t = self.easing.apply(t);
589        self.start_zoom + (self.end_zoom - self.start_zoom) * eased_t
590    }
591
592    /// Get focal point
593    pub fn focal_point(&self) -> (f64, f64) {
594        self.focal_point
595    }
596
597    /// Check if complete
598    pub fn is_complete(&self) -> bool {
599        self.elapsed_ms >= self.duration_ms
600    }
601
602    /// Get progress
603    pub fn progress(&self) -> f64 {
604        if self.duration_ms > 0.0 {
605            (self.elapsed_ms / self.duration_ms).min(1.0)
606        } else {
607            1.0
608        }
609    }
610}
611
612/// Spring animation using physics simulation
613#[derive(Debug, Clone)]
614pub struct SpringAnimation {
615    current: f64,
616    target: f64,
617    velocity: f64,
618    stiffness: f64,
619    damping: f64,
620    threshold: f64,
621}
622
623impl SpringAnimation {
624    /// Create a new spring animation
625    ///
626    /// - `current`: starting value
627    /// - `target`: target value
628    /// - `stiffness`: spring stiffness (0.0 to 1.0, higher = faster)
629    /// - `damping`: damping factor (0.0 to 1.0, higher = less bounce)
630    pub fn new(current: f64, target: f64, stiffness: f64, damping: f64) -> Self {
631        Self {
632            current,
633            target,
634            velocity: 0.0,
635            stiffness: stiffness.clamp(0.0, 1.0),
636            damping: damping.clamp(0.0, 1.0),
637            threshold: 0.01,
638        }
639    }
640
641    /// Update spring animation
642    pub fn update(&mut self, delta_ms: f64) {
643        let delta_s = delta_ms / 1000.0; // Convert to seconds
644
645        // Spring force
646        let spring_force = (self.target - self.current) * self.stiffness;
647
648        // Damping force
649        let damping_force = -self.velocity * self.damping;
650
651        // Update velocity and position
652        self.velocity += (spring_force + damping_force) * delta_s;
653        self.current += self.velocity * delta_s;
654    }
655
656    /// Get current value
657    pub fn current_value(&self) -> f64 {
658        self.current
659    }
660
661    /// Check if spring has settled
662    pub fn is_settled(&self) -> bool {
663        (self.current - self.target).abs() < self.threshold && self.velocity.abs() < self.threshold
664    }
665
666    /// Set new target
667    pub fn set_target(&mut self, target: f64) {
668        self.target = target;
669    }
670
671    /// Set threshold for settlement detection
672    pub fn set_threshold(&mut self, threshold: f64) {
673        self.threshold = threshold;
674    }
675}
676
677/// Animation sequence manager
678pub struct AnimationSequence {
679    animations: Vec<Box<dyn AnimationTrait>>,
680    current_index: usize,
681}
682
683/// Trait for animation types
684pub trait AnimationTrait {
685    /// Update the animation
686    fn update(&mut self, delta_ms: f64);
687
688    /// Check if animation is complete
689    fn is_complete(&self) -> bool;
690
691    /// Get progress (0.0 to 1.0)
692    fn progress(&self) -> f64;
693}
694
695impl AnimationTrait for Animation {
696    fn update(&mut self, delta_ms: f64) {
697        Animation::update(self, delta_ms);
698    }
699
700    fn is_complete(&self) -> bool {
701        Animation::is_complete(self)
702    }
703
704    fn progress(&self) -> f64 {
705        Animation::progress(self)
706    }
707}
708
709impl AnimationTrait for PanAnimation {
710    fn update(&mut self, delta_ms: f64) {
711        PanAnimation::update(self, delta_ms);
712    }
713
714    fn is_complete(&self) -> bool {
715        PanAnimation::is_complete(self)
716    }
717
718    fn progress(&self) -> f64 {
719        PanAnimation::progress(self)
720    }
721}
722
723impl AnimationTrait for ZoomAnimation {
724    fn update(&mut self, delta_ms: f64) {
725        ZoomAnimation::update(self, delta_ms);
726    }
727
728    fn is_complete(&self) -> bool {
729        ZoomAnimation::is_complete(self)
730    }
731
732    fn progress(&self) -> f64 {
733        ZoomAnimation::progress(self)
734    }
735}
736
737impl AnimationSequence {
738    /// Create a new animation sequence
739    pub fn new() -> Self {
740        Self {
741            animations: Vec::new(),
742            current_index: 0,
743        }
744    }
745
746    /// Add an animation to the sequence
747    pub fn add<A: AnimationTrait + 'static>(&mut self, animation: A) {
748        self.animations.push(Box::new(animation));
749    }
750
751    /// Update the current animation in the sequence
752    pub fn update(&mut self, delta_ms: f64) -> Result<(), WasmError> {
753        if self.current_index >= self.animations.len() {
754            return Ok(());
755        }
756
757        let current = &mut self.animations[self.current_index];
758        current.update(delta_ms);
759
760        if current.is_complete() {
761            self.current_index += 1;
762        }
763
764        Ok(())
765    }
766
767    /// Check if the entire sequence is complete
768    pub fn is_complete(&self) -> bool {
769        self.current_index >= self.animations.len()
770    }
771
772    /// Get overall progress
773    pub fn progress(&self) -> f64 {
774        if self.animations.is_empty() {
775            return 1.0;
776        }
777
778        let completed = self.current_index as f64;
779        let current_progress = if self.current_index < self.animations.len() {
780            self.animations[self.current_index].progress()
781        } else {
782            0.0
783        };
784
785        (completed + current_progress) / self.animations.len() as f64
786    }
787
788    /// Reset the sequence to the beginning
789    pub fn reset(&mut self) {
790        self.current_index = 0;
791    }
792}
793
794impl Default for AnimationSequence {
795    fn default() -> Self {
796        Self::new()
797    }
798}
799
800/// Parallel animation manager (runs multiple animations simultaneously)
801pub struct ParallelAnimation {
802    animations: Vec<Box<dyn AnimationTrait>>,
803}
804
805impl ParallelAnimation {
806    /// Create a new parallel animation manager
807    pub fn new() -> Self {
808        Self {
809            animations: Vec::new(),
810        }
811    }
812
813    /// Add an animation
814    pub fn add<A: AnimationTrait + 'static>(&mut self, animation: A) {
815        self.animations.push(Box::new(animation));
816    }
817
818    /// Update all animations
819    pub fn update(&mut self, delta_ms: f64) {
820        for anim in &mut self.animations {
821            if !anim.is_complete() {
822                anim.update(delta_ms);
823            }
824        }
825    }
826
827    /// Check if all animations are complete
828    pub fn is_complete(&self) -> bool {
829        self.animations.iter().all(|a| a.is_complete())
830    }
831
832    /// Get average progress
833    pub fn progress(&self) -> f64 {
834        if self.animations.is_empty() {
835            return 1.0;
836        }
837
838        let total: f64 = self.animations.iter().map(|a| a.progress()).sum();
839        total / self.animations.len() as f64
840    }
841}
842
843impl Default for ParallelAnimation {
844    fn default() -> Self {
845        Self::new()
846    }
847}
848
849/// Delayed animation (starts after a delay)
850#[derive(Debug)]
851pub struct DelayedAnimation<A: AnimationTrait> {
852    animation: A,
853    delay_ms: f64,
854    elapsed_delay_ms: f64,
855    started: bool,
856}
857
858impl<A: AnimationTrait> DelayedAnimation<A> {
859    /// Create a new delayed animation
860    pub fn new(animation: A, delay_ms: f64) -> Self {
861        Self {
862            animation,
863            delay_ms,
864            elapsed_delay_ms: 0.0,
865            started: false,
866        }
867    }
868
869    /// Update animation (including delay)
870    pub fn update(&mut self, delta_ms: f64) {
871        if !self.started {
872            self.elapsed_delay_ms += delta_ms;
873            if self.elapsed_delay_ms >= self.delay_ms {
874                self.started = true;
875                let overflow = self.elapsed_delay_ms - self.delay_ms;
876                if overflow > 0.0 {
877                    self.animation.update(overflow);
878                }
879            }
880        } else {
881            self.animation.update(delta_ms);
882        }
883    }
884
885    /// Check if complete
886    pub fn is_complete(&self) -> bool {
887        self.started && self.animation.is_complete()
888    }
889
890    /// Get progress (including delay)
891    pub fn progress(&self) -> f64 {
892        if !self.started {
893            (self.elapsed_delay_ms / self.delay_ms).min(1.0) * 0.5
894        } else {
895            0.5 + self.animation.progress() * 0.5
896        }
897    }
898}
899
900#[cfg(test)]
901mod tests {
902    use super::*;
903
904    #[test]
905    fn test_linear_easing() {
906        let easing = Easing::Linear;
907        assert_eq!(easing.apply(0.0), 0.0);
908        assert_eq!(easing.apply(0.5), 0.5);
909        assert_eq!(easing.apply(1.0), 1.0);
910    }
911
912    #[test]
913    fn test_quadratic_easing() {
914        let easing = Easing::QuadraticIn;
915        assert_eq!(easing.apply(0.0), 0.0);
916        assert!(easing.apply(0.5) < 0.5);
917        assert_eq!(easing.apply(1.0), 1.0);
918    }
919
920    #[test]
921    fn test_animation_basic() {
922        let mut anim = Animation::new(0.0, 100.0, 1000.0, Easing::Linear);
923
924        assert_eq!(anim.current_value(), 0.0);
925        assert!(!anim.is_complete());
926
927        anim.update(500.0);
928        assert!((anim.current_value() - 50.0).abs() < 0.01);
929
930        anim.update(500.0);
931        assert_eq!(anim.current_value(), 100.0);
932        assert!(anim.is_complete());
933    }
934
935    #[test]
936    fn test_pan_animation() {
937        let mut pan = PanAnimation::new((0.0, 0.0), (100.0, 50.0), 1000.0, Easing::Linear);
938
939        let (x, y) = pan.current_position();
940        assert_eq!(x, 0.0);
941        assert_eq!(y, 0.0);
942
943        pan.update(500.0);
944        let (x, y) = pan.current_position();
945        assert!((x - 50.0).abs() < 0.01);
946        assert!((y - 25.0).abs() < 0.01);
947    }
948
949    #[test]
950    fn test_spring_animation() {
951        // Use higher stiffness and damping for predictable settling
952        let mut spring = SpringAnimation::new(0.0, 100.0, 0.8, 0.98);
953        spring.set_threshold(1.0); // Larger threshold for easier settling
954
955        // Run spring for enough iterations to settle
956        for _ in 0..1000 {
957            spring.update(16.67);
958            if spring.is_settled() {
959                break;
960            }
961        }
962
963        // Spring should settle close to target (within 5% tolerance)
964        assert!((spring.current_value() - 100.0).abs() < 5.0);
965    }
966
967    #[test]
968    fn test_animation_reverse() {
969        let mut anim = Animation::new(0.0, 100.0, 1000.0, Easing::Linear);
970        anim.update(500.0);
971
972        anim.reverse();
973        assert_eq!(anim.current_value(), 100.0);
974
975        anim.update(500.0);
976        assert_eq!(anim.current_value(), 50.0);
977    }
978}