ink.rs

4.8 kB · rust · 213 lines

1use mrlycore::colors::{Theme, DARK, LIGHT};2use mrlycore::Color;3use std::sync::OnceLock;45// THEME67/// The theme every figure is painted in: light when MRLYFIG_THEME is "light", dark otherwise, read once.8pub fn theme() -> &'static Theme {9    static THEME: OnceLock<&'static Theme> = OnceLock::new();10    THEME.get_or_init(|| match std::env::var("MRLYFIG_THEME").as_deref() {11        Ok("light") => &LIGHT,12        _ => &DARK,13    })14}1516/// The name of the theme in press, "dark" or "light".17pub fn name() -> &'static str {18    if *theme() == LIGHT {19        "light"20    } else {21        "dark"22    }23}2425// SURFACES2627/// The ground every figure is painted on.28pub fn ground() -> Color {29    theme().ground30}3132/// The raised panel, one step off the ground.33pub fn panel() -> Color {34    theme().panel35}3637/// The hairline that separates one thing from the next.38pub fn line() -> Color {39    theme().line40}4142/// The foreground, the strongest tone on the ground.43pub fn fg() -> Color {44    theme().fg45}4647/// The dimmed foreground, for anything secondary.48pub fn dim() -> Color {49    theme().dim50}5152// HUES5354/// The red ink.55pub fn red() -> Color {56    theme().red57}5859/// The orange ink.60pub fn orange() -> Color {61    theme().orange62}6364/// The yellow ink.65pub fn yellow() -> Color {66    theme().yellow67}6869/// The green ink.70pub fn green() -> Color {71    theme().green72}7374/// The mint ink.75pub fn mint() -> Color {76    theme().mint77}7879/// The teal ink.80pub fn teal() -> Color {81    theme().teal82}8384/// The cyan ink.85pub fn cyan() -> Color {86    theme().cyan87}8889/// The blue ink.90pub fn blue() -> Color {91    theme().blue92}9394/// The indigo ink.95pub fn indigo() -> Color {96    theme().indigo97}9899/// The purple ink.100pub fn purple() -> Color {101    theme().purple102}103104/// The pink ink.105pub fn pink() -> Color {106    theme().pink107}108109/// The brown ink.110pub fn brown() -> Color {111    theme().brown112}113114/// The gray ink.115pub fn gray() -> Color {116    theme().gray117}118119/// The six inks in their fixed order, the wheel a figure cycles through: blue, orange, yellow, green, pink, indigo.120pub fn inks() -> [Color; 6] {121    theme().inks()122}123124// MIXING125126/// Blends two colors channel by channel, t clamped to the unit interval.127pub fn mix(a: Color, b: Color, t: f64) -> Color {128    let t = t.clamp(0.0, 1.0);129    let lerp = |x: u8, y: u8| (x as f64 + (y as f64 - x as f64) * t).round() as u8;130    Color::rgba(131        lerp(a.r, b.r),132        lerp(a.g, b.g),133        lerp(a.b, b.b),134        lerp(a.a, b.a),135    )136}137138/// Returns the color at a fraction of its opacity, alpha clamped to the unit interval.139pub fn fade(c: Color, alpha: f64) -> Color {140    Color::rgba(c.r, c.g, c.b, (255.0 * alpha.clamp(0.0, 1.0)).round() as u8)141}142143// RAMP144145/// A color ramp: a line through its stops, read at any point of the unit interval.146#[derive(Clone, Debug, PartialEq, Eq)]147pub struct Ramp {148    /// The stops, evenly spaced from zero to one.149    pub stops: Vec<Color>,150}151152impl Ramp {153    /// Builds a ramp from its stops, which must not be empty.154    pub fn new(stops: Vec<Color>) -> Ramp {155        Ramp { stops }156    }157    /// Reads the ramp at t, clamped to the unit interval; the ground when there are no stops.158    pub fn at(&self, t: f64) -> Color {159        if self.stops.is_empty() {160            return ground();161        }162        if self.stops.len() == 1 {163            return self.stops[0];164        }165        let t = t.clamp(0.0, 1.0) * (self.stops.len() - 1) as f64;166        let i = (t.floor() as usize).min(self.stops.len() - 2);167        mix(self.stops[i], self.stops[i + 1], t - i as f64)168    }169    /// The heat ramp: ground, blue, yellow, foreground.170    pub fn heat() -> Ramp {171        Ramp::new(vec![ground(), blue(), yellow(), fg()])172    }173    /// The fire ramp: ground, orange, yellow, foreground.174    pub fn fire() -> Ramp {175        Ramp::new(vec![ground(), orange(), yellow(), fg()])176    }177    /// The diverging ramp: blue through the ground to orange.178    pub fn diverge() -> Ramp {179        Ramp::new(vec![blue(), ground(), orange()])180    }181    /// The two-tone ramp from one color straight to another.182    pub fn tone(a: Color, b: Color) -> Ramp {183        Ramp::new(vec![a, b])184    }185}186187#[cfg(test)]188mod tests {189    use super::*;190    #[test]191    fn ramp_ends_are_its_end_stops() {192        let ramp = Ramp::heat();193        assert_eq!(ramp.at(0.0), ground());194        assert_eq!(ramp.at(1.0), fg());195    }196    #[test]197    fn an_empty_ramp_reads_the_ground() {198        assert_eq!(Ramp::new(vec![]).at(0.5), ground());199    }200    #[test]201    fn mix_halfway_sits_between_the_two() {202        assert_eq!(203            mix(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255), 0.5).r,204            128205        );206    }207    #[test]208    fn the_inks_are_the_themes_six() {209        assert_eq!(inks(), theme().inks());210        assert_eq!(inks()[0], blue());211        assert_eq!(inks()[5], indigo());212    }213}