Skip to main content

egui_map/map/
animation.rs

1//! Built-in animation effects, used when no custom
2//! [`NodeTemplate`](crate::map::objects::NodeTemplate) is installed.
3
4use crate::map::{Error, objects::RawPoint};
5use egui::{Color32, Painter, Shape, epaint::CircleShape};
6use std::time::Instant;
7
8/// Factory for the default node notification animation.
9pub(crate) struct Animation {}
10
11impl Animation {
12    /// Draws one frame of an expanding, fading circle centered on `center`.
13    ///
14    /// The pulse starts at `initial_time` and plays for about 3.5 seconds,
15    /// growing in radius while its transparency decreases. Returns `Ok(true)`
16    /// while the animation is still playing (the caller should request a
17    /// repaint) and `Ok(false)` once it has finished, so the caller can drop
18    /// the notification.
19    pub(crate) fn pulse(
20        painter: &Painter,
21        center: RawPoint,
22        zoom: f32,
23        initial_time: Instant,
24        color: Color32,
25    ) -> Result<bool, Error> {
26        let current_instant = Instant::now();
27        let mut result = false;
28        let time_diff = current_instant.duration_since(initial_time);
29        let secs_played = time_diff.as_secs_f32();
30        let radius = (4.00 + (40.00 * secs_played)) * zoom;
31        let mut transparency = 1.00 - (secs_played / 3.50).abs();
32        if transparency < 0.00 {
33            transparency = 0.00;
34        }
35        let corrected_color = Color32::from_rgba_unmultiplied(
36            color.r(),
37            color.g(),
38            color.b(),
39            (255.00 * transparency).round() as u8,
40        );
41        let circle = Shape::Circle(CircleShape::filled(center.into(), radius, corrected_color));
42        painter.extend(vec![circle]);
43        if secs_played < 3.50 {
44            result = true;
45        }
46        Ok(result)
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use egui::{Context, LayerId, Pos2, Rect, Vec2};
54    use std::time::Duration;
55
56    fn headless_painter() -> Painter {
57        Painter::new(
58            Context::default(),
59            LayerId::background(),
60            Rect::from_min_size(Pos2::ZERO, Vec2::new(100.0, 100.0)),
61        )
62    }
63
64    #[test]
65    fn pulse_returns_true_while_running() {
66        let painter = headless_painter();
67        let result = Animation::pulse(
68            &painter,
69            RawPoint::default(),
70            1.0,
71            Instant::now(),
72            Color32::RED,
73        );
74        assert!(matches!(result, Ok(true)));
75    }
76
77    #[test]
78    fn pulse_returns_false_when_finished() {
79        let painter = headless_painter();
80        // la animación dura 3.5 segundos; 4 segundos después ya terminó
81        let initial_time = Instant::now() - Duration::from_secs(4);
82        let result = Animation::pulse(
83            &painter,
84            RawPoint::default(),
85            1.0,
86            initial_time,
87            Color32::RED,
88        );
89        assert!(matches!(result, Ok(false)));
90    }
91}