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 struct SpriteSheet<'a> {
39    pub image: ImageRef<'a>,
40    pub sprite_w: u32,
41    pub sprite_h: u32,
42    pub columns: u32,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct ReelFrame {
47    pub sprite_index: u16,
48    pub duration_ms: u16,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct ReelPlayer<'a> {
53    pub sheet: SpriteSheet<'a>,
54    pub frames: &'a [ReelFrame],
55    pub repeat: bool,
56    current: usize,
57    elapsed_in_frame_ms: u32,
58    finished: bool,
59}
60
61impl<'a> ReelPlayer<'a> {
62    pub const fn new(sheet: SpriteSheet<'a>, frames: &'a [ReelFrame], repeat: bool) -> Self {
63        Self {
64            sheet,
65            frames,
66            repeat,
67            current: 0,
68            elapsed_in_frame_ms: 0,
69            finished: false,
70        }
71    }
72
73    pub fn tick(&mut self, dt_ms: u32) {
74        if self.frames.is_empty() || self.finished {
75            return;
76        }
77        self.elapsed_in_frame_ms = self.elapsed_in_frame_ms.saturating_add(dt_ms);
78        loop {
79            let frame = self.frames[self.current];
80            let frame_ms = u32::from(frame.duration_ms).max(1);
81            if self.elapsed_in_frame_ms < frame_ms {
82                break;
83            }
84            self.elapsed_in_frame_ms -= frame_ms;
85            if self.current + 1 < self.frames.len() {
86                self.current += 1;
87                continue;
88            }
89            if self.repeat {
90                self.current = 0;
91            } else {
92                self.finished = true;
93            }
94            break;
95        }
96    }
97
98    pub const fn is_finished(&self) -> bool {
99        self.finished
100    }
101
102    pub fn restart(&mut self) {
103        self.current = 0;
104        self.elapsed_in_frame_ms = 0;
105        self.finished = false;
106    }
107
108    pub fn current_sprite_rect(&self) -> Option<Rect> {
109        let frame = self.frames.get(self.current)?;
110        Some(self.sheet.sprite_rect(frame.sprite_index as u32))
111    }
112}
113
114impl<'a> SpriteSheet<'a> {
115    pub const fn new(image: ImageRef<'a>, sprite_w: u32, sprite_h: u32) -> Self {
116        let columns = match image.width.checked_div(sprite_w) {
117            Some(c) => c,
118            None => 1,
119        };
120        Self {
121            image,
122            sprite_w,
123            sprite_h,
124            columns,
125        }
126    }
127
128    pub fn sprite_rect(&self, index: u32) -> Rect {
129        let columns = self.columns.max(1);
130        let col = index % columns;
131        let row = index / columns;
132        Rect::new(
133            (col * self.sprite_w) as i32,
134            (row * self.sprite_h) as i32,
135            self.sprite_w,
136            self.sprite_h,
137        )
138    }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub struct ImageAtlasEntry {
143    pub id: u16,
144    pub rect: Rect,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct ImageAtlas<'a> {
149    pub image: ImageRef<'a>,
150    pub entries: &'a [ImageAtlasEntry],
151}
152
153impl<'a> ImageAtlas<'a> {
154    pub const fn new(image: ImageRef<'a>, entries: &'a [ImageAtlasEntry]) -> Self {
155        Self { image, entries }
156    }
157
158    pub fn rect_for(&self, id: u16) -> Option<Rect> {
159        self.entries
160            .iter()
161            .find(|entry| entry.id == id)
162            .map(|e| e.rect)
163    }
164}
165
166#[cfg(all(feature = "std", feature = "image-decode"))]
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub enum ImageDecodeError {
169    InvalidHeader,
170    Unsupported,
171    InvalidData,
172    Capacity,
173}
174
175#[cfg(all(feature = "std", feature = "image-decode"))]
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum EncodedImageFormat {
178    PpmAscii,
179}
180
181#[cfg(all(feature = "std", feature = "image-decode"))]
182pub trait ImageDecoder {
183    fn decode<const N: usize>(
184        &self,
185        format: EncodedImageFormat,
186        data: &str,
187        out_pixels: &mut heapless::Vec<u16, N>,
188    ) -> Result<(u32, u32), ImageDecodeError>;
189}
190
191#[cfg(all(feature = "std", feature = "image-decode"))]
192#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
193pub struct BasicImageDecoder;
194
195#[cfg(all(feature = "std", feature = "image-decode"))]
196impl ImageDecoder for BasicImageDecoder {
197    fn decode<const N: usize>(
198        &self,
199        format: EncodedImageFormat,
200        data: &str,
201        out_pixels: &mut heapless::Vec<u16, N>,
202    ) -> Result<(u32, u32), ImageDecodeError> {
203        match format {
204            EncodedImageFormat::PpmAscii => decode_ppm_ascii(data, out_pixels),
205        }
206    }
207}
208
209#[cfg(all(feature = "std", feature = "image-decode"))]
210pub fn decode_image_with<const N: usize>(
211    decoder: &impl ImageDecoder,
212    format: EncodedImageFormat,
213    data: &str,
214    out_pixels: &mut heapless::Vec<u16, N>,
215) -> Result<(u32, u32), ImageDecodeError> {
216    decoder.decode(format, data, out_pixels)
217}
218
219#[cfg(all(feature = "std", feature = "image-decode"))]
220pub fn decode_image_auto<const N: usize>(
221    data: &str,
222    out_pixels: &mut heapless::Vec<u16, N>,
223) -> Result<(u32, u32), ImageDecodeError> {
224    let format = if data.trim_start().starts_with("P3") {
225        EncodedImageFormat::PpmAscii
226    } else {
227        return Err(ImageDecodeError::Unsupported);
228    };
229    decode_image_with(&BasicImageDecoder, format, data, out_pixels)
230}
231
232#[cfg(all(feature = "std", feature = "image-decode"))]
233pub fn decode_ppm_ascii<const N: usize>(
234    data: &str,
235    out_pixels: &mut heapless::Vec<u16, N>,
236) -> Result<(u32, u32), ImageDecodeError> {
237    let mut parts = data.split_whitespace();
238    if parts.next() != Some("P3") {
239        return Err(ImageDecodeError::InvalidHeader);
240    }
241    let width: u32 = parts
242        .next()
243        .ok_or(ImageDecodeError::InvalidHeader)?
244        .parse()
245        .map_err(|_| ImageDecodeError::InvalidHeader)?;
246    let height: u32 = parts
247        .next()
248        .ok_or(ImageDecodeError::InvalidHeader)?
249        .parse()
250        .map_err(|_| ImageDecodeError::InvalidHeader)?;
251    let maxv: u32 = parts
252        .next()
253        .ok_or(ImageDecodeError::InvalidHeader)?
254        .parse()
255        .map_err(|_| ImageDecodeError::InvalidHeader)?;
256    if maxv == 0 {
257        return Err(ImageDecodeError::InvalidData);
258    }
259    out_pixels.clear();
260    let count = width.saturating_mul(height);
261    for _ in 0..count {
262        let r: u32 = parts
263            .next()
264            .ok_or(ImageDecodeError::InvalidData)?
265            .parse()
266            .map_err(|_| ImageDecodeError::InvalidData)?;
267        let g: u32 = parts
268            .next()
269            .ok_or(ImageDecodeError::InvalidData)?
270            .parse()
271            .map_err(|_| ImageDecodeError::InvalidData)?;
272        let b: u32 = parts
273            .next()
274            .ok_or(ImageDecodeError::InvalidData)?
275            .parse()
276            .map_err(|_| ImageDecodeError::InvalidData)?;
277        let r5 = ((r.saturating_mul(31)) / maxv) as u16;
278        let g6 = ((g.saturating_mul(63)) / maxv) as u16;
279        let b5 = ((b.saturating_mul(31)) / maxv) as u16;
280        out_pixels
281            .push((r5 << 11) | (g6 << 5) | b5)
282            .map_err(|_| ImageDecodeError::Capacity)?;
283    }
284    Ok((width, height))
285}