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}