1use crate::geometry::Rect;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum ImageFit {
5 Stretch,
6 Center,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct ImageRef<'a> {
11 pub width: u32,
12 pub height: u32,
13 pub pixels: &'a [u16],
14}
15
16impl<'a> ImageRef<'a> {
17 pub const fn new(width: u32, height: u32, pixels: &'a [u16]) -> Self {
18 Self {
19 width,
20 height,
21 pixels,
22 }
23 }
24
25 pub fn bounds_at(&self, rect: Rect, fit: ImageFit) -> Rect {
26 match fit {
27 ImageFit::Stretch => rect,
28 ImageFit::Center => {
29 let x = rect.x + rect.w.saturating_sub(self.width) as i32 / 2;
30 let y = rect.y + rect.h.saturating_sub(self.height) as i32 / 2;
31 Rect::new(x, y, self.width.min(rect.w), self.height.min(rect.h))
32 }
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum TileMode {
39 None,
40 Repeat,
41 Mirror,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct TileRef<'a> {
46 pub width: u32,
47 pub height: u32,
48 pub pixels: &'a [u16],
49 pub mode: TileMode,
50}
51
52impl<'a> TileRef<'a> {
53 pub const fn new(width: u32, height: u32, pixels: &'a [u16], mode: TileMode) -> Self {
54 Self {
55 width,
56 height,
57 pixels,
58 mode,
59 }
60 }
61
62 pub const fn from_image(image: ImageRef<'a>, mode: TileMode) -> Self {
63 Self {
64 width: image.width,
65 height: image.height,
66 pixels: image.pixels,
67 mode,
68 }
69 }
70
71 pub fn get_pixel(&self, u: i32, v: i32) -> Option<embedded_graphics_core::pixelcolor::Rgb565> {
72 let w = self.width as i32;
73 let h = self.height as i32;
74 if w <= 0 || h <= 0 {
75 return None;
76 }
77
78 let (u_final, v_final) = match self.mode {
79 TileMode::None => {
80 if u < 0 || v < 0 || u >= w || v >= h {
81 return None;
82 }
83 (u as usize, v as usize)
84 }
85 TileMode::Repeat => (u.rem_euclid(w) as usize, v.rem_euclid(h) as usize),
86 TileMode::Mirror => {
87 let m_u = u.rem_euclid(2 * w);
88 let m_v = v.rem_euclid(2 * h);
89 let final_u = if m_u >= w { 2 * w - 1 - m_u } else { m_u };
90 let final_v = if m_v >= h { 2 * h - 1 - m_v } else { m_v };
91 (final_u as usize, final_v as usize)
92 }
93 };
94
95 let idx = v_final * self.width as usize + u_final;
96 self.pixels.get(idx).map(|&raw| {
97 embedded_graphics_core::pixelcolor::Rgb565::new(
98 ((raw >> 11) & 0x1F) as u8,
99 ((raw >> 5) & 0x3F) as u8,
100 (raw & 0x1F) as u8,
101 )
102 })
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub struct SpriteSheet<'a> {
108 pub image: ImageRef<'a>,
109 pub sprite_w: u32,
110 pub sprite_h: u32,
111 pub columns: u32,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct ReelFrame {
116 pub sprite_index: u16,
117 pub duration_ms: u16,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub struct ReelPlayer<'a> {
122 pub sheet: SpriteSheet<'a>,
123 pub frames: &'a [ReelFrame],
124 pub repeat: bool,
125 current: usize,
126 elapsed_in_frame_ms: u32,
127 finished: bool,
128}
129
130impl<'a> ReelPlayer<'a> {
131 pub const fn new(sheet: SpriteSheet<'a>, frames: &'a [ReelFrame], repeat: bool) -> Self {
132 Self {
133 sheet,
134 frames,
135 repeat,
136 current: 0,
137 elapsed_in_frame_ms: 0,
138 finished: false,
139 }
140 }
141
142 pub fn tick(&mut self, dt_ms: u32) {
143 if self.frames.is_empty() || self.finished {
144 return;
145 }
146 self.elapsed_in_frame_ms = self.elapsed_in_frame_ms.saturating_add(dt_ms);
147 loop {
148 let frame = self.frames[self.current];
149 let frame_ms = u32::from(frame.duration_ms).max(1);
150 if self.elapsed_in_frame_ms < frame_ms {
151 break;
152 }
153 self.elapsed_in_frame_ms -= frame_ms;
154 if self.current + 1 < self.frames.len() {
155 self.current += 1;
156 continue;
157 }
158 if self.repeat {
159 self.current = 0;
160 } else {
161 self.finished = true;
162 }
163 break;
164 }
165 }
166
167 pub const fn is_finished(&self) -> bool {
168 self.finished
169 }
170
171 pub fn restart(&mut self) {
172 self.current = 0;
173 self.elapsed_in_frame_ms = 0;
174 self.finished = false;
175 }
176
177 pub fn current_sprite_rect(&self) -> Option<Rect> {
178 let frame = self.frames.get(self.current)?;
179 Some(self.sheet.sprite_rect(frame.sprite_index as u32))
180 }
181}
182
183impl<'a> SpriteSheet<'a> {
184 pub const fn new(image: ImageRef<'a>, sprite_w: u32, sprite_h: u32) -> Self {
185 let columns = match image.width.checked_div(sprite_w) {
186 Some(c) => c,
187 None => 1,
188 };
189 Self {
190 image,
191 sprite_w,
192 sprite_h,
193 columns,
194 }
195 }
196
197 pub fn sprite_rect(&self, index: u32) -> Rect {
198 let columns = self.columns.max(1);
199 let col = index % columns;
200 let row = index / columns;
201 Rect::new(
202 (col * self.sprite_w) as i32,
203 (row * self.sprite_h) as i32,
204 self.sprite_w,
205 self.sprite_h,
206 )
207 }
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
211pub struct ImageAtlasEntry {
212 pub id: u16,
213 pub rect: Rect,
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
217pub struct ImageAtlas<'a> {
218 pub image: ImageRef<'a>,
219 pub entries: &'a [ImageAtlasEntry],
220}
221
222impl<'a> ImageAtlas<'a> {
223 pub const fn new(image: ImageRef<'a>, entries: &'a [ImageAtlasEntry]) -> Self {
224 Self { image, entries }
225 }
226
227 pub fn rect_for(&self, id: u16) -> Option<Rect> {
228 self.entries
229 .iter()
230 .find(|entry| entry.id == id)
231 .map(|e| e.rect)
232 }
233}
234
235#[cfg(all(feature = "std", feature = "image-decode"))]
236#[derive(Clone, Debug, PartialEq, Eq)]
237pub enum ImageDecodeError {
238 InvalidHeader,
239 Unsupported,
240 InvalidData,
241 Capacity,
242}
243
244#[cfg(all(feature = "std", feature = "image-decode"))]
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub enum EncodedImageFormat {
247 PpmAscii,
248}
249
250#[cfg(all(feature = "std", feature = "image-decode"))]
251pub trait ImageDecoder {
252 fn decode<const N: usize>(
253 &self,
254 format: EncodedImageFormat,
255 data: &str,
256 out_pixels: &mut heapless::Vec<u16, N>,
257 ) -> Result<(u32, u32), ImageDecodeError>;
258}
259
260#[cfg(all(feature = "std", feature = "image-decode"))]
261#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
262pub struct BasicImageDecoder;
263
264#[cfg(all(feature = "std", feature = "image-decode"))]
265impl ImageDecoder for BasicImageDecoder {
266 fn decode<const N: usize>(
267 &self,
268 format: EncodedImageFormat,
269 data: &str,
270 out_pixels: &mut heapless::Vec<u16, N>,
271 ) -> Result<(u32, u32), ImageDecodeError> {
272 match format {
273 EncodedImageFormat::PpmAscii => decode_ppm_ascii(data, out_pixels),
274 }
275 }
276}
277
278#[cfg(all(feature = "std", feature = "image-decode"))]
279pub fn decode_image_with<const N: usize>(
280 decoder: &impl ImageDecoder,
281 format: EncodedImageFormat,
282 data: &str,
283 out_pixels: &mut heapless::Vec<u16, N>,
284) -> Result<(u32, u32), ImageDecodeError> {
285 decoder.decode(format, data, out_pixels)
286}
287
288#[cfg(all(feature = "std", feature = "image-decode"))]
289pub fn decode_image_auto<const N: usize>(
290 data: &str,
291 out_pixels: &mut heapless::Vec<u16, N>,
292) -> Result<(u32, u32), ImageDecodeError> {
293 let format = if data.trim_start().starts_with("P3") {
294 EncodedImageFormat::PpmAscii
295 } else {
296 return Err(ImageDecodeError::Unsupported);
297 };
298 decode_image_with(&BasicImageDecoder, format, data, out_pixels)
299}
300
301#[cfg(all(feature = "std", feature = "image-decode"))]
302pub fn decode_ppm_ascii<const N: usize>(
303 data: &str,
304 out_pixels: &mut heapless::Vec<u16, N>,
305) -> Result<(u32, u32), ImageDecodeError> {
306 let mut parts = data.split_whitespace();
307 if parts.next() != Some("P3") {
308 return Err(ImageDecodeError::InvalidHeader);
309 }
310 let width: u32 = parts
311 .next()
312 .ok_or(ImageDecodeError::InvalidHeader)?
313 .parse()
314 .map_err(|_| ImageDecodeError::InvalidHeader)?;
315 let height: u32 = parts
316 .next()
317 .ok_or(ImageDecodeError::InvalidHeader)?
318 .parse()
319 .map_err(|_| ImageDecodeError::InvalidHeader)?;
320 let maxv: u32 = parts
321 .next()
322 .ok_or(ImageDecodeError::InvalidHeader)?
323 .parse()
324 .map_err(|_| ImageDecodeError::InvalidHeader)?;
325 if maxv == 0 {
326 return Err(ImageDecodeError::InvalidData);
327 }
328 out_pixels.clear();
329 let count = width.saturating_mul(height);
330 for _ in 0..count {
331 let r: u32 = parts
332 .next()
333 .ok_or(ImageDecodeError::InvalidData)?
334 .parse()
335 .map_err(|_| ImageDecodeError::InvalidData)?;
336 let g: u32 = parts
337 .next()
338 .ok_or(ImageDecodeError::InvalidData)?
339 .parse()
340 .map_err(|_| ImageDecodeError::InvalidData)?;
341 let b: u32 = parts
342 .next()
343 .ok_or(ImageDecodeError::InvalidData)?
344 .parse()
345 .map_err(|_| ImageDecodeError::InvalidData)?;
346 let r5 = ((r.saturating_mul(31)) / maxv) as u16;
347 let g6 = ((g.saturating_mul(63)) / maxv) as u16;
348 let b5 = ((b.saturating_mul(31)) / maxv) as u16;
349 out_pixels
350 .push((r5 << 11) | (g6 << 5) | b5)
351 .map_err(|_| ImageDecodeError::Capacity)?;
352 }
353 Ok((width, height))
354}