css_ast/functions/
color_mix_function.rs

1use super::prelude::*;
2use crate::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]>? ]#{2} )
8/// ```
9#[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: ColorInterpolationMethod,
18	#[cfg_attr(feature = "visitable", visit(skip))]
19	pub comma: T![,],
20	pub first: ColorMixPart<'a>,
21	#[cfg_attr(feature = "visitable", visit(skip))]
22	pub comma2: T![,],
23	pub second: ColorMixPart<'a>,
24	#[cfg_attr(feature = "visitable", visit(skip))]
25	pub close: T![')'],
26}
27
28/// <https://drafts.csswg.org/css-color-4/#color-interpolation-method>
29///
30/// ```text,ignore
31/// <color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]
32/// ```
33#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
35#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
36#[derive(csskit_derives::NodeWithMetadata)]
37pub struct ColorInterpolationMethod {
38	#[atom(CssAtomSet::In)]
39	pub in_keyword: T![Ident],
40	pub color_space: InterpolationColorSpace,
41}
42
43/// The color space for color interpolation, which can be rectangular or polar.
44///
45/// ```text,ignore
46/// <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>?
47/// ```
48#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
50#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
51#[derive(csskit_derives::NodeWithMetadata)]
52pub enum InterpolationColorSpace {
53	Rectangular(RectangularColorSpace),
54	Polar(PolarColorSpace, Option<HueInterpolationMethod>),
55}
56
57/// <https://drafts.csswg.org/css-color-4/#typedef-rectangular-color-space>
58///
59/// ```text,ignore
60/// <rectangular-color-space> = srgb | srgb-linear | display-p3 | a98-rgb |
61///     prophoto-rgb | rec2020 | lab | oklab | xyz | xyz-d50 | xyz-d65
62/// ```
63#[derive(Parse, Peek, IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
65#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
66#[derive(csskit_derives::NodeWithMetadata)]
67pub enum RectangularColorSpace {
68	#[atom(CssAtomSet::Srgb)]
69	Srgb(T![Ident]),
70	#[atom(CssAtomSet::SrgbLinear)]
71	SrgbLinear(T![Ident]),
72	#[atom(CssAtomSet::DisplayP3)]
73	DisplayP3(T![Ident]),
74	#[atom(CssAtomSet::A98Rgb)]
75	A98Rgb(T![Ident]),
76	#[atom(CssAtomSet::ProphotoRgb)]
77	ProphotoRgb(T![Ident]),
78	#[atom(CssAtomSet::Rec2020)]
79	Rec2020(T![Ident]),
80	#[atom(CssAtomSet::Lab)]
81	Lab(T![Ident]),
82	#[atom(CssAtomSet::Oklab)]
83	Oklab(T![Ident]),
84	#[atom(CssAtomSet::Xyz)]
85	Xyz(T![Ident]),
86	#[atom(CssAtomSet::XyzD50)]
87	XyzD50(T![Ident]),
88	#[atom(CssAtomSet::XyzD65)]
89	XyzD65(T![Ident]),
90}
91
92/// <https://drafts.csswg.org/css-color-4/#typedef-polar-color-space>
93///
94/// ```text,ignore
95/// <polar-color-space> = hsl | hwb | lch | oklch
96/// ```
97#[derive(Parse, Peek, IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
99#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
100#[derive(csskit_derives::NodeWithMetadata)]
101pub enum PolarColorSpace {
102	#[atom(CssAtomSet::Hsl)]
103	Hsl(T![Ident]),
104	#[atom(CssAtomSet::Hwb)]
105	Hwb(T![Ident]),
106	#[atom(CssAtomSet::Lch)]
107	Lch(T![Ident]),
108	#[atom(CssAtomSet::Oklch)]
109	Oklch(T![Ident]),
110}
111
112/// <https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method>
113///
114/// ```text,ignore
115/// <hue-interpolation-method> = [ shorter | longer | increasing | decreasing ] hue
116/// ```
117#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
119#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
120#[derive(csskit_derives::NodeWithMetadata)]
121pub struct HueInterpolationMethod {
122	pub direction: HueInterpolationDirection,
123	#[atom(CssAtomSet::Hue)]
124	pub hue_keyword: T![Ident],
125}
126
127/// The direction keyword for hue interpolation.
128///
129/// ```text,ignore
130/// shorter | longer | increasing | decreasing
131/// ```
132#[derive(Parse, Peek, IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
134#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
135#[derive(csskit_derives::NodeWithMetadata)]
136pub enum HueInterpolationDirection {
137	#[atom(CssAtomSet::Shorter)]
138	Shorter(T![Ident]),
139	#[atom(CssAtomSet::Longer)]
140	Longer(T![Ident]),
141	#[atom(CssAtomSet::Increasing)]
142	Increasing(T![Ident]),
143	#[atom(CssAtomSet::Decreasing)]
144	Decreasing(T![Ident]),
145}
146
147/// A color with an optional percentage in a color-mix() function.
148///
149/// ```text,ignore
150/// [ <color> && <percentage [0,100]>? ]
151/// ```
152///
153/// The color and percentage can appear in either order.
154#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
156#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(children))]
157#[derive(csskit_derives::NodeWithMetadata)]
158pub struct ColorMixPart<'a> {
159	pub color: Color<'a>,
160	pub percentage: Option<Percentage>,
161}
162
163impl<'a> Peek<'a> for ColorMixPart<'a> {
164	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
165	where
166		I: Iterator<Item = Cursor> + Clone,
167	{
168		Color::peek(p, c) || Percentage::peek(p, c)
169	}
170}
171
172impl<'a> Parse<'a> for ColorMixPart<'a> {
173	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
174	where
175		I: Iterator<Item = Cursor> + Clone,
176	{
177		// Either order: <color> <percentage>? or <percentage> <color>
178		let mut color = p.parse_if_peek::<Color>()?;
179		let percentage = p.parse_if_peek::<Percentage>()?;
180		if color.is_none() {
181			color = Some(p.parse::<Color>()?);
182		}
183		Ok(Self { color: color.unwrap(), percentage })
184	}
185}
186
187#[cfg(feature = "chromashift")]
188impl HueInterpolationDirection {
189	/// Converts this AST node to the corresponding chromashift hue interpolation direction.
190	pub fn to_hue_interpolation(&self) -> chromashift::HueInterpolation {
191		match self {
192			Self::Shorter(_) => chromashift::HueInterpolation::Shorter,
193			Self::Longer(_) => chromashift::HueInterpolation::Longer,
194			Self::Increasing(_) => chromashift::HueInterpolation::Increasing,
195			Self::Decreasing(_) => chromashift::HueInterpolation::Decreasing,
196		}
197	}
198}
199
200#[cfg(feature = "chromashift")]
201impl InterpolationColorSpace {
202	/// Mixes two colours in this interpolation colour space.
203	///
204	/// `percentage` is how much of the second colour to use (0.0 = all first, 100.0 = all second).
205	pub fn mix(&self, first: chromashift::Color, second: chromashift::Color, percentage: f64) -> chromashift::Color {
206		use chromashift::{
207			A98Rgb, ColorMix, ColorMixPolar, DisplayP3, Hsl, Hwb, Lab, Lch, LinearRgb, Oklab, Oklch, ProphotoRgb,
208			Rec2020, Srgb, XyzD50, XyzD65,
209		};
210		match self {
211			Self::Rectangular(space) => match space {
212				RectangularColorSpace::Srgb(_) => chromashift::Color::Srgb(Srgb::mix(first, second, percentage)),
213				RectangularColorSpace::SrgbLinear(_) => {
214					chromashift::Color::LinearRgb(LinearRgb::mix(first, second, percentage))
215				}
216				RectangularColorSpace::DisplayP3(_) => {
217					chromashift::Color::DisplayP3(DisplayP3::mix(first, second, percentage))
218				}
219				RectangularColorSpace::A98Rgb(_) => chromashift::Color::A98Rgb(A98Rgb::mix(first, second, percentage)),
220				RectangularColorSpace::ProphotoRgb(_) => {
221					chromashift::Color::ProphotoRgb(ProphotoRgb::mix(first, second, percentage))
222				}
223				RectangularColorSpace::Rec2020(_) => {
224					chromashift::Color::Rec2020(Rec2020::mix(first, second, percentage))
225				}
226				RectangularColorSpace::Lab(_) => chromashift::Color::Lab(Lab::mix(first, second, percentage)),
227				RectangularColorSpace::Oklab(_) => chromashift::Color::Oklab(Oklab::mix(first, second, percentage)),
228				RectangularColorSpace::XyzD50(_) => chromashift::Color::XyzD50(XyzD50::mix(first, second, percentage)),
229				RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
230					chromashift::Color::XyzD65(XyzD65::mix(first, second, percentage))
231				}
232			},
233			Self::Polar(space, hue_method) => {
234				let dir = match hue_method {
235					None => chromashift::HueInterpolation::Shorter,
236					Some(him) => him.direction.to_hue_interpolation(),
237				};
238				match space {
239					PolarColorSpace::Hsl(_) => chromashift::Color::Hsl(Hsl::mix_polar(first, second, percentage, dir)),
240					PolarColorSpace::Hwb(_) => chromashift::Color::Hwb(Hwb::mix_polar(first, second, percentage, dir)),
241					PolarColorSpace::Lch(_) => chromashift::Color::Lch(Lch::mix_polar(first, second, percentage, dir)),
242					PolarColorSpace::Oklch(_) => {
243						chromashift::Color::Oklch(Oklch::mix_polar(first, second, percentage, dir))
244					}
245				}
246			}
247		}
248	}
249}
250
251#[cfg(feature = "chromashift")]
252impl crate::ToChromashift for ColorMixFunction<'_> {
253	fn to_chromashift(&self) -> Option<chromashift::Color> {
254		let first_color = self.first.color.to_chromashift()?;
255		let second_color = self.second.color.to_chromashift()?;
256
257		// Resolve percentages per the spec:
258		// - If both omitted: 50% / 50%
259		// - If one omitted: other = 100% - given
260		// - If both given: use as-is (may need normalization if they don't sum to 100%)
261		let p1 = self.first.percentage.as_ref().map(|p| p.value() as f64);
262		let p2 = self.second.percentage.as_ref().map(|p| p.value() as f64);
263
264		let (p1, p2) = match (p1, p2) {
265			(None, None) => (50.0, 50.0),
266			(Some(a), None) => (a, 100.0 - a),
267			(None, Some(b)) => (100.0 - b, b),
268			(Some(a), Some(b)) => (a, b),
269		};
270
271		// Normalize so that p1 + p2 = 100
272		let sum = p1 + p2;
273		if sum == 0.0 {
274			return None;
275		}
276		let p1 = p1 / sum * 100.0;
277
278		// The percentage for mixing is "how much of the second color"
279		let mix_percentage = 100.0 - p1;
280
281		Some(self.interpolation.color_space.mix(first_color, second_color, mix_percentage))
282	}
283}
284
285#[cfg(test)]
286mod tests {
287	use super::*;
288	use crate::CssAtomSet;
289	use css_parse::{assert_parse, assert_parse_error};
290
291	#[test]
292	fn size_test() {
293		assert_eq!(std::mem::size_of::<ColorMixFunction>(), 424);
294		assert_eq!(std::mem::size_of::<ColorInterpolationMethod>(), 56);
295		assert_eq!(std::mem::size_of::<InterpolationColorSpace>(), 44);
296		assert_eq!(std::mem::size_of::<RectangularColorSpace>(), 16);
297		assert_eq!(std::mem::size_of::<PolarColorSpace>(), 16);
298		assert_eq!(std::mem::size_of::<HueInterpolationMethod>(), 28);
299		assert_eq!(std::mem::size_of::<HueInterpolationDirection>(), 16);
300		assert_eq!(std::mem::size_of::<ColorMixPart>(), 160);
301	}
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	}
320
321	#[test]
322	#[cfg(feature = "visitable")]
323	fn test_visits() {
324		use crate::assert_visits;
325		// Named colors
326		assert_visits!("color-mix(in srgb, red, blue)", ColorMixFunction, ColorInterpolationMethod, Color, Color,);
327		// Function colors recurse into ColorFunction and its variant
328		assert_visits!(
329			"color-mix(in srgb, rgb(255, 0, 0), blue)",
330			ColorMixFunction,
331			ColorInterpolationMethod,
332			Color,
333			ColorFunction,
334			RgbFunction,
335			Color,
336		);
337		// Percentages are visited
338		assert_visits!(
339			"color-mix(in srgb, red 50%, blue 50%)",
340			ColorMixFunction,
341			ColorInterpolationMethod,
342			Color,
343			Percentage,
344			Color,
345			Percentage,
346		);
347		// Polar color space with hue interpolation
348		assert_visits!(
349			"color-mix(in oklch shorter hue, red, blue)",
350			ColorMixFunction,
351			ColorInterpolationMethod,
352			Color,
353			Color,
354		);
355	}
356
357	#[test]
358	fn test_errors() {
359		// Missing interpolation method
360		assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue)");
361		// Missing "in" keyword
362		assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(srgb,red,blue)");
363		// Missing second color
364		assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red)");
365	}
366}