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}