Skip to main content

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