diff --git a/Cargo.toml b/Cargo.toml index caf9afa..b4d0742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ crate-type = ["cdylib"] [dependencies] "base64" = "^0.12.3" "bitsy-parser" = "^0.72.3" -"dither" = "1.3.9" "image" = "^0.23.7" "json" = "^0.12.4" "lazy_static" = "^1.4.0" diff --git a/TODO.md b/TODO.md index d0fd61a..bafbfbf 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,7 @@ # todo -* preview -* dithering * tests * if image is exactly 128×128, *don't* crop * custom palette * tile reuse +* find a way to implement Atkinson dithering instead of CatmullRom diff --git a/src/colour_map.rs b/src/colour_map.rs new file mode 100644 index 0000000..1a0b62e --- /dev/null +++ b/src/colour_map.rs @@ -0,0 +1,74 @@ +use image::{Luma, Rgba}; + +#[derive(Clone, Copy)] +pub struct ColourMap { + background: Rgba, + foreground: Rgba, +} + +impl ColourMap { + pub(crate) fn from(palette: &bitsy_parser::Palette) -> ColourMap { + let background = Rgba::from([ + palette.colours[0].red, + palette.colours[0].green, + palette.colours[0].blue, + 255, + ]); + + let foreground = Rgba::from([ + palette.colours[1].red, + palette.colours[1].green, + palette.colours[1].blue, + 255, + ]); + + ColourMap { background, foreground } + } +} + +fn diff(a: &Rgba, b:&Rgba) -> u32 { + let diff_red = (a[0] as i16 - b[0] as i16).abs(); + let diff_green= (a[1] as i16 - b[1] as i16).abs(); + let diff_blue = (a[2] as i16 - b[2] as i16).abs(); + + (diff_red + diff_green + diff_blue) as u32 +} + +impl image::imageops::colorops::ColorMap for ColourMap { + type Color = Rgba; + + #[inline(always)] + fn index_of(&self, color: &Self::Color) -> usize { + let diff_background = diff(color, &self.background); + let diff_foreground = diff(color, &self.foreground); + + if diff_foreground <= diff_background { 1 } else { 0 } + } + + #[inline(always)] + fn lookup(&self, idx: usize) -> Option { + match idx { + 0 => Some(self.background.into()), + 1 => Some(self.foreground.into()), + _ => None, + } + } + + /// Indicate NeuQuant implements `lookup`. + fn has_lookup(&self) -> bool { + true + } + + #[inline(always)] + fn map_color(&self, color: &mut Self::Color) { + let closest = match self.index_of(color) { + 1 => self.foreground, + _ => self.background, + }; + + color[0] = closest[0]; + color[1] = closest[1]; + color[2] = closest[2]; + color[3] = closest[3]; + } +} diff --git a/src/lib.rs b/src/lib.rs index 1268165..173a2eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,15 @@ use bitsy_parser::game::Game; use bitsy_parser::image::Image; use bitsy_parser::tile::Tile; -use image::{GenericImageView, Pixel, DynamicImage}; +use image::{GenericImageView, Pixel, DynamicImage, GenericImage, ImageBuffer}; use lazy_static::lazy_static; use std::sync::Mutex; use wasm_bindgen::prelude::*; -use image::imageops::dither; + +mod colour_map; + +use colour_map::ColourMap; +use image::imageops::FilterType::CatmullRom; const SD: u32 = 8; @@ -156,54 +160,21 @@ fn image_to_base64(image: &DynamicImage) -> String { format!("data:image/png;base64,{}", base64::encode(&bytes)) } -fn colour_difference(compare: image::Rgba, other: &bitsy_parser::Colour) -> u32 { - let diff_red = (compare[0] as i16 - other.red as i16).abs(); - let diff_green= (compare[1] as i16 - other.green as i16).abs(); - let diff_blue = (compare[2] as i16 - other.blue as i16).abs(); - - (diff_red + diff_green + diff_blue) as u32 -} - -fn closest_colour(compare: image::Rgba, colours: &[bitsy_parser::Colour]) -> image::Rgba { - let diff_background = colour_difference(compare, &colours[0]); - let diff_foreground = colour_difference(compare, &colours[1]); - - if diff_foreground <= diff_background { - image::Rgba::from([colours[1].red, colours[1].green, colours[1].blue, 255]) - } else { - image::Rgba::from([colours[0].red, colours[0].green, colours[0].blue, 255]) - } -} - -fn adjust_brightness(pixel: &mut image::Rgba, state: &State) -> image::Rgba { - pixel[0] = (pixel[0] as i32 + state.brightness).clamp(0, 255) as u8; - pixel[1] = (pixel[1] as i32 + state.brightness).clamp(0, 255) as u8; - pixel[2] = (pixel[2] as i32 + state.brightness).clamp(0, 255) as u8; - - *pixel -} - fn render_preview(state: &State) -> DynamicImage { - let image = state.image.as_ref().unwrap().clone(); - let mut preview = image.clone(); + let mut buffer = state.image.as_ref().unwrap().clone().into_rgba(); - // todo dither + // todo get actual chosen palette + let colour_map = crate::ColourMap::from(&state.game.as_ref().unwrap().palettes[0]); - // todo convert to palette colours + image::imageops::brighten(&mut buffer, state.brightness); - // get background and foreground colours from palette - let colours = &state.game.as_ref().unwrap().palettes - .iter() - .find(|palette| &palette.id == state.palette.as_ref().unwrap()) - .unwrap() - .colours[0..2]; - - for (x, y, mut pixel) in image.pixels() { - // is pixel closer to background or foreground? - preview.put_pixel(x, y, closest_colour(adjust_brightness(&mut pixel, &state), colours)); + if state.dither { + image::imageops::dither(&mut buffer, &colour_map); + } else { + image::imageops::colorops::index_colors(&mut buffer, &colour_map); } - preview + image::DynamicImage::ImageRgba8(buffer) } #[wasm_bindgen]