egui_colorgradient/
gradient.rs

1use egui::epaint::{Color32, Hsva, Rgba};
2use std::cmp::Ordering;
3use std::fmt::{Display, Formatter};
4
5/// The method used for interpolating between two points
6#[derive(Copy, Clone, Debug, PartialEq)]
7pub enum InterpolationMethod {
8    /// Use the nearest value to the left of the sample point. If there is no key point to the left
9    /// of the sample, use the nearest point on the _right_ instead.
10    Constant,
11    /// Linearly interpolate between the two stops to the left and right of the sample. If the sample
12    /// is outside the range of the stops, use the value of the single nearest stop.
13    Linear,
14}
15
16impl Display for InterpolationMethod {
17    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
18        write!(
19            f,
20            "{}",
21            match self {
22                Self::Linear => "linear",
23                Self::Constant => "constant",
24            }
25        )
26    }
27}
28
29/// A ColorInterpolator can arbitrarily sample a gradient.
30pub struct ColorInterpolator {
31    method: InterpolationMethod,
32    keys: Vec<(f32, Rgba)>,
33}
34
35impl ColorInterpolator {
36    fn new(
37        keys: impl IntoIterator<Item = (f32, impl Into<Rgba>)>,
38        method: InterpolationMethod,
39    ) -> Self {
40        let keys: Vec<_> = keys.into_iter().map(|(k, v)| (k, v.into())).collect();
41        let mut result = Self { keys, method };
42        result.sort();
43        result
44    }
45
46    fn sort(&mut self) {
47        self.keys
48            .sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap());
49    }
50
51    /// Find the insertion point for x to maintain order
52    fn bisect(&self, x: f32) -> Option<usize> {
53        let mut lo = 0;
54        let mut hi = self.keys.len();
55        while lo < hi {
56            let mid = (lo + hi) / 2;
57            match self.keys[mid].0.partial_cmp(&x)? {
58                Ordering::Less => lo = mid + 1,
59                Ordering::Equal => lo = mid + 1,
60                Ordering::Greater => hi = mid,
61            }
62        }
63
64        Some(lo)
65    }
66
67    /// Sample the gradient at the given position.
68    ///
69    /// Returns `None` if the gradient is empty.
70    pub fn sample_at(&self, x: f32) -> Option<Rgba> {
71        Some(match self.method {
72            InterpolationMethod::Constant => {
73                let insertion_point = self.bisect(x)?;
74                match insertion_point {
75                    0 => self.keys.first()?.1,
76                    n => self.keys.get(n - 1)?.1,
77                }
78            }
79            InterpolationMethod::Linear => {
80                let insertion_point = self.bisect(x)?;
81                match insertion_point {
82                    0 => self.keys.first()?.1,
83                    n if n == self.keys.len() => self.keys.last()?.1,
84                    n => {
85                        let (t0, c0) = *self.keys.get(n - 1)?;
86                        let (t1, c1) = *self.keys.get(n)?;
87
88                        c0 + (c1 + c0 * -1.0_f32) * ((x - t0) / (t1 - t0))
89                    }
90                }
91            }
92        })
93    }
94}
95
96fn argsort_by<T, F>(data: &[T], mut f: F) -> Vec<usize>
97where
98    F: FnMut(T, T) -> Ordering,
99    T: Copy,
100{
101    let mut indices = (0..data.len()).collect::<Vec<_>>();
102    indices.sort_by(|&a, &b| f(data[a], data[b]));
103    indices
104}
105
106/// A color gradient, that will be interpolated between a number of fixed points, a.k.a. _stops_.
107pub struct Gradient {
108    pub stops: Vec<(f32, Hsva)>,
109    pub interpolation_method: InterpolationMethod,
110}
111
112impl Gradient {
113    /// Create a new gradient from an iterator over key colors.
114    pub fn new(
115        interpolation_method: InterpolationMethod,
116        stops: impl IntoIterator<Item = (f32, impl Into<Hsva>)>,
117    ) -> Self {
118        Self {
119            interpolation_method,
120            stops: stops.into_iter().map(|(k, v)| (k, v.into())).collect(),
121        }
122    }
123
124    /// Create a [ColorInterpolator] to evaluate the gradient at any point.
125    pub fn interpolator(&self) -> ColorInterpolator {
126        ColorInterpolator::new(self.stops.iter().copied(), self.interpolation_method)
127    }
128
129    /// Create a [ColorInterpolator] that discards the alpha component of the color gradient and
130    /// always produces an opaque color.
131    pub fn interpolator_opaque(&self) -> ColorInterpolator {
132        ColorInterpolator::new(
133            self.stops.iter().map(|(t, c)| (*t, c.to_opaque())),
134            self.interpolation_method,
135        )
136    }
137
138    /// Produce a list of the indices of the gradient's stops that would place them in order.
139    ///
140    /// Use this to prepare for the upcoming reordering of the stops by [sort()](Gradient::sort).
141    pub fn argsort(&self) -> Vec<usize> {
142        argsort_by(&self.stops, |(a, _), (b, _)| a.partial_cmp(&b).unwrap())
143    }
144
145    /// Sort the gradient's stops by ascending position.
146    pub fn sort(&mut self) {
147        self.stops
148            .sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap())
149    }
150
151    /// Return a vector of the gradient's color sampled on linearly spaced points between 0 and 1.
152    ///
153    /// The first and last samples correspond to the gradient's value at 0.0 and 1.0, respectively.
154    ///
155    /// This is useful for generating a texture.
156    ///
157    /// # Panics
158    ///
159    /// Will panic if the provided size `n` is smaller or equal to 1, or if the gradient is empty.
160    pub fn linear_eval(&self, n: usize, opaque: bool) -> Vec<Color32> {
161        let interp = match opaque {
162            false => self.interpolator(),
163            true => self.interpolator_opaque(),
164        };
165        (0..n)
166            .map(|idx| (idx as f32) / (n - 1) as f32)
167            .map(|t| interp.sample_at(t).unwrap().into())
168            .collect()
169    }
170}
171
172impl Default for Gradient {
173    fn default() -> Self {
174        Self {
175            stops: vec![(0., Color32::BLACK.into()), (1., Color32::WHITE.into())],
176            interpolation_method: InterpolationMethod::Linear,
177        }
178    }
179}