Skip to main content

phosphor/
gradient.rs

1//! Color gradients and RGB color utilities.
2//!
3//! This module provides color types and gradient functionality for mapping
4//! intensity values to colors in the rendered waveform output.
5
6use cgmath::num_traits::ToPrimitive;
7use std::cmp::Ordering;
8use std::ops::{Add, Mul};
9
10/// RGB color with configurable component type.
11#[derive(Copy, Clone, Debug)]
12pub struct RgbColor<T = f32> {
13    /// Red component
14    pub r: T,
15    /// Green component  
16    pub g: T,
17    /// Blue component
18    pub b: T,
19}
20
21impl<T> RgbColor<T> {
22    /// Creates a new RGB color.
23    ///
24    /// # Arguments
25    ///
26    /// * `r` - Red component
27    /// * `g` - Green component  
28    /// * `b` - Blue component
29    ///
30    /// # Example
31    ///
32    /// ```rust
33    /// use phosphor::gradient::RgbColor;
34    ///
35    /// let red = RgbColor::new(1.0, 0.0, 0.0);
36    /// let purple: RgbColor<u8> = RgbColor::new(128, 0, 128);
37    /// ```
38    pub const fn new(r: T, g: T, b: T) -> Self {
39        Self { r, g, b }
40    }
41}
42
43impl Mul<f32> for RgbColor<f32> {
44    type Output = RgbColor<f32>;
45    fn mul(self, rhs: f32) -> Self::Output {
46        RgbColor::new(self.r * rhs, self.g * rhs, self.b * rhs)
47    }
48}
49
50#[cfg(feature = "egui")]
51impl From<egui::Rgba> for RgbColor {
52    fn from(value: egui::Rgba) -> Self {
53        RgbColor {
54            r: value.r(),
55            g: value.g(),
56            b: value.b(),
57        }
58    }
59}
60
61#[cfg(feature = "egui")]
62impl From<RgbColor> for egui::Rgba {
63    fn from(value: RgbColor) -> egui::Rgba {
64        egui::Rgba::from_rgb(value.r, value.g, value.b)
65    }
66}
67
68#[cfg(feature = "egui")]
69impl From<RgbColor> for egui::ecolor::Hsva {
70    fn from(value: RgbColor) -> egui::ecolor::Hsva {
71        Into::<egui::Rgba>::into(value).into()
72    }
73}
74
75#[cfg(feature = "egui")]
76impl From<egui::ecolor::Hsva> for RgbColor {
77    fn from(value: egui::ecolor::Hsva) -> Self {
78        Into::<egui::Rgba>::into(value).into()
79    }
80}
81
82impl Add<RgbColor<f32>> for RgbColor<f32> {
83    type Output = RgbColor<f32>;
84    fn add(self, rhs: RgbColor<f32>) -> Self::Output {
85        RgbColor::new(self.r + rhs.r, self.g + rhs.g, self.b + rhs.b)
86    }
87}
88
89impl RgbColor {
90    pub const BLACK: RgbColor = RgbColor::new(0., 0., 0.);
91    pub const WHITE: RgbColor = RgbColor::new(1., 1., 1.);
92    pub const RED: RgbColor = RgbColor::new(1., 0., 0.);
93    pub const GREEN: RgbColor = RgbColor::new(0., 1., 0.);
94    pub const BLUE: RgbColor = RgbColor::new(0., 0., 1.);
95}
96
97impl From<RgbColor<f32>> for RgbColor<u8> {
98    fn from(value: RgbColor<f32>) -> Self {
99        RgbColor {
100            r: (value.r * 255.).round().clamp(0., 255.).to_u8().unwrap(),
101            g: (value.g * 255.).round().clamp(0., 255.).to_u8().unwrap(),
102            b: (value.b * 255.).round().clamp(0., 255.).to_u8().unwrap(),
103        }
104    }
105}
106
107impl From<[f32; 3]> for RgbColor {
108    fn from([r, g, b]: [f32; 3]) -> Self {
109        RgbColor::new(r, g, b)
110    }
111}
112
113/// A color gradient defined by interpolation between color stops.
114///
115/// Gradients map normalized intensity values (0.0 to 1.0) to colors through linear
116/// interpolation between user-defined color stops. This is used to create the Look-Up
117/// Table (LUT) texture for intensity-to-color mapping in the renderer.
118///
119/// # Example
120///
121/// ```rust
122/// use phosphor::gradient::{Gradient, RgbColor};
123///
124/// // Create classic green oscilloscope gradient
125/// let gradient = Gradient::new(vec![
126///     (0.0, RgbColor::new(0.0, 0.0, 0.0)),    // Black background
127///     (0.3, RgbColor::new(0.0, 0.2, 0.0)),    // Dark green
128///     (0.7, RgbColor::new(0.0, 0.8, 0.0)),    // Bright green
129///     (1.0, RgbColor::new(0.8, 1.0, 0.8)),    // Saturated green
130/// ]);
131/// ```
132#[derive(Debug)]
133pub struct Gradient {
134    stops: Vec<(f32, RgbColor)>,
135}
136
137impl Gradient {
138    /// Creates a new gradient from color stops.
139    ///
140    /// The stops define (position, color) pairs.
141    /// The gradient will interpolate linearly between these stops. Stops are automatically
142    /// sorted by position.
143    ///
144    /// # Arguments
145    ///
146    /// * `stops` - Iterator of (position, color) pairs
147    ///
148    /// # Example
149    ///
150    /// ```rust
151    /// use phosphor::gradient::{Gradient, RgbColor};
152    ///
153    /// let gradient = Gradient::new([
154    ///     (0.0, RgbColor::new(0.0, 0.0, 0.0)),  // Arrays convert to RgbColor
155    ///     (0.5, RgbColor::new(0.5, 0.0, 0.5)),
156    ///     (1.0, RgbColor::WHITE),
157    /// ]);
158    /// ```
159    pub fn new(stops: impl IntoIterator<Item = (f32, impl Into<RgbColor>)>) -> Self {
160        let mut gradient = Gradient {
161            stops: stops.into_iter().map(|(k, v)| (k, v.into())).collect(),
162        };
163        gradient.sort();
164        gradient
165    }
166
167    /// Sort the gradient's stops by ascending position.
168    fn sort(&mut self) {
169        self.stops
170            .sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap())
171    }
172
173    /// Find the insertion point for x to maintain order.
174    fn bisect(&self, x: f32) -> Option<usize> {
175        let mut lo = 0;
176        let mut hi = self.stops.len();
177        while lo < hi {
178            let mid = (lo + hi) / 2;
179            match self.stops[mid].0.partial_cmp(&x)? {
180                Ordering::Less => lo = mid + 1,
181                Ordering::Equal => lo = mid + 1,
182                Ordering::Greater => hi = mid,
183            }
184        }
185
186        Some(lo)
187    }
188
189    /// Sample the gradient at the given position.
190    ///
191    /// Returns `None` if the gradient is empty.
192    fn sample_at(&self, x: f32) -> Option<RgbColor> {
193        let insertion_point = self.bisect(x)?;
194        Some(match insertion_point {
195            0 => self.stops.first()?.1,
196            n if n == self.stops.len() => self.stops.last()?.1,
197            n => {
198                let (t0, c0) = *self.stops.get(n - 1)?;
199                let (t1, c1) = *self.stops.get(n)?;
200
201                c0 + (c1 + c0 * -1.0_f32) * ((x - t0) / (t1 - t0))
202            }
203        })
204    }
205
206    /// Samples the gradient at evenly spaced points.
207    ///
208    /// Returns a vector of colors sampled at `n` linearly spaced points between 0.0 and 1.0.
209    /// This is primarily used internally to generate LUT textures for the GPU.
210    ///
211    /// # Arguments
212    ///
213    /// * `n` - Number of samples to generate (must be ≥ 2)
214    ///
215    /// # Panics
216    ///
217    /// Panics if `n ≤ 1` or if the gradient has no stops.
218    ///
219    /// # Example
220    ///
221    /// ```rust
222    /// use phosphor::gradient::{Gradient, RgbColor};
223    ///
224    /// let gradient = Gradient::new([
225    ///     (0.0, RgbColor::BLACK),
226    ///     (1.0, RgbColor::WHITE),
227    /// ]);
228    ///
229    /// let samples = gradient.linear_eval(5);  // [black, dark_gray, gray, light_gray, white]
230    /// assert_eq!(samples.len(), 5);
231    /// ```
232    pub fn linear_eval(&self, n: usize) -> Vec<RgbColor> {
233        (0..n)
234            .map(|idx| (idx as f32) / (n - 1) as f32)
235            .map(|t| self.sample_at(t).unwrap())
236            .collect()
237    }
238}
239
240impl IntoIterator for Gradient {
241    type Item = (f32, RgbColor);
242    type IntoIter = std::vec::IntoIter<Self::Item>;
243    fn into_iter(self) -> Self::IntoIter {
244        self.stops.into_iter()
245    }
246}
247
248impl<'a> IntoIterator for &'a Gradient {
249    type Item = (f32, RgbColor);
250    type IntoIter = std::iter::Copied<std::slice::Iter<'a, Self::Item>>;
251    fn into_iter(self) -> Self::IntoIter {
252        self.stops.iter().copied()
253    }
254}
255
256#[cfg(feature = "egui")]
257impl From<&egui_colorgradient::Gradient> for Gradient {
258    fn from(value: &egui_colorgradient::Gradient) -> Self {
259        Self::new(value.stops.iter().copied())
260    }
261}