feat: split animated texture away from atlas texture details

This commit is contained in:
2025-07-23 16:25:40 -05:00
parent 5a48e83b1a
commit 66b6cdf01b
4 changed files with 188 additions and 159 deletions

View File

@@ -1,4 +1,4 @@
//! This module provides a simple animation system for textures.
//! This module provides a simple animation and atlas system for textures.
use sdl2::{
rect::Rect,
render::{Canvas, Texture},
@@ -7,144 +7,75 @@ use sdl2::{
use crate::direction::Direction;
/// An animated texture, which is a texture that is rendered as a series of
/// frames.
///
/// This struct manages the state of an animated texture, including the current
/// frame and the number of frames in the animation.
pub struct AnimatedTexture<'a> {
// Parameters
raw_texture: Texture<'a>,
offset: (i32, i32),
ticks_per_frame: u32,
frame_count: u32,
width: u32,
height: u32,
// State
ticker: u32,
reversed: bool,
}
impl<'a> AnimatedTexture<'a> {
pub fn new(
texture: Texture<'a>,
ticks_per_frame: u32,
frame_count: u32,
width: u32,
height: u32,
offset: Option<(i32, i32)>,
) -> Self {
AnimatedTexture {
raw_texture: texture,
ticker: 0,
reversed: false,
ticks_per_frame,
frame_count,
width,
height,
offset: offset.unwrap_or((0, 0)),
}
}
fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame
}
/// Advances the animation by one tick.
///
/// This method updates the internal ticker that tracks the current frame
/// of the animation. The animation automatically reverses direction when
/// it reaches the end, creating a ping-pong effect.
///
/// When `reversed` is `false`, the ticker increments until it reaches
/// the total number of ticks for all frames, then reverses direction.
/// When `reversed` is `true`, the ticker decrements until it reaches 0,
/// then reverses direction again.
pub fn tick(&mut self) {
if self.reversed {
self.ticker -= 1;
if self.ticker == 0 {
self.reversed = !self.reversed;
}
} else {
self.ticker += 1;
if self.ticker + 1 == self.ticks_per_frame * self.frame_count {
self.reversed = !self.reversed;
}
}
}
/// Gets the source rectangle for a specific frame of the animated texture.
///
/// This method calculates the position and dimensions of a frame within the
/// texture atlas. Frames are arranged horizontally in a single row, so the
/// rectangle's x-coordinate is calculated by multiplying the frame index
/// by the frame width.
///
/// # Arguments
///
/// * `frame` - The frame index to get the rectangle for (0-based)
///
/// # Returns
///
/// A `Rect` representing the source rectangle for the specified frame
fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
if frame >= self.frame_count {
return None;
}
Some(Rect::new(
frame as i32 * self.width as i32,
0,
self.width,
self.height,
))
}
pub fn render(
/// Trait for drawable atlas-based textures
pub trait FrameDrawn {
fn render(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
) {
self.render_static(canvas, position, direction, Some(self.current_frame()));
self.tick();
frame: Option<u32>,
);
}
/// A texture atlas abstraction for static (non-animated) rendering.
pub struct AtlasTexture<'a> {
pub raw_texture: Texture<'a>,
pub offset: (i32, i32),
pub frame_count: u32,
pub frame_width: u32,
pub frame_height: u32,
}
impl<'a> AtlasTexture<'a> {
pub fn new(
texture: Texture<'a>,
frame_count: u32,
frame_width: u32,
frame_height: u32,
offset: Option<(i32, i32)>,
) -> Self {
AtlasTexture {
raw_texture: texture,
frame_count,
frame_width,
frame_height,
offset: offset.unwrap_or((0, 0)),
}
}
/// Renders a specific frame of the animated texture to the canvas.
///
/// This method renders a static frame without advancing the animation ticker.
/// It's useful for displaying a specific frame, such as when an entity is stopped
/// or when you want to manually control which frame is displayed.
///
/// # Arguments
///
/// * `canvas` - The SDL canvas to render to
/// * `position` - The pixel position where the texture should be rendered
/// * `direction` - The direction to rotate the texture based on entity facing
/// * `frame` - Optional specific frame to render. If `None`, uses the current frame
///
/// # Panics
///
/// Panics if the specified frame is out of bounds for this texture.
pub fn render_static(
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<'a> FrameDrawn for AtlasTexture<'a> {
fn render(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
) {
let texture_source_frame_rect =
self.get_frame_rect(frame.unwrap_or_else(|| self.current_frame()));
let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0));
let canvas_destination_rect = Rect::new(
position.0 + self.offset.0,
position.1 + self.offset.1,
self.width,
self.height,
self.frame_width,
self.frame_height,
);
canvas
.copy_ex(
&self.raw_texture,
@@ -157,9 +88,88 @@ impl<'a> AnimatedTexture<'a> {
)
.expect("Could not render texture on canvas");
}
}
/// An animated texture using a texture atlas.
pub struct AnimatedAtlasTexture<'a> {
pub atlas: AtlasTexture<'a>,
pub ticks_per_frame: u32,
pub ticker: u32,
pub reversed: bool,
pub paused: bool,
}
impl<'a> AnimatedAtlasTexture<'a> {
pub fn new(
texture: Texture<'a>,
ticks_per_frame: u32,
frame_count: u32,
width: u32,
height: u32,
offset: Option<(i32, i32)>,
) -> Self {
AnimatedAtlasTexture {
atlas: AtlasTexture::new(texture, frame_count, width, height, offset),
ticks_per_frame,
ticker: 0,
reversed: false,
paused: false,
}
}
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 {
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;
}
}
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn is_paused(&self) -> bool {
self.paused
}
/// Sets the color modulation for the texture.
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
self.raw_texture.set_color_mod(r, g, b);
self.atlas.set_color_modulation(r, g, b);
}
}
impl<'a> FrameDrawn for AnimatedAtlasTexture<'a> {
fn render(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
) {
self.atlas.render(
canvas,
position,
direction,
frame.or(Some(self.current_frame())),
);
self.tick();
}
}