use embedded_graphics_core::pixelcolor::Rgb565;
use heapless::Vec as HeaplessVec;
#[derive(Debug, Clone, Copy)]
pub struct Texture {
pub data: &'static [Rgb565],
pub width: u32,
pub height: u32,
width_mask: u32,
height_mask: u32,
}
impl Texture {
pub fn new(data: &'static [Rgb565], width: u32, height: u32) -> Self {
assert!(width.is_power_of_two(), "Texture width must be power of 2");
assert!(
height.is_power_of_two(),
"Texture height must be power of 2"
);
assert_eq!(
data.len(),
(width * height) as usize,
"Texture data length must match width × height"
);
Self {
data,
width,
height,
width_mask: width - 1,
height_mask: height - 1,
}
}
#[inline]
pub fn sample(&self, u: f32, v: f32) -> Rgb565 {
let tex_x = (u * self.width as f32) as u32;
let tex_y = (v * self.height as f32) as u32;
let tex_x = tex_x & self.width_mask;
let tex_y = tex_y & self.height_mask;
self.data[(tex_y * self.width + tex_x) as usize]
}
#[inline]
pub fn sample_fixed(&self, u_fixed: u32, v_fixed: u32) -> Rgb565 {
let tex_x = ((u_fixed >> 16) * self.width) >> 16;
let tex_y = ((v_fixed >> 16) * self.height) >> 16;
let tex_x = tex_x & self.width_mask;
let tex_y = tex_y & self.height_mask;
self.data[(tex_y * self.width + tex_x) as usize]
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
}
const ANIMATION_ID_FLAG: u32 = 0x8000_0000;
const ANIMATION_ID_MASK: u32 = !ANIMATION_ID_FLAG;
const MAX_ANIMATIONS: usize = 16;
const MAX_ANIMATION_FRAMES: usize = 8;
#[derive(Debug, Clone, Copy)]
struct TextureAnimation {
frames: [u32; MAX_ANIMATION_FRAMES],
frame_count: u8,
ticks_per_frame: u16,
tick_accum: u16,
current_frame: u8,
looping: bool,
}
impl TextureAnimation {
fn new(frame_ids: &[u32], ticks_per_frame: u16, looping: bool) -> Option<Self> {
if frame_ids.is_empty() || frame_ids.len() > MAX_ANIMATION_FRAMES {
return None;
}
let mut frames = [0u32; MAX_ANIMATION_FRAMES];
for (i, frame_id) in frame_ids.iter().copied().enumerate() {
frames[i] = frame_id;
}
Some(Self {
frames,
frame_count: frame_ids.len() as u8,
ticks_per_frame: ticks_per_frame.max(1),
tick_accum: 0,
current_frame: 0,
looping,
})
}
#[inline]
fn current_texture_id(&self) -> u32 {
self.frames[self.current_frame as usize]
}
fn tick(&mut self, ticks: u16) {
if self.frame_count <= 1 {
return;
}
let mut accum = self.tick_accum.saturating_add(ticks);
while accum >= self.ticks_per_frame {
accum -= self.ticks_per_frame;
if self.current_frame + 1 < self.frame_count {
self.current_frame += 1;
} else if self.looping {
self.current_frame = 0;
} else {
accum = 0;
break;
}
}
self.tick_accum = accum;
}
}
pub struct TextureManager<const N: usize> {
textures: HeaplessVec<Texture, N>,
animations: HeaplessVec<TextureAnimation, MAX_ANIMATIONS>,
}
impl<const N: usize> TextureManager<N> {
pub fn new() -> Self {
Self {
textures: HeaplessVec::new(),
animations: HeaplessVec::new(),
}
}
pub fn add_texture(&mut self, texture: Texture) -> Option<u32> {
self.textures.push(texture).ok()?;
Some((self.textures.len() - 1) as u32)
}
pub fn get(&self, id: u32) -> Option<&Texture> {
let resolved = self.resolve_texture_id(id)?;
self.textures.get(resolved as usize)
}
pub fn add_animation(
&mut self,
frame_ids: &[u32],
ticks_per_frame: u16,
looping: bool,
) -> Option<u32> {
for frame_id in frame_ids {
let resolved = self.resolve_texture_id(*frame_id)?;
if resolved as usize >= self.textures.len() {
return None;
}
}
let animation = TextureAnimation::new(frame_ids, ticks_per_frame, looping)?;
self.animations.push(animation).ok()?;
let index = (self.animations.len() - 1) as u32;
Some(ANIMATION_ID_FLAG | (index & ANIMATION_ID_MASK))
}
pub fn tick(&mut self, ticks: u16) {
for animation in &mut self.animations {
animation.tick(ticks);
}
}
#[inline]
pub fn is_animation_id(id: u32) -> bool {
(id & ANIMATION_ID_FLAG) != 0
}
pub fn resolve_texture_id(&self, id: u32) -> Option<u32> {
if !Self::is_animation_id(id) {
return Some(id);
}
let anim_idx = (id & ANIMATION_ID_MASK) as usize;
let animation = self.animations.get(anim_idx)?;
Some(animation.current_texture_id())
}
pub fn len(&self) -> usize {
self.textures.len()
}
pub fn is_empty(&self) -> bool {
self.textures.is_empty()
}
pub fn is_full(&self) -> bool {
self.textures.len() >= N
}
}
impl<const N: usize> Default for TextureManager<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
#[test]
fn test_texture_creation() {
static DATA: [Rgb565; 64] = [Rgb565::CSS_RED; 64];
let texture = Texture::new(&DATA, 8, 8);
assert_eq!(texture.width, 8);
assert_eq!(texture.height, 8);
assert_eq!(texture.dimensions(), (8, 8));
}
#[test]
#[should_panic(expected = "width must be power of 2")]
fn test_texture_non_power_of_2_width() {
static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
let _texture = Texture::new(&DATA, 10, 6); }
#[test]
#[should_panic(expected = "height must be power of 2")]
fn test_texture_non_power_of_2_height() {
static DATA: [Rgb565; 48] = [Rgb565::CSS_RED; 48];
let _texture = Texture::new(&DATA, 8, 6); }
#[test]
#[should_panic(expected = "length must match")]
fn test_texture_wrong_data_length() {
static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
let _texture = Texture::new(&DATA, 8, 8); }
#[test]
fn test_texture_sampling() {
static DATA: [Rgb565; 16] = [
Rgb565::CSS_RED,
Rgb565::CSS_GREEN,
Rgb565::CSS_BLUE,
Rgb565::CSS_YELLOW,
Rgb565::CSS_CYAN,
Rgb565::CSS_MAGENTA,
Rgb565::CSS_WHITE,
Rgb565::CSS_BLACK,
Rgb565::CSS_RED,
Rgb565::CSS_GREEN,
Rgb565::CSS_BLUE,
Rgb565::CSS_YELLOW,
Rgb565::CSS_CYAN,
Rgb565::CSS_MAGENTA,
Rgb565::CSS_WHITE,
Rgb565::CSS_BLACK,
];
let texture = Texture::new(&DATA, 4, 4);
let tl = texture.sample(0.0, 0.0);
assert_eq!(tl, Rgb565::CSS_RED);
let mid = texture.sample(0.5, 0.5);
assert_eq!(mid, Rgb565::CSS_BLUE);
}
#[test]
fn test_texture_wrapping() {
static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
let texture = Texture::new(&DATA, 4, 4);
let wrapped = texture.sample(1.5, 1.5);
assert_eq!(wrapped, Rgb565::CSS_RED);
}
#[test]
fn test_texture_manager() {
static DATA1: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
static DATA2: [Rgb565; 64] = [Rgb565::CSS_GREEN; 64];
let mut manager = TextureManager::<4>::new();
assert!(manager.is_empty());
assert!(!manager.is_full());
let id1 = manager.add_texture(Texture::new(&DATA1, 4, 4));
assert_eq!(id1, Some(0));
assert_eq!(manager.len(), 1);
let id2 = manager.add_texture(Texture::new(&DATA2, 8, 8));
assert_eq!(id2, Some(1));
assert_eq!(manager.len(), 2);
let tex1 = manager.get(0).unwrap();
assert_eq!(tex1.width, 4);
let tex2 = manager.get(1).unwrap();
assert_eq!(tex2.width, 8);
}
#[test]
fn test_texture_manager_full() {
static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
let mut manager = TextureManager::<2>::new();
assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
assert!(manager.is_full());
assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_none());
}
#[test]
fn test_texture_animation_looping_sequence() {
static RED: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
static GREEN: [Rgb565; 16] = [Rgb565::CSS_GREEN; 16];
static BLUE: [Rgb565; 16] = [Rgb565::CSS_BLUE; 16];
let mut manager = TextureManager::<8>::new();
let red = manager.add_texture(Texture::new(&RED, 4, 4)).unwrap();
let green = manager.add_texture(Texture::new(&GREEN, 4, 4)).unwrap();
let blue = manager.add_texture(Texture::new(&BLUE, 4, 4)).unwrap();
let anim_id = manager.add_animation(&[red, green, blue], 2, true).unwrap();
assert!(TextureManager::<8>::is_animation_id(anim_id));
assert_eq!(
manager.get(anim_id).unwrap().sample(0.0, 0.0),
Rgb565::CSS_RED
);
manager.tick(2);
assert_eq!(
manager.get(anim_id).unwrap().sample(0.0, 0.0),
Rgb565::CSS_GREEN
);
manager.tick(2);
assert_eq!(
manager.get(anim_id).unwrap().sample(0.0, 0.0),
Rgb565::CSS_BLUE
);
manager.tick(2);
assert_eq!(
manager.get(anim_id).unwrap().sample(0.0, 0.0),
Rgb565::CSS_RED
);
}
#[test]
fn test_texture_animation_non_looping_clamps_final_frame() {
static RED: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
static GREEN: [Rgb565; 16] = [Rgb565::CSS_GREEN; 16];
let mut manager = TextureManager::<4>::new();
let red = manager.add_texture(Texture::new(&RED, 4, 4)).unwrap();
let green = manager.add_texture(Texture::new(&GREEN, 4, 4)).unwrap();
let anim_id = manager.add_animation(&[red, green], 1, false).unwrap();
manager.tick(1);
assert_eq!(
manager.get(anim_id).unwrap().sample(0.0, 0.0),
Rgb565::CSS_GREEN
);
manager.tick(10);
assert_eq!(
manager.get(anim_id).unwrap().sample(0.0, 0.0),
Rgb565::CSS_GREEN
);
}
}