tailwind_rs_core/utilities/
advanced_animations.rs

1//! Advanced animation utilities for tailwind-rs
2//!
3//! This module provides support for custom keyframe animations,
4//! animation composition, and advanced animation features.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::collections::HashMap;
9
10/// Custom keyframe animation
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct CustomKeyframe {
13    /// Animation name
14    pub name: String,
15    /// Keyframe steps (0.0 to 1.0)
16    pub steps: Vec<(f32, KeyframeStep)>,
17    /// Animation duration in milliseconds
18    pub duration: u32,
19    /// Animation timing function
20    pub timing_function: TimingFunction,
21    /// Animation delay in milliseconds
22    pub delay: u32,
23    /// Animation iteration count
24    pub iteration_count: AnimationIteration,
25    /// Animation direction
26    pub direction: AnimationDirection,
27    /// Animation fill mode
28    pub fill_mode: AnimationFillMode,
29    /// Animation play state
30    pub play_state: AnimationPlayState,
31}
32
33/// Keyframe step with properties
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct KeyframeStep {
36    /// CSS properties for this keyframe
37    pub properties: HashMap<String, String>,
38    /// Transform properties
39    pub transform: Option<TransformStep>,
40    /// Opacity value
41    pub opacity: Option<f32>,
42    /// Color value
43    pub color: Option<String>,
44}
45
46/// Transform step for keyframes
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct TransformStep {
49    /// Translation (x, y, z)
50    pub translate: Option<(f32, f32, f32)>,
51    /// Scale (x, y, z)
52    pub scale: Option<(f32, f32, f32)>,
53    /// Rotation (x, y, z) in degrees
54    pub rotate: Option<(f32, f32, f32)>,
55    /// Skew (x, y) in degrees
56    pub skew: Option<(f32, f32)>,
57}
58
59/// Animation timing function
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub enum TimingFunction {
62    /// linear
63    Linear,
64    /// ease
65    Ease,
66    /// ease-in
67    EaseIn,
68    /// ease-out
69    EaseOut,
70    /// ease-in-out
71    EaseInOut,
72    /// cubic-bezier(n,n,n,n)
73    CubicBezier(f32, f32, f32, f32),
74    /// steps(n)
75    Steps(u32),
76    /// steps(n, start|end)
77    StepsWithDirection(u32, StepDirection),
78}
79
80/// Step direction for steps timing function
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub enum StepDirection {
83    /// start
84    Start,
85    /// end
86    End,
87}
88
89/// Animation iteration count
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub enum AnimationIteration {
92    /// infinite
93    Infinite,
94    /// specific number
95    Count(f32),
96}
97
98/// Animation direction
99#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
100pub enum AnimationDirection {
101    /// normal
102    Normal,
103    /// reverse
104    Reverse,
105    /// alternate
106    Alternate,
107    /// alternate-reverse
108    AlternateReverse,
109}
110
111/// Animation fill mode
112#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
113pub enum AnimationFillMode {
114    /// none
115    None,
116    /// forwards
117    Forwards,
118    /// backwards
119    Backwards,
120    /// both
121    Both,
122}
123
124/// Animation play state
125#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
126pub enum AnimationPlayState {
127    /// running
128    Running,
129    /// paused
130    Paused,
131}
132
133/// Animation composition
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct AnimationComposition {
136    /// Composition name
137    pub name: String,
138    /// Animations to compose
139    pub animations: Vec<ComposedAnimation>,
140    /// Composition timing
141    pub timing: CompositionTiming,
142}
143
144/// Composed animation reference
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct ComposedAnimation {
147    /// Animation name or custom keyframe
148    pub animation: AnimationReference,
149    /// Start offset (0.0 to 1.0)
150    pub start_offset: f32,
151    /// End offset (0.0 to 1.0)
152    pub end_offset: f32,
153    /// Animation properties override
154    pub properties: Option<AnimationProperties>,
155}
156
157/// Animation reference
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub enum AnimationReference {
160    /// Built-in animation name
161    BuiltIn(String),
162    /// Custom keyframe animation
163    Custom(CustomKeyframe),
164}
165
166/// Animation properties override
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168pub struct AnimationProperties {
169    /// Duration override
170    pub duration: Option<u32>,
171    /// Timing function override
172    pub timing_function: Option<TimingFunction>,
173    /// Delay override
174    pub delay: Option<u32>,
175    /// Iteration count override
176    pub iteration_count: Option<AnimationIteration>,
177    /// Direction override
178    pub direction: Option<AnimationDirection>,
179}
180
181/// Composition timing
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct CompositionTiming {
184    /// Total duration in milliseconds
185    pub duration: u32,
186    /// Timing function for the composition
187    pub timing_function: TimingFunction,
188    /// Delay for the composition
189    pub delay: u32,
190}
191
192impl CustomKeyframe {
193    /// Create a new custom keyframe animation
194    pub fn new(name: String) -> Self {
195        Self {
196            name,
197            steps: Vec::new(),
198            duration: 1000,
199            timing_function: TimingFunction::Ease,
200            delay: 0,
201            iteration_count: AnimationIteration::Count(1.0),
202            direction: AnimationDirection::Normal,
203            fill_mode: AnimationFillMode::Both,
204            play_state: AnimationPlayState::Running,
205        }
206    }
207
208    /// Add a keyframe step
209    pub fn add_step(&mut self, offset: f32, step: KeyframeStep) {
210        self.steps.push((offset, step));
211        // Sort by offset to maintain order
212        self.steps.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
213    }
214
215    /// Convert to CSS @keyframes rule
216    pub fn to_css_keyframes(&self) -> String {
217        let mut css = format!("@keyframes {} {{\n", self.name);
218        
219        for (offset, step) in &self.steps {
220            let percentage = (offset * 100.0) as u32;
221            css.push_str(&format!("  {}% {{\n", percentage));
222            
223            // Add properties
224            for (property, value) in &step.properties {
225                css.push_str(&format!("    {}: {};\n", property, value));
226            }
227            
228            // Add transform
229            if let Some(transform) = &step.transform {
230                css.push_str(&format!("    transform: {};\n", transform.to_css_value()));
231            }
232            
233            // Add opacity
234            if let Some(opacity) = step.opacity {
235                css.push_str(&format!("    opacity: {};\n", opacity));
236            }
237            
238            // Add color
239            if let Some(color) = &step.color {
240                css.push_str(&format!("    color: {};\n", color));
241            }
242            
243            css.push_str("  }\n");
244        }
245        
246        css.push_str("}\n");
247        css
248    }
249
250    /// Convert to class name
251    pub fn to_class_name(&self) -> String {
252        format!("animate-{}", self.name)
253    }
254
255    /// Convert to CSS animation property
256    pub fn to_css_animation(&self) -> String {
257        format!(
258            "{} {}ms {} {}ms {} {} {} {}",
259            self.name,
260            self.duration,
261            self.timing_function.to_css_value(),
262            self.delay,
263            self.iteration_count.to_css_value(),
264            self.direction.to_css_value(),
265            self.fill_mode.to_css_value(),
266            self.play_state.to_css_value()
267        )
268    }
269}
270
271impl KeyframeStep {
272    /// Create a new keyframe step
273    pub fn new() -> Self {
274        Self {
275            properties: HashMap::new(),
276            transform: None,
277            opacity: None,
278            color: None,
279        }
280    }
281
282    /// Add a CSS property
283    pub fn add_property(&mut self, property: String, value: String) {
284        self.properties.insert(property, value);
285    }
286
287    /// Set transform
288    pub fn set_transform(&mut self, transform: TransformStep) {
289        self.transform = Some(transform);
290    }
291
292    /// Set opacity
293    pub fn set_opacity(&mut self, opacity: f32) {
294        self.opacity = Some(opacity);
295    }
296
297    /// Set color
298    pub fn set_color(&mut self, color: String) {
299        self.color = Some(color);
300    }
301}
302
303impl TransformStep {
304    /// Create a new transform step
305    pub fn new() -> Self {
306        Self {
307            translate: None,
308            scale: None,
309            rotate: None,
310            skew: None,
311        }
312    }
313
314    /// Set translation
315    pub fn set_translate(&mut self, x: f32, y: f32, z: f32) {
316        self.translate = Some((x, y, z));
317    }
318
319    /// Set scale
320    pub fn set_scale(&mut self, x: f32, y: f32, z: f32) {
321        self.scale = Some((x, y, z));
322    }
323
324    /// Set rotation
325    pub fn set_rotate(&mut self, x: f32, y: f32, z: f32) {
326        self.rotate = Some((x, y, z));
327    }
328
329    /// Set skew
330    pub fn set_skew(&mut self, x: f32, y: f32) {
331        self.skew = Some((x, y));
332    }
333
334    /// Convert to CSS transform value
335    pub fn to_css_value(&self) -> String {
336        let mut transforms = Vec::new();
337        
338        if let Some((x, y, z)) = self.translate {
339            if z == 0.0 {
340                transforms.push(format!("translate({}px, {}px)", x, y));
341            } else {
342                transforms.push(format!("translate3d({}px, {}px, {}px)", x, y, z));
343            }
344        }
345        
346        if let Some((x, y, z)) = self.scale {
347            if z == 1.0 {
348                transforms.push(format!("scale({}, {})", x, y));
349            } else {
350                transforms.push(format!("scale3d({}, {}, {})", x, y, z));
351            }
352        }
353        
354        if let Some((x, y, z)) = self.rotate {
355            if x == 0.0 && y == 0.0 {
356                transforms.push(format!("rotate({}deg)", z));
357            } else {
358                transforms.push(format!("rotate3d({}, {}, {}, {}deg)", x, y, z, z));
359            }
360        }
361        
362        if let Some((x, y)) = self.skew {
363            transforms.push(format!("skew({}deg, {}deg)", x, y));
364        }
365        
366        transforms.join(" ")
367    }
368}
369
370impl TimingFunction {
371    /// Convert to CSS value
372    pub fn to_css_value(&self) -> String {
373        match self {
374            TimingFunction::Linear => "linear".to_string(),
375            TimingFunction::Ease => "ease".to_string(),
376            TimingFunction::EaseIn => "ease-in".to_string(),
377            TimingFunction::EaseOut => "ease-out".to_string(),
378            TimingFunction::EaseInOut => "ease-in-out".to_string(),
379            TimingFunction::CubicBezier(x1, y1, x2, y2) => {
380                format!("cubic-bezier({}, {}, {}, {})", x1, y1, x2, y2)
381            }
382            TimingFunction::Steps(count) => format!("steps({})", count),
383            TimingFunction::StepsWithDirection(count, direction) => {
384                let dir = match direction {
385                    StepDirection::Start => "start",
386                    StepDirection::End => "end",
387                };
388                format!("steps({}, {})", count, dir)
389            }
390        }
391    }
392}
393
394impl AnimationIteration {
395    /// Convert to CSS value
396    pub fn to_css_value(&self) -> String {
397        match self {
398            AnimationIteration::Infinite => "infinite".to_string(),
399            AnimationIteration::Count(count) => count.to_string(),
400        }
401    }
402}
403
404impl AnimationDirection {
405    /// Convert to CSS value
406    pub fn to_css_value(&self) -> String {
407        match self {
408            AnimationDirection::Normal => "normal".to_string(),
409            AnimationDirection::Reverse => "reverse".to_string(),
410            AnimationDirection::Alternate => "alternate".to_string(),
411            AnimationDirection::AlternateReverse => "alternate-reverse".to_string(),
412        }
413    }
414}
415
416impl AnimationFillMode {
417    /// Convert to CSS value
418    pub fn to_css_value(&self) -> String {
419        match self {
420            AnimationFillMode::None => "none".to_string(),
421            AnimationFillMode::Forwards => "forwards".to_string(),
422            AnimationFillMode::Backwards => "backwards".to_string(),
423            AnimationFillMode::Both => "both".to_string(),
424        }
425    }
426}
427
428impl AnimationPlayState {
429    /// Convert to CSS value
430    pub fn to_css_value(&self) -> String {
431        match self {
432            AnimationPlayState::Running => "running".to_string(),
433            AnimationPlayState::Paused => "paused".to_string(),
434        }
435    }
436}
437
438impl AnimationComposition {
439    /// Create a new animation composition
440    pub fn new(name: String) -> Self {
441        Self {
442            name,
443            animations: Vec::new(),
444            timing: CompositionTiming {
445                duration: 1000,
446                timing_function: TimingFunction::Ease,
447                delay: 0,
448            },
449        }
450    }
451
452    /// Add an animation to the composition
453    pub fn add_animation(&mut self, animation: ComposedAnimation) {
454        self.animations.push(animation);
455    }
456
457    /// Convert to CSS
458    pub fn to_css(&self) -> String {
459        let mut css = String::new();
460        
461        // Generate keyframes for each animation
462        for composed_anim in &self.animations {
463            if let AnimationReference::Custom(keyframe) = &composed_anim.animation {
464                css.push_str(&keyframe.to_css_keyframes());
465            }
466        }
467        
468        // Generate composition class
469        css.push_str(&format!(".{} {{\n", self.name));
470        css.push_str(&format!("  animation: {} {}ms {} {}ms;\n", 
471            self.name, 
472            self.timing.duration, 
473            self.timing.timing_function.to_css_value(),
474            self.timing.delay
475        ));
476        css.push_str("}\n");
477        
478        css
479    }
480
481    /// Convert to class name
482    pub fn to_class_name(&self) -> String {
483        format!("animate-{}", self.name)
484    }
485}
486
487impl fmt::Display for CustomKeyframe {
488    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489        write!(f, "{}", self.to_css_animation())
490    }
491}
492
493impl fmt::Display for AnimationComposition {
494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495        write!(f, "{}", self.to_css())
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_custom_keyframe_creation() {
505        let mut keyframe = CustomKeyframe::new("fadeIn".to_string());
506        keyframe.duration = 500;
507        keyframe.timing_function = TimingFunction::EaseOut;
508        
509        assert_eq!(keyframe.name, "fadeIn");
510        assert_eq!(keyframe.duration, 500);
511        assert_eq!(keyframe.timing_function, TimingFunction::EaseOut);
512    }
513
514    #[test]
515    fn test_keyframe_step_creation() {
516        let mut step = KeyframeStep::new();
517        step.set_opacity(0.0);
518        step.add_property("transform".to_string(), "scale(0.8)".to_string());
519        
520        assert_eq!(step.opacity, Some(0.0));
521        assert_eq!(step.properties.get("transform"), Some(&"scale(0.8)".to_string()));
522    }
523
524    #[test]
525    fn test_transform_step_creation() {
526        let mut transform = TransformStep::new();
527        transform.set_translate(10.0, 20.0, 0.0);
528        transform.set_scale(1.2, 1.2, 1.0);
529        transform.set_rotate(0.0, 0.0, 45.0);
530        
531        assert_eq!(transform.translate, Some((10.0, 20.0, 0.0)));
532        assert_eq!(transform.scale, Some((1.2, 1.2, 1.0)));
533        assert_eq!(transform.rotate, Some((0.0, 0.0, 45.0)));
534    }
535
536    #[test]
537    fn test_transform_css_generation() {
538        let mut transform = TransformStep::new();
539        transform.set_translate(10.0, 20.0, 0.0);
540        transform.set_scale(1.2, 1.2, 1.0);
541        transform.set_rotate(0.0, 0.0, 45.0);
542        
543        let css = transform.to_css_value();
544        assert!(css.contains("translate(10px, 20px)"));
545        assert!(css.contains("scale(1.2, 1.2)"));
546        assert!(css.contains("rotate(45deg)"));
547    }
548
549    #[test]
550    fn test_timing_function_css_values() {
551        assert_eq!(TimingFunction::Linear.to_css_value(), "linear");
552        assert_eq!(TimingFunction::Ease.to_css_value(), "ease");
553        assert_eq!(TimingFunction::EaseIn.to_css_value(), "ease-in");
554        assert_eq!(TimingFunction::EaseOut.to_css_value(), "ease-out");
555        assert_eq!(TimingFunction::EaseInOut.to_css_value(), "ease-in-out");
556        
557        let cubic_bezier = TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0);
558        assert_eq!(cubic_bezier.to_css_value(), "cubic-bezier(0.25, 0.1, 0.25, 1)");
559        
560        let steps = TimingFunction::Steps(5);
561        assert_eq!(steps.to_css_value(), "steps(5)");
562        
563        let steps_with_dir = TimingFunction::StepsWithDirection(5, StepDirection::Start);
564        assert_eq!(steps_with_dir.to_css_value(), "steps(5, start)");
565    }
566
567    #[test]
568    fn test_animation_iteration_css_values() {
569        assert_eq!(AnimationIteration::Infinite.to_css_value(), "infinite");
570        assert_eq!(AnimationIteration::Count(3.0).to_css_value(), "3");
571        assert_eq!(AnimationIteration::Count(0.5).to_css_value(), "0.5");
572    }
573
574    #[test]
575    fn test_animation_direction_css_values() {
576        assert_eq!(AnimationDirection::Normal.to_css_value(), "normal");
577        assert_eq!(AnimationDirection::Reverse.to_css_value(), "reverse");
578        assert_eq!(AnimationDirection::Alternate.to_css_value(), "alternate");
579        assert_eq!(AnimationDirection::AlternateReverse.to_css_value(), "alternate-reverse");
580    }
581
582    #[test]
583    fn test_animation_fill_mode_css_values() {
584        assert_eq!(AnimationFillMode::None.to_css_value(), "none");
585        assert_eq!(AnimationFillMode::Forwards.to_css_value(), "forwards");
586        assert_eq!(AnimationFillMode::Backwards.to_css_value(), "backwards");
587        assert_eq!(AnimationFillMode::Both.to_css_value(), "both");
588    }
589
590    #[test]
591    fn test_animation_play_state_css_values() {
592        assert_eq!(AnimationPlayState::Running.to_css_value(), "running");
593        assert_eq!(AnimationPlayState::Paused.to_css_value(), "paused");
594    }
595
596    #[test]
597    fn test_custom_keyframe_css_generation() {
598        let mut keyframe = CustomKeyframe::new("fadeIn".to_string());
599        keyframe.duration = 500;
600        keyframe.timing_function = TimingFunction::EaseOut;
601        
602        let mut step0 = KeyframeStep::new();
603        step0.set_opacity(0.0);
604        keyframe.add_step(0.0, step0);
605        
606        let mut step1 = KeyframeStep::new();
607        step1.set_opacity(1.0);
608        keyframe.add_step(1.0, step1);
609        
610        let css = keyframe.to_css_keyframes();
611        assert!(css.contains("@keyframes fadeIn"));
612        assert!(css.contains("0%"));
613        assert!(css.contains("100%"));
614        assert!(css.contains("opacity: 0"));
615        assert!(css.contains("opacity: 1"));
616        
617        let animation_css = keyframe.to_css_animation();
618        assert!(animation_css.contains("fadeIn"));
619        assert!(animation_css.contains("500ms"));
620        assert!(animation_css.contains("ease-out"));
621    }
622
623    #[test]
624    fn test_animation_composition_creation() {
625        let mut composition = AnimationComposition::new("complexAnimation".to_string());
626        composition.timing.duration = 2000;
627        composition.timing.timing_function = TimingFunction::EaseInOut;
628        
629        assert_eq!(composition.name, "complexAnimation");
630        assert_eq!(composition.timing.duration, 2000);
631        assert_eq!(composition.timing.timing_function, TimingFunction::EaseInOut);
632    }
633
634    #[test]
635    fn test_animation_composition_css_generation() {
636        let mut composition = AnimationComposition::new("complexAnimation".to_string());
637        
638        let mut keyframe = CustomKeyframe::new("fadeIn".to_string());
639        let mut step = KeyframeStep::new();
640        step.set_opacity(1.0);
641        keyframe.add_step(1.0, step);
642        
643        let composed_anim = ComposedAnimation {
644            animation: AnimationReference::Custom(keyframe),
645            start_offset: 0.0,
646            end_offset: 1.0,
647            properties: None,
648        };
649        
650        composition.add_animation(composed_anim);
651        
652        let css = composition.to_css();
653        assert!(css.contains("@keyframes fadeIn"));
654        assert!(css.contains(".complexAnimation"));
655    }
656
657    #[test]
658    fn test_custom_keyframe_class_name() {
659        let keyframe = CustomKeyframe::new("fadeIn".to_string());
660        assert_eq!(keyframe.to_class_name(), "animate-fadeIn");
661    }
662
663    #[test]
664    fn test_animation_composition_class_name() {
665        let composition = AnimationComposition::new("complexAnimation".to_string());
666        assert_eq!(composition.to_class_name(), "animate-complexAnimation");
667    }
668
669    #[test]
670    fn test_custom_keyframe_display() {
671        let keyframe = CustomKeyframe::new("fadeIn".to_string());
672        let display = format!("{}", keyframe);
673        assert!(display.contains("fadeIn"));
674    }
675
676    #[test]
677    fn test_animation_composition_display() {
678        let composition = AnimationComposition::new("complexAnimation".to_string());
679        let display = format!("{}", composition);
680        assert!(display.contains("complexAnimation"));
681    }
682
683    #[test]
684    fn test_custom_keyframe_serialization() {
685        let keyframe = CustomKeyframe::new("fadeIn".to_string());
686        let serialized = serde_json::to_string(&keyframe).unwrap();
687        let deserialized: CustomKeyframe = serde_json::from_str(&serialized).unwrap();
688        assert_eq!(keyframe, deserialized);
689    }
690
691    #[test]
692    fn test_animation_composition_serialization() {
693        let composition = AnimationComposition::new("complexAnimation".to_string());
694        let serialized = serde_json::to_string(&composition).unwrap();
695        let deserialized: AnimationComposition = serde_json::from_str(&serialized).unwrap();
696        assert_eq!(composition, deserialized);
697    }
698}