1use std::fmt;
4use std::string::String;
5use std::vec::Vec;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct RecordedSample {
10 pub time: f32,
12 pub value: f64,
14}
15
16#[derive(Clone, Debug, PartialEq)]
18pub struct RecordedTrack {
19 pub label: String,
21 pub samples: Vec<RecordedSample>,
23}
24
25#[derive(Clone, Debug, Default, PartialEq)]
27pub struct AnimationRecorder {
28 active: bool,
29 tracks: Vec<RecordedTrack>,
30}
31
32impl AnimationRecorder {
33 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn start(&mut self) {
40 self.active = true;
41 }
42
43 pub fn stop(&mut self) {
45 self.active = false;
46 }
47
48 pub fn is_recording(&self) -> bool {
50 self.active
51 }
52
53 pub fn clear(&mut self) {
55 self.tracks.clear();
56 }
57
58 pub fn tracks(&self) -> &[RecordedTrack] {
60 &self.tracks
61 }
62
63 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 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 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum RecorderError {
234 InvalidJson,
236 InvalidBinary,
238 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}