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}