1use crate::slots::{Slots, slot_builders};
19use crate::value::Value;
20
21#[derive(Clone, Copy, Debug, Default, PartialEq)]
25pub struct Keyframe {
26 pub at: Option<f32>,
28 pub slots: Slots,
31}
32
33slot_builders!(Keyframe);
34
35impl Keyframe {
36 pub fn at(mut self, at: f32) -> Self {
37 self.at = Some(at);
38 self
39 }
40}
41
42pub fn parse(v: &Value) -> Result<Vec<Keyframe>, String> {
48 parse_with(v, None)
49}
50
51pub fn parse_with(
57 v: &Value,
58 mut refs: Option<&mut crate::tokens::NameRefs<'_>>,
59) -> Result<Vec<Keyframe>, String> {
60 let Value::List(stops) = v else {
61 return Err("keyframes must be a list of stops".into());
62 };
63 let mut frames = Vec::with_capacity(stops.len());
64 let mut last_at = 0.0f32;
65 for (i, stop) in stops.iter().enumerate() {
66 let Value::Map(fields) = stop else {
67 return Err(format!("keyframe {i} must be an object"));
68 };
69 let mut kf = Keyframe::default();
70 for (k, v) in fields {
71 let bad = |what: &str| format!("keyframe {i}: {what}");
72 if kf
73 .slots
74 .parse_field(k, v, refs.as_deref_mut())
75 .map_err(|e| bad(&e))?
76 {
77 continue;
78 }
79 match k.as_str() {
80 "at" => {
81 let at = v
82 .as_float()
83 .ok_or_else(|| bad("at must be a number 0..1"))?
84 as f32;
85 if !(0.0..=1.0).contains(&at) {
86 return Err(bad("at must be within 0..1"));
87 }
88 if at < last_at {
89 return Err(bad("at must not decrease"));
90 }
91 last_at = at;
92 kf.at = Some(at);
93 }
94 other => return Err(bad(&format!("unknown field {other:?}"))),
95 }
96 }
97 frames.push(kf);
98 }
99 Ok(frames)
100}
101
102pub fn offsets(frames: &[Keyframe]) -> Vec<f32> {
105 let n = frames.len();
106 let mut out: Vec<f32> = frames.iter().map(|f| f.at.unwrap_or(f32::NAN)).collect();
107 if n == 0 {
108 return out;
109 }
110 if n > 1 && out[0].is_nan() {
113 out[0] = 0.0;
114 }
115 if out[n - 1].is_nan() {
116 out[n - 1] = 1.0;
117 }
118 let mut i = 0;
119 while i < n {
120 if !out[i].is_nan() {
121 i += 1;
122 continue;
123 }
124 let start = i - 1;
126 let mut end = i;
127 while out[end].is_nan() {
128 end += 1;
129 }
130 let (a, b) = (out[start], out[end]);
131 let span = (end - start) as f32;
132 for (j, slot) in out[start + 1..end].iter_mut().enumerate() {
133 *slot = a + (b - a) * (j + 1) as f32 / span;
134 }
135 i = end;
136 }
137 out
138}
139
140pub(crate) fn track(
145 frames: &[Keyframe],
146 offsets: &[f32],
147 base: [f32; 4],
148 pick: impl Fn(&Keyframe) -> Option<[f32; 4]>,
149) -> Option<Vec<(f32, [f32; 4])>> {
150 let mut out: Vec<(f32, [f32; 4])> = Vec::with_capacity(frames.len() + 2);
151 for (f, &at) in frames.iter().zip(offsets) {
152 if let Some(v) = pick(f) {
153 out.push((at, v));
154 }
155 }
156 if out.is_empty() {
157 return None;
158 }
159 if out[0].0 > 0.0 {
160 out.insert(0, (0.0, base));
161 }
162 if out[out.len() - 1].0 < 1.0 {
163 out.push((1.0, base));
164 }
165 Some(out)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::color::Color;
172 use crate::spec::Sizing;
173
174 fn stop(fields: &[(&'static str, Value)]) -> Value {
175 Value::map(fields.iter().cloned())
176 }
177
178 fn list(stops: Vec<Value>) -> Value {
179 Value::List(stops)
180 }
181
182 #[test]
183 fn offsets_spread_evenly_between_declared_stops() {
184 let f = vec![Keyframe::default(); 3];
185 assert_eq!(offsets(&f), vec![0.0, 0.5, 1.0]);
186 let f = vec![
187 Keyframe::default().at(0.2),
188 Keyframe::default(),
189 Keyframe::default(),
190 Keyframe::default().at(0.8),
191 Keyframe::default(),
192 ];
193 let o = offsets(&f);
194 assert!(
195 (o[1] - 0.4).abs() < 1e-6 && (o[2] - 0.6).abs() < 1e-6,
196 "{o:?}"
197 );
198 assert_eq!(o[4], 1.0);
199 assert_eq!(offsets(&[Keyframe::default().at(0.5)]), vec![0.5]);
200 assert_eq!(
201 offsets(&[Keyframe::default()]),
202 vec![1.0],
203 "a lone stop is the far end"
204 );
205 assert!(offsets(&[]).is_empty());
206 }
207
208 #[test]
209 fn tracks_fill_the_ends_with_the_base_value() {
210 let f = [Keyframe::default().at(0.5).radius(8.0)];
211 let o = offsets(&f);
212 let t = track(&f, &o, [2.0; 4], |k| k.radius.map(|r| [r; 4])).unwrap();
213 assert_eq!(t, vec![(0.0, [2.0; 4]), (0.5, [8.0; 4]), (1.0, [2.0; 4])]);
214 assert!(track(&f, &o, [0.0; 4], |k| k.bg.map(|_| [0.0; 4])).is_none());
215 }
216
217 #[test]
218 fn parses_prop_shaped_values() {
219 let f = parse(&list(vec![
220 stop(&[("width", Value::map([("grow", Value::Int(0))]))]),
221 stop(&[
222 ("at", Value::Int(1)),
223 ("width", Value::str("grow")),
224 ("bg", Value::str("#ff0000")),
225 ("radius", Value::Int(3)),
226 ]),
227 ]))
228 .unwrap();
229 assert_eq!(f[0], Keyframe::default().width(Sizing::Grow(0.0)));
230 assert_eq!(
231 f[1],
232 Keyframe::default()
233 .at(1.0)
234 .grow_width()
235 .bg(Color::hex(0xff0000ff))
236 .radius(3.0)
237 );
238 let f = parse(&list(vec![stop(&[
239 ("height", Value::str("50%")),
240 ("bg", Value::Int(0xffffffff)),
241 ])]))
242 .unwrap();
243 assert_eq!(f[0].height, Some(Sizing::Percent(0.5)));
244 assert_eq!(f[0].bg, Some(Color::hex(0xffffffff)));
245 let f = parse(&list(vec![stop(&[(
247 "width",
248 Value::map([("pct", Value::Int(25))]),
249 )])]))
250 .unwrap();
251 assert_eq!(f[0].width, Some(Sizing::Percent(0.25)));
252 }
253
254 #[test]
255 fn rejects_what_css_would() {
256 let bad = |v: Value| parse(&v).unwrap_err();
257 assert!(bad(stop(&[("at", Value::Int(0))])).contains("list"));
258 assert!(
259 bad(list(vec![
260 stop(&[("at", Value::Float(0.5))]),
261 stop(&[("at", Value::Float(0.2))])
262 ]))
263 .contains("decrease")
264 );
265 assert!(bad(list(vec![stop(&[("at", Value::Int(2))])])).contains("within"));
266 assert!(bad(list(vec![stop(&[("colour", Value::str("#fff"))])])).contains("unknown field"));
267 assert!(bad(list(vec![stop(&[("width", Value::str("wide"))])])).contains("bad sizing"));
268 }
269}