1#[cfg(feature = "performance-metrics")]
4use crate::performance::{
5 AnimationPool, AnimationScheduler, GPULayerManager, PerformanceBudget, PerformanceMonitor,
6};
7use crate::{
8 AnimationError, AnimationHandle, AnimationTarget, AnimationValue, Result, Transform, Transition,
9};
10use std::collections::HashMap;
11#[cfg(feature = "web-sys")]
12use wasm_bindgen::prelude::*;
13#[cfg(feature = "web-sys")]
14use web_sys::window;
15
16pub trait AnimationEngine {
18 fn is_available(&self) -> bool;
20
21 fn animate(&mut self, animation: &AnimationConfig) -> Result<AnimationHandle>;
23
24 fn stop(&mut self, handle: AnimationHandle) -> Result<()>;
26
27 fn pause(&mut self, handle: AnimationHandle) -> Result<()>;
29
30 fn resume(&mut self, handle: AnimationHandle) -> Result<()>;
32
33 fn tick(&mut self, timestamp: f64) -> Result<()>;
35
36 fn get_state(&self, handle: AnimationHandle) -> Result<PlaybackState>;
38
39 fn is_running(&self, handle: AnimationHandle) -> bool;
41
42 #[cfg(feature = "performance-metrics")]
44 fn get_performance_metrics(&self) -> Option<crate::performance::PerformanceReport>;
45
46 #[cfg(not(feature = "performance-metrics"))]
48 fn get_performance_metrics(&self) -> Option<()>;
49}
50
51#[derive(Clone)]
53pub struct AnimationConfig {
54 #[cfg(feature = "web-sys")]
56 pub element: web_sys::Element,
57 pub from: AnimationTarget,
59 pub to: AnimationTarget,
61 pub transition: Transition,
63 pub on_complete_id: Option<u64>,
65 pub on_update_id: Option<u64>,
67}
68
69impl std::fmt::Debug for AnimationConfig {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("AnimationConfig")
72 .field("element", &"<Element>")
73 .field("from", &self.from)
74 .field("to", &self.to)
75 .field("transition", &self.transition)
76 .field(
77 "on_complete",
78 &if self.on_complete_id.is_some() {
79 "Some(<callback>)"
80 } else {
81 "None"
82 },
83 )
84 .field(
85 "on_update",
86 &if self.on_update_id.is_some() {
87 "Some(<callback>)"
88 } else {
89 "None"
90 },
91 )
92 .finish()
93 }
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub enum PlaybackState {
99 Running,
101 Paused,
103 Completed,
105 Cancelled,
107}
108
109pub struct OptimizedHybridEngine {
111 #[cfg(feature = "web-sys")]
112 waapi_engine: WaapiEngine,
113 raf_engine: RafEngine,
114 feature_detector: FeatureDetector,
115 #[cfg(feature = "performance-metrics")]
116 performance_monitor: Option<PerformanceMonitor>,
117 #[cfg(feature = "performance-metrics")]
118 _scheduler: AnimationScheduler,
119 #[cfg(feature = "performance-metrics")]
120 gpu_manager: GPULayerManager,
121 #[cfg(feature = "performance-metrics")]
122 animation_pool: AnimationPool,
123 current_handle: u64,
124 frame_count: u64,
125}
126
127impl OptimizedHybridEngine {
128 pub fn new() -> Self {
130 Self {
131 #[cfg(feature = "web-sys")]
132 waapi_engine: WaapiEngine::new(),
133 raf_engine: RafEngine::new(),
134 feature_detector: FeatureDetector::new(),
135 #[cfg(feature = "performance-metrics")]
136 performance_monitor: {
137 let budget = PerformanceBudget::default();
138 Some(PerformanceMonitor::new(budget))
139 },
140 #[cfg(feature = "performance-metrics")]
141 _scheduler: AnimationScheduler::new(),
142 #[cfg(feature = "performance-metrics")]
143 gpu_manager: GPULayerManager::new(50), #[cfg(feature = "performance-metrics")]
145 animation_pool: AnimationPool::new(100),
146 current_handle: 0,
147 frame_count: 0,
148 }
149 }
150
151 #[cfg(feature = "performance-metrics")]
153 pub fn start_performance_monitoring(&mut self) {
154 if let Some(monitor) = &mut self.performance_monitor {
155 monitor.record_frame_timestamp(std::time::Instant::now());
156 }
157 }
158
159 #[cfg(feature = "performance-metrics")]
161 pub fn end_performance_monitoring(&mut self, animations_updated: usize) {
162 if let Some(monitor) = &mut self.performance_monitor {
163 let memory_usage = self.animation_pool.in_use_count() * 1024; let gpu_layers = self.gpu_manager.layer_count();
165 let _report = monitor.generate_report(animations_updated, memory_usage, gpu_layers);
166 }
167 }
168
169 #[cfg(not(feature = "performance-metrics"))]
171 pub fn start_performance_monitoring(&mut self) {
172 }
174
175 #[cfg(not(feature = "performance-metrics"))]
177 pub fn end_performance_monitoring(&mut self, _animations_updated: usize) {
178 }
180
181 #[cfg(feature = "performance-metrics")]
183 pub fn get_performance_report(&self) -> Option<crate::performance::PerformanceReport> {
184 None
187 }
188
189 #[cfg(not(feature = "performance-metrics"))]
191 pub fn get_performance_report(&self) -> Option<()> {
192 None
193 }
194
195 #[cfg(feature = "web-sys")]
197 pub fn optimize_for_gpu(&mut self, _element: &web_sys::Element) -> bool {
198 false
201 }
202
203 fn select_engine(&self, config: &AnimationConfig) -> EngineChoice {
205 if self.feature_detector.supports_waapi() && self.feature_detector.can_use_waapi_for(config)
206 {
207 EngineChoice::Waapi
208 } else {
209 EngineChoice::Raf
210 }
211 }
212
213 fn next_handle(&mut self) -> AnimationHandle {
215 self.current_handle += 1;
216 AnimationHandle(self.current_handle)
217 }
218}
219
220impl AnimationEngine for OptimizedHybridEngine {
221 fn is_available(&self) -> bool {
222 #[cfg(feature = "web-sys")]
223 let waapi_available = self.waapi_engine.is_available();
224 #[cfg(not(feature = "web-sys"))]
225 let waapi_available = false;
226 waapi_available || self.raf_engine.is_available()
227 }
228
229 fn animate(&mut self, config: &AnimationConfig) -> Result<AnimationHandle> {
230 let handle = self.next_handle();
231
232 self.start_performance_monitoring();
234
235 #[cfg(feature = "web-sys")]
237 self.optimize_for_gpu(&config.element);
238
239 let engine_choice = self.select_engine(config);
241
242 let result = match engine_choice {
243 EngineChoice::Waapi => {
244 #[cfg(feature = "web-sys")]
245 {
246 self.waapi_engine
247 .animate_with_handle(handle, config.clone())
248 }
249 #[cfg(not(feature = "web-sys"))]
250 {
251 Err(crate::AnimationError::EngineUnavailable(
252 "WAAPI not available".to_string(),
253 ))
254 }
255 }
256 EngineChoice::Raf => {
257 #[cfg(feature = "web-sys")]
259 let start_time = web_sys::window().unwrap().performance().unwrap().now();
260 #[cfg(not(feature = "web-sys"))]
261 let start_time = 0.0; let animation = RafAnimation::new(config.clone(), start_time);
263 self.raf_engine.animations.insert(handle, animation);
264
265 if self.raf_engine.raf_handle.is_none() {
267 self.raf_engine.start_raf_loop()?;
268 }
269
270 Ok(())
271 }
272 };
273
274 self.end_performance_monitoring(1);
276
277 result?;
278 Ok(handle)
279 }
280
281 fn stop(&mut self, handle: AnimationHandle) -> Result<()> {
282 #[cfg(feature = "web-sys")]
284 if let Ok(()) = self.waapi_engine.stop(handle) {
285 return Ok(());
286 }
287
288 if let Ok(()) = self.raf_engine.stop(handle) {
290 #[cfg(feature = "performance-metrics")]
292 self.animation_pool.return_animation(handle.0);
293 return Ok(());
294 }
295
296 Err(AnimationError::NotFound { handle })
297 }
298
299 fn pause(&mut self, handle: AnimationHandle) -> Result<()> {
300 #[cfg(feature = "web-sys")]
302 if let Ok(()) = self.waapi_engine.pause(handle) {
303 return Ok(());
304 }
305
306 self.raf_engine.pause(handle)
308 }
309
310 fn resume(&mut self, handle: AnimationHandle) -> Result<()> {
311 #[cfg(feature = "web-sys")]
313 if let Ok(()) = self.waapi_engine.resume(handle) {
314 return Ok(());
315 }
316
317 self.raf_engine.resume(handle)
319 }
320
321 fn tick(&mut self, timestamp: f64) -> Result<()> {
322 self.frame_count += 1;
323
324 self.start_performance_monitoring();
326
327 let raf_result = self.raf_engine.tick(timestamp);
329
330 self.end_performance_monitoring(self.raf_engine.animations.len());
332
333 raf_result
334 }
335
336 fn get_state(&self, handle: AnimationHandle) -> Result<PlaybackState> {
337 #[cfg(feature = "web-sys")]
339 if let Ok(state) = self.waapi_engine.get_state(handle) {
340 return Ok(state);
341 }
342
343 self.raf_engine.get_state(handle)
345 }
346
347 fn is_running(&self, handle: AnimationHandle) -> bool {
348 #[cfg(feature = "web-sys")]
349 let waapi_running = self.waapi_engine.is_running(handle);
350 #[cfg(not(feature = "web-sys"))]
351 let waapi_running = false;
352 waapi_running || self.raf_engine.is_running(handle)
353 }
354
355 #[cfg(feature = "performance-metrics")]
356 fn get_performance_metrics(&self) -> Option<crate::performance::PerformanceReport> {
357 self.get_performance_report()
358 }
359
360 #[cfg(not(feature = "performance-metrics"))]
361 fn get_performance_metrics(&self) -> Option<()> {
362 self.get_performance_report()
363 }
364}
365
366#[cfg(feature = "web-sys")]
368pub struct WaapiEngine {
369 animations: HashMap<AnimationHandle, web_sys::Animation>,
370}
371
372#[cfg(feature = "web-sys")]
373impl WaapiEngine {
374 pub fn new() -> Self {
376 Self {
377 animations: HashMap::new(),
378 }
379 }
380
381 pub fn animate_with_handle(
383 &mut self,
384 handle: AnimationHandle,
385 _config: AnimationConfig,
386 ) -> Result<()> {
387 self.animations
390 .insert(handle, web_sys::Animation::new().unwrap_throw());
391 Ok(())
392 }
393}
394
395#[cfg(feature = "web-sys")]
396impl AnimationEngine for WaapiEngine {
397 fn is_available(&self) -> bool {
398 window()
399 .and_then(|w| w.document())
400 .and_then(|d| d.create_element("div").ok())
401 .and_then(|e| js_sys::Reflect::has(&e, &"animate".into()).ok())
402 .unwrap_or(false)
403 }
404
405 fn animate(&mut self, _config: &AnimationConfig) -> Result<AnimationHandle> {
406 Err(AnimationError::EngineUnavailable(
408 "WAAPI not implemented".to_string(),
409 ))
410 }
411
412 fn stop(&mut self, handle: AnimationHandle) -> Result<()> {
413 if let Some(animation) = self.animations.remove(&handle) {
414 animation.cancel();
415 Ok(())
416 } else {
417 Err(AnimationError::NotFound { handle })
418 }
419 }
420
421 fn pause(&mut self, handle: AnimationHandle) -> Result<()> {
422 if let Some(animation) = self.animations.get(&handle) {
423 let _ = animation.pause();
424 Ok(())
425 } else {
426 Err(AnimationError::NotFound { handle })
427 }
428 }
429
430 fn resume(&mut self, handle: AnimationHandle) -> Result<()> {
431 if let Some(animation) = self.animations.get(&handle) {
432 let _ = animation.play();
433 Ok(())
434 } else {
435 Err(AnimationError::NotFound { handle })
436 }
437 }
438
439 fn tick(&mut self, _timestamp: f64) -> Result<()> {
440 Ok(())
442 }
443
444 fn get_state(&self, handle: AnimationHandle) -> Result<PlaybackState> {
445 if let Some(_animation) = self.animations.get(&handle) {
446 Ok(PlaybackState::Running)
448 } else {
449 Err(AnimationError::NotFound { handle })
450 }
451 }
452
453 fn is_running(&self, handle: AnimationHandle) -> bool {
454 self.animations.contains_key(&handle)
455 }
456
457 #[cfg(feature = "performance-metrics")]
458 fn get_performance_metrics(&self) -> Option<crate::performance::PerformanceReport> {
459 None }
461
462 #[cfg(not(feature = "performance-metrics"))]
463 fn get_performance_metrics(&self) -> Option<()> {
464 None }
466}
467
468pub struct RafEngine {
470 animations: HashMap<AnimationHandle, RafAnimation>,
471 #[cfg(feature = "web-sys")]
472 performance: web_sys::Performance,
473 raf_handle: Option<i32>,
474}
475
476impl RafEngine {
477 pub fn new() -> Self {
479 #[cfg(feature = "web-sys")]
480 let performance = window()
481 .and_then(|w| w.performance())
482 .expect("Performance API not available");
483
484 Self {
485 animations: HashMap::new(),
486 #[cfg(feature = "web-sys")]
487 performance,
488 raf_handle: None,
489 }
490 }
491
492 pub fn animate_with_handle(
494 &mut self,
495 handle: AnimationHandle,
496 config: AnimationConfig,
497 ) -> Result<()> {
498 #[cfg(feature = "web-sys")]
499 let start_time = self.performance.now();
500 #[cfg(not(feature = "web-sys"))]
501 let start_time = 0.0;
502 let animation = RafAnimation::new(config, start_time);
503 self.animations.insert(handle, animation);
504
505 if self.raf_handle.is_none() {
507 self.start_raf_loop()?;
508 }
509
510 Ok(())
511 }
512
513 fn start_raf_loop(&mut self) -> Result<()> {
514 Ok(())
517 }
518}
519
520impl AnimationEngine for RafEngine {
521 fn is_available(&self) -> bool {
522 if cfg!(feature = "web-sys") {
523 #[cfg(feature = "web-sys")]
524 {
525 window().is_some()
526 }
527 #[cfg(not(feature = "web-sys"))]
528 {
529 false
530 }
531 } else {
532 false
533 }
534 }
535
536 fn animate(&mut self, config: &AnimationConfig) -> Result<AnimationHandle> {
537 let handle = AnimationHandle(rand::random());
538 self.animate_with_handle(handle, config.clone())?;
539 Ok(handle)
540 }
541
542 fn stop(&mut self, handle: AnimationHandle) -> Result<()> {
543 if self.animations.remove(&handle).is_some() {
544 Ok(())
545 } else {
546 Err(AnimationError::NotFound { handle })
547 }
548 }
549
550 fn pause(&mut self, handle: AnimationHandle) -> Result<()> {
551 if let Some(animation) = self.animations.get_mut(&handle) {
552 animation.state = PlaybackState::Paused;
553 Ok(())
554 } else {
555 Err(AnimationError::NotFound { handle })
556 }
557 }
558
559 fn resume(&mut self, handle: AnimationHandle) -> Result<()> {
560 if let Some(animation) = self.animations.get_mut(&handle) {
561 animation.state = PlaybackState::Running;
562 Ok(())
563 } else {
564 Err(AnimationError::NotFound { handle })
565 }
566 }
567
568 fn tick(&mut self, timestamp: f64) -> Result<()> {
569 let mut completed = Vec::new();
570
571 for (&handle, animation) in self.animations.iter_mut() {
572 if animation.state == PlaybackState::Running {
573 animation.update(timestamp);
574
575 if animation.is_complete() {
576 animation.state = PlaybackState::Completed;
577 completed.push(handle);
578 }
579 }
580 }
581
582 for handle in completed {
584 self.animations.remove(&handle);
585 }
586
587 Ok(())
588 }
589
590 fn get_state(&self, handle: AnimationHandle) -> Result<PlaybackState> {
591 if let Some(animation) = self.animations.get(&handle) {
592 Ok(animation.state.clone())
593 } else {
594 Err(AnimationError::NotFound { handle })
595 }
596 }
597
598 fn is_running(&self, handle: AnimationHandle) -> bool {
599 self.animations
600 .get(&handle)
601 .map(|a| a.state == PlaybackState::Running)
602 .unwrap_or(false)
603 }
604
605 #[cfg(feature = "performance-metrics")]
606 fn get_performance_metrics(&self) -> Option<crate::performance::PerformanceReport> {
607 None }
609
610 #[cfg(not(feature = "performance-metrics"))]
611 fn get_performance_metrics(&self) -> Option<()> {
612 None }
614}
615
616struct RafAnimation {
618 config: AnimationConfig,
619 start_time: f64,
620 state: PlaybackState,
621}
622
623impl RafAnimation {
624 fn new(config: AnimationConfig, start_time: f64) -> Self {
625 Self {
626 config,
627 start_time,
628 state: PlaybackState::Running,
629 }
630 }
631
632 fn update(&mut self, timestamp: f64) {
633 let elapsed = timestamp - self.start_time;
634 let duration = self.config.transition.duration.unwrap_or(1.0) * 1000.0; if elapsed >= duration {
637 self.state = PlaybackState::Completed;
638 }
639
640 let progress = (elapsed / duration).clamp(0.0, 1.0);
641 let eased_progress = self.config.transition.ease.evaluate(progress);
642
643 self.apply_values(eased_progress);
645
646 if let Some(_callback_id) = self.config.on_update_id {
647 }
650 }
651
652 fn apply_values(&self, progress: f64) {
653 let mut current_values = HashMap::new();
655
656 for (property, from_value) in &self.config.from {
657 if let Some(to_value) = self.config.to.get(property) {
658 let interpolated = self.interpolate_values(from_value, to_value, progress);
660 current_values.insert(property.clone(), interpolated);
661 }
662 }
663
664 self.apply_to_element(¤t_values);
666 }
667
668 fn interpolate_values(
669 &self,
670 from: &AnimationValue,
671 to: &AnimationValue,
672 progress: f64,
673 ) -> AnimationValue {
674 match (from, to) {
675 (AnimationValue::Number(from_val), AnimationValue::Number(to_val)) => {
676 let interpolated = from_val + (to_val - from_val) * progress;
677 AnimationValue::Number(interpolated)
678 }
679 (AnimationValue::Pixels(from_val), AnimationValue::Pixels(to_val)) => {
680 let interpolated = from_val + (to_val - from_val) * progress;
681 AnimationValue::Pixels(interpolated)
682 }
683 (AnimationValue::Percentage(from_val), AnimationValue::Percentage(to_val)) => {
684 let interpolated = from_val + (to_val - from_val) * progress;
685 AnimationValue::Percentage(interpolated)
686 }
687 (AnimationValue::Degrees(from_val), AnimationValue::Degrees(to_val)) => {
688 let interpolated = from_val + (to_val - from_val) * progress;
689 AnimationValue::Degrees(interpolated)
690 }
691 (AnimationValue::Color(from_color), AnimationValue::Color(to_color)) => {
692 if progress < 0.5 {
693 AnimationValue::Color(from_color.clone())
694 } else {
695 AnimationValue::Color(to_color.clone())
696 }
697 }
698 (
699 AnimationValue::Transform(from_transform),
700 AnimationValue::Transform(to_transform),
701 ) => {
702 let interpolated =
703 self.interpolate_transform(from_transform, to_transform, progress);
704 AnimationValue::Transform(interpolated)
705 }
706 _ => {
707 if progress < 0.5 {
708 from.clone()
709 } else {
710 to.clone()
711 }
712 }
713 }
714 }
715
716 fn interpolate_transform(&self, from: &Transform, to: &Transform, progress: f64) -> Transform {
717 Transform {
718 x: self.interpolate_option(from.x, to.x, progress),
719 y: self.interpolate_option(from.y, to.y, progress),
720 z: self.interpolate_option(from.z, to.z, progress),
721 rotate_x: self.interpolate_option(from.rotate_x, to.rotate_x, progress),
722 rotate_y: self.interpolate_option(from.rotate_y, to.rotate_y, progress),
723 rotate_z: self.interpolate_option(from.rotate_z, to.rotate_z, progress),
724 scale: self.interpolate_option(from.scale, to.scale, progress),
725 scale_x: self.interpolate_option(from.scale_x, to.scale_x, progress),
726 scale_y: self.interpolate_option(from.scale_y, to.scale_y, progress),
727 skew_x: self.interpolate_option(from.skew_x, to.skew_x, progress),
728 skew_y: self.interpolate_option(from.skew_y, to.skew_y, progress),
729 }
730 }
731
732 fn interpolate_option(&self, from: Option<f64>, to: Option<f64>, progress: f64) -> Option<f64> {
733 match (from, to) {
734 (Some(from_val), Some(to_val)) => {
735 let interpolated = from_val + (to_val - from_val) * progress;
736 Some(interpolated)
737 }
738 (Some(val), None) | (None, Some(val)) => Some(val),
739 (None, None) => None,
740 }
741 }
742
743 fn apply_to_element(&self, values: &HashMap<String, AnimationValue>) {
744 for (property, value) in values {
746 let css_value = self.animation_value_to_css(property, value);
747 let _ = css_value; }
751 }
752
753 fn animation_value_to_css(&self, _property: &str, value: &AnimationValue) -> String {
754 match value {
755 AnimationValue::Number(val) => val.to_string(),
756 AnimationValue::Pixels(val) => format!("{}px", val),
757 AnimationValue::Percentage(val) => format!("{}%", val),
758 AnimationValue::Degrees(val) => format!("{}deg", val),
759 AnimationValue::Color(color) => color.clone(),
760 AnimationValue::Transform(transform) => self.transform_to_css(transform),
761 AnimationValue::String(s) => s.clone(),
762 _ => "".to_string(),
763 }
764 }
765
766 fn transform_to_css(&self, transform: &Transform) -> String {
767 let mut parts = Vec::new();
768
769 if let Some(x) = transform.x {
770 parts.push(format!("translateX({}px)", x));
771 }
772 if let Some(y) = transform.y {
773 parts.push(format!("translateY({}px)", y));
774 }
775 if let Some(z) = transform.z {
776 parts.push(format!("translateZ({}px)", z));
777 }
778 if let Some(rotate_x) = transform.rotate_x {
779 parts.push(format!("rotateX({}deg)", rotate_x));
780 }
781 if let Some(rotate_y) = transform.rotate_y {
782 parts.push(format!("rotateY({}deg)", rotate_y));
783 }
784 if let Some(rotate_z) = transform.rotate_z {
785 parts.push(format!("rotateZ({}deg)", rotate_z));
786 }
787 if let Some(scale) = transform.scale {
788 parts.push(format!("scale({})", scale));
789 }
790 if let Some(scale_x) = transform.scale_x {
791 parts.push(format!("scaleX({})", scale_x));
792 }
793 if let Some(scale_y) = transform.scale_y {
794 parts.push(format!("scaleY({})", scale_y));
795 }
796 if let Some(skew_x) = transform.skew_x {
797 parts.push(format!("skewX({}deg)", skew_x));
798 }
799 if let Some(skew_y) = transform.skew_y {
800 parts.push(format!("skewY({}deg)", skew_y));
801 }
802
803 parts.join(" ")
804 }
805
806 fn is_complete(&self) -> bool {
807 matches!(
808 self.state,
809 PlaybackState::Completed | PlaybackState::Cancelled
810 )
811 }
812}
813
814#[derive(Default)]
816pub struct FeatureDetector {
817 waapi_available: Option<bool>,
818}
819
820impl FeatureDetector {
821 pub fn new() -> Self {
823 Self::default()
824 }
825
826 pub fn supports_waapi(&self) -> bool {
828 if let Some(available) = self.waapi_available {
830 return available;
831 }
832
833 #[cfg(feature = "web-sys")]
835 let available = window()
836 .and_then(|w| w.document())
837 .and_then(|d| d.create_element("div").ok())
838 .and_then(|e| js_sys::Reflect::has(&e, &"animate".into()).ok())
839 .unwrap_or(false);
840 #[cfg(not(feature = "web-sys"))]
841 let available = false;
842
843 available
844 }
845
846 pub fn can_use_waapi_for(&self, config: &AnimationConfig) -> bool {
848 for property in config.to.keys() {
850 if !self.is_waapi_property(property) {
851 return false;
852 }
853 }
854 true
855 }
856
857 fn is_waapi_property(&self, property: &str) -> bool {
858 matches!(
860 property,
861 "opacity"
862 | "transform"
863 | "left"
864 | "top"
865 | "width"
866 | "height"
867 | "background-color"
868 | "color"
869 | "border-radius"
870 | "scale"
871 | "rotate"
872 | "translateX"
873 | "translateY"
874 | "translateZ"
875 | "rotateX"
876 | "rotateY"
877 | "rotateZ"
878 )
879 }
880}
881
882#[derive(Debug, Clone, Copy)]
883enum EngineChoice {
884 Waapi,
885 Raf,
886}
887
888impl Default for OptimizedHybridEngine {
889 fn default() -> Self {
890 Self::new()
891 }
892}
893
894#[cfg(feature = "web-sys")]
895impl Default for WaapiEngine {
896 fn default() -> Self {
897 Self::new()
898 }
899}
900
901impl Default for RafEngine {
902 fn default() -> Self {
903 Self::new()
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910 use crate::{AnimationValue, Transform};
911 use std::collections::HashMap;
912
913 fn create_test_animation_target() -> (
914 HashMap<String, AnimationValue>,
915 HashMap<String, AnimationValue>,
916 ) {
917 let mut from = HashMap::new();
918 from.insert("opacity".to_string(), AnimationValue::Number(0.0));
919 from.insert("x".to_string(), AnimationValue::Pixels(0.0));
920
921 let mut to = HashMap::new();
922 to.insert("opacity".to_string(), AnimationValue::Number(1.0));
923 to.insert("x".to_string(), AnimationValue::Pixels(100.0));
924
925 (from, to)
926 }
927
928 #[test]
930 fn test_core_types() {
931 let number_val = AnimationValue::Number(42.0);
933 let pixel_val = AnimationValue::Pixels(100.0);
934 let percent_val = AnimationValue::Percentage(50.0);
935 let degree_val = AnimationValue::Degrees(180.0);
936 let color_val = AnimationValue::Color("#ff0000".to_string());
937 let string_val = AnimationValue::String("solid".to_string());
938
939 let _cloned_number = number_val.clone();
941 let _cloned_pixel = pixel_val.clone();
942 let _cloned_percent = percent_val.clone();
943 let _cloned_degree = degree_val.clone();
944 let _cloned_color = color_val.clone();
945 let _cloned_string = string_val.clone();
946
947 let transform = Transform {
949 x: Some(10.0),
950 y: Some(20.0),
951 z: Some(30.0),
952 rotate_x: Some(45.0),
953 rotate_y: Some(90.0),
954 rotate_z: Some(180.0),
955 scale: Some(1.5),
956 scale_x: Some(2.0),
957 scale_y: Some(0.5),
958 skew_x: Some(15.0),
959 skew_y: Some(30.0),
960 };
961
962 let _cloned_transform = transform.clone();
964
965 assert_eq!(transform.x, Some(10.0));
967 assert_eq!(transform.y, Some(20.0));
968 assert_eq!(transform.z, Some(30.0));
969 assert_eq!(transform.rotate_x, Some(45.0));
970 assert_eq!(transform.rotate_y, Some(90.0));
971 assert_eq!(transform.rotate_z, Some(180.0));
972 assert_eq!(transform.scale, Some(1.5));
973 assert_eq!(transform.scale_x, Some(2.0));
974 assert_eq!(transform.scale_y, Some(0.5));
975 assert_eq!(transform.skew_x, Some(15.0));
976 assert_eq!(transform.skew_y, Some(30.0));
977 }
978
979 #[test]
981 fn test_interpolation_math() {
982 let from = 0.0;
984 let to = 100.0;
985
986 let result = from + (to - from) * 0.0;
988 assert_eq!(result, 0.0);
989
990 let result = from + (to - from) * 0.5;
992 assert_eq!(result, 50.0);
993
994 let result = from + (to - from) * 1.0;
996 assert_eq!(result, 100.0);
997
998 let result = from + (to - from) * 0.25;
1000 assert_eq!(result, 25.0);
1001
1002 let from_neg = -50.0;
1004 let to_neg = 50.0;
1005 let result = from_neg + (to_neg - from_neg) * 0.5;
1006 assert_eq!(result, 0.0);
1007 }
1008
1009 #[test]
1011 fn test_option_interpolation() {
1012 let from = Some(0.0);
1014 let to = Some(100.0);
1015
1016 let interpolate = |from: Option<f64>, to: Option<f64>, progress: f64| -> Option<f64> {
1018 match (from, to) {
1019 (Some(from_val), Some(to_val)) => {
1020 let interpolated = from_val + (to_val - from_val) * progress;
1021 Some(interpolated)
1022 }
1023 (Some(val), None) | (None, Some(val)) => Some(val),
1024 (None, None) => None,
1025 }
1026 };
1027
1028 let result = interpolate(from, to, 0.5);
1030 assert_eq!(result, Some(50.0));
1031
1032 let result = interpolate(None, to, 0.5);
1034 assert_eq!(result, Some(100.0));
1035
1036 let result = interpolate(from, None, 0.5);
1038 assert_eq!(result, Some(0.0));
1039
1040 let result = interpolate(None, None, 0.5);
1042 assert_eq!(result, None);
1043 }
1044
1045 #[test]
1047 fn test_basic_functionality() {
1048 let _detector = FeatureDetector::new();
1050
1051 let states = vec![
1053 PlaybackState::Running,
1054 PlaybackState::Paused,
1055 PlaybackState::Completed,
1056 PlaybackState::Cancelled,
1057 ];
1058
1059 for state in states {
1060 let cloned = state.clone();
1062 assert_eq!(state, cloned);
1063 }
1064 }
1065
1066 #[test]
1067 fn test_animation_config_targets() {
1068 let (from, to) = create_test_animation_target();
1069 assert_eq!(from.len(), 2);
1070 assert_eq!(to.len(), 2);
1071 assert!(from.contains_key("opacity"));
1072 assert!(to.contains_key("opacity"));
1073 assert_eq!(from.get("opacity"), Some(&AnimationValue::Number(0.0)));
1074 assert_eq!(to.get("opacity"), Some(&AnimationValue::Number(1.0)));
1075 }
1076
1077 #[test]
1078 fn test_animation_config_callbacks() {
1079 let callback_ids = (Some(123u64), Some(456u64));
1081 assert_eq!(callback_ids.0, Some(123));
1082 assert_eq!(callback_ids.1, Some(456));
1083 }
1084
1085 #[test]
1086 fn test_playback_state_variants() {
1087 let running = PlaybackState::Running;
1088 let paused = PlaybackState::Paused;
1089 let completed = PlaybackState::Completed;
1090 let cancelled = PlaybackState::Cancelled;
1091
1092 assert_eq!(running, PlaybackState::Running);
1093 assert_eq!(paused, PlaybackState::Paused);
1094 assert_eq!(completed, PlaybackState::Completed);
1095 assert_eq!(cancelled, PlaybackState::Cancelled);
1096
1097 assert_ne!(running, paused);
1098 assert_ne!(completed, cancelled);
1099 }
1100
1101 #[test]
1102 fn test_engine_type_basics() {
1103 let waapi_available = false; let raf_available = true; assert!(raf_available || waapi_available); }
1109
1110 #[test]
1111 fn test_handle_generation_logic() {
1112 let mut current_handle = 0u64;
1114
1115 let mut next_handle = || {
1116 current_handle += 1;
1117 AnimationHandle(current_handle)
1118 };
1119
1120 let handle1 = next_handle();
1121 let handle2 = next_handle();
1122 let handle3 = next_handle();
1123
1124 assert_ne!(handle1.0, handle2.0);
1125 assert_ne!(handle2.0, handle3.0);
1126 assert!(handle1.0 < handle2.0);
1127 assert!(handle2.0 < handle3.0);
1128 assert_eq!(handle1.0, 1);
1129 assert_eq!(handle2.0, 2);
1130 assert_eq!(handle3.0, 3);
1131 }
1132
1133 #[test]
1134 fn test_gpu_optimization_logic() {
1135 let gpu_enabled = false; assert!(!gpu_enabled);
1138 }
1139
1140 #[cfg(feature = "web-sys")]
1141 #[test]
1142 fn test_waapi_engine_creation() {
1143 let engine = WaapiEngine::new();
1144 assert!(engine.animations.is_empty());
1145 }
1146
1147 #[cfg(feature = "web-sys")]
1148 #[test]
1149 fn test_waapi_engine_default() {
1150 let engine = WaapiEngine::default();
1151 assert!(engine.animations.is_empty());
1152 }
1153
1154 #[cfg(feature = "web-sys")]
1155 #[test]
1156 fn test_waapi_engine_tick() {
1157 let mut engine = WaapiEngine::new();
1158 let result = engine.tick(123.45);
1159 assert!(result.is_ok()); }
1161
1162 #[cfg(feature = "web-sys")]
1163 #[test]
1164 fn test_waapi_engine_performance_metrics() {
1165 let engine = WaapiEngine::new();
1166 let metrics = engine.get_performance_metrics();
1167 assert!(metrics.is_none()); }
1169
1170 #[test]
1172 fn test_raf_engine_basics() {
1173 let animations = HashMap::<AnimationHandle, RafAnimation>::new();
1175 assert!(animations.is_empty());
1176
1177 let raf_handle: Option<i32> = None;
1178 assert!(raf_handle.is_none());
1179 }
1180
1181 #[test]
1182 fn test_raf_animation_basics() {
1183 let start_time = 100.0;
1185 let state = PlaybackState::Running;
1186
1187 assert_eq!(start_time, 100.0);
1188 assert_eq!(state, PlaybackState::Running);
1189 }
1190
1191 #[test]
1192 fn test_raf_animation_progress_calculation() {
1193 let start_time = 0.0;
1195 let duration_ms = 1000.0; let timestamp1 = 500.0; let elapsed1 = timestamp1 - start_time;
1200 let progress1 = (elapsed1 / duration_ms as f64).clamp(0.0, 1.0);
1201 assert_eq!(progress1, 0.5);
1202
1203 let timestamp2 = 1500.0; let elapsed2 = timestamp2 - start_time;
1205 let progress2 = (elapsed2 / duration_ms as f64).clamp(0.0, 1.0);
1206 assert_eq!(progress2, 1.0);
1207 }
1208
1209 #[test]
1210 fn test_animation_value_interpolation_logic() {
1211 let from_num = 0.0;
1215 let to_num = 100.0;
1216 let progress = 0.5;
1217 let result = from_num + (to_num - from_num) * progress;
1218 assert_eq!(result, 50.0);
1219
1220 let from_px = 0.0;
1222 let to_px = 200.0;
1223 let result = from_px + (to_px - from_px) * 0.25;
1224 assert_eq!(result, 50.0);
1225
1226 let from_pct = 0.0;
1228 let to_pct = 100.0;
1229 let result = from_pct + (to_pct - from_pct) * 0.75;
1230 assert_eq!(result, 75.0);
1231
1232 let from_deg = 0.0;
1234 let to_deg = 360.0;
1235 let result = from_deg + (to_deg - from_deg) * 0.5;
1236 assert_eq!(result, 180.0);
1237
1238 let color_from = "red";
1240 let color_to = "blue";
1241 let result_early = if 0.3 < 0.5 { color_from } else { color_to };
1242 assert_eq!(result_early, "red");
1243 let result_late = if 0.7 < 0.5 { color_from } else { color_to };
1244 assert_eq!(result_late, "blue");
1245 }
1246
1247 #[test]
1248 fn test_transform_interpolation_logic() {
1249 let from_transform = Transform {
1251 x: Some(0.0),
1252 y: Some(0.0),
1253 scale: Some(1.0),
1254 rotate_z: Some(0.0),
1255 ..Transform::default()
1256 };
1257
1258 let to_transform = Transform {
1259 x: Some(100.0),
1260 y: Some(200.0),
1261 scale: Some(2.0),
1262 rotate_z: Some(360.0),
1263 ..Transform::default()
1264 };
1265
1266 let progress = 0.5;
1267
1268 let x_result = match (from_transform.x, to_transform.x) {
1270 (Some(from_val), Some(to_val)) => Some(from_val + (to_val - from_val) * progress),
1271 (Some(val), None) | (None, Some(val)) => Some(val),
1272 (None, None) => None,
1273 };
1274 assert_eq!(x_result, Some(50.0));
1275
1276 let y_result = match (from_transform.y, to_transform.y) {
1277 (Some(from_val), Some(to_val)) => Some(from_val + (to_val - from_val) * progress),
1278 (Some(val), None) | (None, Some(val)) => Some(val),
1279 (None, None) => None,
1280 };
1281 assert_eq!(y_result, Some(100.0));
1282
1283 let scale_result = match (from_transform.scale, to_transform.scale) {
1284 (Some(from_val), Some(to_val)) => Some(from_val + (to_val - from_val) * progress),
1285 (Some(val), None) | (None, Some(val)) => Some(val),
1286 (None, None) => None,
1287 };
1288 assert_eq!(scale_result, Some(1.5));
1289
1290 let rotate_result = match (from_transform.rotate_z, to_transform.rotate_z) {
1291 (Some(from_val), Some(to_val)) => Some(from_val + (to_val - from_val) * progress),
1292 (Some(val), None) | (None, Some(val)) => Some(val),
1293 (None, None) => None,
1294 };
1295 assert_eq!(rotate_result, Some(180.0));
1296 }
1297
1298 #[test]
1299 fn test_animation_value_to_css_logic() {
1300 let number_val = 0.5;
1304 let result = number_val.to_string();
1305 assert_eq!(result, "0.5");
1306
1307 let pixel_val = 100.0;
1309 let result = format!("{}px", pixel_val);
1310 assert_eq!(result, "100px");
1311
1312 let percent_val = 50.0;
1314 let result = format!("{}%", percent_val);
1315 assert_eq!(result, "50%");
1316
1317 let degree_val = 45.0;
1319 let result = format!("{}deg", degree_val);
1320 assert_eq!(result, "45deg");
1321
1322 let color_val = "red";
1324 let result = color_val.to_string();
1325 assert_eq!(result, "red");
1326
1327 let string_val = "block";
1329 let result = string_val.to_string();
1330 assert_eq!(result, "block");
1331 }
1332
1333 #[test]
1334 fn test_transform_to_css_logic() {
1335 let transform = Transform {
1337 x: Some(10.0),
1338 y: Some(20.0),
1339 z: Some(30.0),
1340 rotate_x: Some(45.0),
1341 rotate_y: Some(90.0),
1342 rotate_z: Some(180.0),
1343 scale: Some(1.5),
1344 scale_x: Some(2.0),
1345 scale_y: Some(0.5),
1346 skew_x: Some(15.0),
1347 skew_y: Some(30.0),
1348 };
1349
1350 let mut parts = Vec::new();
1351
1352 if let Some(x) = transform.x {
1353 parts.push(format!("translateX({}px)", x));
1354 }
1355 if let Some(y) = transform.y {
1356 parts.push(format!("translateY({}px)", y));
1357 }
1358 if let Some(z) = transform.z {
1359 parts.push(format!("translateZ({}px)", z));
1360 }
1361 if let Some(rotate_x) = transform.rotate_x {
1362 parts.push(format!("rotateX({}deg)", rotate_x));
1363 }
1364 if let Some(rotate_y) = transform.rotate_y {
1365 parts.push(format!("rotateY({}deg)", rotate_y));
1366 }
1367 if let Some(rotate_z) = transform.rotate_z {
1368 parts.push(format!("rotateZ({}deg)", rotate_z));
1369 }
1370 if let Some(scale) = transform.scale {
1371 parts.push(format!("scale({})", scale));
1372 }
1373 if let Some(scale_x) = transform.scale_x {
1374 parts.push(format!("scaleX({})", scale_x));
1375 }
1376 if let Some(scale_y) = transform.scale_y {
1377 parts.push(format!("scaleY({})", scale_y));
1378 }
1379 if let Some(skew_x) = transform.skew_x {
1380 parts.push(format!("skewX({}deg)", skew_x));
1381 }
1382 if let Some(skew_y) = transform.skew_y {
1383 parts.push(format!("skewY({}deg)", skew_y));
1384 }
1385
1386 let css = parts.join(" ");
1387
1388 assert!(css.contains("translateX(10px)"));
1389 assert!(css.contains("translateY(20px)"));
1390 assert!(css.contains("translateZ(30px)"));
1391 assert!(css.contains("rotateX(45deg)"));
1392 assert!(css.contains("rotateY(90deg)"));
1393 assert!(css.contains("rotateZ(180deg)"));
1394 assert!(css.contains("scale(1.5)"));
1395 assert!(css.contains("scaleX(2)"));
1396 assert!(css.contains("scaleY(0.5)"));
1397 assert!(css.contains("skewX(15deg)"));
1398 assert!(css.contains("skewY(30deg)"));
1399 }
1400
1401 #[test]
1402 fn test_animation_completion_logic() {
1403 let is_complete = |state: &PlaybackState| -> bool {
1405 matches!(state, PlaybackState::Completed | PlaybackState::Cancelled)
1406 };
1407
1408 assert!(!is_complete(&PlaybackState::Running));
1410
1411 assert!(is_complete(&PlaybackState::Completed));
1413
1414 assert!(is_complete(&PlaybackState::Cancelled));
1416
1417 assert!(!is_complete(&PlaybackState::Paused));
1419 }
1420
1421 #[test]
1422 fn test_feature_detector_creation() {
1423 let detector = FeatureDetector::new();
1424 assert!(detector.waapi_available.is_none()); }
1426
1427 #[test]
1428 fn test_waapi_property_detection() {
1429 let is_waapi_property = |property: &str| -> bool {
1431 matches!(
1432 property,
1433 "opacity"
1434 | "transform"
1435 | "left"
1436 | "top"
1437 | "width"
1438 | "height"
1439 | "background-color"
1440 | "color"
1441 | "border-radius"
1442 | "scale"
1443 | "rotate"
1444 | "translateX"
1445 | "translateY"
1446 | "translateZ"
1447 | "rotateX"
1448 | "rotateY"
1449 | "rotateZ"
1450 )
1451 };
1452
1453 assert!(is_waapi_property("opacity"));
1455 assert!(is_waapi_property("transform"));
1456 assert!(is_waapi_property("left"));
1457 assert!(is_waapi_property("top"));
1458 assert!(is_waapi_property("width"));
1459 assert!(is_waapi_property("height"));
1460 assert!(is_waapi_property("background-color"));
1461 assert!(is_waapi_property("color"));
1462 assert!(is_waapi_property("border-radius"));
1463 assert!(is_waapi_property("scale"));
1464 assert!(is_waapi_property("rotate"));
1465 assert!(is_waapi_property("translateX"));
1466 assert!(is_waapi_property("translateY"));
1467 assert!(is_waapi_property("translateZ"));
1468 assert!(is_waapi_property("rotateX"));
1469 assert!(is_waapi_property("rotateY"));
1470 assert!(is_waapi_property("rotateZ"));
1471
1472 assert!(!is_waapi_property("custom-property"));
1474 assert!(!is_waapi_property("unknown"));
1475 assert!(!is_waapi_property(""));
1476 }
1477
1478 #[test]
1479 fn test_engine_choice_variants() {
1480 let waapi = EngineChoice::Waapi;
1481 let raf = EngineChoice::Raf;
1482
1483 match waapi {
1484 EngineChoice::Waapi => {}
1485 _ => panic!("Expected WAAPI variant"),
1486 }
1487
1488 match raf {
1489 EngineChoice::Raf => {}
1490 _ => panic!("Expected RAF variant"),
1491 }
1492 }
1493}
1494
1495mod rand {
1497 pub fn random<T: From<u64>>() -> T {
1498 use std::collections::hash_map::DefaultHasher;
1500 use std::hash::{Hash, Hasher};
1501
1502 let mut hasher = DefaultHasher::new();
1503 #[cfg(feature = "web-sys")]
1504 web_sys::js_sys::Date::now().to_bits().hash(&mut hasher);
1505 #[cfg(not(feature = "web-sys"))]
1506 std::time::SystemTime::now()
1507 .duration_since(std::time::UNIX_EPOCH)
1508 .unwrap()
1509 .as_nanos()
1510 .hash(&mut hasher);
1511 T::from(hasher.finish())
1512 }
1513}