alchemy_styles/
color.rs

1//! Implements `Color`. Heavily based on the `Color` module in Servo's CSS parser, but tweaked
2//! for (what I believe) is a friendlier API, and to separate out the parsing into a separate
3//! module.
4
5#[cfg(feature="parser")]
6use std::{fmt, f32::consts::PI};
7
8#[cfg(feature="parser")]
9use cssparser::{BasicParseError, ParseError, Parser, ToCss, Token};
10
11/// A color with red, green, blue, and alpha components, in a byte each.
12#[derive(Clone, Copy, PartialEq, Debug)]
13pub struct Color {
14    /// The red component.
15    pub red: u8,
16    /// The green component.
17    pub green: u8,
18    /// The blue component.
19    pub blue: u8,
20    /// The alpha component.
21    pub alpha: u8,
22}
23
24impl Default for Color {
25    fn default() -> Color {
26        Color { red: 0, green: 0, blue: 0, alpha: 0 }
27    }
28}
29
30impl Color {
31    /// Constructs a new Color value from float components. It expects the red,
32    /// green, blue and alpha channels in that order, and all values will be
33    /// clamped to the 0.0 ... 1.0 range.
34    #[inline]
35    pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
36        Self::new(
37            clamp_unit_f32(red),
38            clamp_unit_f32(green),
39            clamp_unit_f32(blue),
40            clamp_unit_f32(alpha),
41        )
42    }
43
44    /// Returns a transparent color.
45    #[inline]
46    pub fn transparent() -> Self {
47        Self::new(0, 0, 0, 0)
48    }
49
50    /// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
51    #[inline]
52    pub fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
53        Color {
54            red: red,
55            green: green,
56            blue: blue,
57            alpha: alpha,
58        }
59    }
60
61    /// Returns the red channel in a floating point number form, from 0 to 1.
62    #[inline]
63    pub fn red_f32(&self) -> f32 {
64        self.red as f32 / 255.0
65    }
66
67    /// Returns the green channel in a floating point number form, from 0 to 1.
68    #[inline]
69    pub fn green_f32(&self) -> f32 {
70        self.green as f32 / 255.0
71    }
72
73    /// Returns the blue channel in a floating point number form, from 0 to 1.
74    #[inline]
75    pub fn blue_f32(&self) -> f32 {
76        self.blue as f32 / 255.0
77    }
78
79    /// Returns the alpha channel in a floating point number form, from 0 to 1.
80    #[inline]
81    pub fn alpha_f32(&self) -> f32 {
82        self.alpha as f32 / 255.0
83    }
84    
85    /// Parse a <color> value, per CSS Color Module Level 3.
86    ///
87    /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet.
88    #[cfg(feature="parser")]
89    pub fn parse_with<'i, 't, ComponentParser>(
90        component_parser: &ComponentParser,
91        input: &mut Parser<'i, 't>,
92    ) -> Result<Color, ParseError<'i, ComponentParser::Error>>
93    where
94        ComponentParser: ColorComponentParser<'i>,
95    {
96        // FIXME: remove clone() when lifetimes are non-lexical
97        let location = input.current_source_location();
98        let token = input.next()?.clone();
99        match token {
100            Token::Hash(ref value) | Token::IDHash(ref value) => {
101                Color::parse_hash(value.as_bytes())
102            }
103            Token::Ident(ref value) => parse_color_keyword(&*value),
104            Token::Function(ref name) => {
105                return input.parse_nested_block(|arguments| {
106                    parse_color_function(component_parser, &*name, arguments)
107                })
108            }
109            _ => Err(()),
110        }
111        .map_err(|()| location.new_unexpected_token_error(token))
112    }
113
114    /// Parse a <color> value, per CSS Color Module Level 3.
115    #[cfg(feature="parser")]
116    pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Color, BasicParseError<'i>> {
117        let component_parser = DefaultComponentParser;
118        Self::parse_with(&component_parser, input).map_err(ParseError::basic)
119    }
120
121    /// Parse a color hash, without the leading '#' character.
122    #[cfg(feature="parser")]
123    #[inline]
124    pub fn parse_hash(value: &[u8]) -> Result<Self, ()> {
125        match value.len() {
126            8 => Ok(rgba(
127                from_hex(value[0])? * 16 + from_hex(value[1])?,
128                from_hex(value[2])? * 16 + from_hex(value[3])?,
129                from_hex(value[4])? * 16 + from_hex(value[5])?,
130                from_hex(value[6])? * 16 + from_hex(value[7])?,
131            )),
132            6 => Ok(rgb(
133                from_hex(value[0])? * 16 + from_hex(value[1])?,
134                from_hex(value[2])? * 16 + from_hex(value[3])?,
135                from_hex(value[4])? * 16 + from_hex(value[5])?,
136            )),
137            4 => Ok(rgba(
138                from_hex(value[0])? * 17,
139                from_hex(value[1])? * 17,
140                from_hex(value[2])? * 17,
141                from_hex(value[3])? * 17,
142            )),
143            3 => Ok(rgb(
144                from_hex(value[0])? * 17,
145                from_hex(value[1])? * 17,
146                from_hex(value[2])? * 17,
147            )),
148            _ => Err(()),
149        }
150    }
151
152}
153
154#[cfg(feature="parser")]
155impl ToCss for Color {
156    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
157    where
158        W: fmt::Write,
159    {
160        let serialize_alpha = self.alpha != 255;
161
162        dest.write_str(if serialize_alpha { "rgba(" } else { "rgb(" })?;
163        self.red.to_css(dest)?;
164        dest.write_str(", ")?;
165        self.green.to_css(dest)?;
166        dest.write_str(", ")?;
167        self.blue.to_css(dest)?;
168        if serialize_alpha {
169            dest.write_str(", ")?;
170
171            // Try first with two decimal places, then with three.
172            let mut rounded_alpha = (self.alpha_f32() * 100.).round() / 100.;
173            if clamp_unit_f32(rounded_alpha) != self.alpha {
174                rounded_alpha = (self.alpha_f32() * 1000.).round() / 1000.;
175            }
176
177            rounded_alpha.to_css(dest)?;
178        }
179        dest.write_char(')')
180    }
181}
182
183/// Either a number or a percentage.
184#[cfg(feature="parser")]
185pub enum NumberOrPercentage {
186    /// `<number>`.
187    Number {
188        /// The numeric value parsed, as a float.
189        value: f32,
190    },
191    /// `<percentage>`
192    Percentage {
193        /// The value as a float, divided by 100 so that the nominal range is
194        /// 0.0 to 1.0.
195        unit_value: f32,
196    },
197}
198
199#[cfg(feature="parser")]
200impl NumberOrPercentage {
201    fn unit_value(&self) -> f32 {
202        match *self {
203            NumberOrPercentage::Number { value } => value,
204            NumberOrPercentage::Percentage { unit_value } => unit_value,
205        }
206    }
207}
208
209/// Either an angle or a number.
210#[cfg(feature="parser")]
211pub enum AngleOrNumber {
212    /// `<number>`.
213    Number {
214        /// The numeric value parsed, as a float.
215        value: f32,
216    },
217    /// `<angle>`
218    Angle {
219        /// The value as a number of degrees.
220        degrees: f32,
221    },
222}
223
224#[cfg(feature="parser")]
225impl AngleOrNumber {
226    fn degrees(&self) -> f32 {
227        match *self {
228            AngleOrNumber::Number { value } => value,
229            AngleOrNumber::Angle { degrees } => degrees,
230        }
231    }
232}
233
234/// A trait that can be used to hook into how `cssparser` parses color
235/// components, with the intention of implementing more complicated behavior.
236///
237/// For example, this is used by Servo to support calc() in color.
238#[cfg(feature="parser")]
239pub trait ColorComponentParser<'i> {
240    /// A custom error type that can be returned from the parsing functions.
241    type Error: 'i;
242
243    /// Parse an `<angle>` or `<number>`.
244    ///
245    /// Returns the result in degrees.
246    fn parse_angle_or_number<'t>(
247        &self,
248        input: &mut Parser<'i, 't>,
249    ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
250        let location = input.current_source_location();
251        Ok(match *input.next()? {
252            Token::Number { value, .. } => AngleOrNumber::Number { value },
253            Token::Dimension {
254                value: v, ref unit, ..
255            } => {
256                let degrees = match_ignore_ascii_case! { &*unit,
257                    "deg" => v,
258                    "grad" => v * 360. / 400.,
259                    "rad" => v * 360. / (2. * PI),
260                    "turn" => v * 360.,
261                    _ => return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))),
262                };
263
264                AngleOrNumber::Angle { degrees }
265            }
266            ref t => return Err(location.new_unexpected_token_error(t.clone())),
267        })
268    }
269
270    /// Parse a `<percentage>` value.
271    ///
272    /// Returns the result in a number from 0.0 to 1.0.
273    fn parse_percentage<'t>(
274        &self,
275        input: &mut Parser<'i, 't>,
276    ) -> Result<f32, ParseError<'i, Self::Error>> {
277        input.expect_percentage().map_err(From::from)
278    }
279
280    /// Parse a `<number>` value.
281    fn parse_number<'t>(
282        &self,
283        input: &mut Parser<'i, 't>,
284    ) -> Result<f32, ParseError<'i, Self::Error>> {
285        input.expect_number().map_err(From::from)
286    }
287
288    /// Parse a `<number>` value or a `<percentage>` value.
289    fn parse_number_or_percentage<'t>(
290        &self,
291        input: &mut Parser<'i, 't>,
292    ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
293        let location = input.current_source_location();
294        Ok(match *input.next()? {
295            Token::Number { value, .. } => NumberOrPercentage::Number { value },
296            Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value },
297            ref t => return Err(location.new_unexpected_token_error(t.clone())),
298        })
299    }
300}
301
302#[cfg(feature="parser")]
303struct DefaultComponentParser;
304
305#[cfg(feature="parser")]
306impl<'i> ColorComponentParser<'i> for DefaultComponentParser {
307    type Error = ();
308}
309
310#[cfg(feature="parser")]
311#[inline]
312fn rgb(red: u8, green: u8, blue: u8) -> Color {
313    rgba(red, green, blue, 255)
314}
315
316#[cfg(feature="parser")]
317#[inline]
318fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
319    Color::new(red, green, blue, alpha)
320}
321
322/// Return the named color with the given name.
323///
324/// Matching is case-insensitive in the ASCII range.
325/// CSS escaping (if relevant) should be resolved before calling this function.
326/// (For example, the value of an `Ident` token is fine.)
327#[cfg(feature="parser")]
328#[inline]
329pub fn parse_color_keyword(ident: &str) -> Result<Color, ()> {
330    macro_rules! rgb {
331        ($red: expr, $green: expr, $blue: expr) => {
332            Color {
333                red: $red,
334                green: $green,
335                blue: $blue,
336                alpha: 255,
337            }
338        };
339    }
340
341    ascii_case_insensitive_phf_map! {
342        keyword -> Color = {
343            "black" => rgb!(0, 0, 0),
344            "silver" => rgb!(192, 192, 192),
345            "gray" => rgb!(128, 128, 128),
346            "white" => rgb!(255, 255, 255),
347            "maroon" => rgb!(128, 0, 0),
348            "red" => rgb!(255, 0, 0),
349            "purple" => rgb!(128, 0, 128),
350            "fuchsia" => rgb!(255, 0, 255),
351            "green" => rgb!(0, 128, 0),
352            "lime" => rgb!(0, 255, 0),
353            "olive" => rgb!(128, 128, 0),
354            "yellow" => rgb!(255, 255, 0),
355            "navy" => rgb!(0, 0, 128),
356            "blue" => rgb!(0, 0, 255),
357            "teal" => rgb!(0, 128, 128),
358            "aqua" => rgb!(0, 255, 255),
359
360            "aliceblue" => rgb!(240, 248, 255),
361            "antiquewhite" => rgb!(250, 235, 215),
362            "aquamarine" => rgb!(127, 255, 212),
363            "azure" => rgb!(240, 255, 255),
364            "beige" => rgb!(245, 245, 220),
365            "bisque" => rgb!(255, 228, 196),
366            "blanchedalmond" => rgb!(255, 235, 205),
367            "blueviolet" => rgb!(138, 43, 226),
368            "brown" => rgb!(165, 42, 42),
369            "burlywood" => rgb!(222, 184, 135),
370            "cadetblue" => rgb!(95, 158, 160),
371            "chartreuse" => rgb!(127, 255, 0),
372            "chocolate" => rgb!(210, 105, 30),
373            "coral" => rgb!(255, 127, 80),
374            "cornflowerblue" => rgb!(100, 149, 237),
375            "cornsilk" => rgb!(255, 248, 220),
376            "crimson" => rgb!(220, 20, 60),
377            "cyan" => rgb!(0, 255, 255),
378            "darkblue" => rgb!(0, 0, 139),
379            "darkcyan" => rgb!(0, 139, 139),
380            "darkgoldenrod" => rgb!(184, 134, 11),
381            "darkgray" => rgb!(169, 169, 169),
382            "darkgreen" => rgb!(0, 100, 0),
383            "darkgrey" => rgb!(169, 169, 169),
384            "darkkhaki" => rgb!(189, 183, 107),
385            "darkmagenta" => rgb!(139, 0, 139),
386            "darkolivegreen" => rgb!(85, 107, 47),
387            "darkorange" => rgb!(255, 140, 0),
388            "darkorchid" => rgb!(153, 50, 204),
389            "darkred" => rgb!(139, 0, 0),
390            "darksalmon" => rgb!(233, 150, 122),
391            "darkseagreen" => rgb!(143, 188, 143),
392            "darkslateblue" => rgb!(72, 61, 139),
393            "darkslategray" => rgb!(47, 79, 79),
394            "darkslategrey" => rgb!(47, 79, 79),
395            "darkturquoise" => rgb!(0, 206, 209),
396            "darkviolet" => rgb!(148, 0, 211),
397            "deeppink" => rgb!(255, 20, 147),
398            "deepskyblue" => rgb!(0, 191, 255),
399            "dimgray" => rgb!(105, 105, 105),
400            "dimgrey" => rgb!(105, 105, 105),
401            "dodgerblue" => rgb!(30, 144, 255),
402            "firebrick" => rgb!(178, 34, 34),
403            "floralwhite" => rgb!(255, 250, 240),
404            "forestgreen" => rgb!(34, 139, 34),
405            "gainsboro" => rgb!(220, 220, 220),
406            "ghostwhite" => rgb!(248, 248, 255),
407            "gold" => rgb!(255, 215, 0),
408            "goldenrod" => rgb!(218, 165, 32),
409            "greenyellow" => rgb!(173, 255, 47),
410            "grey" => rgb!(128, 128, 128),
411            "honeydew" => rgb!(240, 255, 240),
412            "hotpink" => rgb!(255, 105, 180),
413            "indianred" => rgb!(205, 92, 92),
414            "indigo" => rgb!(75, 0, 130),
415            "ivory" => rgb!(255, 255, 240),
416            "khaki" => rgb!(240, 230, 140),
417            "lavender" => rgb!(230, 230, 250),
418            "lavenderblush" => rgb!(255, 240, 245),
419            "lawngreen" => rgb!(124, 252, 0),
420            "lemonchiffon" => rgb!(255, 250, 205),
421            "lightblue" => rgb!(173, 216, 230),
422            "lightcoral" => rgb!(240, 128, 128),
423            "lightcyan" => rgb!(224, 255, 255),
424            "lightgoldenrodyellow" => rgb!(250, 250, 210),
425            "lightgray" => rgb!(211, 211, 211),
426            "lightgreen" => rgb!(144, 238, 144),
427            "lightgrey" => rgb!(211, 211, 211),
428            "lightpink" => rgb!(255, 182, 193),
429            "lightsalmon" => rgb!(255, 160, 122),
430            "lightseagreen" => rgb!(32, 178, 170),
431            "lightskyblue" => rgb!(135, 206, 250),
432            "lightslategray" => rgb!(119, 136, 153),
433            "lightslategrey" => rgb!(119, 136, 153),
434            "lightsteelblue" => rgb!(176, 196, 222),
435            "lightyellow" => rgb!(255, 255, 224),
436            "limegreen" => rgb!(50, 205, 50),
437            "linen" => rgb!(250, 240, 230),
438            "magenta" => rgb!(255, 0, 255),
439            "mediumaquamarine" => rgb!(102, 205, 170),
440            "mediumblue" => rgb!(0, 0, 205),
441            "mediumorchid" => rgb!(186, 85, 211),
442            "mediumpurple" => rgb!(147, 112, 219),
443            "mediumseagreen" => rgb!(60, 179, 113),
444            "mediumslateblue" => rgb!(123, 104, 238),
445            "mediumspringgreen" => rgb!(0, 250, 154),
446            "mediumturquoise" => rgb!(72, 209, 204),
447            "mediumvioletred" => rgb!(199, 21, 133),
448            "midnightblue" => rgb!(25, 25, 112),
449            "mintcream" => rgb!(245, 255, 250),
450            "mistyrose" => rgb!(255, 228, 225),
451            "moccasin" => rgb!(255, 228, 181),
452            "navajowhite" => rgb!(255, 222, 173),
453            "oldlace" => rgb!(253, 245, 230),
454            "olivedrab" => rgb!(107, 142, 35),
455            "orange" => rgb!(255, 165, 0),
456            "orangered" => rgb!(255, 69, 0),
457            "orchid" => rgb!(218, 112, 214),
458            "palegoldenrod" => rgb!(238, 232, 170),
459            "palegreen" => rgb!(152, 251, 152),
460            "paleturquoise" => rgb!(175, 238, 238),
461            "palevioletred" => rgb!(219, 112, 147),
462            "papayawhip" => rgb!(255, 239, 213),
463            "peachpuff" => rgb!(255, 218, 185),
464            "peru" => rgb!(205, 133, 63),
465            "pink" => rgb!(255, 192, 203),
466            "plum" => rgb!(221, 160, 221),
467            "powderblue" => rgb!(176, 224, 230),
468            "rebeccapurple" => rgb!(102, 51, 153),
469            "rosybrown" => rgb!(188, 143, 143),
470            "royalblue" => rgb!(65, 105, 225),
471            "saddlebrown" => rgb!(139, 69, 19),
472            "salmon" => rgb!(250, 128, 114),
473            "sandybrown" => rgb!(244, 164, 96),
474            "seagreen" => rgb!(46, 139, 87),
475            "seashell" => rgb!(255, 245, 238),
476            "sienna" => rgb!(160, 82, 45),
477            "skyblue" => rgb!(135, 206, 235),
478            "slateblue" => rgb!(106, 90, 205),
479            "slategray" => rgb!(112, 128, 144),
480            "slategrey" => rgb!(112, 128, 144),
481            "snow" => rgb!(255, 250, 250),
482            "springgreen" => rgb!(0, 255, 127),
483            "steelblue" => rgb!(70, 130, 180),
484            "tan" => rgb!(210, 180, 140),
485            "thistle" => rgb!(216, 191, 216),
486            "tomato" => rgb!(255, 99, 71),
487            "turquoise" => rgb!(64, 224, 208),
488            "violet" => rgb!(238, 130, 238),
489            "wheat" => rgb!(245, 222, 179),
490            "whitesmoke" => rgb!(245, 245, 245),
491            "yellowgreen" => rgb!(154, 205, 50),
492
493            "transparent" => Color { red: 0, green: 0, blue: 0, alpha: 0 }
494        }
495    }
496
497    keyword(ident).cloned().ok_or(())
498}
499
500#[cfg(feature="parser")]
501#[inline]
502fn from_hex(c: u8) -> Result<u8, ()> {
503    match c {
504        b'0'...b'9' => Ok(c - b'0'),
505        b'a'...b'f' => Ok(c - b'a' + 10),
506        b'A'...b'F' => Ok(c - b'A' + 10),
507        _ => Err(()),
508    }
509}
510
511fn clamp_unit_f32(val: f32) -> u8 {
512    // Whilst scaling by 256 and flooring would provide
513    // an equal distribution of integers to percentage inputs,
514    // this is not what Gecko does so we instead multiply by 255
515    // and round (adding 0.5 and flooring is equivalent to rounding)
516    //
517    // Chrome does something similar for the alpha value, but not
518    // the rgb values.
519    //
520    // See https://bugzilla.mozilla.org/show_bug.cgi?id=1340484
521    //
522    // Clamping to 256 and rounding after would let 1.0 map to 256, and
523    // `256.0_f32 as u8` is undefined behavior:
524    //
525    // https://github.com/rust-lang/rust/issues/10184
526    clamp_floor_256_f32(val * 255.)
527}
528
529fn clamp_floor_256_f32(val: f32) -> u8 {
530    val.round().max(0.).min(255.) as u8
531}
532
533#[cfg(feature="parser")]
534#[inline]
535fn parse_color_function<'i, 't, ComponentParser>(
536    component_parser: &ComponentParser,
537    name: &str,
538    arguments: &mut Parser<'i, 't>,
539) -> Result<Color, ParseError<'i, ComponentParser::Error>>
540where
541    ComponentParser: ColorComponentParser<'i>,
542{
543    let (red, green, blue, uses_commas) = match_ignore_ascii_case! { name,
544        "rgb" | "rgba" => parse_rgb_components_rgb(component_parser, arguments)?,
545        "hsl" | "hsla" => parse_rgb_components_hsl(component_parser, arguments)?,
546        _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name.to_owned().into()))),
547    };
548
549    let alpha = if !arguments.is_exhausted() {
550        if uses_commas {
551            arguments.expect_comma()?;
552        } else {
553            arguments.expect_delim('/')?;
554        };
555        clamp_unit_f32(
556            component_parser
557                .parse_number_or_percentage(arguments)?
558                .unit_value(),
559        )
560    } else {
561        255
562    };
563
564    arguments.expect_exhausted()?;
565    Ok(rgba(red, green, blue, alpha))
566}
567
568#[cfg(feature="parser")]
569#[inline]
570fn parse_rgb_components_rgb<'i, 't, ComponentParser>(
571    component_parser: &ComponentParser,
572    arguments: &mut Parser<'i, 't>,
573) -> Result<(u8, u8, u8, bool), ParseError<'i, ComponentParser::Error>>
574where
575    ComponentParser: ColorComponentParser<'i>,
576{
577    // Either integers or percentages, but all the same type.
578    // https://drafts.csswg.org/css-color/#rgb-functions
579    let (red, is_number) = match component_parser.parse_number_or_percentage(arguments)? {
580        NumberOrPercentage::Number { value } => (clamp_floor_256_f32(value), true),
581        NumberOrPercentage::Percentage { unit_value } => (clamp_unit_f32(unit_value), false),
582    };
583
584    let uses_commas = arguments.try_parse(|i| i.expect_comma()).is_ok();
585
586    let green;
587    let blue;
588    if is_number {
589        green = clamp_floor_256_f32(component_parser.parse_number(arguments)?);
590        if uses_commas {
591            arguments.expect_comma()?;
592        }
593        blue = clamp_floor_256_f32(component_parser.parse_number(arguments)?);
594    } else {
595        green = clamp_unit_f32(component_parser.parse_percentage(arguments)?);
596        if uses_commas {
597            arguments.expect_comma()?;
598        }
599        blue = clamp_unit_f32(component_parser.parse_percentage(arguments)?);
600    }
601
602    Ok((red, green, blue, uses_commas))
603}
604
605#[cfg(feature="parser")]
606#[inline]
607fn parse_rgb_components_hsl<'i, 't, ComponentParser>(
608    component_parser: &ComponentParser,
609    arguments: &mut Parser<'i, 't>,
610) -> Result<(u8, u8, u8, bool), ParseError<'i, ComponentParser::Error>>
611where
612    ComponentParser: ColorComponentParser<'i>,
613{
614    // Hue given as an angle
615    // https://drafts.csswg.org/css-values/#angles
616    let hue_degrees = component_parser.parse_angle_or_number(arguments)?.degrees();
617
618    // Subtract an integer before rounding, to avoid some rounding errors:
619    let hue_normalized_degrees = hue_degrees - 360. * (hue_degrees / 360.).floor();
620    let hue = hue_normalized_degrees / 360.;
621
622    // Saturation and lightness are clamped to 0% ... 100%
623    // https://drafts.csswg.org/css-color/#the-hsl-notation
624    let uses_commas = arguments.try_parse(|i| i.expect_comma()).is_ok();
625
626    let saturation = component_parser.parse_percentage(arguments)?;
627    let saturation = saturation.max(0.).min(1.);
628
629    if uses_commas {
630        arguments.expect_comma()?;
631    }
632
633    let lightness = component_parser.parse_percentage(arguments)?;
634    let lightness = lightness.max(0.).min(1.);
635
636    // https://drafts.csswg.org/css-color/#hsl-color
637    // except with h pre-multiplied by 3, to avoid some rounding errors.
638    fn hue_to_rgb(m1: f32, m2: f32, mut h3: f32) -> f32 {
639        if h3 < 0. {
640            h3 += 3.
641        }
642        if h3 > 3. {
643            h3 -= 3.
644        }
645
646        if h3 * 2. < 1. {
647            m1 + (m2 - m1) * h3 * 2.
648        } else if h3 * 2. < 3. {
649            m2
650        } else if h3 < 2. {
651            m1 + (m2 - m1) * (2. - h3) * 2.
652        } else {
653            m1
654        }
655    }
656    let m2 = if lightness <= 0.5 {
657        lightness * (saturation + 1.)
658    } else {
659        lightness + saturation - lightness * saturation
660    };
661    let m1 = lightness * 2. - m2;
662    let hue_times_3 = hue * 3.;
663    let red = clamp_unit_f32(hue_to_rgb(m1, m2, hue_times_3 + 1.));
664    let green = clamp_unit_f32(hue_to_rgb(m1, m2, hue_times_3));
665    let blue = clamp_unit_f32(hue_to_rgb(m1, m2, hue_times_3 - 1.));
666    return Ok((red, green, blue, uses_commas));
667}