sequoia_animation/
mixer.rs1use std::collections::HashMap;
2
3use crate::animation::{Animation, AnimationFrame};
4use crate::value::Value;
5
6#[derive(Clone, Copy, Debug, Default)]
7pub struct Mixer;
8
9impl Mixer {
10 pub fn mix_frames<'a>(
11 frames: impl IntoIterator<Item = (&'a AnimationFrame, f32)>,
12 ) -> AnimationFrame {
13 let mut groups: HashMap<&str, Vec<(&Value, f32)>> = HashMap::new();
14 let mut order: Vec<&str> = Vec::new();
15 for (frame, weight) in frames {
16 if weight <= 0.0 {
17 continue;
18 }
19 for (name, value) in &frame.properties {
20 if !groups.contains_key(name.as_str()) {
21 groups.insert(name.as_str(), Vec::new());
22 order.push(name.as_str());
23 }
24 groups.get_mut(name.as_str()).unwrap().push((value, weight));
25 }
26 }
27 let mut out = AnimationFrame::new();
28 for name in order {
29 let samples = &groups[name];
30 if let Some(v) = Value::mix(samples.iter().copied()) {
31 out.properties.insert(name.to_string(), v);
32 }
33 }
34 out
35 }
36
37 pub fn mix<'a>(entries: impl IntoIterator<Item = (&'a Animation, f64, f32)>) -> AnimationFrame {
38 let frames: Vec<(AnimationFrame, f32)> = entries
39 .into_iter()
40 .map(|(a, t, w)| (a.sample(t), w))
41 .collect();
42 Self::mix_frames(frames.iter().map(|(f, w)| (f, *w)))
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::animation::{Keyframe, LoopMode, Track};
50 use crate::value::Value;
51
52 fn anim(name: &str, from: f32, to: f32) -> Animation {
53 Animation::new(name, 1.0).with_track(
54 "x",
55 Track::try_new(vec![
56 Keyframe::new(0.0, Value::px(from)),
57 Keyframe::new(1.0, Value::px(to)),
58 ])
59 .unwrap(),
60 )
61 }
62
63 #[test]
64 fn mix_frames_blends_shared_properties() {
65 let a = AnimationFrame::new();
66 let mut b = AnimationFrame::new();
67 b.insert("x", Value::px(0.0));
68 let mut c = AnimationFrame::new();
69 c.insert("x", Value::px(100.0));
70 let out = Mixer::mix_frames([(&b, 0.75), (&c, 0.25)]);
71 assert_eq!(out.get("x"), Some(&Value::px(25.0)));
72 let _ = a;
73 }
74
75 #[test]
76 fn mix_animations_samples_then_blends() {
77 let idle = anim("idle", 0.0, 0.0);
78 let wave = anim("wave", 0.0, 100.0);
79
80 let out = Mixer::mix([(&idle, 0.0, 0.5), (&wave, 0.5, 0.5)]);
81 assert_eq!(out.get("x"), Some(&Value::px(25.0)));
82 }
83
84 #[test]
85 fn zero_weight_entries_are_ignored() {
86 let a = anim("a", 10.0, 10.0);
87 let b = anim("b", 0.0, 100.0);
88 let out = Mixer::mix([(&a, 0.0, 0.0), (&b, 1.0, 1.0)]);
89 assert_eq!(out.get("x"), Some(&Value::px(100.0)));
90 }
91
92 #[test]
93 fn looping_animation_mixes_its_wrapped_sample() {
94 let b = anim("b", 0.0, 100.0).with_loop(LoopMode::Loop);
95
96 let out = Mixer::mix([(&b, 1.5, 1.0)]);
97 assert_eq!(out.get("x"), Some(&Value::px(50.0)));
98 }
99}