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!(self, Animation::Spin | Animation::Ping | Animation::Pulse | Animation::Bounce | Animation::Heartbeat)
120 }
121
122 pub fn duration_ms(&self) -> u32 {
124 match self {
125 Animation::None => 0,
126 Animation::Spin => 1000,
127 Animation::Ping => 1000,
128 Animation::Pulse => 2000,
129 Animation::Bounce => 1000,
130 Animation::FadeIn | Animation::FadeOut => 500,
131 Animation::SlideInLeft | Animation::SlideInRight |
132 Animation::SlideInTop | Animation::SlideInBottom => 500,
133 Animation::ZoomIn | Animation::ZoomOut => 500,
134 Animation::Shake => 500,
135 Animation::Wobble | Animation::Flip => 1000,
136 Animation::Heartbeat => 1500,
137 }
138 }
139}
140
141impl fmt::Display for Animation {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "{}", self.to_class_name())
144 }
145}
146
147pub trait AnimationUtilities {
149 fn animation(self, animation: Animation) -> Self;
150
151 fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self;
153
154 fn animation_once(self, animation: Animation) -> Self;
156
157 fn animation_repeat(self, animation: Animation, count: u32) -> Self;
159
160 fn fade_in(self) -> Self;
162
163 fn fade_out(self) -> Self;
165
166 fn slide_in_left(self) -> Self;
168
169 fn slide_in_right(self) -> Self;
171
172 fn slide_in_top(self) -> Self;
174
175 fn slide_in_bottom(self) -> Self;
177
178 fn zoom_in(self) -> Self;
180
181 fn zoom_out(self) -> Self;
183
184 fn wobble(self) -> Self;
186
187 fn shake(self) -> Self;
189
190 fn flip(self) -> Self;
192
193 fn heartbeat(self) -> Self;
195
196 fn hover_animation(self, animation: Animation) -> Self;
198
199 fn focus_animation(self, animation: Animation) -> Self;
201
202 fn animation_pause(self) -> Self;
204
205 fn animation_resume(self) -> Self;
207}
208
209impl AnimationUtilities for ClassBuilder {
210 fn animation(self, animation: Animation) -> Self {
211 self.class(format!("animate-{}", animation.to_class_name()))
212 }
213
214 fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self {
215 self.class(format!("animate-{}", animation.to_class_name()))
216 .class(format!("duration-{}", duration_ms))
217 }
218
219 fn animation_once(self, animation: Animation) -> Self {
220 self.class(format!("animate-{}", animation.to_class_name()))
221 .class("animation-iteration-count-1".to_string())
222 }
223
224 fn animation_repeat(self, animation: Animation, count: u32) -> Self {
225 self.class(format!("animate-{}", animation.to_class_name()))
226 .class(format!("animation-iteration-count-{}", count))
227 }
228
229 fn fade_in(self) -> Self {
230 self.animation(Animation::FadeIn)
231 }
232
233 fn fade_out(self) -> Self {
234 self.animation(Animation::FadeOut)
235 }
236
237 fn slide_in_left(self) -> Self {
238 self.animation(Animation::SlideInLeft)
239 }
240
241 fn slide_in_right(self) -> Self {
242 self.animation(Animation::SlideInRight)
243 }
244
245 fn slide_in_top(self) -> Self {
246 self.animation(Animation::SlideInTop)
247 }
248
249 fn slide_in_bottom(self) -> Self {
250 self.animation(Animation::SlideInBottom)
251 }
252
253 fn zoom_in(self) -> Self {
254 self.animation(Animation::ZoomIn)
255 }
256
257 fn zoom_out(self) -> Self {
258 self.animation(Animation::ZoomOut)
259 }
260
261 fn wobble(self) -> Self {
262 self.animation(Animation::Wobble)
263 }
264
265 fn shake(self) -> Self {
266 self.animation(Animation::Shake)
267 }
268
269 fn flip(self) -> Self {
270 self.animation(Animation::Flip)
271 }
272
273 fn heartbeat(self) -> Self {
274 self.animation(Animation::Heartbeat)
275 }
276
277 fn hover_animation(self, animation: Animation) -> Self {
278 self.class(format!("hover:animate-{}", animation.to_class_name()))
279 }
280
281 fn focus_animation(self, animation: Animation) -> Self {
282 self.class(format!("focus:animate-{}", animation.to_class_name()))
283 }
284
285 fn animation_pause(self) -> Self {
286 self.class("animation-play-state-paused".to_string())
287 }
288
289 fn animation_resume(self) -> Self {
290 self.class("animation-play-state-running".to_string())
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_animation_utilities() {
300 let classes = ClassBuilder::new()
301 .animation(Animation::None)
302 .animation(Animation::Spin)
303 .animation(Animation::Ping)
304 .animation(Animation::Pulse)
305 .animation(Animation::Bounce)
306 .build();
307
308 let css_classes = classes.to_css_classes();
309 assert!(css_classes.contains("animate-none"));
310 assert!(css_classes.contains("animate-spin"));
311 assert!(css_classes.contains("animate-ping"));
312 assert!(css_classes.contains("animate-pulse"));
313 assert!(css_classes.contains("animate-bounce"));
314 }
315
316 #[test]
318 fn test_week12_animation_utilities() {
319 let classes = ClassBuilder::new()
321 .animation(Animation::None)
322 .animation(Animation::Spin)
323 .animation(Animation::Ping)
324 .animation(Animation::Pulse)
325 .animation(Animation::Bounce)
326 .build();
327
328 let css_classes = classes.to_css_classes();
329
330 assert!(css_classes.contains("animate-none"));
332 assert!(css_classes.contains("animate-spin"));
333 assert!(css_classes.contains("animate-ping"));
334 assert!(css_classes.contains("animate-pulse"));
335 assert!(css_classes.contains("animate-bounce"));
336 }
337
338 #[test]
340 fn test_extended_animation_utilities() {
341 let classes = ClassBuilder::new()
342 .animation(Animation::FadeIn)
343 .animation(Animation::FadeOut)
344 .animation(Animation::SlideInLeft)
345 .animation(Animation::SlideInRight)
346 .animation(Animation::SlideInTop)
347 .animation(Animation::SlideInBottom)
348 .animation(Animation::ZoomIn)
349 .animation(Animation::ZoomOut)
350 .animation(Animation::Wobble)
351 .animation(Animation::Shake)
352 .animation(Animation::Flip)
353 .animation(Animation::Heartbeat)
354 .build();
355
356 let css_classes = classes.to_css_classes();
357 assert!(css_classes.contains("animate-fade-in"));
358 assert!(css_classes.contains("animate-fade-out"));
359 assert!(css_classes.contains("animate-slide-in-left"));
360 assert!(css_classes.contains("animate-slide-in-right"));
361 assert!(css_classes.contains("animate-slide-in-top"));
362 assert!(css_classes.contains("animate-slide-in-bottom"));
363 assert!(css_classes.contains("animate-zoom-in"));
364 assert!(css_classes.contains("animate-zoom-out"));
365 assert!(css_classes.contains("animate-wobble"));
366 assert!(css_classes.contains("animate-shake"));
367 assert!(css_classes.contains("animate-flip"));
368 assert!(css_classes.contains("animate-heartbeat"));
369 }
370
371 #[test]
373 fn test_convenience_animation_methods() {
374 let classes = ClassBuilder::new()
375 .fade_in()
376 .fade_out()
377 .slide_in_left()
378 .slide_in_right()
379 .slide_in_top()
380 .slide_in_bottom()
381 .zoom_in()
382 .zoom_out()
383 .wobble()
384 .shake()
385 .flip()
386 .heartbeat()
387 .build();
388
389 let css_classes = classes.to_css_classes();
390 assert!(css_classes.contains("animate-fade-in"));
391 assert!(css_classes.contains("animate-fade-out"));
392 assert!(css_classes.contains("animate-slide-in-left"));
393 assert!(css_classes.contains("animate-slide-in-right"));
394 assert!(css_classes.contains("animate-slide-in-top"));
395 assert!(css_classes.contains("animate-slide-in-bottom"));
396 assert!(css_classes.contains("animate-zoom-in"));
397 assert!(css_classes.contains("animate-zoom-out"));
398 assert!(css_classes.contains("animate-wobble"));
399 assert!(css_classes.contains("animate-shake"));
400 assert!(css_classes.contains("animate-flip"));
401 assert!(css_classes.contains("animate-heartbeat"));
402 }
403
404 #[test]
406 fn test_animation_with_duration() {
407 let classes = ClassBuilder::new()
408 .animation_with_duration(Animation::FadeIn, 1000)
409 .build();
410
411 let css_classes = classes.to_css_classes();
412 assert!(css_classes.contains("animate-fade-in"));
413 assert!(css_classes.contains("duration-1000"));
414 }
415
416 #[test]
418 fn test_animation_repetition_controls() {
419 let classes = ClassBuilder::new()
420 .animation_once(Animation::Bounce)
421 .animation_repeat(Animation::Shake, 3)
422 .build();
423
424 let css_classes = classes.to_css_classes();
425 assert!(css_classes.contains("animate-bounce"));
426 assert!(css_classes.contains("animation-iteration-count-1"));
427 assert!(css_classes.contains("animate-shake"));
428 assert!(css_classes.contains("animation-iteration-count-3"));
429 }
430
431 #[test]
433 fn test_hover_focus_animations() {
434 let classes = ClassBuilder::new()
435 .hover_animation(Animation::Bounce)
436 .focus_animation(Animation::Pulse)
437 .build();
438
439 let css_classes = classes.to_css_classes();
440 assert!(css_classes.contains("hover:animate-bounce"));
441 assert!(css_classes.contains("focus:animate-pulse"));
442 }
443
444 #[test]
446 fn test_animation_play_state() {
447 let classes = ClassBuilder::new()
448 .animation_pause()
449 .animation_resume()
450 .build();
451
452 let css_classes = classes.to_css_classes();
453 assert!(css_classes.contains("animation-play-state-paused"));
454 assert!(css_classes.contains("animation-play-state-running"));
455 }
456
457 #[test]
459 fn test_animation_properties() {
460 let all_animations = Animation::all_values();
462 assert_eq!(all_animations.len(), 17);
463 assert!(all_animations.contains(&Animation::None));
464 assert!(all_animations.contains(&Animation::FadeIn));
465 assert!(all_animations.contains(&Animation::Heartbeat));
466
467 assert!(Animation::Spin.is_infinite());
469 assert!(Animation::Heartbeat.is_infinite());
470 assert!(!Animation::FadeIn.is_infinite());
471 assert!(!Animation::ZoomOut.is_infinite());
472
473 assert_eq!(Animation::None.duration_ms(), 0);
475 assert_eq!(Animation::FadeIn.duration_ms(), 500);
476 assert_eq!(Animation::Spin.duration_ms(), 1000);
477 assert_eq!(Animation::Heartbeat.duration_ms(), 1500);
478 assert_eq!(Animation::Pulse.duration_ms(), 2000);
479 }
480
481 #[test]
483 fn test_animation_css_values() {
484 assert_eq!(Animation::None.to_css_value(), "none");
485 assert_eq!(Animation::FadeIn.to_css_value(), "fadeIn 0.5s ease-in");
486 assert_eq!(Animation::SlideInLeft.to_css_value(), "slideInLeft 0.5s ease-out");
487 assert_eq!(Animation::Wobble.to_css_value(), "wobble 1s ease-in-out");
488 assert_eq!(Animation::Heartbeat.to_css_value(), "heartbeat 1.5s ease-in-out infinite");
489 }
490
491 #[test]
493 fn test_animation_class_names() {
494 assert_eq!(Animation::FadeIn.to_class_name(), "fade-in");
495 assert_eq!(Animation::SlideInLeft.to_class_name(), "slide-in-left");
496 assert_eq!(Animation::ZoomOut.to_class_name(), "zoom-out");
497 assert_eq!(Animation::Heartbeat.to_class_name(), "heartbeat");
498 }
499
500 #[test]
502 fn test_comprehensive_animation_system() {
503 let classes = ClassBuilder::new()
504 .animation(Animation::Spin)
506 .animation(Animation::Bounce)
507 .fade_in()
509 .slide_in_right()
510 .zoom_in()
511 .hover_animation(Animation::Shake)
513 .focus_animation(Animation::Wobble)
514 .animation_once(Animation::Flip)
516 .animation_with_duration(Animation::Heartbeat, 2000)
517 .animation_pause()
518 .build();
519
520 let css_classes = classes.to_css_classes();
521
522 assert!(css_classes.contains("animate-spin"));
524 assert!(css_classes.contains("animate-bounce"));
525
526 assert!(css_classes.contains("animate-fade-in"));
528 assert!(css_classes.contains("animate-slide-in-right"));
529 assert!(css_classes.contains("animate-zoom-in"));
530
531 assert!(css_classes.contains("hover:animate-shake"));
533 assert!(css_classes.contains("focus:animate-wobble"));
534
535 assert!(css_classes.contains("animate-flip"));
537 assert!(css_classes.contains("animation-iteration-count-1"));
538 assert!(css_classes.contains("animate-heartbeat"));
539 assert!(css_classes.contains("duration-2000"));
540 assert!(css_classes.contains("animation-play-state-paused"));
541 }
542}