1use crate::LayoutAnimationConfig;
10use std::collections::HashMap;
11use wasm_bindgen::prelude::*;
12use web_sys::{DomRect, Element};
13
14#[derive(Debug, Clone)]
16pub struct FLIPState {
17 pub first: DomRect,
19 pub last: DomRect,
21 pub inverted: TransformValues,
23 pub progress: f64,
25 pub active: bool,
27}
28
29#[derive(Debug, Clone)]
31pub struct TransformValues {
32 pub translate_x: f64,
34 pub translate_y: f64,
36 pub scale_x: f64,
38 pub scale_y: f64,
40 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 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 pub fn translation(x: f64, y: f64) -> Self {
76 Self::new(x, y, 1.0, 1.0, 0.0)
77 }
78
79 pub fn scale(x: f64, y: f64) -> Self {
81 Self::new(0.0, 0.0, x, y, 0.0)
82 }
83
84 pub fn rotation(degrees: f64) -> Self {
86 Self::new(0.0, 0.0, 1.0, 1.0, degrees)
87 }
88}
89
90#[derive(Debug)]
92pub struct FLIPAnimation {
93 pub id: String,
95 pub element: Element,
97 pub state: FLIPState,
99 pub config: LayoutAnimationConfig,
101 pub start_time: f64,
103 pub duration: f64,
105 pub easing: EasingFunction,
107}
108
109#[derive(Debug, Clone)]
111pub enum EasingFunction {
112 Linear,
114 EaseIn,
116 EaseOut,
118 EaseInOut,
120 CubicBezier(f64, f64, f64, f64),
122 Spring {
124 tension: f64,
126 friction: f64,
128 },
129}
130
131impl Default for EasingFunction {
132 fn default() -> Self {
133 EasingFunction::EaseOut
134 }
135}
136
137impl EasingFunction {
138 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 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 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
174pub struct FLIPAnimator {
176 active_animations: HashMap<String, FLIPAnimation>,
178 animation_frame: Option<i32>,
180 performance_metrics: FLIPPerformanceMetrics,
182}
183
184#[derive(Debug, Clone)]
186pub struct FLIPPerformanceMetrics {
187 pub total_animations: usize,
189 pub average_duration: f64,
191 pub failed_animations: usize,
193 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 pub fn new() -> Self {
211 Self {
212 active_animations: HashMap::new(),
213 animation_frame: None,
214 performance_metrics: FLIPPerformanceMetrics::default(),
215 }
216 }
217
218 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 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 fn get_current_time(&self) -> f64 {
273 js_sys::Date::now()
274 }
275
276 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; 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 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 }
314 }
315
316 for id in completed_ids {
318 self.active_animations.remove(&id);
319 }
320 }
321
322 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 }
344 }
345
346 pub fn active_count(&self) -> usize {
348 self.active_animations.len()
349 }
350
351 pub fn performance_metrics(&self) -> &FLIPPerformanceMetrics {
353 &self.performance_metrics
354 }
355
356 pub fn cancel_all(&mut self) {
358 self.active_animations.clear();
359 }
360
361 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 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 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 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 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 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); assert_eq!(transform.translate_y, -25.0); assert_eq!(transform.scale_x, 0.5); assert_eq!(transform.scale_y, 100.0 / 150.0); }
634}