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)]
14pub struct ColorMixFunction<'a> {
15 #[atom(CssAtomSet::ColorMix)]
16 #[cfg_attr(feature = "visitable", visit(skip))]
17 pub name: T![Function],
18 pub interpolation: Option<ColorInterpolationMethod>,
19 #[cfg_attr(feature = "visitable", visit(skip))]
20 #[semantic_eq(skip)]
21 pub interpolation_comma: Option<T![,]>,
22 pub parts: CommaSeparated<'a, ColorMixPart<'a>, 1>,
23 #[cfg_attr(feature = "visitable", visit(skip))]
24 #[semantic_eq(skip)]
25 pub close: T![')'],
26}
27
28#[node]
34#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
36#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
37#[derive(csskit_derives::NodeWithMetadata)]
38pub struct ColorInterpolationMethod {
39 #[atom(CssAtomSet::In)]
40 pub in_keyword: T![Ident],
41 pub color_space: InterpolationColorSpace,
42}
43
44#[node]
50#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
52#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
53#[derive(csskit_derives::NodeWithMetadata)]
54pub enum InterpolationColorSpace {
55 Rectangular(RectangularColorSpace),
56 Polar(PolarColorSpace, Option<HueInterpolationMethod>),
57}
58
59#[node]
66#[derive(
67 Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
68)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
70#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
71#[derive(csskit_derives::NodeWithMetadata)]
72pub enum RectangularColorSpace {
73 #[atom(CssAtomSet::Srgb)]
74 Srgb(T![Ident]),
75 #[atom(CssAtomSet::SrgbLinear)]
76 SrgbLinear(T![Ident]),
77 #[atom(CssAtomSet::DisplayP3)]
78 DisplayP3(T![Ident]),
79 #[atom(CssAtomSet::A98Rgb)]
80 A98Rgb(T![Ident]),
81 #[atom(CssAtomSet::ProphotoRgb)]
82 ProphotoRgb(T![Ident]),
83 #[atom(CssAtomSet::Rec2020)]
84 Rec2020(T![Ident]),
85 #[atom(CssAtomSet::Lab)]
86 Lab(T![Ident]),
87 #[atom(CssAtomSet::Oklab)]
88 Oklab(T![Ident]),
89 #[atom(CssAtomSet::Xyz)]
90 Xyz(T![Ident]),
91 #[atom(CssAtomSet::XyzD50)]
92 XyzD50(T![Ident]),
93 #[atom(CssAtomSet::XyzD65)]
94 XyzD65(T![Ident]),
95}
96
97#[node]
103#[derive(
104 Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
105)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
107#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
108#[derive(csskit_derives::NodeWithMetadata)]
109pub enum PolarColorSpace {
110 #[atom(CssAtomSet::Hsl)]
111 Hsl(T![Ident]),
112 #[atom(CssAtomSet::Hwb)]
113 Hwb(T![Ident]),
114 #[atom(CssAtomSet::Lch)]
115 Lch(T![Ident]),
116 #[atom(CssAtomSet::Oklch)]
117 Oklch(T![Ident]),
118}
119
120#[node]
126#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
128#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
129#[derive(csskit_derives::NodeWithMetadata)]
130pub struct HueInterpolationMethod {
131 pub direction: HueInterpolationDirection,
132 #[atom(CssAtomSet::Hue)]
133 pub hue_keyword: T![Ident],
134}
135
136#[node]
142#[derive(
143 Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
144)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
146#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
147#[derive(csskit_derives::NodeWithMetadata)]
148pub enum HueInterpolationDirection {
149 #[atom(CssAtomSet::Shorter)]
150 Shorter(T![Ident]),
151 #[atom(CssAtomSet::Longer)]
152 Longer(T![Ident]),
153 #[atom(CssAtomSet::Increasing)]
154 Increasing(T![Ident]),
155 #[atom(CssAtomSet::Decreasing)]
156 Decreasing(T![Ident]),
157}
158
159#[node]
167#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
169#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(children))]
170#[derive(csskit_derives::NodeWithMetadata)]
171pub struct ColorMixPart<'a> {
172 pub color: Color<'a>,
173 pub percentage: Option<CalcableValue<'a, Percentage>>,
174}
175
176impl<'a> Peek<'a> for ColorMixPart<'a> {
177 const PEEK_KINDSET: KindSet = Color::PEEK_KINDSET.combine(CalcableValue::<Percentage>::PEEK_KINDSET);
178
179 #[inline(always)]
180 fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
181 where
182 I: Iterator<Item = Cursor> + Clone,
183 {
184 Color::peek(p, c) || CalcableValue::<Percentage>::peek(p, c)
185 }
186}
187
188impl<'a> Parse<'a> for ColorMixPart<'a> {
189 fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
190 where
191 I: Iterator<Item = Cursor> + Clone,
192 {
193 let mut color = p.parse_if_peek::<Color>()?;
195 let percentage = p.parse_if_peek::<CalcableValue<Percentage>>()?;
196 if color.is_none() {
197 color = Some(p.parse::<Color>()?);
198 }
199 Ok(Self { color: color.unwrap(), percentage })
200 }
201}
202
203#[cfg(feature = "chromashift")]
204impl HueInterpolationDirection {
205 pub fn to_hue_interpolation(&self) -> chromashift::HueInterpolation {
207 match self {
208 Self::Shorter(_) => chromashift::HueInterpolation::Shorter,
209 Self::Longer(_) => chromashift::HueInterpolation::Longer,
210 Self::Increasing(_) => chromashift::HueInterpolation::Increasing,
211 Self::Decreasing(_) => chromashift::HueInterpolation::Decreasing,
212 }
213 }
214}
215
216#[cfg(feature = "chromashift")]
217impl crate::ToChromashift for ColorMixFunction<'_> {
218 fn to_chromashift(&self) -> Option<chromashift::Color> {
219 use chromashift::{
220 A98Rgb, Channel, DisplayP3, Hsl, Hwb, Lab, Lch, LinearRgb, Oklab, Oklch, PolarLayout, ProphotoRgb, Rec2020,
221 Srgb, XyzD50, XyzD65, mix_channels,
222 };
223
224 fn mix_in<C>(
226 a: &Color<'_>,
227 b: &Color<'_>,
228 percentage: f64,
229 hue: chromashift::HueInterpolation,
230 ) -> Option<chromashift::Color>
231 where
232 C: From<chromashift::Color>
233 + Into<[Channel; 4]>
234 + From<[Channel; 4]>
235 + Into<chromashift::Color>
236 + PolarLayout,
237 {
238 let fa = a.to_mix_channels::<C>()?;
239 let fb = b.to_mix_channels::<C>()?;
240 Some(C::from(mix_channels(fa, fb, percentage, C::HUE_INDEX, hue)).into())
241 }
242
243 let color_space = self.interpolation.as_ref().map(|i| &i.color_space);
244
245 let hue = match color_space {
246 Some(InterpolationColorSpace::Polar(_, Some(him))) => him.direction.to_hue_interpolation(),
247 _ => chromashift::HueInterpolation::Shorter,
248 };
249
250 let parts: std::vec::Vec<(&Color<'_>, Option<f64>)> = (&self.parts)
253 .into_iter()
254 .map(|(p, _)| {
255 let pct = match &p.percentage {
256 None => None,
257 Some(CalcableValue::Literal(pct)) => Some(pct.value() as f64),
258 Some(_) => return None,
259 };
260 Some((&p.color, pct))
261 })
262 .collect::<Option<_>>()?;
263
264 let n = parts.len() as f64;
266 let default_pct = 100.0 / n;
267 let mut stack: std::vec::Vec<(chromashift::Color, f64)> = std::vec::Vec::with_capacity(parts.len());
268 for (color, pct) in &parts {
269 let p = pct.unwrap_or(default_pct);
270 stack.push((color.to_chromashift()?, p));
271 }
272
273 if stack.iter().map(|(_, p)| p).sum::<f64>() == 0.0 {
275 return Some(chromashift::Color::Srgb(chromashift::Srgb::new(0, 0, 0, 0.0)));
276 }
277
278 while stack.len() >= 2 {
280 let (color_b, pct_b) = stack.remove(1);
281 let (color_a, pct_a) = stack.remove(0);
282 let combined = pct_a + pct_b;
283 let progress = pct_b / combined;
284
285 let idx = parts.len() - stack.len() - 2;
287 let ast_a = parts[idx].0;
288 let ast_b = parts[idx + 1].0;
289
290 let mixed = if idx == 0 {
294 let dispatch = |space: &InterpolationColorSpace| match space {
295 InterpolationColorSpace::Rectangular(s) => match s {
296 RectangularColorSpace::Srgb(_) => mix_in::<Srgb>(ast_a, ast_b, progress * 100.0, hue),
297 RectangularColorSpace::SrgbLinear(_) => {
298 mix_in::<LinearRgb>(ast_a, ast_b, progress * 100.0, hue)
299 }
300 RectangularColorSpace::DisplayP3(_) => mix_in::<DisplayP3>(ast_a, ast_b, progress * 100.0, hue),
301 RectangularColorSpace::A98Rgb(_) => mix_in::<A98Rgb>(ast_a, ast_b, progress * 100.0, hue),
302 RectangularColorSpace::ProphotoRgb(_) => {
303 mix_in::<ProphotoRgb>(ast_a, ast_b, progress * 100.0, hue)
304 }
305 RectangularColorSpace::Rec2020(_) => mix_in::<Rec2020>(ast_a, ast_b, progress * 100.0, hue),
306 RectangularColorSpace::Lab(_) => mix_in::<Lab>(ast_a, ast_b, progress * 100.0, hue),
307 RectangularColorSpace::Oklab(_) => mix_in::<Oklab>(ast_a, ast_b, progress * 100.0, hue),
308 RectangularColorSpace::XyzD50(_) => mix_in::<XyzD50>(ast_a, ast_b, progress * 100.0, hue),
309 RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
310 mix_in::<XyzD65>(ast_a, ast_b, progress * 100.0, hue)
311 }
312 },
313 InterpolationColorSpace::Polar(s, _) => match s {
314 PolarColorSpace::Hsl(_) => mix_in::<Hsl>(ast_a, ast_b, progress * 100.0, hue),
315 PolarColorSpace::Hwb(_) => mix_in::<Hwb>(ast_a, ast_b, progress * 100.0, hue),
316 PolarColorSpace::Lch(_) => mix_in::<Lch>(ast_a, ast_b, progress * 100.0, hue),
317 PolarColorSpace::Oklch(_) => mix_in::<Oklch>(ast_a, ast_b, progress * 100.0, hue),
318 },
319 };
320 if let Some(space) = color_space {
322 dispatch(space)
323 } else {
324 mix_in::<Oklab>(ast_a, ast_b, progress * 100.0, hue)
325 }
326 } else {
327 let mix_direct = |space: &InterpolationColorSpace| {
329 let fa: [Channel; 4] = match space {
330 InterpolationColorSpace::Rectangular(s) => match s {
331 RectangularColorSpace::Srgb(_) => Srgb::from(color_a).into(),
332 RectangularColorSpace::SrgbLinear(_) => LinearRgb::from(color_a).into(),
333 RectangularColorSpace::DisplayP3(_) => DisplayP3::from(color_a).into(),
334 RectangularColorSpace::A98Rgb(_) => A98Rgb::from(color_a).into(),
335 RectangularColorSpace::ProphotoRgb(_) => ProphotoRgb::from(color_a).into(),
336 RectangularColorSpace::Rec2020(_) => Rec2020::from(color_a).into(),
337 RectangularColorSpace::Lab(_) => Lab::from(color_a).into(),
338 RectangularColorSpace::Oklab(_) => Oklab::from(color_a).into(),
339 RectangularColorSpace::XyzD50(_) => XyzD50::from(color_a).into(),
340 RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
341 XyzD65::from(color_a).into()
342 }
343 },
344 InterpolationColorSpace::Polar(s, _) => match s {
345 PolarColorSpace::Hsl(_) => Hsl::from(color_a).into(),
346 PolarColorSpace::Hwb(_) => Hwb::from(color_a).into(),
347 PolarColorSpace::Lch(_) => Lch::from(color_a).into(),
348 PolarColorSpace::Oklch(_) => Oklch::from(color_a).into(),
349 },
350 };
351 let fb: [Channel; 4] = match space {
352 InterpolationColorSpace::Rectangular(s) => match s {
353 RectangularColorSpace::Srgb(_) => Srgb::from(color_b).into(),
354 RectangularColorSpace::SrgbLinear(_) => LinearRgb::from(color_b).into(),
355 RectangularColorSpace::DisplayP3(_) => DisplayP3::from(color_b).into(),
356 RectangularColorSpace::A98Rgb(_) => A98Rgb::from(color_b).into(),
357 RectangularColorSpace::ProphotoRgb(_) => ProphotoRgb::from(color_b).into(),
358 RectangularColorSpace::Rec2020(_) => Rec2020::from(color_b).into(),
359 RectangularColorSpace::Lab(_) => Lab::from(color_b).into(),
360 RectangularColorSpace::Oklab(_) => Oklab::from(color_b).into(),
361 RectangularColorSpace::XyzD50(_) => XyzD50::from(color_b).into(),
362 RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
363 XyzD65::from(color_b).into()
364 }
365 },
366 InterpolationColorSpace::Polar(s, _) => match s {
367 PolarColorSpace::Hsl(_) => Hsl::from(color_b).into(),
368 PolarColorSpace::Hwb(_) => Hwb::from(color_b).into(),
369 PolarColorSpace::Lch(_) => Lch::from(color_b).into(),
370 PolarColorSpace::Oklch(_) => Oklch::from(color_b).into(),
371 },
372 };
373 Some(match space {
374 InterpolationColorSpace::Rectangular(s) => match s {
375 RectangularColorSpace::Srgb(_) => {
376 Srgb::from(mix_channels(fa, fb, progress * 100.0, Srgb::HUE_INDEX, hue)).into()
377 }
378 RectangularColorSpace::SrgbLinear(_) => {
379 LinearRgb::from(mix_channels(fa, fb, progress * 100.0, LinearRgb::HUE_INDEX, hue))
380 .into()
381 }
382 RectangularColorSpace::DisplayP3(_) => {
383 DisplayP3::from(mix_channels(fa, fb, progress * 100.0, DisplayP3::HUE_INDEX, hue))
384 .into()
385 }
386 RectangularColorSpace::A98Rgb(_) => {
387 A98Rgb::from(mix_channels(fa, fb, progress * 100.0, A98Rgb::HUE_INDEX, hue)).into()
388 }
389 RectangularColorSpace::ProphotoRgb(_) => {
390 ProphotoRgb::from(mix_channels(fa, fb, progress * 100.0, ProphotoRgb::HUE_INDEX, hue))
391 .into()
392 }
393 RectangularColorSpace::Rec2020(_) => {
394 Rec2020::from(mix_channels(fa, fb, progress * 100.0, Rec2020::HUE_INDEX, hue)).into()
395 }
396 RectangularColorSpace::Lab(_) => {
397 Lab::from(mix_channels(fa, fb, progress * 100.0, Lab::HUE_INDEX, hue)).into()
398 }
399 RectangularColorSpace::Oklab(_) => {
400 Oklab::from(mix_channels(fa, fb, progress * 100.0, Oklab::HUE_INDEX, hue)).into()
401 }
402 RectangularColorSpace::XyzD50(_) => {
403 XyzD50::from(mix_channels(fa, fb, progress * 100.0, XyzD50::HUE_INDEX, hue)).into()
404 }
405 RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
406 XyzD65::from(mix_channels(fa, fb, progress * 100.0, XyzD65::HUE_INDEX, hue)).into()
407 }
408 },
409 InterpolationColorSpace::Polar(s, _) => match s {
410 PolarColorSpace::Hsl(_) => {
411 Hsl::from(mix_channels(fa, fb, progress * 100.0, Hsl::HUE_INDEX, hue)).into()
412 }
413 PolarColorSpace::Hwb(_) => {
414 Hwb::from(mix_channels(fa, fb, progress * 100.0, Hwb::HUE_INDEX, hue)).into()
415 }
416 PolarColorSpace::Lch(_) => {
417 Lch::from(mix_channels(fa, fb, progress * 100.0, Lch::HUE_INDEX, hue)).into()
418 }
419 PolarColorSpace::Oklch(_) => {
420 Oklch::from(mix_channels(fa, fb, progress * 100.0, Oklch::HUE_INDEX, hue)).into()
421 }
422 },
423 })
424 };
425 if let Some(space) = color_space {
426 mix_direct(space)
427 } else {
428 let fa: [Channel; 4] = Oklab::from(color_a).into();
429 let fb: [Channel; 4] = Oklab::from(color_b).into();
430 Some(Oklab::from(mix_channels(fa, fb, progress * 100.0, Oklab::HUE_INDEX, hue)).into())
431 }
432 }?;
433
434 stack.insert(0, (mixed, combined));
435 }
436
437 stack.into_iter().next().map(|(c, _)| c)
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::CssAtomSet;
445 use css_parse::{assert_parse, assert_parse_error};
446
447 #[test]
448 fn test_writes() {
449 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red,blue)");
450 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 50%,blue 50%)");
451 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklch,red,blue)");
452 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklch longer hue,red,blue)");
453 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl shorter hue,red,blue)");
454 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl increasing hue,red,blue)");
455 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl decreasing hue,red,blue)");
456 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in lab,rgb(255 0 0),rgb(0 0 255))");
457 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,50% red,blue)");
458 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 50%,blue)");
459 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklab,#fff 30%,#000 70%)");
460 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in xyz-d50,red,green)");
461 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in xyz-d65,red,green)");
462 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb-linear,red,green)");
463 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue)");
464 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red 50%,blue 50%)");
465 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklab,red,blue,green)");
466 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 33%,blue 33%,green 34%)");
467 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue,green)");
468 }
469
470 #[test]
471 #[cfg(feature = "visitable")]
472 fn test_visits() {
473 use crate::assert_visits;
474 assert_visits!("color-mix(in srgb, red, blue)", ColorMixFunction, ColorInterpolationMethod, Color, Color,);
476 assert_visits!(
478 "color-mix(in srgb, rgb(255, 0, 0), blue)",
479 ColorMixFunction,
480 ColorInterpolationMethod,
481 Color,
482 ColorFunction,
483 RgbFunction,
484 Color,
485 );
486 assert_visits!(
488 "color-mix(in srgb, red 50%, blue 50%)",
489 ColorMixFunction,
490 ColorInterpolationMethod,
491 Color,
492 Percentage,
493 Color,
494 Percentage,
495 );
496 assert_visits!(
498 "color-mix(in oklch shorter hue, red, blue)",
499 ColorMixFunction,
500 ColorInterpolationMethod,
501 Color,
502 Color,
503 );
504 assert_visits!("color-mix(red, blue)", ColorMixFunction, Color, Color,);
505 assert_visits!(
506 "color-mix(in oklab, red, blue, green)",
507 ColorMixFunction,
508 ColorInterpolationMethod,
509 Color,
510 Color,
511 Color,
512 );
513 }
514
515 #[test]
516 fn test_errors() {
517 assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(srgb,red,blue)");
518 }
519
520 #[test]
521 fn substitution_in_percentage() {
522 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red calc(50%),blue)");
523 assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red var(--p),blue)");
524 }
525}