Skip to main content

animato_driver/
recorder.rs

1//! Frame-value recording and replay helpers.
2
3use std::fmt;
4use std::string::String;
5use std::vec::Vec;
6
7/// A recorded scalar sample.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct RecordedSample {
10    /// Absolute time in seconds.
11    pub time: f32,
12    /// Recorded scalar value.
13    pub value: f64,
14}
15
16/// Samples for one recorded animation label.
17#[derive(Clone, Debug, PartialEq)]
18pub struct RecordedTrack {
19    /// Track label.
20    pub label: String,
21    /// Time-ordered samples.
22    pub samples: Vec<RecordedSample>,
23}
24
25/// Captures scalar animation values for later replay or DevTools export.
26#[derive(Clone, Debug, Default, PartialEq)]
27pub struct AnimationRecorder {
28    active: bool,
29    tracks: Vec<RecordedTrack>,
30}
31
32impl AnimationRecorder {
33    /// Create an inactive empty recorder.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Start accepting samples.
39    pub fn start(&mut self) {
40        self.active = true;
41    }
42
43    /// Stop accepting samples.
44    pub fn stop(&mut self) {
45        self.active = false;
46    }
47
48    /// Return whether recording is active.
49    pub fn is_recording(&self) -> bool {
50        self.active
51    }
52
53    /// Remove all recorded tracks.
54    pub fn clear(&mut self) {
55        self.tracks.clear();
56    }
57
58    /// Recorded tracks.
59    pub fn tracks(&self) -> &[RecordedTrack] {
60        &self.tracks
61    }
62
63    /// Record one scalar sample if the recorder is active.
64    pub fn record(&mut self, label: impl AsRef<str>, time: f32, value: f64) {
65        if !self.active || !time.is_finite() || !value.is_finite() {
66            return;
67        }
68        let label = label.as_ref();
69        let sample = RecordedSample {
70            time: time.max(0.0),
71            value,
72        };
73        let track = match self.tracks.iter_mut().find(|track| track.label == label) {
74            Some(track) => track,
75            None => {
76                self.tracks.push(RecordedTrack {
77                    label: label.to_owned(),
78                    samples: Vec::new(),
79                });
80                self.tracks.last_mut().expect("track just pushed")
81            }
82        };
83
84        match track
85            .samples
86            .binary_search_by(|existing| existing.time.total_cmp(&sample.time))
87        {
88            Ok(index) => track.samples[index] = sample,
89            Err(index) => track.samples.insert(index, sample),
90        }
91    }
92
93    /// Export recorded data as deterministic JSON.
94    pub fn export_json(&self) -> String {
95        let mut out = String::from("{\"tracks\":[");
96        for (track_index, track) in self.tracks.iter().enumerate() {
97            if track_index > 0 {
98                out.push(',');
99            }
100            out.push_str("{\"label\":\"");
101            push_escaped(&mut out, &track.label);
102            out.push_str("\",\"frames\":[");
103            for (sample_index, sample) in track.samples.iter().enumerate() {
104                if sample_index > 0 {
105                    out.push(',');
106                }
107                out.push('[');
108                push_float(&mut out, sample.time as f64);
109                out.push(',');
110                push_float(&mut out, sample.value);
111                out.push(']');
112            }
113            out.push_str("]}");
114        }
115        out.push_str("]}");
116        out
117    }
118
119    /// Import data previously produced by [`export_json`](Self::export_json).
120    pub fn import_json(json: &str) -> Result<Self, RecorderError> {
121        let mut cursor = JsonCursor::new(json);
122        cursor.seek("\"tracks\"")?;
123        cursor.seek("[")?;
124        let mut recorder = Self::new();
125        loop {
126            cursor.skip_ws();
127            if cursor.consume(']') {
128                break;
129            }
130            cursor.expect('{')?;
131            cursor.seek("\"label\"")?;
132            cursor.seek(":")?;
133            let label = cursor.string()?;
134            cursor.seek("\"frames\"")?;
135            cursor.seek("[")?;
136            let mut samples = Vec::new();
137            loop {
138                cursor.skip_ws();
139                if cursor.consume(']') {
140                    break;
141                }
142                cursor.expect('[')?;
143                let time = cursor.number()? as f32;
144                cursor.expect(',')?;
145                let value = cursor.number()?;
146                cursor.expect(']')?;
147                samples.push(RecordedSample { time, value });
148                cursor.skip_ws();
149                cursor.consume(',');
150            }
151            samples.sort_by(|a, b| a.time.total_cmp(&b.time));
152            recorder.tracks.push(RecordedTrack { label, samples });
153            cursor.skip_ws();
154            cursor.expect('}')?;
155            cursor.skip_ws();
156            cursor.consume(',');
157        }
158        Ok(recorder)
159    }
160
161    /// Export recorded data as a compact deterministic binary format.
162    pub fn export_binary(&self) -> Vec<u8> {
163        let mut out = Vec::new();
164        out.extend_from_slice(b"ANIMREC1");
165        out.extend_from_slice(&(self.tracks.len() as u32).to_le_bytes());
166        for track in &self.tracks {
167            let label = track.label.as_bytes();
168            out.extend_from_slice(&(label.len() as u32).to_le_bytes());
169            out.extend_from_slice(label);
170            out.extend_from_slice(&(track.samples.len() as u32).to_le_bytes());
171            for sample in &track.samples {
172                out.extend_from_slice(&sample.time.to_le_bytes());
173                out.extend_from_slice(&sample.value.to_le_bytes());
174            }
175        }
176        out
177    }
178
179    /// Import data previously produced by [`export_binary`](Self::export_binary).
180    pub fn import_binary(bytes: &[u8]) -> Result<Self, RecorderError> {
181        let mut reader = BinaryReader::new(bytes);
182        reader.expect_magic(b"ANIMREC1")?;
183        let track_count = reader.u32()? as usize;
184        let mut tracks = Vec::with_capacity(track_count);
185        for _ in 0..track_count {
186            let label_len = reader.u32()? as usize;
187            let label = String::from_utf8(reader.bytes(label_len)?.to_vec())
188                .map_err(|_| RecorderError::InvalidUtf8)?;
189            let sample_count = reader.u32()? as usize;
190            let mut samples = Vec::with_capacity(sample_count);
191            for _ in 0..sample_count {
192                samples.push(RecordedSample {
193                    time: reader.f32()?,
194                    value: reader.f64()?,
195                });
196            }
197            tracks.push(RecordedTrack { label, samples });
198        }
199        Ok(Self {
200            active: false,
201            tracks,
202        })
203    }
204
205    /// Replay a recorded track at `time`, linearly interpolating between samples.
206    pub fn replay(&self, label: &str, time: f32) -> Option<f64> {
207        let track = self.tracks.iter().find(|track| track.label == label)?;
208        match track.samples.as_slice() {
209            [] => None,
210            [only] => Some(only.value),
211            samples => {
212                let time = time.max(0.0);
213                if time <= samples[0].time {
214                    return Some(samples[0].value);
215                }
216                let last = samples.len() - 1;
217                if time >= samples[last].time {
218                    return Some(samples[last].value);
219                }
220                let upper = samples.partition_point(|sample| sample.time <= time);
221                let a = samples[upper - 1];
222                let b = samples[upper];
223                let span = (b.time - a.time).max(f32::EPSILON) as f64;
224                let t = ((time - a.time) as f64 / span).clamp(0.0, 1.0);
225                Some(a.value + (b.value - a.value) * t)
226            }
227        }
228    }
229}
230
231/// Error returned when recorder import fails.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum RecorderError {
234    /// JSON input does not match the recorder export shape.
235    InvalidJson,
236    /// Binary input has invalid magic or is truncated.
237    InvalidBinary,
238    /// A binary label was not valid UTF-8.
239    InvalidUtf8,
240}
241
242impl fmt::Display for RecorderError {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        match self {
245            Self::InvalidJson => f.write_str("invalid recorder JSON"),
246            Self::InvalidBinary => f.write_str("invalid recorder binary"),
247            Self::InvalidUtf8 => f.write_str("invalid recorder UTF-8"),
248        }
249    }
250}
251
252impl std::error::Error for RecorderError {}
253
254fn push_escaped(out: &mut String, value: &str) {
255    for ch in value.chars() {
256        match ch {
257            '"' => out.push_str("\\\""),
258            '\\' => out.push_str("\\\\"),
259            '\n' => out.push_str("\\n"),
260            '\r' => out.push_str("\\r"),
261            '\t' => out.push_str("\\t"),
262            _ => out.push(ch),
263        }
264    }
265}
266
267fn push_float(out: &mut String, value: f64) {
268    if !value.is_finite() {
269        out.push_str("0.000000");
270        return;
271    }
272    let mut value = value;
273    if value < 0.0 {
274        out.push('-');
275        value = -value;
276    }
277    let scaled = (value * 1_000_000.0 + 0.5) as u64;
278    push_u64(out, scaled / 1_000_000);
279    out.push('.');
280    let frac = scaled % 1_000_000;
281    let mut place = 100_000;
282    while place > 0 {
283        out.push(char::from(b'0' + ((frac / place) % 10) as u8));
284        place /= 10;
285    }
286}
287
288fn push_u64(out: &mut String, mut value: u64) {
289    if value == 0 {
290        out.push('0');
291        return;
292    }
293    let mut digits = [0_u8; 20];
294    let mut len = 0;
295    while value > 0 {
296        digits[len] = (value % 10) as u8;
297        value /= 10;
298        len += 1;
299    }
300    for digit in digits[..len].iter().rev() {
301        out.push(char::from(b'0' + *digit));
302    }
303}
304
305struct JsonCursor<'a> {
306    input: &'a str,
307    pos: usize,
308}
309
310impl<'a> JsonCursor<'a> {
311    fn new(input: &'a str) -> Self {
312        Self { input, pos: 0 }
313    }
314
315    fn seek(&mut self, needle: &str) -> Result<(), RecorderError> {
316        let offset = self.input[self.pos..]
317            .find(needle)
318            .ok_or(RecorderError::InvalidJson)?;
319        self.pos += offset + needle.len();
320        Ok(())
321    }
322
323    fn skip_ws(&mut self) {
324        while self
325            .input
326            .as_bytes()
327            .get(self.pos)
328            .is_some_and(u8::is_ascii_whitespace)
329        {
330            self.pos += 1;
331        }
332    }
333
334    fn consume(&mut self, ch: char) -> bool {
335        self.skip_ws();
336        if self.input[self.pos..].starts_with(ch) {
337            self.pos += ch.len_utf8();
338            true
339        } else {
340            false
341        }
342    }
343
344    fn expect(&mut self, ch: char) -> Result<(), RecorderError> {
345        if self.consume(ch) {
346            Ok(())
347        } else {
348            Err(RecorderError::InvalidJson)
349        }
350    }
351
352    fn string(&mut self) -> Result<String, RecorderError> {
353        self.skip_ws();
354        self.expect('"')?;
355        let mut out = String::new();
356        while let Some(ch) = self.input[self.pos..].chars().next() {
357            self.pos += ch.len_utf8();
358            match ch {
359                '"' => return Ok(out),
360                '\\' => {
361                    let escaped = self.input[self.pos..]
362                        .chars()
363                        .next()
364                        .ok_or(RecorderError::InvalidJson)?;
365                    self.pos += escaped.len_utf8();
366                    match escaped {
367                        '"' => out.push('"'),
368                        '\\' => out.push('\\'),
369                        'n' => out.push('\n'),
370                        'r' => out.push('\r'),
371                        't' => out.push('\t'),
372                        _ => return Err(RecorderError::InvalidJson),
373                    }
374                }
375                _ => out.push(ch),
376            }
377        }
378        Err(RecorderError::InvalidJson)
379    }
380
381    fn number(&mut self) -> Result<f64, RecorderError> {
382        self.skip_ws();
383        let start = self.pos;
384        while let Some(byte) = self.input.as_bytes().get(self.pos) {
385            if byte.is_ascii_digit() || matches!(*byte, b'-' | b'+' | b'.' | b'e' | b'E') {
386                self.pos += 1;
387            } else {
388                break;
389            }
390        }
391        self.input[start..self.pos]
392            .parse::<f64>()
393            .map_err(|_| RecorderError::InvalidJson)
394    }
395}
396
397struct BinaryReader<'a> {
398    bytes: &'a [u8],
399    pos: usize,
400}
401
402impl<'a> BinaryReader<'a> {
403    fn new(bytes: &'a [u8]) -> Self {
404        Self { bytes, pos: 0 }
405    }
406
407    fn expect_magic(&mut self, magic: &[u8]) -> Result<(), RecorderError> {
408        if self.bytes(magic.len())? == magic {
409            Ok(())
410        } else {
411            Err(RecorderError::InvalidBinary)
412        }
413    }
414
415    fn bytes(&mut self, len: usize) -> Result<&'a [u8], RecorderError> {
416        let end = self
417            .pos
418            .checked_add(len)
419            .ok_or(RecorderError::InvalidBinary)?;
420        if end > self.bytes.len() {
421            return Err(RecorderError::InvalidBinary);
422        }
423        let out = &self.bytes[self.pos..end];
424        self.pos = end;
425        Ok(out)
426    }
427
428    fn u32(&mut self) -> Result<u32, RecorderError> {
429        let bytes: [u8; 4] = self
430            .bytes(4)?
431            .try_into()
432            .map_err(|_| RecorderError::InvalidBinary)?;
433        Ok(u32::from_le_bytes(bytes))
434    }
435
436    fn f32(&mut self) -> Result<f32, RecorderError> {
437        let bytes: [u8; 4] = self
438            .bytes(4)?
439            .try_into()
440            .map_err(|_| RecorderError::InvalidBinary)?;
441        Ok(f32::from_le_bytes(bytes))
442    }
443
444    fn f64(&mut self) -> Result<f64, RecorderError> {
445        let bytes: [u8; 8] = self
446            .bytes(8)?
447            .try_into()
448            .map_err(|_| RecorderError::InvalidBinary)?;
449        Ok(f64::from_le_bytes(bytes))
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn recorder_round_trips_json_and_replays() {
459        let mut recorder = AnimationRecorder::new();
460        recorder.start();
461        recorder.record("x", 0.0, 0.0);
462        recorder.record("x", 1.0, 10.0);
463        let json = recorder.export_json();
464        let imported = AnimationRecorder::import_json(&json).expect("json import");
465        assert_eq!(imported.replay("x", 0.5), Some(5.0));
466    }
467
468    #[test]
469    fn recorder_round_trips_binary() {
470        let mut recorder = AnimationRecorder::new();
471        recorder.start();
472        recorder.record("scale", 0.0, 1.0);
473        recorder.record("scale", 1.0, 2.0);
474        let binary = recorder.export_binary();
475        let imported = AnimationRecorder::import_binary(&binary).expect("binary import");
476        assert_eq!(imported.replay("scale", 0.25), Some(1.25));
477    }
478}