dioxus_motion/
lib.rs

1//! Dioxus Motion - Animation library for Dioxus
2//!
3//! Provides smooth animations for web and native applications built with Dioxus.
4//! Supports both spring physics and tween-based animations with configurable parameters.
5//!
6//! # Features
7//! - **Simplified Animatable trait** - Uses standard Rust operators (`+`, `-`, `*`) for math operations
8//! - **High-performance optimizations** - Automatic memory pooling, state machine dispatch, and resource management
9//! - Spring physics animations with optimized integration
10//! - Tween animations with custom easing
11//! - Color interpolation
12//! - Transform animations
13//! - Configurable animation loops
14//! - Animation sequences with atomic step management
15//! - Single default epsilon (0.01) for consistent animation completion
16//! - Automatic resource pool management for maximum performance
17//!
18//! # Example
19//! ```rust,no_run
20//! use dioxus_motion::prelude::*;
21//!
22//! // Optional: Configure resource pools for optimal performance (recommended for production)
23//! resource_pools::init_high_performance();
24//!
25//! let mut value = use_motion(0.0f32);
26//!
27//! // Basic animation - automatically uses all optimizations
28//! value.animate_to(100.0, AnimationConfig::new(AnimationMode::Spring(Spring::default())));
29//!
30//! // Animation with custom epsilon for fine-tuned performance (optional)
31//! value.animate_to(
32//!     100.0,
33//!     AnimationConfig::new(AnimationMode::Spring(Spring::default()))
34//!         .with_epsilon(0.001) // Tighter threshold for high-precision animations
35//! );
36//!
37//! // Check if animation is running
38//! if value.is_running() {
39//!     println!("Animation is active with current value: {}", value.get_value());
40//! }
41//! ```
42//!
43//! # Creating Custom Animatable Types
44//!
45//! The simplified `Animatable` trait requires only two methods and leverages standard Rust traits:
46//!
47//! ```rust
48//! use dioxus_motion::prelude::*;
49//! use dioxus_motion::animations::core::Animatable;
50//!
51//! #[derive(Debug, Copy, Clone, PartialEq, Default)]
52//! struct Point { x: f32, y: f32 }
53//!
54//! // Implement standard math operators
55//! impl std::ops::Add for Point {
56//!     type Output = Self;
57//!     fn add(self, other: Self) -> Self {
58//!         Self { x: self.x + other.x, y: self.y + other.y }
59//!     }
60//! }
61//!
62//! impl std::ops::Sub for Point {
63//!     type Output = Self;
64//!     fn sub(self, other: Self) -> Self {
65//!         Self { x: self.x - other.x, y: self.y - other.y }
66//!     }
67//! }
68//!
69//! impl std::ops::Mul<f32> for Point {
70//!     type Output = Self;
71//!     fn mul(self, factor: f32) -> Self {
72//!         Self { x: self.x * factor, y: self.y * factor }
73//!     }
74//! }
75//!
76//! // Implement Animatable with just two methods
77//! impl Animatable for Point {
78//!     fn interpolate(&self, target: &Self, t: f32) -> Self {
79//!         *self + (*target - *self) * t
80//!     }
81//!     
82//!     fn magnitude(&self) -> f32 {
83//!         (self.x * self.x + self.y * self.y).sqrt()
84//!     }
85//! }
86//! ```
87
88#![deny(clippy::unwrap_used)]
89#![deny(clippy::panic)]
90#![deny(unused_variables)]
91#![deny(unused_must_use)]
92#![deny(unsafe_code)] // Prevent unsafe blocks
93#![deny(clippy::unwrap_in_result)] // No unwrap() on Result
94// #![deny(clippy::indexing_slicing)] // Prevent unchecked indexing
95#![deny(rustdoc::broken_intra_doc_links)] // Check doc links
96// #![deny(clippy::arithmetic_side_effects)] // Check for integer overflow
97#![deny(clippy::modulo_arithmetic)] // Check modulo operations
98#![deny(clippy::option_if_let_else)] // Prefer map/and_then
99
100use animations::core::Animatable;
101use dioxus::prelude::*;
102pub use instant::Duration;
103
104pub mod animations;
105pub mod keyframes;
106pub mod manager;
107pub mod motion;
108pub mod pool;
109pub mod sequence;
110#[cfg(feature = "transitions")]
111pub mod transitions;
112
113#[cfg(feature = "transitions")]
114pub use dioxus_motion_transitions_macro;
115
116pub use animations::platform::{MotionTime, TimeProvider};
117
118pub use keyframes::{Keyframe, KeyframeAnimation};
119pub use manager::AnimationManager;
120
121use motion::Motion;
122
123// Re-exports
124pub mod prelude {
125    pub use crate::animations::core::{AnimationConfig, AnimationMode, LoopMode};
126    pub use crate::animations::{
127        colors::Color, spring::Spring, transform::Transform, tween::Tween,
128    };
129    #[cfg(feature = "transitions")]
130    pub use crate::dioxus_motion_transitions_macro::MotionTransitions;
131    pub use crate::sequence::AnimationSequence;
132    #[cfg(feature = "transitions")]
133    pub use crate::transitions::config::TransitionVariant;
134    #[cfg(feature = "transitions")]
135    pub use crate::transitions::page_transitions::TransitionVariantResolver;
136    #[cfg(feature = "transitions")]
137    pub use crate::transitions::page_transitions::{AnimatableRoute, AnimatedOutlet};
138    pub use crate::{AnimationManager, Duration, Time, TimeProvider, use_motion};
139
140    // Performance optimization exports
141    pub use crate::motion::MotionOptimizationStats;
142    pub use crate::pool::resource_pools;
143    pub use crate::pool::{PoolConfig, PoolStats};
144}
145
146pub type Time = MotionTime;
147
148/// Helper function to calculate the appropriate delay for the animation loop
149fn calculate_delay(dt: f32, running_frames: u32) -> Duration {
150    #[cfg(feature = "web")]
151    {
152        // running_frames is not used in web builds but kept for API consistency
153        let _ = running_frames;
154        match dt {
155            x if x < 0.008 => Duration::from_millis(8),  // ~120fps
156            x if x < 0.016 => Duration::from_millis(16), // ~60fps
157            _ => Duration::from_millis(32),              // ~30fps
158        }
159    }
160    #[cfg(not(feature = "web"))]
161    {
162        if running_frames <= 200 {
163            Duration::from_micros(8333) // ~120fps
164        } else {
165            match dt {
166                x if x < 0.005 => Duration::from_millis(8),  // ~120fps
167                x if x < 0.011 => Duration::from_millis(16), // ~60fps
168                _ => Duration::from_millis(33),              // ~30fps
169            }
170        }
171    }
172}
173
174/// Creates an animation manager that continuously updates a motion state.
175///
176/// This function initializes a motion state with the provided initial value and spawns an asynchronous loop
177/// that updates the animation state based on the elapsed time between frames. When the animation is running,
178/// it updates the state using the calculated time delta and dynamically adjusts the update interval to optimize CPU usage;
179/// when the animation is inactive, it waits longer before polling again.
180///
181/// # Example
182///
183/// ```no_run
184/// use dioxus_motion::prelude::*;
185/// use dioxus::prelude::*;
186///
187/// fn app() -> Element {
188///     let mut value = use_motion(0.0f32);
189///
190///     // Animate to 100 with spring physics
191///     value.animate_to(
192///         100.0,
193///         AnimationConfig::new(AnimationMode::Spring(Spring::default()))
194///     );
195///
196///     rsx! {
197///         div {
198///             style: "transform: translateY({value.get_value()}px)",
199///             "Animated content"
200///         }
201///     }
202/// }
203/// ```
204pub fn use_motion<T: Animatable + Send + 'static>(initial: T) -> impl AnimationManager<T> {
205    let mut state = use_signal(|| Motion::new(initial));
206
207    #[cfg(feature = "web")]
208    let idle_poll_rate = Duration::from_millis(100);
209
210    #[cfg(not(feature = "web"))]
211    let idle_poll_rate = Duration::from_millis(33);
212
213    use_effect(move || {
214        // This executes after rendering is complete
215        spawn(async move {
216            let mut last_frame = Time::now();
217            let mut running_frames = 0u32;
218
219            loop {
220                let now = Time::now();
221                let dt = (now.duration_since(last_frame).as_secs_f32()).min(0.1);
222                last_frame = now;
223
224                // Only check if running first, then write to the signal
225                if (*state.peek()).is_running() {
226                    running_frames += 1;
227                    let prev_value = (*state.peek()).get_value();
228                    let updated = (*state.write()).update(dt);
229                    let new_value = (*state.peek()).get_value();
230                    let epsilon = (*state.peek()).get_epsilon();
231                    // Only trigger a re-render if the value changed significantly
232                    if (new_value - prev_value).magnitude() > epsilon || updated {
233                        // State has changed enough, continue
234                    } else {
235                        // Skip this frame's update to avoid unnecessary re-render
236                        let delay = calculate_delay(dt, running_frames);
237                        Time::delay(delay).await;
238                        continue;
239                    }
240
241                    let delay = calculate_delay(dt, running_frames);
242                    Time::delay(delay).await;
243                } else {
244                    running_frames = 0;
245                    Time::delay(idle_poll_rate).await;
246                }
247            }
248        });
249    });
250
251    state
252}