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