1use crate::quant::{DitherType, Rgb};
4
5#[derive(Debug, Default)]
7pub struct FrameDelta {
8 pub x: u16,
10 pub y: u16,
12 pub width: u16,
14 pub height: u16,
16 pub indices: Vec<u8>,
18}
19
20#[inline]
22fn rgb_dist_sq(c1: Rgb, c2: Rgb) -> u32 {
23 let dr = c1.r as i32 - c2.r as i32;
24 let dg = c1.g as i32 - c2.g as i32;
25 let db = c1.b as i32 - c2.b as i32;
26 (dr * dr + dg * dg + db * db) as u32
27}
28
29pub struct DeltaOptions<'a> {
32 pub width: u16,
34 pub height: u16,
36 pub palette: &'a [Rgb],
38 pub transparent_idx: u8,
40 pub fuzz_threshold: u32,
42 pub dither: DitherType,
44 pub dither_strength: f32,
46}
47
48pub fn find_delta_fuzzy(
57 curr_pixels: &[Rgb],
58 prev_pixels: &[Rgb],
59 prev_indices: Option<&[u8]>,
60 options: &DeltaOptions,
61) -> Option<FrameDelta> {
62 if curr_pixels.len() != prev_pixels.len() {
63 return None;
64 }
65
66 let mut min_x = options.width;
67 let mut max_x = 0;
68 let mut min_y = options.height;
69 let mut max_y = 0;
70 let mut changed = false;
71
72 for y in 0..options.height {
74 for x in 0..options.width {
75 let idx = (y as usize * options.width as usize) + x as usize;
76 if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) > options.fuzz_threshold {
77 if x < min_x {
78 min_x = x;
79 }
80 if x > max_x {
81 max_x = x;
82 }
83 if y < min_y {
84 min_y = y;
85 }
86 if y > max_y {
87 max_y = y;
88 }
89 changed = true;
90 }
91 }
92 }
93
94 if !changed {
95 return None;
96 }
97
98 let delta_width = max_x - min_x + 1;
99 let delta_height = max_y - min_y + 1;
100
101 let lab_palette = if options.dither == DitherType::BlueNoise {
105 let lp: Vec<crate::color::Lab> = options
106 .palette
107 .iter()
108 .map(|p| crate::color::rgb_to_lab(p.r, p.g, p.b))
109 .collect();
110 let pp = crate::simd::PlanarLabPalette::from_lab(&lp);
111 Some((lp, pp))
112 } else {
113 None
114 };
115
116 #[cfg(feature = "rayon")]
117 use rayon::prelude::*;
118
119 #[cfg(feature = "rayon")]
120 let delta_indices: Vec<u8> = (min_y..=max_y)
121 .into_par_iter()
122 .flat_map(|y| {
123 let lab_palette_ref = lab_palette.as_ref();
124 (min_x..=max_x).into_par_iter().map(move |x| {
125 let idx = (y as usize * options.width as usize) + x as usize;
126 if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) <= options.fuzz_threshold {
128 options.transparent_idx
129 } else {
130 if let Some(prev_idx_buffer) = prev_indices {
134 let prev_idx = prev_idx_buffer[idx];
135 if prev_idx != options.transparent_idx {
136 let prev_color = options.palette[prev_idx as usize];
137 if rgb_dist_sq(curr_pixels[idx], prev_color)
139 <= options.fuzz_threshold / 2
140 {
141 return prev_idx;
142 }
143 }
144 }
145
146 match options.dither {
147 DitherType::BlueNoise => {
148 if let Some((_, pp)) = lab_palette_ref {
149 let (ol, oa, ob) = crate::quant::dither::get_blue_noise_offset(
150 x,
151 y,
152 options.dither_strength,
153 );
154 let p = curr_pixels[idx];
155 let mut lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
156 lab.l = (lab.l + ol).clamp(0.0, 100.0);
157 lab.a = (lab.a + oa).clamp(-128.0, 127.0);
158 lab.b = (lab.b + ob).clamp(-128.0, 127.0);
159 crate::simd::find_nearest_color_lab(lab, pp) as u8
160 } else {
161 crate::simd::find_nearest_color(curr_pixels[idx], options.palette)
162 as u8
163 }
164 }
165 _ => {
166 crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8
167 }
168 }
169 }
170 })
171 })
172 .collect();
173
174 #[cfg(not(feature = "rayon"))]
175 let delta_indices: Vec<u8> = (min_y..=max_y)
176 .flat_map(|y| {
177 let lab_palette_ref = lab_palette.as_ref();
178 (min_x..=max_x).map(move |x| {
179 let idx = (y as usize * options.width as usize) + x as usize;
180 if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) <= options.fuzz_threshold {
181 options.transparent_idx
182 } else {
183 if let Some(prev_idx_buffer) = prev_indices {
185 let prev_idx = prev_idx_buffer[idx];
186 if prev_idx != options.transparent_idx {
187 let prev_color = options.palette[prev_idx as usize];
188 if rgb_dist_sq(curr_pixels[idx], prev_color)
189 <= options.fuzz_threshold / 2
190 {
191 return prev_idx;
192 }
193 }
194 }
195
196 match options.dither {
197 DitherType::BlueNoise => {
198 if let Some((_, pp)) = lab_palette_ref {
199 let (ol, oa, ob) = crate::quant::dither::get_blue_noise_offset(
200 x,
201 y,
202 options.dither_strength,
203 );
204 let p = curr_pixels[idx];
205 let mut lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
206 lab.l = (lab.l + ol).clamp(0.0, 100.0);
207 lab.a = (lab.a + oa).clamp(-128.0, 127.0);
208 lab.b = (lab.b + ob).clamp(-128.0, 127.0);
209 crate::simd::find_nearest_color_lab(lab, pp) as u8
210 } else {
211 crate::simd::find_nearest_color(curr_pixels[idx], options.palette)
212 as u8
213 }
214 }
215 _ => {
216 crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8
217 }
218 }
219 }
220 })
221 })
222 .collect();
223 Some(FrameDelta {
224 x: min_x,
225 y: min_y,
226 width: delta_width,
227 height: delta_height,
228 indices: delta_indices,
229 })
230}