1use std::fmt;
8use std::str::FromStr;
9
10use crate::media::color::{split_components, strip_css_function, try_parse_double_or_percent};
11use crate::media::{Color, HsvColor};
12use crate::utilities::math_utilities::{self, MathUtilities};
13use crate::utilities::span_helpers::{try_parse_double, FixedF2, InvariantF64, NumberStyles};
14use crate::utilities::{CultureInfo, FormatError};
15
16#[derive(Clone, Copy, Debug, PartialEq, Default)]
19pub struct HslColor {
20 pub a: f64,
23 pub h: f64,
27 pub s: f64,
30 pub l: f64,
33}
34
35#[inline]
36fn round_to_byte(value: f64) -> u8 {
37 value.round_ties_even() as u8
38}
39
40#[inline]
41fn round_to_int(value: f64) -> i32 {
42 value.round_ties_even() as i32
43}
44
45#[inline]
53fn wrap_hue(mut hue: f64) -> f64 {
54 while hue >= 360.0 {
55 let next = hue - 360.0;
56 if next == hue {
57 break;
58 }
59 hue = next;
60 }
61
62 while hue < 0.0 {
63 let next = hue + 360.0;
64 if next == hue {
65 break;
66 }
67 hue = next;
68 }
69
70 hue
71}
72
73#[inline]
74fn clamp01(value: f64) -> f64 {
75 let value = if value < 0.0 { 0.0 } else { value };
76 if value > 1.0 {
77 1.0
78 } else {
79 value
80 }
81}
82
83impl HslColor {
84 #[inline]
87 pub fn new(alpha: f64, hue: f64, saturation: f64, lightness: f64) -> Self {
88 Self::new_with_clamp(alpha, hue, saturation, lightness, true)
89 }
90
91 #[inline]
97 pub fn new_with_clamp(
98 alpha: f64,
99 hue: f64,
100 saturation: f64,
101 lightness: f64,
102 clamp_values: bool,
103 ) -> Self {
104 if clamp_values {
105 let h = MathUtilities::clamp(hue, 0.0, 360.0);
106
107 Self {
108 a: MathUtilities::clamp(alpha, 0.0, 1.0),
109 h: if h == 360.0 { 0.0 } else { h },
112 s: MathUtilities::clamp(saturation, 0.0, 1.0),
113 l: MathUtilities::clamp(lightness, 0.0, 1.0),
114 }
115 } else {
116 Self {
117 a: alpha,
118 h: hue,
119 s: saturation,
120 l: lightness,
121 }
122 }
123 }
124
125 #[inline]
127 pub fn from_color(color: Color) -> Self {
128 color.to_hsl()
129 }
130
131 #[inline]
132 pub fn equals(&self, other: HslColor) -> bool {
133 other.a == self.a && other.h == self.h && other.s == self.s && other.l == self.l
134 }
135
136 #[inline]
138 pub fn to_rgb(&self) -> Color {
139 HslColor::hsl_to_rgb(self.h, self.s, self.l, self.a)
140 }
141
142 #[inline]
144 pub fn to_hsv(&self) -> HsvColor {
145 HslColor::hsl_to_hsv(self.h, self.s, self.l, self.a)
146 }
147
148 pub fn to_string_format(
159 &self,
160 format: Option<&str>,
161 format_provider: Option<&CultureInfo>,
162 ) -> Result<String, FormatError> {
163 let format = match format {
164 Some(format) if !format.is_empty() => format,
165 _ => return Ok(self.to_string()),
166 };
167
168 match format {
169 "X" | "x" | "H" | "R" | "r" | "R%" | "r%" => self.to_rgb().to_string_format(Some(format), format_provider),
170 "L" => Ok(self.format_hsl_css(true)),
171 "l" => Ok(self.format_hsl_css(false)),
172 "L%" => Ok(self.format_hsl_percent_css(true)),
173 "l%" => Ok(self.format_hsl_percent_css(false)),
174 "V" | "v" | "V%" | "v%" => self.to_hsv().to_string_format(Some(format), format_provider),
175 _ => Err(FormatError::from_string(format!(
176 "Format string '{format}' is not supported."
177 ))),
178 }
179 }
180
181 fn format_hsl_css(&self, include_alpha: bool) -> String {
182 let h_deg = round_to_int(self.h);
183 let s_pct = round_to_int(self.s * 100.0);
184 let l_pct = round_to_int(self.l * 100.0);
185
186 if include_alpha {
187 return format!("hsla({h_deg}, {s_pct}%, {l_pct}%, {})", FixedF2(self.a));
188 }
189
190 format!("hsl({h_deg}, {s_pct}%, {l_pct}%)")
191 }
192
193 fn format_hsl_percent_css(&self, include_alpha: bool) -> String {
194 let h_pct = round_to_int(self.h / 360.0 * 100.0);
195 let s_pct = round_to_int(self.s * 100.0);
196 let l_pct = round_to_int(self.l * 100.0);
197
198 if include_alpha {
199 let a_pct = round_to_int(self.a * 100.0);
200 return format!("hsla({h_pct}%, {s_pct}%, {l_pct}%, {a_pct}%)");
201 }
202
203 format!("hsl({h_pct}%, {s_pct}%, {l_pct}%)")
204 }
205
206 pub fn parse(s: &str) -> Result<HslColor, FormatError> {
208 match HslColor::try_parse(s) {
209 Some(hsl_color) => Ok(hsl_color),
210 None => Err(FormatError::from_string(format!(
211 "Invalid HSL color string: '{s}'."
212 ))),
213 }
214 }
215
216 pub fn try_parse(s: &str) -> Option<HslColor> {
220 let working = strip_css_function(s, "hsla(", "hsl(")?;
221 let (components, count) = split_components(working);
222
223 if count == 3 {
224 let hue = try_parse_double(components[0], NumberStyles::NUMBER)?;
226 let saturation = try_parse_double_or_percent(components[1])?;
227 let lightness = try_parse_double_or_percent(components[2])?;
228
229 Some(HslColor::new(1.0, hue, saturation, lightness))
230 } else if count == 4 {
231 let hue = try_parse_double(components[0], NumberStyles::NUMBER)?;
233 let saturation = try_parse_double_or_percent(components[1])?;
234 let lightness = try_parse_double_or_percent(components[2])?;
235 let alpha = try_parse_double_or_percent(components[3])?;
236
237 Some(HslColor::new(alpha, hue, saturation, lightness))
238 } else {
239 None
240 }
241 }
242
243 #[inline]
245 pub fn from_ahsl(a: f64, h: f64, s: f64, l: f64) -> HslColor {
246 HslColor::new(a, h, s, l)
247 }
248
249 #[inline]
251 pub fn from_hsl(h: f64, s: f64, l: f64) -> HslColor {
252 HslColor::new(1.0, h, s, l)
253 }
254
255 pub fn hsl_to_rgb(hue: f64, saturation: f64, lightness: f64, alpha: f64) -> Color {
259 let hue = wrap_hue(hue);
265
266 let saturation = clamp01(saturation);
268 let lightness = clamp01(lightness);
269 let alpha = clamp01(alpha);
270
271 let chroma = (1.0 - ((2.0 * lightness) - 1.0).abs()) * saturation;
272 let h1 = hue / 60.0;
273 let x = chroma * (1.0 - ((h1 % 2.0) - 1.0).abs());
274 let m = lightness - (0.5 * chroma);
275 let (r1, g1, b1);
276
277 if h1 < 1.0 {
278 r1 = chroma;
279 g1 = x;
280 b1 = 0.0;
281 } else if h1 < 2.0 {
282 r1 = x;
283 g1 = chroma;
284 b1 = 0.0;
285 } else if h1 < 3.0 {
286 r1 = 0.0;
287 g1 = chroma;
288 b1 = x;
289 } else if h1 < 4.0 {
290 r1 = 0.0;
291 g1 = x;
292 b1 = chroma;
293 } else if h1 < 5.0 {
294 r1 = x;
295 g1 = 0.0;
296 b1 = chroma;
297 } else {
298 r1 = chroma;
299 g1 = 0.0;
300 b1 = x;
301 }
302
303 Color::new(
304 round_to_byte(255.0 * alpha),
305 round_to_byte(255.0 * (r1 + m)),
306 round_to_byte(255.0 * (g1 + m)),
307 round_to_byte(255.0 * (b1 + m)),
308 )
309 }
310
311 pub fn hsl_to_hsv(hue: f64, saturation: f64, lightness: f64, alpha: f64) -> HsvColor {
313 let hue = wrap_hue(hue);
316
317 let saturation = clamp01(saturation);
319 let lightness = clamp01(lightness);
320 let alpha = clamp01(alpha);
321
322 let v = lightness + (saturation * math_utilities::min(lightness, 1.0 - lightness));
326
327 let s = if v <= 0.0 {
328 0.0
329 } else {
330 2.0 * (1.0 - (lightness / v))
331 };
332
333 HsvColor::new(alpha, hue, s, v)
334 }
335}
336
337impl From<HslColor> for Color {
339 #[inline]
340 fn from(hsl_color: HslColor) -> Color {
341 hsl_color.to_rgb()
342 }
343}
344
345impl FromStr for HslColor {
346 type Err = FormatError;
347 #[inline]
348 fn from_str(s: &str) -> Result<Self, FormatError> {
349 HslColor::parse(s)
350 }
351}
352
353impl fmt::Display for HslColor {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 write!(
356 f,
357 "hsla({}, {}, {}, {})",
358 InvariantF64(self.h),
359 InvariantF64(self.s),
360 InvariantF64(self.l),
361 InvariantF64(self.a)
362 )
363 }
364}
365
366#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn construction_clamps_and_wraps() {
374 assert_eq!(
375 HslColor::new(2.0, 360.0, -1.0, 5.0),
376 HslColor::new_with_clamp(1.0, 0.0, 0.0, 1.0, false)
377 );
378 assert!(HslColor::new(1.0, f64::NAN, 0.0, 0.0).h.is_nan());
379 assert_eq!(
380 HslColor::from_hsl(10.0, 0.5, 0.5),
381 HslColor::from_ahsl(1.0, 10.0, 0.5, 0.5)
382 );
383 assert_eq!(HslColor::default(), HslColor::new(0.0, 0.0, 0.0, 0.0));
384 }
385
386 #[test]
387 fn display_parse_and_conversions() {
388 assert_eq!(
389 HslColor::new(0.8, 200.0, 0.6, 0.4).to_string(),
390 "hsla(200, 0.6, 0.4, 0.8)"
391 );
392 assert_eq!(
393 HslColor::parse("nope").unwrap_err().message(),
394 "Invalid HSL color string: 'nope'."
395 );
396 assert_eq!(
397 "hsl(120, 100%, 50%)".parse::<HslColor>().unwrap(),
398 HslColor::new(1.0, 120.0, 1.0, 0.5)
399 );
400 assert_eq!(HslColor::try_parse("hsl(120, 100%)"), None);
401 assert_eq!(HslColor::try_parse("hsl(1, 2, 3, 4, 5)"), None);
402 assert_eq!(
403 Color::from(HslColor::new(1.0, 120.0, 1.0, 0.5)),
404 Color::new(255, 0, 255, 0)
405 );
406 assert_eq!(
407 HslColor::from_color(Color::new(255, 0, 0, 255)),
408 HslColor::new(1.0, 240.0, 1.0, 0.5)
409 );
410 assert_eq!(
412 HslColor::hsl_to_rgb(480.0, 1.0, 0.5, 1.0),
413 Color::new(255, 0, 255, 0)
414 );
415 assert_eq!(
416 HslColor::hsl_to_rgb(-240.0, 1.0, 0.5, 1.0),
417 Color::new(255, 0, 255, 0)
418 );
419 let _ = HslColor::hsl_to_rgb(f64::INFINITY, 1.0, 0.5, 1.0);
420 let _ = HslColor::hsl_to_rgb(f64::NEG_INFINITY, 1.0, 0.5, 1.0);
421 let _ = HslColor::hsl_to_rgb(1e300, 1.0, 0.5, 1.0);
422 }
423}