1use crate::widget_id::WidgetId;
32use ahash::HashMap;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum AnimatableProperty {
37 Opacity,
39 PositionX,
41 PositionY,
43 Width,
45 Height,
47 Rotation,
49 ScaleX,
51 ScaleY,
53 ColorR,
55 ColorG,
57 ColorB,
59 ColorA,
61 BorderRadius,
63 Padding,
65 TranslateX,
67 TranslateY,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73pub enum EasingFunction {
74 Linear,
76 EaseIn,
78 EaseOut,
80 EaseInOut,
82 Bounce,
84 Elastic,
86 QuadIn,
88 QuadOut,
90 QuadInOut,
92 CubicIn,
94 CubicOut,
96 CubicInOut,
98}
99
100impl EasingFunction {
101 pub fn apply(&self, t: f32) -> f32 {
103 let t = t.clamp(0.0, 1.0);
104
105 match self {
106 EasingFunction::Linear => t,
107 EasingFunction::EaseIn => t * t,
108 EasingFunction::EaseOut => t * (2.0 - t),
109 EasingFunction::EaseInOut => {
110 if t < 0.5 {
111 2.0 * t * t
112 } else {
113 -1.0 + (4.0 - 2.0 * t) * t
114 }
115 }
116 EasingFunction::Bounce => {
117 if t < 1.0 / 2.75 {
118 7.5625 * t * t
119 } else if t < 2.0 / 2.75 {
120 let t = t - 1.5 / 2.75;
121 7.5625 * t * t + 0.75
122 } else if t < 2.5 / 2.75 {
123 let t = t - 2.25 / 2.75;
124 7.5625 * t * t + 0.9375
125 } else {
126 let t = t - 2.625 / 2.75;
127 7.5625 * t * t + 0.984375
128 }
129 }
130 EasingFunction::Elastic => {
131 if t == 0.0 || t == 1.0 {
132 t
133 } else {
134 let p = 0.3;
135 let s = p / 4.0;
136 let t = t - 1.0;
137 -(2.0f32.powf(10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin())
138 }
139 }
140 EasingFunction::QuadIn => t * t,
141 EasingFunction::QuadOut => t * (2.0 - t),
142 EasingFunction::QuadInOut => {
143 if t < 0.5 {
144 2.0 * t * t
145 } else {
146 -1.0 + (4.0 - 2.0 * t) * t
147 }
148 }
149 EasingFunction::CubicIn => t * t * t,
150 EasingFunction::CubicOut => {
151 let t = t - 1.0;
152 t * t * t + 1.0
153 }
154 EasingFunction::CubicInOut => {
155 let t = t * 2.0;
156 if t < 1.0 {
157 0.5 * t * t * t
158 } else {
159 let t = t - 2.0;
160 0.5 * (t * t * t + 2.0)
161 }
162 }
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq)]
169pub enum AnimationState {
170 Running,
172 Paused,
174 Completed,
176}
177
178#[derive(Debug, Clone)]
180pub struct Animation {
181 property: AnimatableProperty,
183 from: f32,
185 to: f32,
187 duration: f32,
189 elapsed: f32,
191 easing: EasingFunction,
193 state: AnimationState,
195 looping: bool,
197 yoyo: bool,
199 direction: f32,
201 delay: f32,
203 delay_elapsed: f32,
205}
206
207impl Animation {
208 pub fn new(property: AnimatableProperty) -> Self {
210 Self {
211 property,
212 from: 0.0,
213 to: 1.0,
214 duration: 1.0,
215 elapsed: 0.0,
216 easing: EasingFunction::Linear,
217 state: AnimationState::Running,
218 looping: false,
219 yoyo: false,
220 direction: 1.0,
221 delay: 0.0,
222 delay_elapsed: 0.0,
223 }
224 }
225
226 pub fn from(mut self, value: f32) -> Self {
228 self.from = value;
229 self
230 }
231
232 pub fn to(mut self, value: f32) -> Self {
234 self.to = value;
235 self
236 }
237
238 pub fn duration(mut self, duration: f32) -> Self {
240 self.duration = duration;
241 self
242 }
243
244 pub fn easing(mut self, easing: EasingFunction) -> Self {
246 self.easing = easing;
247 self
248 }
249
250 pub fn looping(mut self, looping: bool) -> Self {
252 self.looping = looping;
253 self
254 }
255
256 pub fn yoyo(mut self, yoyo: bool) -> Self {
258 self.yoyo = yoyo;
259 self
260 }
261
262 pub fn delay(mut self, delay: f32) -> Self {
264 self.delay = delay;
265 self
266 }
267
268 pub fn property(&self) -> AnimatableProperty {
270 self.property
271 }
272
273 pub fn value(&self) -> f32 {
275 if self.delay_elapsed < self.delay {
277 return self.from;
278 }
279
280 let t = (self.elapsed / self.duration).clamp(0.0, 1.0);
281 let eased_t = self.easing.apply(t);
282
283 self.from + (self.to - self.from) * eased_t
285 }
286
287 pub fn state(&self) -> AnimationState {
289 self.state
290 }
291
292 pub fn pause(&mut self) {
294 self.state = AnimationState::Paused;
295 }
296
297 pub fn resume(&mut self) {
299 if self.state == AnimationState::Paused {
300 self.state = AnimationState::Running;
301 }
302 }
303
304 pub fn reset(&mut self) {
306 self.elapsed = 0.0;
307 self.delay_elapsed = 0.0;
308 self.state = AnimationState::Running;
309 self.direction = 1.0;
310 }
311
312 pub fn update(&mut self, delta_time: f32) -> bool {
316 if self.state != AnimationState::Running {
317 return self.state != AnimationState::Completed;
318 }
319
320 if self.delay_elapsed < self.delay {
322 self.delay_elapsed += delta_time;
323 if self.delay_elapsed < self.delay {
324 return true;
325 }
326 let _ = delta_time - (self.delay - self.delay_elapsed);
328 }
329
330 self.elapsed += delta_time * self.direction;
331
332 if self.direction > 0.0 && self.elapsed >= self.duration {
333 if self.yoyo {
334 self.direction = -1.0;
335 self.elapsed = self.duration;
336 } else if self.looping {
337 self.elapsed = 0.0;
338 } else {
339 self.elapsed = self.duration;
340 self.state = AnimationState::Completed;
341 return false;
342 }
343 } else if self.direction < 0.0 && self.elapsed <= 0.0 {
344 if self.looping {
345 self.direction = 1.0;
346 self.elapsed = 0.0;
347 } else {
348 self.elapsed = 0.0;
349 self.state = AnimationState::Completed;
350 return false;
351 }
352 }
353
354 true
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct WidgetAnimations {
361 animations: HashMap<AnimatableProperty, Animation>,
363}
364
365impl WidgetAnimations {
366 pub fn new() -> Self {
368 Self {
369 animations: HashMap::default(),
370 }
371 }
372
373 pub fn add(&mut self, animation: Animation) {
375 self.animations.insert(animation.property(), animation);
376 }
377
378 pub fn remove(&mut self, property: AnimatableProperty) {
380 self.animations.remove(&property);
381 }
382
383 pub fn get(&self, property: AnimatableProperty) -> Option<&Animation> {
385 self.animations.get(&property)
386 }
387
388 pub fn get_mut(&mut self, property: AnimatableProperty) -> Option<&mut Animation> {
390 self.animations.get_mut(&property)
391 }
392
393 pub fn update(&mut self, delta_time: f32) -> bool {
397 let mut any_running = false;
398
399 self.animations.retain(|_, animation| {
400 let running = animation.update(delta_time);
401 any_running |= running;
402 running
403 });
404
405 any_running
406 }
407
408 pub fn values(&self) -> HashMap<AnimatableProperty, f32> {
410 self.animations
411 .iter()
412 .map(|(prop, anim)| (*prop, anim.value()))
413 .collect()
414 }
415
416 pub fn is_empty(&self) -> bool {
418 self.animations.is_empty()
419 }
420
421 pub fn clear(&mut self) {
423 self.animations.clear();
424 }
425}
426
427impl Default for WidgetAnimations {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433pub struct AnimationSystem {
435 widget_animations: HashMap<WidgetId, WidgetAnimations>,
437}
438
439impl AnimationSystem {
440 pub fn new() -> Self {
442 Self {
443 widget_animations: HashMap::default(),
444 }
445 }
446
447 pub fn animate(&mut self, widget_id: WidgetId, animation: Animation) {
449 self.widget_animations
450 .entry(widget_id)
451 .or_default()
452 .add(animation);
453 }
454
455 pub fn remove_animation(&mut self, widget_id: WidgetId, property: AnimatableProperty) {
457 if let Some(animations) = self.widget_animations.get_mut(&widget_id) {
458 animations.remove(property);
459 if animations.is_empty() {
460 self.widget_animations.remove(&widget_id);
461 }
462 }
463 }
464
465 pub fn remove_widget_animations(&mut self, widget_id: WidgetId) {
467 self.widget_animations.remove(&widget_id);
468 }
469
470 pub fn get_animations(&self, widget_id: WidgetId) -> Option<&WidgetAnimations> {
472 self.widget_animations.get(&widget_id)
473 }
474
475 pub fn get_animations_mut(&mut self, widget_id: WidgetId) -> Option<&mut WidgetAnimations> {
477 self.widget_animations.get_mut(&widget_id)
478 }
479
480 pub fn update(&mut self, delta_time: f32) {
482 self.widget_animations.retain(|_, animations| {
483 animations.update(delta_time);
484 !animations.is_empty()
485 });
486 }
487
488 pub fn animated_values(&self) -> HashMap<WidgetId, HashMap<AnimatableProperty, f32>> {
492 self.widget_animations
493 .iter()
494 .map(|(id, animations)| (*id, animations.values()))
495 .collect()
496 }
497
498 pub fn clear(&mut self) {
500 self.widget_animations.clear();
501 }
502
503 pub fn widget_count(&self) -> usize {
505 self.widget_animations.len()
506 }
507
508 pub fn has_active(&self) -> bool {
510 !self.widget_animations.is_empty()
511 }
512
513 pub fn tick(&mut self, delta_time: f32, ui: &mut crate::UiCore) {
526 self.widget_animations.retain(|&widget_id, animations| {
527 animations.update(delta_time);
528
529 for (prop, value) in animations.values() {
531 match prop {
532 AnimatableProperty::Opacity => {
533 ui.update_opacity(widget_id, value);
534 }
535 AnimatableProperty::TranslateX | AnimatableProperty::PositionX => {
536 ui.update_translate_x(widget_id, value);
537 }
538 AnimatableProperty::TranslateY | AnimatableProperty::PositionY => {
539 ui.update_translate_y(widget_id, value);
540 }
541 AnimatableProperty::ScaleX => {
542 ui.update_scale_x(widget_id, value);
543 }
544 AnimatableProperty::ScaleY => {
545 ui.update_scale_y(widget_id, value);
546 }
547 _ => {}
549 }
550 }
551
552 !animations.is_empty()
553 });
554 }
555
556 pub fn tick_system(&mut self, delta_time: f32, ui: &mut crate::UiSystem) {
560 self.tick(delta_time, ui.core_mut());
561 }
562}
563
564impl Default for AnimationSystem {
565 fn default() -> Self {
566 Self::new()
567 }
568}
569
570pub fn fade_in(duration: f32) -> Animation {
572 Animation::new(AnimatableProperty::Opacity)
573 .from(0.0)
574 .to(1.0)
575 .duration(duration)
576 .easing(EasingFunction::EaseInOut)
577}
578
579pub fn fade_out(duration: f32) -> Animation {
581 Animation::new(AnimatableProperty::Opacity)
582 .from(1.0)
583 .to(0.0)
584 .duration(duration)
585 .easing(EasingFunction::EaseInOut)
586}
587
588pub fn slide_in_left(from_x: f32, to_x: f32, duration: f32) -> Animation {
590 Animation::new(AnimatableProperty::PositionX)
591 .from(from_x)
592 .to(to_x)
593 .duration(duration)
594 .easing(EasingFunction::EaseOut)
595}
596
597pub fn slide_in_top(from_y: f32, to_y: f32, duration: f32) -> Animation {
599 Animation::new(AnimatableProperty::PositionY)
600 .from(from_y)
601 .to(to_y)
602 .duration(duration)
603 .easing(EasingFunction::EaseOut)
604}
605
606pub fn scale(from: f32, to: f32, duration: f32) -> Animation {
608 Animation::new(AnimatableProperty::ScaleX)
609 .from(from)
610 .to(to)
611 .duration(duration)
612 .easing(EasingFunction::EaseInOut)
613}
614
615pub fn bounce(duration: f32) -> Animation {
617 Animation::new(AnimatableProperty::ScaleY)
618 .from(1.0)
619 .to(1.2)
620 .duration(duration)
621 .easing(EasingFunction::Bounce)
622 .yoyo(true)
623 .looping(true)
624}
625
626pub fn translate_x(from: f32, to: f32, duration: f32) -> Animation {
628 Animation::new(AnimatableProperty::TranslateX)
629 .from(from)
630 .to(to)
631 .duration(duration)
632 .easing(EasingFunction::EaseOut)
633}
634
635pub fn translate_y(from: f32, to: f32, duration: f32) -> Animation {
637 Animation::new(AnimatableProperty::TranslateY)
638 .from(from)
639 .to(to)
640 .duration(duration)
641 .easing(EasingFunction::EaseOut)
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647
648 #[test]
649 fn test_linear_easing() {
650 let easing = EasingFunction::Linear;
651 assert_eq!(easing.apply(0.0), 0.0);
652 assert_eq!(easing.apply(0.5), 0.5);
653 assert_eq!(easing.apply(1.0), 1.0);
654 }
655
656 #[test]
657 fn test_animation_update() {
658 let mut anim = Animation::new(AnimatableProperty::Opacity)
659 .from(0.0)
660 .to(1.0)
661 .duration(1.0);
662
663 assert_eq!(anim.value(), 0.0);
665
666 assert!(anim.update(0.5));
668 assert!((anim.value() - 0.5).abs() < 0.01);
669
670 assert!(!anim.update(0.5));
672 assert_eq!(anim.value(), 1.0);
673 assert_eq!(anim.state(), AnimationState::Completed);
674 }
675
676 #[test]
677 fn test_animation_system() {
678 let mut system = AnimationSystem::new();
679 let widget_id = WidgetId::new("test_widget");
680
681 system.animate(widget_id, fade_in(1.0));
682
683 assert_eq!(system.widget_count(), 1);
684
685 system.update(1.0);
686
687 assert_eq!(system.widget_count(), 0);
689 }
690
691 #[test]
692 fn test_looping_animation() {
693 let mut anim = Animation::new(AnimatableProperty::Opacity)
694 .from(0.0)
695 .to(1.0)
696 .duration(1.0)
697 .looping(true);
698
699 anim.update(1.0);
701 assert_eq!(anim.state(), AnimationState::Running);
702 assert_eq!(anim.value(), 0.0); anim.update(0.5);
706 assert!((anim.value() - 0.5).abs() < 0.01);
707 }
708
709 #[test]
710 fn test_yoyo_animation() {
711 let mut anim = Animation::new(AnimatableProperty::Opacity)
712 .from(0.0)
713 .to(1.0)
714 .duration(1.0)
715 .yoyo(true);
716
717 assert!(anim.update(1.0));
719 assert!((anim.value() - 1.0).abs() < 0.01);
720 assert_eq!(anim.state(), AnimationState::Running);
721
722 assert!(!anim.update(1.0));
724 assert!((anim.value() - 0.0).abs() < 0.01);
725 assert_eq!(anim.state(), AnimationState::Completed);
726 }
727
728 #[test]
729 fn test_translate_animation_helpers() {
730 let anim_x = translate_x(-100.0, 0.0, 0.5);
731 assert_eq!(anim_x.property(), AnimatableProperty::TranslateX);
732 assert_eq!(anim_x.value(), -100.0); let anim_y = translate_y(-50.0, 0.0, 0.3);
735 assert_eq!(anim_y.property(), AnimatableProperty::TranslateY);
736 assert_eq!(anim_y.value(), -50.0); }
738
739 #[test]
740 fn test_has_active() {
741 let mut system = AnimationSystem::new();
742 assert!(!system.has_active());
743
744 let widget_id = WidgetId::new("test");
745 system.animate(widget_id, fade_in(1.0));
746 assert!(system.has_active());
747
748 system.update(2.0); assert!(!system.has_active());
750 }
751
752 #[test]
753 fn test_easing_boundary_values() {
754 let easings = [
756 EasingFunction::Linear,
757 EasingFunction::EaseIn,
758 EasingFunction::EaseOut,
759 EasingFunction::EaseInOut,
760 EasingFunction::QuadIn,
761 EasingFunction::QuadOut,
762 EasingFunction::QuadInOut,
763 EasingFunction::CubicIn,
764 EasingFunction::CubicOut,
765 EasingFunction::CubicInOut,
766 ];
767 for easing in &easings {
768 assert!(
769 (easing.apply(0.0) - 0.0).abs() < 0.001,
770 "{easing:?} failed at t=0"
771 );
772 assert!(
773 (easing.apply(1.0) - 1.0).abs() < 0.001,
774 "{easing:?} failed at t=1"
775 );
776 }
777 }
778}