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 dx: Option<f32>,
47 pub dy: Option<f32>,
48 pub slots: Slots,
51}
52
53slot_builders!(Keyframe);
54
55impl Keyframe {
56 pub fn at(mut self, at: f32) -> Self {
57 self.at = Some(at);
58 self
59 }
60
61 pub fn offset(mut self, dx: f32, dy: f32) -> Self {
63 self.dx = Some(dx);
64 self.dy = Some(dy);
65 self
66 }
67
68 pub fn dx(mut self, dx: f32) -> Self {
70 self.dx = Some(dx);
71 self
72 }
73
74 pub fn dy(mut self, dy: f32) -> Self {
76 self.dy = Some(dy);
77 self
78 }
79
80 #[inline]
82 pub fn offsets(&self) -> bool {
83 self.dx.is_some() || self.dy.is_some()
84 }
85
86 pub(crate) fn offset_lanes(&self) -> Option<[f32; 4]> {
89 self.offsets()
90 .then(|| [self.dx.unwrap_or(0.0), self.dy.unwrap_or(0.0), 0.0, 0.0])
91 }
92}
93
94pub fn parse(v: &Value) -> Result<Vec<Keyframe>, String> {
101 parse_with(v, None)
102}
103
104pub fn parse_with(
110 v: &Value,
111 mut refs: Option<&mut crate::tokens::NameRefs<'_>>,
112) -> Result<Vec<Keyframe>, String> {
113 let Value::List(stops) = v else {
114 return Err("keyframes must be a list of stops".into());
115 };
116 let mut frames = Vec::with_capacity(stops.len());
117 let mut last_at = 0.0f32;
118 for (i, stop) in stops.iter().enumerate() {
119 let Value::Map(fields) = stop else {
120 return Err(format!("keyframe {i} must be an object"));
121 };
122 let mut kf = Keyframe::default();
123 for (k, v) in fields {
124 let bad = |what: &str| format!("keyframe {i}: {what}");
125 if kf
126 .slots
127 .parse_field(k, v, refs.as_deref_mut())
128 .map_err(|e| bad(&e))?
129 {
130 continue;
131 }
132 match k.as_str() {
133 "at" => {
134 let at = v
135 .as_float()
136 .ok_or_else(|| bad("at must be a number 0..1"))?
137 as f32;
138 if !(0.0..=1.0).contains(&at) {
139 return Err(bad("at must be within 0..1"));
140 }
141 if at < last_at {
142 return Err(bad("at must not decrease"));
143 }
144 last_at = at;
145 kf.at = Some(at);
146 }
147 "dx" | "dy" => {
148 let n = v
149 .as_float()
150 .ok_or_else(|| bad(&format!("{k} must be a number of px")))?
151 as f32;
152 if k == "dx" {
153 kf.dx = Some(n);
154 } else {
155 kf.dy = Some(n);
156 }
157 }
158 other => return Err(bad(&format!("unknown field {other:?}"))),
159 }
160 }
161 frames.push(kf);
162 }
163 Ok(frames)
164}
165
166pub fn offsets(frames: &[Keyframe]) -> Vec<f32> {
169 let n = frames.len();
170 let mut out: Vec<f32> = frames.iter().map(|f| f.at.unwrap_or(f32::NAN)).collect();
171 if n == 0 {
172 return out;
173 }
174 if n > 1 && out[0].is_nan() {
177 out[0] = 0.0;
178 }
179 if out[n - 1].is_nan() {
180 out[n - 1] = 1.0;
181 }
182 let mut i = 0;
183 while i < n {
184 if !out[i].is_nan() {
185 i += 1;
186 continue;
187 }
188 let start = i - 1;
190 let mut end = i;
191 while out[end].is_nan() {
192 end += 1;
193 }
194 let (a, b) = (out[start], out[end]);
195 let span = (end - start) as f32;
196 for (j, slot) in out[start + 1..end].iter_mut().enumerate() {
197 *slot = a + (b - a) * (j + 1) as f32 / span;
198 }
199 i = end;
200 }
201 out
202}
203
204pub(crate) fn track(
209 frames: &[Keyframe],
210 offsets: &[f32],
211 base: [f32; 4],
212 pick: impl Fn(&Keyframe) -> Option<[f32; 4]>,
213) -> Option<Vec<(f32, [f32; 4])>> {
214 let mut out: Vec<(f32, [f32; 4])> = Vec::with_capacity(frames.len() + 2);
215 for (f, &at) in frames.iter().zip(offsets) {
216 if let Some(v) = pick(f) {
217 out.push((at, v));
218 }
219 }
220 if out.is_empty() {
221 return None;
222 }
223 if out[0].0 > 0.0 {
224 out.insert(0, (0.0, base));
225 }
226 if out[out.len() - 1].0 < 1.0 {
227 out.push((1.0, base));
228 }
229 Some(out)
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::color::Color;
236 use crate::spec::Sizing;
237
238 fn stop(fields: &[(&'static str, Value)]) -> Value {
239 Value::map(fields.iter().cloned())
240 }
241
242 fn list(stops: Vec<Value>) -> Value {
243 Value::List(stops)
244 }
245
246 #[test]
247 fn offsets_spread_evenly_between_declared_stops() {
248 let f = vec![Keyframe::default(); 3];
249 assert_eq!(offsets(&f), vec![0.0, 0.5, 1.0]);
250 let f = vec![
251 Keyframe::default().at(0.2),
252 Keyframe::default(),
253 Keyframe::default(),
254 Keyframe::default().at(0.8),
255 Keyframe::default(),
256 ];
257 let o = offsets(&f);
258 assert!(
259 (o[1] - 0.4).abs() < 1e-6 && (o[2] - 0.6).abs() < 1e-6,
260 "{o:?}"
261 );
262 assert_eq!(o[4], 1.0);
263 assert_eq!(offsets(&[Keyframe::default().at(0.5)]), vec![0.5]);
264 assert_eq!(
265 offsets(&[Keyframe::default()]),
266 vec![1.0],
267 "a lone stop is the far end"
268 );
269 assert!(offsets(&[]).is_empty());
270 }
271
272 #[test]
273 fn tracks_fill_the_ends_with_the_base_value() {
274 let f = [Keyframe::default().at(0.5).radius(8.0)];
275 let o = offsets(&f);
276 let t = track(&f, &o, [2.0; 4], |k| k.radius.map(|r| [r; 4])).unwrap();
277 assert_eq!(t, vec![(0.0, [2.0; 4]), (0.5, [8.0; 4]), (1.0, [2.0; 4])]);
278 assert!(track(&f, &o, [0.0; 4], |k| k.bg.map(|_| [0.0; 4])).is_none());
279 }
280
281 #[test]
282 fn parses_prop_shaped_values() {
283 let f = parse(&list(vec![
284 stop(&[("width", Value::map([("grow", Value::Int(0))]))]),
285 stop(&[
286 ("at", Value::Int(1)),
287 ("width", Value::str("grow")),
288 ("bg", Value::str("#ff0000")),
289 ("radius", Value::Int(3)),
290 ]),
291 ]))
292 .unwrap();
293 assert_eq!(f[0], Keyframe::default().width(Sizing::Grow(0.0)));
294 assert_eq!(
295 f[1],
296 Keyframe::default()
297 .at(1.0)
298 .grow_width()
299 .bg(Color::hex(0xff0000ff))
300 .radius(3.0)
301 );
302 let f = parse(&list(vec![stop(&[
303 ("height", Value::str("50%")),
304 ("bg", Value::Int(0xffffffff)),
305 ])]))
306 .unwrap();
307 assert_eq!(f[0].height, Some(Sizing::Percent(0.5)));
308 assert_eq!(f[0].bg, Some(Color::hex(0xffffffff)));
309 let f = parse(&list(vec![stop(&[(
311 "width",
312 Value::map([("pct", Value::Int(25))]),
313 )])]))
314 .unwrap();
315 assert_eq!(f[0].width, Some(Sizing::Percent(0.25)));
316 }
317
318 #[test]
319 fn rejects_what_css_would() {
320 let bad = |v: Value| parse(&v).unwrap_err();
321 assert!(bad(stop(&[("at", Value::Int(0))])).contains("list"));
322 assert!(
323 bad(list(vec![
324 stop(&[("at", Value::Float(0.5))]),
325 stop(&[("at", Value::Float(0.2))])
326 ]))
327 .contains("decrease")
328 );
329 assert!(bad(list(vec![stop(&[("at", Value::Int(2))])])).contains("within"));
330 assert!(bad(list(vec![stop(&[("colour", Value::str("#fff"))])])).contains("unknown field"));
331 assert!(bad(list(vec![stop(&[("width", Value::str("wide"))])])).contains("bad sizing"));
332 }
333}