use crate::cyd::display::CydFrame;
use embedded_graphics::{
Drawable, Pixel,
pixelcolor::{Rgb565, raw::RawU16},
prelude::{DrawTarget, Point, Size},
primitives::Rectangle,
};
pub const fn mask_byte_count(width: usize, height: usize) -> usize {
(width * height).div_ceil(8)
}
pub struct Image888Fixed<const W: usize, const H: usize, const N: usize> {
pub pixels: [[u8; 3]; N],
}
#[cfg_attr(
feature = "doc-images",
doc = ::embed_doc_image::embed_image!(
"image565_fixed",
"docs/assets/image565_fixed.png"
)
)]
#[cfg_attr(
feature = "host",
doc = r#"
```rust
use device_envoy_core::{
UnwrapInfallible,
cyd::{
Cyd, CydDisplay,
display::{CydFrame, Image565Fixed, tga},
},
};
use embedded_graphics::{Drawable, prelude::Point};
const IMAGE: Image565Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/assets/cyd_fill_contiguous.tga"
))
.to_565();
async fn draw<C: Cyd>(cyd: &mut C) -> Result<(), C::Error> {
let display = cyd.display();
let mut frame = display.full_frame_mut();
for top_left in [
Point::new(50, 84),
Point::new(138, 84),
Point::new(226, 84),
] {
IMAGE.at(top_left).draw(&mut frame).unwrap_infallible();
}
frame.flush().await
}
# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
# use embedded_graphics::{
# mono_font::ascii::FONT_9X15_BOLD,
# pixelcolor::Rgb888,
# prelude::{RgbColor, Size},
# };
# let mut cyd_memory = CydMemory::new(
# Size::new(320, 240),
# Rgb888::BLACK,
# Rgb888::WHITE,
# &FONT_9X15_BOLD,
# );
# futures_executor::block_on(draw(&mut cyd_memory))?;
# let golden_result = assert_framebuffer_matches_expected_png(
# &cyd_memory,
# env!("CARGO_MANIFEST_DIR"),
# "image565_fixed.png",
# );
# assert!(golden_result.is_ok(), "{golden_result:?}");
# Ok::<(), device_envoy_core::memory::Error>(())
```
![The same fixed RGB565 image drawn at three display positions][image565_fixed]
"#
)]
pub struct Image565Fixed<const W: usize, const H: usize, const N: usize> {
pub pixels: [u16; N],
}
#[cfg_attr(
feature = "doc-images",
doc = ::embed_doc_image::embed_image!("mask_fixed", "docs/assets/mask_fixed.png")
)]
#[cfg_attr(
feature = "host",
doc = r#"
```rust
use device_envoy_core::{
UnwrapInfallible,
cyd::{
Cyd, CydDisplay,
display::{
CydFrame, Image565Fixed, Image888Fixed, MaskFixed, MaskedDrawable,
mask_byte_count, tga,
},
},
};
use embedded_graphics::{Drawable, prelude::Point};
const SOURCE: Image888Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/assets/cyd_fill_contiguous.tga"
));
const IMAGE: Image565Fixed<45, 73, { 45 * 73 }> = SOURCE.to_565();
const MASK: MaskFixed<45, 73, { mask_byte_count(45, 73) }> =
SOURCE.to_mask_magenta();
async fn draw<C: Cyd>(cyd: &mut C) -> Result<(), C::Error> {
let display = cyd.display();
let mut frame = display.full_frame_mut();
IMAGE
.at(Point::new(80, 84))
.draw(&mut frame)
.unwrap_infallible();
IMAGE
.at(Point::new(200, 84))
.draw_masked(&MASK, &mut frame)
.unwrap_infallible();
frame.flush().await
}
assert!(!MASK.is_set(0));
# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
# use embedded_graphics::{
# mono_font::ascii::FONT_9X15_BOLD,
# pixelcolor::Rgb888,
# prelude::{RgbColor, Size},
# };
# let mut cyd_memory = CydMemory::new(
# Size::new(320, 240),
# Rgb888::BLACK,
# Rgb888::WHITE,
# &FONT_9X15_BOLD,
# );
# futures_executor::block_on(draw(&mut cyd_memory))?;
# let golden_result = assert_framebuffer_matches_expected_png(
# &cyd_memory,
# env!("CARGO_MANIFEST_DIR"),
# "mask_fixed.png",
# );
# assert!(golden_result.is_ok(), "{golden_result:?}");
# Ok::<(), device_envoy_core::memory::Error>(())
```
The opaque RGB565 image is on the left. The masked drawing on the right skips
the magenta background:
![An opaque RGB565 image beside the same image drawn with a transparency mask][mask_fixed]
"#
)]
pub struct MaskFixed<const W: usize, const H: usize, const MASK_N: usize> {
pub bits: [u8; MASK_N],
}
pub trait MaskedDrawable<const W: usize, const H: usize>: Drawable<Output = ()> {
fn draw_masked<const MASK_N: usize, D>(
&self,
mask: &MaskFixed<W, H, MASK_N>,
target: &mut D,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>;
}
struct PlacedImage565<'a, const W: usize, const H: usize, const N: usize> {
image: &'a Image565Fixed<W, H, N>,
top_left: Point,
}
const fn read_u16(bytes: &[u8], offset: usize) -> u16 {
bytes[offset] as u16 | ((bytes[offset + 1] as u16) << 8)
}
const fn parse_header(bytes: &[u8], width: usize, height: usize) -> (usize, usize, bool) {
assert!(bytes.len() >= 18, "TGA: file shorter than header");
assert!(bytes[1] == 0, "TGA: color maps are not supported");
assert!(
bytes[2] == 2,
"TGA: only uncompressed true-color images are supported"
);
assert!(
bytes[3] == 0 && bytes[4] == 0 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0,
"TGA: color map specification is not supported"
);
assert!(
read_u16(bytes, 12) as usize == width,
"TGA: width does not match const argument"
);
assert!(
read_u16(bytes, 14) as usize == height,
"TGA: height does not match const argument"
);
assert!(
bytes[16] == 24 || bytes[16] == 32,
"TGA: only 24-bit BGR or 32-bit BGRA is supported"
);
assert!(
bytes[17] & 0x10 == 0,
"TGA: right-to-left origin is not supported"
);
let bytes_per_pixel = (bytes[16] / 8) as usize;
let pixel_start = 18 + bytes[0] as usize;
assert!(
bytes.len() >= pixel_start + width * height * bytes_per_pixel,
"TGA: pixel data is shorter than width * height"
);
(pixel_start, bytes_per_pixel, bytes[17] & 0x20 != 0)
}
impl<const W: usize, const H: usize, const N: usize> Image888Fixed<W, H, N> {
pub const fn from_tga(bytes: &[u8]) -> Self {
assert!(N == W * H, "Image888Fixed: N must equal W * H");
let (pixel_start, bytes_per_pixel, top_origin) = parse_header(bytes, W, H);
let mut pixels = [[0u8; 3]; N];
let mut y = 0;
while y < H {
let mut x = 0;
while x < W {
let source_y = if top_origin { y } else { H - 1 - y };
let offset = pixel_start + (source_y * W + x) * bytes_per_pixel;
let red = bytes[offset + 2];
let green = bytes[offset + 1];
let blue = bytes[offset];
pixels[y * W + x] = [red, green, blue];
x += 1;
}
y += 1;
}
Self { pixels }
}
pub const fn to_565(&self) -> Image565Fixed<W, H, N> {
let mut pixels = [0u16; N];
let mut index = 0;
while index < N {
let [red, green, blue] = self.pixels[index];
pixels[index] =
((red as u16 >> 3) << 11) | ((green as u16 >> 2) << 5) | (blue as u16 >> 3);
index += 1;
}
Image565Fixed { pixels }
}
pub const fn to_mask_magenta<const MASK_N: usize>(&self) -> MaskFixed<W, H, MASK_N> {
assert!(
MASK_N == mask_byte_count(W, H),
"Mask: MASK_N must match image dimensions"
);
let mut bits = [0u8; MASK_N];
let mut index = 0;
while index < N {
let pixel = self.pixels[index];
let red = pixel[0];
let green = pixel[1];
let blue = pixel[2];
if !(red >= 200 && blue >= 200 && green <= 60) {
bits[index / 8] |= 1 << (index % 8);
}
index += 1;
}
MaskFixed { bits }
}
}
impl<const W: usize, const H: usize, const N: usize> Image565Fixed<W, H, N> {
pub const fn at(&self, top_left: Point) -> impl MaskedDrawable<W, H, Color = Rgb565> + '_ {
PlacedImage565 {
image: self,
top_left,
}
}
pub const fn view(&'static self) -> super::Image565View {
self.view_rect(Rectangle::new(Point::zero(), Size::new(W as u32, H as u32)))
}
pub const fn view_rect(&'static self, source: Rectangle) -> super::Image565View {
assert!(
source.top_left.x >= 0 && source.top_left.y >= 0,
"view_rect: negative origin"
);
assert!(
source.top_left.x as usize + source.size.width as usize <= W
&& source.top_left.y as usize + source.size.height as usize <= H,
"view_rect: rectangle is outside image"
);
super::Image565View::new_cropped(&self.pixels, W as u32, source)
}
pub fn copy_to<F: CydFrame>(&self, frame: &mut F) -> crate::Result<()> {
frame.copy_from_565(&self.pixels)
}
}
impl<const W: usize, const H: usize, const MASK_N: usize> MaskFixed<W, H, MASK_N> {
pub const fn is_set(&self, index: usize) -> bool {
self.bits[index / 8] & (1 << (index % 8)) != 0
}
}
impl<const W: usize, const H: usize, const N: usize> MaskedDrawable<W, H>
for PlacedImage565<'_, W, H, N>
{
fn draw_masked<const MASK_N: usize, D>(
&self,
mask: &MaskFixed<W, H, MASK_N>,
target: &mut D,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
let mut pixels = ImagePixels::<W, N> {
pixels: &self.image.pixels,
top_left: self.top_left,
index: 0,
};
target.draw_iter(core::iter::from_fn(|| {
loop {
let pixel = pixels.next()?;
let index = pixels.index - 1;
if mask.is_set(index) {
return Some(pixel);
}
}
}))
}
}
impl<const W: usize, const H: usize, const N: usize> MaskedDrawable<W, H>
for Image565Fixed<W, H, N>
{
fn draw_masked<const MASK_N: usize, D>(
&self,
mask: &MaskFixed<W, H, MASK_N>,
target: &mut D,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
self.at(Point::zero()).draw_masked(mask, target)
}
}
struct ImagePixels<'a, const W: usize, const N: usize> {
pixels: &'a [u16; N],
top_left: Point,
index: usize,
}
impl<const W: usize, const N: usize> Iterator for ImagePixels<'_, W, N> {
type Item = Pixel<Rgb565>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= N {
return None;
}
let index = self.index;
self.index += 1;
Some(Pixel(
self.top_left + Point::new((index % W) as i32, (index / W) as i32),
Rgb565::from(RawU16::new(self.pixels[index])),
))
}
}
impl<const W: usize, const H: usize, const N: usize> Drawable for PlacedImage565<'_, W, H, N> {
type Color = Rgb565;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
{
target.draw_iter(ImagePixels::<W, N> {
pixels: &self.image.pixels,
top_left: self.top_left,
index: 0,
})
}
}
impl<const W: usize, const H: usize, const N: usize> Drawable for Image565Fixed<W, H, N> {
type Color = Rgb565;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
{
self.at(Point::zero()).draw(target)
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __cyd_tga {
($path:expr) => {
$crate::cyd::display::Image888Fixed::from_tga(include_bytes!($path))
};
($path:expr, $width:expr, $height:expr) => {
$crate::cyd::display::Image888Fixed::<$width, $height, { $width * $height }>::from_tga(
include_bytes!($path),
)
};
}