implement colour mapping and dithering properly
This commit is contained in:
parent
5916c4af17
commit
fd5f141f19
|
@ -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"
|
||||||
|
|
3
TODO.md
3
TODO.md
|
@ -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
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
59
src/lib.rs
59
src/lib.rs
|
@ -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]
|
||||||
|
|
Loading…
Reference in New Issue