Skip to main content

css_ast/functions/
color_mix_function.rs

1use super::prelude::*;
2use crate::{CalcableValue, Percentage};
3
4/// <https://drafts.csswg.org/css-color-5/#color-mix>
5///
6/// ```text,ignore
7/// color-mix() = color-mix( <color-interpolation-method>? , [ <color> && <percentage [0,100]>? ]# )
8/// ```
9#[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/// <https://drafts.csswg.org/css-color-4/#color-interpolation-method>
30///
31/// ```text,ignore
32/// <color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]
33/// ```
34#[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/// The color space for color interpolation, which can be rectangular or polar.
46///
47/// ```text,ignore
48/// <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>?
49/// ```
50#[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/// <https://drafts.csswg.org/css-color-4/#typedef-rectangular-color-space>
61///
62/// ```text,ignore
63/// <rectangular-color-space> = srgb | srgb-linear | display-p3 | a98-rgb |
64///     prophoto-rgb | rec2020 | lab | oklab | xyz | xyz-d50 | xyz-d65
65/// ```
66#[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/// <https://drafts.csswg.org/css-color-4/#typedef-polar-color-space>
99///
100/// ```text,ignore
101/// <polar-color-space> = hsl | hwb | lch | oklch
102/// ```
103#[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/// <https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method>
122///
123/// ```text,ignore
124/// <hue-interpolation-method> = [ shorter | longer | increasing | decreasing ] hue
125/// ```
126#[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/// The direction keyword for hue interpolation.
138///
139/// ```text,ignore
140/// shorter | longer | increasing | decreasing
141/// ```
142#[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/// A color with an optional percentage in a color-mix() function.
161///
162/// ```text,ignore
163/// [ <color> && <percentage [0,100]>? ]
164/// ```
165///
166/// The color and percentage can appear in either order.
167#[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		// Either order: <color> <percentage>? or <percentage> <color>
195		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	/// Converts this AST node to the corresponding chromashift hue interpolation direction.
207	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	/// Converts this AST node to the chromashift space it selects, with the hue interpolation it selects for polar spaces.
220	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		// Named colors
331		assert_visits!("color-mix(in srgb, red, blue)", ColorMixFunction, ColorInterpolationMethod, Color, Color,);
332		// Function colors recurse into ColorFunction and its variant
333		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		// Percentages are visited
343		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		// Polar color space with hue interpolation
353		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}