feat: atlas decoding & frame acquisition

This commit is contained in:
2025-07-25 12:27:19 -05:00
parent 8cf30cd78d
commit 6ca2e01fba
8 changed files with 206 additions and 152 deletions

46
Cargo.lock generated
View File

@@ -108,6 +108,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -184,6 +190,8 @@ dependencies = [
"pathfinding", "pathfinding",
"rand", "rand",
"sdl2", "sdl2",
"serde",
"serde_json",
"spin_sleep", "spin_sleep",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
@@ -304,6 +312,12 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "sdl2" name = "sdl2"
version = "0.38.0" version = "0.38.0"
@@ -335,6 +349,38 @@ version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 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]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.4" version = "0.1.4"

View File

@@ -18,6 +18,8 @@ once_cell = "1.21.3"
thiserror = "1.0" thiserror = "1.0"
anyhow = "1.0" anyhow = "1.0"
glam = "0.30.4" glam = "0.30.4"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141"
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -1,34 +1,24 @@
//! This module provides a simple animation and atlas system for textures. //! This module provides a simple animation and atlas system for textures.
use anyhow::Result;
use glam::IVec2; use glam::IVec2;
use sdl2::{ use sdl2::render::WindowCanvas;
render::{Canvas, Texture},
video::Window,
};
use crate::entity::direction::Direction; use crate::texture::sprite::AtlasTile;
use crate::texture::atlas::AtlasTexture;
use crate::texture::FrameDrawn;
/// An animated texture using a texture atlas. /// An animated texture using a texture atlas.
pub struct AnimatedAtlasTexture { #[derive(Clone)]
pub atlas: AtlasTexture, pub struct AnimatedTexture {
pub frames: Vec<AtlasTile>,
pub ticks_per_frame: u32, pub ticks_per_frame: u32,
pub ticker: u32, pub ticker: u32,
pub reversed: bool, pub reversed: bool,
pub paused: bool, pub paused: bool,
} }
impl AnimatedAtlasTexture { impl AnimatedTexture {
pub fn new( pub fn new(frames: Vec<AtlasTile>, ticks_per_frame: u32) -> Self {
texture: Texture<'static>, AnimatedTexture {
ticks_per_frame: u32, frames,
frame_count: u32,
width: u32,
height: u32,
offset: Option<IVec2>,
) -> Self {
AnimatedAtlasTexture {
atlas: AtlasTexture::new(texture, frame_count, width, height, offset),
ticks_per_frame, ticks_per_frame,
ticker: 0, ticker: 0,
reversed: false, 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. /// Advances the animation by one tick, unless paused.
pub fn tick(&mut self) { pub fn tick(&mut self) {
if self.paused { if self.paused || self.ticks_per_frame == 0 {
return; return;
} }
if self.reversed {
if self.ticker > 0 { self.ticker += 1;
self.ticker -= 1; }
}
if self.ticker == 0 { pub fn current_tile(&self) -> &AtlasTile {
self.reversed = !self.reversed; if self.ticks_per_frame == 0 {
} return &self.frames[0];
} else {
self.ticker += 1;
if self.ticker + 1 == self.ticks_per_frame * self.atlas.frame_count {
self.reversed = !self.reversed;
}
} }
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) { pub fn render(&self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> {
self.atlas.set_color_modulation(r, g, b); let tile = self.current_tile();
} tile.render(canvas, dest)
}
impl FrameDrawn for AnimatedAtlasTexture {
fn render(&self, canvas: &mut Canvas<Window>, position: IVec2, direction: Direction, frame: Option<u32>) {
let frame = frame.unwrap_or_else(|| self.current_frame());
self.atlas.render(canvas, position, direction, Some(frame));
} }
} }

View File

@@ -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<'a>, 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<IVec2>) -> 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<Rect> {
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<Window>, position: IVec2, direction: Direction, frame: Option<u32>) {
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");
}
}

View File

@@ -1,16 +1,13 @@
//! A texture that blinks on/off for a specified number of ticks. //! A texture that blinks on/off for a specified number of ticks.
use anyhow::Result;
use glam::IVec2; use glam::IVec2;
use sdl2::{ use sdl2::render::WindowCanvas;
render::{Canvas, Texture},
video::Window,
};
use crate::texture::atlas::AtlasTexture; use crate::texture::animated::AnimatedTexture;
use crate::texture::FrameDrawn;
use crate::{entity::direction::Direction, texture::atlas::texture_to_static};
#[derive(Clone)]
pub struct BlinkingTexture { pub struct BlinkingTexture {
pub atlas: AtlasTexture, pub animation: AnimatedTexture,
pub on_ticks: u32, pub on_ticks: u32,
pub off_ticks: u32, pub off_ticks: u32,
pub ticker: u32, pub ticker: u32,
@@ -18,17 +15,9 @@ pub struct BlinkingTexture {
} }
impl BlinkingTexture { impl BlinkingTexture {
pub fn new( pub fn new(animation: AnimatedTexture, on_ticks: u32, off_ticks: u32) -> Self {
texture: Texture<'_>,
frame_count: u32,
width: u32,
height: u32,
offset: Option<IVec2>,
on_ticks: u32,
off_ticks: u32,
) -> Self {
BlinkingTexture { BlinkingTexture {
atlas: AtlasTexture::new(unsafe { texture_to_static(texture) }, frame_count, width, height, offset), animation,
on_ticks, on_ticks,
off_ticks, off_ticks,
ticker: 0, ticker: 0,
@@ -38,6 +27,7 @@ impl BlinkingTexture {
/// Advances the blinking state by one tick. /// Advances the blinking state by one tick.
pub fn tick(&mut self) { pub fn tick(&mut self) {
self.animation.tick();
self.ticker += 1; self.ticker += 1;
if self.visible && self.ticker >= self.on_ticks { if self.visible && self.ticker >= self.on_ticks {
self.visible = false; self.visible = false;
@@ -48,15 +38,12 @@ impl BlinkingTexture {
} }
} }
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) { /// Renders the blinking texture.
self.atlas.set_color_modulation(r, g, b); pub fn render(&self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> {
}
}
impl FrameDrawn for BlinkingTexture {
fn render(&self, canvas: &mut Canvas<Window>, position: IVec2, direction: Direction, frame: Option<u32>) {
if self.visible { if self.visible {
self.atlas.render(canvas, position, direction, frame); self.animation.render(canvas, dest)
} else {
Ok(())
} }
} }
} }

View File

@@ -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<AtlasTile>,
pub down: Vec<AtlasTile>,
pub left: Vec<AtlasTile>,
pub right: Vec<AtlasTile>,
pub ticker: u32,
pub ticks_per_frame: u32,
}
impl DirectionalAnimatedTexture {
pub fn new(
up: Vec<AtlasTile>,
down: Vec<AtlasTile>,
left: Vec<AtlasTile>,
right: Vec<AtlasTile>,
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)
}
}

View File

@@ -1,13 +1,16 @@
use glam::IVec2; use glam::IVec2;
use sdl2::{render::Canvas, video::Window}; use sdl2::{render::Canvas, video::Window};
use crate::entity::direction::Direction; use std::rc::Rc;
/// Trait for drawable atlas-based textures use crate::entity::direction::Direction;
pub trait FrameDrawn { use crate::texture::sprite::{AtlasTile, SpriteAtlas};
fn render(&self, canvas: &mut Canvas<Window>, position: IVec2, direction: Direction, frame: Option<u32>);
}
pub mod animated; pub mod animated;
pub mod atlas;
pub mod blinking; pub mod blinking;
pub mod directional;
pub mod sprite;
pub fn get_atlas_tile(atlas: &Rc<SpriteAtlas>, name: &str) -> AtlasTile {
SpriteAtlas::get_tile(atlas, name).unwrap_or_else(|| panic!("Could not find tile {}", name))
}

61
src/texture/sprite.rs Normal file
View File

@@ -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<String, MapperFrame>,
}
#[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<SpriteAtlas>,
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<String, MapperFrame>,
}
impl SpriteAtlas {
pub fn new(texture: Texture<'static>, mapper: AtlasMapper) -> Self {
Self {
texture,
tiles: mapper.frames,
}
}
pub fn get_tile(atlas: &Rc<SpriteAtlas>, name: &str) -> Option<AtlasTile> {
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)
}