tailwind_rs_core/utilities/
animations.rs1use crate::classes::ClassBuilder;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Animation {
13 None,
15 Spin,
17 Ping,
19 Pulse,
21 Bounce,
23 FadeIn,
25 FadeOut,
27 SlideInLeft,
29 SlideInRight,
31 SlideInTop,
33 SlideInBottom,
35 ZoomIn,
37 ZoomOut,
39 Wobble,
41 Shake,
43 Flip,
45 Heartbeat,
47}
48
49impl Animation {
50 pub fn to_class_name(&self) -> String {
51 match self {
52 Animation::None => "none".to_string(),
53 Animation::Spin => "spin".to_string(),
54 Animation::Ping => "ping".to_string(),
55 Animation::Pulse => "pulse".to_string(),
56 Animation::Bounce => "bounce".to_string(),
57 Animation::FadeIn => "fade-in".to_string(),
58 Animation::FadeOut => "fade-out".to_string(),
59 Animation::SlideInLeft => "slide-in-left".to_string(),
60 Animation::SlideInRight => "slide-in-right".to_string(),
61 Animation::SlideInTop => "slide-in-top".to_string(),
62 Animation::SlideInBottom => "slide-in-bottom".to_string(),
63 Animation::ZoomIn => "zoom-in".to_string(),
64 Animation::ZoomOut => "zoom-out".to_string(),
65 Animation::Wobble => "wobble".to_string(),
66 Animation::Shake => "shake".to_string(),
67 Animation::Flip => "flip".to_string(),
68 Animation::Heartbeat => "heartbeat".to_string(),
69 }
70 }
71
72 pub fn to_css_value(&self) -> String {
73 match self {
74 Animation::None => "none".to_string(),
75 Animation::Spin => "spin 1s linear infinite".to_string(),
76 Animation::Ping => "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite".to_string(),
77 Animation::Pulse => "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite".to_string(),
78 Animation::Bounce => "bounce 1s infinite".to_string(),
79 Animation::FadeIn => "fadeIn 0.5s ease-in".to_string(),
80 Animation::FadeOut => "fadeOut 0.5s ease-out".to_string(),
81 Animation::SlideInLeft => "slideInLeft 0.5s ease-out".to_string(),
82 Animation::SlideInRight => "slideInRight 0.5s ease-out".to_string(),
83 Animation::SlideInTop => "slideInTop 0.5s ease-out".to_string(),
84 Animation::SlideInBottom => "slideInBottom 0.5s ease-out".to_string(),
85 Animation::ZoomIn => "zoomIn 0.5s ease-out".to_string(),
86 Animation::ZoomOut => "zoomOut 0.5s ease-in".to_string(),
87 Animation::Wobble => "wobble 1s ease-in-out".to_string(),
88 Animation::Shake => "shake 0.5s ease-in-out".to_string(),
89 Animation::Flip => "flip 1s ease-in-out".to_string(),
90 Animation::Heartbeat => "heartbeat 1.5s ease-in-out infinite".to_string(),
91 }
92 }
93
94 pub fn all_values() -> Vec<Animation> {
96 vec![
97 Animation::None,
98 Animation::Spin,
99 Animation::Ping,
100 Animation::Pulse,
101 Animation::Bounce,
102 Animation::FadeIn,
103 Animation::FadeOut,
104 Animation::SlideInLeft,
105 Animation::SlideInRight,
106 Animation::SlideInTop,
107 Animation::SlideInBottom,
108 Animation::ZoomIn,
109 Animation::ZoomOut,
110 Animation::Wobble,
111 Animation::Shake,
112 Animation::Flip,
113 Animation::Heartbeat,
114 ]
115 }
116
117 pub fn is_infinite(&self) -> bool {
119 matches!(
120 self,
121 Animation::Spin
122 | Animation::Ping
123 | Animation::Pulse
124 | Animation::Bounce
125 | Animation::Heartbeat
126 )
127 }
128
129 pub fn duration_ms(&self) -> u32 {
131 match self {
132 Animation::None => 0,
133 Animation::Spin => 1000,
134 Animation::Ping => 1000,
135 Animation::Pulse => 2000,
136 Animation::Bounce => 1000,
137 Animation::FadeIn | Animation::FadeOut => 500,
138 Animation::SlideInLeft
139 | Animation::SlideInRight
140 | Animation::SlideInTop
141 | Animation::SlideInBottom => 500,
142 Animation::ZoomIn | Animation::ZoomOut => 500,
143 Animation::Shake => 500,
144 Animation::Wobble | Animation::Flip => 1000,
145 Animation::Heartbeat => 1500,
146 }
147 }
148}
149
150impl fmt::Display for Animation {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 write!(f, "{}", self.to_class_name())
153 }
154}
155
156pub trait AnimationUtilities {
158 fn animation(self, animation: Animation) -> Self;
159
160 fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self;
162
163 fn animation_once(self, animation: Animation) -> Self;
165
166 fn animation_repeat(self, animation: Animation, count: u32) -> Self;
168
169 fn fade_in(self) -> Self;
171
172 fn fade_out(self) -> Self;
174
175 fn slide_in_left(self) -> Self;
177
178 fn slide_in_right(self) -> Self;
180
181 fn slide_in_top(self) -> Self;
183
184 fn slide_in_bottom(self) -> Self;
186
187 fn zoom_in(self) -> Self;
189
190 fn zoom_out(self) -> Self;
192
193 fn wobble(self) -> Self;
195
196 fn shake(self) -> Self;
198
199 fn flip(self) -> Self;
201
202 fn heartbeat(self) -> Self;
204
205 fn hover_animation(self, animation: Animation) -> Self;
207
208 fn focus_animation(self, animation: Animation) -> Self;
210
211 fn animation_pause(self) -> Self;
213
214 fn animation_resume(self) -> Self;
216}
217
218impl AnimationUtilities for ClassBuilder {
219 fn animation(self, animation: Animation) -> Self {
220 self.class(format!("animate-{}", animation.to_class_name()))
221 }
222
223 fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self {
224 self.class(format!("animate-{}", animation.to_class_name()))
225 .class(format!("duration-{}", duration_ms))
226 }
227
228 fn animation_once(self, animation: Animation) -> Self {
229 self.class(format!("animate-{}", animation.to_class_name()))
230 .class("animation-iteration-count-1".to_string())
231 }
232
233 fn animation_repeat(self, animation: Animation, count: u32) -> Self {
234 self.class(format!("animate-{}", animation.to_class_name()))
235 .class(format!("animation-iteration-count-{}", count))
236 }
237
238 fn fade_in(self) -> Self {
239 self.animation(Animation::FadeIn)
240 }
241
242 fn fade_out(self) -> Self {
243 self.animation(Animation::FadeOut)
244 }
245
246 fn slide_in_left(self) -> Self {
247 self.animation(Animation::SlideInLeft)
248 }
249
250 fn slide_in_right(self) -> Self {
251 self.animation(Animation::SlideInRight)
252 }
253
254 fn slide_in_top(self) -> Self {
255 self.animation(Animation::SlideInTop)
256 }
257
258 fn slide_in_bottom(self) -> Self {
259 self.animation(Animation::SlideInBottom)
260 }
261
262 fn zoom_in(self) -> Self {
263 self.animation(Animation::ZoomIn)
264 }
265
266 fn zoom_out(self) -> Self {
267 self.animation(Animation::ZoomOut)
268 }
269
270 fn wobble(self) -> Self {
271 self.animation(Animation::Wobble)
272 }
273
274 fn shake(self) -> Self {
275 self.animation(Animation::Shake)
276 }
277
278 fn flip(self) -> Self {
279 self.animation(Animation::Flip)
280 }
281
282 fn heartbeat(self) -> Self {
283 self.animation(Animation::Heartbeat)
284 }
285
286 fn hover_animation(self, animation: Animation) -> Self {
287 self.class(format!("hover:animate-{}", animation.to_class_name()))
288 }
289
290 fn focus_animation(self, animation: Animation) -> Self {
291 self.class(format!("focus:animate-{}", animation.to_class_name()))
292 }
293
294 fn animation_pause(self) -> Self {
295 self.class("animation-play-state-paused".to_string())
296 }
297
298 fn animation_resume(self) -> Self {
299 self.class("animation-play-state-running".to_string())
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_animation_utilities() {
309 let classes = ClassBuilder::new()
310 .animation(Animation::None)
311 .animation(Animation::Spin)
312 .animation(Animation::Ping)
313 .animation(Animation::Pulse)
314 .animation(Animation::Bounce)
315 .build();
316
317 let css_classes = classes.to_css_classes();
318 assert!(css_classes.contains("animate-none"));
319 assert!(css_classes.contains("animate-spin"));
320 assert!(css_classes.contains("animate-ping"));
321 assert!(css_classes.contains("animate-pulse"));
322 assert!(css_classes.contains("animate-bounce"));
323 }
324
325 #[test]
327 fn test_week12_animation_utilities() {
328 let classes = ClassBuilder::new()
330 .animation(Animation::None)
331 .animation(Animation::Spin)
332 .animation(Animation::Ping)
333 .animation(Animation::Pulse)
334 .animation(Animation::Bounce)
335 .build();
336
337 let css_classes = classes.to_css_classes();
338
339 assert!(css_classes.contains("animate-none"));
341 assert!(css_classes.contains("animate-spin"));
342 assert!(css_classes.contains("animate-ping"));
343 assert!(css_classes.contains("animate-pulse"));
344 assert!(css_classes.contains("animate-bounce"));
345 }
346
347 #[test]
349 fn test_extended_animation_utilities() {
350 let classes = ClassBuilder::new()
351 .animation(Animation::FadeIn)
352 .animation(Animation::FadeOut)
353 .animation(Animation::SlideInLeft)
354 .animation(Animation::SlideInRight)
355 .animation(Animation::SlideInTop)
356 .animation(Animation::SlideInBottom)
357 .animation(Animation::ZoomIn)
358 .animation(Animation::ZoomOut)
359 .animation(Animation::Wobble)
360 .animation(Animation::Shake)
361 .animation(Animation::Flip)
362 .animation(Animation::Heartbeat)
363 .build();
364
365 let css_classes = classes.to_css_classes();
366 assert!(css_classes.contains("animate-fade-in"));
367 assert!(css_classes.contains("animate-fade-out"));
368 assert!(css_classes.contains("animate-slide-in-left"));
369 assert!(css_classes.contains("animate-slide-in-right"));
370 assert!(css_classes.contains("animate-slide-in-top"));
371 assert!(css_classes.contains("animate-slide-in-bottom"));
372 assert!(css_classes.contains("animate-zoom-in"));
373 assert!(css_classes.contains("animate-zoom-out"));
374 assert!(css_classes.contains("animate-wobble"));
375 assert!(css_classes.contains("animate-shake"));
376 assert!(css_classes.contains("animate-flip"));
377 assert!(css_classes.contains("animate-heartbeat"));
378 }
379
380 #[test]
382 fn test_convenience_animation_methods() {
383 let classes = ClassBuilder::new()
384 .fade_in()
385 .fade_out()
386 .slide_in_left()
387 .slide_in_right()
388 .slide_in_top()
389 .slide_in_bottom()
390 .zoom_in()
391 .zoom_out()
392 .wobble()
393 .shake()
394 .flip()
395 .heartbeat()
396 .build();
397
398 let css_classes = classes.to_css_classes();
399 assert!(css_classes.contains("animate-fade-in"));
400 assert!(css_classes.contains("animate-fade-out"));
401 assert!(css_classes.contains("animate-slide-in-left"));
402 assert!(css_classes.contains("animate-slide-in-right"));
403 assert!(css_classes.contains("animate-slide-in-top"));
404 assert!(css_classes.contains("animate-slide-in-bottom"));
405 assert!(css_classes.contains("animate-zoom-in"));
406 assert!(css_classes.contains("animate-zoom-out"));
407 assert!(css_classes.contains("animate-wobble"));
408 assert!(css_classes.contains("animate-shake"));
409 assert!(css_classes.contains("animate-flip"));
410 assert!(css_classes.contains("animate-heartbeat"));
411 }
412
413 #[test]
415 fn test_animation_with_duration() {
416 let classes = ClassBuilder::new()
417 .animation_with_duration(Animation::FadeIn, 1000)
418 .build();
419
420 let css_classes = classes.to_css_classes();
421 assert!(css_classes.contains("animate-fade-in"));
422 assert!(css_classes.contains("duration-1000"));
423 }
424
425 #[test]
427 fn test_animation_repetition_controls() {
428 let classes = ClassBuilder::new()
429 .animation_once(Animation::Bounce)
430 .animation_repeat(Animation::Shake, 3)
431 .build();
432
433 let css_classes = classes.to_css_classes();
434 assert!(css_classes.contains("animate-bounce"));
435 assert!(css_classes.contains("animation-iteration-count-1"));
436 assert!(css_classes.contains("animate-shake"));
437 assert!(css_classes.contains("animation-iteration-count-3"));
438 }
439
440 #[test]
442 fn test_hover_focus_animations() {
443 let classes = ClassBuilder::new()
444 .hover_animation(Animation::Bounce)
445 .focus_animation(Animation::Pulse)
446 .build();
447
448 let css_classes = classes.to_css_classes();
449 assert!(css_classes.contains("hover:animate-bounce"));
450 assert!(css_classes.contains("focus:animate-pulse"));
451 }
452
453 #[test]
455 fn test_animation_play_state() {
456 let classes = ClassBuilder::new()
457 .animation_pause()
458 .animation_resume()
459 .build();
460
461 let css_classes = classes.to_css_classes();
462 assert!(css_classes.contains("animation-play-state-paused"));
463 assert!(css_classes.contains("animation-play-state-running"));
464 }
465
466 #[test]
468 fn test_animation_properties() {
469 let all_animations = Animation::all_values();
471 assert_eq!(all_animations.len(), 17);
472 assert!(all_animations.contains(&Animation::None));
473 assert!(all_animations.contains(&Animation::FadeIn));
474 assert!(all_animations.contains(&Animation::Heartbeat));
475
476 assert!(Animation::Spin.is_infinite());
478 assert!(Animation::Heartbeat.is_infinite());
479 assert!(!Animation::FadeIn.is_infinite());
480 assert!(!Animation::ZoomOut.is_infinite());
481
482 assert_eq!(Animation::None.duration_ms(), 0);
484 assert_eq!(Animation::FadeIn.duration_ms(), 500);
485 assert_eq!(Animation::Spin.duration_ms(), 1000);
486 assert_eq!(Animation::Heartbeat.duration_ms(), 1500);
487 assert_eq!(Animation::Pulse.duration_ms(), 2000);
488 }
489
490 #[test]
492 fn test_animation_css_values() {
493 assert_eq!(Animation::None.to_css_value(), "none");
494 assert_eq!(Animation::FadeIn.to_css_value(), "fadeIn 0.5s ease-in");
495 assert_eq!(
496 Animation::SlideInLeft.to_css_value(),
497 "slideInLeft 0.5s ease-out"
498 );
499 assert_eq!(Animation::Wobble.to_css_value(), "wobble 1s ease-in-out");
500 assert_eq!(
501 Animation::Heartbeat.to_css_value(),
502 "heartbeat 1.5s ease-in-out infinite"
503 );
504 }
505
506 #[test]
508 fn test_animation_class_names() {
509 assert_eq!(Animation::FadeIn.to_class_name(), "fade-in");
510 assert_eq!(Animation::SlideInLeft.to_class_name(), "slide-in-left");
511 assert_eq!(Animation::ZoomOut.to_class_name(), "zoom-out");
512 assert_eq!(Animation::Heartbeat.to_class_name(), "heartbeat");
513 }
514
515 #[test]
517 fn test_comprehensive_animation_system() {
518 let classes = ClassBuilder::new()
519 .animation(Animation::Spin)
521 .animation(Animation::Bounce)
522 .fade_in()
524 .slide_in_right()
525 .zoom_in()
526 .hover_animation(Animation::Shake)
528 .focus_animation(Animation::Wobble)
529 .animation_once(Animation::Flip)
531 .animation_with_duration(Animation::Heartbeat, 2000)
532 .animation_pause()
533 .build();
534
535 let css_classes = classes.to_css_classes();
536
537 assert!(css_classes.contains("animate-spin"));
539 assert!(css_classes.contains("animate-bounce"));
540
541 assert!(css_classes.contains("animate-fade-in"));
543 assert!(css_classes.contains("animate-slide-in-right"));
544 assert!(css_classes.contains("animate-zoom-in"));
545
546 assert!(css_classes.contains("hover:animate-shake"));
548 assert!(css_classes.contains("focus:animate-wobble"));
549
550 assert!(css_classes.contains("animate-flip"));
552 assert!(css_classes.contains("animation-iteration-count-1"));
553 assert!(css_classes.contains("animate-heartbeat"));
554 assert!(css_classes.contains("duration-2000"));
555 assert!(css_classes.contains("animation-play-state-paused"));
556 }
557}