Skip to main content

ringkernel_accnet/gui/
animation.rs

1//! Flow particle animation system.
2//!
3//! Animates particles flowing along edges to visualize money movement
4//! through the accounting network.
5
6use eframe::egui::Color32;
7use nalgebra::Vector2;
8use std::collections::VecDeque;
9
10/// A particle flowing along an edge.
11#[derive(Debug, Clone)]
12pub struct FlowParticle {
13    /// Source node index.
14    pub source: u16,
15    /// Target node index.
16    pub target: u16,
17    /// Progress along the edge (0.0 to 1.0).
18    pub progress: f32,
19    /// Particle speed (progress per second).
20    pub speed: f32,
21    /// Particle color.
22    pub color: Color32,
23    /// Particle size.
24    pub size: f32,
25    /// Whether this is a suspicious flow.
26    pub suspicious: bool,
27    /// Unique flow ID for grouping.
28    pub flow_id: uuid::Uuid,
29}
30
31impl FlowParticle {
32    /// Create a new particle.
33    pub fn new(source: u16, target: u16, flow_id: uuid::Uuid) -> Self {
34        Self {
35            source,
36            target,
37            progress: 0.0,
38            speed: 0.25, // Slower, more subtle movement
39            color: Color32::from_rgba_unmultiplied(100, 200, 160, 150), // Semi-transparent
40            size: 2.5,   // Smaller, more subtle
41            suspicious: false,
42            flow_id,
43        }
44    }
45
46    /// Update particle position.
47    pub fn update(&mut self, dt: f32) -> bool {
48        self.progress += self.speed * dt;
49        self.progress < 1.0
50    }
51
52    /// Get current position given source and target positions.
53    pub fn position(&self, source_pos: Vector2<f32>, target_pos: Vector2<f32>) -> Vector2<f32> {
54        // Ease-in-out interpolation for smoother motion
55        let t = self.ease_in_out(self.progress);
56        source_pos + (target_pos - source_pos) * t
57    }
58
59    /// Ease-in-out function.
60    fn ease_in_out(&self, t: f32) -> f32 {
61        if t < 0.5 {
62            2.0 * t * t
63        } else {
64            1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
65        }
66    }
67}
68
69/// Manages the particle animation system.
70pub struct ParticleSystem {
71    /// Active particles.
72    particles: VecDeque<FlowParticle>,
73    /// Maximum particles.
74    pub max_particles: usize,
75    /// Spawn rate (particles per second per active flow).
76    pub spawn_rate: f32,
77    /// Time accumulator for spawning.
78    spawn_accumulator: f32,
79    /// Pending spawns (source, target, flow_id, color, suspicious).
80    pending_spawns: Vec<(u16, u16, uuid::Uuid, Color32, bool)>,
81    /// Whether animation is paused.
82    pub paused: bool,
83}
84
85impl ParticleSystem {
86    /// Create a new particle system.
87    pub fn new(max_particles: usize) -> Self {
88        Self {
89            particles: VecDeque::with_capacity(max_particles),
90            max_particles,
91            spawn_rate: 0.8, // Reduced spawn rate for subtler animation
92            spawn_accumulator: 0.0,
93            pending_spawns: Vec::new(),
94            paused: false,
95        }
96    }
97
98    /// Queue a flow for particle spawning.
99    pub fn queue_flow(
100        &mut self,
101        source: u16,
102        target: u16,
103        flow_id: uuid::Uuid,
104        color: Color32,
105        suspicious: bool,
106    ) {
107        self.pending_spawns
108            .push((source, target, flow_id, color, suspicious));
109    }
110
111    /// Clear pending flows.
112    pub fn clear_pending(&mut self) {
113        self.pending_spawns.clear();
114    }
115
116    /// Update all particles.
117    pub fn update(&mut self, dt: f32) {
118        if self.paused {
119            return;
120        }
121
122        // Remove finished particles
123        self.particles.retain(|p| p.progress < 1.0);
124
125        // Update active particles
126        for particle in &mut self.particles {
127            particle.update(dt);
128        }
129
130        // Spawn new particles
131        self.spawn_accumulator += dt;
132        let spawn_interval = 1.0 / self.spawn_rate;
133
134        while self.spawn_accumulator >= spawn_interval && !self.pending_spawns.is_empty() {
135            self.spawn_accumulator -= spawn_interval;
136
137            // Round-robin through pending flows
138            if let Some((source, target, flow_id, color, suspicious)) = self.pending_spawns.pop() {
139                if self.particles.len() < self.max_particles {
140                    let mut particle = FlowParticle::new(source, target, flow_id);
141                    // Make particles semi-transparent for subtler effect
142                    particle.color = Color32::from_rgba_unmultiplied(
143                        color.r(),
144                        color.g(),
145                        color.b(),
146                        if suspicious { 180 } else { 100 }, // More transparent for normal flows
147                    );
148                    particle.suspicious = suspicious;
149                    particle.size = if suspicious { 4.0 } else { 2.5 }; // Smaller particles
150                    particle.speed = if suspicious { 0.2 } else { 0.25 }; // Slower movement
151                    self.particles.push_back(particle);
152                }
153
154                // Re-add to queue for continuous spawning
155                self.pending_spawns
156                    .insert(0, (source, target, flow_id, color, suspicious));
157            }
158        }
159    }
160
161    /// Get active particles.
162    pub fn particles(&self) -> impl Iterator<Item = &FlowParticle> {
163        self.particles.iter()
164    }
165
166    /// Get particle count.
167    pub fn count(&self) -> usize {
168        self.particles.len()
169    }
170
171    /// Clear all particles.
172    pub fn clear(&mut self) {
173        self.particles.clear();
174        self.pending_spawns.clear();
175    }
176
177    /// Spawn a burst of particles for a specific flow.
178    pub fn burst(
179        &mut self,
180        source: u16,
181        target: u16,
182        flow_id: uuid::Uuid,
183        color: Color32,
184        count: usize,
185    ) {
186        for i in 0..count {
187            if self.particles.len() >= self.max_particles {
188                break;
189            }
190
191            let mut particle = FlowParticle::new(source, target, flow_id);
192            particle.color = color;
193            particle.progress = i as f32 * 0.1; // Stagger them
194            particle.speed = 0.3 + (i as f32 * 0.05); // Vary speed
195            self.particles.push_back(particle);
196        }
197    }
198}
199
200impl Default for ParticleSystem {
201    fn default() -> Self {
202        Self::new(300) // Reduced max particles for subtler effect
203    }
204}
205
206/// Trail effect for particles.
207#[derive(Debug, Clone)]
208pub struct ParticleTrail {
209    /// Trail positions.
210    positions: VecDeque<Vector2<f32>>,
211    /// Maximum trail length.
212    max_length: usize,
213    /// Trail color.
214    pub color: Color32,
215}
216
217impl ParticleTrail {
218    /// Create a new trail.
219    pub fn new(max_length: usize, color: Color32) -> Self {
220        Self {
221            positions: VecDeque::with_capacity(max_length),
222            max_length,
223            color,
224        }
225    }
226
227    /// Add a position to the trail.
228    pub fn push(&mut self, position: Vector2<f32>) {
229        self.positions.push_front(position);
230        while self.positions.len() > self.max_length {
231            self.positions.pop_back();
232        }
233    }
234
235    /// Get trail positions with alpha values.
236    pub fn points(&self) -> impl Iterator<Item = (Vector2<f32>, f32)> + '_ {
237        let len = self.positions.len() as f32;
238        self.positions.iter().enumerate().map(move |(i, &pos)| {
239            let alpha = 1.0 - (i as f32 / len);
240            (pos, alpha)
241        })
242    }
243
244    /// Clear the trail.
245    pub fn clear(&mut self) {
246        self.positions.clear();
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_particle_creation() {
256        let particle = FlowParticle::new(0, 1, uuid::Uuid::new_v4());
257        assert_eq!(particle.progress, 0.0);
258        assert_eq!(particle.source, 0);
259        assert_eq!(particle.target, 1);
260    }
261
262    #[test]
263    fn test_particle_update() {
264        let mut particle = FlowParticle::new(0, 1, uuid::Uuid::new_v4());
265        particle.speed = 1.0;
266        assert!(particle.update(0.5));
267        assert_eq!(particle.progress, 0.5);
268        assert!(!particle.update(0.6));
269    }
270
271    #[test]
272    fn test_particle_system() {
273        let mut system = ParticleSystem::new(100);
274        system.spawn_rate = 2.0; // Faster for test
275        system.queue_flow(0, 1, uuid::Uuid::new_v4(), Color32::WHITE, false);
276        system.update(1.0);
277        assert!(system.count() > 0);
278    }
279}