pixie_anim_lib/
delta.rs

1//! Inter-frame Delta Compression.
2
3use crate::quant::{DitherType, Rgb};
4
5/// Represents the difference between two consecutive frames.
6#[derive(Debug, Default)]
7pub struct FrameDelta {
8    /// X coordinate of the delta bounding box.
9    pub x: u16,
10    /// Y coordinate of the delta bounding box.
11    pub y: u16,
12    /// Width of the delta bounding box.
13    pub width: u16,
14    /// Height of the delta bounding box.
15    pub height: u16,
16    /// Palette indices for the pixels within the bounding box.
17    pub indices: Vec<u8>,
18}
19
20/// Fast RGB distance for fuzzy delta matching
21#[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
29/// Options for delta compression.
30pub struct DeltaOptions<'a> {
31    /// Frame width
32    pub width: u16,
33    /// Frame height
34    pub height: u16,
35    /// The global color palette
36    pub palette: &'a [Rgb],
37    /// The index reserved for transparency
38    pub transparent_idx: u8,
39    /// Squared distance threshold for "fuzzy" equality
40    pub fuzz_threshold: u32,
41    /// The dithering algorithm to apply to changed pixels
42    pub dither: DitherType,
43}
44
45/// Finds the smallest bounding box and maps pixels to transparent if they are "close enough"
46/// to the previous frame's color.
47/// 
48/// # Arguments
49/// * `curr_pixels` - Pixels of the current frame
50/// * `prev_pixels` - Pixels of the previous frame
51/// * `prev_indices` - Palette indices used in the previous frame at these coordinates
52/// * `options` - Delta compression configuration
53pub fn find_delta_fuzzy(
54    curr_pixels: &[Rgb], 
55    prev_pixels: &[Rgb],
56    prev_indices: Option<&[u8]>,
57    options: &DeltaOptions,
58) -> Option<FrameDelta> {
59    if curr_pixels.len() != prev_pixels.len() {
60        return None;
61    }
62    
63    let mut min_x = options.width;
64    let mut max_x = 0;
65    let mut min_y = options.height;
66    let mut max_y = 0;
67    let mut changed = false;
68
69    // 1. Find bounding box using fuzzy equality
70    for y in 0..options.height {
71        for x in 0..options.width {
72            let idx = (y as usize * options.width as usize) + x as usize;
73            if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) > options.fuzz_threshold {
74                if x < min_x {
75                    min_x = x;
76                }
77                if x > max_x {
78                    max_x = x;
79                }
80                if y < min_y {
81                    min_y = y;
82                }
83                if y > max_y {
84                    max_y = y;
85                }
86                changed = true;
87            }
88        }
89    }
90
91    if !changed {
92        return None;
93    }
94
95    let delta_width = max_x - min_x + 1;
96    let delta_height = max_y - min_y + 1;
97
98    // 2. Map pixels to indices
99    
100    // Prepare Blue Noise if needed
101    let lab_palette = if options.dither == DitherType::BlueNoise {
102        let lp: Vec<crate::color::Lab> = options.palette
103            .iter()
104            .map(|p| crate::color::rgb_to_lab(p.r, p.g, p.b))
105            .collect();
106        let pp = crate::simd::PlanarLabPalette::from_lab(&lp);
107        Some((lp, pp))
108    } else {
109        None
110    };
111
112    #[cfg(feature = "rayon")]
113    use rayon::prelude::*;
114
115    #[cfg(feature = "rayon")]
116    let delta_indices: Vec<u8> = (min_y..=max_y)
117        .into_par_iter()
118        .flat_map(|y| {
119            let lab_palette_ref = lab_palette.as_ref();
120            (min_x..=max_x).into_par_iter().map(move |x| {
121                let idx = (y as usize * options.width as usize) + x as usize;
122                // If it's close enough to the previous pixel, make it transparent
123                if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) <= options.fuzz_threshold {
124                    options.transparent_idx
125                } else {
126                    // TEMPORAL DENOISING:
127                    // If we used an index in the previous frame, check if that same color 
128                    // is "close enough" to our current pixel. Reusing indices is LZW-friendly.
129                    if let Some(prev_idx_buffer) = prev_indices {
130                        let prev_idx = prev_idx_buffer[idx];
131                        if prev_idx != options.transparent_idx {
132                            let prev_color = options.palette[prev_idx as usize];
133                            // Use a tighter threshold for index-reuse than for transparency
134                            if rgb_dist_sq(curr_pixels[idx], prev_color) <= options.fuzz_threshold / 2 {
135                                return prev_idx;
136                            }
137                        }
138                    }
139
140                    match options.dither {
141                        DitherType::BlueNoise => {
142                            if let Some((_, pp)) = lab_palette_ref {
143                                let (ol, oa, ob) =
144                                    crate::quant::dither::get_blue_noise_offset(x, y);
145                                let p = curr_pixels[idx];
146                                let mut lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
147                                lab.l = (lab.l + ol).clamp(0.0, 100.0);
148                                lab.a = (lab.a + oa).clamp(-128.0, 127.0);
149                                lab.b = (lab.b + ob).clamp(-128.0, 127.0);
150                                crate::simd::find_nearest_color_lab(lab, pp) as u8
151                            } else {
152                                crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8
153                            }
154                        }
155                        _ => crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8,
156                    }
157                }
158            })
159        })
160        .collect();
161
162    #[cfg(not(feature = "rayon"))]
163    let delta_indices: Vec<u8> = (min_y..=max_y)
164        .flat_map(|y| {
165            let lab_palette_ref = lab_palette.as_ref();
166            (min_x..=max_x).map(move |x| {
167                let idx = (y as usize * options.width as usize) + x as usize;
168                if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) <= options.fuzz_threshold {
169                    options.transparent_idx
170                } else {
171                    // TEMPORAL DENOISING:
172                    if let Some(prev_idx_buffer) = prev_indices {
173                        let prev_idx = prev_idx_buffer[idx];
174                        if prev_idx != options.transparent_idx {
175                            let prev_color = options.palette[prev_idx as usize];
176                            if rgb_dist_sq(curr_pixels[idx], prev_color) <= options.fuzz_threshold / 2 {
177                                return prev_idx;
178                            }
179                        }
180                    }
181
182                    match options.dither {
183                        DitherType::BlueNoise => {
184                            if let Some((_, pp)) = lab_palette_ref {
185                                let (ol, oa, ob) =
186                                    crate::quant::dither::get_blue_noise_offset(x, y);
187                                let p = curr_pixels[idx];
188                                let mut lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
189                                lab.l = (lab.l + ol).clamp(0.0, 100.0);
190                                lab.a = (lab.a + oa).clamp(-128.0, 127.0);
191                                lab.b = (lab.b + ob).clamp(-128.0, 127.0);
192                                crate::simd::find_nearest_color_lab(lab, pp) as u8
193                            } else {
194                                crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8
195                            }
196                        }
197                        _ => crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8,
198                    }
199                }
200            })
201        })
202        .collect();
203    Some(FrameDelta {
204        x: min_x,
205        y: min_y,
206        width: delta_width,
207        height: delta_height,
208        indices: delta_indices,
209    })
210}