Skip to main content

embedded_gui/
image.rs

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}