pixie_anim_lib/quant/
dither.rs

1//! Perceptual Dithering Algorithms.
2
3use crate::color::{rgb_to_lab, Lab};
4use crate::quant::Rgb;
5use crate::simd::{find_nearest_color_lab, PlanarLabPalette};
6
7/// Blue Noise Mask (32x32)
8/// Generated using Void-and-Cluster algorithm.
9const BLUE_NOISE_32: [u8; 1024] = [
10    191, 14, 131, 231, 101, 12, 161, 42, 203, 115, 24, 142, 215, 56, 178, 89, 51, 245, 78, 168, 32,
11    212, 122, 253, 67, 182, 234, 10, 154, 226, 135, 198, 112, 145, 23, 194, 95, 151, 238, 84, 128,
12    219, 164, 48, 207, 72, 118, 37, 250, 62, 175, 108, 222, 1, 187, 138, 30, 242, 92, 157, 230,
13    104, 181, 211, 125, 235, 40, 148, 216, 75, 111, 254, 53, 197, 132, 21, 171, 81, 247, 15, 18,
14    162, 241, 87, 121, 190, 34, 225, 150, 60, 239, 100, 141, 213, 124, 177, 205, 94, 50, 228, 137,
15    174, 248, 11, 184, 218, 45, 159, 70, 252, 114, 31, 129, 214, 167, 43, 106, 232, 65, 144, 221,
16    19, 193, 127, 244, 82, 170, 201, 55, 240, 79, 180, 251, 91, 153, 236, 117, 172, 52, 210, 134,
17    227, 39, 147, 110, 143, 22, 202, 120, 224, 33, 160, 246, 8, 189, 103, 255, 68, 113, 196, 220,
18    64, 173, 97, 243, 139, 208, 74, 156, 233, 47, 140, 217, 126, 179, 86, 29, 192, 130, 211, 41,
19    165, 249, 107, 183, 223, 90, 152, 58, 241, 102, 237, 155, 234, 105, 119, 253, 17, 195, 133,
20    204, 36, 169, 229, 116, 186, 21, 149, 71, 206, 83, 176, 242, 146, 221, 54, 111, 251, 93, 158,
21    238, 66, 199, 123, 128, 38, 215, 110, 61, 236, 136, 185, 212, 14, 163, 245, 101, 172, 44, 209,
22    248, 166, 244, 194, 96, 150, 230, 10, 188, 222, 118, 49, 203, 77, 131, 254, 114, 225, 35, 142,
23    217, 73, 161, 247, 134, 226, 57, 178, 88, 213, 121, 197, 59, 239, 104, 181, 252, 92, 154, 233,
24    46, 147, 210, 125, 240, 16, 168, 231, 112, 145, 25, 205, 138, 228, 12, 184, 219, 100, 164, 246,
25    85, 191, 132, 202, 249, 69, 174, 98, 243, 1, 159, 235, 51, 214, 127, 171, 224, 33, 115, 250,
26    130, 216, 41, 141, 211, 122, 193, 237, 106, 182, 223, 19, 149, 232, 63, 176, 55, 241, 80, 169,
27    255, 109, 153, 221, 117, 173, 52, 208, 135, 227, 102, 195, 113, 144, 22, 204, 133, 225, 36,
28    160, 245, 10, 187, 234, 119, 253, 17, 170, 251, 67, 175, 95, 239, 136, 218, 76, 151, 229, 44,
29    143, 212, 124, 180, 89, 30, 192, 131, 213, 48, 166, 248, 105, 183, 222, 91, 155, 58, 244, 103,
30    236, 157, 235, 108, 120, 252, 15, 196, 139, 207, 32, 162, 230, 111, 185, 20, 148, 72, 203, 84,
31    177, 243, 147, 220, 53, 114, 254, 97, 152, 233, 65, 190, 126, 129, 39, 214, 116, 60, 237, 134,
32    186, 211, 13, 165, 246, 100, 171, 47, 209, 247, 167, 242, 193, 94, 150, 228, 11, 189, 221, 118,
33    50, 202, 78, 132, 255, 115, 226, 34, 140, 216, 74, 163, 249, 137, 227, 56, 179, 86, 215, 121,
34    198, 59, 240, 101, 181, 253, 93, 156, 234, 45, 146, 210, 125, 241, 18, 169, 231, 112, 144, 23,
35    206, 138, 229, 12, 182, 218, 99, 164, 245, 87, 194, 130, 203, 250, 68, 173, 96, 244, 2, 158,
36    232, 51, 212, 127, 170, 225, 31, 117, 251, 131, 217, 42, 142, 213, 123, 192, 238, 107, 183,
37    222, 20, 149, 233, 62, 177, 54, 242, 81, 168, 254, 108, 154, 220, 116, 174, 53, 207, 136, 226,
38    103, 196, 113, 145, 21, 205, 132, 224, 37, 161, 246, 9, 188, 235, 119, 252, 16, 171, 252, 66,
39    176, 94, 240, 135, 219, 75, 152, 228, 43, 144, 211, 124, 181, 88, 30, 191, 130, 212, 49, 167,
40    247, 104, 184, 223, 90, 156, 57, 243, 102, 237, 158, 234, 109, 121, 253, 14, 197, 138, 208, 33,
41    163, 231, 110, 186, 19, 147, 73, 204, 83, 178, 242, 146, 221, 52, 115, 255, 98, 153, 232, 64,
42    189, 127, 128, 40, 215, 117, 61, 236, 133, 185, 210, 12, 166, 245, 101, 172, 46, 209, 248, 168,
43    241, 194, 93, 151, 229, 10, 188, 220, 118, 51, 203, 79, 131, 254, 114, 227, 35, 141, 217, 74,
44    162, 250, 137, 226, 55, 180, 85, 216, 122, 199, 58, 239, 100, 182, 252, 92, 155, 233, 44, 145,
45    211, 126, 240, 17, 170, 230, 113, 143, 24, 207, 139, 228, 13, 181, 219, 98, 165, 244, 86, 195,
46    129, 202, 249, 69, 174, 97, 243, 1, 159, 231, 50, 213, 128, 171, 224, 32, 116, 251, 130, 218,
47    41, 143, 212, 123, 193, 237, 106, 184, 223, 21, 148, 232, 63, 177, 53, 242, 80, 169, 255, 109,
48    154, 221, 117, 173, 52, 208, 134, 227, 102, 196, 112, 144, 22, 204, 133, 225, 36, 160, 245, 11,
49    187, 234, 120, 253, 18, 170, 250, 67, 175, 95, 238, 136, 217, 76, 151, 229, 45, 142, 213, 124,
50    180, 90, 29, 192, 131, 211, 48, 166, 249, 105, 182, 222, 91, 155, 59, 244, 103, 236, 157, 235,
51    108, 119, 252, 15, 195, 139, 206, 33, 162, 230, 111, 185, 20, 149, 71, 203, 84, 176, 243, 147,
52    220, 54, 114, 254, 96, 152, 233, 66, 190, 125, 128, 38, 214, 116, 61, 237, 134, 186, 212, 14,
53    165, 246, 101, 172, 47, 208, 247, 167, 241, 194, 94, 150, 228, 10, 188, 221, 118, 49, 202, 77,
54    132, 255, 115, 226, 34, 140, 216, 73, 163, 248, 137, 227, 56, 179, 87, 215, 122, 197, 60, 240,
55    104, 181, 253, 92, 156, 234, 46, 146, 210, 126, 241, 17, 168, 231, 113, 144, 23, 205, 138, 229,
56    12, 183, 219, 99, 164, 245, 88, 193, 130, 203, 250, 68, 173, 98, 242, 2, 158, 232, 51, 214,
57    127, 171, 224, 31, 117, 251, 131, 217, 42, 141, 211, 123, 192, 238, 107, 182, 223, 19, 149,
58    233, 62, 176, 55, 242, 79, 169, 254, 108, 153, 222, 118, 174, 53, 207, 135, 226, 102, 195, 114,
59    145, 22, 204, 133, 225, 37, 160, 246, 9, 187, 235, 119, 252, 16, 172, 252, 65, 175, 95, 239,
60    136, 218, 75, 151, 228, 44, 143, 213, 124, 181, 89, 30, 191, 132, 212, 48, 167, 248, 104, 184,
61    222, 90, 156, 58, 243, 101, 237,
62];
63
64/// Applies perceptual Floyd-Steinberg dithering to a frame.
65pub fn dither_floyd_steinberg(
66    width: u16,
67    height: u16,
68    pixels: &[Rgb],
69    palette: &[Rgb],
70    strength: f32,
71) -> Vec<u8> {
72    let w = width as usize;
73    let h = height as usize;
74    let mut indices = vec![0u8; w * h];
75
76    let lab_palette: Vec<Lab> = palette.iter().map(|p| rgb_to_lab(p.r, p.g, p.b)).collect();
77    let planar_palette = PlanarLabPalette::from_lab(&lab_palette);
78
79    let mut error_buf_l = vec![0.0f32; w * h];
80    let mut error_buf_a = vec![0.0f32; w * h];
81    let mut error_buf_b = vec![0.0f32; w * h];
82
83    for y in 0..h {
84        for x in 0..w {
85            let idx = y * w + x;
86            let original_lab = rgb_to_lab(pixels[idx].r, pixels[idx].g, pixels[idx].b);
87            let current_lab = Lab {
88                l: (original_lab.l + error_buf_l[idx]).clamp(0.0, 100.0),
89                a: (original_lab.a + error_buf_a[idx]).clamp(-128.0, 127.0),
90                b: (original_lab.b + error_buf_b[idx]).clamp(-128.0, 127.0),
91            };
92
93            let color_idx = find_nearest_color_lab(current_lab, &planar_palette);
94            indices[idx] = color_idx as u8;
95
96            let best_color_lab = lab_palette[color_idx];
97            let err_l = (current_lab.l - best_color_lab.l) * strength;
98            let err_a = (current_lab.a - best_color_lab.a) * strength;
99            let err_b = (current_lab.b - best_color_lab.b) * strength;
100
101            let mut bufs = ErrorBuffers {
102                l: &mut error_buf_l,
103                a: &mut error_buf_a,
104                b: &mut error_buf_b,
105            };
106
107            if x + 1 < w {
108                diffuse(w, &mut bufs, x + 1, y, err_l, err_a, err_b, 7.0 / 16.0);
109            }
110            if y + 1 < h {
111                if x > 0 {
112                    diffuse(w, &mut bufs, x - 1, y + 1, err_l, err_a, err_b, 3.0 / 16.0);
113                }
114                diffuse(w, &mut bufs, x, y + 1, err_l, err_a, err_b, 5.0 / 16.0);
115                if x + 1 < w {
116                    diffuse(w, &mut bufs, x + 1, y + 1, err_l, err_a, err_b, 1.0 / 16.0);
117                }
118            }
119        }
120    }
121    indices
122}
123
124const BAYER_8X8: [u8; 64] = [
125    0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26, 12, 44, 4, 36, 14, 46, 6, 38, 60,
126    28, 52, 20, 62, 30, 54, 22, 3, 35, 11, 43, 1, 33, 9, 41, 51, 19, 59, 27, 49, 17, 57, 25, 15,
127    47, 7, 39, 13, 45, 5, 37, 63, 31, 55, 23, 61, 29, 53, 21,
128];
129
130/// Applies Ordered Dithering (Bayer 8x8) to a frame.
131pub fn dither_ordered(
132    width: u16,
133    height: u16,
134    pixels: &[Rgb],
135    palette: &[Rgb],
136    strength: f32,
137) -> Vec<u8> {
138    let w = width as usize;
139    let h = height as usize;
140    let mut indices = vec![0u8; w * h];
141
142    let lab_palette: Vec<Lab> = palette.iter().map(|p| rgb_to_lab(p.r, p.g, p.b)).collect();
143    let planar_palette = PlanarLabPalette::from_lab(&lab_palette);
144
145    // Scaling factor for Ordered dither
146    let spread = 4.0f32 * strength;
147
148    for y in 0..h {
149        for x in 0..w {
150            let idx = y * w + x;
151            let bayer_val = BAYER_8X8[(y % 8) * 8 + (x % 8)] as f32 / 64.0;
152            let offset = (bayer_val - 0.5) * spread;
153
154            let original_lab = rgb_to_lab(pixels[idx].r, pixels[idx].g, pixels[idx].b);
155            let current_lab = Lab {
156                l: (original_lab.l + offset).clamp(0.0, 100.0),
157                a: (original_lab.a + offset * 0.5).clamp(-128.0, 127.0),
158                b: (original_lab.b + offset * 0.5).clamp(-128.0, 127.0),
159            };
160
161            let color_idx = find_nearest_color_lab(current_lab, &planar_palette);
162            indices[idx] = color_idx as u8;
163        }
164    }
165    indices
166}
167
168/// Applies Blue Noise dithering to a frame.
169pub fn dither_blue_noise(
170    width: u16,
171    height: u16,
172    pixels: &[Rgb],
173    palette: &[Rgb],
174    strength: f32,
175) -> Vec<u8> {
176    let w = width as usize;
177    let h = height as usize;
178    let mut indices = vec![0u8; w * h];
179
180    let lab_palette: Vec<Lab> = palette.iter().map(|p| rgb_to_lab(p.r, p.g, p.b)).collect();
181    let planar_palette = PlanarLabPalette::from_lab(&lab_palette);
182
183    for y in 0..h {
184        for x in 0..w {
185            let idx = y * w + x;
186            let (ol, oa, ob) = get_blue_noise_offset(x as u16, y as u16, strength);
187
188            let original_lab = rgb_to_lab(pixels[idx].r, pixels[idx].g, pixels[idx].b);
189            let current_lab = Lab {
190                l: (original_lab.l + ol).clamp(0.0, 100.0),
191                a: (original_lab.a + oa).clamp(-128.0, 127.0),
192                b: (original_lab.b + ob).clamp(-128.0, 127.0),
193            };
194
195            let color_idx = find_nearest_color_lab(current_lab, &planar_palette);
196            indices[idx] = color_idx as u8;
197        }
198    }
199    indices
200}
201
202/// Helper to get a deterministic Lab offset for a given coordinate
203#[inline]
204pub fn get_blue_noise_offset(x: u16, y: u16, strength: f32) -> (f32, f32, f32) {
205    let noise_val = BLUE_NOISE_32[((y % 32) as usize * 32) + (x % 32) as usize] as f32 / 255.0;
206    let noise_intensity = 3.0f32 * strength;
207    let offset = (noise_val - 0.5) * noise_intensity;
208    (offset, offset * 0.5, offset * 0.5)
209}
210
211struct ErrorBuffers<'a> {
212    l: &'a mut [f32],
213    a: &'a mut [f32],
214    b: &'a mut [f32],
215}
216
217#[inline]
218#[allow(clippy::too_many_arguments)]
219fn diffuse(
220    w: usize,
221    bufs: &mut ErrorBuffers,
222    x: usize,
223    y: usize,
224    el: f32,
225    ea: f32,
226    eb: f32,
227    weight: f32,
228) {
229    let idx = y * w + x;
230    bufs.l[idx] += el * weight;
231    bufs.a[idx] += ea * weight;
232    bufs.b[idx] += eb * weight;
233}