Skip to main content

device_envoy_core/cyd/display/
draw_item.rs

1use crate::pixel_target::{
2    PixelTarget, PixelTargetAdapter, fill_ellipse_pixels, pixel_put, pixel_put_565,
3};
4use embedded_graphics::{
5    Drawable,
6    pixelcolor::{Rgb565, Rgb888, raw::RawU16},
7    prelude::{IntoStorage, Point, Size},
8    primitives::Rectangle,
9    primitives::{Circle, Line, Primitive, PrimitiveStyle},
10};
11
12/// A read-only view of all or part of a statically stored RGB565 image.
13///
14/// A view borrows the original pixels without copying or allocating. Create a
15/// full-image view with [`Image565Fixed::view`](super::tga::Image565Fixed::view),
16/// or select a rectangular portion with
17/// [`Image565Fixed::view_rect`](super::tga::Image565Fixed::view_rect).
18///
19/// Coordinates passed to [`Image565View::pixel_at`] are local to the view, so
20/// `(0, 0)` addresses the crop's top-left pixel rather than the original
21/// image's top-left pixel.
22///
23/// Views contain only color pixels and draw opaquely. For color-key
24/// transparency, draw an [`Image565Fixed`](super::Image565Fixed) with a
25/// [`MaskFixed`](super::MaskFixed), as shown in the
26/// [`MaskFixed` example](super::MaskFixed).
27///
28/// # Example
29#[cfg_attr(
30    feature = "doc-images",
31    doc = ::embed_doc_image::embed_image!(
32        "image565_view",
33        "docs/assets/image565_view.png"
34    )
35)]
36#[cfg_attr(
37    feature = "host",
38    doc = r#"
39
40```rust
41use device_envoy_core::cyd::{
42    Cyd, CydDisplay,
43    display::{CydFrame, DrawItem, Image565Fixed, tga},
44};
45use embedded_graphics::{
46    prelude::{Point, Size},
47    primitives::Rectangle,
48};
49
50const IMAGE: Image565Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
51    env!("CARGO_MANIFEST_DIR"),
52    "/docs/assets/cyd_fill_contiguous.tga"
53))
54.to_565();
55
56async fn draw<C: Cyd>(cyd: &mut C) -> Result<(), C::Error> {
57    let full_view = IMAGE.view();
58    let source = Rectangle::new(Point::new(2, 35), Size::new(41, 36));
59    let cropped_view = IMAGE.view_rect(source);
60
61    assert_eq!(cropped_view.size(), source.size);
62    assert_eq!(
63        cropped_view.pixel_at(Point::zero()),
64        full_view.pixel_at(source.top_left),
65    );
66    assert_eq!(
67        cropped_view.rgb565_iter().count(),
68        source.size.width as usize * source.size.height as usize,
69    );
70
71    let display = cyd.display();
72    let mut frame = display.full_frame_mut();
73    DrawItem::Bitmap {
74        view: full_view,
75        top_left: Point::new(80, 84),
76    }
77    .draw(&mut frame);
78    DrawItem::Bitmap {
79        view: cropped_view,
80        top_left: Point::new(200, 102),
81    }
82    .draw(&mut frame);
83    frame.flush().await
84}
85
86# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
87# use embedded_graphics::{
88#     mono_font::ascii::FONT_9X15_BOLD,
89#     pixelcolor::Rgb888,
90#     prelude::RgbColor,
91# };
92# let mut cyd_memory = CydMemory::new(
93#     Size::new(320, 240),
94#     Rgb888::BLACK,
95#     Rgb888::WHITE,
96#     &FONT_9X15_BOLD,
97# );
98# futures_executor::block_on(draw(&mut cyd_memory))?;
99# let golden_result = assert_framebuffer_matches_expected_png(
100#     &cyd_memory,
101#     env!("CARGO_MANIFEST_DIR"),
102#     "image565_view.png",
103# );
104# assert!(golden_result.is_ok(), "{golden_result:?}");
105# Ok::<(), device_envoy_core::memory::Error>(())
106```
107
108The complete image is shown on the left and its cropped view on the right:
109
110![A complete RGB565 image beside a cropped view of the same image][image565_view]
111"#
112)]
113#[derive(Clone, Copy, Debug)]
114pub struct Image565View {
115    pixels: &'static [u16],
116    stride: u32,
117    source: Rectangle,
118}
119
120impl Image565View {
121    /// Creates a full-image view from a row-major RGB565 pixel slice.
122    ///
123    /// Use this when pixels are already available as packed RGB565 values. For
124    /// a compile-time TGA image, prefer [`Image565Fixed::view`](super::tga::Image565Fixed::view),
125    /// as shown in the [`Image565View` example](Image565View).
126    ///
127    /// Panics if `pixels.len() != size.width * size.height`.
128    #[must_use]
129    pub const fn new(pixels: &'static [u16], size: Size) -> Self {
130        assert!(
131            pixels.len() == size.width as usize * size.height as usize,
132            "Image565View pixels must match width * height"
133        );
134        Self {
135            pixels,
136            stride: size.width,
137            source: Rectangle::new(Point::zero(), size),
138        }
139    }
140
141    /// Cropped view — `source` is in image coordinates, `stride` is the full
142    /// image row width. Prefer [`Image565Fixed::view_rect`] at call sites.
143    #[must_use]
144    pub(crate) const fn new_cropped(
145        pixels: &'static [u16],
146        stride: u32,
147        source: Rectangle,
148    ) -> Self {
149        Self {
150            pixels,
151            stride,
152            source,
153        }
154    }
155
156    /// Returns this view's dimensions.
157    ///
158    /// For a cropped view, these are the crop dimensions rather than the full
159    /// image dimensions. See the [`Image565View` example](Image565View).
160    #[must_use]
161    pub const fn size(&self) -> Size {
162        self.source.size
163    }
164
165    /// Returns the pixel at a view-local coordinate.
166    ///
167    /// `(0, 0)` is the top-left of this view, not necessarily the top-left of
168    /// the underlying image. See the [`Image565View` example](Image565View).
169    ///
170    /// Panics if `point` is outside the view.
171    #[must_use]
172    pub fn pixel_at(&self, point: Point) -> Rgb565 {
173        assert!(
174            point.x >= 0 && point.y >= 0,
175            "Image565View pixel coordinate must be non-negative"
176        );
177        let vx = point.x as usize;
178        let vy = point.y as usize;
179        assert!(
180            vx < self.source.size.width as usize && vy < self.source.size.height as usize,
181            "Image565View pixel coordinate must be inside the view"
182        );
183        let source_x = self.source.top_left.x as usize + vx;
184        let source_y = self.source.top_left.y as usize + vy;
185        let index = source_y * self.stride as usize + source_x;
186        Rgb565::from(RawU16::new(self.pixels[index]))
187    }
188
189    /// Iterate over the view's pixels in row-major order as `Rgb565` values.
190    ///
191    /// Cropped views skip the pixels outside the view while preserving the
192    /// view's local row order.
193    ///
194    /// See the [`Image565View` example](Image565View).
195    pub fn rgb565_iter(&self) -> impl Iterator<Item = Rgb565> + '_ {
196        Image565ViewPixels {
197            view: *self,
198            index: 0,
199        }
200    }
201}
202
203struct Image565ViewPixels {
204    view: Image565View,
205    index: usize,
206}
207
208impl Iterator for Image565ViewPixels {
209    type Item = Rgb565;
210
211    fn next(&mut self) -> Option<Self::Item> {
212        let width = self.view.source.size.width as usize;
213        let height = self.view.source.size.height as usize;
214        if self.index >= width * height {
215            return None;
216        }
217
218        let view_x = self.index % width;
219        let view_y = self.index / width;
220        let source_x = self.view.source.top_left.x as usize + view_x;
221        let source_y = self.view.source.top_left.y as usize + view_y;
222        let source_index = source_y * self.view.stride as usize + source_x;
223        self.index += 1;
224        Some(Rgb565::from(RawU16::new(self.view.pixels[source_index])))
225    }
226}
227
228/// A 2D drawing command that can be rendered onto a [`PixelTarget`].
229///
230/// `DrawItem` is a compact, [`Copy`] representation for a heterogeneous scene.
231/// Its floating-point geometry is convenient for calculated or projected
232/// coordinates, but projection is not required. The same items can be passed to
233/// [`CydDisplay::draw_items`](crate::cyd::CydDisplay::draw_items), which
234/// composites and streams them without a pixel frame buffer, or rendered
235/// directly with [`DrawItem::draw`] when a frame is available.
236///
237/// For ordinary imperative drawing into a
238/// [`CydFrame`](crate::cyd::display::CydFrame), embedded-graphics primitives are
239/// also appropriate, particularly when their integer-coordinate geometry and
240/// styling API fit the scene. `DrawItem::draw` uses embedded-graphics internally
241/// for strokes and circles as an implementation detail; `DrawItem` does not
242/// replace the broader embedded-graphics API.
243///
244/// Coordinates and sizes are measured in display pixels. Colors are specified
245/// as [`Rgb888`], and the target converts them to its native pixel format when
246/// needed.
247///
248/// # Example
249///
250/// This example loads a TGA bitmap at compile time and draws one of each item
251/// variant onto a full-screen frame.
252#[cfg_attr(
253    feature = "doc-images",
254    doc = ::embed_doc_image::embed_image!(
255        "draw_item_bitmap",
256        "docs/assets/draw_item_bitmap.png"
257    )
258)]
259#[cfg_attr(
260    feature = "host",
261    doc = r#"
262
263```rust
264use device_envoy_core::cyd::{
265    Cyd, CydDisplay,
266    display::{CydFrame, DrawItem, Image565Fixed, tga},
267};
268use embedded_graphics::{
269    pixelcolor::Rgb888,
270    prelude::{Point, RgbColor},
271};
272
273const BITMAP: Image565Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
274    env!("CARGO_MANIFEST_DIR"),
275    "/docs/assets/cyd_fill_contiguous.tga"
276))
277.to_565();
278
279async fn draw<C: Cyd>(cyd: &mut C) -> Result<(), C::Error> {
280    let display = cyd.display();
281    let mut frame = display.full_frame_mut();
282    let draw_items = [
283        DrawItem::Bitmap {
284            view: BITMAP.view(),
285            top_left: Point::new(35, 84),
286        },
287        DrawItem::Circle {
288            center: (125.0, 120.0),
289            pixel_radius: 32.0,
290            color: Rgb888::CYAN,
291        },
292        DrawItem::Ellipse {
293            center: (215.0, 120.0),
294            axis_a: (38.0, 0.0),
295            axis_b: (12.0, 24.0),
296            color: Rgb888::GREEN,
297        },
298        DrawItem::Stroke {
299            start: (280.0, 80.0),
300            end: (300.0, 160.0),
301            color: Rgb888::YELLOW,
302            pixel_width: 8.0,
303        },
304    ];
305    for draw_item in draw_items {
306        draw_item.draw(&mut frame);
307    }
308    frame.flush().await
309}
310
311# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
312# use embedded_graphics::{mono_font::ascii::FONT_9X15_BOLD, prelude::Size};
313# let mut cyd_memory = CydMemory::new(
314#     Size::new(320, 240),
315#     Rgb888::BLACK,
316#     Rgb888::WHITE,
317#     &FONT_9X15_BOLD,
318# );
319# futures_executor::block_on(draw(&mut cyd_memory))?;
320# let golden_result = assert_framebuffer_matches_expected_png(
321#     &cyd_memory,
322#     env!("CARGO_MANIFEST_DIR"),
323#     "draw_item_bitmap.png",
324# );
325# assert!(golden_result.is_ok(), "{golden_result:?}");
326# Ok::<(), device_envoy_core::memory::Error>(())
327```
328
329![Examples of all four DrawItem variants][draw_item_bitmap]
330"#
331)]
332#[derive(Clone, Copy, Debug)]
333pub enum DrawItem {
334    /// A line stroke from `start` to `end` with the given pixel width.
335    Stroke {
336        /// Start point in display coordinates.
337        start: (f32, f32),
338        /// End point in display coordinates.
339        end: (f32, f32),
340        /// Stroke color.
341        color: Rgb888,
342        /// Stroke width in pixels.
343        pixel_width: f32,
344    },
345    /// A filled ellipse. It can also represent a projected disk.
346    ///
347    /// The ellipse is the locus of `center + s·axis_a + t·axis_b` with `s²+t² ≤ 1`.
348    Ellipse {
349        /// Center in display coordinates.
350        center: (f32, f32),
351        /// First radius vector, measured in pixels.
352        axis_a: (f32, f32),
353        /// Second radius vector, measured in pixels.
354        axis_b: (f32, f32),
355        /// Fill color.
356        color: Rgb888,
357    },
358    /// A filled circle. It can also represent a projected sphere.
359    Circle {
360        /// Center in display coordinates.
361        center: (f32, f32),
362        /// Radius in pixels.
363        pixel_radius: f32,
364        /// Fill color.
365        color: Rgb888,
366    },
367    /// A statically stored RGB565 bitmap placed at a display position.
368    Bitmap {
369        /// Bitmap pixels and dimensions.
370        view: Image565View,
371        /// Top-left corner in display coordinates.
372        top_left: Point,
373    },
374}
375
376impl DrawItem {
377    /// Draw this item onto a [`PixelTarget`].
378    ///
379    /// Strokes use the embedded-graphics
380    /// [`Line`](https://docs.rs/embedded-graphics/latest/embedded_graphics/primitives/struct.Line.html)
381    /// primitive and circles use
382    /// [`Circle`](https://docs.rs/embedded-graphics/latest/embedded_graphics/primitives/struct.Circle.html);
383    /// the general ellipse is rasterized with
384    /// [`fill_ellipse_pixels`].
385    ///
386    /// See the [`DrawItem` example](DrawItem).
387    pub fn draw<T: PixelTarget>(&self, target: &mut T) {
388        match *self {
389            DrawItem::Stroke {
390                start,
391                end,
392                color,
393                pixel_width,
394            } => {
395                let width = ((pixel_width + 0.5) as u32).max(1);
396                Line::new(
397                    embedded_graphics::prelude::Point::new(start.0 as i32, start.1 as i32),
398                    embedded_graphics::prelude::Point::new(end.0 as i32, end.1 as i32),
399                )
400                .into_styled(PrimitiveStyle::with_stroke(color, width))
401                .draw(&mut PixelTargetAdapter(target))
402                .expect("drawing onto a PixelTargetAdapter is Infallible");
403            }
404            DrawItem::Ellipse {
405                center,
406                axis_a,
407                axis_b,
408                color,
409            } => {
410                fill_ellipse_pixels(center, axis_a, axis_b, |position_x, position_y| {
411                    pixel_put(target, position_x, position_y, color);
412                });
413            }
414            DrawItem::Circle {
415                center,
416                pixel_radius,
417                color,
418            } => {
419                let diameter = (((pixel_radius * 2.0) + 0.5) as u32).max(1);
420                Circle::with_center(
421                    embedded_graphics::prelude::Point::new(center.0 as i32, center.1 as i32),
422                    diameter,
423                )
424                .into_styled(PrimitiveStyle::with_fill(color))
425                .draw(&mut PixelTargetAdapter(target))
426                .expect("drawing onto a PixelTargetAdapter is Infallible");
427            }
428            DrawItem::Bitmap { view, top_left } => {
429                let size = view.size();
430                for dy in 0..size.height as i32 {
431                    for dx in 0..size.width as i32 {
432                        let view_point = Point::new(dx, dy);
433                        let target_point = top_left + view_point;
434                        pixel_put_565(
435                            target,
436                            target_point.x,
437                            target_point.y,
438                            view.pixel_at(view_point).into_storage(),
439                        );
440                    }
441                }
442            }
443        }
444    }
445}