ratio_color/
gradient.rs

1//! # Color gradient module
2//!
3//! Treats interpolating between bounds and colors as well as the creation of numerical palettes.
4//!
5//! ## License
6//!
7//! This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
8//! If a copy of the MPL was not distributed with this file,
9//! You can obtain one at <https://mozilla.org/MPL/2.0/>.
10//!
11//! **Code examples both in the docstrings and rendered documentation are free to use.**
12
13use std::collections::BTreeMap;
14
15use enterpolation::linear::{Linear, LinearError};
16use enterpolation::{Identity, Signal, Sorted};
17use palette::{LinSrgba, Srgba};
18use snafu::prelude::*;
19
20use crate::{HexRgba, Numerical, Palette, Selector};
21
22/// Ratio Color error.
23#[derive(Clone, Debug, Snafu)]
24#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
25pub enum Error {
26    /// Linear interpolation error.
27    Linear { source: LinearError },
28}
29
30/// Capability of interpolating a color at a given fraction between 0.0 and 1.0.
31pub trait ColorAtFraction<Color> {
32    /// Interpolated color at a given fraction between 0.0 and 1.0.
33    fn color_at(&self, fraction: f64) -> Color;
34}
35
36/// Enable a CSS gradient generation function.
37pub trait CssGradient {
38    /// Create a CSS gradient string with the given fractions between 0.0 and 1.0 as knots.
39    fn css_gradient(&self, fractions: &[f64]) -> String;
40}
41
42/// A linearly interpolated gradient.
43#[derive(Clone, Debug, PartialEq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
46pub struct LinearGradient {
47    pub gradient: Linear<Sorted<Vec<f64>>, Vec<LinSrgba<f64>>, Identity>,
48}
49impl Default for LinearGradient {
50    fn default() -> Self {
51        Numerical::Ratio.into()
52    }
53}
54impl LinearGradient {
55    /// Create a new linear gradient.
56    pub fn try_new<Color, Colors>(colors: Colors, knots: Option<&[f64]>) -> Result<Self, Error>
57    where
58        Color: Into<LinSrgba<f64>>,
59        Colors: IntoIterator<Item = Color>,
60    {
61        let elements: Vec<LinSrgba<f64>> = colors.into_iter().map(|color| color.into()).collect();
62        let knots = match knots {
63            Some(knots) => knots.to_vec(),
64            None => {
65                let len = elements.len();
66                let step = if len <= 1 {
67                    1.0
68                } else {
69                    1.0 / ((len - 1) as f64)
70                };
71                (0..len).map(|x| x as f64 * step).collect()
72            }
73        };
74        let linear = Linear::builder()
75            .elements(elements)
76            .knots(knots)
77            .build()
78            .with_context(|_| LinearSnafu)?;
79        Ok(Self { gradient: linear })
80    }
81}
82
83impl<C: Into<LinSrgba<f64>>> From<Vec<C>> for LinearGradient {
84    fn from(value: Vec<C>) -> Self {
85        Self::try_new(value, None).unwrap()
86    }
87}
88
89impl ColorAtFraction<LinSrgba<f64>> for LinearGradient {
90    fn color_at(&self, fraction: f64) -> LinSrgba<f64> {
91        self.gradient.eval(fraction)
92    }
93}
94
95impl ColorAtFraction<Srgba<f64>> for LinearGradient {
96    fn color_at(&self, fraction: f64) -> Srgba<f64> {
97        Srgba::from_linear(self.gradient.eval(fraction))
98    }
99}
100
101impl ColorAtFraction<Srgba<u8>> for LinearGradient {
102    fn color_at(&self, fraction: f64) -> Srgba<u8> {
103        Srgba::from_linear(self.gradient.eval(fraction))
104    }
105}
106
107impl ColorAtFraction<HexRgba> for LinearGradient {
108    fn color_at(&self, fraction: f64) -> HexRgba {
109        Srgba::<u8>::from_linear(self.gradient.eval(fraction)).into()
110    }
111}
112
113impl CssGradient for LinearGradient {
114    fn css_gradient(&self, fractions: &[f64]) -> String {
115        let colors: String = fractions
116            .iter()
117            .map(|&f| {
118                let color = self.color_at(f);
119                format!(
120                    "{} {:.0}%",
121                    to_rgba_string(Srgba::<u8>::from_linear(color)),
122                    100.0 * f
123                )
124            })
125            .collect::<Vec<_>>()
126            .join(",");
127        format!("linear-gradient({colors})")
128    }
129}
130
131/// Convert a Srgba (u8) color to a CSS string.
132pub fn to_rgba_string<C: Into<Srgba<u8>>>(color: C) -> String {
133    let color = color.into();
134    format!(
135        "rgba({},{},{},{})",
136        color.red, color.green, color.blue, color.alpha
137    )
138}
139
140/// Numerical color scale information. Either knot values paired with colors, or just an array of
141/// colors to be mapped across entire domains.
142#[derive(Clone, Debug, PartialEq)]
143#[cfg_attr(
144    feature = "serde",
145    derive(serde::Serialize, serde::Deserialize),
146    serde(rename_all = "camelCase", untagged)
147)]
148#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
149#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
150pub enum NumericalColorScale<Color> {
151    Knots(Vec<(f64, Color)>),
152    Colors(Vec<Color>),
153}
154
155impl<Color: From<Srgba<u8>>> Default for NumericalColorScale<Color> {
156    fn default() -> Self {
157        Self::Colors(
158            crate::Numerical::Ratio
159                .colors()
160                .into_iter()
161                .map(Into::into)
162                .collect(),
163        )
164    }
165}
166
167impl<C, Color: From<C>> From<Vec<C>> for NumericalColorScale<Color> {
168    fn from(value: Vec<C>) -> Self {
169        Self::Colors(value.into_iter().map(Into::into).collect())
170    }
171}
172
173impl From<NumericalColorScale<Srgba<u8>>> for NumericalColorScale<HexRgba> {
174    fn from(value: NumericalColorScale<Srgba<u8>>) -> Self {
175        match value {
176            NumericalColorScale::Colors(colors) => {
177                NumericalColorScale::Colors(colors.into_iter().map(Into::into).collect())
178            }
179            NumericalColorScale::Knots(knots) => NumericalColorScale::Knots(
180                knots
181                    .into_iter()
182                    .map(|(knot, color)| (knot, color.into()))
183                    .collect(),
184            ),
185        }
186    }
187}
188
189impl<Color: Into<LinSrgba<f64>>> TryInto<LinearGradient> for NumericalColorScale<Color> {
190    type Error = crate::Error;
191    fn try_into(self) -> Result<LinearGradient, Self::Error> {
192        match self {
193            NumericalColorScale::Colors(colors) => LinearGradient::try_new(colors, None),
194            NumericalColorScale::Knots(knots) => {
195                let (knots, colors): (Vec<f64>, Vec<Color>) = knots.into_iter().unzip();
196                LinearGradient::try_new(colors, Some(&knots))
197            }
198        }
199    }
200}
201
202impl<Key: Ord, Color: Into<LinSrgba<f64>>> TryFrom<Palette<Key, NumericalColorScale<Color>>>
203    for Palette<Key, LinearGradient>
204{
205    type Error = crate::Error;
206    fn try_from(value: Palette<Key, NumericalColorScale<Color>>) -> Result<Self, Self::Error> {
207        Ok(Self {
208            values: value
209                .values
210                .into_iter()
211                .map(TryInto::<LinearGradient>::try_into)
212                .collect::<Result<Vec<LinearGradient>, Self::Error>>()?,
213            selectors: value
214                .selectors
215                .into_iter()
216                .map(|(k, sel)| {
217                    // convert selector
218                    match sel {
219                        Selector::Index(index) => Ok(Selector::<LinearGradient>::Index(index)),
220                        Selector::KeyOrder => Ok(Selector::<LinearGradient>::KeyOrder),
221                        Selector::Value(scale) => {
222                            scale.try_into().map(Selector::<LinearGradient>::Value)
223                        }
224                    }
225                    // re-pack with key
226                    .map(|sel| (k, sel))
227                })
228                .collect::<Result<BTreeMap<Key, Selector<LinearGradient>>, Self::Error>>()?,
229        })
230    }
231}