From 7ee6df7d7ce65b386231ce1fba02afebfec01ece Mon Sep 17 00:00:00 2001 From: gered Date: Wed, 13 Jul 2022 07:34:36 -0400 Subject: [PATCH] add BlendMap --- libretrogd/src/graphics/blendmap.rs | 236 ++++++++++++++++++++++++++++ libretrogd/src/graphics/mod.rs | 2 + 2 files changed, 238 insertions(+) create mode 100644 libretrogd/src/graphics/blendmap.rs diff --git a/libretrogd/src/graphics/blendmap.rs b/libretrogd/src/graphics/blendmap.rs new file mode 100644 index 0000000..2190c25 --- /dev/null +++ b/libretrogd/src/graphics/blendmap.rs @@ -0,0 +1,236 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BlendMapError { + #[error("Source color {0} is out of range for this BlendMap")] + InvalidSourceColor(u8), +} + +/// A lookup table used by [`BlendMap`]s. This table stores destination color to blend color +/// mappings, where the indices are the destination colors and the values at those indices are the +/// blend colors. +pub type BlendMapping = [u8; 256]; + +/// A blend map containing a lookup table to match source colors with destination colors to +/// produce blended colors. +/// +/// Some definitions: +/// * **source color**: colors in some source bitmap that is to be drawn onto a destination +/// * **destination color**: colors on the destination that will be drawn over by the source colors +/// * **blended color**: the final drawn color, found by looking up the source and destination colors +/// +/// A blend map will not necessarily have mappings for all possible 256 source colors. But for each +/// source color, it will have 256 destination to blended color mappings. +#[derive(Clone)] +pub struct BlendMap { + start_color: u8, + end_color: u8, + mapping: Box<[BlendMapping]>, +} + +impl BlendMap { + /// Creates and returns a new [`BlendMap`] with source color mappings for the given inclusive + /// range only. The `start_color` and `end_color` may also be equal to create a blend map with + /// only a single source color mapping. + pub fn new(start_color: u8, end_color: u8) -> Self { + let (start_color, end_color) = if start_color > end_color { + (end_color, start_color) + } else { + (start_color, end_color) + }; + let num_colors = (end_color - start_color) as usize + 1; + BlendMap { + start_color, + end_color, + mapping: vec![[0u8; 256]; num_colors].into_boxed_slice(), + } + } + + /// The beginning source color that is mapped in this blend map. + #[inline] + pub fn start_color(&self) -> u8 { + self.start_color + } + + /// The ending source color that is mapped in this blend map. + #[inline] + pub fn end_color(&self) -> u8 { + self.end_color + } + + /// Returns true if the given source color is mapped in this blend map. + #[inline] + pub fn is_mapped(&self, color: u8) -> bool { + color >= self.start_color && color <= self.end_color + } + + #[inline] + fn get_mapping_index(&self, color: u8) -> Option { + if color >= self.start_color && color <= self.end_color { + let index = (color - self.start_color) as usize; + Some(index) + } else { + None + } + } + + /// Returns a reference to the destination-to-blend color mapping table for the given source + /// color. Returns `None` if the specified source color is not in this blend map. + #[inline] + pub fn get_mapping(&self, color: u8) -> Option<&BlendMapping> { + if let Some(index) = self.get_mapping_index(color) { + // safety: index cannot be outside 0-255 since color and start_color are both u8 + unsafe { Some(self.mapping.get_unchecked(index)) } + } else { + None + } + } + + /// Returns a mutable reference to the destination-to-blend color mapping table for the given + /// source color. Returns `None` if the specified source color is not in this blend map. + #[inline] + pub fn get_mapping_mut(&mut self, color: u8) -> Option<&mut BlendMapping> { + if let Some(index) = self.get_mapping_index(color) { + // safety: index cannot be outside 0-255 since color and start_color are both u8 + unsafe { Some(self.mapping.get_unchecked_mut(index)) } + } else { + None + } + } + + /// Sets the blend color mapping for the given source color and destination color combination. + pub fn set_mapping(&mut self, source_color: u8, dest_color: u8, blended_color: u8) -> Result<(), BlendMapError> { + if let Some(mapping) = self.get_mapping_mut(source_color) { + mapping[dest_color as usize] = blended_color; + Ok(()) + } else { + Err(BlendMapError::InvalidSourceColor(source_color)) + } + } + + + /// Sets a series of blend color mappings for the given source color and starting from a base + /// destination color. + pub fn set_mappings(&mut self, source_color: u8, base_dest_color: u8, mappings: [u8; N]) -> Result<(), BlendMapError> { + if let Some(mapping) = self.get_mapping_mut(source_color) { + assert!((base_dest_color as usize + N - 1) <= 255, "mappings array is too big for the remaining colors available"); + for index in 0..N { + mapping[index + base_dest_color as usize] = mappings[index]; + } + Ok(()) + } else { + Err(BlendMapError::InvalidSourceColor(source_color)) + } + } + + /// Returns the blend color for the given source and destination colors. If the source color + /// is not in this blend map, `None` is returned. + #[inline] + pub fn blend(&self, source_color: u8, dest_color: u8) -> Option { + if let Some(mapping) = self.get_mapping(source_color) { + Some(mapping[dest_color as usize]) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use claim::*; + + use super::*; + + #[test] + pub fn create() -> Result<(), BlendMapError> { + let blend_map = BlendMap::new(10, 12); + assert_eq!(10, blend_map.start_color()); + assert_eq!(12, blend_map.end_color()); + assert!(blend_map.is_mapped(10)); + assert!(blend_map.is_mapped(11)); + assert!(blend_map.is_mapped(12)); + assert!(!blend_map.is_mapped(9)); + assert!(!blend_map.is_mapped(13)); + assert_some!(blend_map.get_mapping(10)); + assert_some!(blend_map.get_mapping(11)); + assert_some!(blend_map.get_mapping(12)); + assert_none!(blend_map.get_mapping(9)); + assert_none!(blend_map.get_mapping(13)); + + let blend_map = BlendMap::new(12, 10); + assert_eq!(10, blend_map.start_color()); + assert_eq!(12, blend_map.end_color()); + assert!(blend_map.is_mapped(10)); + assert!(blend_map.is_mapped(11)); + assert!(blend_map.is_mapped(12)); + assert!(!blend_map.is_mapped(9)); + assert!(!blend_map.is_mapped(13)); + assert_some!(blend_map.get_mapping(10)); + assert_some!(blend_map.get_mapping(11)); + assert_some!(blend_map.get_mapping(12)); + assert_none!(blend_map.get_mapping(9)); + assert_none!(blend_map.get_mapping(13)); + + let blend_map = BlendMap::new(130, 130); + assert_eq!(130, blend_map.start_color()); + assert_eq!(130, blend_map.end_color()); + assert!(blend_map.is_mapped(130)); + assert!(!blend_map.is_mapped(129)); + assert!(!blend_map.is_mapped(131)); + assert_some!(blend_map.get_mapping(130)); + assert_none!(blend_map.get_mapping(129)); + assert_none!(blend_map.get_mapping(131)); + + Ok(()) + } + + #[test] + pub fn mapping() -> Result<(), BlendMapError> { + let mut blend_map = BlendMap::new(16, 31); + + assert_none!(blend_map.blend(15, 0)); + assert_eq!(Some(0), blend_map.blend(16, 0)); + assert_eq!(Some(0), blend_map.blend(16, 1)); + assert_ok!(blend_map.set_mapping(16, 0, 116)); + assert_eq!(Some(116), blend_map.blend(16, 0)); + assert_eq!(Some(0), blend_map.blend(16, 1)); + + let mapping = blend_map.get_mapping(16).unwrap(); + assert_eq!(116, mapping[0]); + assert_eq!(0, mapping[1]); + + assert_eq!(Some(0), blend_map.blend(17, 0)); + assert_ok!(blend_map.set_mapping(17, 0, 117)); + assert_eq!(Some(117), blend_map.blend(17, 0)); + let mapping = blend_map.get_mapping_mut(17).unwrap(); + assert_eq!(117, mapping[0]); + mapping[0] = 217; + assert_eq!(Some(217), blend_map.blend(17, 0)); + + assert_matches!( + blend_map.set_mapping(64, 1, 2), + Err(BlendMapError::InvalidSourceColor(64)) + ); + + Ok(()) + } + + #[test] + pub fn bulk_mappings() -> Result<(), BlendMapError> { + let mut blend_map = BlendMap::new(0, 7); + + let mapping = blend_map.get_mapping(2).unwrap(); + assert_eq!([0, 0, 0, 0, 0, 0, 0, 0], mapping[0..8]); + + assert_ok!(blend_map.set_mappings(2, 4, [1, 2, 3, 4, 5, 6, 7, 8])); + + let mapping = blend_map.get_mapping(2).unwrap(); + assert_eq!( + [0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0], + mapping[0..16] + ); + + + Ok(()) + } +} \ No newline at end of file diff --git a/libretrogd/src/graphics/mod.rs b/libretrogd/src/graphics/mod.rs index 8ab4bf0..53bf14b 100644 --- a/libretrogd/src/graphics/mod.rs +++ b/libretrogd/src/graphics/mod.rs @@ -1,10 +1,12 @@ pub use self::bitmap::*; pub use self::bitmapatlas::*; +pub use self::blendmap::*; pub use self::font::*; pub use self::palette::*; pub mod bitmap; pub mod bitmapatlas; +pub mod blendmap; pub mod font; pub mod palette;