├── .gitignore ├── reader ├── .gitignore ├── .DS_Store ├── tests │ └── test_cases │ │ ├── simple.aseprite │ │ └── multiple_frames_layers.aseprite ├── src │ ├── lib.rs │ ├── error.rs │ ├── computed.rs │ └── raw.rs ├── Cargo.toml ├── README.MD └── LICENSE ├── .DS_Store ├── .vscode └── settings.json ├── assets ├── .DS_Store ├── player.ase ├── crow.aseprite └── Share-Regular.ttf ├── derive ├── Cargo.toml └── src │ └── lib.rs ├── Cargo.toml ├── src ├── error.rs ├── lib.rs ├── loader.rs └── anim.rs ├── README.MD └── examples └── show_aseprite.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /reader/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/.DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [".\\Cargo.toml"] 3 | } 4 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/assets/.DS_Store -------------------------------------------------------------------------------- /assets/player.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/assets/player.ase -------------------------------------------------------------------------------- /reader/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/reader/.DS_Store -------------------------------------------------------------------------------- /assets/crow.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/assets/crow.aseprite -------------------------------------------------------------------------------- /assets/Share-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/assets/Share-Regular.ttf -------------------------------------------------------------------------------- /reader/tests/test_cases/simple.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/reader/tests/test_cases/simple.aseprite -------------------------------------------------------------------------------- /reader/tests/test_cases/multiple_frames_layers.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdenchev/bevy_aseprite/HEAD/reader/tests/test_cases/multiple_frames_layers.aseprite -------------------------------------------------------------------------------- /reader/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.MD")] 2 | 3 | /// Errors used in this crate 4 | pub mod error; 5 | 6 | /// Raw data types 7 | /// 8 | /// These are used to then construct the main [`Aseprite`] type. 9 | pub mod raw; 10 | 11 | mod computed; 12 | 13 | pub use computed::*; 14 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_aseprite_derive" 3 | version = "0.3.1" 4 | description = "Bevy aseprite loader derive" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | syn = "2.0.23" 14 | quote = "1.0" 15 | bevy_aseprite_reader = { path = "../reader", version = "0.1" } 16 | proc-macro-error = "1.0.4" 17 | heck = "0.4" 18 | -------------------------------------------------------------------------------- /reader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_aseprite_reader" 3 | authors = ["Neikos ", "Michail Denchev "] 4 | version = "0.1.1" 5 | description = "Aseprite reader" 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | resolver = "2" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | flate2 = "1.0.20" 14 | image = { version = "0.24.1", default-features = false } 15 | nom = "7.1.0" 16 | thiserror = "1.0.26" 17 | tracing = "0.1.26" 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_aseprite" 3 | version = "0.12.0" 4 | description = "Bevy aseprite loader" 5 | edition = "2021" 6 | resolver = "2" 7 | license = "MIT OR Apache-2.0" 8 | exclude = ["assets/"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | [workspace] 12 | members = ["derive", "reader"] 13 | 14 | [dependencies] 15 | anyhow = "1.0.43" 16 | bevy = { version = "0.12.0", default-features = false, features = [ 17 | "bevy_asset", 18 | "bevy_render", 19 | "bevy_sprite" 20 | ] } 21 | bevy_aseprite_derive = { path = "./derive", version = "0.3" } 22 | bevy_aseprite_reader = { path = "./reader", version = "0.1" } 23 | 24 | [dev-dependencies] 25 | bevy = { version = "0.12.0" } 26 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use bevy_aseprite_reader as reader; 2 | 3 | #[derive(Debug)] 4 | pub enum AsepriteLoaderError { 5 | Aseprite(reader::error::AsepriteError), 6 | Anyhow(anyhow::Error), 7 | Io(std::io::Error), 8 | } 9 | 10 | impl From for AsepriteLoaderError { 11 | fn from(value: reader::error::AsepriteError) -> Self { 12 | Self::Aseprite(value) 13 | } 14 | } 15 | 16 | impl From for AsepriteLoaderError { 17 | fn from(value: anyhow::Error) -> Self { 18 | Self::Anyhow(value) 19 | } 20 | } 21 | 22 | impl From for AsepriteLoaderError { 23 | fn from(value: std::io::Error) -> Self { 24 | Self::Io(value) 25 | } 26 | } 27 | 28 | impl std::fmt::Display for AsepriteLoaderError { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | write!(f, "{self:?}") 31 | } 32 | } 33 | 34 | impl std::error::Error for AsepriteLoaderError {} 35 | -------------------------------------------------------------------------------- /reader/README.MD: -------------------------------------------------------------------------------- 1 | # Aseprite Reader 2 | 3 | > ❕ Note: This, `aseprite-reader2`, is a fork of https://github.com/TheNeikos/aseprite-reader. 4 | 5 | `aseprite-reader2` is a parsing crate for `.aseprite` files, made by the [Aseprite Editor](https://www.aseprite.org/). 6 | 7 | It's focus is on speed and completeness[^1]. 8 | 9 | It exports a main [`Aseprite`] type, through which the parsed contents can be accessed. 10 | 11 | 12 | 13 | [^1]: Currently embedded ICC profiles are not supported 14 | 15 | 16 | 17 | ## Examples 18 | 19 | ```rust 20 | use bevy_aseprite_reader::Aseprite; 21 | 22 | fn load_character() { 23 | let aseprite = Aseprite::from_path("assets/sprites/character.aseprite") 24 | .expect("Could not read aseprite file."); 25 | 26 | let tags = aseprite.tags(); 27 | 28 | let walk_tag = &tags["walk"]; 29 | println!("This tag uses these frames: {:?}", walk_tag.frames); // `.frames` is a range 30 | 31 | let all_frames = aseprite.frames(); 32 | let frames = all_frames.get_for(&walk_tag.frames); 33 | let images = frames.get_images(); 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Bevy Aseprite Parser and Loader 2 | 3 | This is a fork of TheNeikos/bevy_spicy_aseprite. 4 | 5 | Add `bevy_aseprite = "0.12"` to your Cargo.toml. 6 | 7 | Compatability table 8 | 9 | | bevy | bevy_aseprite | 10 | | ---- | ------------- | 11 | | 0.12 | 0.12 | 12 | | 0.11 | 0.11 | 13 | | 0.10 | 0.10 | 14 | | 0.9 | 0.9 | 15 | 16 | 17 | ## How to use it without derives 18 | 19 | ```rust,ignore 20 | commands.spawn(AsepriteBundle { 21 | aseprite: asset_server.load("player.ase"), 22 | animation: AsepriteAnimation::from("walk"), 23 | transform: Transform {...}, 24 | ..Default::default() 25 | }); 26 | ``` 27 | 28 | 29 | ## How to use it with derive (for compile time validation) 30 | 31 | ```rust,ignore 32 | mod sprites { 33 | use bevy_aseprite::aseprite; 34 | aseprite!(pub Player, "player.ase"); 35 | } 36 | 37 | ... 38 | 39 | commands.spawn(AsepriteBundle { 40 | aseprite: asset_server.load(sprites::Player::PATH), 41 | animation: AsepriteAnimation::from(sprites::Player::tags::LEFT_WALK), 42 | transform: Transform {...}, 43 | ..Default::default() 44 | }); 45 | ``` 46 | 47 | ## Examples 48 | 49 | Check out the example to see how it could be used: 50 | 51 | ```bash 52 | cargo run --example show_aseprite 53 | ``` 54 | 55 | ## Limitations 56 | 57 | Currently no support for slices or toggling layers. 58 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | #![doc = include_str!("../README.MD")] 3 | 4 | pub mod anim; 5 | mod error; 6 | mod loader; 7 | 8 | use anim::AsepriteAnimation; 9 | use bevy::{ 10 | app::{Plugin, Update}, 11 | asset::{Asset, AssetApp, Handle}, 12 | ecs::{ 13 | bundle::Bundle, 14 | schedule::{IntoSystemConfigs, SystemSet}, 15 | }, 16 | reflect::{TypePath, TypeUuid}, 17 | sprite::TextureAtlas, 18 | transform::components::{GlobalTransform, Transform}, 19 | }; 20 | 21 | use bevy_aseprite_reader as reader; 22 | 23 | pub use bevy::sprite::TextureAtlasBuilder; 24 | pub use bevy_aseprite_derive::aseprite; 25 | use reader::AsepriteInfo; 26 | 27 | pub struct AsepritePlugin; 28 | 29 | #[derive(Debug, SystemSet, Clone, Hash, PartialEq, Eq)] 30 | enum AsepriteSystems { 31 | InsertSpriteSheet, 32 | } 33 | 34 | impl Plugin for AsepritePlugin { 35 | fn build(&self, app: &mut bevy::prelude::App) { 36 | app.init_asset::() 37 | .register_asset_loader(loader::AsepriteLoader) 38 | .add_systems(Update, loader::process_load) 39 | .add_systems( 40 | Update, 41 | loader::insert_sprite_sheet.in_set(AsepriteSystems::InsertSpriteSheet), 42 | ) 43 | .add_systems( 44 | Update, 45 | anim::update_animations.after(AsepriteSystems::InsertSpriteSheet), 46 | ); 47 | } 48 | } 49 | 50 | #[derive(Debug, Clone, TypePath, TypeUuid, Asset)] 51 | #[uuid = "b29abc81-6179-42e4-b696-3a5a52f44f73"] 52 | pub struct Aseprite { 53 | // Data is dropped after the atlas is built 54 | data: Option, 55 | // Info stores data such as tags and slices 56 | info: Option, 57 | // TextureAtlasBuilder might shift the index order when building so 58 | // we keep a mapping of frame# -> atlas index here 59 | frame_to_idx: Vec, 60 | // Atlas that gets built from the frame info of the aseprite file 61 | atlas: Option>, 62 | } 63 | 64 | /// A bundle defining a drawn aseprite 65 | #[derive(Debug, Bundle, Default)] 66 | pub struct AsepriteBundle { 67 | pub transform: Transform, 68 | pub global_transform: GlobalTransform, 69 | pub animation: AsepriteAnimation, 70 | pub aseprite: Handle, 71 | } 72 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy_aseprite_reader::Aseprite; 2 | use heck::ToShoutySnekCase; 3 | use proc_macro::TokenStream; 4 | use proc_macro_error::abort; 5 | use proc_macro_error::proc_macro_error; 6 | use quote::{format_ident, quote}; 7 | use syn::{parse::Parse, parse_macro_input, Ident, LitStr, Token, Visibility}; 8 | 9 | extern crate proc_macro; 10 | 11 | struct AsepriteDeclaration { 12 | vis: Visibility, 13 | name: Ident, 14 | path: LitStr, 15 | prefix_path: Option, 16 | } 17 | 18 | impl Parse for AsepriteDeclaration { 19 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 20 | let vis: Visibility = input.parse()?; 21 | let name: Ident = input.parse()?; 22 | input.parse::()?; 23 | let path: LitStr = input.parse()?; 24 | let prefix_path: Option = match input.parse::() { 25 | Ok(_) => Some(input.parse()?), 26 | Err(_) => None, 27 | }; 28 | 29 | Ok(AsepriteDeclaration { 30 | vis, 31 | name, 32 | path, 33 | prefix_path, 34 | }) 35 | } 36 | } 37 | 38 | #[proc_macro] 39 | #[proc_macro_error] 40 | pub fn aseprite(input: TokenStream) -> TokenStream { 41 | let AsepriteDeclaration { 42 | vis, 43 | name, 44 | path, 45 | prefix_path, 46 | } = parse_macro_input!(input as AsepriteDeclaration); 47 | 48 | let prefix = match prefix_path { 49 | Some(path) => format!("{}/", path.value()), 50 | None => String::default(), 51 | }; 52 | 53 | let aseprite = match Aseprite::from_path(format!("{}assets/{}", prefix, path.value())) { 54 | Ok(aseprite) => aseprite, 55 | Err(err) => { 56 | abort!(path, "Could not load file."; note = err); 57 | } 58 | }; 59 | 60 | let tags = aseprite.tags(); 61 | let tag_names = tags 62 | .all() 63 | .map(|tag| format_ident!("{}", tag.name.TO_SHOUTY_SNEK_CASE())); 64 | let tag_values = tags.all().map(|tag| &tag.name); 65 | 66 | let slices = aseprite.slices(); 67 | 68 | let slice_names = slices 69 | .get_all() 70 | .map(|slice| format_ident!("{}", slice.name.TO_SHOUTY_SNEK_CASE())); 71 | let slice_values = slices.get_all().map(|slice| &slice.name); 72 | 73 | let expanded = quote! { 74 | #[allow(non_snake_case)] 75 | #vis mod #name { 76 | pub const PATH: &'static str = #path; 77 | 78 | pub mod tags { 79 | #( pub const #tag_names: &'static str = #tag_values; )* 80 | } 81 | 82 | pub mod slices { 83 | #( pub const #slice_names: &'static str = #slice_values; )* 84 | } 85 | } 86 | }; 87 | 88 | TokenStream::from(expanded) 89 | } 90 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::{anim::AsepriteAnimation, Aseprite, error}; 2 | use bevy::{ 3 | asset::{AssetLoader, AsyncReadExt}, 4 | prelude::*, 5 | render::render_resource::{Extent3d, TextureDimension, TextureFormat}, 6 | }; 7 | use bevy_aseprite_reader as reader; 8 | 9 | 10 | #[derive(Debug, Default)] 11 | pub struct AsepriteLoader; 12 | 13 | impl AssetLoader for AsepriteLoader { 14 | type Asset = Aseprite; 15 | type Settings = (); 16 | type Error = error::AsepriteLoaderError; 17 | 18 | fn load<'a>( 19 | &'a self, 20 | reader: &'a mut bevy::asset::io::Reader, 21 | _settings: &'a Self::Settings, 22 | load_context: &'a mut bevy::asset::LoadContext, 23 | ) -> bevy::utils::BoxedFuture<'a, Result> { 24 | Box::pin(async move { 25 | debug!("Loading aseprite at {:?}", load_context.path()); 26 | 27 | let mut buffer = vec![]; 28 | let _ = reader.read_to_end(&mut buffer).await?; 29 | let data = Some(reader::Aseprite::from_bytes(buffer)?); 30 | 31 | Ok(Aseprite { 32 | data, 33 | info: None, 34 | frame_to_idx: vec![], 35 | atlas: None, 36 | }) 37 | }) 38 | } 39 | 40 | fn extensions(&self) -> &[&str] { 41 | &["ase", "aseprite"] 42 | } 43 | } 44 | 45 | pub(crate) fn process_load( 46 | mut asset_events: EventReader>, 47 | mut aseprites: ResMut>, 48 | mut images: ResMut>, 49 | mut atlases: ResMut>, 50 | ) { 51 | asset_events.read().for_each(|event| { 52 | if let AssetEvent::Added { id } | AssetEvent::Modified { id } = event { 53 | // Get the created/modified aseprite 54 | match aseprites.get(*id) { 55 | Some(aseprite) => match aseprite.atlas.is_some() { 56 | true => return, 57 | false => {} 58 | }, 59 | None => { 60 | error!("Aseprite handle doesn't hold anything?"); 61 | return; 62 | } 63 | } 64 | 65 | let ase = match aseprites.get_mut(*id) { 66 | Some(ase) => ase, 67 | None => { 68 | error!("Aseprite handle doesn't hold anything?"); 69 | return; 70 | } 71 | }; 72 | let data = match ase.data.take() { 73 | Some(data) => data, 74 | None => { 75 | error!("Ase data is empty"); 76 | return; 77 | } 78 | }; 79 | 80 | // Build out texture atlas 81 | let frames = data.frames(); 82 | let ase_images = frames 83 | .get_for(&(0..frames.count() as u16)) 84 | .get_images() 85 | .unwrap(); 86 | 87 | let mut frame_handles = vec![]; 88 | let mut atlas = TextureAtlasBuilder::default(); 89 | 90 | for (idx, image) in ase_images.into_iter().enumerate() { 91 | let texture = Image::new( 92 | Extent3d { 93 | width: image.width(), 94 | height: image.height(), 95 | depth_or_array_layers: 1, 96 | }, 97 | TextureDimension::D2, 98 | image.into_raw(), 99 | TextureFormat::Rgba8UnormSrgb, 100 | ); 101 | let _label = format!("Frame{}", idx); 102 | let texture_handle = images.add(texture.clone()); 103 | frame_handles.push(texture_handle.clone_weak()); 104 | 105 | atlas.add_texture(texture_handle.id(), &texture); 106 | } 107 | let atlas = match atlas.finish(&mut *images) { 108 | Ok(atlas) => atlas, 109 | Err(err) => { 110 | error!("{:?}", err); 111 | return; 112 | } 113 | }; 114 | for handle in frame_handles { 115 | let atlas_idx = atlas.get_texture_index(&handle).unwrap(); 116 | ase.frame_to_idx.push(atlas_idx); 117 | } 118 | let atlas_handle = atlases.add(atlas); 119 | ase.info = Some(data.into()); 120 | ase.atlas = Some(atlas_handle); 121 | } 122 | }); 123 | } 124 | 125 | pub(crate) fn insert_sprite_sheet( 126 | mut commands: Commands, 127 | aseprites: ResMut>, 128 | mut query: Query< 129 | ( 130 | Entity, 131 | &Transform, 132 | &Handle, 133 | &mut AsepriteAnimation, 134 | ), 135 | Without, 136 | >, 137 | ) { 138 | for (entity, &transform, handle, _anim) in query.iter_mut() { 139 | // FIXME The first time the query runs the aseprite atlas might not be ready 140 | // so failing to find it is expected. 141 | let aseprite = match aseprites.get(handle) { 142 | Some(aseprite) => aseprite, 143 | None => { 144 | debug!("Aseprite handle invalid"); 145 | continue; 146 | } 147 | }; 148 | let mut atlas = match aseprite.atlas.clone() { 149 | Some(atlas) => atlas, 150 | None => { 151 | debug!("Aseprite atlas not ready"); 152 | continue; 153 | } 154 | }; 155 | 156 | commands.entity(entity).insert(SpriteSheetBundle { 157 | texture_atlas: atlas, 158 | transform, 159 | ..Default::default() 160 | }); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /reader/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, string::FromUtf8Error}; 2 | 3 | use flate2::DecompressError; 4 | use nom::{error::ParseError, IResult}; 5 | 6 | /// Errors that can occur during parsing 7 | /// 8 | /// Encountering an error could mean one (or several) of these problems: 9 | /// - You passed in an incomplete file (For example when loading immediately after a file change, as this means that the file could be empty or only partially saved) 10 | /// - You passed in an invalid file (Not representing an .aseprite file) 11 | /// - You are passing a file that is either too old, or too new. Make sure that you are saving/using a compatible Aseprite file. 12 | /// 13 | /// If you encounter this error even though you have checked for the above problems, please report this as a bug. 14 | #[derive(Debug, thiserror::Error)] 15 | pub enum AsepriteParseError { 16 | /// Color depth was invalid 17 | #[error("Found invalid color depth {0}. Expected 32/16/8.")] 18 | InvalidColorDepth(u16), 19 | /// An embedded string was not utf-8 20 | #[error("Found invalid UTF-8 {0}")] 21 | InvalidUtf8(FromUtf8Error), 22 | /// An invalid layer type was found 23 | #[error("Found invalid layer type {0}. Expected 0 (Normal) / 1 (Group)")] 24 | InvalidLayerType(u16), 25 | /// An invalid blend mode was found 26 | #[error("Found invalid blend mode {0}")] 27 | InvalidBlendMode(u16), 28 | /// The pixel data could not be decompressed 29 | #[error("Found invalid compressed data {0}")] 30 | InvalidCompressedData(DecompressError), 31 | /// There was not enough compressed data 32 | #[error("Did not find enough compressed data. File invalid.")] 33 | NotEnoughCompressedData, 34 | /// An invalid cel was found while decompressing 35 | #[error("Found invalid cel while decompressing")] 36 | InvalidCel, 37 | /// An invalid cel type was found 38 | #[error("Found invalid cel type {0}")] 39 | InvalidCelType(u16), 40 | /// An invalid animation direction was found 41 | #[error("Found invalid animation type {0}")] 42 | InvalidAnimationDirection(u8), 43 | 44 | /// A generic [`nom`] error was found 45 | #[error("Nom error: {nom:?}")] 46 | GenericNom { 47 | /// The input causing the error 48 | input: I, 49 | /// The error kind reported by [`nom`] 50 | nom: nom::error::ErrorKind, 51 | }, 52 | 53 | /// Could not parse a layer chunk 54 | #[error("An error occured while parsing a layer_chunk")] 55 | InvalidLayerChunk(Box>), 56 | /// Could not parse a cel chunk 57 | #[error("An error occured while parsing a layer_chunk")] 58 | InvalidCelChunk(Box>), 59 | /// Could not parse a cel extra chunk 60 | #[error("An error occured while parsing a layer_chunk")] 61 | InvalidCelExtraChunk(Box>), 62 | /// Could not parse a tags chunk 63 | #[error("An error occured while parsing a layer_chunk")] 64 | InvalidTagsChunk(Box>), 65 | /// Could not parse a palette chunk 66 | #[error("An error occured while parsing a layer_chunk")] 67 | InvalidPaletteChunk(Box>), 68 | /// Could not parse a user data chunk 69 | #[error("An error occured while parsing a layer_chunk")] 70 | InvalidUserDataChunk(Box>), 71 | /// Could not parse a slice chunk 72 | #[error("An error occured while parsing a layer_chunk")] 73 | InvalidSliceChunk(Box>), 74 | /// Could not parse a color profile chunk 75 | #[error("An error occured while parsing a layer_chunk")] 76 | InvalidColorProfileChunk(Box>), 77 | } 78 | 79 | impl ParseError for AsepriteParseError { 80 | fn from_error_kind(input: I, kind: nom::error::ErrorKind) -> Self { 81 | AsepriteParseError::GenericNom { input, nom: kind } 82 | } 83 | 84 | fn append(_input: I, _kind: nom::error::ErrorKind, other: Self) -> Self { 85 | other 86 | } 87 | } 88 | 89 | /// Errors that can happen while loading an aseprite file 90 | #[derive(Debug, thiserror::Error)] 91 | #[non_exhaustive] 92 | pub enum AsepriteError { 93 | /// An error occured during parsing, see [`AsepriteParseError`] for possible causes 94 | /// 95 | /// Either way, this cannot be recovered from 96 | #[error("An error occured during parsing: {0}")] 97 | Parse(String), 98 | /// An IO error occured 99 | #[error("An IO error occured")] 100 | Io(#[from] std::io::Error), 101 | /// An invalid configuration was found while decoding 102 | #[error("Invalid configuration of the aseprite file")] 103 | InvalidConfiguration(#[from] AsepriteInvalidError), 104 | } 105 | 106 | impl<'a> From> for AsepriteError { 107 | fn from(other: AsepriteParseError<&'a [u8]>) -> Self { 108 | AsepriteError::Parse(other.to_string()) 109 | } 110 | } 111 | 112 | /// An invalid configuration exists in the aseprite file 113 | /// 114 | /// This should not happen with files that have not been manually edited 115 | #[derive(Debug, thiserror::Error)] 116 | #[non_exhaustive] 117 | pub enum AsepriteInvalidError { 118 | /// An invalid layer was specified in the aseprite file 119 | #[error("An invalid layer was specified")] 120 | InvalidLayer(usize), 121 | /// An invalid frame was specified in the frame 122 | #[error("An invalid frame was specified")] 123 | InvalidFrame(usize), 124 | /// An invalid palette index was specified as a color 125 | #[error("An invalid palette index was specified as a color")] 126 | InvalidPaletteIndex(usize), 127 | } 128 | 129 | pub(crate) type AseParseResult<'a, R> = IResult<&'a [u8], R, AsepriteParseError<&'a [u8]>>; 130 | pub(crate) type AseResult = std::result::Result; 131 | -------------------------------------------------------------------------------- /examples/show_aseprite.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_aseprite::{anim::AsepriteAnimation, AsepriteBundle, AsepritePlugin}; 3 | 4 | #[derive(Component, Clone, Copy, Debug)] 5 | struct CrowTag; 6 | 7 | #[derive(Component, Clone, Copy, Debug)] 8 | struct PlayerTag; 9 | 10 | mod sprites { 11 | use bevy_aseprite::aseprite; 12 | 13 | // https://meitdev.itch.io/crow 14 | aseprite!(pub Crow, "crow.aseprite"); 15 | // https://shubibubi.itch.io/cozy-people 16 | aseprite!(pub Player, "player.ase"); 17 | } 18 | 19 | fn main() { 20 | App::new() 21 | .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) 22 | .add_plugins(AsepritePlugin) 23 | .add_systems(Startup, setup) 24 | .add_systems(Startup, setup_text) 25 | .add_systems(Update, change_animation) 26 | .run(); 27 | } 28 | 29 | fn setup(mut commands: Commands, asset_server: Res) { 30 | commands.spawn(Camera2dBundle::default()); 31 | 32 | commands 33 | .spawn(AsepriteBundle { 34 | aseprite: asset_server.load(sprites::Crow::PATH), 35 | animation: AsepriteAnimation::from(sprites::Crow::tags::FLAP_WINGS), 36 | transform: Transform { 37 | scale: Vec3::splat(4.), 38 | translation: Vec3::new(0., 80., 0.), 39 | ..Default::default() 40 | }, 41 | ..Default::default() 42 | }) 43 | .insert(CrowTag); 44 | 45 | commands 46 | .spawn(AsepriteBundle { 47 | aseprite: asset_server.load(sprites::Player::PATH), 48 | animation: AsepriteAnimation::from(sprites::Player::tags::LEFT_WALK), 49 | transform: Transform { 50 | scale: Vec3::splat(4.), 51 | translation: Vec3::new(0., -100., 0.), 52 | ..Default::default() 53 | }, 54 | ..Default::default() 55 | }) 56 | .insert(PlayerTag); 57 | } 58 | 59 | fn change_animation( 60 | keys: Res>, 61 | mut aseprites: ParamSet<( 62 | Query<&mut AsepriteAnimation, With>, 63 | Query<&mut AsepriteAnimation, With>, 64 | )>, 65 | ) { 66 | if keys.just_pressed(KeyCode::Key1) { 67 | for mut crow_anim in aseprites.p0().iter_mut() { 68 | *crow_anim = AsepriteAnimation::from(sprites::Crow::tags::FLAP_WINGS); 69 | } 70 | for mut player_anim in aseprites.p1().iter_mut() { 71 | *player_anim = AsepriteAnimation::from(sprites::Player::tags::LEFT_WALK); 72 | } 73 | } 74 | if keys.just_pressed(KeyCode::Key2) { 75 | for mut crow_anim in aseprites.p0().iter_mut() { 76 | *crow_anim = AsepriteAnimation::from(sprites::Crow::tags::GROOVE); 77 | } 78 | for mut player_anim in aseprites.p1().iter_mut() { 79 | *player_anim = AsepriteAnimation::from(sprites::Player::tags::RIGHT_WALK); 80 | } 81 | } 82 | 83 | if keys.pressed(KeyCode::S) { 84 | for mut crow_anim in aseprites.p0().iter_mut() { 85 | crow_anim.custom_size = Some(crow_anim.custom_size.unwrap_or(Vec2::splat(40.)) + Vec2::splat(- 2.)); 86 | } 87 | for mut player_anim in aseprites.p1().iter_mut() { 88 | player_anim.custom_size = Some(player_anim.custom_size.unwrap_or(Vec2::splat(40.)) + Vec2::splat(- 2.)); 89 | } 90 | } 91 | 92 | if keys.pressed(KeyCode::W) { 93 | for mut crow_anim in aseprites.p0().iter_mut() { 94 | crow_anim.custom_size = Some(crow_anim.custom_size.unwrap_or(Vec2::splat(40.)) + Vec2::splat(2.)); 95 | } 96 | for mut player_anim in aseprites.p1().iter_mut() { 97 | player_anim.custom_size = Some(player_anim.custom_size.unwrap_or(Vec2::splat(40.)) + Vec2::splat(2.)); 98 | } 99 | } 100 | 101 | if keys.just_pressed(KeyCode::Space) { 102 | for mut crow_anim in aseprites.p0().iter_mut() { 103 | crow_anim.toggle(); 104 | } 105 | for mut player_anim in aseprites.p1().iter_mut() { 106 | player_anim.toggle(); 107 | } 108 | } 109 | } 110 | 111 | fn setup_text(mut commands: Commands, asset_server: Res) { 112 | let font = asset_server.load("Share-Regular.ttf"); 113 | 114 | let text_style = TextStyle { 115 | font: font.clone(), 116 | font_size: 30., 117 | ..Default::default() 118 | }; 119 | 120 | let credits_text_style = TextStyle { 121 | font, 122 | font_size: 20., 123 | ..Default::default() 124 | }; 125 | 126 | commands.spawn(Text2dBundle { 127 | text: Text { 128 | alignment: TextAlignment::Center, 129 | sections: vec![TextSection { 130 | value: String::from("Press '1' and '2' to switch animations."), 131 | style: TextStyle { 132 | color: Color::WHITE, 133 | ..text_style.clone() 134 | }, 135 | }], 136 | ..Default::default() 137 | }, 138 | transform: Transform::from_translation(Vec3::new(0., 300., 0.)), 139 | ..Default::default() 140 | }); 141 | 142 | commands.spawn(Text2dBundle { 143 | text: Text { 144 | alignment: TextAlignment::Center, 145 | sections: vec![TextSection { 146 | value: String::from("Press 'space' to pause."), 147 | style: TextStyle { 148 | color: Color::WHITE, 149 | ..text_style.clone() 150 | }, 151 | }], 152 | ..Default::default() 153 | }, 154 | transform: Transform::from_translation(Vec3::new(0., 250., 0.)), 155 | ..Default::default() 156 | }); 157 | 158 | commands.spawn(Text2dBundle { 159 | text: Text { 160 | alignment: TextAlignment::Center, 161 | sections: vec![ 162 | TextSection { 163 | value: String::from("The crow was made by "), 164 | style: TextStyle { 165 | color: Color::WHITE, 166 | ..credits_text_style.clone() 167 | }, 168 | }, 169 | TextSection { 170 | value: String::from("meitdev"), 171 | style: TextStyle { 172 | color: Color::LIME_GREEN, 173 | ..credits_text_style.clone() 174 | }, 175 | }, 176 | TextSection { 177 | value: String::from(" on itch.io"), 178 | style: TextStyle { 179 | color: Color::WHITE, 180 | ..credits_text_style.clone() 181 | }, 182 | }, 183 | ], 184 | ..Default::default() 185 | }, 186 | transform: Transform::from_translation(Vec3::new(0., -250., 0.)), 187 | ..Default::default() 188 | }); 189 | 190 | commands.spawn(Text2dBundle { 191 | text: Text { 192 | alignment: TextAlignment::Center, 193 | sections: vec![ 194 | TextSection { 195 | value: String::from("The human was made by "), 196 | style: TextStyle { 197 | color: Color::WHITE, 198 | ..credits_text_style.clone() 199 | }, 200 | }, 201 | TextSection { 202 | value: String::from("shubibubi"), 203 | style: TextStyle { 204 | color: Color::BLUE, 205 | ..credits_text_style.clone() 206 | }, 207 | }, 208 | TextSection { 209 | value: String::from(" on itch.io"), 210 | style: TextStyle { 211 | color: Color::WHITE, 212 | ..credits_text_style 213 | }, 214 | }, 215 | ], 216 | ..Default::default() 217 | }, 218 | transform: Transform::from_translation(Vec3::new(0., -280., 0.)), 219 | ..Default::default() 220 | }); 221 | } 222 | -------------------------------------------------------------------------------- /src/anim.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::prelude::*; 4 | 5 | use crate::{Aseprite, AsepriteInfo}; 6 | use bevy_aseprite_reader as reader; 7 | 8 | /// A tag representing an animation 9 | #[derive(Debug, Default, Component, Copy, Clone, PartialEq, Eq)] 10 | pub struct AsepriteTag(&'static str); 11 | 12 | impl std::ops::Deref for AsepriteTag { 13 | type Target = &'static str; 14 | 15 | fn deref(&self) -> &Self::Target { 16 | &self.0 17 | } 18 | } 19 | 20 | impl AsepriteTag { 21 | /// Create a new tag 22 | pub const fn new(id: &'static str) -> AsepriteTag { 23 | AsepriteTag(id) 24 | } 25 | } 26 | 27 | #[derive(Debug, Component, PartialEq)] 28 | pub struct AsepriteAnimation { 29 | pub is_playing: bool, 30 | tag: Option, 31 | pub current_frame: usize, 32 | pub custom_size: Option, 33 | forward: bool, 34 | time_elapsed: Duration, 35 | tag_changed: bool, 36 | } 37 | 38 | impl Default for AsepriteAnimation { 39 | fn default() -> Self { 40 | Self { 41 | is_playing: true, 42 | tag: Default::default(), 43 | current_frame: Default::default(), 44 | custom_size: None, 45 | forward: Default::default(), 46 | time_elapsed: Default::default(), 47 | tag_changed: true, 48 | } 49 | } 50 | } 51 | 52 | impl AsepriteAnimation { 53 | fn reset(&mut self, info: &AsepriteInfo) { 54 | self.tag_changed = false; 55 | match &self.tag { 56 | Some(tag) => { 57 | let tag = match info.tags.get(tag) { 58 | Some(tag) => tag, 59 | None => { 60 | error!("Tag {} wasn't found.", tag); 61 | return; 62 | } 63 | }; 64 | 65 | let range = tag.frames.clone(); 66 | use reader::raw::AsepriteAnimationDirection; 67 | match tag.animation_direction { 68 | AsepriteAnimationDirection::Forward | AsepriteAnimationDirection::PingPong => { 69 | self.current_frame = range.start as usize; 70 | self.forward = true; 71 | } 72 | AsepriteAnimationDirection::Reverse => { 73 | self.current_frame = range.end as usize - 1; 74 | self.forward = false; 75 | } 76 | } 77 | } 78 | None => { 79 | self.current_frame = 0; 80 | self.forward = true; 81 | } 82 | } 83 | } 84 | 85 | fn next_frame(&mut self, info: &AsepriteInfo) { 86 | match &self.tag { 87 | Some(tag) => { 88 | let tag = match info.tags.get(tag) { 89 | Some(tag) => tag, 90 | None => { 91 | error!("Tag {} wasn't found.", tag); 92 | return; 93 | } 94 | }; 95 | 96 | let range = tag.frames.clone(); 97 | match tag.animation_direction { 98 | reader::raw::AsepriteAnimationDirection::Forward => { 99 | let next_frame = self.current_frame + 1; 100 | if range.contains(&(next_frame as u16)) { 101 | self.current_frame = next_frame; 102 | } else { 103 | self.current_frame = range.start as usize; 104 | } 105 | } 106 | reader::raw::AsepriteAnimationDirection::Reverse => { 107 | let next_frame = self.current_frame.checked_sub(1); 108 | if let Some(next_frame) = next_frame { 109 | if range.contains(&((next_frame) as u16)) { 110 | self.current_frame = next_frame; 111 | } else { 112 | self.current_frame = range.end as usize - 1; 113 | } 114 | } else { 115 | self.current_frame = range.end as usize - 1; 116 | } 117 | } 118 | reader::raw::AsepriteAnimationDirection::PingPong => { 119 | if self.forward { 120 | let next_frame = self.current_frame + 1; 121 | if range.contains(&(next_frame as u16)) { 122 | self.current_frame = next_frame; 123 | } else { 124 | self.current_frame = next_frame.saturating_sub(1); 125 | self.forward = false; 126 | } 127 | } else { 128 | let next_frame = self.current_frame.checked_sub(1); 129 | if let Some(next_frame) = next_frame { 130 | if range.contains(&(next_frame as u16)) { 131 | self.current_frame = next_frame 132 | } 133 | } 134 | self.current_frame += 1; 135 | self.forward = true; 136 | } 137 | } 138 | } 139 | } 140 | None => { 141 | self.current_frame = (self.current_frame + 1) % info.frame_count; 142 | } 143 | } 144 | } 145 | 146 | pub fn current_frame_duration(&self, info: &AsepriteInfo) -> Duration { 147 | Duration::from_millis(info.frame_infos[self.current_frame].delay_ms as u64) 148 | } 149 | 150 | // Returns whether the frame was changed 151 | pub fn update(&mut self, info: &AsepriteInfo, dt: Duration) -> bool { 152 | if self.tag_changed { 153 | self.reset(info); 154 | return true; 155 | } 156 | 157 | if self.is_paused() { 158 | return false; 159 | } 160 | 161 | self.time_elapsed += dt; 162 | let mut current_frame_duration = self.current_frame_duration(info); 163 | let mut frame_changed = false; 164 | while self.time_elapsed >= current_frame_duration { 165 | self.time_elapsed -= current_frame_duration; 166 | self.next_frame(info); 167 | current_frame_duration = self.current_frame_duration(info); 168 | frame_changed = true; 169 | } 170 | frame_changed 171 | } 172 | 173 | /// Get the current frame 174 | pub fn current_frame(&self) -> usize { 175 | self.current_frame 176 | } 177 | 178 | /// Start or resume playing an animation 179 | pub fn play(&mut self) { 180 | self.is_playing = true; 181 | } 182 | 183 | /// Pause the current animation 184 | pub fn pause(&mut self) { 185 | self.is_playing = false; 186 | } 187 | 188 | /// Returns `true` if the animation is playing 189 | pub fn is_playing(&self) -> bool { 190 | self.is_playing 191 | } 192 | 193 | /// Returns `true` if the animation is paused 194 | pub fn is_paused(&self) -> bool { 195 | !self.is_playing 196 | } 197 | 198 | /// Toggle state between playing and pausing 199 | pub fn toggle(&mut self) { 200 | self.is_playing = !self.is_playing; 201 | } 202 | 203 | pub const fn with_size(mut self, size: Option) -> Self { 204 | self.custom_size = size; 205 | self 206 | } 207 | } 208 | 209 | pub(crate) fn update_animations( 210 | time: Res