waterui_core/animation.rs
1//! # `WaterUI` Animation System
2//!
3//! A reactive animation system that seamlessly integrates with `WaterUI`'s reactive state management.
4//!
5//! ## Overview
6//!
7//! The `WaterUI` animation system leverages the reactive framework to create smooth, declarative
8//! animations that automatically run when reactive values change. By attaching animation metadata
9//! to reactive values through convenient extension methods, the system can intelligently
10//! determine how to animate between different states without requiring explicit animation code.
11//!
12//! ```text
13//! ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
14//! │ Reactive Values │─────>│ Change Propagation│─────>│ Animation System │
15//! │ (Binding/Compute)│ │ (With Animations) │ │ (Renderer) │
16//! └───────────────────┘ └───────────────────┘ └───────────────────┘
17//! ```
18//!
19//! ## Core Concepts
20//!
21//! ### Animation Extension Methods
22//!
23//! `WaterUI` provides convenient extension methods on all reactive types to easily attach
24//! animation configurations:
25//!
26//! ```rust
27//! use waterui_core::{animation::Animation, AnimationExt, SignalExt};
28//! use nami::binding;
29//! use core::time::Duration;
30//!
31//! let opacity: nami::Binding<f32> = binding(1.0);
32//!
33//! // Use the .animated() method to apply default animation
34//! let _animated_opacity = opacity.clone().animated();
35//!
36//! // Or specify a specific animation type
37//! let faded: nami::Binding<f32> = binding(0.0);
38//! let _custom_animated =
39//! faded.with_animation(Animation::ease_in_out(Duration::from_millis(300)));
40//! ```
41//!
42//! The system supports various animation types:
43//!
44//! - **`Linear`**: Constant velocity from start to finish
45//! - **`EaseIn`**: Starts slow and accelerates
46//! - **`EaseOut`**: Starts fast and decelerates
47//! - **`EaseInOut`**: Combines ease-in and ease-out for natural movement
48//! - **`Spring`**: Physics-based animation with configurable stiffness and damping
49//!
50//! ### Integration with UI Components
51//!
52//! UI components automatically respect animation metadata when rendering:
53//!
54//! ```rust
55//! use waterui_core::{animation::Animation, AnimationExt, SignalExt};
56//! use nami::binding;
57//! use core::time::Duration;
58//!
59//! let scale: nami::Binding<f32> = binding(1.0);
60//!
61//! // Three different ways to animate properties:
62//!
63//! // 1. Default animation (uses system defaults)
64//! let _view1_scale = scale.clone().animated();
65//!
66//! // 2. Custom animation using convenience methods
67//! let expanded: nami::Binding<f32> = binding(2.0);
68//! let _view2_scale =
69//! expanded.with_animation(Animation::ease_in_out(Duration::from_millis(300)));
70//!
71//! // 3. Spring animation using the convenience method
72//! let bouncing: nami::Binding<f32> = binding(0.5);
73//! let _view3_scale = bouncing.with_animation(Animation::spring(100.0, 10.0));
74//! ```
75//!
76//! ## Animation Pipeline
77//!
78//! 1. **Reactive Setup**: Reactive values are wrapped with animation metadata using extension methods
79//! 2. **State Change**: When the underlying value changes, the animation information is preserved
80//! 3. **Propagation**: The change and animation details are propagated through the reactive system
81//! 4. **Value Interpolation**: The renderer calculates intermediate values based on animation type
82//! 5. **Rendering**: The UI is continuously updated with interpolated values until animation completes
83//!
84//! ## Advanced Features
85//!
86//! ### Animation Choreography
87//!
88//! Complex animations can be created by coordinating multiple animated values:
89//!
90//! ```rust
91//! use waterui_core::{animation::Animation, AnimationExt, SignalExt};
92//! use nami::binding;
93//! use core::time::Duration;
94//!
95//! let opacity: nami::Binding<f32> = binding(0.0);
96//! let position: nami::Binding<(f32, f32)> = binding((0.0, 0.0));
97//!
98//! // Create a choreographed animation sequence
99//! let animated_opacity =
100//! opacity.with_animation(Animation::ease_in_out(Duration::from_millis(300)));
101//!
102//! // Position animates with a spring physics model
103//! let animated_position = position.with_animation(Animation::spring(100.0, 10.0));
104//!
105//! // Both animated values can be used in views
106//! // The UI framework will automatically handle the animation timing
107//! drop((animated_opacity, animated_position));
108//! ```
109//!
110//! ### Composition with Other Reactive Features
111//!
112//! Animation metadata seamlessly composes with other reactive features:
113//!
114//! ```rust
115//! use waterui_core::{animation::Animation, AnimationExt, SignalExt};
116//! use nami::binding;
117//! use core::time::Duration;
118//!
119//! let count: nami::Binding<i32> = binding(0i32);
120//! let value1: nami::Binding<i32> = binding(1i32);
121//! let value2: nami::Binding<i32> = binding(2i32);
122//!
123//! // Combine mapping and animation
124//! let opacity = count
125//! .map(|n: i32| if n > 5 { 1.0 } else { 0.5 })
126//! .animated(); // Apply animation to the mapped result
127//!
128//! // Combine multiple reactive values with animation
129//! let combined = value1
130//! .zip(value2)
131//! .map(|(a, b)| a + b)
132//! .with_animation(Animation::ease_in_out(Duration::from_millis(250)));
133//!
134//! drop((opacity, combined)); // Prevent unused variable warnings
135//! ```
136//!
137
138use core::time::Duration;
139
140/// An enumeration representing different types of animations
141///
142/// This enum provides various animation types for UI elements or graphics:
143/// - `Linear`: Constant speed from start to finish
144/// - `EaseIn`: Starts slow and accelerates
145/// - `EaseOut`: Starts fast and decelerates
146/// - `EaseInOut`: Starts and ends slowly with acceleration in the middle
147/// - `Spring`: Physics-based movement with configurable stiffness and damping
148///
149/// Each animation type (except Spring) takes a Duration parameter that specifies
150/// how long the animation should take to complete.
151#[derive(Debug, Default, Clone, PartialEq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub enum Animation {
154 /// Default animation behavior (uses system defaults)
155 #[default]
156 Default,
157 /// Linear animation with constant velocity
158 Linear(Duration),
159 /// Ease-in animation that starts slow and accelerates
160 EaseIn(Duration),
161 /// Ease-out animation that starts fast and decelerates
162 EaseOut(Duration),
163 /// Ease-in-out animation that starts and ends slowly with acceleration in the middle
164 EaseInOut(Duration),
165 /// Spring animation with physics-based movement
166 Spring {
167 /// Stiffness of the spring (higher values create faster animations)
168 stiffness: f32,
169 /// Damping factor to control oscillation (higher values reduce bouncing)
170 damping: f32,
171 },
172}
173
174impl Animation {
175 /// Creates a new Linear animation with the specified duration
176 ///
177 /// This is an ergonomic constructor that accepts any type that can be converted
178 /// into a Duration (such as u64 milliseconds, etc.)
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use waterui_core::animation::Animation;
184 /// use core::time::Duration;
185 ///
186 /// let animation = Animation::linear(Duration::from_millis(300)); // 300ms
187 /// let animation = Animation::linear(Duration::from_secs(1)); // 1 second
188 /// ```
189 pub fn linear(duration: impl Into<Duration>) -> Self {
190 Self::Linear(duration.into())
191 }
192
193 /// Creates a new ease-in animation with the specified duration
194 ///
195 /// This is an ergonomic constructor that accepts any type that can be converted
196 /// into a Duration (such as u64 milliseconds, etc.)
197 ///
198 /// # Examples
199 ///
200 /// ```
201 /// use waterui_core::animation::Animation;
202 /// use core::time::Duration;
203 ///
204 /// let animation = Animation::ease_in(Duration::from_millis(300)); // 300ms
205 /// let animation = Animation::ease_in(Duration::from_secs(1)); // 1 second
206 /// ```
207 pub fn ease_in(duration: impl Into<Duration>) -> Self {
208 Self::EaseIn(duration.into())
209 }
210
211 /// Creates a new ease-out animation with the specified duration
212 ///
213 /// This is an ergonomic constructor that accepts any type that can be converted
214 /// into a Duration (such as u64 milliseconds, etc.)
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use waterui_core::animation::Animation;
220 /// use core::time::Duration;
221 ///
222 /// let animation = Animation::ease_out(Duration::from_millis(300)); // 300ms
223 /// let animation = Animation::ease_out(Duration::from_secs(1)); // 1 second
224 /// ```
225 pub fn ease_out(duration: impl Into<Duration>) -> Self {
226 Self::EaseOut(duration.into())
227 }
228
229 /// Creates a new ease-in-out animation with the specified duration
230 ///
231 /// This is an ergonomic constructor that accepts any type that can be converted
232 /// into a Duration (such as u64 milliseconds, etc.)
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use waterui_core::animation::Animation;
238 /// use core::time::Duration;
239 ///
240 /// let animation = Animation::ease_in_out(Duration::from_millis(300)); // 300ms
241 /// let animation = Animation::ease_in_out(Duration::from_secs(1)); // 1 second
242 /// ```
243 pub fn ease_in_out(duration: impl Into<Duration>) -> Self {
244 Self::EaseInOut(duration.into())
245 }
246
247 /// Creates a new Spring animation with the specified stiffness and damping
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// use waterui_core::animation::Animation;
253 ///
254 /// let animation = Animation::spring(100.0, 10.0);
255 /// ```
256 #[must_use]
257 pub const fn spring(stiffness: f32, damping: f32) -> Self {
258 Self::Spring { stiffness, damping }
259 }
260}
261
262use nami::signal::WithMetadata;
263
264/// Extension trait providing animation methods for reactive values
265pub trait AnimationExt: nami::SignalExt {
266 /// Apply default animation to this reactive value
267 ///
268 /// Uses a reasonable default animation (ease-in-out with 250ms duration)
269 fn animated(self) -> WithMetadata<Self, Animation>
270 where
271 Self: Sized,
272 {
273 self.with(Animation::ease_in_out(Duration::from_millis(250)))
274 }
275
276 /// Apply a specific animation to this reactive value
277 ///
278 /// # Arguments
279 ///
280 /// * `animation` - The animation to apply
281 fn with_animation(self, animation: Animation) -> WithMetadata<Self, Animation>
282 where
283 Self: Sized,
284 {
285 self.with(animation)
286 }
287}
288
289// Implement AnimationExt for all types that implement SignalExt
290impl<S: nami::SignalExt> AnimationExt for S {}