sequoia-animation 0.1.0

Sequoia GUI framework — Rive-style animation (tween, keyframes, mixer, state machines)
Documentation
use std::collections::HashMap;

use crate::animation::{Animation, AnimationFrame};
use crate::value::Value;

#[derive(Clone, Copy, Debug, Default)]
pub struct Mixer;

impl Mixer {
    pub fn mix_frames<'a>(
        frames: impl IntoIterator<Item = (&'a AnimationFrame, f32)>,
    ) -> AnimationFrame {
        let mut groups: HashMap<&str, Vec<(&Value, f32)>> = HashMap::new();
        let mut order: Vec<&str> = Vec::new();
        for (frame, weight) in frames {
            if weight <= 0.0 {
                continue;
            }
            for (name, value) in &frame.properties {
                if !groups.contains_key(name.as_str()) {
                    groups.insert(name.as_str(), Vec::new());
                    order.push(name.as_str());
                }
                groups.get_mut(name.as_str()).unwrap().push((value, weight));
            }
        }
        let mut out = AnimationFrame::new();
        for name in order {
            let samples = &groups[name];
            if let Some(v) = Value::mix(samples.iter().copied()) {
                out.properties.insert(name.to_string(), v);
            }
        }
        out
    }

    pub fn mix<'a>(entries: impl IntoIterator<Item = (&'a Animation, f64, f32)>) -> AnimationFrame {
        let frames: Vec<(AnimationFrame, f32)> = entries
            .into_iter()
            .map(|(a, t, w)| (a.sample(t), w))
            .collect();
        Self::mix_frames(frames.iter().map(|(f, w)| (f, *w)))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::animation::{Keyframe, LoopMode, Track};
    use crate::value::Value;

    fn anim(name: &str, from: f32, to: f32) -> Animation {
        Animation::new(name, 1.0).with_track(
            "x",
            Track::try_new(vec![
                Keyframe::new(0.0, Value::px(from)),
                Keyframe::new(1.0, Value::px(to)),
            ])
            .unwrap(),
        )
    }

    #[test]
    fn mix_frames_blends_shared_properties() {
        let a = AnimationFrame::new();
        let mut b = AnimationFrame::new();
        b.insert("x", Value::px(0.0));
        let mut c = AnimationFrame::new();
        c.insert("x", Value::px(100.0));
        let out = Mixer::mix_frames([(&b, 0.75), (&c, 0.25)]);
        assert_eq!(out.get("x"), Some(&Value::px(25.0)));
        let _ = a;
    }

    #[test]
    fn mix_animations_samples_then_blends() {
        let idle = anim("idle", 0.0, 0.0);
        let wave = anim("wave", 0.0, 100.0);

        let out = Mixer::mix([(&idle, 0.0, 0.5), (&wave, 0.5, 0.5)]);
        assert_eq!(out.get("x"), Some(&Value::px(25.0)));
    }

    #[test]
    fn zero_weight_entries_are_ignored() {
        let a = anim("a", 10.0, 10.0);
        let b = anim("b", 0.0, 100.0);
        let out = Mixer::mix([(&a, 0.0, 0.0), (&b, 1.0, 1.0)]);
        assert_eq!(out.get("x"), Some(&Value::px(100.0)));
    }

    #[test]
    fn looping_animation_mixes_its_wrapped_sample() {
        let b = anim("b", 0.0, 100.0).with_loop(LoopMode::Loop);

        let out = Mixer::mix([(&b, 1.5, 1.0)]);
        assert_eq!(out.get("x"), Some(&Value::px(50.0)));
    }
}