1use std::sync::Arc;
35
36use crate::color::Color;
37use crate::error::PaletteError;
38use crate::manipulation::{
39 lerp_oklab, lerp_oklch, oklab_to_srgb, oklch_to_oklab, srgb_to_oklab, srgb_to_oklch,
40};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44#[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
45pub enum ColorSpace {
46 #[default]
48 OkLab,
49 OkLch,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
59#[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
60pub enum GradientColor {
61 Literal(Color),
63 Token {
65 section: Arc<str>,
67 field: Arc<str>,
69 },
70}
71
72#[derive(Debug, Clone, PartialEq)]
77#[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
78pub struct GradientDef {
79 stops: Box<[(GradientColor, f64)]>,
80 space: ColorSpace,
81}
82
83impl GradientDef {
84 pub(crate) fn new(stops: Box<[(GradientColor, f64)]>, space: ColorSpace) -> Self {
88 Self { stops, space }
89 }
90
91 pub fn stops(&self) -> &[(GradientColor, f64)] {
93 &self.stops
94 }
95
96 pub fn space(&self) -> ColorSpace {
98 self.space
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
104#[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
105pub struct GradientStop {
106 pub color: Color,
108 pub position: f64,
110}
111
112#[derive(Debug, Clone, PartialEq)]
114#[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
115pub struct Gradient {
116 stops: Box<[GradientStop]>,
117 space: ColorSpace,
118}
119
120impl Gradient {
121 pub(crate) fn new_unchecked(stops: impl Into<Box<[GradientStop]>>, space: ColorSpace) -> Self {
126 Self {
127 stops: stops.into(),
128 space,
129 }
130 }
131
132 pub fn new(
137 stops: impl Into<Box<[GradientStop]>>,
138 space: ColorSpace,
139 ) -> Result<Self, PaletteError> {
140 let stops = stops.into();
141 match stops.len() < 2 {
142 true => return Err(PaletteError::InsufficientStops { count: stops.len() }),
143 false => {}
144 }
145 for stop in stops.iter() {
146 match stop.position.is_nan() || !(0.0..=1.0).contains(&stop.position) {
147 true => {
148 return Err(PaletteError::InvalidGradientPosition {
149 position: stop.position,
150 });
151 }
152 false => {}
153 }
154 }
155 let sorted = stops.windows(2).all(|w| w[0].position <= w[1].position);
156 match sorted {
157 true => Ok(Self { stops, space }),
158 false => Err(PaletteError::UnsortedStops),
159 }
160 }
161
162 pub fn at(&self, t: f64) -> Color {
165 let t = match t.is_nan() {
166 true => 0.0,
167 false => t.clamp(0.0, 1.0),
168 };
169 interpolate_at(&self.stops, self.space, t)
170 }
171
172 pub fn sample(&self, n: usize) -> Box<[Color]> {
178 match n {
179 0 => Box::new([]),
180 1 => Box::new([self.at(0.0)]),
181 _ => {
182 let divisor = (n - 1) as f64;
183 (0..n)
184 .map(|i| self.at(i as f64 / divisor))
185 .collect::<Vec<_>>()
186 .into_boxed_slice()
187 }
188 }
189 }
190
191 pub fn stops(&self) -> &[GradientStop] {
193 &self.stops
194 }
195
196 pub fn space(&self) -> ColorSpace {
198 self.space
199 }
200
201 pub fn to_css(&self) -> Box<str> {
206 use std::fmt::Write;
207
208 let space_str = match self.space {
209 ColorSpace::OkLab => "oklab",
210 ColorSpace::OkLch => "oklch",
211 };
212
213 let mut buf = String::with_capacity(64);
214 let _ = write!(buf, "linear-gradient(in {space_str},");
215
216 let evenly_spaced = is_evenly_spaced(&self.stops);
217
218 for (i, stop) in self.stops.iter().enumerate() {
219 let hex = stop.color.to_hex();
220 match evenly_spaced {
221 true => {
222 let _ = write!(buf, " {hex}");
223 }
224 false => {
225 let pct = stop.position * 100.0;
226 let _ = write!(buf, " {hex} {pct}%");
227 }
228 }
229 match i < self.stops.len() - 1 {
230 true => buf.push(','),
231 false => {}
232 }
233 }
234
235 buf.push(')');
236 buf.into_boxed_str()
237 }
238}
239
240fn is_evenly_spaced(stops: &[GradientStop]) -> bool {
242 match stops.len() {
243 0..=2 => true,
244 n => {
245 let first = stops[0].position;
246 let last = stops[n - 1].position;
247 let step = (last - first) / (n - 1) as f64;
248 stops
249 .iter()
250 .enumerate()
251 .all(|(i, s)| (s.position - (first + step * i as f64)).abs() < 1e-9)
252 }
253 }
254}
255
256fn interpolate_at(stops: &[GradientStop], space: ColorSpace, t: f64) -> Color {
258 let first = stops[0];
260 let last = stops[stops.len() - 1];
261 match () {
262 _ if t <= first.position => return first.color,
263 _ if t >= last.position => return last.color,
264 _ => {}
265 }
266
267 let idx = match stops[1..].iter().position(|s| s.position >= t) {
269 Some(i) => i,
270 None => return last.color,
271 };
272
273 let a = &stops[idx];
274 let b = &stops[idx + 1];
275
276 let span = b.position - a.position;
278 match span <= f64::EPSILON {
279 true => return a.color,
280 false => {}
281 }
282
283 let local_t = (t - a.position) / span;
284 interpolate_colors(a.color, b.color, space, local_t)
285}
286
287fn interpolate_colors(a: Color, b: Color, space: ColorSpace, t: f64) -> Color {
288 match space {
289 ColorSpace::OkLab => {
290 let lab_a = srgb_to_oklab(a);
291 let lab_b = srgb_to_oklab(b);
292 oklab_to_srgb(lerp_oklab(lab_a, lab_b, t))
293 }
294 ColorSpace::OkLch => {
295 let lch_a = srgb_to_oklch(a);
296 let lch_b = srgb_to_oklch(b);
297 oklab_to_srgb(oklch_to_oklab(lerp_oklch(lch_a, lch_b, t)))
298 }
299 }
300}