leptos_motion_core/
engine.rs

1//! Animation engine traits and implementations
2
3#[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
16/// Core animation engine trait
17pub trait AnimationEngine {
18    /// Check if this engine is available in current environment
19    fn is_available(&self) -> bool;
20
21    /// Start an animation and return a handle
22    fn animate(&mut self, animation: &AnimationConfig) -> Result<AnimationHandle>;
23
24    /// Stop an animation by handle
25    fn stop(&mut self, handle: AnimationHandle) -> Result<()>;
26
27    /// Pause an animation
28    fn pause(&mut self, handle: AnimationHandle) -> Result<()>;
29
30    /// Resume a paused animation
31    fn resume(&mut self, handle: AnimationHandle) -> Result<()>;
32
33    /// Update all animations (for RAF-based engines)
34    fn tick(&mut self, timestamp: f64) -> Result<()>;
35
36    /// Get current playback state
37    fn get_state(&self, handle: AnimationHandle) -> Result<PlaybackState>;
38
39    /// Check if an animation is running
40    fn is_running(&self, handle: AnimationHandle) -> bool;
41
42    /// Get performance metrics
43    #[cfg(feature = "performance-metrics")]
44    fn get_performance_metrics(&self) -> Option<crate::performance::PerformanceReport>;
45
46    /// Get performance metrics (no-op when feature disabled)
47    #[cfg(not(feature = "performance-metrics"))]
48    fn get_performance_metrics(&self) -> Option<()>;
49}
50
51/// Animation configuration
52#[derive(Clone)]
53pub struct AnimationConfig {
54    /// Target element
55    #[cfg(feature = "web-sys")]
56    pub element: web_sys::Element,
57    /// Animation properties
58    pub from: AnimationTarget,
59    /// Target values
60    pub to: AnimationTarget,
61    /// Transition configuration
62    pub transition: Transition,
63    /// Completion callback ID (for now, simplified)
64    pub on_complete_id: Option<u64>,
65    /// Update callback ID (for now, simplified)
66    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/// Animation playback state
97#[derive(Debug, Clone, PartialEq)]
98pub enum PlaybackState {
99    /// Animation is running
100    Running,
101    /// Animation is paused
102    Paused,
103    /// Animation has completed
104    Completed,
105    /// Animation was cancelled
106    Cancelled,
107}
108
109/// Optimized hybrid animation engine with performance monitoring
110pub 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    /// Create a new optimized hybrid engine instance
129    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), // Max 50 GPU layers
144            #[cfg(feature = "performance-metrics")]
145            animation_pool: AnimationPool::new(100),
146            current_handle: 0,
147            frame_count: 0,
148        }
149    }
150
151    /// Start performance monitoring
152    #[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    /// End performance monitoring
160    #[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; // Rough estimate
164            let gpu_layers = self.gpu_manager.layer_count();
165            let _report = monitor.generate_report(animations_updated, memory_usage, gpu_layers);
166        }
167    }
168
169    /// Start performance monitoring (no-op when feature disabled)
170    #[cfg(not(feature = "performance-metrics"))]
171    pub fn start_performance_monitoring(&mut self) {
172        // No-op when performance metrics are disabled
173    }
174
175    /// End performance monitoring (no-op when feature disabled)
176    #[cfg(not(feature = "performance-metrics"))]
177    pub fn end_performance_monitoring(&mut self, _animations_updated: usize) {
178        // No-op when performance metrics are disabled
179    }
180
181    /// Get performance report
182    #[cfg(feature = "performance-metrics")]
183    pub fn get_performance_report(&self) -> Option<crate::performance::PerformanceReport> {
184        // We can't generate a report here since we need mutable access
185        // This is a limitation of the current design
186        None
187    }
188
189    /// Get performance report (no-op when feature disabled)
190    #[cfg(not(feature = "performance-metrics"))]
191    pub fn get_performance_report(&self) -> Option<()> {
192        None
193    }
194
195    /// Optimize element for GPU acceleration
196    #[cfg(feature = "web-sys")]
197    pub fn optimize_for_gpu(&mut self, _element: &web_sys::Element) -> bool {
198        // For now, skip GPU optimization to avoid compilation issues
199        // In a real implementation, this would check element attributes
200        false
201    }
202
203    /// Select the appropriate engine for an animation
204    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    /// Get next animation handle
214    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        // Start performance monitoring
233        self.start_performance_monitoring();
234
235        // Optimize element for GPU if possible
236        #[cfg(feature = "web-sys")]
237        self.optimize_for_gpu(&config.element);
238
239        // Select engine based on performance and capabilities
240        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                // Create RAF animation directly
258                #[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; // Fallback for minimal builds
262                let animation = RafAnimation::new(config.clone(), start_time);
263                self.raf_engine.animations.insert(handle, animation);
264
265                // Start RAF loop if not already running
266                if self.raf_engine.raf_handle.is_none() {
267                    self.raf_engine.start_raf_loop()?;
268                }
269
270                Ok(())
271            }
272        };
273
274        // End performance monitoring
275        self.end_performance_monitoring(1);
276
277        result?;
278        Ok(handle)
279    }
280
281    fn stop(&mut self, handle: AnimationHandle) -> Result<()> {
282        // Try WAAPI first
283        #[cfg(feature = "web-sys")]
284        if let Ok(()) = self.waapi_engine.stop(handle) {
285            return Ok(());
286        }
287
288        // Try RAF
289        if let Ok(()) = self.raf_engine.stop(handle) {
290            // Return animation to pool
291            #[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        // Try WAAPI first
301        #[cfg(feature = "web-sys")]
302        if let Ok(()) = self.waapi_engine.pause(handle) {
303            return Ok(());
304        }
305
306        // Try RAF
307        self.raf_engine.pause(handle)
308    }
309
310    fn resume(&mut self, handle: AnimationHandle) -> Result<()> {
311        // Try WAAPI first
312        #[cfg(feature = "web-sys")]
313        if let Ok(()) = self.waapi_engine.resume(handle) {
314            return Ok(());
315        }
316
317        // Try RAF
318        self.raf_engine.resume(handle)
319    }
320
321    fn tick(&mut self, timestamp: f64) -> Result<()> {
322        self.frame_count += 1;
323
324        // Start performance monitoring
325        self.start_performance_monitoring();
326
327        // Update RAF engine first
328        let raf_result = self.raf_engine.tick(timestamp);
329
330        // End performance monitoring
331        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        // Try WAAPI first
338        #[cfg(feature = "web-sys")]
339        if let Ok(state) = self.waapi_engine.get_state(handle) {
340            return Ok(state);
341        }
342
343        // Try RAF
344        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/// Web Animations API engine
367#[cfg(feature = "web-sys")]
368pub struct WaapiEngine {
369    animations: HashMap<AnimationHandle, web_sys::Animation>,
370}
371
372#[cfg(feature = "web-sys")]
373impl WaapiEngine {
374    /// Create a new WAAPI engine
375    pub fn new() -> Self {
376        Self {
377            animations: HashMap::new(),
378        }
379    }
380
381    /// Animate with a specific handle
382    pub fn animate_with_handle(
383        &mut self,
384        handle: AnimationHandle,
385        _config: AnimationConfig,
386    ) -> Result<()> {
387        // Implementation would create Web Animation API animation
388        // This is a simplified version
389        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        // Create Web Animation would go here
407        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        // WAAPI doesn't need manual ticking
441        Ok(())
442    }
443
444    fn get_state(&self, handle: AnimationHandle) -> Result<PlaybackState> {
445        if let Some(_animation) = self.animations.get(&handle) {
446            // Would check animation.playState
447            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 // WAAPI doesn't provide performance metrics
460    }
461
462    #[cfg(not(feature = "performance-metrics"))]
463    fn get_performance_metrics(&self) -> Option<()> {
464        None // WAAPI doesn't provide performance metrics
465    }
466}
467
468/// RequestAnimationFrame-based engine
469pub 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    /// Create a new RAF engine
478    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    /// Animate with a specific handle
493    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        // Start RAF loop if not already running
506        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        // This would set up the RAF callback
515        // Simplified for now
516        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        // Clean up completed animations
583        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 // RAF engine doesn't provide performance metrics
608    }
609
610    #[cfg(not(feature = "performance-metrics"))]
611    fn get_performance_metrics(&self) -> Option<()> {
612        None // RAF engine doesn't provide performance metrics
613    }
614}
615
616/// RAF animation state
617struct 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; // Convert to ms
635
636        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        // Apply animation values to element
644        self.apply_values(eased_progress);
645
646        if let Some(_callback_id) = self.config.on_update_id {
647            // Call callback by ID (simplified for now)
648            // In a real implementation, this would look up the callback by ID
649        }
650    }
651
652    fn apply_values(&self, progress: f64) {
653        // Interpolate between from and to values based on progress
654        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                // Interpolate between from and to values
659                let interpolated = self.interpolate_values(from_value, to_value, progress);
660                current_values.insert(property.clone(), interpolated);
661            }
662        }
663
664        // Apply interpolated values to the element
665        self.apply_to_element(&current_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        // Apply values directly to element properties
745        for (property, value) in values {
746            let css_value = self.animation_value_to_css(property, value);
747            // For now, we'll use a simplified approach
748            // In a real implementation, this would update the element's style
749            let _ = css_value; // Suppress unused variable warning
750        }
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/// Feature detection for animation capabilities
815#[derive(Default)]
816pub struct FeatureDetector {
817    waapi_available: Option<bool>,
818}
819
820impl FeatureDetector {
821    /// Create a new feature detector
822    pub fn new() -> Self {
823        Self::default()
824    }
825
826    /// Check if WAAPI is supported
827    pub fn supports_waapi(&self) -> bool {
828        // Cache the result since it won't change during runtime
829        if let Some(available) = self.waapi_available {
830            return available;
831        }
832
833        // Check for Web Animations API support
834        #[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    /// Check if WAAPI can be used for a specific animation
847    pub fn can_use_waapi_for(&self, config: &AnimationConfig) -> bool {
848        // Check if the specific animation properties are supported by WAAPI
849        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        // List of properties that are well-supported by WAAPI
859        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 that our core types can be created and manipulated
929    #[test]
930    fn test_core_types() {
931        // Test AnimationValue creation
932        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        // Test that values can be cloned
940        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        // Test Transform creation
948        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        // Test that transform can be cloned
963        let _cloned_transform = transform.clone();
964
965        // Test that we can access transform fields
966        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 interpolation logic without web dependencies
980    #[test]
981    fn test_interpolation_math() {
982        // Test basic interpolation formula
983        let from = 0.0;
984        let to = 100.0;
985
986        // Test at 0% progress
987        let result = from + (to - from) * 0.0;
988        assert_eq!(result, 0.0);
989
990        // Test at 50% progress
991        let result = from + (to - from) * 0.5;
992        assert_eq!(result, 50.0);
993
994        // Test at 100% progress
995        let result = from + (to - from) * 1.0;
996        assert_eq!(result, 100.0);
997
998        // Test at 25% progress
999        let result = from + (to - from) * 0.25;
1000        assert_eq!(result, 25.0);
1001
1002        // Test negative values
1003        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 option interpolation logic
1010    #[test]
1011    fn test_option_interpolation() {
1012        // Test both values present
1013        let from = Some(0.0);
1014        let to = Some(100.0);
1015
1016        // This would be the logic from interpolate_option method
1017        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        // Test at 50% progress
1029        let result = interpolate(from, to, 0.5);
1030        assert_eq!(result, Some(50.0));
1031
1032        // Test from value missing
1033        let result = interpolate(None, to, 0.5);
1034        assert_eq!(result, Some(100.0));
1035
1036        // Test to value missing
1037        let result = interpolate(from, None, 0.5);
1038        assert_eq!(result, Some(0.0));
1039
1040        // Test both values missing
1041        let result = interpolate(None, None, 0.5);
1042        assert_eq!(result, None);
1043    }
1044
1045    // Test basic functionality without web dependencies
1046    #[test]
1047    fn test_basic_functionality() {
1048        // Test that we can create basic structures
1049        let _detector = FeatureDetector::new();
1050
1051        // Test playback states
1052        let states = vec![
1053            PlaybackState::Running,
1054            PlaybackState::Paused,
1055            PlaybackState::Completed,
1056            PlaybackState::Cancelled,
1057        ];
1058
1059        for state in states {
1060            // Just ensure the states can be cloned and compared
1061            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        // Test callback IDs without requiring DOM elements
1080        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        // Test engine availability logic without creating engines
1104        let waapi_available = false; // Assume not available in test environment
1105        let raf_available = true; // RAF should always be available
1106
1107        assert!(raf_available || waapi_available); // At least one should be available
1108    }
1109
1110    #[test]
1111    fn test_handle_generation_logic() {
1112        // Test handle generation logic without full engine
1113        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        // Test GPU optimization logic without DOM dependencies
1136        let gpu_enabled = false; // For now, always false
1137        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()); // WAAPI doesn't need manual ticking
1160    }
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()); // WAAPI doesn't provide metrics
1168    }
1169
1170    // Test basic RAF engine functionality without web dependencies
1171    #[test]
1172    fn test_raf_engine_basics() {
1173        // Test basic RAF engine structure without requiring DOM
1174        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        // Test basic RAF animation concepts without full DOM setup
1184        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        // Test animation progress calculation without DOM dependencies
1194        let start_time = 0.0;
1195        let duration_ms = 1000.0; // 1 second
1196
1197        // Test at different timestamps
1198        let timestamp1 = 500.0; // 50% progress
1199        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; // 150% (should clamp to 100%)
1204        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        // Test interpolation logic without requiring DOM or RafAnimation creation
1212
1213        // Number interpolation
1214        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        // Pixel interpolation (same math, different units)
1221        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        // Percentage interpolation
1227        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        // Degrees interpolation
1233        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        // Color interpolation (step function logic)
1239        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        // Test transform interpolation logic without requiring DOM or RafAnimation
1250        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        // Test individual field interpolation
1269        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        // Test CSS value conversion logic without requiring RafAnimation
1301
1302        // Test number
1303        let number_val = 0.5;
1304        let result = number_val.to_string();
1305        assert_eq!(result, "0.5");
1306
1307        // Test pixels
1308        let pixel_val = 100.0;
1309        let result = format!("{}px", pixel_val);
1310        assert_eq!(result, "100px");
1311
1312        // Test percentage
1313        let percent_val = 50.0;
1314        let result = format!("{}%", percent_val);
1315        assert_eq!(result, "50%");
1316
1317        // Test degrees
1318        let degree_val = 45.0;
1319        let result = format!("{}deg", degree_val);
1320        assert_eq!(result, "45deg");
1321
1322        // Test color
1323        let color_val = "red";
1324        let result = color_val.to_string();
1325        assert_eq!(result, "red");
1326
1327        // Test string
1328        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        // Test transform to CSS conversion logic without requiring RafAnimation
1336        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        // Test animation completion logic without requiring RafAnimation
1404        let is_complete = |state: &PlaybackState| -> bool {
1405            matches!(state, PlaybackState::Completed | PlaybackState::Cancelled)
1406        };
1407
1408        // Initially running, not complete
1409        assert!(!is_complete(&PlaybackState::Running));
1410
1411        // Complete the animation
1412        assert!(is_complete(&PlaybackState::Completed));
1413
1414        // Cancel the animation
1415        assert!(is_complete(&PlaybackState::Cancelled));
1416
1417        // Paused is not complete
1418        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()); // Not yet determined
1425    }
1426
1427    #[test]
1428    fn test_waapi_property_detection() {
1429        // Test WAAPI property detection logic
1430        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        // Test supported properties
1454        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        // Test unsupported properties
1473        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
1495// Temporary random function for handle generation
1496mod rand {
1497    pub fn random<T: From<u64>>() -> T {
1498        // Simple PRNG for demo purposes
1499        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}