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