Skip to main content

sequoia_animation/
mixer.rs

1use 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}