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: 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#[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#[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#[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#[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#[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#[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#[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 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 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 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 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 let sum = p1 + p2;
273 if sum == 0.0 {
274 return None;
275 }
276 let p1 = p1 / sum * 100.0;
277
278 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 assert_visits!("color-mix(in srgb, red, blue)", ColorMixFunction, ColorInterpolationMethod, Color, Color,);
327 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 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 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 assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue)");
361 assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(srgb,red,blue)");
363 assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red)");
365 }
366}