1extern 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; #[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 pub direction: String,
48}
49
50
51pub type FrameDuration = i32;
52
53#[derive(Debug, Clone)]
56pub struct CellInfo {
57 pub idx: usize,
58 pub duration: FrameDuration,
59}
60
61
62#[derive(PartialEq, Debug, Clone)]
65pub enum PlayMode {
66 OneShot,
69 Hold,
72 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 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 _ => 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#[derive(Debug, Clone)]
147pub struct AnimationClip {
148 pub name: String,
149 pub current_time: Delta, 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 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 pub fn reset(&mut self) {
228 self.set_time(0.);
229 }
230
231 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 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 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 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 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 }
442
443 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 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}