leptos_motion_layout/
flip.rs

1//! FLIP (First, Last, Invert, Play) animation implementation
2//!
3//! FLIP is a technique for creating smooth layout animations by:
4//! 1. First: Record the initial position
5//! 2. Last: Record the final position
6//! 3. Invert: Apply transforms to make it appear in the initial position
7//! 4. Play: Animate the transforms to their natural values
8
9use crate::LayoutAnimationConfig;
10use std::collections::HashMap;
11use wasm_bindgen::prelude::*;
12use web_sys::{DomRect, Element};
13
14/// FLIP animation state
15#[derive(Debug, Clone)]
16pub struct FLIPState {
17    /// Initial position and dimensions
18    pub first: DomRect,
19    /// Final position and dimensions
20    pub last: DomRect,
21    /// Inverted transform values
22    pub inverted: TransformValues,
23    /// Current animation progress (0.0 to 1.0)
24    pub progress: f64,
25    /// Whether the animation is active
26    pub active: bool,
27}
28
29/// Transform values for FLIP animations
30#[derive(Debug, Clone)]
31pub struct TransformValues {
32    /// X translation
33    pub translate_x: f64,
34    /// Y translation
35    pub translate_y: f64,
36    /// Scale X
37    pub scale_x: f64,
38    /// Scale Y
39    pub scale_y: f64,
40    /// Rotation in degrees
41    pub rotation: f64,
42}
43
44impl Default for TransformValues {
45    fn default() -> Self {
46        Self {
47            translate_x: 0.0,
48            translate_y: 0.0,
49            scale_x: 1.0,
50            scale_y: 1.0,
51            rotation: 0.0,
52        }
53    }
54}
55
56impl TransformValues {
57    /// Create new transform values
58    pub fn new(
59        translate_x: f64,
60        translate_y: f64,
61        scale_x: f64,
62        scale_y: f64,
63        rotation: f64,
64    ) -> Self {
65        Self {
66            translate_x,
67            translate_y,
68            scale_x,
69            scale_y,
70            rotation,
71        }
72    }
73
74    /// Create translation-only transform
75    pub fn translation(x: f64, y: f64) -> Self {
76        Self::new(x, y, 1.0, 1.0, 0.0)
77    }
78
79    /// Create scale-only transform
80    pub fn scale(x: f64, y: f64) -> Self {
81        Self::new(0.0, 0.0, x, y, 0.0)
82    }
83
84    /// Create rotation-only transform
85    pub fn rotation(degrees: f64) -> Self {
86        Self::new(0.0, 0.0, 1.0, 1.0, degrees)
87    }
88}
89
90/// FLIP animation instance
91#[derive(Debug)]
92pub struct FLIPAnimation {
93    /// Unique animation ID
94    pub id: String,
95    /// Target element
96    pub element: Element,
97    /// FLIP state
98    pub state: FLIPState,
99    /// Animation configuration
100    pub config: LayoutAnimationConfig,
101    /// Start time
102    pub start_time: f64,
103    /// Duration
104    pub duration: f64,
105    /// Easing function
106    pub easing: EasingFunction,
107}
108
109/// Easing function for FLIP animations
110#[derive(Debug, Clone)]
111pub enum EasingFunction {
112    /// Linear easing
113    Linear,
114    /// Ease-in
115    EaseIn,
116    /// Ease-out
117    EaseOut,
118    /// Ease-in-out
119    EaseInOut,
120    /// Custom cubic-bezier
121    CubicBezier(f64, f64, f64, f64),
122    /// Spring physics
123    Spring {
124        /// Spring tension
125        tension: f64,
126        /// Spring friction
127        friction: f64,
128    },
129}
130
131impl Default for EasingFunction {
132    fn default() -> Self {
133        EasingFunction::EaseOut
134    }
135}
136
137impl EasingFunction {
138    /// Evaluate the easing function at given progress (0.0 to 1.0)
139    pub fn evaluate(&self, t: f64) -> f64 {
140        let t = t.clamp(0.0, 1.0);
141        match self {
142            EasingFunction::Linear => t,
143            EasingFunction::EaseIn => t * t,
144            EasingFunction::EaseOut => 1.0 - (1.0 - t).powi(2),
145            EasingFunction::EaseInOut => {
146                if t < 0.5 {
147                    2.0 * t * t
148                } else {
149                    1.0 - 2.0 * (1.0 - t).powi(2)
150                }
151            }
152            EasingFunction::CubicBezier(p1, _p2, p3, _p4) => {
153                // Simplified cubic bezier evaluation
154                let u = 1.0 - t;
155                u * u * u * 0.0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p3 + t * t * t * 1.0
156            }
157            EasingFunction::Spring { tension, friction } => {
158                // Simplified spring physics
159                let damping = friction / (2.0 * (tension).sqrt());
160                let frequency = (tension).sqrt();
161
162                if damping < 1.0 {
163                    let damped_freq = frequency * (1.0 - damping * damping).sqrt();
164                    let decay = (-damping * frequency * t).exp();
165                    1.0 - decay * (damped_freq * t + damping * frequency * t / damped_freq).cos()
166                } else {
167                    1.0 - (-frequency * t).exp()
168                }
169            }
170        }
171    }
172}
173
174/// FLIP animator for managing layout transitions
175pub struct FLIPAnimator {
176    /// Active FLIP animations
177    active_animations: HashMap<String, FLIPAnimation>,
178    /// Animation frame callback
179    animation_frame: Option<i32>,
180    /// Performance tracking
181    performance_metrics: FLIPPerformanceMetrics,
182}
183
184/// FLIP performance metrics
185#[derive(Debug, Clone)]
186pub struct FLIPPerformanceMetrics {
187    /// Total animations created
188    pub total_animations: usize,
189    /// Average animation duration
190    pub average_duration: f64,
191    /// Failed animations
192    pub failed_animations: usize,
193    /// Performance score (0.0 to 1.0)
194    pub performance_score: f64,
195}
196
197impl Default for FLIPPerformanceMetrics {
198    fn default() -> Self {
199        Self {
200            total_animations: 0,
201            average_duration: 0.0,
202            failed_animations: 0,
203            performance_score: 1.0,
204        }
205    }
206}
207
208impl FLIPAnimator {
209    /// Create a new FLIP animator
210    pub fn new() -> Self {
211        Self {
212            active_animations: HashMap::new(),
213            animation_frame: None,
214            performance_metrics: FLIPPerformanceMetrics::default(),
215        }
216    }
217
218    /// Start a FLIP animation
219    pub fn animate(
220        &mut self,
221        id: String,
222        element: Element,
223        first: DomRect,
224        last: DomRect,
225        config: LayoutAnimationConfig,
226    ) -> Result<(), String> {
227        let inverted = self.calculate_transform_values(&first, &last);
228
229        let state = FLIPState {
230            first,
231            last,
232            inverted,
233            progress: 0.0,
234            active: true,
235        };
236
237        let animation = FLIPAnimation {
238            id: id.clone(),
239            element,
240            state,
241            config: config.clone(),
242            start_time: self.get_current_time(),
243            duration: config.duration,
244            easing: config.easing,
245        };
246
247        self.active_animations.insert(id, animation);
248        self.performance_metrics.total_animations += 1;
249
250        Ok(())
251    }
252
253    /// Calculate transform values from first to last positions
254    fn calculate_transform_values(&self, first: &DomRect, last: &DomRect) -> TransformValues {
255        let translate_x = first.x() - last.x();
256        let translate_y = first.y() - last.y();
257        let scale_x = if last.width() > 0.0 {
258            first.width() / last.width()
259        } else {
260            1.0
261        };
262        let scale_y = if last.height() > 0.0 {
263            first.height() / last.height()
264        } else {
265            1.0
266        };
267
268        TransformValues::new(translate_x, translate_y, scale_x, scale_y, 0.0)
269    }
270
271    /// Get current time in milliseconds
272    fn get_current_time(&self) -> f64 {
273        js_sys::Date::now()
274    }
275
276    /// Update all active animations
277    pub fn update(&mut self) {
278        let current_time = self.get_current_time();
279        let mut completed_ids = Vec::new();
280
281        for (id, animation) in &mut self.active_animations {
282            let elapsed = (current_time - animation.start_time) / 1000.0; // Convert to seconds
283            let progress = (elapsed / animation.duration).clamp(0.0, 1.0);
284            let eased_progress = animation.easing.evaluate(progress);
285
286            animation.state.progress = eased_progress;
287
288            if progress >= 1.0 {
289                animation.state.active = false;
290                completed_ids.push(id.clone());
291            }
292
293            // Apply transform inline to avoid borrow checker issues
294            let inverted = &animation.state.inverted;
295            let current_x = inverted.translate_x * (1.0 - eased_progress);
296            let current_y = inverted.translate_y * (1.0 - eased_progress);
297            let current_scale_x = 1.0 + (inverted.scale_x - 1.0) * (1.0 - eased_progress);
298            let current_scale_y = 1.0 + (inverted.scale_y - 1.0) * (1.0 - eased_progress);
299
300            let transform = format!(
301                "translateX({}px) translateY({}px) scaleX({}) scaleY({})",
302                current_x, current_y, current_scale_x, current_scale_y
303            );
304
305            if let Ok(_) = animation
306                .element
307                .dyn_ref::<web_sys::HtmlElement>()
308                .unwrap()
309                .style()
310                .set_property("transform", &transform)
311            {
312                // Transform applied successfully
313            }
314        }
315
316        // Remove completed animations
317        for id in completed_ids {
318            self.active_animations.remove(&id);
319        }
320    }
321
322    /// Apply transform to element based on progress
323    fn apply_transform(&self, animation: &FLIPAnimation, progress: f64) {
324        let inverted = &animation.state.inverted;
325        let current_x = inverted.translate_x * (1.0 - progress);
326        let current_y = inverted.translate_y * (1.0 - progress);
327        let current_scale_x = 1.0 + (inverted.scale_x - 1.0) * (1.0 - progress);
328        let current_scale_y = 1.0 + (inverted.scale_y - 1.0) * (1.0 - progress);
329
330        let transform = format!(
331            "translateX({}px) translateY({}px) scaleX({}) scaleY({})",
332            current_x, current_y, current_scale_x, current_scale_y
333        );
334
335        if let Ok(style) = animation
336            .element
337            .dyn_ref::<web_sys::HtmlElement>()
338            .unwrap()
339            .style()
340            .set_property("transform", &transform)
341        {
342            // Transform applied successfully
343        }
344    }
345
346    /// Get active animation count
347    pub fn active_count(&self) -> usize {
348        self.active_animations.len()
349    }
350
351    /// Get performance metrics
352    pub fn performance_metrics(&self) -> &FLIPPerformanceMetrics {
353        &self.performance_metrics
354    }
355
356    /// Cancel all animations
357    pub fn cancel_all(&mut self) {
358        self.active_animations.clear();
359    }
360
361    /// Cancel specific animation
362    pub fn cancel(&mut self, id: &str) -> bool {
363        self.active_animations.remove(id).is_some()
364    }
365
366    fn parse_easing_function(&self, easing: &str) -> Result<EasingFunction, String> {
367        match easing {
368            "linear" => Ok(EasingFunction::Linear),
369            "ease-in" => Ok(EasingFunction::EaseIn),
370            "ease-out" => Ok(EasingFunction::EaseOut),
371            "ease-in-out" => Ok(EasingFunction::EaseInOut),
372            _ => {
373                if easing.starts_with("cubic-bezier(") && easing.ends_with(')') {
374                    // Parse cubic-bezier values
375                    let values_str = &easing[13..easing.len() - 1];
376                    let values: Result<Vec<f64>, _> =
377                        values_str.split(',').map(|s| s.trim().parse()).collect();
378
379                    match values {
380                        Ok(v) if v.len() == 4 => {
381                            Ok(EasingFunction::CubicBezier(v[0], v[1], v[2], v[3]))
382                        }
383                        _ => Err("Invalid cubic-bezier format".to_string()),
384                    }
385                } else if easing.starts_with("spring(") && easing.ends_with(')') {
386                    // Parse spring values
387                    let values_str = &easing[7..easing.len() - 1];
388                    let values: Result<Vec<f64>, _> =
389                        values_str.split(',').map(|s| s.trim().parse()).collect();
390
391                    match values {
392                        Ok(v) if v.len() == 2 => Ok(EasingFunction::Spring {
393                            tension: v[0],
394                            friction: v[1],
395                        }),
396                        _ => Err("Invalid spring format".to_string()),
397                    }
398                } else {
399                    Err(format!("Unknown easing function: {}", easing))
400                }
401            }
402        }
403    }
404
405    fn update_performance_metrics(&mut self, duration: f64) {
406        let total = self.performance_metrics.total_animations as f64;
407        let current_avg = self.performance_metrics.average_duration;
408        self.performance_metrics.average_duration =
409            (current_avg * (total - 1.0) + duration) / total;
410
411        // Update performance score based on success rate
412        let success_rate = 1.0 - (self.performance_metrics.failed_animations as f64 / total);
413        self.performance_metrics.performance_score = success_rate;
414    }
415}
416
417impl Default for FLIPAnimator {
418    fn default() -> Self {
419        Self::new()
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use wasm_bindgen_test::*;
427
428    wasm_bindgen_test_configure!(run_in_browser);
429
430    #[test]
431    fn test_transform_values_new() {
432        let transform = TransformValues::new(10.0, 20.0, 1.5, 2.0, 45.0);
433        assert_eq!(transform.translate_x, 10.0);
434        assert_eq!(transform.translate_y, 20.0);
435        assert_eq!(transform.scale_x, 1.5);
436        assert_eq!(transform.scale_y, 2.0);
437        assert_eq!(transform.rotation, 45.0);
438    }
439
440    #[test]
441    fn test_transform_values_translation() {
442        let transform = TransformValues::translation(10.0, 20.0);
443        assert_eq!(transform.translate_x, 10.0);
444        assert_eq!(transform.translate_y, 20.0);
445        assert_eq!(transform.scale_x, 1.0);
446        assert_eq!(transform.scale_y, 1.0);
447        assert_eq!(transform.rotation, 0.0);
448    }
449
450    #[test]
451    fn test_transform_values_scale() {
452        let transform = TransformValues::scale(1.5, 2.0);
453        assert_eq!(transform.translate_x, 0.0);
454        assert_eq!(transform.translate_y, 0.0);
455        assert_eq!(transform.scale_x, 1.5);
456        assert_eq!(transform.scale_y, 2.0);
457        assert_eq!(transform.rotation, 0.0);
458    }
459
460    #[test]
461    fn test_transform_values_rotation() {
462        let transform = TransformValues::rotation(45.0);
463        assert_eq!(transform.translate_x, 0.0);
464        assert_eq!(transform.translate_y, 0.0);
465        assert_eq!(transform.scale_x, 1.0);
466        assert_eq!(transform.scale_y, 1.0);
467        assert_eq!(transform.rotation, 45.0);
468    }
469
470    #[test]
471    fn test_transform_values_default() {
472        let transform = TransformValues::default();
473        assert_eq!(transform.translate_x, 0.0);
474        assert_eq!(transform.translate_y, 0.0);
475        assert_eq!(transform.scale_x, 1.0);
476        assert_eq!(transform.scale_y, 1.0);
477        assert_eq!(transform.rotation, 0.0);
478    }
479
480    #[test]
481    fn test_easing_function_linear() {
482        let easing = EasingFunction::Linear;
483        assert_eq!(easing.evaluate(0.0), 0.0);
484        assert_eq!(easing.evaluate(0.5), 0.5);
485        assert_eq!(easing.evaluate(1.0), 1.0);
486    }
487
488    #[test]
489    fn test_easing_function_ease_in() {
490        let easing = EasingFunction::EaseIn;
491        assert_eq!(easing.evaluate(0.0), 0.0);
492        assert_eq!(easing.evaluate(0.5), 0.25);
493        assert_eq!(easing.evaluate(1.0), 1.0);
494    }
495
496    #[test]
497    fn test_easing_function_ease_out() {
498        let easing = EasingFunction::EaseOut;
499        assert_eq!(easing.evaluate(0.0), 0.0);
500        assert_eq!(easing.evaluate(0.5), 0.75);
501        assert_eq!(easing.evaluate(1.0), 1.0);
502    }
503
504    #[test]
505    fn test_easing_function_ease_in_out() {
506        let easing = EasingFunction::EaseInOut;
507        assert_eq!(easing.evaluate(0.0), 0.0);
508        assert_eq!(easing.evaluate(0.25), 0.125);
509        assert_eq!(easing.evaluate(0.75), 0.875);
510        assert_eq!(easing.evaluate(1.0), 1.0);
511    }
512
513    #[test]
514    fn test_easing_function_cubic_bezier() {
515        let easing = EasingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0);
516        let result = easing.evaluate(0.5);
517        assert!(result > 0.0 && result < 1.0);
518    }
519
520    #[test]
521    fn test_easing_function_spring() {
522        let easing = EasingFunction::Spring {
523            tension: 120.0,
524            friction: 14.0,
525        };
526        let result = easing.evaluate(0.5);
527        // Spring animations can temporarily exceed bounds due to oscillation
528        assert!(
529            result >= -0.5 && result <= 1.5,
530            "Spring result was: {}",
531            result
532        );
533    }
534
535    #[test]
536    fn test_easing_function_default() {
537        let easing = EasingFunction::default();
538        match easing {
539            EasingFunction::EaseOut => {}
540            _ => panic!("Default should be EaseOut"),
541        }
542    }
543
544    #[test]
545    fn test_flip_animator_creation() {
546        let animator = FLIPAnimator::new();
547        assert_eq!(animator.active_count(), 0);
548        assert_eq!(animator.performance_metrics().total_animations, 0);
549    }
550
551    #[test]
552    fn test_flip_animator_default() {
553        let animator = FLIPAnimator::default();
554        assert_eq!(animator.active_count(), 0);
555    }
556
557    #[test]
558    fn test_performance_metrics_default() {
559        let metrics = FLIPPerformanceMetrics::default();
560        assert_eq!(metrics.total_animations, 0);
561        assert_eq!(metrics.average_duration, 0.0);
562        assert_eq!(metrics.failed_animations, 0);
563        assert_eq!(metrics.performance_score, 1.0);
564    }
565
566    #[wasm_bindgen_test]
567    fn test_flip_animator_cancel_all() {
568        let mut animator = FLIPAnimator::new();
569        animator.cancel_all();
570        assert_eq!(animator.active_count(), 0);
571    }
572
573    #[wasm_bindgen_test]
574    fn test_flip_animator_cancel_nonexistent() {
575        let mut animator = FLIPAnimator::new();
576        assert!(!animator.cancel("nonexistent"));
577    }
578
579    #[test]
580    fn test_easing_function_parsing() {
581        let animator = FLIPAnimator::new();
582
583        assert!(matches!(
584            animator.parse_easing_function("linear"),
585            Ok(EasingFunction::Linear)
586        ));
587        assert!(matches!(
588            animator.parse_easing_function("ease-in"),
589            Ok(EasingFunction::EaseIn)
590        ));
591        assert!(matches!(
592            animator.parse_easing_function("ease-out"),
593            Ok(EasingFunction::EaseOut)
594        ));
595        assert!(matches!(
596            animator.parse_easing_function("ease-in-out"),
597            Ok(EasingFunction::EaseInOut)
598        ));
599
600        assert!(matches!(
601            animator.parse_easing_function("cubic-bezier(0.25, 0.1, 0.25, 1.0)"),
602            Ok(EasingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0))
603        ));
604
605        assert!(matches!(
606            animator.parse_easing_function("spring(120, 14)"),
607            Ok(EasingFunction::Spring {
608                tension: 120.0,
609                friction: 14.0
610            })
611        ));
612
613        assert!(animator.parse_easing_function("invalid").is_err());
614    }
615
616    #[wasm_bindgen_test]
617    fn test_transform_values_calculation() {
618        let animator = FLIPAnimator::new();
619
620        // Create mock DomRect objects
621        let first = web_sys::DomRect::new_with_x_and_y_and_width_and_height(0.0, 0.0, 100.0, 100.0)
622            .unwrap();
623        let last =
624            web_sys::DomRect::new_with_x_and_y_and_width_and_height(50.0, 25.0, 200.0, 150.0)
625                .unwrap();
626
627        let transform = animator.calculate_transform_values(&first, &last);
628
629        assert_eq!(transform.translate_x, -50.0); // 0 - 50
630        assert_eq!(transform.translate_y, -25.0); // 0 - 25
631        assert_eq!(transform.scale_x, 0.5); // 100 / 200
632        assert_eq!(transform.scale_y, 100.0 / 150.0); // 100 / 150
633    }
634}