diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2824d..b1eb985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## [Unreleased] - ReleaseDate +### Added + +- Added pixel aspect ratio support to render non-square pixels + ## [0.8.0] - 2025-10-10 ### Added diff --git a/src/display.rs b/src/display.rs index 7f17bfc..8ac82fe 100644 --- a/src/display.rs +++ b/src/display.rs @@ -99,11 +99,25 @@ impl SimulatorDisplay { /// Calculates the rendered size of this display based on the output settings. /// /// This method takes into account the [`scale`](OutputSettings::scale) and - /// [`pixel_spacing`](OutputSettings::pixel_spacing) settings to determine - /// the size of this display in output pixels. + /// [`pixel_spacing`](OutputSettings::pixel_spacing) settings as well as the + /// [`pixel_aspect_ratio`](OutputSettings::pixel_aspect_ratio) setting to + /// determine the size of this display in output pixels. pub fn output_size(&self, output_settings: &OutputSettings) -> Size { - self.size * output_settings.scale - + self.size.saturating_sub(Size::new_equal(1)) * output_settings.pixel_spacing + let pixel_size = Size::new( + output_settings + .scale + .saturating_mul(output_settings.pixel_aspect_ratio.width), + output_settings + .scale + .saturating_mul(output_settings.pixel_aspect_ratio.height), + ); + + let width = self.size.width.saturating_mul(pixel_size.width) + + self.size.width.saturating_sub(1) * output_settings.pixel_spacing; + let height = self.size.height.saturating_mul(pixel_size.height) + + self.size.height.saturating_sub(1) * output_settings.pixel_spacing; + + Size::new(width, height) } } diff --git a/src/output_image.rs b/src/output_image.rs index 033381c..bc8cee8 100644 --- a/src/output_image.rs +++ b/src/output_image.rs @@ -64,7 +64,16 @@ where ) .unwrap(); - if output_settings.scale == 1 { + let pixel_size = Size::new( + output_settings + .scale + .saturating_mul(output_settings.pixel_aspect_ratio.width), + output_settings + .scale + .saturating_mul(output_settings.pixel_aspect_ratio.height), + ); + + if pixel_size == Size::new(1, 1) && output_settings.pixel_spacing == 0 { display .bounding_box() .points() @@ -78,8 +87,8 @@ where .draw(self) .unwrap(); } else { - let pixel_pitch = (output_settings.scale + output_settings.pixel_spacing) as i32; - let pixel_size = Size::new(output_settings.scale, output_settings.scale); + let pitch_x = (pixel_size.width + output_settings.pixel_spacing) as i32; + let pitch_y = (pixel_size.height + output_settings.pixel_spacing) as i32; for p in display.bounding_box().points() { let raw_color = display.get_pixel(p).into(); @@ -87,7 +96,10 @@ where let output_color = C::from(themed_color); self.fill_solid( - &Rectangle::new(p * pixel_pitch + position, pixel_size), + &Rectangle::new( + Point::new(p.x * pitch_x, p.y * pitch_y) + position, + pixel_size, + ), output_color, ) .unwrap(); diff --git a/src/output_settings.rs b/src/output_settings.rs index c8766cf..b47f9e5 100644 --- a/src/output_settings.rs +++ b/src/output_settings.rs @@ -1,6 +1,35 @@ use crate::theme::BinaryColorTheme; use embedded_graphics::prelude::*; +/// Pixel aspect ratio. +/// +/// The aspect ratio is given as `width:height` and is applied to the output pixel size. +/// A value of `1:1` represents square pixels. +/// +/// The aspect ratio scales the pixel size in the simulator output. For example, using +/// `pixel_aspect_ratio(2, 1)` will render pixels twice as wide as they are tall. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct PixelAspectRatio { + /// Pixel width ratio component. + pub width: u32, + /// Pixel height ratio component. + pub height: u32, +} + +impl PixelAspectRatio { + /// Creates a new pixel aspect ratio. + /// + /// # Panics + /// + /// Panics if `width` or `height` is `0`. + pub const fn new(width: u32, height: u32) -> Self { + assert!(width > 0, "pixel aspect ratio width must be > 0"); + assert!(height > 0, "pixel aspect ratio height must be > 0"); + + Self { width, height } + } +} + /// Output settings. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct OutputSettings { @@ -8,6 +37,8 @@ pub struct OutputSettings { pub scale: u32, /// Spacing between pixels. pub pixel_spacing: u32, + /// Pixel aspect ratio. + pub pixel_aspect_ratio: PixelAspectRatio, /// Binary color theme. pub theme: BinaryColorTheme, } @@ -15,13 +46,24 @@ pub struct OutputSettings { #[cfg(feature = "with-sdl")] impl OutputSettings { /// Translates a output coordinate to the corresponding display coordinate. - pub(crate) const fn output_to_display(&self, output_point: Point) -> Point { - let pitch = self.pixel_pitch() as i32; - Point::new(output_point.x / pitch, output_point.y / pitch) + pub(crate) fn output_to_display(&self, output_point: Point) -> Point { + let pitch = self.pixel_pitch(); + Point::new(output_point.x / pitch.x, output_point.y / pitch.y) + } + + pub(crate) fn pixel_size(&self) -> Size { + Size::new( + self.scale.saturating_mul(self.pixel_aspect_ratio.width), + self.scale.saturating_mul(self.pixel_aspect_ratio.height), + ) } - pub(crate) const fn pixel_pitch(&self) -> u32 { - self.scale + self.pixel_spacing + pub(crate) fn pixel_pitch(&self) -> Point { + let pixel_size = self.pixel_size(); + Point::new( + (pixel_size.width + self.pixel_spacing) as i32, + (pixel_size.height + self.pixel_spacing) as i32, + ) } } @@ -36,6 +78,7 @@ impl Default for OutputSettings { pub struct OutputSettingsBuilder { scale: Option, pixel_spacing: Option, + pixel_aspect_ratio: PixelAspectRatio, theme: BinaryColorTheme, } @@ -94,12 +137,32 @@ impl OutputSettingsBuilder { self } + /// Sets the pixel aspect ratio. + /// + /// The aspect ratio is given as `width:height`. A value of `1:1` represents square pixels. + /// + /// # Panics + /// + /// Panics if `width` or `height` is `0`. + pub fn pixel_aspect_ratio(mut self, width: u32, height: u32) -> Self { + self.pixel_aspect_ratio = PixelAspectRatio::new(width, height); + + self + } + /// Builds the output settings. pub fn build(self) -> OutputSettings { OutputSettings { scale: self.scale.unwrap_or(1), pixel_spacing: self.pixel_spacing.unwrap_or(0), + pixel_aspect_ratio: self.pixel_aspect_ratio, theme: self.theme, } } } + +impl Default for PixelAspectRatio { + fn default() -> Self { + Self::new(1, 1) + } +}