tiny_gradient/
gradient.rs

1//! A module contains [Gradient] generator.
2
3use libm::powf;
4
5use crate::rgb::RGB;
6
7/// Gradient generator.
8/// 
9/// It implements an [Iterator] interface.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Gradient {
12    from: RGB,
13    to: RGB,
14    steps: usize,
15}
16
17impl Gradient {
18    /// Creates [Gradient] generator from one color to another in N steps.
19    pub fn new(from: RGB, to: RGB, steps: usize) -> Self {
20        Self { from, to, steps }
21    }
22}
23
24impl IntoIterator for Gradient {
25    type Item = RGB;
26    type IntoIter = GradientIter;
27
28    fn into_iter(self) -> Self::IntoIter {
29        GradientIter {
30            gradient: self,
31            i: 0,
32        }
33    }
34}
35
36/// [Gradient] iterator which yields [RGB].
37pub struct GradientIter {
38    gradient: Gradient,
39    i: usize,
40}
41
42impl Iterator for GradientIter {
43    type Item = RGB;
44
45    fn next(&mut self) -> Option<Self::Item> {
46        if self.i == self.gradient.steps {
47            return None;
48        }
49
50        let mut mix = self.i as f32 / (self.gradient.steps - 1) as f32;
51        if mix.is_nan() {
52            mix = 0.0;
53        }
54
55        self.i += 1;
56
57        let color = mix_color(self.gradient.from, self.gradient.to, mix);
58
59        Some(color)
60    }
61}
62
63// Mix [0..1]
64//      0   --> all c1
65//      0.5 --> equal mix of c1 and c2
66//      1   --> all c2
67fn mix_color(c1: RGB, c2: RGB, mix: f32) -> RGB {
68    //Convert color from 0..255 to 0..1
69    let c1 = normalize_rgb(c1);
70    let c2 = normalize_rgb(c2);
71
72    //Invert sRGB gamma compression
73    let c1 = srgb_inverse_companding(c1);
74    let c2 = srgb_inverse_companding(c2);
75
76    let mut c = rgb_linear_interpolation(c1, c2, mix);
77
78    //Apply adjustment factor to each rgb value based
79    if c.r + c.g + c.b != 0.0 {
80        //Compute a measure of brightness of the two colors using empirically determined gamma
81        let gamma = 0.43;
82        let c1_bright = rgb_brightness(c1, gamma);
83        let c2_bright = rgb_brightness(c2, gamma);
84
85        //Interpolate a new brightness value, and convert back to linear light
86        let brightness = linear_interpolation(c1_bright, c2_bright, mix);
87        let intensity = powf(brightness, 1.0 / gamma);
88
89        let factor = intensity / (c.r + c.g + c.b);
90
91        c = RGB {
92            r: c.r * factor,
93            g: c.g * factor,
94            b: c.b * factor,
95        };
96    }
97
98    //Reapply sRGB gamma compression
99    let c = srgb_companding(c);
100
101    normalize_back_rgb(c)
102}
103
104//Inverse Red, Green, and Blue
105fn srgb_inverse_companding(c: RGB<f32>) -> RGB<f32> {
106    RGB {
107        r: srgb_inverse_color(c.r),
108        b: srgb_inverse_color(c.b),
109        g: srgb_inverse_color(c.g),
110    }
111}
112
113fn srgb_inverse_color(c: f32) -> f32 {
114    if c > 0.04045 {
115        powf((c + 0.055) / 1.055, 2.4)
116    } else {
117        c / 12.92
118    }
119}
120
121fn normalize_rgb(c: RGB) -> RGB<f32> {
122    RGB {
123        r: normalize_color(c.r),
124        g: normalize_color(c.g),
125        b: normalize_color(c.b),
126    }
127}
128
129fn normalize_back_rgb(c: RGB<f32>) -> RGB {
130    RGB {
131        r: (c.r * 255.9999) as u8,
132        g: (c.g * 255.9999) as u8,
133        b: (c.b * 255.9999) as u8,
134    }
135}
136
137fn normalize_color(c: u8) -> f32 {
138    c as f32 / 255.0
139}
140
141fn rgb_linear_interpolation(c1: RGB<f32>, c2: RGB<f32>, mix: f32) -> RGB<f32> {
142    RGB {
143        r: linear_interpolation(c1.r, c2.r, mix),
144        g: linear_interpolation(c1.g, c2.g, mix),
145        b: linear_interpolation(c1.b, c2.b, mix),
146    }
147}
148
149fn linear_interpolation(c1: f32, c2: f32, frac: f32) -> f32 {
150    (c1 * (1.0 - frac)) + (c2 * frac)
151}
152
153fn srgb_companding(c: RGB<f32>) -> RGB<f32> {
154    RGB {
155        r: srgb_apply_companding_color(c.r),
156        g: srgb_apply_companding_color(c.g),
157        b: srgb_apply_companding_color(c.b),
158    }
159}
160
161fn srgb_apply_companding_color(c: f32) -> f32 {
162    if c > 0.0031308 {
163        1.055 * powf(c, 1.0 / 2.4) - 0.055
164    } else {
165        c * 12.92
166    }
167}
168
169fn rgb_brightness(c: RGB<f32>, gamma: f32) -> f32 {
170    powf(c.r + c.g + c.b, gamma)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::{mix_color, Gradient, RGB};
176
177    #[test]
178    fn mix_color_test() {
179        assert_eq!(
180            mix_color(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 0.50),
181            RGB::new(123, 123, 123),
182        );
183        assert_eq!(
184            mix_color(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 0.25),
185            RGB::new(56, 56, 56),
186        );
187        assert_eq!(
188            mix_color(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 0.1),
189            RGB::new(14, 14, 14),
190        );
191        assert_eq!(
192            mix_color(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 1.0),
193            RGB::new(255, 255, 255),
194        );
195        assert_eq!(
196            mix_color(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 0.0),
197            RGB::new(0, 0, 0),
198        );
199    }
200
201    #[test]
202    fn gradient_test() {
203        test_gradient(
204            Gradient::new(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 0).into_iter(),
205            &[],
206        );
207        test_gradient(
208            Gradient::new(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 1).into_iter(),
209            &[RGB::new(0, 0, 0)],
210        );
211        test_gradient(
212            Gradient::new(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 2).into_iter(),
213            &[RGB::new(0, 0, 0), RGB::new(255, 255, 255)],
214        );
215        test_gradient(
216            Gradient::new(RGB::new(0, 0, 0), RGB::new(255, 255, 255), 3).into_iter(),
217            &[
218                RGB::new(0, 0, 0),
219                RGB::new(123, 123, 123),
220                RGB::new(255, 255, 255),
221            ],
222        );
223    }
224
225    fn test_gradient(mut iter: impl Iterator<Item = RGB>, expected: &[RGB]) {
226        for rgb in expected {
227            let got = iter.next().unwrap();
228            assert_eq!(got, *rgb);
229        }
230
231        assert!(iter.next().is_none());
232    }
233}