diff --git a/src/fs.rs b/src/fs.rs index f7f7350..6f6e3a8 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,10 +1,10 @@ //! Access file system: the game ROM files and the data dir. -use crate::graphics::{Point, Size}; +use crate::graphics::*; #[cfg(feature = "alloc")] -use alloc::vec; +use alloc::boxed::Box; #[cfg(feature = "alloc")] -use alloc::vec::Vec; +use alloc::vec; /// Like [File] but owns the buffer. /// @@ -13,7 +13,7 @@ use alloc::vec::Vec; /// and [`data::load`] instead. #[cfg(feature = "alloc")] pub struct FileBuf { - pub(crate) raw: Vec, + pub(crate) raw: Box<[u8]>, } #[cfg(feature = "alloc")] @@ -49,17 +49,17 @@ pub struct File<'a> { impl<'a> File<'a> { #[must_use] - pub fn data(&self) -> &[u8] { + pub const fn data(&self) -> &[u8] { self.raw } #[must_use] - pub fn as_font(&self) -> Font { + pub const fn as_font(&self) -> Font { Font { raw: self.raw } } #[must_use] - pub fn as_image(&self) -> Image { + pub const fn as_image(&self) -> Image { Image { raw: self.raw } } } @@ -106,7 +106,9 @@ pub fn load_file_buf(name: &str) -> Option { } let mut buf = vec![0; size]; load_file(name, &mut buf); - Some(FileBuf { raw: buf }) + Some(FileBuf { + raw: buf.into_boxed_slice(), + }) } /// Write the buffer into the given file in the data dir. @@ -155,46 +157,6 @@ impl<'a> From<&'a FileBuf> for Font<'a> { } } -/// A loaded image file. -/// -/// Can be loaded as [`FileBuf`] from ROM with [`rom::load_buf`] -/// and then cast using [Into]. -pub struct Image<'a> { - pub(crate) raw: &'a [u8], -} - -impl<'a> From> for Image<'a> { - fn from(value: File<'a>) -> Self { - Self { raw: value.raw } - } -} - -#[cfg(feature = "alloc")] -impl<'a> From<&'a FileBuf> for Image<'a> { - fn from(value: &'a FileBuf) -> Self { - Self { raw: &value.raw } - } -} - -impl<'a> Image<'a> { - /// Get a rectangle subregion of the image. - #[must_use] - pub fn sub(&self, p: Point, s: Size) -> SubImage<'a> { - SubImage { - point: p, - size: s, - raw: self.raw, - } - } -} - -/// A subregion of an image. Constructed using [`Image::sub`]. -pub struct SubImage<'a> { - pub(crate) point: Point, - pub(crate) size: Size, - pub(crate) raw: &'a [u8], -} - mod bindings { #[link(wasm_import_module = "fs")] extern { diff --git a/src/graphics/angle.rs b/src/graphics/angle.rs index 973fbd5..ea2e702 100644 --- a/src/graphics/angle.rs +++ b/src/graphics/angle.rs @@ -26,7 +26,7 @@ impl Angle { /// An angle in radians where [TAU] (doubled [PI]) is the full circle. #[must_use] - pub fn from_radians(r: f32) -> Self { + pub const fn from_radians(r: f32) -> Self { Self(r) } @@ -50,7 +50,7 @@ impl Angle { /// Get the angle value in radians where [TAU] (doubled [PI]) is the full circle. #[must_use] - pub fn to_radians(self) -> f32 { + pub const fn to_radians(self) -> f32 { self.0 } diff --git a/src/graphics/bindings.rs b/src/graphics/bindings.rs index d0e0523..2d2337a 100644 --- a/src/graphics/bindings.rs +++ b/src/graphics/bindings.rs @@ -99,4 +99,6 @@ extern { sub_height: i32, ); pub(crate) fn draw_image(ptr: u32, len: u32, x: i32, y: i32); + pub(crate) fn set_canvas(ptr: u32, len: u32); + pub(crate) fn unset_canvas(); } diff --git a/src/graphics/canvas.rs b/src/graphics/canvas.rs new file mode 100644 index 0000000..1ac2cd4 --- /dev/null +++ b/src/graphics/canvas.rs @@ -0,0 +1,99 @@ +use crate::*; +#[cfg(feature = "alloc")] +use alloc::boxed::Box; +#[cfg(feature = "alloc")] +use alloc::vec; + +// TODO: add statically-allocated version when prepare_slice can be turned into static fn. +// It is blocked by this feature going into stable: +// https://github.com/rust-lang/rust/issues/57349 + +/// Canvas is an [`Image`] that can be drawn upon. +/// +/// [`CanvasBuf`] is the same as [`Canvas`] but holds the ownership of the underlying slice. +#[cfg(feature = "alloc")] +#[expect(clippy::module_name_repetitions)] +pub struct CanvasBuf { + pub(crate) raw: Box<[u8]>, +} + +#[cfg(feature = "alloc")] +impl CanvasBuf { + /// Create new empty canvas. + #[must_use] + #[expect(clippy::cast_sign_loss)] + pub fn new(s: Size) -> Self { + const HEADER_SIZE: usize = 5 + 8; + let body_size = s.width * s.height / 2; + let mut raw = vec![0; HEADER_SIZE + body_size as usize]; + prepare_slice(&mut raw, s.width); + Self { + raw: raw.into_boxed_slice(), + } + } + + /// Represent the canvas as an [`Image`]. + #[must_use] + pub const fn as_image(&self) -> Image<'_> { + Image { raw: &self.raw } + } + + /// Represent the buffered canvas as [`Canvas`]. + #[must_use] + pub const fn as_canvas(&self) -> Canvas<'_> { + Canvas { raw: &self.raw } + } +} + +/// Canvas is an [`Image`] that can be drawn upon. +pub struct Canvas<'a> { + pub(crate) raw: &'a [u8], +} + +impl<'a> Canvas<'a> { + /// Create new empty canvas using the given slice. + /// + /// Returns [`None`] if the slice is too small for the given image size. + /// A bigger slice than needed is fine. + #[must_use] + #[expect(clippy::cast_sign_loss)] + pub fn new(s: Size, raw: &'a mut [u8]) -> Option { + const HEADER_SIZE: usize = 5 + 8; + let body_size = s.width * s.height / 2; + let exp_size = HEADER_SIZE + body_size as usize; + if raw.len() < exp_size { + return None; + } + prepare_slice(raw, s.width); + Some(Self { + raw: &raw[..exp_size], + }) + } + + /// Represent the canvas as an [`Image`]. + #[must_use] + pub const fn as_image(&self) -> Image<'a> { + Image { raw: self.raw } + } +} + +#[cfg(feature = "alloc")] +impl<'a> From<&'a CanvasBuf> for Canvas<'a> { + fn from(value: &'a CanvasBuf) -> Self { + Self { raw: &value.raw } + } +} + +#[expect(clippy::cast_sign_loss)] +fn prepare_slice(raw: &mut [u8], width: i32) { + raw[0] = 0x21; // magic number + raw[1] = 4; // BPP + raw[2] = width as u8; // width + raw[3] = (width >> 8) as u8; // width + raw[4] = 255; // transparency + + // color swaps + for i in 0u8..8u8 { + raw[5 + i as usize] = ((i * 2) << 4) | (i * 2 + 1); + } +} diff --git a/src/graphics/funcs.rs b/src/graphics/funcs.rs index 5d04c08..b6ce78d 100644 --- a/src/graphics/funcs.rs +++ b/src/graphics/funcs.rs @@ -1,5 +1,5 @@ use super::{bindings as b, *}; -use crate::fs::{Font, Image, SubImage}; +use crate::*; /// Fill the whole frame with the given color. pub fn clear_screen(c: Color) { @@ -191,3 +191,19 @@ pub fn draw_sub_image(i: &SubImage, p: Point) { ); } } + +/// Set canvas to be used for all subsequent drawing operations. +pub fn set_canvas(c: &Canvas) { + let ptr = c.raw.as_ptr(); + let len = c.raw.len(); + unsafe { + b::set_canvas(ptr as u32, len as u32); + } +} + +/// Unset canvas set by [`set_canvas`]. All subsequent drawing operations will target frame buffer. +pub fn unset_canvas() { + unsafe { + b::unset_canvas(); + } +} diff --git a/src/graphics/image.rs b/src/graphics/image.rs new file mode 100644 index 0000000..c7cc584 --- /dev/null +++ b/src/graphics/image.rs @@ -0,0 +1,128 @@ +use crate::*; + +/// A loaded image file. +/// +/// Can be loaded as [`FileBuf`] from ROM with [`load_file_buf`] +/// and then cast using [`Into`]. +pub struct Image<'a> { + pub(crate) raw: &'a [u8], +} + +impl<'a> From> for Image<'a> { + fn from(value: File<'a>) -> Self { + Self { raw: value.raw } + } +} + +#[cfg(feature = "alloc")] +impl<'a> From<&'a FileBuf> for Image<'a> { + fn from(value: &'a FileBuf) -> Self { + Self { raw: &value.raw } + } +} + +impl<'a> From> for Image<'a> { + fn from(value: Canvas<'a>) -> Self { + Self { raw: value.raw } + } +} + +#[cfg(feature = "alloc")] +impl<'a> From<&'a CanvasBuf> for Image<'a> { + fn from(value: &'a CanvasBuf) -> Self { + Self { raw: &value.raw } + } +} + +impl<'a> Image<'a> { + /// Get a rectangle subregion of the image. + #[must_use] + pub const fn sub(&self, p: Point, s: Size) -> SubImage<'a> { + SubImage { + point: p, + size: s, + raw: self.raw, + } + } + + /// Bits per pixel. One of: 1, 2, or 4. + #[must_use] + pub const fn bpp(&self) -> u8 { + self.raw[1] + } + + /// The color used for transparency. If no transparency, returns [`Color::None`]. + #[must_use] + pub fn transparency(&self) -> Color { + let c = usize::from(self.raw[4]) + 1; + c.try_into().unwrap_or(Color::None) + } + + // pub fn set_transparency(&mut self, c: Color) { + // let c: i32 = c.into(); + // if c == 0 { + // self.raw[4] = 16; + // } else { + // self.raw[4] = c as u8; + // } + // } + + /// The number of pixels the image has. + #[must_use] + pub const fn pixels(&self) -> usize { + self.raw.len() * 8 / self.bpp() as usize + } + + /// The image width in pixels. + #[must_use] + pub fn width(&self) -> u16 { + let big = u16::from(self.raw[2]); + let little = u16::from(self.raw[3]); + big | (little << 8) + } + + /// The image height in pixels. + #[must_use] + pub fn height(&self) -> u16 { + let p = self.pixels(); + let w = self.width() as usize; + p.checked_div(w).unwrap_or(0) as u16 + } + + /// The image size in pixels. + #[must_use] + pub fn size(&self) -> Size { + Size { + width: i32::from(self.width()), + height: i32::from(self.height()), + } + } + + /// Get the color used to represent the given pixel value. + #[must_use] + pub fn get_color(&self, p: u8) -> Color { + if p > 15 { + return Color::None; + } + let byte_idx = usize::from(5 + p / 2); + let mut byte_val = self.raw[byte_idx]; + if p % 2 == 0 { + byte_val >>= 4; + } + byte_val &= 0b1111; + let transp = self.raw[4]; + if byte_val == transp { + return Color::None; + } + let color_val = usize::from(byte_val + 1); + color_val.try_into().unwrap_or(Color::None) + } +} + +/// A subregion of an image. Constructed using [`Image::sub`]. +#[expect(clippy::module_name_repetitions)] +pub struct SubImage<'a> { + pub(crate) point: Point, + pub(crate) size: Size, + pub(crate) raw: &'a [u8], +} diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs index ec5a970..8934c5e 100644 --- a/src/graphics/mod.rs +++ b/src/graphics/mod.rs @@ -1,14 +1,20 @@ //! Draw shapes, images, and text on the screen. +#![deny(missing_docs)] + mod angle; mod bindings; +mod canvas; mod funcs; +mod image; mod point; mod size; mod types; pub use angle::*; +pub use canvas::*; pub use funcs::*; +pub use image::*; pub use point::*; pub use size::*; pub use types::*; diff --git a/src/graphics/point.rs b/src/graphics/point.rs index 0e62e04..bb8501e 100644 --- a/src/graphics/point.rs +++ b/src/graphics/point.rs @@ -9,7 +9,9 @@ use nalgebra::{base::Scalar, Vector2}; /// Typically, the upper-left corner of a bounding box of a shape. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct Point { + /// Pixel distance from left. pub x: i32, + /// Pixel distance from the top. pub y: i32, } @@ -22,6 +24,7 @@ impl Point { /// The coordinate of the top-left corner on the screen. pub const MIN: Point = Point { x: 0, y: 0 }; + /// Create a new point casting the argument types. #[must_use] pub fn new>(x: I, y: I) -> Self { Self { @@ -32,7 +35,7 @@ impl Point { /// Set x and y to their absolute (non-negative) value. #[must_use] - pub fn abs(self) -> Self { + pub const fn abs(self) -> Self { Self { x: self.x.abs(), y: self.y.abs(), diff --git a/src/graphics/size.rs b/src/graphics/size.rs index bb1a11e..8c2fb9a 100644 --- a/src/graphics/size.rs +++ b/src/graphics/size.rs @@ -14,7 +14,9 @@ pub const HEIGHT: i32 = 160; /// The width and height must be positive. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct Size { + /// Horizontal size in pixels. pub width: i32, + /// Vertical size in pixels. pub height: i32, } @@ -25,6 +27,7 @@ impl Size { height: HEIGHT, }; + /// Create a new size casting the argument types. #[must_use] pub fn new>(width: I, height: I) -> Self { Self { @@ -35,7 +38,7 @@ impl Size { /// Set both width and height to their absolute (non-negative) value. #[must_use] - pub fn abs(self) -> Self { + pub const fn abs(self) -> Self { Self { width: self.width.abs(), height: self.height.abs(), diff --git a/src/graphics/types.rs b/src/graphics/types.rs index c88f4a0..1a9b8ce 100644 --- a/src/graphics/types.rs +++ b/src/graphics/types.rs @@ -1,8 +1,11 @@ /// The RGB value of a color in the palette. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct RGB { + /// Red component. pub r: u8, + /// Green component. pub g: u8, + /// Blue component. pub b: u8, } @@ -36,7 +39,7 @@ impl Style { /// /// [`LineStyle`] is the same as [Style] except it doesn't have a fill color. #[must_use] - pub fn as_line_style(&self) -> LineStyle { + pub const fn as_line_style(&self) -> LineStyle { LineStyle { color: self.stroke_color, width: self.stroke_width, @@ -47,7 +50,9 @@ impl Style { /// The same as [Style] but without a fill color (only stroke color and width). #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct LineStyle { + /// The line stroke color. pub color: Color, + /// The line stroke width. pub width: i32, } @@ -63,22 +68,39 @@ impl From