Skip to main content

agg_rust/
gradient_lut.rs

1//! Gradient color lookup table.
2//!
3//! Port of `agg_gradient_lut.h` — builds a LUT (lookup table) from SVG-style
4//! color stops. Used by `SpanGradient` to map gradient distances to colors.
5
6use crate::basics::uround;
7use crate::color::Rgba8;
8use crate::dda_line::DdaLineInterpolator;
9
10// ============================================================================
11// ColorFunction trait
12// ============================================================================
13
14/// Trait for color lookup functions used by span_gradient.
15///
16/// Provides indexed access to a color palette of known size.
17pub trait ColorFunction {
18    type Color;
19
20    fn size(&self) -> usize;
21    fn get(&self, index: usize) -> Self::Color;
22}
23
24// ============================================================================
25// ColorInterpolator — generic version using gradient() method
26// ============================================================================
27
28/// Generic color interpolator using the color type's `gradient()` method.
29///
30/// Port of C++ `color_interpolator<ColorT>` (generic template).
31struct ColorInterpolatorGeneric<C> {
32    c1: C,
33    c2: C,
34    len: u32,
35    count: u32,
36}
37
38impl<C: Clone> ColorInterpolatorGeneric<C> {
39    fn new(c1: &C, c2: &C, len: u32) -> Self {
40        Self {
41            c1: c1.clone(),
42            c2: c2.clone(),
43            len,
44            count: 0,
45        }
46    }
47
48    fn inc(&mut self) {
49        self.count += 1;
50    }
51}
52
53impl ColorInterpolatorGeneric<Rgba8> {
54    fn color(&self) -> Rgba8 {
55        self.c1
56            .gradient(&self.c2, self.count as f64 / self.len as f64)
57    }
58}
59
60// ============================================================================
61// ColorInterpolatorRgba8 — fast DDA specialization for Rgba8
62// ============================================================================
63
64/// Fast RGBA8 color interpolator using 14-bit DDA interpolation.
65///
66/// Port of C++ `color_interpolator<rgba8>` specialization.
67struct ColorInterpolatorRgba8 {
68    r: DdaLineInterpolator<14, 0>,
69    g: DdaLineInterpolator<14, 0>,
70    b: DdaLineInterpolator<14, 0>,
71    a: DdaLineInterpolator<14, 0>,
72}
73
74impl ColorInterpolatorRgba8 {
75    fn new(c1: &Rgba8, c2: &Rgba8, len: u32) -> Self {
76        Self {
77            r: DdaLineInterpolator::new(c1.r as i32, c2.r as i32, len),
78            g: DdaLineInterpolator::new(c1.g as i32, c2.g as i32, len),
79            b: DdaLineInterpolator::new(c1.b as i32, c2.b as i32, len),
80            a: DdaLineInterpolator::new(c1.a as i32, c2.a as i32, len),
81        }
82    }
83
84    fn inc(&mut self) {
85        self.r.inc();
86        self.g.inc();
87        self.b.inc();
88        self.a.inc();
89    }
90
91    fn color(&self) -> Rgba8 {
92        Rgba8::new(
93            self.r.y() as u32,
94            self.g.y() as u32,
95            self.b.y() as u32,
96            self.a.y() as u32,
97        )
98    }
99}
100
101// ============================================================================
102// GradientLut
103// ============================================================================
104
105/// Color stop for gradient definition.
106#[derive(Clone)]
107struct ColorPoint {
108    offset: f64,
109    color: Rgba8,
110}
111
112impl ColorPoint {
113    fn new(offset: f64, color: Rgba8) -> Self {
114        Self {
115            offset: offset.clamp(0.0, 1.0),
116            color,
117        }
118    }
119}
120
121/// Gradient color lookup table.
122///
123/// Builds a 256-entry (or custom size) color LUT from SVG-style color stops.
124/// Supports arbitrary numbers of stops at positions [0..1].
125///
126/// Port of C++ `gradient_lut<ColorInterpolator, ColorLutSize>`.
127pub struct GradientLut {
128    color_profile: Vec<ColorPoint>,
129    color_lut: Vec<Rgba8>,
130    lut_size: usize,
131    use_fast_interpolator: bool,
132}
133
134impl GradientLut {
135    /// Create a new gradient LUT with the specified size (default 256).
136    pub fn new(lut_size: usize) -> Self {
137        Self {
138            color_profile: Vec::new(),
139            color_lut: vec![Rgba8::default(); lut_size],
140            lut_size,
141            use_fast_interpolator: true,
142        }
143    }
144
145    /// Create a new gradient LUT with default size of 256.
146    pub fn new_default() -> Self {
147        Self::new(256)
148    }
149
150    /// Set whether to use the fast DDA interpolator (default: true).
151    pub fn set_use_fast_interpolator(&mut self, fast: bool) {
152        self.use_fast_interpolator = fast;
153    }
154
155    /// Remove all color stops.
156    pub fn remove_all(&mut self) {
157        self.color_profile.clear();
158    }
159
160    /// Add a color stop at the given offset (clamped to [0..1]).
161    pub fn add_color(&mut self, offset: f64, color: Rgba8) {
162        self.color_profile.push(ColorPoint::new(offset, color));
163    }
164
165    /// Build the lookup table by interpolating between color stops.
166    ///
167    /// Must have at least 2 color stops. Stops are sorted by offset
168    /// and duplicates are removed.
169    pub fn build_lut(&mut self) {
170        // Sort by offset
171        self.color_profile
172            .sort_by(|a, b| a.offset.partial_cmp(&b.offset).unwrap());
173        // Remove duplicates (same offset)
174        self.color_profile
175            .dedup_by(|a, b| (a.offset - b.offset).abs() < 1e-10);
176
177        if self.color_profile.len() < 2 {
178            return;
179        }
180
181        let size = self.lut_size;
182        let mut start = uround(self.color_profile[0].offset * size as f64) as usize;
183
184        // Fill before first stop with first color
185        let c = self.color_profile[0].color;
186        for i in 0..start.min(size) {
187            self.color_lut[i] = c;
188        }
189
190        // Interpolate between stops
191        for i in 1..self.color_profile.len() {
192            let end = uround(self.color_profile[i].offset * size as f64) as usize;
193            let seg_len = if end > start { end - start + 1 } else { 1 };
194
195            if self.use_fast_interpolator {
196                let mut ci = ColorInterpolatorRgba8::new(
197                    &self.color_profile[i - 1].color,
198                    &self.color_profile[i].color,
199                    seg_len as u32,
200                );
201                while start < end && start < size {
202                    self.color_lut[start] = ci.color();
203                    ci.inc();
204                    start += 1;
205                }
206            } else {
207                let mut ci = ColorInterpolatorGeneric::new(
208                    &self.color_profile[i - 1].color,
209                    &self.color_profile[i].color,
210                    seg_len as u32,
211                );
212                while start < end && start < size {
213                    self.color_lut[start] = ci.color();
214                    ci.inc();
215                    start += 1;
216                }
217            }
218        }
219
220        // Fill after last stop with last color
221        let c = self.color_profile.last().unwrap().color;
222        let mut end = start;
223        while end < size {
224            self.color_lut[end] = c;
225            end += 1;
226        }
227    }
228}
229
230impl ColorFunction for GradientLut {
231    type Color = Rgba8;
232
233    fn size(&self) -> usize {
234        self.lut_size
235    }
236
237    fn get(&self, index: usize) -> Rgba8 {
238        self.color_lut[index]
239    }
240}
241
242// ============================================================================
243// GradientLinearColor — simple 2-color linear interpolation
244// ============================================================================
245
246/// Simple 2-color linear gradient color function.
247///
248/// Interpolates between two colors based on index/size ratio.
249///
250/// Port of C++ `gradient_linear_color<ColorT>`.
251pub struct GradientLinearColor {
252    c1: Rgba8,
253    c2: Rgba8,
254    size: usize,
255}
256
257impl GradientLinearColor {
258    pub fn new(c1: Rgba8, c2: Rgba8, size: usize) -> Self {
259        Self { c1, c2, size }
260    }
261
262    pub fn colors(&mut self, c1: Rgba8, c2: Rgba8) {
263        self.c1 = c1;
264        self.c2 = c2;
265    }
266}
267
268impl ColorFunction for GradientLinearColor {
269    type Color = Rgba8;
270
271    fn size(&self) -> usize {
272        self.size
273    }
274
275    fn get(&self, index: usize) -> Rgba8 {
276        self.c1
277            .gradient(&self.c2, index as f64 / (self.size - 1).max(1) as f64)
278    }
279}
280
281// ============================================================================
282// Tests
283// ============================================================================
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_gradient_lut_new() {
291        let lut = GradientLut::new_default();
292        assert_eq!(lut.size(), 256);
293    }
294
295    #[test]
296    fn test_gradient_lut_two_stops() {
297        let mut lut = GradientLut::new_default();
298        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
299        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
300        lut.build_lut();
301
302        // First should be red
303        let c0 = lut.get(0);
304        assert_eq!(c0.r, 255);
305        assert_eq!(c0.b, 0);
306
307        // Last should be nearly blue (DDA with seg_len=257 over 256 entries
308        // doesn't quite reach the endpoint — matches C++ AGG behavior)
309        let c255 = lut.get(255);
310        assert!(c255.r <= 3, "c255.r={}", c255.r);
311        assert!(c255.b >= 252, "c255.b={}", c255.b);
312
313        // Middle should be roughly equal
314        let c128 = lut.get(128);
315        assert!(c128.r > 50 && c128.r < 200, "Mid r={}", c128.r);
316        assert!(c128.b > 50 && c128.b < 200, "Mid b={}", c128.b);
317    }
318
319    #[test]
320    fn test_gradient_lut_three_stops() {
321        let mut lut = GradientLut::new_default();
322        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
323        lut.add_color(0.5, Rgba8::new(0, 255, 0, 255));
324        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
325        lut.build_lut();
326
327        // Start: red
328        assert_eq!(lut.get(0).r, 255);
329        // End: nearly blue (multi-segment has ~2% error from DDA)
330        assert!(lut.get(255).b >= 248, "last.b={}", lut.get(255).b);
331        // Middle: should be mostly green
332        let mid = lut.get(128);
333        assert!(mid.g > 128, "Mid green={}", mid.g);
334    }
335
336    #[test]
337    fn test_gradient_lut_remove_all() {
338        let mut lut = GradientLut::new_default();
339        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
340        lut.remove_all();
341        lut.add_color(0.0, Rgba8::new(0, 255, 0, 255));
342        lut.add_color(1.0, Rgba8::new(0, 255, 0, 255));
343        lut.build_lut();
344        assert_eq!(lut.get(0).g, 255);
345    }
346
347    #[test]
348    fn test_gradient_lut_generic_interpolator() {
349        let mut lut = GradientLut::new_default();
350        lut.set_use_fast_interpolator(false);
351        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
352        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
353        lut.build_lut();
354
355        let c0 = lut.get(0);
356        assert_eq!(c0.r, 255);
357        let c255 = lut.get(255);
358        assert!(c255.b >= 252, "c255.b={}", c255.b);
359    }
360
361    #[test]
362    fn test_gradient_lut_custom_size() {
363        let mut lut = GradientLut::new(64);
364        lut.add_color(0.0, Rgba8::new(0, 0, 0, 255));
365        lut.add_color(1.0, Rgba8::new(255, 255, 255, 255));
366        lut.build_lut();
367        assert_eq!(lut.size(), 64);
368        assert_eq!(lut.get(0).r, 0);
369        assert!(lut.get(63).r >= 244, "last.r={}", lut.get(63).r);
370    }
371
372    #[test]
373    fn test_gradient_linear_color() {
374        let gc = GradientLinearColor::new(
375            Rgba8::new(0, 0, 0, 255),
376            Rgba8::new(255, 255, 255, 255),
377            256,
378        );
379        assert_eq!(gc.size(), 256);
380
381        let c0 = gc.get(0);
382        assert_eq!(c0.r, 0);
383
384        let c255 = gc.get(255);
385        assert_eq!(c255.r, 255);
386
387        let c128 = gc.get(128);
388        assert!(c128.r > 100 && c128.r < 160, "c128.r={}", c128.r);
389    }
390
391    #[test]
392    fn test_gradient_lut_unsorted_stops() {
393        let mut lut = GradientLut::new_default();
394        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
395        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
396        lut.build_lut();
397
398        // Should still work — stops are sorted internally
399        assert_eq!(lut.get(0).r, 255);
400        assert!(lut.get(255).b >= 252, "last.b={}", lut.get(255).b);
401    }
402
403    #[test]
404    fn test_color_interpolator_rgba8_fast() {
405        let c1 = Rgba8::new(0, 0, 0, 255);
406        let c2 = Rgba8::new(255, 255, 255, 255);
407        let mut ci = ColorInterpolatorRgba8::new(&c1, &c2, 10);
408
409        let first = ci.color();
410        assert_eq!(first.r, 0);
411
412        for _ in 0..10 {
413            ci.inc();
414        }
415        let last = ci.color();
416        assert_eq!(last.r, 255);
417    }
418
419    #[test]
420    fn test_gradient_linear_color_set_colors() {
421        let mut gc = GradientLinearColor::new(
422            Rgba8::new(0, 0, 0, 255),
423            Rgba8::new(255, 255, 255, 255),
424            256,
425        );
426        gc.colors(Rgba8::new(255, 0, 0, 255), Rgba8::new(0, 255, 0, 255));
427        assert_eq!(gc.get(0).r, 255);
428        assert_eq!(gc.get(0).g, 0);
429        assert_eq!(gc.get(255).r, 0);
430        assert_eq!(gc.get(255).g, 255);
431    }
432}