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, Default)]
111pub enum EasingFunction {
112    /// Linear easing
113    Linear,
114    /// Ease-in
115    EaseIn,
116    /// Ease-out
117    #[default]
118    EaseOut,
119    /// Ease-in-out
120    EaseInOut,
121    /// Custom cubic-bezier
122    CubicBezier(f64, f64, f64, f64),
123    /// Spring physics
124    Spring {
125        /// Spring tension
126        tension: f64,
127        /// Spring friction
128        friction: f64,
129    },
130}
131
132impl EasingFunction {
133    /// Evaluate the easing function at given progress (0.0 to 1.0)
134    pub fn evaluate(&self, t: f64) -> f64 {
135        let t = t.clamp(0.0, 1.0);
136        match self {
137            EasingFunction::Linear => t,
138            EasingFunction::EaseIn => t * t,
139            EasingFunction::EaseOut => 1.0 - (1.0 - t).powi(2),
140            EasingFunction::EaseInOut => {
141                if t < 0.5 {
142                    2.0 * t * t
143                } else {
144                    1.0 - 2.0 * (1.0 - t).powi(2)
145                }
146            }
147            EasingFunction::CubicBezier(p1, _p2, p3, _p4) => {
148                // Simplified cubic bezier evaluation
149                let u = 1.0 - t;
150                u * u * u * 0.0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p3 + t * t * t * 1.0
151            }
152            EasingFunction::Spring { tension, friction } => {
153                // Simplified spring physics
154                let damping = friction / (2.0 * (tension).sqrt());
155                let frequency = (tension).sqrt();
156
157                if damping < 1.0 {
158                    let damped_freq = frequency * (1.0 - damping * damping).sqrt();
159                    let decay = (-damping * frequency * t).exp();
160                    1.0 - decay * (damped_freq * t + damping * frequency * t / damped_freq).cos()
161                } else {
162                    1.0 - (-frequency * t).exp()
163                }
164            }
165        }
166    }
167}
168
169/// FLIP animator for managing layout transitions
170pub struct FLIPAnimator {
171    /// Active FLIP animations
172    active_animations: HashMap<String, FLIPAnimation>,
173    /// Animation frame callback
174    #[allow(dead_code)]
175    animation_frame: Option<i32>,
176    /// Performance tracking
177    performance_metrics: FLIPPerformanceMetrics,
178}
179
180/// FLIP performance metrics
181#[derive(Debug, Clone)]
182pub struct FLIPPerformanceMetrics {
183    /// Total animations created
184    pub total_animations: usize,
185    /// Average animation duration
186    pub average_duration: f64,
187    /// Failed animations
188    pub failed_animations: usize,
189    /// Performance score (0.0 to 1.0)
190    pub performance_score: f64,
191}
192
193impl Default for FLIPPerformanceMetrics {
194    fn default() -> Self {
195        Self {
196            total_animations: 0,
197            average_duration: 0.0,
198            failed_animations: 0,
199            performance_score: 1.0,
200        }
201    }
202}
203
204impl FLIPAnimator {
205    /// Create a new FLIP animator
206    pub fn new() -> Self {
207        Self {
208            active_animations: HashMap::new(),
209            animation_frame: None,
210            performance_metrics: FLIPPerformanceMetrics::default(),
211        }
212    }
213
214    /// Start a FLIP animation
215    pub fn animate(
216        &mut self,
217        id: String,
218        element: Element,
219        first: DomRect,
220        last: DomRect,
221        config: LayoutAnimationConfig,
222    ) -> Result<(), String> {
223        let inverted = self.calculate_transform_values(&first, &last);
224
225        let state = FLIPState {
226            first,
227            last,
228            inverted,
229            progress: 0.0,
230            active: true,
231        };
232
233        let animation = FLIPAnimation {
234            id: id.clone(),
235            element,
236            state,
237            config: config.clone(),
238            start_time: self.get_current_time(),
239            duration: config.duration,
240            easing: config.easing,
241        };
242
243        self.active_animations.insert(id, animation);
244        self.performance_metrics.total_animations += 1;
245
246        Ok(())
247    }
248
249    /// Calculate transform values from first to last positions
250    fn calculate_transform_values(&self, first: &DomRect, last: &DomRect) -> TransformValues {
251        let translate_x = first.x() - last.x();
252        let translate_y = first.y() - last.y();
253        let scale_x = if last.width() > 0.0 {
254            first.width() / last.width()
255        } else {
256            1.0
257        };
258        let scale_y = if last.height() > 0.0 {
259            first.height() / last.height()
260        } else {
261            1.0
262        };
263
264        TransformValues::new(translate_x, translate_y, scale_x, scale_y, 0.0)
265    }
266
267    /// Get current time in milliseconds
268    fn get_current_time(&self) -> f64 {
269        js_sys::Date::now()
270    }
271
272    /// Update all active animations
273    pub fn update(&mut self) {
274        let current_time = self.get_current_time();
275        let mut completed_ids = Vec::new();
276
277        for (id, animation) in &mut self.active_animations {
278            let elapsed = (current_time - animation.start_time) / 1000.0; // Convert to seconds
279            let progress = (elapsed / animation.duration).clamp(0.0, 1.0);
280            let eased_progress = animation.easing.evaluate(progress);
281
282            animation.state.progress = eased_progress;
283
284            if progress >= 1.0 {
285                animation.state.active = false;
286                completed_ids.push(id.clone());
287            }
288
289            // Apply transform inline to avoid borrow checker issues
290            let inverted = &animation.state.inverted;
291            let current_x = inverted.translate_x * (1.0 - eased_progress);
292            let current_y = inverted.translate_y * (1.0 - eased_progress);
293            let current_scale_x = 1.0 + (inverted.scale_x - 1.0) * (1.0 - eased_progress);
294            let current_scale_y = 1.0 + (inverted.scale_y - 1.0) * (1.0 - eased_progress);
295
296            let transform = format!(
297                "translateX({}px) translateY({}px) scaleX({}) scaleY({})",
298                current_x, current_y, current_scale_x, current_scale_y
299            );
300
301            if animation
302                .element
303                .dyn_ref::<web_sys::HtmlElement>()
304                .unwrap()
305                .style()
306                .set_property("transform", &transform)
307                .is_ok()
308            {
309                // Transform applied successfully
310            }
311        }
312
313        // Remove completed animations
314        for id in completed_ids {
315            self.active_animations.remove(&id);
316        }
317    }
318
319    /// Apply transform to element based on progress
320    #[allow(dead_code)]
321    fn apply_transform(&self, animation: &FLIPAnimation, progress: f64) {
322        let inverted = &animation.state.inverted;
323        let current_x = inverted.translate_x * (1.0 - progress);
324        let current_y = inverted.translate_y * (1.0 - progress);
325        let current_scale_x = 1.0 + (inverted.scale_x - 1.0) * (1.0 - progress);
326        let current_scale_y = 1.0 + (inverted.scale_y - 1.0) * (1.0 - progress);
327
328        let transform = format!(
329            "translateX({}px) translateY({}px) scaleX({}) scaleY({})",
330            current_x, current_y, current_scale_x, current_scale_y
331        );
332
333        if let Ok(_style) = animation
334            .element
335            .dyn_ref::<web_sys::HtmlElement>()
336            .unwrap()
337            .style()
338            .set_property("transform", &transform)
339        {
340            // Transform applied successfully
341        }
342    }
343
344    /// Get active animation count
345    pub fn active_count(&self) -> usize {
346        self.active_animations.len()
347    }
348
349    /// Get performance metrics
350    pub fn performance_metrics(&self) -> &FLIPPerformanceMetrics {
351        &self.performance_metrics
352    }
353
354    /// Cancel all animations
355    pub fn cancel_all(&mut self) {
356        self.active_animations.clear();
357    }
358
359    /// Cancel specific animation
360    pub fn cancel(&mut self, id: &str) -> bool {
361        self.active_animations.remove(id).is_some()
362    }
363
364    #[allow(dead_code)]
365    fn parse_easing_function(&self, easing: &str) -> Result<EasingFunction, String> {
366        match easing {
367            "linear" => Ok(EasingFunction::Linear),
368            "ease-in" => Ok(EasingFunction::EaseIn),
369            "ease-out" => Ok(EasingFunction::EaseOut),
370            "ease-in-out" => Ok(EasingFunction::EaseInOut),
371            _ => {
372                if easing.starts_with("cubic-bezier(") && easing.ends_with(')') {
373                    // Parse cubic-bezier values
374                    let values_str = &easing[13..easing.len() - 1];
375                    let values: Result<Vec<f64>, _> =
376                        values_str.split(',').map(|s| s.trim().parse()).collect();
377
378                    match values {
379                        Ok(v) if v.len() == 4 => {
380                            Ok(EasingFunction::CubicBezier(v[0], v[1], v[2], v[3]))
381                        }
382                        _ => Err("Invalid cubic-bezier format".to_string()),
383                    }
384                } else if easing.starts_with("spring(") && easing.ends_with(')') {
385                    // Parse spring values
386                    let values_str = &easing[7..easing.len() - 1];
387                    let values: Result<Vec<f64>, _> =
388                        values_str.split(',').map(|s| s.trim().parse()).collect();
389
390                    match values {
391                        Ok(v) if v.len() == 2 => Ok(EasingFunction::Spring {
392                            tension: v[0],
393                            friction: v[1],
394                        }),
395                        _ => Err("Invalid spring format".to_string()),
396                    }
397                } else {
398                    Err(format!("Unknown easing function: {}", easing))
399                }
400            }
401        }
402    }
403
404    #[allow(dead_code)]
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            (-0.5..=1.5).contains(&result),
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}