implement colour mapping and dithering properly

This commit is contained in:
Max Bradbury 2020-11-06 15:33:26 +00:00
parent 5916c4af17
commit fd5f141f19
4 changed files with 90 additions and 47 deletions

View File

@ -13,7 +13,6 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
"base64" = "^0.12.3" "base64" = "^0.12.3"
"bitsy-parser" = "^0.72.3" "bitsy-parser" = "^0.72.3"
"dither" = "1.3.9"
"image" = "^0.23.7" "image" = "^0.23.7"
"json" = "^0.12.4" "json" = "^0.12.4"
"lazy_static" = "^1.4.0" "lazy_static" = "^1.4.0"

View File

@ -1,8 +1,7 @@
# todo # todo
* preview
* dithering
* tests * tests
* if image is exactly 128×128, *don't* crop * if image is exactly 128×128, *don't* crop
* custom palette * custom palette
* tile reuse * tile reuse
* find a way to implement Atkinson dithering instead of CatmullRom

74
src/colour_map.rs Normal file
View File

@ -0,0 +1,74 @@
use image::{Luma, Rgba};
#[derive(Clone, Copy)]
pub struct ColourMap {
background: Rgba<u8>,
foreground: Rgba<u8>,
}
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<u8>, b:&Rgba<u8>) -> 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<u8>;
#[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<Self::Color> {
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];
}
}

View File

@ -3,11 +3,15 @@
use bitsy_parser::game::Game; use bitsy_parser::game::Game;
use bitsy_parser::image::Image; use bitsy_parser::image::Image;
use bitsy_parser::tile::Tile; use bitsy_parser::tile::Tile;
use image::{GenericImageView, Pixel, DynamicImage}; use image::{GenericImageView, Pixel, DynamicImage, GenericImage, ImageBuffer};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::Mutex; use std::sync::Mutex;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use image::imageops::dither;
mod colour_map;
use colour_map::ColourMap;
use image::imageops::FilterType::CatmullRom;
const SD: u32 = 8; const SD: u32 = 8;
@ -156,54 +160,21 @@ fn image_to_base64(image: &DynamicImage) -> String {
format!("data:image/png;base64,{}", base64::encode(&bytes)) format!("data:image/png;base64,{}", base64::encode(&bytes))
} }
fn colour_difference(compare: image::Rgba<u8>, 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<u8>, colours: &[bitsy_parser::Colour]) -> image::Rgba<u8> {
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<u8>, state: &State) -> image::Rgba<u8> {
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 { fn render_preview(state: &State) -> DynamicImage {
let image = state.image.as_ref().unwrap().clone(); let mut buffer = state.image.as_ref().unwrap().clone().into_rgba();
let mut preview = image.clone();
// 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 if state.dither {
let colours = &state.game.as_ref().unwrap().palettes image::imageops::dither(&mut buffer, &colour_map);
.iter() } else {
.find(|palette| &palette.id == state.palette.as_ref().unwrap()) image::imageops::colorops::index_colors(&mut buffer, &colour_map);
.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));
} }
preview image::DynamicImage::ImageRgba8(buffer)
} }
#[wasm_bindgen] #[wasm_bindgen]