textiler_core/theme/
gradient.rs

1use crate::theme::color::SimpleColor;
2use crate::theme::Color;
3use crate::utils::bounded_float::BoundedFloat;
4use indexmap::IndexMap;
5use serde::de::Error;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::ops::Index;
9
10/// A gradient defines a gradient, with potential multiple points of color inflection
11#[derive(Debug, Serialize, Deserialize)]
12pub struct Gradient {
13    #[serde(flatten)]
14    #[serde(deserialize_with = "de_gradient")]
15    points: BTreeMap<BoundedFloat<0, 1>, Color>,
16}
17
18fn de_gradient<'de, D: Deserializer<'de>>(
19    des: D,
20) -> Result<BTreeMap<BoundedFloat<0, 1>, Color>, D::Error> {
21    let map = IndexMap::<String, Color>::deserialize(des)?;
22    let points = map
23        .into_iter()
24        .map(|(k, v)| {
25            let float: f32 = k.parse().map_err(|e| D::Error::custom(e))?;
26            let bounded = BoundedFloat::<0, 1>::new(float)
27                .ok_or_else(|| D::Error::custom(format!("{} not in bounds [0,1]", float)))?;
28            Ok((bounded, v))
29        })
30        .collect::<Result<BTreeMap<BoundedFloat<0, 1>, Color>, _>>()?;
31    if points.get(&BoundedFloat::<0, 1>::MIN).is_none()
32        || points.get(&BoundedFloat::<0, 1>::MAX).is_none()
33    {
34        return Err(D::Error::custom(
35            "must specify 0 value and 1 value in gradient",
36        ));
37    }
38    Ok(points)
39}
40
41fn ser_gradient<S: Serializer>(gradient: &Gradient, serde: S) -> Result<S::Ok, S::Error> {
42    gradient
43        .points
44        .iter()
45        .map(|(k, v)| (format!("{}", *k), v.clone()))
46        .collect::<HashMap<String, Color>>()
47        .serialize(serde)
48}
49
50impl Gradient {
51    pub fn new(low: Color, high: Color) -> Self {
52        Self::from_iter([
53            (BoundedFloat::<0, 1>::new(0.0).unwrap(), low),
54            (BoundedFloat::<0, 1>::new(1.0).unwrap(), high),
55        ])
56    }
57
58    fn calc_color_at(&self, bounded_float: &BoundedFloat<0, 1>) -> Option<Color> {
59        let (&high_pt, high) = self.points.range(*bounded_float..).next()?;
60        let (&low_pt, low) = self.points.range(..=*bounded_float).rev().next()?;
61        if high_pt == low_pt || high == low {
62            return Some(low.clone());
63        }
64
65        let color_pt: f32 = (*bounded_float - low_pt) / (high_pt - low_pt);
66        match (low.to_simple().ok()?, high.to_simple().ok()?) {
67            (SimpleColor::Hsla(l_h, l_s, l_l, l_a), SimpleColor::Hsla(h_h, h_s, h_l, h_a)) => {
68                let h = (h_h - l_h) * color_pt + l_h;
69                let s = (h_s - l_s) * color_pt + l_s;
70                let l = (h_l - l_l) * color_pt + l_l;
71                let a = (h_a - l_a) * color_pt + l_a;
72
73                Some(Color::Hsla {
74                    h: (h * 360.0).round() as u16,
75                    s: (s * 100.0).round() as u8,
76                    l: (l * 100.0).round() as u8,
77                    a: (a * 100.0).round() as u8,
78                })
79            }
80            (SimpleColor::Rgba(l_r, l_g, l_b, l_a), SimpleColor::Rgba(h_r, h_g, h_b, h_a)) => {
81                let r = (h_r as f32 - l_r as f32) * color_pt + l_r as f32;
82                let g = (h_g as f32 - l_g as f32) * color_pt + l_g as f32;
83                let b = (h_b as f32 - l_b as f32) * color_pt + l_b as f32;
84                let a = (h_a as f32 - l_a as f32) * color_pt + l_a as f32;
85
86                Some(Color::Rgba {
87                    r: (r).round() as u8,
88                    g: (g).round() as u8,
89                    b: (b).round() as u8,
90                    a: (a).round() as u8,
91                })
92            }
93            _ => None,
94        }
95    }
96
97    pub fn get(&self, ref bounded_float: BoundedFloat<0, 1>) -> Color {
98        self.points.get(bounded_float).cloned().unwrap_or_else(|| {
99            self.calc_color_at(bounded_float)
100                .expect("could not get a color")
101        })
102    }
103
104    pub fn get_mut(&mut self, bounded_float: BoundedFloat<0, 1>) -> &mut Color {
105        let c = self.calc_color_at(&bounded_float).unwrap();
106        self.points.entry(bounded_float).or_insert(c)
107    }
108
109    /// Creates an inflection point at the given index, without returning a mutable-reference
110    pub fn inflect_at(&mut self, bounded_float: BoundedFloat<0, 1>) {
111        let _ = self.get_mut(bounded_float);
112    }
113
114    pub fn print_gradient(&self) {
115        let mut set = HashSet::new();
116        for i in 0..=100 {
117            let color = self.get(BoundedFloat::new(i as f32 / 100.0).unwrap());
118            let [r, g, b, ..] = color.to_rgba().unwrap();
119            print!("\x1b[48;2;{};{};{}m \x1b[0m", r, g, b);
120            set.insert(color.to_rgba().unwrap());
121        }
122        println!("  resolution: {}", set.len());
123    }
124}
125
126impl FromIterator<(BoundedFloat<0, 1>, Color)> for Gradient {
127    fn from_iter<T: IntoIterator<Item = (BoundedFloat<0, 1>, Color)>>(iter: T) -> Self {
128        Self {
129            points: iter.into_iter().collect(),
130        }
131    }
132}
133
134impl<'a> IntoIterator for &'a Gradient {
135    type Item = (&'a BoundedFloat<0, 1>, &'a Color);
136    type IntoIter = <&'a BTreeMap<BoundedFloat<0, 1>, Color> as IntoIterator>::IntoIter;
137
138    fn into_iter(self) -> Self::IntoIter {
139        self.points.iter()
140    }
141}
142
143impl IntoIterator for Gradient {
144    type Item = (BoundedFloat<0, 1>, Color);
145    type IntoIter = <BTreeMap<BoundedFloat<0, 1>, Color> as IntoIterator>::IntoIter;
146
147    fn into_iter(self) -> Self::IntoIter {
148        self.points.into_iter()
149    }
150}
151#[cfg(test)]
152mod tests {
153    use crate::theme::gradient::Gradient;
154    use crate::theme::Color;
155    use crate::utils::bounded_float::BoundedFloat;
156
157    #[test]
158    fn test_gradient() {
159        for saturation in 0..=100 {
160            let light = 75;
161            let mut gradient = Gradient::new(
162                Color::hsl(0, saturation, light),
163                Color::hsl(360, saturation, light),
164            );
165            *gradient.get_mut(BoundedFloat::new(1. / 6.).unwrap()) =
166                Color::hsl(60, saturation, light);
167            *gradient.get_mut(BoundedFloat::new(2. / 6.).unwrap()) =
168                Color::hsl(120, saturation, light);
169            *gradient.get_mut(BoundedFloat::new(3. / 6.).unwrap()) =
170                Color::hsl(180, saturation, light);
171            *gradient.get_mut(BoundedFloat::new(4. / 6.).unwrap()) =
172                Color::hsl(240, saturation, light);
173            *gradient.get_mut(BoundedFloat::new(5. / 6.).unwrap()) =
174                Color::hsl(300, saturation, light);
175
176            gradient.print_gradient();
177        }
178    }
179}