Skip to main content

sequoia_animation/
value.rs

1use crate::Lerp;
2use sequoia_core::{Color, Point, Size};
3
4#[derive(Clone, Copy, Debug, PartialEq)]
5#[non_exhaustive]
6pub enum Value {
7    F32(f32),
8    Px(f32),
9    Color(Color),
10    Point(Point),
11    Size(Size),
12}
13
14impl Value {
15    pub const fn f32(v: f32) -> Self {
16        Self::F32(v)
17    }
18    pub const fn px(v: f32) -> Self {
19        Self::Px(v)
20    }
21    pub const fn color(c: Color) -> Self {
22        Self::Color(c)
23    }
24    pub const fn point(x: f32, y: f32) -> Self {
25        Self::Point(Point::new(x, y))
26    }
27    pub const fn size(w: f32, h: f32) -> Self {
28        Self::Size(Size::new(w, h))
29    }
30
31    pub fn as_f32(&self) -> Option<f32> {
32        match self {
33            Self::F32(v) => Some(*v),
34            _ => None,
35        }
36    }
37
38    pub fn as_px(&self) -> Option<f32> {
39        match self {
40            Self::Px(v) => Some(*v),
41            _ => None,
42        }
43    }
44
45    pub fn as_pixels(&self) -> Option<f32> {
46        self.as_px()
47    }
48    pub fn as_color(&self) -> Option<Color> {
49        match self {
50            Self::Color(v) => Some(*v),
51            _ => None,
52        }
53    }
54    pub fn as_point(&self) -> Option<Point> {
55        match self {
56            Self::Point(v) => Some(*v),
57            _ => None,
58        }
59    }
60    pub fn as_size(&self) -> Option<Size> {
61        match self {
62            Self::Size(v) => Some(*v),
63            _ => None,
64        }
65    }
66
67    pub fn lerp(&self, other: &Value, t: f32) -> Value {
68        match (self, other) {
69            (Self::F32(a), Self::F32(b)) => Self::F32(a.lerp(*b, t)),
70            (Self::Px(a), Self::Px(b)) => Self::Px(a.lerp(*b, t)),
71            (Self::Color(a), Self::Color(b)) => Self::Color(a.lerp(*b, t)),
72            (Self::Point(a), Self::Point(b)) => Self::Point(a.lerp(*b, t)),
73            (Self::Size(a), Self::Size(b)) => Self::Size(a.lerp(*b, t)),
74            (a, _) => *a,
75        }
76    }
77
78    pub fn mix<'a>(samples: impl IntoIterator<Item = (&'a Value, f32)>) -> Option<Value> {
79        let mut iter = samples.into_iter().peekable();
80        let first = iter.peek().map(|(v, _)| *v)?;
81        match first {
82            Self::F32(_) => {
83                let (mut acc, mut wsum) = (0.0f32, 0.0f32);
84                for (v, w) in iter {
85                    if let Self::F32(x) = v {
86                        acc += x * w;
87                        wsum += w;
88                    }
89                }
90                (wsum > 0.0).then(|| Self::F32(acc / wsum))
91            }
92            Self::Px(_) => {
93                let (mut acc, mut wsum) = (0.0f32, 0.0f32);
94                for (v, w) in iter {
95                    if let Self::Px(x) = v {
96                        acc += x * w;
97                        wsum += w;
98                    }
99                }
100                (wsum > 0.0).then(|| Self::Px(acc / wsum))
101            }
102            Self::Color(_) => {
103                let mut acc = [0.0f32; 4];
104                let mut wsum = 0.0f32;
105                for (v, w) in iter {
106                    if let Self::Color(c) = v {
107                        acc[0] += c.r * w;
108                        acc[1] += c.g * w;
109                        acc[2] += c.b * w;
110                        acc[3] += c.a * w;
111                        wsum += w;
112                    }
113                }
114                (wsum > 0.0).then(|| {
115                    Self::Color(Color::rgba(
116                        acc[0] / wsum,
117                        acc[1] / wsum,
118                        acc[2] / wsum,
119                        acc[3] / wsum,
120                    ))
121                })
122            }
123            Self::Point(_) => {
124                let (mut ax, mut ay, mut wsum) = (0.0f32, 0.0f32, 0.0f32);
125                for (v, w) in iter {
126                    if let Self::Point(p) = v {
127                        ax += p.x * w;
128                        ay += p.y * w;
129                        wsum += w;
130                    }
131                }
132                (wsum > 0.0).then(|| Self::Point(Point::new(ax / wsum, ay / wsum)))
133            }
134            Self::Size(_) => {
135                let (mut aw, mut ah, mut wsum) = (0.0f32, 0.0f32, 0.0f32);
136                for (v, w) in iter {
137                    if let Self::Size(s) = v {
138                        aw += s.width * w;
139                        ah += s.height * w;
140                        wsum += w;
141                    }
142                }
143                (wsum > 0.0).then(|| Self::Size(Size::new(aw / wsum, ah / wsum)))
144            }
145        }
146    }
147}
148
149impl From<f32> for Value {
150    fn from(v: f32) -> Self {
151        Self::F32(v)
152    }
153}
154impl From<Color> for Value {
155    fn from(v: Color) -> Self {
156        Self::Color(v)
157    }
158}
159impl From<Point> for Value {
160    fn from(v: Point) -> Self {
161        Self::Point(v)
162    }
163}
164impl From<Size> for Value {
165    fn from(v: Size) -> Self {
166        Self::Size(v)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn lerp_matches_variants() {
176        let a = Value::f32(0.0);
177        let b = Value::f32(10.0);
178        assert_eq!(a.lerp(&b, 0.25), Value::f32(2.5));
179
180        let c = Value::color(Color::rgb8(0, 0, 0));
181        let d = Value::color(Color::rgb8(100, 200, 0));
182        let mid = c.lerp(&d, 0.5).as_color().unwrap();
183        let (r, g, bl) = mid.to_rgb8();
184        assert_eq!((r, g, bl), (50, 100, 0));
185    }
186
187    #[test]
188    fn lerp_holds_on_mismatched_types() {
189        let a = Value::f32(5.0);
190        let b = Value::px(10.0);
191        assert_eq!(a.lerp(&b, 0.5), Value::f32(5.0));
192    }
193
194    #[test]
195    fn mix_weights_values() {
196        let a = Value::f32(0.0);
197        let b = Value::f32(100.0);
198        assert_eq!(Value::mix([(&a, 0.75), (&b, 0.25)]), Some(Value::f32(25.0)));
199    }
200
201    #[test]
202    fn mix_skips_mismatched_and_empty_weights() {
203        let a = Value::f32(10.0);
204        let p = Value::px(99.0);
205        assert_eq!(Value::mix([(&a, 1.0), (&p, 1.0)]), Some(Value::f32(10.0)));
206        assert_eq!(Value::mix([(&a, 0.0)]), None);
207        assert_eq!(Value::mix(std::iter::empty()), None);
208    }
209
210    #[test]
211    fn mix_blends_colors_per_channel() {
212        let black = Value::color(Color::BLACK);
213        let white = Value::color(Color::WHITE);
214        let out = Value::mix([(&black, 0.5), (&white, 0.5)])
215            .unwrap()
216            .as_color()
217            .unwrap();
218        let (r, g, b) = out.to_rgb8();
219        assert!((r as i16 - 127).abs() <= 1, "r={r}");
220        assert!((g as i16 - 127).abs() <= 1, "g={g}");
221        assert!((b as i16 - 127).abs() <= 1, "b={b}");
222    }
223}