From 6ca2e01fbab90dc975a4e03a9baa437a57d03734 Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 25 Jul 2025 12:27:19 -0500 Subject: [PATCH] feat: atlas decoding & frame acquisition --- Cargo.lock | 46 ++++++++++++++++++++++++ Cargo.toml | 2 ++ src/texture/animated.rs | 69 ++++++++++++----------------------- src/texture/atlas.rs | 74 -------------------------------------- src/texture/blinking.rs | 39 +++++++------------- src/texture/directional.rs | 52 +++++++++++++++++++++++++++ src/texture/mod.rs | 15 ++++---- src/texture/sprite.rs | 61 +++++++++++++++++++++++++++++++ 8 files changed, 206 insertions(+), 152 deletions(-) delete mode 100644 src/texture/atlas.rs create mode 100644 src/texture/directional.rs create mode 100644 src/texture/sprite.rs diff --git a/Cargo.lock b/Cargo.lock index 0347601..9abb849 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "lazy_static" version = "1.5.0" @@ -184,6 +190,8 @@ dependencies = [ "pathfinding", "rand", "sdl2", + "serde", + "serde_json", "spin_sleep", "thiserror 1.0.69", "tracing", @@ -304,6 +312,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "sdl2" version = "0.38.0" @@ -335,6 +349,38 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 1547be9..ebabc4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ once_cell = "1.21.3" thiserror = "1.0" anyhow = "1.0" glam = "0.30.4" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.141" [profile.release] lto = true diff --git a/src/texture/animated.rs b/src/texture/animated.rs index cbcca5c..c40025c 100644 --- a/src/texture/animated.rs +++ b/src/texture/animated.rs @@ -1,34 +1,24 @@ //! This module provides a simple animation and atlas system for textures. +use anyhow::Result; use glam::IVec2; -use sdl2::{ - render::{Canvas, Texture}, - video::Window, -}; +use sdl2::render::WindowCanvas; -use crate::entity::direction::Direction; -use crate::texture::atlas::AtlasTexture; -use crate::texture::FrameDrawn; +use crate::texture::sprite::AtlasTile; /// An animated texture using a texture atlas. -pub struct AnimatedAtlasTexture { - pub atlas: AtlasTexture, +#[derive(Clone)] +pub struct AnimatedTexture { + pub frames: Vec, pub ticks_per_frame: u32, pub ticker: u32, pub reversed: bool, pub paused: bool, } -impl AnimatedAtlasTexture { - pub fn new( - texture: Texture<'static>, - ticks_per_frame: u32, - frame_count: u32, - width: u32, - height: u32, - offset: Option, - ) -> Self { - AnimatedAtlasTexture { - atlas: AtlasTexture::new(texture, frame_count, width, height, offset), +impl AnimatedTexture { + pub fn new(frames: Vec, ticks_per_frame: u32) -> Self { + AnimatedTexture { + frames, ticks_per_frame, ticker: 0, reversed: false, @@ -36,38 +26,25 @@ impl AnimatedAtlasTexture { } } - fn current_frame(&self) -> u32 { - self.ticker / self.ticks_per_frame - } - /// Advances the animation by one tick, unless paused. pub fn tick(&mut self) { - if self.paused { + if self.paused || self.ticks_per_frame == 0 { return; } - if self.reversed { - if self.ticker > 0 { - self.ticker -= 1; - } - if self.ticker == 0 { - self.reversed = !self.reversed; - } - } else { - self.ticker += 1; - if self.ticker + 1 == self.ticks_per_frame * self.atlas.frame_count { - self.reversed = !self.reversed; - } + + self.ticker += 1; + } + + pub fn current_tile(&self) -> &AtlasTile { + if self.ticks_per_frame == 0 { + return &self.frames[0]; } + let frame_index = (self.ticker / self.ticks_per_frame) as usize % self.frames.len(); + &self.frames[frame_index] } - pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) { - self.atlas.set_color_modulation(r, g, b); - } -} - -impl FrameDrawn for AnimatedAtlasTexture { - fn render(&self, canvas: &mut Canvas, position: IVec2, direction: Direction, frame: Option) { - let frame = frame.unwrap_or_else(|| self.current_frame()); - self.atlas.render(canvas, position, direction, Some(frame)); + pub fn render(&self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> { + let tile = self.current_tile(); + tile.render(canvas, dest) } } diff --git a/src/texture/atlas.rs b/src/texture/atlas.rs deleted file mode 100644 index 0d9976c..0000000 --- a/src/texture/atlas.rs +++ /dev/null @@ -1,74 +0,0 @@ -use glam::IVec2; -use sdl2::{ - rect::Rect, - render::{Canvas, Texture}, - video::Window, -}; - -use crate::{entity::direction::Direction, texture::FrameDrawn}; - -/// Unsafely converts a Texture with any lifetime to a 'static lifetime. -/// Only use this if you guarantee the renderer/context will never be dropped! -pub unsafe fn texture_to_static<'a>(texture: Texture<'a>) -> Texture<'static> { - std::mem::transmute::, Texture<'static>>(texture) -} - -/// A texture atlas abstraction for static (non-animated) rendering. -pub struct AtlasTexture { - pub raw_texture: Texture<'static>, - pub offset: IVec2, - pub frame_count: u32, - pub frame_width: u32, - pub frame_height: u32, -} - -impl AtlasTexture { - pub fn new(texture: Texture<'static>, frame_count: u32, frame_width: u32, frame_height: u32, offset: Option) -> Self { - AtlasTexture { - raw_texture: texture, - frame_count, - frame_width, - frame_height, - offset: offset.unwrap_or(IVec2::new(0, 0)).into(), - } - } - - pub fn get_frame_rect(&self, frame: u32) -> Option { - if frame >= self.frame_count { - return None; - } - Some(Rect::new( - frame as i32 * self.frame_width as i32, - 0, - self.frame_width, - self.frame_height, - )) - } - - pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) { - self.raw_texture.set_color_mod(r, g, b); - } -} - -impl FrameDrawn for AtlasTexture { - fn render(&self, canvas: &mut Canvas, position: IVec2, direction: Direction, frame: Option) { - let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0)); - let canvas_destination_rect = Rect::new( - position.x + self.offset.x, - position.y + self.offset.y, - self.frame_width, - self.frame_height, - ); - canvas - .copy_ex( - &self.raw_texture, - texture_source_frame_rect, - Some(canvas_destination_rect), - direction.angle(), - None, - false, - false, - ) - .expect("Could not render texture on canvas"); - } -} diff --git a/src/texture/blinking.rs b/src/texture/blinking.rs index 59ad3a1..9747e35 100644 --- a/src/texture/blinking.rs +++ b/src/texture/blinking.rs @@ -1,16 +1,13 @@ //! A texture that blinks on/off for a specified number of ticks. +use anyhow::Result; use glam::IVec2; -use sdl2::{ - render::{Canvas, Texture}, - video::Window, -}; +use sdl2::render::WindowCanvas; -use crate::texture::atlas::AtlasTexture; -use crate::texture::FrameDrawn; -use crate::{entity::direction::Direction, texture::atlas::texture_to_static}; +use crate::texture::animated::AnimatedTexture; +#[derive(Clone)] pub struct BlinkingTexture { - pub atlas: AtlasTexture, + pub animation: AnimatedTexture, pub on_ticks: u32, pub off_ticks: u32, pub ticker: u32, @@ -18,17 +15,9 @@ pub struct BlinkingTexture { } impl BlinkingTexture { - pub fn new( - texture: Texture<'_>, - frame_count: u32, - width: u32, - height: u32, - offset: Option, - on_ticks: u32, - off_ticks: u32, - ) -> Self { + pub fn new(animation: AnimatedTexture, on_ticks: u32, off_ticks: u32) -> Self { BlinkingTexture { - atlas: AtlasTexture::new(unsafe { texture_to_static(texture) }, frame_count, width, height, offset), + animation, on_ticks, off_ticks, ticker: 0, @@ -38,6 +27,7 @@ impl BlinkingTexture { /// Advances the blinking state by one tick. pub fn tick(&mut self) { + self.animation.tick(); self.ticker += 1; if self.visible && self.ticker >= self.on_ticks { self.visible = false; @@ -48,15 +38,12 @@ impl BlinkingTexture { } } - pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) { - self.atlas.set_color_modulation(r, g, b); - } -} - -impl FrameDrawn for BlinkingTexture { - fn render(&self, canvas: &mut Canvas, position: IVec2, direction: Direction, frame: Option) { + /// Renders the blinking texture. + pub fn render(&self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> { if self.visible { - self.atlas.render(canvas, position, direction, frame); + self.animation.render(canvas, dest) + } else { + Ok(()) } } } diff --git a/src/texture/directional.rs b/src/texture/directional.rs new file mode 100644 index 0000000..f914af8 --- /dev/null +++ b/src/texture/directional.rs @@ -0,0 +1,52 @@ +//! A texture that changes based on the direction of an entity. +use crate::entity::direction::Direction; +use crate::texture::sprite::AtlasTile; +use anyhow::Result; +use glam::IVec2; +use sdl2::render::WindowCanvas; + +pub struct DirectionalAnimatedTexture { + pub up: Vec, + pub down: Vec, + pub left: Vec, + pub right: Vec, + pub ticker: u32, + pub ticks_per_frame: u32, +} + +impl DirectionalAnimatedTexture { + pub fn new( + up: Vec, + down: Vec, + left: Vec, + right: Vec, + ticks_per_frame: u32, + ) -> Self { + Self { + up, + down, + left, + right, + ticker: 0, + ticks_per_frame, + } + } + + pub fn tick(&mut self) { + self.ticker += 1; + } + + pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect, direction: Direction) -> Result<()> { + let frames = match direction { + Direction::Up => &self.up, + Direction::Down => &self.down, + Direction::Left => &self.left, + Direction::Right => &self.right, + }; + + let frame_index = (self.ticker / self.ticks_per_frame) as usize % frames.len(); + let tile = &frames[frame_index]; + + tile.render(canvas, dest) + } +} diff --git a/src/texture/mod.rs b/src/texture/mod.rs index c1e49fc..cd9507d 100644 --- a/src/texture/mod.rs +++ b/src/texture/mod.rs @@ -1,13 +1,16 @@ use glam::IVec2; use sdl2::{render::Canvas, video::Window}; -use crate::entity::direction::Direction; +use std::rc::Rc; -/// Trait for drawable atlas-based textures -pub trait FrameDrawn { - fn render(&self, canvas: &mut Canvas, position: IVec2, direction: Direction, frame: Option); -} +use crate::entity::direction::Direction; +use crate::texture::sprite::{AtlasTile, SpriteAtlas}; pub mod animated; -pub mod atlas; pub mod blinking; +pub mod directional; +pub mod sprite; + +pub fn get_atlas_tile(atlas: &Rc, name: &str) -> AtlasTile { + SpriteAtlas::get_tile(atlas, name).unwrap_or_else(|| panic!("Could not find tile {}", name)) +} diff --git a/src/texture/sprite.rs b/src/texture/sprite.rs new file mode 100644 index 0000000..ffe4e71 --- /dev/null +++ b/src/texture/sprite.rs @@ -0,0 +1,61 @@ +use anyhow::Result; +use glam::U16Vec2; +use sdl2::rect::Rect; +use sdl2::render::{Texture, WindowCanvas}; +use serde::Deserialize; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Clone, Debug, Deserialize)] +pub struct AtlasMapper { + pub frames: HashMap, +} + +#[derive(Copy, Clone, Debug, Deserialize)] +pub struct MapperFrame { + pub x: u16, + pub y: u16, + pub width: u16, + pub height: u16, +} + +#[derive(Clone)] +pub struct AtlasTile { + pub atlas: Rc, + pub pos: U16Vec2, + pub size: U16Vec2, +} + +impl AtlasTile { + pub fn render(&self, canvas: &mut WindowCanvas, dest: Rect) -> Result<()> { + let src = Rect::new(self.pos.x as i32, self.pos.y as i32, self.size.x as u32, self.size.y as u32); + canvas.copy(&self.atlas.texture, src, dest).map_err(anyhow::Error::msg)?; + Ok(()) + } +} + +pub struct SpriteAtlas { + texture: Texture<'static>, + tiles: HashMap, +} + +impl SpriteAtlas { + pub fn new(texture: Texture<'static>, mapper: AtlasMapper) -> Self { + Self { + texture, + tiles: mapper.frames, + } + } + + pub fn get_tile(atlas: &Rc, name: &str) -> Option { + atlas.tiles.get(name).map(|frame| AtlasTile { + atlas: atlas.clone(), + pos: U16Vec2::new(frame.x, frame.y), + size: U16Vec2::new(frame.width, frame.height), + }) + } +} + +pub unsafe fn texture_to_static<'a>(texture: Texture<'a>) -> Texture<'static> { + std::mem::transmute(texture) +}