1use super::prelude::*;
2use crate::{CalcableValue, Percentage};
3
4#[node]
10#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
12#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(all))]
13#[derive(csskit_derives::NodeWithMetadata)]
14#[metadata(value_kinds = Color)]
15pub struct ColorMixFunction<'a> {
16 #[atom(CssAtomSet::ColorMix)]
17 #[cfg_attr(feature = "visitable", visit(skip))]
18 pub name: T![Function],
19 pub interpolation: Option<ColorInterpolationMethod>,
20 #[cfg_attr(feature = "visitable", visit(skip))]
21 #[semantic_eq(skip)]
22 pub interpolation_comma: Option<T![,]>,
23 pub parts: CommaSeparated<'a, ColorMixPart<'a>, 1>,
24 #[cfg_attr(feature = "visitable", visit(skip))]
25 #[semantic_eq(skip)]
26 pub close: T![')'],
27}
28
29#[node]
35#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
37#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
38#[derive(csskit_derives::NodeWithMetadata)]
39pub struct ColorInterpolationMethod {
40 #[atom(CssAtomSet::In)]
41 pub in_keyword: T![Ident],
42 pub color_space: InterpolationColorSpace,
43}
44
45#[node]
51#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
53#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
54#[derive(csskit_derives::NodeWithMetadata)]
55pub enum InterpolationColorSpace {
56 Rectangular(RectangularColorSpace),
57 Polar(PolarColorSpace, Option<HueInterpolationMethod>),
58}
59
60#[node]
67#[derive(
68 Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
69)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
71#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
72#[derive(csskit_derives::NodeWithMetadata)]
73pub enum RectangularColorSpace {
74 #[atom(CssAtomSet::Srgb)]
75 Srgb(T![Ident]),
76 #[atom(CssAtomSet::SrgbLinear)]
77 SrgbLinear(T![Ident]),
78 #[atom(CssAtomSet::DisplayP3)]
79 DisplayP3(T![Ident]),
80 #[atom(CssAtomSet::A98Rgb)]
81 A98Rgb(T![Ident]),
82 #[atom(CssAtomSet::ProphotoRgb)]
83 ProphotoRgb(T![Ident]),
84 #[atom(CssAtomSet::Rec2020)]
85 Rec2020(T![Ident]),
86 #[atom(CssAtomSet::Lab)]
87 Lab(T![Ident]),
88 #[atom(CssAtomSet::Oklab)]
89 Oklab(T![Ident]),
90 #[atom(CssAtomSet::Xyz)]
91 Xyz(T![Ident]),
92 #[atom(CssAtomSet::XyzD50)]
93 XyzD50(T![Ident]),
94 #[atom(CssAtomSet::XyzD65)]
95 XyzD65(T![Ident]),
96}
97
98#[node]
104#[derive(
105 Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
106)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
108#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
109#[derive(csskit_derives::NodeWithMetadata)]
110pub enum PolarColorSpace {
111 #[atom(CssAtomSet::Hsl)]
112 Hsl(T![Ident]),
113 #[atom(CssAtomSet::Hwb)]
114 Hwb(T![Ident]),
115 #[atom(CssAtomSet::Lch)]
116 Lch(T![Ident]),
117 #[atom(CssAtomSet::Oklch)]
118 Oklch(T![Ident]),
119}
120
121#[node]
127#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
129#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
130#[derive(csskit_derives::NodeWithMetadata)]
131pub struct HueInterpolationMethod {
132 pub direction: HueInterpolationDirection,
133 #[atom(CssAtomSet::Hue)]
134 pub hue_keyword: T![Ident],
135}
136
137#[node]
143#[derive(
144 Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
145)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
147#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
148#[derive(csskit_derives::NodeWithMetadata)]
149pub enum HueInterpolationDirection {
150 #[atom(CssAtomSet::Shorter)]
151 Shorter(T![Ident]),
152 #[atom(CssAtomSet::Longer)]
153 Longer(T![Ident]),
154 #[atom(CssAtomSet::Increasing)]
155 Increasing(T![Ident]),
156 #[atom(CssAtomSet::Decreasing)]
157 Decreasing(T![Ident]),
158}
159
160#[node]
168#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
170#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(children))]
171#[derive(csskit_derives::NodeWithMetadata)]
172pub struct ColorMixPart<'a> {
173 pub color: Color<'a>,
174 pub percentage: Option<CalcableValue<'a, Percentage>>,
175}
176
177impl<'a> Peek<'a> for ColorMixPart<'a> {
178 const PEEK_KINDSET: KindSet = Color::PEEK_KINDSET.combine(CalcableValue::<Percentage>::PEEK_KINDSET);
179
180 #[inline(always)]
181 fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
182 where
183 I: Iterator<Item = Cursor> + Clone,
184 {
185 Color::peek(p, c) || CalcableValue::<Percentage>::peek(p, c)
186 }
187}
188
189impl<'a> Parse<'a> for ColorMixPart<'a> {
190 fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
191 where
192 I: Iterator<Item = Cursor> + Clone,
193 {
194 let mut color = p.parse_if_peek::<Color>()?;
196 let percentage = p.parse_if_peek::<CalcableValue<Percentage>>()?;
197 if color.is_none() {
198 color = Some(p.parse::<Color>()?);
199 }
200 Ok(Self { color: color.unwrap(), percentage })
201 }
202}
203
204#[cfg(feature = "chromashift")]
205impl HueInterpolationDirection {
206 pub fn to_hue_interpolation(&self) -> chromashift::HueInterpolation {
208 match self {
209 Self::Shorter(_) => chromashift::HueInterpolation::Shorter,
210 Self::Longer(_) => chromashift::HueInterpolation::Longer,
211 Self::Increasing(_) => chromashift::HueInterpolation::Increasing,
212 Self::Decreasing(_) => chromashift::HueInterpolation::Decreasing,
213 }
214 }
215}
216
217#[cfg(feature = "chromashift")]
218impl InterpolationColorSpace {
219 pub fn to_chromashift(&self) -> chromashift::InterpolationSpace {
221 use chromashift::InterpolationSpace;
222 match self {
223 Self::Rectangular(space) => match space {
224 RectangularColorSpace::Srgb(_) => InterpolationSpace::Srgb,
225 RectangularColorSpace::SrgbLinear(_) => InterpolationSpace::SrgbLinear,
226 RectangularColorSpace::DisplayP3(_) => InterpolationSpace::DisplayP3,
227 RectangularColorSpace::A98Rgb(_) => InterpolationSpace::A98Rgb,
228 RectangularColorSpace::ProphotoRgb(_) => InterpolationSpace::ProphotoRgb,
229 RectangularColorSpace::Rec2020(_) => InterpolationSpace::Rec2020,
230 RectangularColorSpace::Lab(_) => InterpolationSpace::Lab,
231 RectangularColorSpace::Oklab(_) => InterpolationSpace::Oklab,
232 RectangularColorSpace::XyzD50(_) => InterpolationSpace::XyzD50,
233 RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => InterpolationSpace::XyzD65,
234 },
235 Self::Polar(space, hue) => {
236 let hue = hue.as_ref().map(|hue| hue.direction.to_hue_interpolation()).unwrap_or_default();
237 match space {
238 PolarColorSpace::Hsl(_) => InterpolationSpace::Hsl(hue),
239 PolarColorSpace::Hwb(_) => InterpolationSpace::Hwb(hue),
240 PolarColorSpace::Lch(_) => InterpolationSpace::Lch(hue),
241 PolarColorSpace::Oklch(_) => InterpolationSpace::Oklch(hue),
242 }
243 }
244 }
245 }
246}
247
248#[cfg(feature = "chromashift")]
249impl crate::ToChromashift for ColorMixFunction<'_> {
250 fn to_chromashift(&self) -> Option<chromashift::Color> {
251 use chromashift::{Channel, InterpolationSpace};
252
253 let space = self.interpolation.as_ref().map_or(InterpolationSpace::Oklab, |i| i.color_space.to_chromashift());
254 let channels = |color: &Color| {
255 let mut channels = space.channels(color.to_chromashift()?);
256 for (channel, missing) in channels.iter_mut().zip(color.missing_channels()) {
257 if missing {
258 *channel = None;
259 }
260 }
261 Some(channels)
262 };
263
264 let parts: std::vec::Vec<(&Color<'_>, Option<f64>)> = (&self.parts)
265 .into_iter()
266 .map(|(p, _)| {
267 let pct = match &p.percentage {
268 None => None,
269 Some(CalcableValue::Literal(pct)) => Some(pct.value() as f64),
270 Some(_) => return None,
271 };
272 Some((&p.color, pct))
273 })
274 .collect::<Option<_>>()?;
275
276 let default_pct = 100.0 / parts.len() as f64;
277 let mut stack: std::vec::Vec<([Channel; 4], f64)> = std::vec::Vec::with_capacity(parts.len());
278 for (color, pct) in &parts {
279 stack.push((channels(color)?, pct.unwrap_or(default_pct)));
280 }
281
282 if stack.iter().map(|(_, p)| p).sum::<f64>() == 0.0 {
283 return Some(chromashift::Color::Srgb(chromashift::Srgb::new(0.0, 0.0, 0.0, 0.0)));
284 }
285
286 while stack.len() >= 2 {
287 let (b, pct_b) = stack.remove(1);
288 let (a, pct_a) = stack.remove(0);
289 let combined = pct_a + pct_b;
290 stack.insert(0, (space.mix(a, b, pct_b / combined * 100.0), combined));
291 }
292
293 stack.into_iter().next().map(|(channels, _)| space.color(channels))
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::CssAtomSet;
301 use css_parse::{assert_parse, assert_parse_error};
302
303 #[test]
304 fn test_writes() {
305 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red,blue)");
306 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 50%,blue 50%)");
307 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklch,red,blue)");
308 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklch longer hue,red,blue)");
309 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl shorter hue,red,blue)");
310 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl increasing hue,red,blue)");
311 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl decreasing hue,red,blue)");
312 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in lab,rgb(255 0 0),rgb(0 0 255))");
313 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,50% red,blue)");
314 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 50%,blue)");
315 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklab,#fff 30%,#000 70%)");
316 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in xyz-d50,red,green)");
317 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in xyz-d65,red,green)");
318 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb-linear,red,green)");
319 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue)");
320 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red 50%,blue 50%)");
321 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklab,red,blue,green)");
322 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 33%,blue 33%,green 34%)");
323 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue,green)");
324 }
325
326 #[test]
327 #[cfg(feature = "visitable")]
328 fn test_visits() {
329 use crate::assert_visits;
330 assert_visits!("color-mix(in srgb, red, blue)", ColorMixFunction, ColorInterpolationMethod, Color, Color,);
332 assert_visits!(
334 "color-mix(in srgb, rgb(255, 0, 0), blue)",
335 ColorMixFunction,
336 ColorInterpolationMethod,
337 Color,
338 ColorFunction,
339 RgbFunction,
340 Color,
341 );
342 assert_visits!(
344 "color-mix(in srgb, red 50%, blue 50%)",
345 ColorMixFunction,
346 ColorInterpolationMethod,
347 Color,
348 Percentage,
349 Color,
350 Percentage,
351 );
352 assert_visits!(
354 "color-mix(in oklch shorter hue, red, blue)",
355 ColorMixFunction,
356 ColorInterpolationMethod,
357 Color,
358 Color,
359 );
360 assert_visits!("color-mix(red, blue)", ColorMixFunction, Color, Color,);
361 assert_visits!(
362 "color-mix(in oklab, red, blue, green)",
363 ColorMixFunction,
364 ColorInterpolationMethod,
365 Color,
366 Color,
367 Color,
368 );
369 }
370
371 #[test]
372 fn test_errors() {
373 assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(srgb,red,blue)");
374 }
375
376 #[test]
377 fn substitution_in_percentage() {
378 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red calc(50%),blue)");
379 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red var(--p),blue)");
380 }
381}