1use 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#[derive(Clone, Debug, Snafu)]
24#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
25pub enum Error {
26 Linear { source: LinearError },
28}
29
30pub trait ColorAtFraction<Color> {
32 fn color_at(&self, fraction: f64) -> Color;
34}
35
36pub trait CssGradient {
38 fn css_gradient(&self, fractions: &[f64]) -> String;
40}
41
42#[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 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
131pub 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#[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 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 .map(|sel| (k, sel))
227 })
228 .collect::<Result<BTreeMap<Key, Selector<LinearGradient>>, Self::Error>>()?,
229 })
230 }
231}