Skip to main content

rustmotion_core/vars/
track.rs

1use std::collections::BTreeMap;
2
3use crate::engine::animator::ease;
4use crate::schema::animation::EasingType;
5use crate::schema::time::{TimeCtx, TimeError};
6
7use super::schema::{VarDef, VarSet};
8
9#[derive(Debug, Clone, PartialEq, thiserror::Error)]
10pub enum VarsError {
11    #[error("variable `{name}`: {source}")]
12    Time {
13        name: String,
14        #[source]
15        source: TimeError,
16    },
17    #[error(
18        "variable `{name}`: keyframe {index} starts at {at}s, which is not after keyframe {prev_index}'s start ({prev_at}s) — keyframes must be strictly increasing"
19    )]
20    KeyframesNotIncreasing {
21        name: String,
22        index: usize,
23        at: f64,
24        prev_index: usize,
25        prev_at: f64,
26    },
27    #[error("variable `{name}`: keyframe {index} has a negative duration ({duration}s)")]
28    NegativeDuration {
29        name: String,
30        index: usize,
31        duration: f64,
32    },
33}
34
35#[derive(Debug, Clone, PartialEq)]
36struct Segment {
37    at: f64,
38    duration: f64,
39    start: f64,
40    to: f64,
41    easing: EasingType,
42}
43
44impl Segment {
45    fn value_at(&self, t: f64) -> f64 {
46        if self.duration <= 0.0 {
47            return self.to;
48        }
49        let raw = ((t - self.at) / self.duration).clamp(0.0, 1.0);
50        let eased = ease(raw, &self.easing);
51        self.start + (self.to - self.start) * eased
52    }
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct VarTrack {
57    default: f64,
58    segments: Vec<Segment>,
59}
60
61impl VarTrack {
62    pub fn compile(name: &str, def: &VarDef, ctx: &TimeCtx) -> Result<Self, VarsError> {
63        let mut segments = Vec::with_capacity(def.animation.len());
64        let mut running = def.default;
65        let mut prev: Option<(usize, f64)> = None;
66
67        for (index, kf) in def.animation.iter().enumerate() {
68            let at_ctx = TimeCtx {
69                scene_start: 0.0,
70                ..*ctx
71            };
72            let at = kf
73                .at
74                .resolve_absolute(&at_ctx)
75                .map_err(|source| VarsError::Time {
76                    name: name.to_string(),
77                    source,
78                })?;
79
80            if let Some((prev_index, prev_at)) = prev {
81                if at <= prev_at {
82                    return Err(VarsError::KeyframesNotIncreasing {
83                        name: name.to_string(),
84                        index,
85                        at,
86                        prev_index,
87                        prev_at,
88                    });
89                }
90            }
91            prev = Some((index, at));
92
93            let duration_ctx = TimeCtx {
94                beat_offset: 0.0,
95                scene_start: 0.0,
96                ..*ctx
97            };
98            let duration = kf
99                .duration
100                .resolve_relative(&duration_ctx)
101                .map_err(|source| VarsError::Time {
102                    name: name.to_string(),
103                    source,
104                })?;
105            if duration < 0.0 {
106                return Err(VarsError::NegativeDuration {
107                    name: name.to_string(),
108                    index,
109                    duration,
110                });
111            }
112
113            segments.push(Segment {
114                at,
115                duration,
116                start: running,
117                to: kf.to,
118                easing: kf.easing.clone(),
119            });
120            running = kf.to;
121        }
122
123        Ok(VarTrack {
124            default: def.default,
125            segments,
126        })
127    }
128
129    pub fn value_at(&self, t: f64) -> f64 {
130        let mut value = self.default;
131        for seg in &self.segments {
132            if t < seg.at {
133                return value;
134            }
135            let end = seg.at + seg.duration;
136            if t <= end {
137                return seg.value_at(t);
138            }
139            value = seg.to;
140        }
141        value
142    }
143}
144
145#[derive(Debug, Clone, PartialEq, Default)]
146pub struct VarTable {
147    tracks: BTreeMap<String, VarTrack>,
148}
149
150impl VarTable {
151    pub fn compile(vars: &VarSet, ctx: &TimeCtx) -> Result<Self, VarsError> {
152        let mut tracks = BTreeMap::new();
153        for (name, def) in vars {
154            tracks.insert(name.clone(), VarTrack::compile(name, def, ctx)?);
155        }
156        Ok(VarTable { tracks })
157    }
158
159    pub fn value_at(&self, name: &str, t: f64) -> Option<f64> {
160        self.tracks.get(name).map(|track| track.value_at(t))
161    }
162
163    pub fn names(&self) -> impl Iterator<Item = &str> {
164        self.tracks.keys().map(String::as_str)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::schema::time::TimePoint;
172    use crate::vars::schema::VarKeyframe;
173
174    fn ctx(bpm: Option<f64>, beat_offset: f64) -> TimeCtx {
175        TimeCtx {
176            bpm,
177            beat_offset,
178            scene_start: 0.0,
179        }
180    }
181
182    fn linear_0_to_1_over_3_beats() -> VarDef {
183        VarDef {
184            default: 0.0,
185            animation: vec![VarKeyframe {
186                at: TimePoint::Spec("@4.85s".to_string()),
187                to: 1.0,
188                duration: TimePoint::Spec("3b".to_string()),
189                easing: EasingType::Linear,
190            }],
191        }
192    }
193
194    #[test]
195    fn value_before_first_keyframe_is_default() {
196        let def = linear_0_to_1_over_3_beats();
197        let track = VarTrack::compile("keyDraw", &def, &ctx(Some(120.0), 0.0)).unwrap();
198        assert_eq!(track.value_at(0.0), 0.0);
199        assert_eq!(track.value_at(4.84), 0.0);
200    }
201
202    #[test]
203    fn value_at_several_sampled_instants_across_the_tween() {
204        let def = linear_0_to_1_over_3_beats();
205        let track = VarTrack::compile("keyDraw", &def, &ctx(Some(120.0), 0.0)).unwrap();
206
207        assert_eq!(track.value_at(4.85), 0.0);
208        assert!((track.value_at(5.6) - 0.5).abs() < 1e-9);
209        assert_eq!(track.value_at(6.35), 1.0);
210    }
211
212    #[test]
213    fn value_after_last_keyframe_holds_at_to() {
214        let def = linear_0_to_1_over_3_beats();
215        let track = VarTrack::compile("keyDraw", &def, &ctx(Some(120.0), 0.0)).unwrap();
216        assert_eq!(track.value_at(6.36), 1.0);
217        assert_eq!(track.value_at(1000.0), 1.0);
218    }
219
220    #[test]
221    fn beat_offset_does_not_leak_into_duration() {
222        let def = linear_0_to_1_over_3_beats();
223        let track = VarTrack::compile("keyDraw", &def, &ctx(Some(120.0), 2.2)).unwrap();
224        assert_eq!(track.value_at(4.85), 0.0);
225        assert_eq!(track.value_at(6.35), 1.0);
226    }
227
228    #[test]
229    fn multiple_keyframes_chain_start_values() {
230        let def = VarDef {
231            default: 0.0,
232            animation: vec![
233                VarKeyframe {
234                    at: TimePoint::Seconds(1.0),
235                    to: 10.0,
236                    duration: TimePoint::Seconds(1.0),
237                    easing: EasingType::Linear,
238                },
239                VarKeyframe {
240                    at: TimePoint::Seconds(3.0),
241                    to: 0.0,
242                    duration: TimePoint::Seconds(1.0),
243                    easing: EasingType::Linear,
244                },
245            ],
246        };
247        let track = VarTrack::compile("v", &def, &ctx(None, 0.0)).unwrap();
248        assert_eq!(track.value_at(0.0), 0.0);
249        assert_eq!(track.value_at(1.0), 0.0);
250        assert_eq!(track.value_at(1.5), 5.0);
251        assert_eq!(track.value_at(2.0), 10.0);
252        assert_eq!(track.value_at(2.5), 10.0);
253        assert_eq!(track.value_at(3.0), 10.0);
254        assert_eq!(track.value_at(3.5), 5.0);
255        assert_eq!(track.value_at(4.0), 0.0);
256        assert_eq!(track.value_at(100.0), 0.0);
257    }
258
259    #[test]
260    fn zero_duration_keyframe_snaps() {
261        let def = VarDef {
262            default: 0.0,
263            animation: vec![VarKeyframe {
264                at: TimePoint::Seconds(1.0),
265                to: 1.0,
266                duration: TimePoint::Seconds(0.0),
267                easing: EasingType::Linear,
268            }],
269        };
270        let track = VarTrack::compile("v", &def, &ctx(None, 0.0)).unwrap();
271        assert_eq!(track.value_at(0.999), 0.0);
272        assert_eq!(track.value_at(1.0), 1.0);
273        assert_eq!(track.value_at(2.0), 1.0);
274    }
275
276    #[test]
277    fn ease_in_out_is_not_linear_at_the_midpoint() {
278        let def = VarDef {
279            default: 0.0,
280            animation: vec![VarKeyframe {
281                at: TimePoint::Seconds(0.0),
282                to: 1.0,
283                duration: TimePoint::Seconds(1.0),
284                easing: EasingType::EaseInOut,
285            }],
286        };
287        let track = VarTrack::compile("v", &def, &ctx(None, 0.0)).unwrap();
288        let quarter = track.value_at(0.25);
289        assert!(
290            quarter < 0.25,
291            "expected eased value below linear, got {quarter}"
292        );
293        assert!((track.value_at(0.5) - 0.5).abs() < 1e-9);
294    }
295
296    #[test]
297    fn beat_unit_without_bpm_errors_with_the_variable_name() {
298        let def = linear_0_to_1_over_3_beats();
299        let err = VarTrack::compile("keyDraw", &def, &ctx(None, 0.0)).unwrap_err();
300        assert!(matches!(err, VarsError::Time { name, .. } if name == "keyDraw"));
301    }
302
303    #[test]
304    fn non_increasing_keyframes_are_rejected() {
305        let def = VarDef {
306            default: 0.0,
307            animation: vec![
308                VarKeyframe {
309                    at: TimePoint::Seconds(2.0),
310                    to: 1.0,
311                    duration: TimePoint::Seconds(1.0),
312                    easing: EasingType::Linear,
313                },
314                VarKeyframe {
315                    at: TimePoint::Seconds(2.0),
316                    to: 0.0,
317                    duration: TimePoint::Seconds(1.0),
318                    easing: EasingType::Linear,
319                },
320            ],
321        };
322        let err = VarTrack::compile("v", &def, &ctx(None, 0.0)).unwrap_err();
323        assert!(matches!(
324            err,
325            VarsError::KeyframesNotIncreasing { index: 1, .. }
326        ));
327    }
328
329    #[test]
330    fn negative_duration_is_rejected() {
331        let def = VarDef {
332            default: 0.0,
333            animation: vec![VarKeyframe {
334                at: TimePoint::Spec("@2s-3s".to_string()),
335                to: 1.0,
336                duration: TimePoint::Spec("1s-3s".to_string()),
337                easing: EasingType::Linear,
338            }],
339        };
340        let err = VarTrack::compile("v", &def, &ctx(None, 0.0)).unwrap_err();
341        assert!(matches!(err, VarsError::NegativeDuration { .. }));
342    }
343
344    #[test]
345    fn var_table_compiles_and_resolves_by_name() {
346        let mut vars = VarSet::new();
347        vars.insert("keyDraw".to_string(), linear_0_to_1_over_3_beats());
348        vars.insert(
349            "constant".to_string(),
350            VarDef {
351                default: 42.0,
352                animation: Vec::new(),
353            },
354        );
355
356        let table = VarTable::compile(&vars, &ctx(Some(120.0), 0.0)).unwrap();
357        assert_eq!(table.value_at("constant", 0.0), Some(42.0));
358        assert_eq!(table.value_at("constant", 999.0), Some(42.0));
359        assert_eq!(table.value_at("keyDraw", 0.0), Some(0.0));
360        assert_eq!(table.value_at("keyDraw", 1000.0), Some(1.0));
361        assert_eq!(table.value_at("nope", 0.0), None);
362
363        let mut names: Vec<&str> = table.names().collect();
364        names.sort_unstable();
365        assert_eq!(names, vec!["constant", "keyDraw"]);
366    }
367}