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, Default)]
111pub enum EasingFunction {
112 Linear,
114 EaseIn,
116 #[default]
118 EaseOut,
119 EaseInOut,
121 CubicBezier(f64, f64, f64, f64),
123 Spring {
125 tension: f64,
127 friction: f64,
129 },
130}
131
132impl EasingFunction {
133 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 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 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
169pub struct FLIPAnimator {
171 active_animations: HashMap<String, FLIPAnimation>,
173 #[allow(dead_code)]
175 animation_frame: Option<i32>,
176 performance_metrics: FLIPPerformanceMetrics,
178}
179
180#[derive(Debug, Clone)]
182pub struct FLIPPerformanceMetrics {
183 pub total_animations: usize,
185 pub average_duration: f64,
187 pub failed_animations: usize,
189 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 pub fn new() -> Self {
207 Self {
208 active_animations: HashMap::new(),
209 animation_frame: None,
210 performance_metrics: FLIPPerformanceMetrics::default(),
211 }
212 }
213
214 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 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 fn get_current_time(&self) -> f64 {
269 js_sys::Date::now()
270 }
271
272 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; 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 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 }
311 }
312
313 for id in completed_ids {
315 self.active_animations.remove(&id);
316 }
317 }
318
319 #[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 }
342 }
343
344 pub fn active_count(&self) -> usize {
346 self.active_animations.len()
347 }
348
349 pub fn performance_metrics(&self) -> &FLIPPerformanceMetrics {
351 &self.performance_metrics
352 }
353
354 pub fn cancel_all(&mut self) {
356 self.active_animations.clear();
357 }
358
359 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 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 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 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 (-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 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}