1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
8use heapless::Vec as HeaplessVec;
9
10#[derive(Debug, Clone, Copy)]
15pub struct Texture {
16 pub data: &'static [Rgb565],
18 pub width: u32,
20 pub height: u32,
22 width_mask: u32,
24 height_mask: u32,
26}
27
28impl Texture {
29 pub fn new(data: &'static [Rgb565], width: u32, height: u32) -> Self {
39 assert!(width.is_power_of_two(), "Texture width must be power of 2");
40 assert!(
41 height.is_power_of_two(),
42 "Texture height must be power of 2"
43 );
44 assert_eq!(
45 data.len(),
46 (width * height) as usize,
47 "Texture data length must match width × height"
48 );
49
50 Self {
51 data,
52 width,
53 height,
54 width_mask: width - 1,
55 height_mask: height - 1,
56 }
57 }
58
59 #[inline]
70 pub fn sample(&self, u: f32, v: f32) -> Rgb565 {
71 let tex_x = (u * self.width as f32) as u32;
73 let tex_y = (v * self.height as f32) as u32;
74
75 let tex_x = tex_x & self.width_mask;
77 let tex_y = tex_y & self.height_mask;
78
79 self.data[(tex_y * self.width + tex_x) as usize]
81 }
82
83 #[inline]
91 pub fn sample_fixed(&self, u_fixed: u32, v_fixed: u32) -> Rgb565 {
92 let tex_x = ((u_fixed >> 16) * self.width) >> 16;
95 let tex_y = ((v_fixed >> 16) * self.height) >> 16;
96
97 let tex_x = tex_x & self.width_mask;
99 let tex_y = tex_y & self.height_mask;
100
101 self.data[(tex_y * self.width + tex_x) as usize]
102 }
103
104 #[inline(always)]
108 pub fn sample_affine_q16(&self, u_q16: u32, v_q16: u32) -> Rgb565 {
109 let tex_x = (((u_q16 as u64 * self.width as u64) >> 16) as u32) & self.width_mask;
110 let tex_y = (((v_q16 as u64 * self.height as u64) >> 16) as u32) & self.height_mask;
111 self.data[(tex_y * self.width + tex_x) as usize]
112 }
113
114 #[inline]
119 pub fn sample_affine_2xssaa_q16(&self, u_q16: u32, v_q16: u32) -> Rgb565 {
120 const OFF: u32 = 0x4000; let p0 = self.sample_affine_q16(u_q16.wrapping_sub(OFF), v_q16.wrapping_sub(OFF));
122 let p1 = self.sample_affine_q16(u_q16.wrapping_add(OFF), v_q16.wrapping_sub(OFF));
123 let p2 = self.sample_affine_q16(u_q16.wrapping_sub(OFF), v_q16.wrapping_add(OFF));
124 let p3 = self.sample_affine_q16(u_q16.wrapping_add(OFF), v_q16.wrapping_add(OFF));
125
126 let r = (p0.r() as u32 + p1.r() as u32 + p2.r() as u32 + p3.r() as u32 + 2) >> 2;
127 let g = (p0.g() as u32 + p1.g() as u32 + p2.g() as u32 + p3.g() as u32 + 2) >> 2;
128 let b = (p0.b() as u32 + p1.b() as u32 + p2.b() as u32 + p3.b() as u32 + 2) >> 2;
129
130 Rgb565::new(r as u8, g as u8, b as u8)
131 }
132
133 pub fn sample_affine_scanline_q16(
135 &self,
136 mut u_q16: u32,
137 mut v_q16: u32,
138 du_q16: u32,
139 dv_q16: u32,
140 out_buffer: &mut [Rgb565],
141 ) {
142 for pixel in out_buffer.iter_mut() {
143 *pixel = self.sample_affine_q16(u_q16, v_q16);
144 u_q16 = u_q16.wrapping_add(du_q16);
145 v_q16 = v_q16.wrapping_add(dv_q16);
146 }
147 }
148
149 pub fn dimensions(&self) -> (u32, u32) {
151 (self.width, self.height)
152 }
153}
154
155const ANIMATION_ID_FLAG: u32 = 0x8000_0000;
156const ANIMATION_ID_MASK: u32 = !ANIMATION_ID_FLAG;
157const MAX_ANIMATIONS: usize = 16;
158const MAX_ANIMATION_FRAMES: usize = 8;
159
160#[derive(Debug, Clone, Copy)]
161struct TextureAnimation {
162 frames: [u32; MAX_ANIMATION_FRAMES],
163 frame_count: u8,
164 ticks_per_frame: u16,
165 tick_accum: u16,
166 current_frame: u8,
167 looping: bool,
168}
169
170impl TextureAnimation {
171 fn new(frame_ids: &[u32], ticks_per_frame: u16, looping: bool) -> Option<Self> {
172 if frame_ids.is_empty() || frame_ids.len() > MAX_ANIMATION_FRAMES {
173 return None;
174 }
175 let mut frames = [0u32; MAX_ANIMATION_FRAMES];
176 for (i, frame_id) in frame_ids.iter().copied().enumerate() {
177 frames[i] = frame_id;
178 }
179 Some(Self {
180 frames,
181 frame_count: frame_ids.len() as u8,
182 ticks_per_frame: ticks_per_frame.max(1),
183 tick_accum: 0,
184 current_frame: 0,
185 looping,
186 })
187 }
188
189 #[inline]
190 fn current_texture_id(&self) -> u32 {
191 self.frames[self.current_frame as usize]
192 }
193
194 fn tick(&mut self, ticks: u16) {
195 if self.frame_count <= 1 {
196 return;
197 }
198 let mut accum = self.tick_accum.saturating_add(ticks);
199 while accum >= self.ticks_per_frame {
200 accum -= self.ticks_per_frame;
201 if self.current_frame + 1 < self.frame_count {
202 self.current_frame += 1;
203 } else if self.looping {
204 self.current_frame = 0;
205 } else {
206 accum = 0;
208 break;
209 }
210 }
211 self.tick_accum = accum;
212 }
213}
214
215pub struct TextureManager<const N: usize> {
220 textures: HeaplessVec<Texture, N>,
221 animations: HeaplessVec<TextureAnimation, MAX_ANIMATIONS>,
222}
223
224impl<const N: usize> TextureManager<N> {
225 pub fn new() -> Self {
227 Self {
228 textures: HeaplessVec::new(),
229 animations: HeaplessVec::new(),
230 }
231 }
232
233 pub fn add_texture(&mut self, texture: Texture) -> Option<u32> {
240 self.textures.push(texture).ok()?;
241 Some((self.textures.len() - 1) as u32)
242 }
243
244 pub fn get(&self, id: u32) -> Option<&Texture> {
252 let resolved = self.resolve_texture_id(id)?;
253 self.textures.get(resolved as usize)
254 }
255
256 pub fn add_animation(
260 &mut self,
261 frame_ids: &[u32],
262 ticks_per_frame: u16,
263 looping: bool,
264 ) -> Option<u32> {
265 for frame_id in frame_ids {
267 let resolved = self.resolve_texture_id(*frame_id)?;
268 if resolved as usize >= self.textures.len() {
269 return None;
270 }
271 }
272
273 let animation = TextureAnimation::new(frame_ids, ticks_per_frame, looping)?;
274 self.animations.push(animation).ok()?;
275 let index = (self.animations.len() - 1) as u32;
276 Some(ANIMATION_ID_FLAG | (index & ANIMATION_ID_MASK))
277 }
278
279 pub fn tick(&mut self, ticks: u16) {
281 for animation in &mut self.animations {
282 animation.tick(ticks);
283 }
284 }
285
286 #[inline]
288 pub fn is_animation_id(id: u32) -> bool {
289 (id & ANIMATION_ID_FLAG) != 0
290 }
291
292 pub fn resolve_texture_id(&self, id: u32) -> Option<u32> {
297 if !Self::is_animation_id(id) {
298 return Some(id);
299 }
300 let anim_idx = (id & ANIMATION_ID_MASK) as usize;
301 let animation = self.animations.get(anim_idx)?;
302 Some(animation.current_texture_id())
303 }
304
305 pub fn len(&self) -> usize {
307 self.textures.len()
308 }
309
310 pub fn is_empty(&self) -> bool {
312 self.textures.is_empty()
313 }
314
315 pub fn is_full(&self) -> bool {
317 self.textures.len() >= N
318 }
319}
320
321impl<const N: usize> Default for TextureManager<N> {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 extern crate std;
330 use super::*;
331 use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
332
333 #[test]
334 fn test_texture_creation() {
335 static DATA: [Rgb565; 64] = [Rgb565::CSS_RED; 64];
336 let texture = Texture::new(&DATA, 8, 8);
337
338 assert_eq!(texture.width, 8);
339 assert_eq!(texture.height, 8);
340 assert_eq!(texture.dimensions(), (8, 8));
341 }
342
343 #[test]
344 #[should_panic(expected = "width must be power of 2")]
345 fn test_texture_non_power_of_2_width() {
346 static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
347 let _texture = Texture::new(&DATA, 10, 6); }
349
350 #[test]
351 #[should_panic(expected = "height must be power of 2")]
352 fn test_texture_non_power_of_2_height() {
353 static DATA: [Rgb565; 48] = [Rgb565::CSS_RED; 48];
354 let _texture = Texture::new(&DATA, 8, 6); }
356
357 #[test]
358 #[should_panic(expected = "length must match")]
359 fn test_texture_wrong_data_length() {
360 static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
361 let _texture = Texture::new(&DATA, 8, 8); }
363
364 #[test]
365 fn test_texture_sampling() {
366 static DATA: [Rgb565; 16] = [
367 Rgb565::CSS_RED,
368 Rgb565::CSS_GREEN,
369 Rgb565::CSS_BLUE,
370 Rgb565::CSS_YELLOW,
371 Rgb565::CSS_CYAN,
372 Rgb565::CSS_MAGENTA,
373 Rgb565::CSS_WHITE,
374 Rgb565::CSS_BLACK,
375 Rgb565::CSS_RED,
376 Rgb565::CSS_GREEN,
377 Rgb565::CSS_BLUE,
378 Rgb565::CSS_YELLOW,
379 Rgb565::CSS_CYAN,
380 Rgb565::CSS_MAGENTA,
381 Rgb565::CSS_WHITE,
382 Rgb565::CSS_BLACK,
383 ];
384
385 let texture = Texture::new(&DATA, 4, 4);
386
387 let tl = texture.sample(0.0, 0.0);
389 assert_eq!(tl, Rgb565::CSS_RED);
390
391 let mid = texture.sample(0.5, 0.5);
393 assert_eq!(mid, Rgb565::CSS_BLUE);
394 }
395
396 #[test]
397 fn test_texture_wrapping() {
398 static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
399 let texture = Texture::new(&DATA, 4, 4);
400
401 let wrapped = texture.sample(1.5, 1.5);
403 assert_eq!(wrapped, Rgb565::CSS_RED);
404 }
405
406 #[test]
407 fn test_texture_manager() {
408 static DATA1: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
409 static DATA2: [Rgb565; 64] = [Rgb565::CSS_GREEN; 64];
410
411 let mut manager = TextureManager::<4>::new();
412
413 assert!(manager.is_empty());
414 assert!(!manager.is_full());
415
416 let id1 = manager.add_texture(Texture::new(&DATA1, 4, 4));
417 assert_eq!(id1, Some(0));
418 assert_eq!(manager.len(), 1);
419
420 let id2 = manager.add_texture(Texture::new(&DATA2, 8, 8));
421 assert_eq!(id2, Some(1));
422 assert_eq!(manager.len(), 2);
423
424 let tex1 = manager.get(0).unwrap();
426 assert_eq!(tex1.width, 4);
427
428 let tex2 = manager.get(1).unwrap();
429 assert_eq!(tex2.width, 8);
430 }
431
432 #[test]
433 fn test_texture_manager_full() {
434 static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
435
436 let mut manager = TextureManager::<2>::new();
437
438 assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
440 assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
441 assert!(manager.is_full());
442
443 assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_none());
445 }
446
447 #[test]
448 fn test_texture_animation_looping_sequence() {
449 static RED: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
450 static GREEN: [Rgb565; 16] = [Rgb565::CSS_GREEN; 16];
451 static BLUE: [Rgb565; 16] = [Rgb565::CSS_BLUE; 16];
452
453 let mut manager = TextureManager::<8>::new();
454 let red = manager.add_texture(Texture::new(&RED, 4, 4)).unwrap();
455 let green = manager.add_texture(Texture::new(&GREEN, 4, 4)).unwrap();
456 let blue = manager.add_texture(Texture::new(&BLUE, 4, 4)).unwrap();
457
458 let anim_id = manager.add_animation(&[red, green, blue], 2, true).unwrap();
459 assert!(TextureManager::<8>::is_animation_id(anim_id));
460
461 assert_eq!(
462 manager.get(anim_id).unwrap().sample(0.0, 0.0),
463 Rgb565::CSS_RED
464 );
465 manager.tick(2);
466 assert_eq!(
467 manager.get(anim_id).unwrap().sample(0.0, 0.0),
468 Rgb565::CSS_GREEN
469 );
470 manager.tick(2);
471 assert_eq!(
472 manager.get(anim_id).unwrap().sample(0.0, 0.0),
473 Rgb565::CSS_BLUE
474 );
475 manager.tick(2);
476 assert_eq!(
477 manager.get(anim_id).unwrap().sample(0.0, 0.0),
478 Rgb565::CSS_RED
479 );
480 }
481
482 #[test]
483 fn test_texture_animation_non_looping_clamps_final_frame() {
484 static RED: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
485 static GREEN: [Rgb565; 16] = [Rgb565::CSS_GREEN; 16];
486
487 let mut manager = TextureManager::<4>::new();
488 let red = manager.add_texture(Texture::new(&RED, 4, 4)).unwrap();
489 let green = manager.add_texture(Texture::new(&GREEN, 4, 4)).unwrap();
490
491 let anim_id = manager.add_animation(&[red, green], 1, false).unwrap();
492 manager.tick(1);
493 assert_eq!(
494 manager.get(anim_id).unwrap().sample(0.0, 0.0),
495 Rgb565::CSS_GREEN
496 );
497 manager.tick(10);
498 assert_eq!(
499 manager.get(anim_id).unwrap().sample(0.0, 0.0),
500 Rgb565::CSS_GREEN
501 );
502 }
503
504 #[test]
505 fn test_texture_sample_affine_q16() {
506 static DATA: [Rgb565; 16] = [
507 Rgb565::CSS_RED,
508 Rgb565::CSS_GREEN,
509 Rgb565::CSS_BLUE,
510 Rgb565::CSS_YELLOW,
511 Rgb565::CSS_CYAN,
512 Rgb565::CSS_MAGENTA,
513 Rgb565::CSS_WHITE,
514 Rgb565::CSS_BLACK,
515 Rgb565::CSS_RED,
516 Rgb565::CSS_GREEN,
517 Rgb565::CSS_BLUE,
518 Rgb565::CSS_YELLOW,
519 Rgb565::CSS_CYAN,
520 Rgb565::CSS_MAGENTA,
521 Rgb565::CSS_WHITE,
522 Rgb565::CSS_BLACK,
523 ];
524 let texture = Texture::new(&DATA, 4, 4);
525
526 let sample = texture.sample_affine_q16(32768, 32768);
528 assert_eq!(sample, Rgb565::CSS_BLUE);
529 }
530
531 #[test]
532 fn test_texture_sample_affine_scanline_q16() {
533 static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
534 let texture = Texture::new(&DATA, 4, 4);
535
536 let mut scanline = [Rgb565::CSS_BLACK; 4];
537 texture.sample_affine_scanline_q16(0, 0, 16384, 0, &mut scanline);
538 assert_eq!(scanline[0], Rgb565::CSS_RED);
539 }
540}