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::collections::HashMap;
8use std::fmt;
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 Default for KeyframeStep {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl KeyframeStep {
278    /// Create a new keyframe step
279    pub fn new() -> Self {
280        Self {
281            properties: HashMap::new(),
282            transform: None,
283            opacity: None,
284            color: None,
285        }
286    }
287
288    /// Add a CSS property
289    pub fn add_property(&mut self, property: String, value: String) {
290        self.properties.insert(property, value);
291    }
292
293    /// Set transform
294    pub fn set_transform(&mut self, transform: TransformStep) {
295        self.transform = Some(transform);
296    }
297
298    /// Set opacity
299    pub fn set_opacity(&mut self, opacity: f32) {
300        self.opacity = Some(opacity);
301    }
302
303    /// Set color
304    pub fn set_color(&mut self, color: String) {
305        self.color = Some(color);
306    }
307}
308
309impl Default for TransformStep {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315impl TransformStep {
316    /// Create a new transform step
317    pub fn new() -> Self {
318        Self {
319            translate: None,
320            scale: None,
321            rotate: None,
322            skew: None,
323        }
324    }
325
326    /// Set translation
327    pub fn set_translate(&mut self, x: f32, y: f32, z: f32) {
328        self.translate = Some((x, y, z));
329    }
330
331    /// Set scale
332    pub fn set_scale(&mut self, x: f32, y: f32, z: f32) {
333        self.scale = Some((x, y, z));
334    }
335
336    /// Set rotation
337    pub fn set_rotate(&mut self, x: f32, y: f32, z: f32) {
338        self.rotate = Some((x, y, z));
339    }
340
341    /// Set skew
342    pub fn set_skew(&mut self, x: f32, y: f32) {
343        self.skew = Some((x, y));
344    }
345
346    /// Convert to CSS transform value
347    pub fn to_css_value(&self) -> String {
348        let mut transforms = Vec::new();
349
350        if let Some((x, y, z)) = self.translate {
351            if z == 0.0 {
352                transforms.push(format!("translate({}px, {}px)", x, y));
353            } else {
354                transforms.push(format!("translate3d({}px, {}px, {}px)", x, y, z));
355            }
356        }
357
358        if let Some((x, y, z)) = self.scale {
359            if z == 1.0 {
360                transforms.push(format!("scale({}, {})", x, y));
361            } else {
362                transforms.push(format!("scale3d({}, {}, {})", x, y, z));
363            }
364        }
365
366        if let Some((x, y, z)) = self.rotate {
367            if x == 0.0 && y == 0.0 {
368                transforms.push(format!("rotate({}deg)", z));
369            } else {
370                transforms.push(format!("rotate3d({}, {}, {}, {}deg)", x, y, z, z));
371            }
372        }
373
374        if let Some((x, y)) = self.skew {
375            transforms.push(format!("skew({}deg, {}deg)", x, y));
376        }
377
378        transforms.join(" ")
379    }
380}
381
382impl TimingFunction {
383    /// Convert to CSS value
384    pub fn to_css_value(&self) -> String {
385        match self {
386            TimingFunction::Linear => "linear".to_string(),
387            TimingFunction::Ease => "ease".to_string(),
388            TimingFunction::EaseIn => "ease-in".to_string(),
389            TimingFunction::EaseOut => "ease-out".to_string(),
390            TimingFunction::EaseInOut => "ease-in-out".to_string(),
391            TimingFunction::CubicBezier(x1, y1, x2, y2) => {
392                format!("cubic-bezier({}, {}, {}, {})", x1, y1, x2, y2)
393            }
394            TimingFunction::Steps(count) => format!("steps({})", count),
395            TimingFunction::StepsWithDirection(count, direction) => {
396                let dir = match direction {
397                    StepDirection::Start => "start",
398                    StepDirection::End => "end",
399                };
400                format!("steps({}, {})", count, dir)
401            }
402        }
403    }
404}
405
406impl AnimationIteration {
407    /// Convert to CSS value
408    pub fn to_css_value(&self) -> String {
409        match self {
410            AnimationIteration::Infinite => "infinite".to_string(),
411            AnimationIteration::Count(count) => count.to_string(),
412        }
413    }
414}
415
416impl AnimationDirection {
417    /// Convert to CSS value
418    pub fn to_css_value(&self) -> String {
419        match self {
420            AnimationDirection::Normal => "normal".to_string(),
421            AnimationDirection::Reverse => "reverse".to_string(),
422            AnimationDirection::Alternate => "alternate".to_string(),
423            AnimationDirection::AlternateReverse => "alternate-reverse".to_string(),
424        }
425    }
426}
427
428impl AnimationFillMode {
429    /// Convert to CSS value
430    pub fn to_css_value(&self) -> String {
431        match self {
432            AnimationFillMode::None => "none".to_string(),
433            AnimationFillMode::Forwards => "forwards".to_string(),
434            AnimationFillMode::Backwards => "backwards".to_string(),
435            AnimationFillMode::Both => "both".to_string(),
436        }
437    }
438}
439
440impl AnimationPlayState {
441    /// Convert to CSS value
442    pub fn to_css_value(&self) -> String {
443        match self {
444            AnimationPlayState::Running => "running".to_string(),
445            AnimationPlayState::Paused => "paused".to_string(),
446        }
447    }
448}
449
450impl AnimationComposition {
451    /// Create a new animation composition
452    pub fn new(name: String) -> Self {
453        Self {
454            name,
455            animations: Vec::new(),
456            timing: CompositionTiming {
457                duration: 1000,
458                timing_function: TimingFunction::Ease,
459                delay: 0,
460            },
461        }
462    }
463
464    /// Add an animation to the composition
465    pub fn add_animation(&mut self, animation: ComposedAnimation) {
466        self.animations.push(animation);
467    }
468
469    /// Convert to CSS
470    pub fn to_css(&self) -> String {
471        let mut css = String::new();
472
473        // Generate keyframes for each animation
474        for composed_anim in &self.animations {
475            if let AnimationReference::Custom(keyframe) = &composed_anim.animation {
476                css.push_str(&keyframe.to_css_keyframes());
477            }
478        }
479
480        // Generate composition class
481        css.push_str(&format!(".{} {{\n", self.name));
482        css.push_str(&format!(
483            "  animation: {} {}ms {} {}ms;\n",
484            self.name,
485            self.timing.duration,
486            self.timing.timing_function.to_css_value(),
487            self.timing.delay
488        ));
489        css.push_str("}\n");
490
491        css
492    }
493
494    /// Convert to class name
495    pub fn to_class_name(&self) -> String {
496        format!("animate-{}", self.name)
497    }
498}
499
500impl fmt::Display for CustomKeyframe {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        write!(f, "{}", self.to_css_animation())
503    }
504}
505
506impl fmt::Display for AnimationComposition {
507    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508        write!(f, "{}", self.to_css())
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn test_custom_keyframe_creation() {
518        let mut keyframe = CustomKeyframe::new("fadeIn".to_string());
519        keyframe.duration = 500;
520        keyframe.timing_function = TimingFunction::EaseOut;
521
522        assert_eq!(keyframe.name, "fadeIn");
523        assert_eq!(keyframe.duration, 500);
524        assert_eq!(keyframe.timing_function, TimingFunction::EaseOut);
525    }
526
527    #[test]
528    fn test_keyframe_step_creation() {
529        let mut step = KeyframeStep::new();
530        step.set_opacity(0.0);
531        step.add_property("transform".to_string(), "scale(0.8)".to_string());
532
533        assert_eq!(step.opacity, Some(0.0));
534        assert_eq!(
535            step.properties.get("transform"),
536            Some(&"scale(0.8)".to_string())
537        );
538    }
539
540    #[test]
541    fn test_transform_step_creation() {
542        let mut transform = TransformStep::new();
543        transform.set_translate(10.0, 20.0, 0.0);
544        transform.set_scale(1.2, 1.2, 1.0);
545        transform.set_rotate(0.0, 0.0, 45.0);
546
547        assert_eq!(transform.translate, Some((10.0, 20.0, 0.0)));
548        assert_eq!(transform.scale, Some((1.2, 1.2, 1.0)));
549        assert_eq!(transform.rotate, Some((0.0, 0.0, 45.0)));
550    }
551
552    #[test]
553    fn test_transform_css_generation() {
554        let mut transform = TransformStep::new();
555        transform.set_translate(10.0, 20.0, 0.0);
556        transform.set_scale(1.2, 1.2, 1.0);
557        transform.set_rotate(0.0, 0.0, 45.0);
558
559        let css = transform.to_css_value();
560        assert!(css.contains("translate(10px, 20px)"));
561        assert!(css.contains("scale(1.2, 1.2)"));
562        assert!(css.contains("rotate(45deg)"));
563    }
564
565    #[test]
566    fn test_timing_function_css_values() {
567        assert_eq!(TimingFunction::Linear.to_css_value(), "linear");
568        assert_eq!(TimingFunction::Ease.to_css_value(), "ease");
569        assert_eq!(TimingFunction::EaseIn.to_css_value(), "ease-in");
570        assert_eq!(TimingFunction::EaseOut.to_css_value(), "ease-out");
571        assert_eq!(TimingFunction::EaseInOut.to_css_value(), "ease-in-out");
572
573        let cubic_bezier = TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0);
574        assert_eq!(
575            cubic_bezier.to_css_value(),
576            "cubic-bezier(0.25, 0.1, 0.25, 1)"
577        );
578
579        let steps = TimingFunction::Steps(5);
580        assert_eq!(steps.to_css_value(), "steps(5)");
581
582        let steps_with_dir = TimingFunction::StepsWithDirection(5, StepDirection::Start);
583        assert_eq!(steps_with_dir.to_css_value(), "steps(5, start)");
584    }
585
586    #[test]
587    fn test_animation_iteration_css_values() {
588        assert_eq!(AnimationIteration::Infinite.to_css_value(), "infinite");
589        assert_eq!(AnimationIteration::Count(3.0).to_css_value(), "3");
590        assert_eq!(AnimationIteration::Count(0.5).to_css_value(), "0.5");
591    }
592
593    #[test]
594    fn test_animation_direction_css_values() {
595        assert_eq!(AnimationDirection::Normal.to_css_value(), "normal");
596        assert_eq!(AnimationDirection::Reverse.to_css_value(), "reverse");
597        assert_eq!(AnimationDirection::Alternate.to_css_value(), "alternate");
598        assert_eq!(
599            AnimationDirection::AlternateReverse.to_css_value(),
600            "alternate-reverse"
601        );
602    }
603
604    #[test]
605    fn test_animation_fill_mode_css_values() {
606        assert_eq!(AnimationFillMode::None.to_css_value(), "none");
607        assert_eq!(AnimationFillMode::Forwards.to_css_value(), "forwards");
608        assert_eq!(AnimationFillMode::Backwards.to_css_value(), "backwards");
609        assert_eq!(AnimationFillMode::Both.to_css_value(), "both");
610    }
611
612    #[test]
613    fn test_animation_play_state_css_values() {
614        assert_eq!(AnimationPlayState::Running.to_css_value(), "running");
615        assert_eq!(AnimationPlayState::Paused.to_css_value(), "paused");
616    }
617
618    #[test]
619    fn test_custom_keyframe_css_generation() {
620        let mut keyframe = CustomKeyframe::new("fadeIn".to_string());
621        keyframe.duration = 500;
622        keyframe.timing_function = TimingFunction::EaseOut;
623
624        let mut step0 = KeyframeStep::new();
625        step0.set_opacity(0.0);
626        keyframe.add_step(0.0, step0);
627
628        let mut step1 = KeyframeStep::new();
629        step1.set_opacity(1.0);
630        keyframe.add_step(1.0, step1);
631
632        let css = keyframe.to_css_keyframes();
633        assert!(css.contains("@keyframes fadeIn"));
634        assert!(css.contains("0%"));
635        assert!(css.contains("100%"));
636        assert!(css.contains("opacity: 0"));
637        assert!(css.contains("opacity: 1"));
638
639        let animation_css = keyframe.to_css_animation();
640        assert!(animation_css.contains("fadeIn"));
641        assert!(animation_css.contains("500ms"));
642        assert!(animation_css.contains("ease-out"));
643    }
644
645    #[test]
646    fn test_animation_composition_creation() {
647        let mut composition = AnimationComposition::new("complexAnimation".to_string());
648        composition.timing.duration = 2000;
649        composition.timing.timing_function = TimingFunction::EaseInOut;
650
651        assert_eq!(composition.name, "complexAnimation");
652        assert_eq!(composition.timing.duration, 2000);
653        assert_eq!(
654            composition.timing.timing_function,
655            TimingFunction::EaseInOut
656        );
657    }
658
659    #[test]
660    fn test_animation_composition_css_generation() {
661        let mut composition = AnimationComposition::new("complexAnimation".to_string());
662
663        let mut keyframe = CustomKeyframe::new("fadeIn".to_string());
664        let mut step = KeyframeStep::new();
665        step.set_opacity(1.0);
666        keyframe.add_step(1.0, step);
667
668        let composed_anim = ComposedAnimation {
669            animation: AnimationReference::Custom(keyframe),
670            start_offset: 0.0,
671            end_offset: 1.0,
672            properties: None,
673        };
674
675        composition.add_animation(composed_anim);
676
677        let css = composition.to_css();
678        assert!(css.contains("@keyframes fadeIn"));
679        assert!(css.contains(".complexAnimation"));
680    }
681
682    #[test]
683    fn test_custom_keyframe_class_name() {
684        let keyframe = CustomKeyframe::new("fadeIn".to_string());
685        assert_eq!(keyframe.to_class_name(), "animate-fadeIn");
686    }
687
688    #[test]
689    fn test_animation_composition_class_name() {
690        let composition = AnimationComposition::new("complexAnimation".to_string());
691        assert_eq!(composition.to_class_name(), "animate-complexAnimation");
692    }
693
694    #[test]
695    fn test_custom_keyframe_display() {
696        let keyframe = CustomKeyframe::new("fadeIn".to_string());
697        let display = format!("{}", keyframe);
698        assert!(display.contains("fadeIn"));
699    }
700
701    #[test]
702    fn test_animation_composition_display() {
703        let composition = AnimationComposition::new("complexAnimation".to_string());
704        let display = format!("{}", composition);
705        assert!(display.contains("complexAnimation"));
706    }
707
708    #[test]
709    fn test_custom_keyframe_serialization() {
710        let keyframe = CustomKeyframe::new("fadeIn".to_string());
711        let serialized = serde_json::to_string(&keyframe).unwrap();
712        let deserialized: CustomKeyframe = serde_json::from_str(&serialized).unwrap();
713        assert_eq!(keyframe, deserialized);
714    }
715
716    #[test]
717    fn test_animation_composition_serialization() {
718        let composition = AnimationComposition::new("complexAnimation".to_string());
719        let serialized = serde_json::to_string(&composition).unwrap();
720        let deserialized: AnimationComposition = serde_json::from_str(&serialized).unwrap();
721        assert_eq!(composition, deserialized);
722    }
723}