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/// * `options` - Delta compression configuration
52pub fn find_delta_fuzzy(
53    curr_pixels: &[Rgb], 
54    prev_pixels: &[Rgb],
55    options: &DeltaOptions,
56) -> Option<FrameDelta> {
57    if curr_pixels.len() != prev_pixels.len() {
58        return None;
59    }
60    
61    let mut min_x = options.width;
62    let mut max_x = 0;
63    let mut min_y = options.height;
64    let mut max_y = 0;
65    let mut changed = false;
66
67    // 1. Find bounding box using fuzzy equality
68    for y in 0..options.height {
69        for x in 0..options.width {
70            let idx = (y as usize * options.width as usize) + x as usize;
71            if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) > options.fuzz_threshold {
72                if x < min_x {
73                    min_x = x;
74                }
75                if x > max_x {
76                    max_x = x;
77                }
78                if y < min_y {
79                    min_y = y;
80                }
81                if y > max_y {
82                    max_y = y;
83                }
84                changed = true;
85            }
86        }
87    }
88
89    if !changed {
90        return None;
91    }
92
93    let delta_width = max_x - min_x + 1;
94    let delta_height = max_y - min_y + 1;
95
96    // 2. Map pixels to indices
97    
98    // Prepare Blue Noise if needed
99    let lab_palette = if options.dither == DitherType::BlueNoise {
100        let lp: Vec<crate::color::Lab> = options.palette
101            .iter()
102            .map(|p| crate::color::rgb_to_lab(p.r, p.g, p.b))
103            .collect();
104        let pp = crate::simd::PlanarLabPalette::from_lab(&lp);
105        Some((lp, pp))
106    } else {
107        None
108    };
109
110    #[cfg(feature = "rayon")]
111    use rayon::prelude::*;
112
113    #[cfg(feature = "rayon")]
114    let delta_indices: Vec<u8> = (min_y..=max_y)
115        .into_par_iter()
116        .flat_map(|y| {
117            let lab_palette_ref = lab_palette.as_ref();
118            (min_x..=max_x).into_par_iter().map(move |x| {
119                let idx = (y as usize * options.width as usize) + x as usize;
120                // If it's close enough to the previous pixel, make it transparent
121                if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) <= options.fuzz_threshold {
122                    options.transparent_idx
123                } else {
124                    match options.dither {
125                        DitherType::BlueNoise => {
126                            if let Some((_, pp)) = lab_palette_ref {
127                                let (ol, oa, ob) =
128                                    crate::quant::dither::get_blue_noise_offset(x, y);
129                                let p = curr_pixels[idx];
130                                let mut lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
131                                lab.l = (lab.l + ol).clamp(0.0, 100.0);
132                                lab.a = (lab.a + oa).clamp(-128.0, 127.0);
133                                lab.b = (lab.b + ob).clamp(-128.0, 127.0);
134                                crate::simd::find_nearest_color_lab(lab, pp) as u8
135                            } else {
136                                crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8
137                            }
138                        }
139                        _ => crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8,
140                    }
141                }
142            })
143        })
144        .collect();
145
146    #[cfg(not(feature = "rayon"))]
147    let delta_indices: Vec<u8> = (min_y..=max_y)
148        .flat_map(|y| {
149            let lab_palette_ref = lab_palette.as_ref();
150            (min_x..=max_x).map(move |x| {
151                let idx = (y as usize * options.width as usize) + x as usize;
152                if rgb_dist_sq(curr_pixels[idx], prev_pixels[idx]) <= options.fuzz_threshold {
153                    options.transparent_idx
154                } else {
155                    match options.dither {
156                        DitherType::BlueNoise => {
157                            if let Some((_, pp)) = lab_palette_ref {
158                                let (ol, oa, ob) =
159                                    crate::quant::dither::get_blue_noise_offset(x, y);
160                                let p = curr_pixels[idx];
161                                let mut lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
162                                lab.l = (lab.l + ol).clamp(0.0, 100.0);
163                                lab.a = (lab.a + oa).clamp(-128.0, 127.0);
164                                lab.b = (lab.b + ob).clamp(-128.0, 127.0);
165                                crate::simd::find_nearest_color_lab(lab, pp) as u8
166                            } else {
167                                crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8
168                            }
169                        }
170                        _ => crate::simd::find_nearest_color(curr_pixels[idx], options.palette) as u8,
171                    }
172                }
173            })
174        })
175        .collect();
176    Some(FrameDelta {
177        x: min_x,
178        y: min_y,
179        width: delta_width,
180        height: delta_height,
181        indices: delta_indices,
182    })
183}