use crate::palette::{add_bias, map_rgb_to_spectra6_nearest, Spectra6};
pub trait DitherStrategy {
fn map(&mut self, x: u32, y: u32, rgb: [u8; 3]) -> Spectra6;
}
#[cfg(feature = "dither-bayer")]
pub struct Bayer4x4;
#[cfg(feature = "dither-bayer")]
impl DitherStrategy for Bayer4x4 {
fn map(&mut self, x: u32, y: u32, rgb: [u8; 3]) -> Spectra6 {
const M: [[i16; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
let t = M[(y as usize) & 3][(x as usize) & 3] as i16; let bias = t - 8;
let b = [bias, bias, bias];
let nudged = add_bias(rgb, b);
map_rgb_to_spectra6_nearest(nudged)
}
}
#[cfg(feature = "dither-fs")]
pub struct FloydSteinberg {
width: u32,
cur: alloc::vec::Vec<i16>,
nxt: alloc::vec::Vec<i16>,
x: u32,
y: u32,
}
#[cfg(feature = "dither-fs")]
impl FloydSteinberg {
pub fn new(width: u32) -> Self {
let len = (width as usize) * 3;
Self {
width,
cur: alloc::vec![0; len],
nxt: alloc::vec![0; len],
x: 0,
y: 0,
}
}
pub fn start_line(&mut self, y: u32) {
if y != self.y {
core::mem::swap(&mut self.cur, &mut self.nxt);
for v in &mut self.nxt {
*v = 0;
}
self.y = y;
self.x = 0;
}
}
}
#[cfg(feature = "dither-fs")]
impl DitherStrategy for FloydSteinberg {
fn map(&mut self, x: u32, y: u32, rgb: [u8; 3]) -> Spectra6 {
if y != self.y || (x == 0 && self.x != 0) {
self.start_line(y);
}
self.x = x;
let idx = (x as usize) * 3;
let adj = [
crate::palette::clamp_u8(rgb[0] as i32 + self.cur[idx + 0] as i32),
crate::palette::clamp_u8(rgb[1] as i32 + self.cur[idx + 1] as i32),
crate::palette::clamp_u8(rgb[2] as i32 + self.cur[idx + 2] as i32),
];
let q = map_rgb_to_spectra6_nearest(adj);
let qc = match q {
Spectra6::White => [255, 255, 255],
Spectra6::Black => [0, 0, 0],
Spectra6::Yellow => [255, 255, 0],
Spectra6::Red => [255, 0, 0],
Spectra6::Green => [0, 255, 0],
Spectra6::Blue => [0, 0, 255],
};
let er = adj[0] as i16 - qc[0] as i16;
let eg = adj[1] as i16 - qc[1] as i16;
let eb = adj[2] as i16 - qc[2] as i16;
if x + 1 < self.width {
let j = idx + 3;
self.cur[j + 0] = self.cur[j + 0].saturating_add((er * 7) / 16);
self.cur[j + 1] = self.cur[j + 1].saturating_add((eg * 7) / 16);
self.cur[j + 2] = self.cur[j + 2].saturating_add((eb * 7) / 16);
}
let below_base = idx;
if x > 0 {
let j = below_base - 3;
self.nxt[j + 0] = self.nxt[j + 0].saturating_add((er * 3) / 16);
self.nxt[j + 1] = self.nxt[j + 1].saturating_add((eg * 3) / 16);
self.nxt[j + 2] = self.nxt[j + 2].saturating_add((eb * 3) / 16);
}
{
let j = below_base;
self.nxt[j + 0] = self.nxt[j + 0].saturating_add((er * 5) / 16);
self.nxt[j + 1] = self.nxt[j + 1].saturating_add((eg * 5) / 16);
self.nxt[j + 2] = self.nxt[j + 2].saturating_add((eb * 5) / 16);
}
if x + 1 < self.width {
let j = below_base + 3;
self.nxt[j + 0] = self.nxt[j + 0].saturating_add((er * 1) / 16);
self.nxt[j + 1] = self.nxt[j + 1].saturating_add((eg * 1) / 16);
self.nxt[j + 2] = self.nxt[j + 2].saturating_add((eb * 1) / 16);
}
q
}
}
#[cfg(feature = "halftone")]
pub struct Halftone {
pub tile: u8,
}
#[cfg(feature = "halftone")]
impl Halftone {
pub fn new(tile: u8) -> Self {
Self {
tile: if tile < 2 { 2 } else { tile.min(3) },
}
}
#[inline]
fn level_from_rgb(rgb: [u8; 3]) -> u8 {
let y = (3 * rgb[0] as u16 + 6 * rgb[1] as u16 + 1 * rgb[2] as u16) / 10;
if y < 32 {
0
} else if y < 96 {
1
} else if y < 160 {
2
} else if y < 224 {
3
} else {
4
}
}
}
#[cfg(feature = "halftone")]
impl DitherStrategy for Halftone {
fn map(&mut self, x: u32, y: u32, rgb: [u8; 3]) -> Spectra6 {
let lvl = Self::level_from_rgb(rgb);
let n = self.tile as u32;
let xi = (x % n) as u8;
let yi = (y % n) as u8;
let on = if self.tile == 2 {
let rank = match (xi, yi) {
(0, 0) => 0,
(1, 1) => 1,
(1, 0) => 2,
_ => 3,
};
lvl > rank
} else {
let rank = match (xi, yi) {
(1, 1) => 0,
(2, 0) | (0, 2) => 1,
(0, 1) | (1, 0) | (1, 2) | (2, 1) => 2,
(0, 0) | (2, 2) => 3,
_ => 4,
};
lvl > rank
};
if on {
Spectra6::White
} else {
Spectra6::Black
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "dither-bayer")]
#[test]
fn bayer_deterministic() {
let mut b = Bayer4x4;
let a = b.map(10, 10, [120, 130, 140]);
let a2 = b.map(10, 10, [120, 130, 140]);
assert_eq!(a, a2);
}
#[cfg(feature = "halftone")]
#[test]
fn halftone_levels() {
let mut h = Halftone::new(2);
let c1 = h.map(0, 0, [10, 10, 10]);
let c2 = h.map(0, 0, [240, 240, 240]);
assert!(matches!(c1, Spectra6::Black));
assert!(matches!(c2, Spectra6::White));
}
}