omn_sprites/
lib.rs

1//! This crate contains types and functions for managing playback of frame sequences
2//! over time.
3
4extern crate serde;
5#[macro_use]
6extern crate serde_derive;
7extern crate serde_json;
8
9pub mod aseprite;
10
11use std::collections::hash_map::HashMap;
12
13
14pub type Delta = f32; // FIXME
15
16#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
17pub struct Region {
18    pub x: i32,
19    pub y: i32,
20    #[serde(rename = "w")]
21    pub width: i32,
22    #[serde(rename = "h")]
23    pub height: i32,
24}
25
26#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
27pub struct Frame {
28    pub duration: i32,
29    #[serde(rename = "frame")]
30    pub bbox: Region,
31}
32
33#[derive(Debug, Clone)]
34pub enum Direction {
35    Forward,
36    Reverse,
37    PingPong,
38    Unknown,
39}
40
41#[derive(Serialize, Deserialize, Debug, PartialEq)]
42pub struct FrameTag {
43    pub name: String,
44    pub from: usize,
45    pub to: usize,
46    // one of "forward", "reverse", "pingpong"
47    pub direction: String,
48}
49
50
51pub type FrameDuration = i32;
52
53/// `CellInfo.idx` points to an index in `SpriteSheetData.cells` and `CellInfo.duration` indicates
54/// how long this section of the texture atlas should be displayed as per an `AnimationClip`.
55#[derive(Debug, Clone)]
56pub struct CellInfo {
57    pub idx: usize,
58    pub duration: FrameDuration,
59}
60
61
62/// `PlayMode` controls how the current frame data for a clip at a certain time is calculated with
63/// regards to the duration bounds.
64#[derive(PartialEq, Debug, Clone)]
65pub enum PlayMode {
66    /// `OneShot` will play start to finish, but requests for `CellInfo` after the duration will get
67    /// you None.
68    OneShot,
69    /// `Hold` is similar to `OneShot` however time past the end of the duration will repeat
70    /// the final frame.
71    Hold,
72    /// A `Loop` clip never ends and will return to the start of the clip when exhausted.
73    Loop,
74}
75
76#[derive(Debug)]
77pub struct AnimationClipTemplate {
78    pub cells: Vec<CellInfo>,
79    pub direction: Direction,
80    pub duration: Delta,
81    pub name: String,
82}
83
84impl AnimationClipTemplate {
85    pub fn new(name: String, frames: &[Frame], direction: Direction, offset: usize) -> Self {
86        let cell_info: Vec<CellInfo> = match direction {
87            Direction::Reverse =>
88                frames.iter().enumerate().rev()
89                    .map(|(idx, x)| CellInfo { idx: offset + idx, duration: x.duration})
90                    .collect(),
91            // Look at what aseprite does about each end (double frame problem)
92            Direction::PingPong =>
93                frames.iter().enumerate().chain(frames.iter().enumerate().rev())
94                    .map(|(idx, x)| CellInfo { idx: offset + idx, duration: x.duration})
95                    .collect(),
96            _ =>  // assumes Forward in the fallback case
97                frames.iter().enumerate()
98                    .map(|(idx, x)| CellInfo { idx: offset + idx, duration: x.duration})
99                    .collect()
100
101        };
102        let duration = cell_info.iter().map(|x| x.duration as Delta).sum();
103        Self {
104            name: name,
105            cells: cell_info,
106            direction: direction,
107            duration: duration,
108        }
109    }
110}
111
112/// `AnimationClip` is a group of cell indexes paired with durations such that it can track
113/// playback progress over time. It answers the question of "what subsection of a sprite sheet
114/// should I render at this time?"
115///
116/// It is unusual to construct these yourself. Normally, `AnimationClip` instances will be
117/// created by a `ClipStore` instance via `ClipStore::create()`.
118///
119/// # Examples
120///
121/// ```
122/// use omn_sprites::{AnimationClip, CellInfo, Delta, Frame, Region, Direction, PlayMode};
123///
124/// let frames = vec![
125///     Frame { duration: 1000, bbox: Region { x: 0, y: 0, width: 32, height: 32 } },
126///     Frame { duration: 1000, bbox: Region { x: 32, y: 0, width: 32, height: 32 } },
127/// ];
128///
129/// let mut clip =
130///   AnimationClip::from_frames("Two Frames", Direction::Forward, PlayMode::Loop, &frames);
131///
132/// assert_eq!(clip.get_cell(), Some(0));
133/// clip.update(800.);
134///
135/// assert_eq!(clip.get_cell(), Some(0));
136/// clip.update(800.);
137///
138/// // as playback progresses, we get different frames as a return
139/// assert_eq!(clip.get_cell(), Some(1));
140/// clip.update(800.);
141///
142/// // and as the "play head" extends beyond the total duration of the clip, it'll loop back
143/// // around to the start. This wrapping behaviour can be customized via the `Direction` parameter.
144/// assert_eq!(clip.get_cell(), Some(0));
145/// ```
146#[derive(Debug, Clone)]
147pub struct AnimationClip {
148    pub name: String,
149    pub current_time: Delta, // represents the "play head"
150    pub direction: Direction,
151    pub duration: Delta,
152    cells: Vec<CellInfo>,
153    mode: PlayMode,
154    pub drained: bool,
155}
156
157
158impl AnimationClip {
159    pub fn new(template: &AnimationClipTemplate, play_mode: PlayMode) -> Self {
160
161        AnimationClip {
162            name: template.name.to_owned(),
163            current_time: 0.,
164            direction: template.direction.clone(),
165            duration: template.duration,
166            cells: template.cells.clone(),
167            mode: play_mode,
168            drained: false,
169        }
170    }
171
172    pub fn from_frames(
173        name: &str,
174        direction: Direction,
175        play_mode: PlayMode,
176        frames: &[Frame],
177    ) -> Self {
178        AnimationClip {
179            name: name.to_string(),
180            cells: frames
181                .iter()
182                .enumerate()
183                .map(|(idx, x)| {
184                    CellInfo {
185                        idx: idx,
186                        duration: x.duration,
187                    }
188                })
189                .collect(),
190            current_time: 0.,
191            duration: frames.iter().map(|x| x.duration as Delta).sum(),
192            direction: direction,
193            mode: play_mode,
194            drained: false,
195        }
196    }
197
198    pub fn update(&mut self, dt: Delta) {
199        let updated = self.current_time + dt;
200
201        self.current_time = if updated > self.duration {
202            self.drained = match self.mode {
203                PlayMode::OneShot | PlayMode::Hold => true,
204                _ => false,
205            };
206
207            updated % self.duration
208        } else {
209            updated
210        };
211    }
212
213    /// Explicitly sets the current time of the clip and adjusts the internal
214    /// `AnimationClip.drained` value based on the clip's mode and whether the new time is larger
215    /// than the duration.
216    pub fn set_time(&mut self, time: Delta) {
217        self.current_time = if time > self.duration {
218            self.drained = self.mode != PlayMode::Loop;
219            time % self.duration
220        } else {
221            time
222        }
223
224    }
225
226    /// Put the play head back to the start of the clip.
227    pub fn reset(&mut self) {
228        self.set_time(0.);
229    }
230
231    /// Returns the cell index for the current time of the clip or None if the clip is over.
232    pub fn get_cell(&self) -> Option<usize> {
233
234        if self.drained {
235            return if self.mode == PlayMode::OneShot {
236                None
237            } else {
238                Some(self.cells.last().unwrap().idx)
239            };
240        }
241
242        let mut remaining_time = self.current_time;
243
244        if self.mode == PlayMode::Loop {
245            // FIXME: dupe code caused by iter() and cycle() having different types (otherwise
246            // would return a generic iter from match and loop over after).
247            for cell in self.cells.iter().cycle() {
248                remaining_time -= cell.duration as Delta;
249                if remaining_time <= 0. {
250                    return Some(cell.idx);
251                }
252            }
253        } else {
254            for cell in self.cells.iter() {
255                remaining_time -= cell.duration as Delta;
256                if remaining_time <= 0. {
257                    return Some(cell.idx);
258                }
259            }
260        }
261
262        if self.mode == PlayMode::Hold {
263            Some(self.cells.len() - 1)
264        } else {
265            None
266        }
267    }
268}
269
270
271#[derive(Debug)]
272pub struct ClipStore {
273    store: HashMap<String, AnimationClipTemplate>,
274}
275
276
277pub type SpriteSheetData = aseprite::ExportData;
278
279
280impl ClipStore {
281    pub fn new(data: &SpriteSheetData) -> Self {
282        ClipStore {
283            store: {
284                let mut clips = HashMap::new();
285
286                for tag in &data.meta.frame_tags {
287
288                    let direction = match tag.direction.as_ref() {
289                        "forward" => Direction::Forward,
290                        "reverse" => Direction::Reverse,
291                        "pingpong" => Direction::PingPong,
292                        _ => Direction::Unknown,
293                    };
294                    let frames: &[Frame] = &data.frames[tag.from..tag.to + 1];
295                    clips.insert(
296                        tag.name.clone(),
297                        AnimationClipTemplate::new(tag.name.clone(), frames, direction, tag.from),
298                    );
299                }
300
301                clips
302            },
303        }
304    }
305
306    pub fn create(&self, key: &str, mode: PlayMode) -> Option<AnimationClip> {
307        self.store.get(key).map(|x| AnimationClip::new(x, mode))
308    }
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314
315    #[test]
316    fn test_read_from_file() {
317        let sheet = SpriteSheetData::from_file("resources/numbers-matrix-tags.array.json");
318        let clips = ClipStore::new(&sheet);
319
320        let alpha = clips.create("Alpha", PlayMode::Loop).unwrap();
321        let beta = clips.create("Beta", PlayMode::Loop).unwrap();
322        let gamma = clips.create("Gamma", PlayMode::Loop).unwrap();
323        assert_eq!(alpha.get_cell(), Some(0));
324        assert_eq!(beta.get_cell(), Some(10));
325        assert_eq!(gamma.get_cell(), Some(20));
326    }
327
328    #[test]
329    fn test_clips_are_distinct() {
330        let sheet = SpriteSheetData::from_file("resources/numbers-matrix-tags.array.json");
331        let clips = ClipStore::new(&sheet);
332
333
334        // Each time we get a named clip, we're creating a new instance, and each have their
335        // own internal clock.
336        let mut alpha1 = clips.create("Alpha", PlayMode::Loop).unwrap();
337        let mut alpha2 = clips.create("Alpha", PlayMode::Loop).unwrap();
338
339        alpha1.update(20.);
340        alpha2.update(120.);
341
342        assert_eq!(alpha1.get_cell(), Some(0));
343        assert_eq!(alpha2.get_cell(), Some(1));
344    }
345
346    #[test]
347    fn test_clip_cell_count() {
348        let sheet = get_two_sheet();
349        let clips = ClipStore::new(&sheet);
350
351        let alpha1 = clips.create("Alpha", PlayMode::Loop).unwrap();
352        assert_eq!(alpha1.cells.len(), 2);
353    }
354
355    #[test]
356    fn test_clip_duration() {
357        let sheet = get_two_sheet();
358        let clips = ClipStore::new(&sheet);
359
360        let alpha1 = clips.create("Alpha", PlayMode::Loop).unwrap();
361        assert!((alpha1.duration - 30.).abs() < 0.1);
362    }
363
364    #[test]
365    fn test_oneshot_bounds() {
366        let sheet = get_two_sheet();
367        let clips = ClipStore::new(&sheet);
368
369
370        let mut alpha1 = clips.create("Alpha", PlayMode::OneShot).unwrap();
371
372        assert_eq!(alpha1.get_cell(), Some(0));
373
374        alpha1.update(10.);
375        assert_eq!(alpha1.get_cell(), Some(0));
376
377        alpha1.update(1.);
378        assert_eq!(alpha1.get_cell(), Some(1));
379
380        alpha1.update(19.);
381        assert_eq!(alpha1.get_cell(), Some(1));
382
383        // we should be at the end of the clip at this point
384        assert!((alpha1.current_time - alpha1.duration).abs() < 0.1);
385
386
387        alpha1.update(1.);
388        assert_eq!(alpha1.get_cell(), None);
389
390    }
391
392    #[test]
393    fn test_hold_bounds() {
394        let sheet = get_two_sheet();
395        let clips = ClipStore::new(&sheet);
396
397
398        let mut alpha1 = clips.create("Alpha", PlayMode::Hold).unwrap();
399
400        assert_eq!(alpha1.get_cell(), Some(0));
401
402        alpha1.update(10.);
403        assert_eq!(alpha1.get_cell(), Some(0));
404
405        alpha1.update(1.);
406        assert_eq!(alpha1.get_cell(), Some(1));
407
408        alpha1.update(19.);
409        assert_eq!(alpha1.get_cell(), Some(1));
410
411        // we should be at the end of the clip at this point
412        assert!((alpha1.current_time - alpha1.duration).abs() < 0.1);
413        assert_eq!(alpha1.drained, false);
414
415        alpha1.update(1.);
416        assert_eq!(alpha1.drained, true);
417
418        assert_eq!(alpha1.get_cell(), Some(1));
419    }
420
421    #[test]
422    fn test_deep_clips_report_correct_index() {
423
424        let sheet = get_pitcher_sheet();
425        let clips = ClipStore::new(&sheet);
426
427
428        let mut not_ready = clips.create("Not Ready", PlayMode::OneShot).unwrap();
429
430        not_ready.update(100.);
431        assert_eq!(not_ready.get_cell(), Some(18));
432        not_ready.update(100.);
433        assert_eq!(not_ready.get_cell(), Some(19));
434        not_ready.update(100.);
435        assert_eq!(not_ready.get_cell(), Some(20));
436        not_ready.update(100.);
437        assert_eq!(not_ready.get_cell(), None);
438
439        //        let mut pitching = clips.create("Pitching", PlayMode::OneShot);
440
441    }
442
443    /// Generates a new sprite sheet with a 2 frame clip.
444    fn get_two_sheet() -> SpriteSheetData {
445        aseprite::ExportData::parse_str(
446            r#"{
447          "frames": [
448            {
449              "frame": { "x": 0, "y": 0, "w": 32, "h": 32 },
450              "duration": 10
451            },
452            {
453              "frame": { "x": 32, "y": 0, "w": 32, "h": 32 },
454              "duration": 20
455            }
456          ],
457          "meta": {
458            "size": { "w": 64, "h": 32 },
459            "frameTags": [
460              { "name": "Alpha", "from": 0, "to": 1, "direction": "forward" }
461            ]
462          }
463        }"#,
464        )
465    }
466    /// a real-world usage from LD38
467    fn get_pitcher_sheet() -> SpriteSheetData {
468        aseprite::ExportData::parse_str(
469            r#"{
470            "frames": [
471                {"frame": { "x": 0, "y": 0, "w": 256, "h": 256 }, "duration": 100},
472                {"frame": { "x": 0, "y": 257, "w": 256, "h": 256 }, "duration": 100},
473                {"frame": { "x": 0, "y": 514, "w": 256, "h": 256 }, "duration": 100},
474                {"frame": { "x": 257, "y": 0, "w": 256, "h": 256 }, "duration": 100},
475                {"frame": { "x": 257, "y": 257, "w": 256, "h": 256 }, "duration": 100},
476                {"frame": { "x": 257, "y": 514, "w": 256, "h": 256 }, "duration": 100},
477                {"frame": { "x": 514, "y": 0, "w": 256, "h": 256 }, "duration": 100},
478                {"frame": { "x": 514, "y": 257, "w": 256, "h": 256 }, "duration": 100},
479                {"frame": { "x": 514, "y": 514, "w": 256, "h": 256 }, "duration": 1000},
480                {"frame": { "x": 771, "y": 0, "w": 256, "h": 256 }, "duration": 200},
481                {"frame": { "x": 771, "y": 257, "w": 256, "h": 256 }, "duration": 400},
482                {"frame": { "x": 771, "y": 514, "w": 256, "h": 256 }, "duration": 200},
483                {"frame": { "x": 1028, "y": 0, "w": 256, "h": 256 }, "duration": 150},
484                {"frame": { "x": 1028, "y": 257, "w": 256, "h": 256 }, "duration": 150},
485                {"frame": { "x": 1028, "y": 514, "w": 256, "h": 256 }, "duration": 100},
486                {"frame": { "x": 1285, "y": 0, "w": 256, "h": 256 }, "duration": 100},
487                {"frame": { "x": 1285, "y": 257, "w": 256, "h": 256 }, "duration": 100},
488                {"frame": { "x": 1285, "y": 514, "w": 256, "h": 256 }, "duration": 100},
489                {"frame": { "x": 1542, "y": 0, "w": 256, "h": 256 }, "duration": 100},
490                {"frame": { "x": 1542, "y": 257, "w": 256, "h": 256 }, "duration": 100},
491                {"frame": { "x": 1542, "y": 514, "w": 256, "h": 256 }, "duration": 100}
492            ],
493              "meta": {
494                "size": { "w": 2048, "h": 1024 },
495                "frameTags": [
496                  { "name": "Ready", "from": 0, "to": 7, "direction": "forward" },
497                  { "name": "Winding", "from": 8, "to": 13, "direction": "forward" },
498                  { "name": "Pitching", "from": 14, "to": 17, "direction": "forward" },
499                  { "name": "Not Ready", "from": 18, "to": 20, "direction": "forward" }
500                ]
501              }
502            }"#,
503        )
504    }
505}