1use crate::anim::{Easing, HaloEmphasis, lerp, lerp2, lerp4};
15use crate::interner::{LabelId, NodeIndex};
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct SceneNode {
19 pub pos: [f32; 2],
20 pub radius: f32,
21 pub color: [f32; 4],
22 pub opacity: f32,
23 pub shape: u32,
24 pub label: LabelId,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct SceneEdge {
29 pub a: NodeIndex,
30 pub b: NodeIndex,
31 pub color: [f32; 4],
32 pub width: f32,
33 pub opacity: f32,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct SceneHalo {
38 pub node: NodeIndex,
39 pub emphasis: HaloEmphasis,
40}
41
42#[derive(Debug, Clone, Default, PartialEq)]
47pub struct Scene {
48 pub nodes: Vec<Option<SceneNode>>,
49 pub edges: Vec<SceneEdge>,
50 pub halos: Vec<SceneHalo>,
51}
52
53impl Scene {
54 pub fn set(&mut self, ix: NodeIndex, node: SceneNode) {
56 let i = ix as usize;
57 if i >= self.nodes.len() { self.nodes.resize(i + 1, None); }
58 self.nodes[i] = Some(node);
59 }
60 pub fn get(&self, ix: NodeIndex) -> Option<&SceneNode> {
61 self.nodes.get(ix as usize).and_then(|s| s.as_ref())
62 }
63 pub fn clear(&mut self) {
64 self.nodes.clear();
65 self.edges.clear();
66 self.halos.clear();
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct FrameNode {
75 pub index: NodeIndex,
76 pub pos: [f32; 2],
77 pub radius: f32,
78 pub color: [f32; 4],
79 pub opacity: f32,
80 pub shape: u32,
81 pub label: LabelId,
82}
83#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct FrameEdge { pub a: [f32; 2], pub b: [f32; 2], pub color: [f32; 4], pub width: f32, pub opacity: f32 }
85#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct FrameHalo { pub node: NodeIndex, pub pos: [f32; 2], pub radius: f32, pub emphasis: HaloEmphasis }
87
88#[derive(Debug, Clone, Default, PartialEq)]
90pub struct Frame {
91 pub nodes: Vec<FrameNode>,
92 pub edges: Vec<FrameEdge>,
93 pub halos: Vec<FrameHalo>,
94}
95
96pub struct Animator {
98 displayed: Scene,
99 from_buf: Scene,
102 to: Scene,
103 start_ms: f64,
104 dur_ms: f32,
105 easing: Easing,
106 animating: bool,
107 scratch: Vec<Option<SceneNode>>,
110}
111
112impl Default for Animator { fn default() -> Self { Self::new() } }
113
114impl Animator {
115 pub fn new() -> Self {
116 Self {
117 displayed: Scene::default(),
118 from_buf: Scene::default(),
119 to: Scene::default(),
120 start_ms: 0.0,
121 dur_ms: 0.0,
122 easing: Easing::Linear,
123 animating: false,
124 scratch: Vec::new(),
125 }
126 }
127
128 pub fn set_target(&mut self, target: Scene, now_ms: f64, dur_ms: f32, easing: Easing, tween: bool) {
132 let has_displayed = self.displayed.nodes.iter().any(|n| n.is_some());
133 if tween && dur_ms > 0.0 && has_displayed {
134 self.from_buf.nodes.clone_from(&self.displayed.nodes);
135 self.from_buf.edges.clear();
137 self.from_buf.halos.clear();
138 self.displayed.edges.clone_from(&target.edges);
142 self.displayed.halos.clone_from(&target.halos);
143 self.to = target;
144 self.start_ms = now_ms;
145 self.dur_ms = dur_ms;
146 self.easing = easing;
147 self.animating = true;
148 } else {
149 self.displayed.nodes.clone_from(&target.nodes);
150 self.displayed.edges.clone_from(&target.edges);
151 self.displayed.halos.clone_from(&target.halos);
152 self.to = target;
153 self.animating = false;
154 }
155 }
156
157 pub fn tick_into(&mut self, now_ms: f64, out: &mut Frame) {
160 self.advance(now_ms);
161 frame_into(&self.displayed, out);
162 }
163
164 pub fn tick_positioned_into(&mut self, now_ms: f64, pos: &[Option<[f32; 2]>], out: &mut Frame) {
181 self.advance(now_ms);
182 for (i, slot) in self.displayed.nodes.iter_mut().enumerate() {
183 if let (Some(n), Some(Some(p))) = (slot.as_mut(), pos.get(i)) {
184 n.pos = *p;
185 }
186 }
187 frame_into(&self.displayed, out);
188 }
189
190 fn advance(&mut self, now_ms: f64) {
192 if self.animating {
193 let raw = ((now_ms - self.start_ms) as f32 / self.dur_ms).clamp(0.0, 1.0);
194 let t = self.easing.apply(raw);
195 interpolate_into(&self.from_buf, &self.to, t, &mut self.scratch);
196 self.displayed.nodes.clone_from(&self.scratch);
199 if raw >= 1.0 {
200 self.displayed.nodes.clone_from(&self.to.nodes);
201 self.animating = false;
202 }
203 }
204 }
205
206 pub fn target_positions(&self) -> impl Iterator<Item = [f32; 2]> + '_ {
208 self.to.nodes.iter().flatten().map(|n| n.pos)
209 }
210
211 pub fn target_position(&self, ix: NodeIndex) -> Option<[f32; 2]> {
213 self.to.get(ix).map(|n| n.pos)
214 }
215}
216
217fn interpolate_into(from: &Scene, to: &Scene, t: f32, out: &mut Vec<Option<SceneNode>>) {
221 let len = from.nodes.len().max(to.nodes.len());
222 out.clear();
223 out.resize(len, None);
224 for (i, slot) in out.iter_mut().enumerate() {
225 let f = from.nodes.get(i).and_then(|s| s.as_ref());
226 let g = to.nodes.get(i).and_then(|s| s.as_ref());
227 *slot = match (f, g) {
228 (Some(fnode), Some(tn)) => Some(SceneNode {
229 pos: lerp2(fnode.pos, tn.pos, t),
230 radius: lerp(fnode.radius, tn.radius, t),
231 color: lerp4(fnode.color, tn.color, t),
232 opacity: lerp(fnode.opacity, tn.opacity, t),
233 shape: tn.shape,
234 label: tn.label,
235 }),
236 (None, Some(tn)) => Some(SceneNode { opacity: tn.opacity * t, ..*tn }),
237 (Some(fnode), None) if t < 1.0 => {
238 Some(SceneNode { opacity: fnode.opacity * (1.0 - t), ..*fnode })
239 }
240 _ => None,
241 };
242 }
243}
244
245fn frame_into(s: &Scene, out: &mut Frame) {
249 out.nodes.clear();
250 out.edges.clear();
251 out.halos.clear();
252 for (i, n) in s.nodes.iter().enumerate() {
253 if let Some(n) = n {
254 out.nodes.push(FrameNode {
255 index: i as NodeIndex,
256 pos: n.pos, radius: n.radius, color: n.color,
257 opacity: n.opacity, shape: n.shape, label: n.label,
258 });
259 }
260 }
261 for e in &s.edges {
262 let (Some(a), Some(b)) = (s.get(e.a), s.get(e.b)) else { continue };
263 out.edges.push(FrameEdge { a: a.pos, b: b.pos, color: e.color, width: e.width, opacity: e.opacity });
264 }
265 for h in &s.halos {
266 let Some(n) = s.get(h.node) else { continue };
267 out.halos.push(FrameHalo { node: h.node, pos: n.pos, radius: n.radius, emphasis: h.emphasis });
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn node(pos: [f32; 2], radius: f32, opacity: f32) -> SceneNode {
276 SceneNode { pos, radius, color: [1.0, 0.0, 0.0, 1.0], opacity, shape: 0, label: 0 }
277 }
278
279 fn scene_with(nodes: &[(NodeIndex, SceneNode)]) -> Scene {
280 let mut s = Scene::default();
281 for (ix, n) in nodes { s.set(*ix, *n); }
282 s
283 }
284
285 #[test]
286 fn no_transition_emits_target_immediately() {
287 let mut a = Animator::new();
288 let s = scene_with(&[(0, node([1.0, 2.0], 10.0, 1.0))]);
289 a.set_target(s, 0.0, 200.0, Easing::Linear, true); let mut f = Frame::default();
291 a.tick_into(0.0, &mut f);
292 assert_eq!(f.nodes.len(), 1);
293 assert_eq!(f.nodes[0].pos, [1.0, 2.0]);
294 }
295
296 #[test]
297 fn moved_node_interpolates_position() {
298 let mut b = Animator::new();
299 let mut f = Frame::default();
300 b.set_target(scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
301 b.tick_into(0.0, &mut f); b.set_target(scene_with(&[(0, node([10.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
303 b.tick_into(0.0, &mut f);
304 assert!((f.nodes[0].pos[0] - 0.0).abs() < 1e-4);
305 b.tick_into(100.0, &mut f);
306 assert!((f.nodes[0].pos[0] - 5.0).abs() < 1e-4);
307 b.tick_into(200.0, &mut f);
308 assert!((f.nodes[0].pos[0] - 10.0).abs() < 1e-4);
309 }
310
311 #[test]
312 fn entering_node_fades_in_exiting_fades_out() {
313 let mut a = Animator::new();
314 let mut f = Frame::default();
315 a.set_target(scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
316 a.tick_into(0.0, &mut f);
317 a.set_target(scene_with(&[(1, node([5.0, 5.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
318 a.tick_into(100.0, &mut f); let get = |f: &Frame, ix: NodeIndex| f.nodes.iter().find(|n| n.index == ix).map(|n| n.opacity);
320 assert!((get(&f, 0).unwrap() - 0.5).abs() < 1e-4, "exiting alpha");
321 assert!((get(&f, 1).unwrap() - 0.5).abs() < 1e-4, "entering alpha");
322 a.tick_into(200.0, &mut f);
323 assert_eq!(f.nodes.len(), 1);
324 assert_eq!(f.nodes[0].index, 1);
325 }
326
327 #[test]
328 fn edges_resolve_endpoints_from_interpolated_positions() {
329 let mut a = Animator::new();
330 let mut f = Frame::default();
331 let mut s0 = scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0)), (1, node([0.0, 0.0], 10.0, 1.0))]);
332 s0.edges.push(SceneEdge { a: 0, b: 1, color: [0.0; 4], width: 1.0, opacity: 1.0 });
333 a.set_target(s0, 0.0, 200.0, Easing::Linear, true);
334 a.tick_into(0.0, &mut f);
335 let mut s1 = scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0)), (1, node([10.0, 0.0], 10.0, 1.0))]);
336 s1.edges.push(SceneEdge { a: 0, b: 1, color: [0.0; 4], width: 1.0, opacity: 1.0 });
337 a.set_target(s1, 0.0, 200.0, Easing::Linear, true);
338 a.tick_into(100.0, &mut f);
339 assert_eq!(f.edges.len(), 1);
340 assert!((f.edges[0].b[0] - 5.0).abs() < 1e-4); }
342
343 #[test]
344 fn halos_resolve_position_and_radius_from_their_node() {
345 let mut a = Animator::new();
346 let mut f = Frame::default();
347 let mut s = scene_with(&[(0, node([3.0, 4.0], 12.0, 1.0))]);
348 s.halos.push(SceneHalo { node: 0, emphasis: HaloEmphasis::Primary });
349 a.set_target(s, 0.0, 0.0, Easing::Linear, false); a.tick_into(0.0, &mut f);
351 assert_eq!(f.halos.len(), 1);
352 assert_eq!(f.halos[0].pos, [3.0, 4.0]);
353 assert_eq!(f.halos[0].radius, 12.0);
354 assert_eq!(f.halos[0].node, 0);
355 }
356
357 #[test]
358 fn edges_and_halos_referencing_absent_nodes_are_skipped() {
359 let mut a = Animator::new();
360 let mut f = Frame::default();
361 let mut s = scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]);
362 s.edges.push(SceneEdge { a: 0, b: 7, color: [0.0; 4], width: 1.0, opacity: 1.0 });
365 s.halos.push(SceneHalo { node: 7, emphasis: HaloEmphasis::Primary });
366 a.set_target(s, 0.0, 0.0, Easing::Linear, false);
367 a.tick_into(0.0, &mut f);
368 assert_eq!(f.nodes.len(), 1);
369 assert!(f.edges.is_empty());
370 assert!(f.halos.is_empty());
371 }
372
373 #[test]
374 fn target_position_looks_up_one_node_by_index() {
375 let mut a = Animator::new();
376 a.set_target(scene_with(&[(0, node([3.0, 4.0], 10.0, 1.0))]), 0.0, 0.0, Easing::Linear, false);
377 assert_eq!(a.target_position(0), Some([3.0, 4.0]));
378 assert_eq!(a.target_position(9), None); }
380
381 #[test]
382 fn target_positions_reflect_destination_even_mid_transition() {
383 let mut a = Animator::new();
384 let mut f = Frame::default();
385 a.set_target(scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
386 a.tick_into(0.0, &mut f);
387 a.set_target(scene_with(&[(0, node([100.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
388 a.tick_into(0.0, &mut f);
390 assert!((f.nodes[0].pos[0] - 0.0).abs() < 1e-4);
391 let tgt: Vec<[f32; 2]> = a.target_positions().collect();
393 assert_eq!(tgt, vec![[100.0, 0.0]]);
394 }
395
396 #[test]
397 fn tween_disabled_snaps_positions() {
398 let mut a = Animator::new();
399 let mut f = Frame::default();
400 a.set_target(scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, false);
401 a.tick_into(0.0, &mut f);
402 a.set_target(scene_with(&[(0, node([10.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, false);
403 a.tick_into(0.0, &mut f);
404 assert_eq!(f.nodes[0].pos[0], 10.0); }
406
407 #[test]
408 fn tick_positioned_overrides_node_positions() {
409 let mut a = Animator::new();
410 let mut f = Frame::default();
411 let s = scene_with(&[(0, node([0.0, 0.0], 5.0, 1.0))]);
412 a.set_target(s, 0.0, 0.0, Easing::Linear, false);
413
414 let pos = vec![Some([77.0, -3.0])];
415 a.tick_positioned_into(0.0, &pos, &mut f);
416 assert_eq!(f.nodes[0].pos, [77.0, -3.0], "the simulation owns position");
417 assert_eq!(f.nodes[0].radius, 5.0, "the animator still owns appearance");
418 }
419
420 #[test]
421 fn edges_and_halos_follow_the_overridden_positions() {
422 let mut sc = scene_with(&[(0, node([0.0, 0.0], 5.0, 1.0)), (1, node([1.0, 1.0], 5.0, 1.0))]);
426 sc.edges.push(SceneEdge { a: 0, b: 1, color: [1.0; 4], width: 1.0, opacity: 1.0 });
427 sc.halos.push(SceneHalo { node: 0, emphasis: HaloEmphasis::Primary });
428
429 let mut an = Animator::new();
430 let mut f = Frame::default();
431 an.set_target(sc, 0.0, 0.0, Easing::Linear, false);
432
433 let pos = vec![Some([100.0, 0.0]), Some([200.0, 50.0])];
434 an.tick_positioned_into(0.0, &pos, &mut f);
435
436 assert_eq!(f.edges[0].a, [100.0, 0.0]);
437 assert_eq!(f.edges[0].b, [200.0, 50.0]);
438 assert_eq!(f.halos[0].pos, [100.0, 0.0]);
439 }
440
441 #[test]
442 fn a_node_absent_from_the_overrides_keeps_its_interpolated_position() {
443 let mut a = Animator::new();
447 let mut f = Frame::default();
448 a.set_target(scene_with(&[(0, node([9.0, 9.0], 5.0, 1.0))]), 0.0, 0.0, Easing::Linear, false);
449 let empty: Vec<Option<[f32; 2]>> = Vec::new();
450 a.tick_positioned_into(0.0, &empty, &mut f);
451 assert_eq!(f.nodes[0].pos, [9.0, 9.0]);
452 a.tick_positioned_into(0.0, &[None], &mut f);
453 assert_eq!(f.nodes[0].pos, [9.0, 9.0]);
454 }
455
456 #[test]
457 fn tick_positioned_still_advances_the_transition() {
458 let mut a = Animator::new();
461 let mut f = Frame::default();
462 a.set_target(scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]), 0.0, 0.0, Easing::Linear, false);
463 a.set_target(scene_with(&[(0, node([0.0, 0.0], 20.0, 1.0))]), 0.0, 100.0, Easing::Linear, true);
464
465 let pos: Vec<Option<[f32; 2]>> = Vec::new();
466 a.tick_positioned_into(50.0, &pos, &mut f);
467 assert!((f.nodes[0].radius - 15.0).abs() < 0.01, "radius should be halfway");
468 a.tick_positioned_into(100.0, &pos, &mut f);
469 assert_eq!(f.nodes[0].radius, 20.0);
470 }
471
472 #[test]
473 fn exiting_node_keeps_its_simulated_position_while_it_fades() {
474 let mut a = Animator::new();
484 let mut f = Frame::default();
485 let s0 = scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0)), (1, node([0.0, 0.0], 10.0, 1.0))]);
486 a.set_target(s0, 0.0, 0.0, Easing::Linear, false); let pos = vec![Some([500.0, 0.0]), Some([10.0, 10.0])];
489 a.tick_positioned_into(0.0, &pos, &mut f); let s1 = scene_with(&[(0, node([1.0, 1.0], 10.0, 1.0))]);
493 a.set_target(s1, 0.0, 200.0, Easing::Linear, true); let pos2 = vec![Some([501.0, 0.0]), None]; a.tick_positioned_into(0.0, &pos2, &mut f); let b = f.nodes.iter().find(|n| n.index == 1).expect("node 1 still fading");
499 assert_eq!(b.pos, [10.0, 10.0], "exiting node should stay at its last simulated position while fading, not teleport to a stale scene-authored one");
500 }
501
502 #[test]
503 fn mid_flight_retarget_starts_from_the_displayed_position_not_the_old_target() {
504 let mut a = Animator::new();
509 let mut f = Frame::default();
510 a.set_target(scene_with(&[(0, node([0.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true); a.set_target(scene_with(&[(0, node([100.0, 0.0], 10.0, 1.0))]), 0.0, 200.0, Easing::Linear, true);
512 a.tick_into(100.0, &mut f); a.set_target(scene_with(&[(0, node([300.0, 0.0], 10.0, 1.0))]), 100.0, 200.0, Easing::Linear, true);
515 a.tick_into(100.0, &mut f); assert!((f.nodes[0].pos[0] - 50.0).abs() < 1e-4, "new transition should start from the displayed position (50), not the old target (100)");
517 }
518
519 #[test]
520 fn frame_buffers_do_not_reallocate_when_warm() {
521 let mut a = Animator::new();
522 let mut s = Scene::default();
523 for i in 0..100u32 { s.set(i, node([i as f32, 0.0], 5.0, 1.0)); }
524 for i in 0..99u32 {
525 s.edges.push(SceneEdge { a: i, b: i + 1, color: [1.0; 4], width: 1.0, opacity: 1.0 });
526 }
527 a.set_target(s.clone(), 0.0, 100.0, Easing::Linear, true);
529 let mut f = Frame::default();
530 a.tick_into(0.0, &mut f);
531 let mut s2 = s.clone();
535 for n in s2.nodes.iter_mut().flatten() { n.pos[0] += 50.0; }
536 a.set_target(s2, 0.0, 1e9, Easing::Linear, true);
537 a.tick_into(1.0, &mut f); let caps = (f.nodes.capacity(), f.edges.capacity(), f.halos.capacity());
539 for t in 2..150 { a.tick_into(t as f64, &mut f); }
540 a.set_target(s, 150.0, 1e9, Easing::Linear, true);
542 for t in 151..300 { a.tick_into(t as f64, &mut f); }
543 assert_eq!(caps, (f.nodes.capacity(), f.edges.capacity(), f.halos.capacity()));
544 }
545
546 #[test]
547 fn dense_gaps_are_skipped_not_emitted() {
548 let mut s = Scene::default();
549 s.set(5, node([1.0, 2.0], 5.0, 1.0)); let mut a = Animator::new();
551 a.set_target(s, 0.0, 0.0, Easing::Linear, false);
552 let mut f = Frame::default();
553 a.tick_into(0.0, &mut f);
554 assert_eq!(f.nodes.len(), 1);
555 assert_eq!(f.nodes[0].index, 5);
556 }
557}