Skip to main content

proof_engine/graph/
dynamics.rs

1use glam::Vec2;
2use std::collections::HashMap;
3use super::graph_core::{NodeId, EdgeId};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum AnimationKind {
7    AddNode,
8    RemoveNode,
9    AddEdge,
10    RemoveEdge,
11}
12
13#[derive(Debug, Clone)]
14pub struct NodeAnimation {
15    pub kind: AnimationKind,
16    pub node_id: NodeId,
17    pub start_pos: Vec2,
18    pub target_pos: Vec2,
19    pub progress: f32,
20    pub duration: f32,
21    pub scale: f32,
22    pub alpha: f32,
23}
24
25#[derive(Debug, Clone)]
26pub struct EdgeAnimation {
27    pub kind: AnimationKind,
28    pub edge_id: EdgeId,
29    pub from: NodeId,
30    pub to: NodeId,
31    pub progress: f32,
32    pub duration: f32,
33    pub draw_progress: f32, // 0..1 how much of the edge is drawn
34    pub alpha: f32,
35}
36
37#[derive(Debug, Clone)]
38pub struct AnimationState {
39    pub node_positions: HashMap<NodeId, Vec2>,
40    pub node_scales: HashMap<NodeId, f32>,
41    pub node_alphas: HashMap<NodeId, f32>,
42    pub edge_draw_progress: HashMap<EdgeId, f32>,
43    pub edge_alphas: HashMap<EdgeId, f32>,
44}
45
46impl AnimationState {
47    pub fn new() -> Self {
48        Self {
49            node_positions: HashMap::new(),
50            node_scales: HashMap::new(),
51            node_alphas: HashMap::new(),
52            edge_draw_progress: HashMap::new(),
53            edge_alphas: HashMap::new(),
54        }
55    }
56}
57
58pub struct GraphAnimator {
59    node_anims: Vec<NodeAnimation>,
60    edge_anims: Vec<EdgeAnimation>,
61    default_duration: f32,
62    completed_removes: Vec<(AnimationKind, u32)>, // (kind, id)
63}
64
65impl GraphAnimator {
66    pub fn new() -> Self {
67        Self {
68            node_anims: Vec::new(),
69            edge_anims: Vec::new(),
70            default_duration: 0.5,
71            completed_removes: Vec::new(),
72        }
73    }
74
75    pub fn with_duration(mut self, duration: f32) -> Self {
76        self.default_duration = duration;
77        self
78    }
79
80    /// Animate a node appearing: starts at center (0,0), scales from 0 to 1, tweens to target.
81    pub fn animate_add_node(&mut self, id: NodeId, target_pos: Vec2) {
82        self.node_anims.push(NodeAnimation {
83            kind: AnimationKind::AddNode,
84            node_id: id,
85            start_pos: Vec2::ZERO,
86            target_pos,
87            progress: 0.0,
88            duration: self.default_duration,
89            scale: 0.0,
90            alpha: 0.0,
91        });
92    }
93
94    /// Animate a node disappearing: fades out and scales to 0.
95    pub fn animate_remove_node(&mut self, id: NodeId, current_pos: Vec2) {
96        self.node_anims.push(NodeAnimation {
97            kind: AnimationKind::RemoveNode,
98            node_id: id,
99            start_pos: current_pos,
100            target_pos: current_pos,
101            progress: 0.0,
102            duration: self.default_duration,
103            scale: 1.0,
104            alpha: 1.0,
105        });
106    }
107
108    /// Animate an edge being drawn from one end to the other.
109    pub fn animate_add_edge(&mut self, id: EdgeId, from: NodeId, to: NodeId) {
110        self.edge_anims.push(EdgeAnimation {
111            kind: AnimationKind::AddEdge,
112            edge_id: id,
113            from,
114            to,
115            progress: 0.0,
116            duration: self.default_duration,
117            draw_progress: 0.0,
118            alpha: 1.0,
119        });
120    }
121
122    /// Animate an edge fading out.
123    pub fn animate_remove_edge(&mut self, id: EdgeId, from: NodeId, to: NodeId) {
124        self.edge_anims.push(EdgeAnimation {
125            kind: AnimationKind::RemoveEdge,
126            edge_id: id,
127            from,
128            to,
129            progress: 0.0,
130            duration: self.default_duration,
131            draw_progress: 1.0,
132            alpha: 1.0,
133        });
134    }
135
136    /// Advance all animations by dt seconds.
137    pub fn tick(&mut self, dt: f32) -> AnimationState {
138        let mut state = AnimationState::new();
139        self.completed_removes.clear();
140
141        // Update node animations
142        for anim in &mut self.node_anims {
143            anim.progress = (anim.progress + dt / anim.duration).min(1.0);
144            let t = ease_out_cubic(anim.progress);
145
146            match anim.kind {
147                AnimationKind::AddNode => {
148                    let pos = anim.start_pos.lerp(anim.target_pos, t);
149                    anim.scale = t;
150                    anim.alpha = t;
151                    state.node_positions.insert(anim.node_id, pos);
152                    state.node_scales.insert(anim.node_id, anim.scale);
153                    state.node_alphas.insert(anim.node_id, anim.alpha);
154                }
155                AnimationKind::RemoveNode => {
156                    anim.scale = 1.0 - t;
157                    anim.alpha = 1.0 - t;
158                    state.node_positions.insert(anim.node_id, anim.start_pos);
159                    state.node_scales.insert(anim.node_id, anim.scale);
160                    state.node_alphas.insert(anim.node_id, anim.alpha);
161                }
162                _ => {}
163            }
164        }
165
166        // Update edge animations
167        for anim in &mut self.edge_anims {
168            anim.progress = (anim.progress + dt / anim.duration).min(1.0);
169            let t = ease_out_cubic(anim.progress);
170
171            match anim.kind {
172                AnimationKind::AddEdge => {
173                    anim.draw_progress = t;
174                    anim.alpha = 1.0;
175                    state.edge_draw_progress.insert(anim.edge_id, anim.draw_progress);
176                    state.edge_alphas.insert(anim.edge_id, anim.alpha);
177                }
178                AnimationKind::RemoveEdge => {
179                    anim.alpha = 1.0 - t;
180                    state.edge_draw_progress.insert(anim.edge_id, 1.0);
181                    state.edge_alphas.insert(anim.edge_id, anim.alpha);
182                }
183                _ => {}
184            }
185        }
186
187        // Remove completed animations
188        self.node_anims.retain(|a| a.progress < 1.0);
189        self.edge_anims.retain(|a| a.progress < 1.0);
190
191        state
192    }
193
194    /// Returns true if any animations are still running.
195    pub fn is_animating(&self) -> bool {
196        !self.node_anims.is_empty() || !self.edge_anims.is_empty()
197    }
198
199    /// Number of active animations.
200    pub fn active_count(&self) -> usize {
201        self.node_anims.len() + self.edge_anims.len()
202    }
203}
204
205/// Cubic ease-out: decelerating to zero velocity.
206fn ease_out_cubic(t: f32) -> f32 {
207    let t1 = t - 1.0;
208    t1 * t1 * t1 + 1.0
209}
210
211/// Cubic ease-in-out.
212fn ease_in_out_cubic(t: f32) -> f32 {
213    if t < 0.5 {
214        4.0 * t * t * t
215    } else {
216        let t1 = -2.0 * t + 2.0;
217        1.0 - t1 * t1 * t1 / 2.0
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_add_node_animation() {
227        let mut animator = GraphAnimator::new().with_duration(1.0);
228        let nid = NodeId(0);
229        animator.animate_add_node(nid, Vec2::new(100.0, 200.0));
230
231        assert!(animator.is_animating());
232
233        // Tick halfway
234        let state = animator.tick(0.5);
235        let pos = state.node_positions[&nid];
236        let scale = state.node_scales[&nid];
237        assert!(pos.x > 0.0 && pos.x < 100.0);
238        assert!(scale > 0.0 && scale < 1.0);
239
240        // Tick to completion
241        let state = animator.tick(0.6);
242        assert!(!animator.is_animating());
243    }
244
245    #[test]
246    fn test_remove_node_animation() {
247        let mut animator = GraphAnimator::new().with_duration(1.0);
248        let nid = NodeId(0);
249        animator.animate_remove_node(nid, Vec2::new(50.0, 50.0));
250
251        let state = animator.tick(0.5);
252        let alpha = state.node_alphas[&nid];
253        assert!(alpha > 0.0 && alpha < 1.0);
254
255        let state = animator.tick(0.6);
256        assert!(!animator.is_animating());
257    }
258
259    #[test]
260    fn test_add_edge_animation() {
261        let mut animator = GraphAnimator::new().with_duration(1.0);
262        let eid = EdgeId(0);
263        animator.animate_add_edge(eid, NodeId(0), NodeId(1));
264
265        let state = animator.tick(0.5);
266        let draw = state.edge_draw_progress[&eid];
267        assert!(draw > 0.0 && draw < 1.0);
268    }
269
270    #[test]
271    fn test_remove_edge_animation() {
272        let mut animator = GraphAnimator::new().with_duration(1.0);
273        let eid = EdgeId(0);
274        animator.animate_remove_edge(eid, NodeId(0), NodeId(1));
275
276        let state = animator.tick(0.5);
277        let alpha = state.edge_alphas[&eid];
278        assert!(alpha > 0.0 && alpha < 1.0);
279    }
280
281    #[test]
282    fn test_multiple_animations() {
283        let mut animator = GraphAnimator::new().with_duration(0.5);
284        animator.animate_add_node(NodeId(0), Vec2::new(10.0, 10.0));
285        animator.animate_add_node(NodeId(1), Vec2::new(20.0, 20.0));
286        animator.animate_add_edge(EdgeId(0), NodeId(0), NodeId(1));
287
288        assert_eq!(animator.active_count(), 3);
289        let state = animator.tick(0.25);
290        assert_eq!(state.node_positions.len(), 2);
291
292        let state = animator.tick(0.3);
293        assert!(!animator.is_animating());
294    }
295
296    #[test]
297    fn test_ease_out_cubic() {
298        assert!((ease_out_cubic(0.0) - 0.0).abs() < 1e-6);
299        assert!((ease_out_cubic(1.0) - 1.0).abs() < 1e-6);
300        // Monotonically increasing
301        assert!(ease_out_cubic(0.5) > ease_out_cubic(0.25));
302    }
303
304    #[test]
305    fn test_no_animations() {
306        let mut animator = GraphAnimator::new();
307        assert!(!animator.is_animating());
308        let state = animator.tick(0.1);
309        assert!(state.node_positions.is_empty());
310    }
311}