leptos_motion_core/
animation.rs

1//! Core animation types and builders
2
3use crate::{AnimationTarget, AnimationValue, Easing, Transition};
4use std::collections::HashMap;
5
6/// Animation builder for creating animations fluently
7#[derive(Debug, Clone)]
8pub struct AnimationBuilder {
9    initial: Option<AnimationTarget>,
10    animate: Option<AnimationTarget>,
11    exit: Option<AnimationTarget>,
12    transition: Option<Transition>,
13    variants: Option<Variants>,
14}
15
16impl AnimationBuilder {
17    /// Create a new animation builder
18    pub fn new() -> Self {
19        Self {
20            initial: None,
21            animate: None,
22            exit: None,
23            transition: None,
24            variants: None,
25        }
26    }
27
28    /// Set initial animation values
29    pub fn initial(mut self, initial: AnimationTarget) -> Self {
30        self.initial = Some(initial);
31        self
32    }
33
34    /// Set target animation values
35    pub fn animate(mut self, animate: AnimationTarget) -> Self {
36        self.animate = Some(animate);
37        self
38    }
39
40    /// Set exit animation values
41    pub fn exit(mut self, exit: AnimationTarget) -> Self {
42        self.exit = Some(exit);
43        self
44    }
45
46    /// Set transition configuration
47    pub fn transition(mut self, transition: Transition) -> Self {
48        self.transition = Some(transition);
49        self
50    }
51
52    /// Set variants
53    pub fn variants(mut self, variants: Variants) -> Self {
54        self.variants = Some(variants);
55        self
56    }
57
58    /// Build the final animation configuration
59    pub fn build(self) -> AnimationConfig {
60        AnimationConfig {
61            initial: self.initial.unwrap_or_default(),
62            animate: self.animate.unwrap_or_default(),
63            exit: self.exit.unwrap_or_default(),
64            transition: self.transition.unwrap_or_default(),
65            variants: self.variants,
66        }
67    }
68}
69
70/// Complete animation configuration
71#[derive(Debug, Clone, Default)]
72pub struct AnimationConfig {
73    /// Initial animation state
74    pub initial: AnimationTarget,
75    /// Target animation state
76    pub animate: AnimationTarget,
77    /// Exit animation state
78    pub exit: AnimationTarget,
79    /// Transition configuration
80    pub transition: Transition,
81    /// Animation variants
82    pub variants: Option<Variants>,
83}
84
85/// Animation variants for orchestrated animations
86#[derive(Debug, Clone)]
87pub struct Variants {
88    /// All variant definitions
89    pub variants: HashMap<String, AnimationTarget>,
90    /// Initial variant
91    pub initial: Option<String>,
92    /// Animate variant
93    pub animate: Option<String>,
94    /// Exit variant
95    pub exit: Option<String>,
96    /// Hover variant
97    pub hover: Option<String>,
98    /// Tap variant
99    pub tap: Option<String>,
100    /// Focus variant
101    pub focus: Option<String>,
102    /// Drag variant
103    pub drag: Option<String>,
104}
105
106impl Variants {
107    /// Create a new variants configuration
108    pub fn new() -> Self {
109        Self {
110            variants: HashMap::new(),
111            initial: None,
112            animate: None,
113            exit: None,
114            hover: None,
115            tap: None,
116            focus: None,
117            drag: None,
118        }
119    }
120
121    /// Add a variant
122    pub fn add_variant(mut self, name: impl Into<String>, target: AnimationTarget) -> Self {
123        self.variants.insert(name.into(), target);
124        self
125    }
126
127    /// Set initial variant
128    pub fn initial(mut self, name: impl Into<String>) -> Self {
129        self.initial = Some(name.into());
130        self
131    }
132
133    /// Set animate variant
134    pub fn animate(mut self, name: impl Into<String>) -> Self {
135        self.animate = Some(name.into());
136        self
137    }
138
139    /// Set exit variant
140    pub fn exit(mut self, name: impl Into<String>) -> Self {
141        self.exit = Some(name.into());
142        self
143    }
144
145    /// Set hover variant
146    pub fn hover(mut self, name: impl Into<String>) -> Self {
147        self.hover = Some(name.into());
148        self
149    }
150
151    /// Set tap variant
152    pub fn tap(mut self, name: impl Into<String>) -> Self {
153        self.tap = Some(name.into());
154        self
155    }
156
157    /// Get a variant by name
158    pub fn get_variant(&self, name: &str) -> Option<&AnimationTarget> {
159        self.variants.get(name)
160    }
161}
162
163/// Animation keyframe sequence
164#[derive(Debug, Clone)]
165pub struct Keyframes {
166    /// Keyframe positions (0.0 to 1.0)
167    pub times: Vec<f64>,
168    /// Values at each keyframe
169    pub values: Vec<AnimationTarget>,
170    /// Easing between keyframes
171    pub easings: Vec<Easing>,
172}
173
174impl Keyframes {
175    /// Create a new keyframe sequence
176    pub fn new() -> Self {
177        Self {
178            times: Vec::new(),
179            values: Vec::new(),
180            easings: Vec::new(),
181        }
182    }
183
184    /// Add a keyframe
185    pub fn add(mut self, time: f64, value: AnimationTarget, easing: Option<Easing>) -> Self {
186        self.times.push(time.clamp(0.0, 1.0));
187        self.values.push(value);
188        self.easings.push(easing.unwrap_or(Easing::Linear));
189        self
190    }
191
192    /// Get interpolated value at given progress
193    pub fn interpolate_at(&self, progress: f64) -> Option<AnimationTarget> {
194        if self.values.is_empty() {
195            return None;
196        }
197
198        if self.values.len() == 1 {
199            return Some(self.values[0].clone());
200        }
201
202        let progress = progress.clamp(0.0, 1.0);
203
204        // Find the keyframe segment
205        for i in 0..self.times.len() - 1 {
206            if progress >= self.times[i] && progress <= self.times[i + 1] {
207                let segment_progress =
208                    (progress - self.times[i]) / (self.times[i + 1] - self.times[i]);
209                let eased_progress = self.easings[i].evaluate(segment_progress);
210
211                return Some(self.interpolate_targets(
212                    &self.values[i],
213                    &self.values[i + 1],
214                    eased_progress,
215                ));
216            }
217        }
218
219        // Return last value if beyond range
220        Some(self.values.last().unwrap().clone())
221    }
222
223    fn interpolate_targets(
224        &self,
225        from: &AnimationTarget,
226        to: &AnimationTarget,
227        progress: f64,
228    ) -> AnimationTarget {
229        let mut result = HashMap::new();
230
231        // Collect all property names
232        let mut properties: std::collections::HashSet<_> = from.keys().collect();
233        properties.extend(to.keys());
234
235        for property in properties {
236            let from_value = from
237                .get(property)
238                .cloned()
239                .unwrap_or(AnimationValue::Number(0.0));
240            let to_value = to
241                .get(property)
242                .cloned()
243                .unwrap_or(AnimationValue::Number(0.0));
244
245            result.insert(
246                property.clone(),
247                from_value.interpolate(&to_value, progress),
248            );
249        }
250
251        result
252    }
253}
254
255/// Convenience macros for creating animation targets
256#[macro_export]
257macro_rules! animate {
258    ($($key:expr => $value:expr),* $(,)?) => {
259        {
260            let mut target = std::collections::HashMap::new();
261            $(
262                target.insert($key.to_string(), $value.into());
263            )*
264            target
265        }
266    };
267}
268
269/// Convenience functions for common animations
270// Animation presets are now in a separate file
271pub mod presets;
272
273impl Default for AnimationBuilder {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279impl Default for Variants {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285impl Default for Keyframes {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::AnimationPresets;
295
296    #[test]
297    fn test_animation_builder() {
298        let animation = AnimationBuilder::new()
299            .initial(animate!("opacity" => AnimationValue::Number(0.0)))
300            .animate(animate!("opacity" => AnimationValue::Number(1.0)))
301            .transition(Transition {
302                duration: Some(1.0),
303                ..Default::default()
304            })
305            .build();
306
307        assert!(animation.initial.contains_key("opacity"));
308        assert!(animation.animate.contains_key("opacity"));
309        assert_eq!(animation.transition.duration, Some(1.0));
310    }
311
312    #[test]
313    fn test_variants() {
314        let variants = Variants::new()
315            .add_variant("hidden", animate!("opacity" => AnimationValue::Number(0.0)))
316            .add_variant(
317                "visible",
318                animate!("opacity" => AnimationValue::Number(1.0)),
319            )
320            .initial("hidden")
321            .animate("visible");
322
323        assert!(variants.get_variant("hidden").is_some());
324        assert!(variants.get_variant("visible").is_some());
325        assert_eq!(variants.initial, Some("hidden".to_string()));
326        assert_eq!(variants.animate, Some("visible".to_string()));
327    }
328
329    #[test]
330    fn test_keyframes() {
331        let keyframes = Keyframes::new()
332            .add(0.0, animate!("scale" => AnimationValue::Number(1.0)), None)
333            .add(0.5, animate!("scale" => AnimationValue::Number(1.2)), None)
334            .add(1.0, animate!("scale" => AnimationValue::Number(1.0)), None);
335
336        let mid_value = keyframes.interpolate_at(0.25).unwrap();
337        assert!(mid_value.contains_key("scale"));
338
339        // Should be between 1.0 and 1.2
340        if let AnimationValue::Number(scale) = &mid_value["scale"] {
341            assert!(*scale > 1.0 && *scale < 1.2);
342        }
343    }
344
345    #[test]
346    fn test_presets() {
347        let fade_in = AnimationPresets::fade_in();
348        assert!(fade_in.initial.contains_key("opacity"));
349        assert!(fade_in.animate.contains_key("opacity"));
350
351        let slide_up = AnimationPresets::slide_up(50.0);
352        assert!(slide_up.initial.contains_key("y"));
353
354        let bounce = AnimationPresets::bounce();
355        assert_eq!(bounce.times.len(), 3);
356        assert_eq!(bounce.values.len(), 3);
357    }
358
359    #[test]
360    fn test_animate_macro() {
361        let target: HashMap<String, AnimationValue> = animate!(
362            "opacity" => AnimationValue::Number(0.5),
363            "x" => AnimationValue::Pixels(100.0)
364        );
365
366        assert_eq!(target.len(), 2);
367        assert!(target.contains_key("opacity"));
368        assert!(target.contains_key("x"));
369    }
370}