From ad34dc4f700c9b126022e92b0374c551eda1881d Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 20 Jan 2024 09:36:12 -0800 Subject: [PATCH] Reworked sprite content --- content/sprite.toml | 17 +- crates/content/src/animautomaton.rs | 234 +++++++++++++++ crates/content/src/handle.rs | 8 - crates/content/src/lib.rs | 16 +- crates/content/src/part/effect.rs | 3 +- crates/content/src/part/mod.rs | 2 +- crates/content/src/part/sprite.rs | 431 ++++++++++++++++++++-------- 7 files changed, 561 insertions(+), 150 deletions(-) create mode 100644 crates/content/src/animautomaton.rs diff --git a/content/sprite.toml b/content/sprite.toml index 4e80950..47ef0c3 100644 --- a/content/sprite.toml +++ b/content/sprite.toml @@ -27,8 +27,8 @@ file = "ship/gypsum.png" [sprite."ship::peregrine"] timing.duration = 2 -repeat = "reverse" -random_start_frame = true +top = "reverse" +bot = "reverse" frames = [ "ship/peregrine/01.png", "ship/peregrine/02.png", @@ -73,7 +73,6 @@ file = "ui/landscape-mask.png" [sprite."particle::blaster"] timing.duration = 0.15 -repeat = "once" frames = [ "particle/blaster/01.png", "particle/blaster/02.png", @@ -84,7 +83,6 @@ frames = [ [sprite."particle::explosion::tiny"] timing.fps = 15 -repeat = "once" frames = [ "particle/explosion-tiny/01.png", "particle/explosion-tiny/02.png", @@ -96,7 +94,6 @@ frames = [ [sprite."particle::explosion::small"] timing.fps = 15 -repeat = "once" frames = [ "particle/explosion-small/01.png", "particle/explosion-small/02.png", @@ -109,7 +106,6 @@ frames = [ [sprite."particle::explosion::medium"] timing.fps = 15 -repeat = "once" frames = [ "particle/explosion-medium/01.png", "particle/explosion-medium/02.png", @@ -124,7 +120,6 @@ frames = [ [sprite."particle::explosion::large"] timing.fps = 15 -repeat = "once" frames = [ "particle/explosion-large/01.png", "particle/explosion-large/02.png", @@ -139,7 +134,6 @@ frames = [ [sprite."particle::explosion::huge"] timing.fps = 15 -repeat = "once" frames = [ "particle/explosion-huge/01.png", "particle/explosion-huge/02.png", @@ -156,9 +150,8 @@ frames = [ [sprite."particle::spark::blue"] timing.duration = 0.3 -#timing.rng = 0.2 # each frame will be independently sped up/slowed by this factor -#timing.uniform_rng = 0.2 # one factor for all frames -repeat = "reverse" +top = "reverse" +bot = "reverse" frames = [ "particle/spark-blue/01.png", "particle/spark-blue/02.png", @@ -170,7 +163,6 @@ frames = [ [sprite."particle::spark::yellow"] timing.duration = 0.3 timing.rng = 0.2 -repeat = "once" frames = [ "particle/spark-yellow/01.png", "particle/spark-yellow/02.png", @@ -182,7 +174,6 @@ frames = [ [sprite."particle::spark::red"] timing.duration = 0.3 timing.rng = 0.2 -repeat = "once" frames = [ "particle/spark-red/01.png", "particle/spark-red/02.png", diff --git a/crates/content/src/animautomaton.rs b/crates/content/src/animautomaton.rs new file mode 100644 index 0000000..7ff9546 --- /dev/null +++ b/crates/content/src/animautomaton.rs @@ -0,0 +1,234 @@ +use crate::{AnimSectionHandle, Content, SectionEdge, SpriteHandle}; + +/// A single frame's state +#[derive(Debug, Clone)] +pub struct SpriteAnimationFrame { + /// The index of the texture we're fading from + pub texture_a: u32, + + /// The index of the texture we're fading to + pub texture_b: u32, + + /// Between 0.0 and 1.0, denoting how far we are between + /// texture_a and texture_b + /// 0.0 means fully show texture_a; + /// 1.0 means fully show texture_b. + pub fade: f32, +} + +impl SpriteAnimationFrame { + /// Convenience method. + /// Get texture index as an array + pub fn texture_index(&self) -> [u32; 2] { + [self.texture_a, self.texture_b] + } +} + +/// What direction are we playing our animation in? +#[derive(Debug, Clone)] +enum AnimDirection { + /// Top to bottom, with increasing frame indices + /// (normal) + Up, + + /// Bottom to top, with decreasing frame indices + /// (reverse) + Down, + + /// Stopped, no animation + Stop, +} + +/// Manages a single sprite's animation state. +#[derive(Debug, Clone)] +pub struct AnimAutomaton { + /// The sprite we're animating + sprite: SpriteHandle, + + /// Which animation section we're on + /// This MUST be a section from this Automaton's sprite + current_section: AnimSectionHandle, + + /// Which frame we're on + current_frame: usize, + + /// Where we are between frames. + /// Always between zero and one. + current_fade: f32, + + /// In what direction are we playing the current section? + current_direction: AnimDirection, + + /// The texture we're fading from + /// (if we're moving downwards) + last_texture: u32, + + /// The texture we're fading to + /// (if we're moving downwards) + next_texture: u32, +} + +impl AnimAutomaton { + /// Create a new AnimAutomaton + pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self { + let sprite = ct.get_sprite(sprite_handle); + Self { + current_direction: AnimDirection::Down, + sprite: sprite.handle, + current_frame: 0, + current_fade: 0.0, + current_section: sprite.default_section, + + last_texture: *sprite + .get_section(sprite.default_section) + .frames + .first() + .unwrap(), + + next_texture: *sprite + .get_section(sprite.default_section) + .frames + .first() + .unwrap(), + } + } + + /// Reset this animation + pub fn reset(&mut self, ct: &Content) { + let sprite = ct.get_sprite(self.sprite); + self.current_fade = 0.0; + self.current_frame = 0; + self.current_section = sprite.default_section + } + + /// Reverse this animation's direction + pub fn reverse(&mut self) { + match self.current_direction { + AnimDirection::Stop => {} + AnimDirection::Up => { + self.current_direction = AnimDirection::Down; + } + AnimDirection::Down => { + self.current_direction = AnimDirection::Up; + } + } + } + + /// Step this animation by `t` seconds + pub fn step(&mut self, ct: &Content, t: f32) { + let sprite = ct.get_sprite(self.sprite); + let current_section = sprite.get_section(self.current_section); + + // Current_fade and current_frame keep track of where we are in the current section. + // current_frame indexes this section frames. When it exceeds the number of frames + // or falls below zero (when moving in reverse), we switch to the next section. + // + // current_fade keeps track of our state between frames. It is zero once a frame starts, + // and we switch to the next frame when it hits 1.0. If we are stepping foward, it increases, + // and if we are stepping backwards, it decreases. + + // If this is zero, this section isn't animated. + if current_section.frame_duration == 0.0 { + return; + } + + match self.current_direction { + AnimDirection::Down => self.current_fade += t / current_section.frame_duration, + AnimDirection::Up => self.current_fade -= t / current_section.frame_duration, + AnimDirection::Stop => {} + } + + // We're stepping foward and finished this frame + // (implies we're travelling downwards) + if self.current_fade > 1.0 { + while self.current_fade > 1.0 { + self.current_fade -= 1.0; + } + + if self.current_frame < current_section.frames.len() - 1 { + self.current_frame += 1; + } else { + match current_section.edge_bot { + SectionEdge::Stop => { + self.current_fade = 0.0; + self.current_frame = current_section.frames.len() - 1; + self.current_direction = AnimDirection::Stop; + } + SectionEdge::Top { section } => { + self.current_section = section; + self.current_frame = 0; + } + SectionEdge::Bot { section } => { + let s = sprite.get_section(section); + self.current_section = section; + self.current_frame = s.frames.len() - 1; + self.reverse(); + } + SectionEdge::Restart => { + self.current_frame = 0; + } + SectionEdge::Reverse => { + // Jump to SECOND frame, since we've already shown the + // first during the fade transition + self.current_frame = current_section.frames.len() - 1; + self.reverse() + } + } + } + + let current_section = sprite.get_section(self.current_section); + self.last_texture = self.next_texture; + self.next_texture = current_section.frames[self.current_frame]; + } + + // We're stepping backward and finished this frame + // (implies we're travelling upwards) + if self.current_fade < 0.0 { + while self.current_fade < 0.0 { + self.current_fade += 1.0; + } + + if self.current_frame > 0 { + self.current_frame -= 1; + } else { + match current_section.edge_top { + SectionEdge::Stop => { + self.current_fade = 0.0; + self.current_frame = 0; + self.current_direction = AnimDirection::Stop; + } + SectionEdge::Top { section } => { + self.current_section = section; + self.current_frame = 0; + self.reverse(); + } + SectionEdge::Bot { section } => { + let s = sprite.get_section(section); + self.current_section = section; + self.current_frame = s.frames.len() - 1; + } + SectionEdge::Reverse => { + self.current_frame = 0; + self.reverse(); + } + SectionEdge::Restart => { + self.current_frame = current_section.frames.len() - 1; + } + } + } + + let current_section = sprite.get_section(self.current_section); + self.next_texture = self.last_texture; + self.last_texture = current_section.frames[self.current_frame]; + } + } + + /// Get the current frame of this animation + pub fn get_texture_idx(&self) -> SpriteAnimationFrame { + return SpriteAnimationFrame { + texture_a: self.last_texture, + texture_b: self.next_texture, + fade: self.current_fade, + }; + } +} diff --git a/crates/content/src/handle.rs b/crates/content/src/handle.rs index 6d5ab93..3447527 100644 --- a/crates/content/src/handle.rs +++ b/crates/content/src/handle.rs @@ -16,14 +16,6 @@ pub struct SpriteHandle { pub aspect: f32, } -impl SpriteHandle { - /// The index of this sprite in content's sprite array. - /// Render uses this to build its buffers. - pub fn get_index(&self) -> u32 { - self.index as u32 - } -} - impl Hash for SpriteHandle { fn hash(&self, state: &mut H) { self.index.hash(state) diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index ace5697..c3881ae 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -3,6 +3,7 @@ //! This subcrate is responsible for loading, parsing, validating game content, //! which is usually stored in `./content`. +mod animautomaton; mod handle; mod part; mod util; @@ -18,6 +19,7 @@ use std::{ use toml; use walkdir::WalkDir; +pub use animautomaton::*; pub use handle::*; pub use part::*; @@ -279,11 +281,13 @@ impl Content { } /// Get the handle for the starfield sprite - pub fn get_starfield_handle(&self) -> SpriteHandle { - match self.starfield_handle { + pub fn get_starfield_texture(&self) -> u32 { + let h = match self.starfield_handle { Some(h) => h, None => unreachable!("Starfield sprite hasn't been loaded yet!"), - } + }; + let sprite = self.get_sprite(h); + sprite.get_section(sprite.default_section).frames[0] } /// Get a handle from a sprite name @@ -306,9 +310,9 @@ impl Content { return &self.sprite_atlas.atlas_list; } - /// Get a sprite from a path - pub fn get_image(&self, p: &Path) -> &SpriteAtlasImage { - self.sprite_atlas.index.get(p).unwrap() + /// Get a texture by its index + pub fn get_image(&self, idx: u32) -> &SpriteAtlasImage { + &self.sprite_atlas.index[idx as usize] } /// Get an outfit from a handle diff --git a/crates/content/src/part/effect.rs b/crates/content/src/part/effect.rs index a3b847f..6195bd8 100644 --- a/crates/content/src/part/effect.rs +++ b/crates/content/src/part/effect.rs @@ -57,7 +57,8 @@ pub(crate) mod syntax { TextOrFloat::Text(s) => { if s == "inherit" { let sprite = content.get_sprite(sprite); - sprite.frame_duration * sprite.frames.len() as f32 + let sec = sprite.get_section(sprite.default_section); + sec.frame_duration * sec.frames.len() as f32 } else { bail!("bad effect lifetime, must be float or \"inherit\"",) } diff --git a/crates/content/src/part/mod.rs b/crates/content/src/part/mod.rs index 52dd207..0eb49b5 100644 --- a/crates/content/src/part/mod.rs +++ b/crates/content/src/part/mod.rs @@ -18,5 +18,5 @@ pub use ship::{ CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship, ShipCollapse, }; -pub use sprite::{RepeatMode, Sprite}; +pub use sprite::*; pub use system::{System, SystemObject}; diff --git a/crates/content/src/part/sprite.rs b/crates/content/src/part/sprite.rs index b6f1c73..de61224 100644 --- a/crates/content/src/part/sprite.rs +++ b/crates/content/src/part/sprite.rs @@ -1,44 +1,36 @@ -use anyhow::{bail, Context, Result}; -use image::io::Reader; -use serde::Deserialize; -use std::{collections::HashMap, path::PathBuf}; +use anyhow::{anyhow, bail, Context, Result}; +use std::collections::HashMap; use crate::{handle::SpriteHandle, Content, ContentBuildContext}; pub(crate) mod syntax { + use crate::{Content, ContentBuildContext}; + use anyhow::{anyhow, bail, Context, Ok, Result}; use serde::Deserialize; - use std::path::PathBuf; + use std::{collections::HashMap, path::PathBuf}; - use super::RepeatMode; + use super::AnimSectionHandle; // Raw serde syntax structs. // These are never seen by code outside this crate. + /// Convenience variants of sprite definitions #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum Sprite { Static(StaticSprite), - Frames(FrameSprite), - } - - #[derive(Debug, Deserialize)] - pub struct StaticSprite { - pub file: PathBuf, - } - - #[derive(Debug, Deserialize)] - pub struct FrameSprite { - pub frames: Vec, - pub timing: Timing, - pub repeat: RepeatMode, - pub random_start_frame: Option, + OneSection(SpriteSection), + Complete(CompleteSprite), } + /// Two ways to specify animation length #[derive(Debug, Deserialize)] pub enum TimingVariant { + /// The duration of this whole section #[serde(rename = "duration")] Duration(f32), + /// The fps of this section #[serde(rename = "fps")] Fps(f32), } @@ -47,36 +39,176 @@ pub(crate) mod syntax { pub struct Timing { #[serde(flatten)] pub variant: TimingVariant, - //pub uniform_rng: Option, } -} -/// How to replay a texture's animation -#[derive(Debug, Deserialize, Clone, Copy)] -pub enum RepeatMode { - /// Play this animation once, and stop at the last frame - #[serde(rename = "once")] - Once, + /// An unanimated sprite + #[derive(Debug, Deserialize)] + pub struct StaticSprite { + pub file: PathBuf, + } - /// After the first frame, jump to the last frame - #[serde(rename = "repeat")] - Repeat, + /// The proper, full sprite definition + #[derive(Debug, Deserialize)] + pub struct CompleteSprite { + pub section: HashMap, + pub default_section: String, + } - /// Play this animation in reverse after the last frame - #[serde(rename = "reverse")] - Reverse, -} + /// A single animation section + #[derive(Debug, Deserialize)] + pub struct SpriteSection { + pub frames: Vec, + pub timing: Timing, + pub top: Option, + pub bot: Option, + } -impl RepeatMode { - /// Represent this repeatmode as an integer - /// Used to pass this enum into shaders - pub fn as_int(&self) -> u32 { - match self { - Self::Repeat => 0, - Self::Once => 1, - Self::Reverse => 2, + impl SpriteSection { + pub fn add_to( + &self, + _build_context: &mut ContentBuildContext, + content: &mut Content, + + // An index of all sections in this sprite, used to resolve + // top and bot edges. + all_sections: &HashMap, + ) -> Result<((u32, u32), super::SpriteSection)> { + // Make sure all frames have the same size and add them + // to the frame vector + let mut dim = None; + let mut frames = Vec::new(); + for f in &self.frames { + let idx = match content.sprite_atlas.path_map.get(f) { + Some(s) => *s, + None => { + bail!("error: file `{}` isn't in the sprite atlas", f.display()); + } + }; + let img = &content.sprite_atlas.index[idx as usize]; + + match dim { + None => dim = Some(img.true_size), + Some(e) => { + if img.true_size != e { + bail!("failed to load section frames because frames have different sizes.",) + } + } + } + + frames.push(img.idx); + } + let dim = dim.unwrap(); + + let frame_duration = match self.timing.variant { + TimingVariant::Duration(d) => d / self.frames.len() as f32, + TimingVariant::Fps(f) => 1.0 / f, + }; + + if frame_duration <= 0.0 { + bail!("frame duration must be positive (and therefore nonzero).") + } + + let edge_top = match &self.top { + Some(x) => x.resolve(all_sections)?, + None => super::SectionEdge::Stop, + }; + + let edge_bot = match &self.bot { + Some(x) => x.resolve(all_sections)?, + None => super::SectionEdge::Stop, + }; + + return Ok(( + dim, + super::SpriteSection { + frames, + frame_duration, + edge_top, + edge_bot, + }, + )); } } + + /// A link between two animation sections + #[derive(Debug, Deserialize)] + #[serde(transparent)] + pub struct SectionEdge { + pub val: String, + } + + impl SectionEdge { + pub fn resolve( + &self, + all_sections: &HashMap, + ) -> Result { + if self.val == "stop" { + return Ok(super::SectionEdge::Stop); + } + + if self.val == "reverse" { + return Ok(super::SectionEdge::Reverse); + } + + if self.val == "restart" { + return Ok(super::SectionEdge::Restart); + } + + let (s, p) = match self.val.split_once(":") { + Some(x) => x, + None => { + bail!("bad section edge specification `{}`", self.val); + } + }; + + let section = match all_sections.get(s) { + Some(s) => *s, + None => { + return Err(anyhow!("bad section edge specification `{}`", self.val)) + .with_context(|| format!("section `{}` doesn't exist", s)); + } + }; + + match p { + "top" => Ok(super::SectionEdge::Top { section }), + "bot" => Ok(super::SectionEdge::Bot { section }), + _ => { + return Err(anyhow!("bad section edge specification `{}`", self.val)) + .with_context(|| format!("invalid target `{}`", p)); + } + } + } + } +} + +// TODO: should be pub crate +/// A handle for an animation section inside a sprite +#[derive(Debug, Copy, Clone)] +pub struct AnimSectionHandle(usize); + +/// An edge between two animation sections +#[derive(Debug, Clone)] +pub enum SectionEdge { + /// Stop at the last frame of this section + Stop, + + /// Play the given section from the bottm + Bot { + /// The section to play + section: AnimSectionHandle, + }, + + /// Play the given section from the top + Top { + /// The section to play + section: AnimSectionHandle, + }, + + /// Replay this section in the opposite direction + Reverse, + + /// Restart this section from the opposite end + Restart, } /// Represents a sprite that may be used in the game. @@ -88,25 +220,44 @@ pub struct Sprite { /// This sprite's handle pub handle: SpriteHandle, - /// The file names of frames of this sprite. - /// unanimated sprites have one frame. - pub frames: Vec, + /// This sprite's default section + pub default_section: AnimSectionHandle, - /// The speed of this sprite's animation. - /// This is zero for unanimate sprites. - pub frame_duration: f32, - - /// All frames will be sped up/slowed by this factor. - //pub frame_uniform_rng: f32, - - /// How to replay this sprite's animation - pub repeat: RepeatMode, + /// This sprite's animation sections + sections: Vec, /// Aspect ratio of this sprite (width / height) pub aspect: f32, +} - /// If true, start on a random frame of this sprite. - pub random_start_frame: bool, +impl Sprite { + /// Get an animation section from a handle + pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection { + &self.sections[section.0] + } + + /// Iterate this sprite's sections + pub fn iter_sections(&self) -> impl Iterator { + self.sections.iter() + } +} + +/// A part of a sprite's animation +#[derive(Debug, Clone)] +pub struct SpriteSection { + /// The texture index of each frame in this animation section. + /// unanimated sections have one frame. + pub frames: Vec, + + /// The speed of this sprite's animation. + /// This must always be positive (and therefore, nonzero) + pub frame_duration: f32, + + /// What to do when we reach the top of this section + pub edge_top: SectionEdge, + + /// What to do when we reach the bottom of this section + pub edge_bot: SectionEdge, } impl crate::Build for Sprite { @@ -114,31 +265,32 @@ impl crate::Build for Sprite { fn build( sprites: Self::InputSyntaxType, - _build_context: &mut ContentBuildContext, + build_context: &mut ContentBuildContext, content: &mut Content, ) -> Result<()> { for (sprite_name, t) in sprites { match t { syntax::Sprite::Static(t) => { - let file = content.config.sprite_root.join(&t.file); - let reader = Reader::open(&file).with_context(|| { - format!( - "Failed to read file `{}` in sprite `{}`", - file.display(), - sprite_name, - ) - })?; - let dim = reader.into_dimensions().with_context(|| { - format!( - "Failed to get dimensions of file `{}` in sprite `{}`", - file.display(), - sprite_name, - ) - })?; + let idx = match content.sprite_atlas.path_map.get(&t.file) { + Some(s) => *s, + None => { + return Err( + anyhow!("error while processing sprite `{}`", sprite_name,), + ) + .with_context(|| { + format!( + "file `{}` isn't in the sprite atlas, cannot proceed", + t.file.display() + ) + }); + } + }; + let img = &content.sprite_atlas.index[idx as usize]; + let aspect = img.w / img.h; let h = SpriteHandle { index: content.sprites.len(), - aspect: dim.0 as f32 / dim.1 as f32, + aspect, }; if sprite_name == content.config.starfield_sprite { @@ -154,71 +306,108 @@ impl crate::Build for Sprite { content.sprites.push(Self { name: sprite_name, - frames: vec![t.file], - frame_duration: 0.0, - //frame_uniform_rng: 0.0, + default_section: AnimSectionHandle(0), + sections: vec![SpriteSection { + frames: vec![img.idx], + // We implement unanimated sprites with a very fast framerate + // and STOP endpoints. + frame_duration: 0.01, + edge_top: SectionEdge::Stop, + edge_bot: SectionEdge::Stop, + }], handle: h, - repeat: RepeatMode::Once, - aspect: dim.0 as f32 / dim.1 as f32, - random_start_frame: false, + aspect, }); } - syntax::Sprite::Frames(t) => { - let mut dim = None; - for f in &t.frames { - let file = content.config.sprite_root.join(f); - let reader = Reader::open(&file).with_context(|| { - format!( - "Failed to read file `{}` in sprite `{}`", - file.display(), - sprite_name, - ) - })?; - let d = reader.into_dimensions().with_context(|| { - format!( - "Failed to get dimensions of file `{}` in sprite `{}`", - file.display(), - sprite_name, - ) - })?; - match dim { - None => dim = Some(d), - Some(e) => { - if d != e { - bail!( - "Failed to load frames of sprite `{}` because frames have different sizes.", - sprite_name, - ) - } - } - } - } - let dim = dim.unwrap(); + syntax::Sprite::OneSection(s) => { + let mut section_names: HashMap = HashMap::new(); + // Name the one section in this sprite "anim" + section_names.insert("anim".to_owned(), AnimSectionHandle(0)); + let (dim, section) = s + .add_to(build_context, content, §ion_names) + .with_context(|| format!("while parsing sprite `{}`", sprite_name))?; + let aspect = dim.0 as f32 / dim.1 as f32; let h = SpriteHandle { index: content.sprites.len(), - aspect: dim.0 as f32 / dim.1 as f32, + aspect, }; + // TODO: remove? if sprite_name == content.config.starfield_sprite { unreachable!("Starfield texture may not be animated") } - let frame_duration = match t.timing.variant { - syntax::TimingVariant::Duration(d) => d / t.frames.len() as f32, - syntax::TimingVariant::Fps(f) => 1.0 / f, + let mut sections = Vec::new(); + sections.push(section); + + content.sprite_index.insert(sprite_name.clone(), h); + content.sprites.push(Self { + name: sprite_name, + sections, + default_section: AnimSectionHandle(0), + handle: h, + aspect, + }); + } + syntax::Sprite::Complete(s) => { + let mut idx = 0; + let mut section_names = HashMap::new(); + for (name, _) in &s.section { + section_names.insert(name.to_owned(), AnimSectionHandle(idx)); + idx += 1; + } + + if !section_names.contains_key(&s.default_section) { + bail!( + "could not load sprite `{}`, default section `{}` doesn't exist", + sprite_name, + s.default_section + ); + } + + let mut sections = Vec::with_capacity(idx); + let mut dim = None; + + // Make sure we add sections in order + let mut names = section_names.iter().collect::>(); + names.sort_by(|a, b| (a.1).0.cmp(&(b.1).0)); + + for (k, _) in names { + let v = s.section.get(k).unwrap(); + let (d, s) = v + .add_to(build_context, content, §ion_names) + .with_context(|| format!("while parsing sprite `{}`", sprite_name)) + .with_context(|| format!("while parsing section `{}`", k))?; + + // Make sure all dimensions are the same + if dim.is_none() { + dim = Some(d); + } else if dim.unwrap() != d { + bail!( + "could not load sprite `{}`, image sizes in section `{}` are different", + sprite_name, + k + ); + } + + sections.push(s); + } + let dim = dim.unwrap(); + let aspect = dim.0 as f32 / dim.1 as f32; + + let h = SpriteHandle { + index: content.sprites.len(), + aspect, }; content.sprite_index.insert(sprite_name.clone(), h); content.sprites.push(Self { name: sprite_name, - frames: t.frames, - frame_duration, - //frame_uniform_rng: t.timing.uniform_rng.unwrap_or(0.0), + sections, + default_section: *section_names.get(&s.default_section).unwrap(), handle: h, - repeat: t.repeat, - aspect: dim.0 as f32 / dim.1 as f32, - random_start_frame: t.random_start_frame.unwrap_or(false), + aspect, }); } }