Skip to main content

noise/utils/
color_gradient.rs

1use alloc::vec::Vec;
2
3#[cfg(feature = "libm")]
4use num_traits::MulAdd;
5
6pub type Color = [u8; 4];
7
8#[derive(Clone, Copy, Debug, Default)]
9struct GradientPoint {
10    pos: f64,
11    color: Color,
12}
13
14#[derive(Clone, Copy, Debug, Default)]
15struct GradientDomain {
16    min: f64,
17    max: f64,
18}
19
20impl GradientDomain {
21    pub fn new(min: f64, max: f64) -> Self {
22        Self { min, max }
23    }
24
25    pub fn set_min(&mut self, min: f64) {
26        self.min = min;
27    }
28
29    pub fn set_max(&mut self, max: f64) {
30        self.max = max;
31    }
32}
33
34#[derive(Clone, Debug, Default)]
35pub struct ColorGradient {
36    gradient_points: Vec<GradientPoint>,
37    domain: GradientDomain,
38}
39
40impl ColorGradient {
41    pub fn new() -> Self {
42        let gradient = Self {
43            gradient_points: Vec::new(),
44            domain: GradientDomain::new(0.0, 1.0),
45        };
46
47        gradient.build_grayscale_gradient()
48    }
49
50    pub fn add_gradient_point(mut self, pos: f64, color: Color) -> Self {
51        let new_point = GradientPoint { pos, color };
52
53        // first check to see if the position is within the domain of the gradient. if the position
54        // is not within the domain, expand the domain and add the GradientPoint
55        if self.domain.min > pos {
56            self.domain.set_min(pos);
57            // since the new position is the smallest value, insert it at the beginning of the
58            // gradient
59            self.gradient_points.insert(0, new_point);
60        } else if self.domain.max < pos {
61            self.domain.set_max(pos);
62
63            // since the new position is at the largest value, insert it at the end of the gradient
64            self.gradient_points.push(new_point)
65        } else if !self // new point must be somewhere inside the existing domain. Check to see if
66            // it doesn't exist already
67            .gradient_points
68            .iter()
69            .any(|&x| (x.pos - pos).abs() < f64::EPSILON)
70        {
71            // it doesn't, so find the correct position to insert the new
72            // control point.
73            let insertion_point = self.find_insertion_point(pos);
74
75            // add the new control point at the correct position.
76            self.gradient_points.insert(insertion_point, new_point);
77        }
78
79        self
80    }
81
82    fn find_insertion_point(&self, pos: f64) -> usize {
83        self.gradient_points
84            .iter()
85            .position(|x| x.pos >= pos)
86            .unwrap_or(self.gradient_points.len())
87    }
88
89    pub fn clear_gradient(mut self) -> Self {
90        self.gradient_points.clear();
91        self.domain = GradientDomain::new(0.0, 0.0);
92
93        self
94    }
95
96    pub fn build_grayscale_gradient(self) -> Self {
97        self.clear_gradient()
98            .add_gradient_point(-1.0, [0, 0, 0, 255])
99            .add_gradient_point(1.0, [255, 255, 255, 255])
100    }
101
102    #[rustfmt::skip]
103    pub fn build_terrain_gradient(self) -> Self {
104        self.clear_gradient()
105            .add_gradient_point(-1.00,              [  0,   0,   0, 255])
106            .add_gradient_point(-256.0 / 16384.0,   [  6,  58, 127, 255])
107            .add_gradient_point(-1.0 / 16384.0,     [ 14, 112, 192, 255])
108            .add_gradient_point(0.0,                [ 70, 120,  60, 255])
109            .add_gradient_point(1024.0 / 16384.0,   [110, 140,  75, 255])
110            .add_gradient_point(2048.0 / 16384.0,   [160, 140, 111, 255])
111            .add_gradient_point(3072.0 / 16384.0,   [184, 163, 141, 255])
112            .add_gradient_point(4096.0 / 16384.0,   [128, 128, 128, 255])
113            .add_gradient_point(5632.0 / 16384.0,   [128, 128, 128, 255])
114            .add_gradient_point(6144.0 / 16384.0,   [250, 250, 250, 255])
115            .add_gradient_point(1.0,                [255, 255, 255, 255])
116    }
117
118    #[rustfmt::skip]
119    pub fn build_rainbow_gradient(self) -> Self {
120        self.clear_gradient()
121            .add_gradient_point(-1.0, [255,   0,   0, 255])
122            .add_gradient_point(-0.7, [255, 255,   0, 255])
123            .add_gradient_point(-0.4, [  0, 255,   0, 255])
124            .add_gradient_point( 0.0, [  0, 255, 255, 255])
125            .add_gradient_point( 0.3, [  0,   0, 255, 255])
126            .add_gradient_point( 0.6, [255,   0, 255, 255])
127            .add_gradient_point( 1.0, [255,   0,   0, 255])
128    }
129
130    pub fn get_color(&self, pos: f64) -> Color {
131        let mut color = Color::default();
132
133        // If there are no colors in the gradient, return black
134        if !self.gradient_points.is_empty() {
135            match () {
136                _ if pos < self.domain.min => color = self.gradient_points.first().unwrap().color,
137                _ if pos > self.domain.max => color = self.gradient_points.last().unwrap().color,
138                _ => {
139                    for points in self.gradient_points.windows(2) {
140                        if (points[0].pos <= pos) && (points[1].pos > pos) {
141                            // Compute the alpha value used for linear interpolation
142                            let alpha = (pos - points[0].pos) / (points[1].pos - points[0].pos);
143
144                            // Now perform the interpolation and return.
145                            color = interpolate_color(points[0].color, points[1].color, alpha)
146                        }
147                    }
148                }
149            };
150        };
151
152        color
153    }
154}
155
156fn interpolate_color(color0: Color, color1: Color, alpha: f64) -> Color {
157    fn blend_channel(channel0: u8, channel1: u8, alpha: f64) -> u8 {
158        let c0 = (f64::from(channel0)) / 255.0;
159        let c1 = (f64::from(channel1)) / 255.0;
160
161        ((c1 - c0).mul_add(alpha, c0) * 255.0) as u8
162    }
163
164    let mut color = Color::default();
165
166    for i in 0..color.len() {
167        color[i] = blend_channel(color0[i], color1[i], alpha);
168    }
169
170    color
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn linerp_color_1() {
179        assert_eq!(
180            [0, 127, 255, 0],
181            interpolate_color([0, 0, 255, 0], [0, 255, 255, 0], 0.5)
182        );
183    }
184
185    #[test]
186    fn color_gradient_1() {
187        let gradient = ColorGradient::new();
188
189        let gradient = gradient
190            .clear_gradient()
191            .add_gradient_point(0.0, [0, 0, 0, 0])
192            .add_gradient_point(1.0, [255, 255, 255, 255]);
193
194        assert_eq!([127, 127, 127, 127], gradient.get_color(0.5));
195    }
196}