eger 1.2.0

A high-level, parallel ASCII-art media pipeline built on top of iascii: images, video, GIFs, dithering, animated text/banners, and procedural illusions, all rendered to ANSI.
Documentation
//! Core color vocabulary: [`Rgb`] and HSV conversion.
//!
//! This module is intentionally tiny and dependency-free so every other
//! module — including the always-on core (`style`, `render`, `config`) —
//! can speak color without opting into the text-animation machinery in
//! `text` (which re-exports [`Rgb`] for compatibility).

/// An RGB color used for text coloring / gradients.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Rgb(pub u8, pub u8, pub u8);

impl Rgb {
    pub const BLACK: Rgb = Rgb(0, 0, 0);
    pub const WHITE: Rgb = Rgb(255, 255, 255);
    pub const RED: Rgb = Rgb(255, 0, 0);
    pub const GREEN: Rgb = Rgb(0, 255, 0);
    pub const BLUE: Rgb = Rgb(0, 0, 255);
    pub const YELLOW: Rgb = Rgb(255, 255, 0);
    pub const CYAN: Rgb = Rgb(0, 255, 255);
    pub const MAGENTA: Rgb = Rgb(255, 0, 255);
    pub const ORANGE: Rgb = Rgb(255, 165, 0);
    pub const PURPLE: Rgb = Rgb(128, 0, 128);
    pub const PINK: Rgb = Rgb(255, 105, 180);
    pub const GRAY: Rgb = Rgb(128, 128, 128);
    pub const SILVER: Rgb = Rgb(192, 192, 192);
    pub const GOLD: Rgb = Rgb(255, 215, 0);
    pub const NAVY: Rgb = Rgb(0, 0, 128);
    pub const TEAL: Rgb = Rgb(0, 128, 128);
    pub const MAROON: Rgb = Rgb(128, 0, 0);
    pub const OLIVE: Rgb = Rgb(128, 128, 0);
    pub const LIME: Rgb = Rgb(50, 205, 50);
    pub const INDIGO: Rgb = Rgb(75, 0, 130);
    pub const VIOLET: Rgb = Rgb(238, 130, 238);
    pub const BROWN: Rgb = Rgb(139, 69, 19);
    pub const CORAL: Rgb = Rgb(255, 127, 80);
    pub const SALMON: Rgb = Rgb(250, 128, 114);
    pub const TURQUOISE: Rgb = Rgb(64, 224, 208);
    pub const CRIMSON: Rgb = Rgb(220, 20, 60);
    pub const SEPIA: Rgb = Rgb(112, 66, 20);

    pub const fn new(r: u8, g: u8, b: u8) -> Self {
        Self(r, g, b)
    }

    #[inline]
    pub const fn r(self) -> u8 {
        self.0
    }
    #[inline]
    pub const fn g(self) -> u8 {
        self.1
    }
    #[inline]
    pub const fn b(self) -> u8 {
        self.2
    }

    pub(crate) fn ansi_fg(self) -> String {
        let mut buf = Vec::with_capacity(19);
        crate::fast_ansi::push_truecolor_escape(&mut buf, self.0, self.1, self.2);
        // Safe: escapes are pure ASCII.
        unsafe { String::from_utf8_unchecked(buf) }
    }

    pub(crate) fn scale(self, brightness: f32) -> Self {
        let brightness = brightness.clamp(0.0, 1.0);
        Rgb(
            (f32::from(self.0) * brightness).round() as u8,
            (f32::from(self.1) * brightness).round() as u8,
            (f32::from(self.2) * brightness).round() as u8,
        )
    }

    /// Linearly interpolates between `self` (`t = 0.0`) and `other`
    /// (`t = 1.0`), clamping `t` to `[0.0, 1.0]` first. Used by
    /// `PixelAnimation::Gradient` (see [`crate::colorize`]) and available
    /// as a general-purpose building block for custom
    /// animations/transforms.
    pub fn mix(self, other: Rgb, t: f32) -> Rgb {
        let t = t.clamp(0.0, 1.0);
        let lerp = |a: u8, b: u8| (f32::from(a) + (f32::from(b) - f32::from(a)) * t).round() as u8;
        Rgb(
            lerp(self.0, other.0),
            lerp(self.1, other.1),
            lerp(self.2, other.2),
        )
    }
}

/// Converts an HSV color (`h` in `0..360`, `s`/`v` in `0.0..=1.0`) to RGB.
pub(crate) fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Rgb {
    let c = v * s;
    let hp = h / 60.0;
    let x = c * (1.0 - (hp.rem_euclid(2.0) - 1.0).abs());
    let (r1, g1, b1) = match hp as u32 {
        0 => (c, x, 0.0),
        1 => (x, c, 0.0),
        2 => (0.0, c, x),
        3 => (0.0, x, c),
        4 => (x, 0.0, c),
        _ => (c, 0.0, x),
    };
    let m = v - c;
    Rgb(
        ((r1 + m) * 255.0).round() as u8,
        ((g1 + m) * 255.0).round() as u8,
        ((b1 + m) * 255.0).round() as u8,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hsv_to_rgb_primary_hues_are_exact() {
        assert_eq!(hsv_to_rgb(0.0, 1.0, 1.0), Rgb::new(255, 0, 0));
        assert_eq!(hsv_to_rgb(120.0, 1.0, 1.0), Rgb::new(0, 255, 0));
        assert_eq!(hsv_to_rgb(240.0, 1.0, 1.0), Rgb::new(0, 0, 255));
    }
}