phosphor-crt 0.1.0

A real-time plotter of waveforms, imitating oscillscope CRTs
Documentation
//! Color gradients and RGB color utilities.
//!
//! This module provides color types and gradient functionality for mapping
//! intensity values to colors in the rendered waveform output.

use cgmath::num_traits::ToPrimitive;
use std::cmp::Ordering;
use std::ops::{Add, Mul};

/// RGB color with configurable component type.
#[derive(Copy, Clone, Debug)]
pub struct RgbColor<T = f32> {
    /// Red component
    pub r: T,
    /// Green component  
    pub g: T,
    /// Blue component
    pub b: T,
}

impl<T> RgbColor<T> {
    /// Creates a new RGB color.
    ///
    /// # Arguments
    ///
    /// * `r` - Red component
    /// * `g` - Green component  
    /// * `b` - Blue component
    ///
    /// # Example
    ///
    /// ```rust
    /// use phosphor::gradient::RgbColor;
    ///
    /// let red = RgbColor::new(1.0, 0.0, 0.0);
    /// let purple: RgbColor<u8> = RgbColor::new(128, 0, 128);
    /// ```
    pub const fn new(r: T, g: T, b: T) -> Self {
        Self { r, g, b }
    }
}

impl Mul<f32> for RgbColor<f32> {
    type Output = RgbColor<f32>;
    fn mul(self, rhs: f32) -> Self::Output {
        RgbColor::new(self.r * rhs, self.g * rhs, self.b * rhs)
    }
}

#[cfg(feature = "egui")]
impl From<egui::Rgba> for RgbColor {
    fn from(value: egui::Rgba) -> Self {
        RgbColor {
            r: value.r(),
            g: value.g(),
            b: value.b(),
        }
    }
}

#[cfg(feature = "egui")]
impl From<RgbColor> for egui::Rgba {
    fn from(value: RgbColor) -> egui::Rgba {
        egui::Rgba::from_rgb(value.r, value.g, value.b)
    }
}

#[cfg(feature = "egui")]
impl From<RgbColor> for egui::ecolor::Hsva {
    fn from(value: RgbColor) -> egui::ecolor::Hsva {
        Into::<egui::Rgba>::into(value).into()
    }
}

#[cfg(feature = "egui")]
impl From<egui::ecolor::Hsva> for RgbColor {
    fn from(value: egui::ecolor::Hsva) -> Self {
        Into::<egui::Rgba>::into(value).into()
    }
}

impl Add<RgbColor<f32>> for RgbColor<f32> {
    type Output = RgbColor<f32>;
    fn add(self, rhs: RgbColor<f32>) -> Self::Output {
        RgbColor::new(self.r + rhs.r, self.g + rhs.g, self.b + rhs.b)
    }
}

impl RgbColor {
    pub const BLACK: RgbColor = RgbColor::new(0., 0., 0.);
    pub const WHITE: RgbColor = RgbColor::new(1., 1., 1.);
    pub const RED: RgbColor = RgbColor::new(1., 0., 0.);
    pub const GREEN: RgbColor = RgbColor::new(0., 1., 0.);
    pub const BLUE: RgbColor = RgbColor::new(0., 0., 1.);
}

impl From<RgbColor<f32>> for RgbColor<u8> {
    fn from(value: RgbColor<f32>) -> Self {
        RgbColor {
            r: (value.r * 255.).round().clamp(0., 255.).to_u8().unwrap(),
            g: (value.g * 255.).round().clamp(0., 255.).to_u8().unwrap(),
            b: (value.b * 255.).round().clamp(0., 255.).to_u8().unwrap(),
        }
    }
}

impl From<[f32; 3]> for RgbColor {
    fn from([r, g, b]: [f32; 3]) -> Self {
        RgbColor::new(r, g, b)
    }
}

/// A color gradient defined by interpolation between color stops.
///
/// Gradients map normalized intensity values (0.0 to 1.0) to colors through linear
/// interpolation between user-defined color stops. This is used to create the Look-Up
/// Table (LUT) texture for intensity-to-color mapping in the renderer.
///
/// # Example
///
/// ```rust
/// use phosphor::gradient::{Gradient, RgbColor};
///
/// // Create classic green oscilloscope gradient
/// let gradient = Gradient::new(vec![
///     (0.0, RgbColor::new(0.0, 0.0, 0.0)),    // Black background
///     (0.3, RgbColor::new(0.0, 0.2, 0.0)),    // Dark green
///     (0.7, RgbColor::new(0.0, 0.8, 0.0)),    // Bright green
///     (1.0, RgbColor::new(0.8, 1.0, 0.8)),    // Saturated green
/// ]);
/// ```
#[derive(Debug)]
pub struct Gradient {
    stops: Vec<(f32, RgbColor)>,
}

impl Gradient {
    /// Creates a new gradient from color stops.
    ///
    /// The stops define (position, color) pairs.
    /// The gradient will interpolate linearly between these stops. Stops are automatically
    /// sorted by position.
    ///
    /// # Arguments
    ///
    /// * `stops` - Iterator of (position, color) pairs
    ///
    /// # Example
    ///
    /// ```rust
    /// use phosphor::gradient::{Gradient, RgbColor};
    ///
    /// let gradient = Gradient::new([
    ///     (0.0, RgbColor::new(0.0, 0.0, 0.0)),  // Arrays convert to RgbColor
    ///     (0.5, RgbColor::new(0.5, 0.0, 0.5)),
    ///     (1.0, RgbColor::WHITE),
    /// ]);
    /// ```
    pub fn new(stops: impl IntoIterator<Item = (f32, impl Into<RgbColor>)>) -> Self {
        let mut gradient = Gradient {
            stops: stops.into_iter().map(|(k, v)| (k, v.into())).collect(),
        };
        gradient.sort();
        gradient
    }

    /// Sort the gradient's stops by ascending position.
    fn sort(&mut self) {
        self.stops
            .sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap())
    }

    /// Find the insertion point for x to maintain order.
    fn bisect(&self, x: f32) -> Option<usize> {
        let mut lo = 0;
        let mut hi = self.stops.len();
        while lo < hi {
            let mid = (lo + hi) / 2;
            match self.stops[mid].0.partial_cmp(&x)? {
                Ordering::Less => lo = mid + 1,
                Ordering::Equal => lo = mid + 1,
                Ordering::Greater => hi = mid,
            }
        }

        Some(lo)
    }

    /// Sample the gradient at the given position.
    ///
    /// Returns `None` if the gradient is empty.
    fn sample_at(&self, x: f32) -> Option<RgbColor> {
        let insertion_point = self.bisect(x)?;
        Some(match insertion_point {
            0 => self.stops.first()?.1,
            n if n == self.stops.len() => self.stops.last()?.1,
            n => {
                let (t0, c0) = *self.stops.get(n - 1)?;
                let (t1, c1) = *self.stops.get(n)?;

                c0 + (c1 + c0 * -1.0_f32) * ((x - t0) / (t1 - t0))
            }
        })
    }

    /// Samples the gradient at evenly spaced points.
    ///
    /// Returns a vector of colors sampled at `n` linearly spaced points between 0.0 and 1.0.
    /// This is primarily used internally to generate LUT textures for the GPU.
    ///
    /// # Arguments
    ///
    /// * `n` - Number of samples to generate (must be ≥ 2)
    ///
    /// # Panics
    ///
    /// Panics if `n ≤ 1` or if the gradient has no stops.
    ///
    /// # Example
    ///
    /// ```rust
    /// use phosphor::gradient::{Gradient, RgbColor};
    ///
    /// let gradient = Gradient::new([
    ///     (0.0, RgbColor::BLACK),
    ///     (1.0, RgbColor::WHITE),
    /// ]);
    ///
    /// let samples = gradient.linear_eval(5);  // [black, dark_gray, gray, light_gray, white]
    /// assert_eq!(samples.len(), 5);
    /// ```
    pub fn linear_eval(&self, n: usize) -> Vec<RgbColor> {
        (0..n)
            .map(|idx| (idx as f32) / (n - 1) as f32)
            .map(|t| self.sample_at(t).unwrap())
            .collect()
    }
}

impl IntoIterator for Gradient {
    type Item = (f32, RgbColor);
    type IntoIter = std::vec::IntoIter<Self::Item>;
    fn into_iter(self) -> Self::IntoIter {
        self.stops.into_iter()
    }
}

impl<'a> IntoIterator for &'a Gradient {
    type Item = (f32, RgbColor);
    type IntoIter = std::iter::Copied<std::slice::Iter<'a, Self::Item>>;
    fn into_iter(self) -> Self::IntoIter {
        self.stops.iter().copied()
    }
}

#[cfg(feature = "egui")]
impl From<&egui_colorgradient::Gradient> for Gradient {
    fn from(value: &egui_colorgradient::Gradient) -> Self {
        Self::new(value.stops.iter().copied())
    }
}