1 pub mod ansi; 2 pub mod colorspace; 3 pub mod delta_e; 4 pub mod distinct; 5 mod helper; 6 pub mod named; 7 pub mod parser; 8 pub mod random; 9 mod types; 10 11 use std::fmt; 12 13 use colorspace::ColorSpace; 14 pub use helper::Fraction; 15 use helper::{clamp, interpolate, interpolate_angle, mod_positive}; 16 use types::{Hue, Scalar}; 17 18 /// The representation of a color. 19 /// 20 /// Note: 21 /// - Colors outside the sRGB gamut (which cannot be displayed on a typical 22 /// computer screen) can not be represented by `Color`. 23 /// - The `PartialEq` instance compares two `Color`s by comparing their (integer) 24 /// RGB values. This is different from comparing the HSL values. For example, 25 /// HSL has many different representations of black (arbitrary hue and 26 /// saturation values). 27 #[derive(Clone)] 28 pub struct Color { 29 hue: Hue, 30 saturation: Scalar, 31 lightness: Scalar, 32 alpha: Scalar, 33 } 34 35 // Illuminant D65 constants used for Lab color space conversions. 36 const D65_XN: Scalar = 0.950_470; 37 const D65_YN: Scalar = 1.0; 38 const D65_ZN: Scalar = 1.088_830; 39 40 impl Color { from_hsla(hue: Scalar, saturation: Scalar, lightness: Scalar, alpha: Scalar) -> Color41 pub fn from_hsla(hue: Scalar, saturation: Scalar, lightness: Scalar, alpha: Scalar) -> Color { 42 Self::from(&HSLA { 43 h: hue, 44 s: saturation, 45 l: lightness, 46 alpha, 47 }) 48 } 49 50 /// from_hsl(hue: Scalar, saturation: Scalar, lightness: Scalar) -> Color51 pub fn from_hsl(hue: Scalar, saturation: Scalar, lightness: Scalar) -> Color { 52 Self::from(&HSLA { 53 h: hue, 54 s: saturation, 55 l: lightness, 56 alpha: 1.0, 57 }) 58 } 59 60 /// Create a `Color` from integer RGB values between 0 and 255 and a floating 61 /// point alpha value between 0.0 and 1.0. from_rgba(r: u8, g: u8, b: u8, alpha: Scalar) -> Color62 pub fn from_rgba(r: u8, g: u8, b: u8, alpha: Scalar) -> Color { 63 // RGB to HSL conversion algorithm adapted from 64 // https://en.wikipedia.org/wiki/HSL_and_HSV 65 Self::from(&RGBA::<u8> { r, g, b, alpha }) 66 } 67 68 /// Create a `Color` from integer RGB values between 0 and 255. from_rgb(r: u8, g: u8, b: u8) -> Color69 pub fn from_rgb(r: u8, g: u8, b: u8) -> Color { 70 Self::from(&RGBA::<u8> { 71 r, 72 g, 73 b, 74 alpha: 1.0, 75 }) 76 } 77 78 /// Create a `Color` from RGB and alpha values between 0.0 and 1.0. Values outside this range 79 /// will be clamped. from_rgba_float(r: Scalar, g: Scalar, b: Scalar, alpha: Scalar) -> Color80 pub fn from_rgba_float(r: Scalar, g: Scalar, b: Scalar, alpha: Scalar) -> Color { 81 Self::from(&RGBA::<f64> { r, g, b, alpha }) 82 } 83 84 /// Create a `Color` from RGB values between 0.0 and 1.0. Values outside this range will be 85 /// clamped. from_rgb_float(r: Scalar, g: Scalar, b: Scalar) -> Color86 pub fn from_rgb_float(r: Scalar, g: Scalar, b: Scalar) -> Color { 87 Self::from(&RGBA::<f64> { 88 r, 89 g, 90 b, 91 alpha: 1.0, 92 }) 93 } 94 95 /// Create a `Color` from XYZ coordinates in the CIE 1931 color space. Note that a `Color` 96 /// always represents a color in the sRGB gamut (colors that can be represented on a typical 97 /// computer screen) while the XYZ color space is bigger. This function will tend to create 98 /// fully saturated colors at the edge of the sRGB gamut if the coordinates lie outside the 99 /// sRGB range. 100 /// 101 /// See: 102 /// - https://en.wikipedia.org/wiki/CIE_1931_color_space 103 /// - https://en.wikipedia.org/wiki/SRGB from_xyz(x: Scalar, y: Scalar, z: Scalar, alpha: Scalar) -> Color104 pub fn from_xyz(x: Scalar, y: Scalar, z: Scalar, alpha: Scalar) -> Color { 105 Self::from(&XYZ { x, y, z, alpha }) 106 } 107 108 /// Create a `Color` from LMS coordinates. This is the matrix inverse of the matrix that 109 /// appears in `to_lms`. from_lms(l: Scalar, m: Scalar, s: Scalar, alpha: Scalar) -> Color110 pub fn from_lms(l: Scalar, m: Scalar, s: Scalar, alpha: Scalar) -> Color { 111 Self::from(&LMS { l, m, s, alpha }) 112 } 113 114 /// Create a `Color` from L, a and b coordinates coordinates in the Lab color 115 /// space. Note: See documentation for `from_xyz`. The same restrictions apply here. 116 /// 117 /// See: https://en.wikipedia.org/wiki/Lab_color_space from_lab(l: Scalar, a: Scalar, b: Scalar, alpha: Scalar) -> Color118 pub fn from_lab(l: Scalar, a: Scalar, b: Scalar, alpha: Scalar) -> Color { 119 Self::from(&Lab { l, a, b, alpha }) 120 } 121 122 /// Create a `Color` from lightness, chroma and hue coordinates in the CIE LCh color space. 123 /// This is a cylindrical transform of the Lab color space. Note: See documentation for 124 /// `from_xyz`. The same restrictions apply here. 125 /// 126 /// See: https://en.wikipedia.org/wiki/Lab_color_space from_lch(l: Scalar, c: Scalar, h: Scalar, alpha: Scalar) -> Color127 pub fn from_lch(l: Scalar, c: Scalar, h: Scalar, alpha: Scalar) -> Color { 128 Self::from(&LCh { l, c, h, alpha }) 129 } 130 131 /// Create a `Color` from the four colours of the CMYK model: Cyan, Magenta, Yellow and Black. 132 /// The CMYK colours are subtractive. This means the colours get darker as you blend them together from_cmyk(c: Scalar, m: Scalar, y: Scalar, k: Scalar) -> Color133 pub fn from_cmyk(c: Scalar, m: Scalar, y: Scalar, k: Scalar) -> Color { 134 Self::from(&CMYK { c, m, y, k }) 135 } 136 137 /// Convert a `Color` to its hue, saturation, lightness and alpha values. The hue is given 138 /// in degrees, as a number between 0.0 and 360.0. Saturation, lightness and alpha are numbers 139 /// between 0.0 and 1.0. to_hsla(&self) -> HSLA140 pub fn to_hsla(&self) -> HSLA { 141 HSLA::from(self) 142 } 143 144 /// Format the color as a HSL-representation string (`hsl(123, 50.3%, 80.1%)`). to_hsl_string(&self, format: Format) -> String145 pub fn to_hsl_string(&self, format: Format) -> String { 146 format!( 147 "hsl({:.0},{space}{:.1}%,{space}{:.1}%)", 148 self.hue.value(), 149 100.0 * self.saturation, 150 100.0 * self.lightness, 151 space = if format == Format::Spaces { " " } else { "" } 152 ) 153 } 154 155 /// Convert a `Color` to its red, green, blue and alpha values. The RGB values are integers in 156 /// the range from 0 to 255. The alpha channel is a number between 0.0 and 1.0. to_rgba(&self) -> RGBA<u8>157 pub fn to_rgba(&self) -> RGBA<u8> { 158 RGBA::<u8>::from(self) 159 } 160 161 /// Format the color as a RGB-representation string (`rgb(255, 127, 0)`). to_rgb_string(&self, format: Format) -> String162 pub fn to_rgb_string(&self, format: Format) -> String { 163 let rgba = RGBA::<u8>::from(self); 164 format!( 165 "rgb({r},{space}{g},{space}{b})", 166 r = rgba.r, 167 g = rgba.g, 168 b = rgba.b, 169 space = if format == Format::Spaces { " " } else { "" } 170 ) 171 } 172 173 /// Convert a `Color` to its cyan, magenta, yellow, and black values. The CMYK 174 /// values are floats smaller than or equal to 1.0. to_cmyk(&self) -> CMYK175 pub fn to_cmyk(&self) -> CMYK { 176 CMYK::from(self) 177 } 178 179 /// Format the color as a CMYK-representation string (`cmyk(0, 50, 100, 100)`). to_cmyk_string(&self, format: Format) -> String180 pub fn to_cmyk_string(&self, format: Format) -> String { 181 let cmyk = CMYK::from(self); 182 format!( 183 "cmyk({c},{space}{m},{space}{y},{space}{k})", 184 c = (cmyk.c * 100.0).round(), 185 m = (cmyk.m * 100.0).round(), 186 y = (cmyk.y * 100.0).round(), 187 k = (cmyk.k * 100.0).round(), 188 space = if format == Format::Spaces { " " } else { "" } 189 ) 190 } 191 192 /// Format the color as a floating point RGB-representation string (`rgb(1.0, 0.5, 0)`). to_rgb_float_string(&self, format: Format) -> String193 pub fn to_rgb_float_string(&self, format: Format) -> String { 194 let rgba = RGBA::<f64>::from(self); 195 format!( 196 "rgb({r:.3},{space}{g:.3},{space}{b:.3})", 197 r = rgba.r, 198 g = rgba.g, 199 b = rgba.b, 200 space = if format == Format::Spaces { " " } else { "" } 201 ) 202 } 203 204 /// Format the color as a RGB-representation string (`#fc0070`). to_rgb_hex_string(&self, leading_hash: bool) -> String205 pub fn to_rgb_hex_string(&self, leading_hash: bool) -> String { 206 let rgba = self.to_rgba(); 207 format!( 208 "{}{:02x}{:02x}{:02x}", 209 if leading_hash { "#" } else { "" }, 210 rgba.r, 211 rgba.g, 212 rgba.b 213 ) 214 } 215 216 /// Convert a `Color` to its red, green, blue and alpha values. All numbers are from the range 217 /// between 0.0 and 1.0. to_rgba_float(&self) -> RGBA<Scalar>218 pub fn to_rgba_float(&self) -> RGBA<Scalar> { 219 RGBA::<f64>::from(self) 220 } 221 222 /// Return the color as an integer in RGB representation (`0xRRGGBB`) to_u32(&self) -> u32223 pub fn to_u32(&self) -> u32 { 224 let rgba = self.to_rgba(); 225 u32::from(rgba.r).wrapping_shl(16) + u32::from(rgba.g).wrapping_shl(8) + u32::from(rgba.b) 226 } 227 228 /// Get XYZ coordinates according to the CIE 1931 color space. 229 /// 230 /// See: 231 /// - https://en.wikipedia.org/wiki/CIE_1931_color_space 232 /// - https://en.wikipedia.org/wiki/SRGB to_xyz(&self) -> XYZ233 pub fn to_xyz(&self) -> XYZ { 234 XYZ::from(self) 235 } 236 237 /// Get coordinates according to the LSM color space 238 /// 239 /// See https://en.wikipedia.org/wiki/LMS_color_space for info on the color space as well as an 240 /// algorithm for converting from CIE XYZ to_lms(&self) -> LMS241 pub fn to_lms(&self) -> LMS { 242 LMS::from(self) 243 } 244 245 /// Get L, a and b coordinates according to the Lab color space. 246 /// 247 /// See: https://en.wikipedia.org/wiki/Lab_color_space to_lab(&self) -> Lab248 pub fn to_lab(&self) -> Lab { 249 Lab::from(self) 250 } 251 252 /// Format the color as a Lab-representation string (`Lab(41, 83, -93)`). to_lab_string(&self, format: Format) -> String253 pub fn to_lab_string(&self, format: Format) -> String { 254 let lab = Lab::from(self); 255 format!( 256 "Lab({l:.0},{space}{a:.0},{space}{b:.0})", 257 l = lab.l, 258 a = lab.a, 259 b = lab.b, 260 space = if format == Format::Spaces { " " } else { "" } 261 ) 262 } 263 264 /// Get L, C and h coordinates according to the CIE LCh color space. 265 /// 266 /// See: https://en.wikipedia.org/wiki/Lab_color_space to_lch(&self) -> LCh267 pub fn to_lch(&self) -> LCh { 268 LCh::from(self) 269 } 270 271 /// Format the color as a LCh-representation string (`LCh(0.3, 0.2, 0.1)`). to_lch_string(&self, format: Format) -> String272 pub fn to_lch_string(&self, format: Format) -> String { 273 let lch = LCh::from(self); 274 format!( 275 "LCh({l:.0},{space}{c:.0},{space}{h:.0})", 276 l = lch.l, 277 c = lch.c, 278 h = lch.h, 279 space = if format == Format::Spaces { " " } else { "" } 280 ) 281 } 282 283 /// Pure black. black() -> Color284 pub fn black() -> Color { 285 Color::from_hsl(0.0, 0.0, 0.0) 286 } 287 288 /// Pure white. white() -> Color289 pub fn white() -> Color { 290 Color::from_hsl(0.0, 0.0, 1.0) 291 } 292 293 /// Red (`#ff0000`) red() -> Color294 pub fn red() -> Color { 295 Color::from_rgb(255, 0, 0) 296 } 297 298 /// Green (`#008000`) green() -> Color299 pub fn green() -> Color { 300 Color::from_rgb(0, 128, 0) 301 } 302 303 /// Blue (`#0000ff`) blue() -> Color304 pub fn blue() -> Color { 305 Color::from_rgb(0, 0, 255) 306 } 307 308 /// Yellow (`#ffff00`) yellow() -> Color309 pub fn yellow() -> Color { 310 Color::from_rgb(255, 255, 0) 311 } 312 313 /// Fuchsia (`#ff00ff`) fuchsia() -> Color314 pub fn fuchsia() -> Color { 315 Color::from_rgb(255, 0, 255) 316 } 317 318 /// Aqua (`#00ffff`) aqua() -> Color319 pub fn aqua() -> Color { 320 Color::from_rgb(0, 255, 255) 321 } 322 323 /// Lime (`#00ff00`) lime() -> Color324 pub fn lime() -> Color { 325 Color::from_rgb(0, 255, 0) 326 } 327 328 /// Maroon (`#800000`) maroon() -> Color329 pub fn maroon() -> Color { 330 Color::from_rgb(128, 0, 0) 331 } 332 333 /// Olive (`#808000`) olive() -> Color334 pub fn olive() -> Color { 335 Color::from_rgb(128, 128, 0) 336 } 337 338 /// Navy (`#000080`) navy() -> Color339 pub fn navy() -> Color { 340 Color::from_rgb(0, 0, 128) 341 } 342 343 /// Purple (`#800080`) purple() -> Color344 pub fn purple() -> Color { 345 Color::from_rgb(128, 0, 128) 346 } 347 348 /// Teal (`#008080`) teal() -> Color349 pub fn teal() -> Color { 350 Color::from_rgb(0, 128, 128) 351 } 352 353 /// Silver (`#c0c0c0`) silver() -> Color354 pub fn silver() -> Color { 355 Color::from_rgb(192, 192, 192) 356 } 357 358 /// Gray (`#808080`) gray() -> Color359 pub fn gray() -> Color { 360 Color::from_rgb(128, 128, 128) 361 } 362 363 /// Create a gray tone from a lightness value (0.0 is black, 1.0 is white). graytone(lightness: Scalar) -> Color364 pub fn graytone(lightness: Scalar) -> Color { 365 Color::from_hsl(0.0, 0.0, lightness) 366 } 367 368 /// Rotate along the "hue" axis. rotate_hue(&self, delta: Scalar) -> Color369 pub fn rotate_hue(&self, delta: Scalar) -> Color { 370 Self::from_hsla( 371 self.hue.value() + delta, 372 self.saturation, 373 self.lightness, 374 self.alpha, 375 ) 376 } 377 378 /// Get the complementary color (hue rotated by 180°). complementary(&self) -> Color379 pub fn complementary(&self) -> Color { 380 self.rotate_hue(180.0) 381 } 382 383 /// Lighten a color by adding a certain amount (number between -1.0 and 1.0) to the lightness 384 /// channel. If the number is negative, the color is darkened. lighten(&self, f: Scalar) -> Color385 pub fn lighten(&self, f: Scalar) -> Color { 386 Self::from_hsla( 387 self.hue.value(), 388 self.saturation, 389 self.lightness + f, 390 self.alpha, 391 ) 392 } 393 394 /// Darken a color by subtracting a certain amount (number between -1.0 and 1.0) from the 395 /// lightness channel. If the number is negative, the color is lightened. darken(&self, f: Scalar) -> Color396 pub fn darken(&self, f: Scalar) -> Color { 397 self.lighten(-f) 398 } 399 400 /// Increase the saturation of a color by adding a certain amount (number between -1.0 and 1.0) 401 /// to the saturation channel. If the number is negative, the color is desaturated. saturate(&self, f: Scalar) -> Color402 pub fn saturate(&self, f: Scalar) -> Color { 403 Self::from_hsla( 404 self.hue.value(), 405 self.saturation + f, 406 self.lightness, 407 self.alpha, 408 ) 409 } 410 411 /// Decrease the saturation of a color by subtracting a certain amount (number between -1.0 and 412 /// 1.0) from the saturation channel. If the number is negative, the color is saturated. desaturate(&self, f: Scalar) -> Color413 pub fn desaturate(&self, f: Scalar) -> Color { 414 self.saturate(-f) 415 } 416 417 /// Adjust the long-, medium-, and short-wavelength cone perception of a color to simulate what 418 /// a colorblind person sees. Since there are multiple kinds of colorblindness, the desired 419 /// kind must be specified in `cb_ty`. simulate_colorblindness(&self, cb_ty: ColorblindnessType) -> Color420 pub fn simulate_colorblindness(&self, cb_ty: ColorblindnessType) -> Color { 421 // Coefficients here are taken from 422 // https://ixora.io/projects/colorblindness/color-blindness-simulation-research/ 423 let (l, m, s, alpha) = match cb_ty { 424 ColorblindnessType::Protanopia => { 425 let LMS { m, s, alpha, .. } = self.to_lms(); 426 let l = 1.051_182_94 * m - 0.051_160_99 * s; 427 (l, m, s, alpha) 428 } 429 ColorblindnessType::Deuteranopia => { 430 let LMS { l, s, alpha, .. } = self.to_lms(); 431 let m = 0.951_309_2 * l + 0.048_669_92 * s; 432 (l, m, s, alpha) 433 } 434 ColorblindnessType::Tritanopia => { 435 let LMS { l, m, alpha, .. } = self.to_lms(); 436 let s = -0.867_447_36 * l + 1.867_270_89 * m; 437 (l, m, s, alpha) 438 } 439 }; 440 441 Color::from_lms(l, m, s, alpha) 442 } 443 444 /// Convert a color to a gray tone with the same perceived luminance (see `luminance`). to_gray(&self) -> Color445 pub fn to_gray(&self) -> Color { 446 let hue = self.hue; 447 let c = self.to_lch(); 448 449 // the desaturation step is only needed to correct minor rounding errors. 450 let mut gray = Color::from_lch(c.l, 0.0, 0.0, 1.0).desaturate(1.0); 451 452 // Restore the hue value (does not alter the color, but makes it able to add saturation 453 // again) 454 gray.hue = hue; 455 456 gray 457 } 458 459 /// The percieved brightness of the color (A number between 0.0 and 1.0). 460 /// 461 /// See: https://www.w3.org/TR/AERT#color-contrast brightness(&self) -> Scalar462 pub fn brightness(&self) -> Scalar { 463 let c = self.to_rgba_float(); 464 (299.0 * c.r + 587.0 * c.g + 114.0 * c.b) / 1000.0 465 } 466 467 /// Determine whether a color is perceived as a light color (perceived brightness is larger 468 /// than 0.5). is_light(&self) -> bool469 pub fn is_light(&self) -> bool { 470 self.brightness() > 0.5 471 } 472 473 /// The relative brightness of a color (normalized to 0.0 for darkest black 474 /// and 1.0 for lightest white), according to the WCAG definition. 475 /// 476 /// See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef luminance(&self) -> Scalar477 pub fn luminance(&self) -> Scalar { 478 fn f(s: Scalar) -> Scalar { 479 if s <= 0.03928 { 480 s / 12.92 481 } else { 482 Scalar::powf((s + 0.055) / 1.055, 2.4) 483 } 484 } 485 486 let c = self.to_rgba_float(); 487 let r = f(c.r); 488 let g = f(c.g); 489 let b = f(c.b); 490 491 0.2126 * r + 0.7152 * g + 0.0722 * b 492 } 493 494 /// Contrast ratio between two colors as defined by the WCAG. The ratio can range from 1.0 495 /// to 21.0. Two colors with a contrast ratio of 4.5 or higher can be used as text color and 496 /// background color and should be well readable. 497 /// 498 /// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef contrast_ratio(&self, other: &Color) -> Scalar499 pub fn contrast_ratio(&self, other: &Color) -> Scalar { 500 let l_self = self.luminance(); 501 let l_other = other.luminance(); 502 503 if l_self > l_other { 504 (l_self + 0.05) / (l_other + 0.05) 505 } else { 506 (l_other + 0.05) / (l_self + 0.05) 507 } 508 } 509 510 /// Return a readable foreground text color (either `black` or `white`) for a 511 /// given background color. text_color(&self) -> Color512 pub fn text_color(&self) -> Color { 513 // This threshold can be easily computed by solving 514 // 515 // contrast(L_threshold, L_black) == contrast(L_threshold, L_white) 516 // 517 // where contrast(.., ..) is the color contrast as defined by the WCAG (see above) 518 const THRESHOLD: Scalar = 0.179; 519 520 if self.luminance() > THRESHOLD { 521 Color::black() 522 } else { 523 Color::white() 524 } 525 } 526 527 /// Compute the perceived 'distance' between two colors according to the CIE76 delta-E 528 /// standard. A distance below ~2.3 is not noticable. 529 /// 530 /// See: https://en.wikipedia.org/wiki/Color_difference distance_delta_e_cie76(&self, other: &Color) -> Scalar531 pub fn distance_delta_e_cie76(&self, other: &Color) -> Scalar { 532 delta_e::cie76(&self.to_lab(), &other.to_lab()) 533 } 534 535 /// Compute the perceived 'distance' between two colors according to the CIEDE2000 delta-E 536 /// standard. 537 /// 538 /// See: https://en.wikipedia.org/wiki/Color_difference distance_delta_e_ciede2000(&self, other: &Color) -> Scalar539 pub fn distance_delta_e_ciede2000(&self, other: &Color) -> Scalar { 540 delta_e::ciede2000(&self.to_lab(), &other.to_lab()) 541 } 542 543 /// Mix two colors by linearly interpolating between them in the specified color space. 544 /// For the angle-like components (hue), the shortest path along the unit circle is chosen. mix<C: ColorSpace>(self: &Color, other: &Color, fraction: Fraction) -> Color545 pub fn mix<C: ColorSpace>(self: &Color, other: &Color, fraction: Fraction) -> Color { 546 C::from_color(self) 547 .mix(&C::from_color(other), fraction) 548 .into_color() 549 } 550 } 551 552 // by default Colors will be printed into HSLA fromat 553 impl fmt::Display for Color { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result554 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 555 write!(f, "{}", HSLA::from(self).to_string()) 556 } 557 } 558 559 impl fmt::Debug for Color { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result560 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 561 write!(f, "Color::from_{}", self.to_rgb_string(Format::NoSpaces)) 562 } 563 } 564 565 impl PartialEq for Color { eq(&self, other: &Color) -> bool566 fn eq(&self, other: &Color) -> bool { 567 self.to_rgba() == other.to_rgba() 568 } 569 } 570 571 impl From<&HSLA> for Color { from(color: &HSLA) -> Self572 fn from(color: &HSLA) -> Self { 573 Color { 574 hue: Hue::from(color.h), 575 saturation: clamp(0.0, 1.0, color.s), 576 lightness: clamp(0.0, 1.0, color.l), 577 alpha: clamp(0.0, 1.0, color.alpha), 578 } 579 } 580 } 581 582 impl From<&RGBA<u8>> for Color { from(color: &RGBA<u8>) -> Self583 fn from(color: &RGBA<u8>) -> Self { 584 let max_chroma = u8::max(u8::max(color.r, color.g), color.b); 585 let min_chroma = u8::min(u8::min(color.r, color.g), color.b); 586 587 let chroma = max_chroma - min_chroma; 588 let chroma_s = Scalar::from(chroma) / 255.0; 589 590 let r_s = Scalar::from(color.r) / 255.0; 591 let g_s = Scalar::from(color.g) / 255.0; 592 let b_s = Scalar::from(color.b) / 255.0; 593 594 let hue = 60.0 595 * (if chroma == 0 { 596 0.0 597 } else if color.r == max_chroma { 598 mod_positive((g_s - b_s) / chroma_s, 6.0) 599 } else if color.g == max_chroma { 600 (b_s - r_s) / chroma_s + 2.0 601 } else { 602 (r_s - g_s) / chroma_s + 4.0 603 }); 604 605 let lightness = (Scalar::from(max_chroma) + Scalar::from(min_chroma)) / (255.0 * 2.0); 606 let saturation = if chroma == 0 { 607 0.0 608 } else { 609 chroma_s / (1.0 - Scalar::abs(2.0 * lightness - 1.0)) 610 }; 611 Self::from(&HSLA { 612 h: hue, 613 s: saturation, 614 l: lightness, 615 alpha: color.alpha, 616 }) 617 } 618 } 619 620 impl From<&RGBA<f64>> for Color { from(color: &RGBA<f64>) -> Self621 fn from(color: &RGBA<f64>) -> Self { 622 let r = Scalar::round(clamp(0.0, 255.0, 255.0 * color.r)) as u8; 623 let g = Scalar::round(clamp(0.0, 255.0, 255.0 * color.g)) as u8; 624 let b = Scalar::round(clamp(0.0, 255.0, 255.0 * color.b)) as u8; 625 Self::from(&RGBA::<u8> { 626 r, 627 g, 628 b, 629 alpha: color.alpha, 630 }) 631 } 632 } 633 634 impl From<&XYZ> for Color { from(color: &XYZ) -> Self635 fn from(color: &XYZ) -> Self { 636 #![allow(clippy::many_single_char_names)] 637 let f = |c| { 638 if c <= 0.003_130_8 { 639 12.92 * c 640 } else { 641 1.055 * Scalar::powf(c, 1.0 / 2.4) - 0.055 642 } 643 }; 644 645 let r = f(3.2406 * color.x - 1.5372 * color.y - 0.4986 * color.z); 646 let g = f(-0.9689 * color.x + 1.8758 * color.y + 0.0415 * color.z); 647 let b = f(0.0557 * color.x - 0.2040 * color.y + 1.0570 * color.z); 648 649 Self::from(&RGBA::<f64> { 650 r, 651 g, 652 b, 653 alpha: color.alpha, 654 }) 655 } 656 } 657 658 impl From<&LMS> for Color { from(color: &LMS) -> Self659 fn from(color: &LMS) -> Self { 660 #![allow(clippy::many_single_char_names)] 661 let x = 1.91020 * color.l - 1.112_120 * color.m + 0.201_908 * color.s; 662 let y = 0.37095 * color.l + 0.629_054 * color.m + 0.000_000 * color.s; 663 let z = 0.00000 * color.l + 0.000_000 * color.m + 1.000_000 * color.s; 664 Self::from(&XYZ { 665 x, 666 y, 667 z, 668 alpha: color.alpha, 669 }) 670 } 671 } 672 673 impl From<&Lab> for Color { from(color: &Lab) -> Self674 fn from(color: &Lab) -> Self { 675 #![allow(clippy::many_single_char_names)] 676 const DELTA: Scalar = 6.0 / 29.0; 677 678 let finv = |t| { 679 if t > DELTA { 680 Scalar::powf(t, 3.0) 681 } else { 682 3.0 * DELTA * DELTA * (t - 4.0 / 29.0) 683 } 684 }; 685 686 let l_ = (color.l + 16.0) / 116.0; 687 let x = D65_XN * finv(l_ + color.a / 500.0); 688 let y = D65_YN * finv(l_); 689 let z = D65_ZN * finv(l_ - color.b / 200.0); 690 691 Self::from(&XYZ { 692 x, 693 y, 694 z, 695 alpha: color.alpha, 696 }) 697 } 698 } 699 700 impl From<&LCh> for Color { from(color: &LCh) -> Self701 fn from(color: &LCh) -> Self { 702 #![allow(clippy::many_single_char_names)] 703 const DEG2RAD: Scalar = std::f64::consts::PI / 180.0; 704 705 let a = color.c * Scalar::cos(color.h * DEG2RAD); 706 let b = color.c * Scalar::sin(color.h * DEG2RAD); 707 708 Self::from(&Lab { 709 l: color.l, 710 a, 711 b, 712 alpha: color.alpha, 713 }) 714 } 715 } 716 717 // from CMYK to Color so you can do -> let new_color = Color::from(&some_cmyk); 718 impl From<&CMYK> for Color { from(color: &CMYK) -> Self719 fn from(color: &CMYK) -> Self { 720 #![allow(clippy::many_single_char_names)] 721 let r = 255.0 * ((1.0 - color.c) / 100.0) * ((1.0 - color.k) / 100.0); 722 let g = 255.0 * ((1.0 - color.m) / 100.0) * ((1.0 - color.k) / 100.0); 723 let b = 255.0 * ((1.0 - color.y) / 100.0) * ((1.0 - color.k) / 100.0); 724 725 Color::from(&RGBA::<f64> { 726 r, 727 g, 728 b, 729 alpha: 1.0, 730 }) 731 } 732 } 733 734 #[derive(Debug, Clone, PartialEq)] 735 pub struct RGBA<T> { 736 pub r: T, 737 pub g: T, 738 pub b: T, 739 pub alpha: Scalar, 740 } 741 742 impl ColorSpace for RGBA<f64> { from_color(c: &Color) -> Self743 fn from_color(c: &Color) -> Self { 744 c.to_rgba_float() 745 } 746 into_color(&self) -> Color747 fn into_color(&self) -> Color { 748 Color::from_rgba_float(self.r, self.g, self.b, self.alpha) 749 } 750 mix(&self, other: &Self, fraction: Fraction) -> Self751 fn mix(&self, other: &Self, fraction: Fraction) -> Self { 752 Self { 753 r: interpolate(self.r, other.r, fraction), 754 g: interpolate(self.g, other.g, fraction), 755 b: interpolate(self.b, other.b, fraction), 756 alpha: interpolate(self.alpha, other.alpha, fraction), 757 } 758 } 759 } 760 761 impl From<&Color> for RGBA<f64> { from(color: &Color) -> Self762 fn from(color: &Color) -> Self { 763 let h_s = color.hue.value() / 60.0; 764 let chr = (1.0 - Scalar::abs(2.0 * color.lightness - 1.0)) * color.saturation; 765 let m = color.lightness - chr / 2.0; 766 let x = chr * (1.0 - Scalar::abs(h_s % 2.0 - 1.0)); 767 768 struct RGB(Scalar, Scalar, Scalar); 769 770 let col = if h_s < 1.0 { 771 RGB(chr, x, 0.0) 772 } else if 1.0 <= h_s && h_s < 2.0 { 773 RGB(x, chr, 0.0) 774 } else if 2.0 <= h_s && h_s < 3.0 { 775 RGB(0.0, chr, x) 776 } else if 3.0 <= h_s && h_s < 4.0 { 777 RGB(0.0, x, chr) 778 } else if 4.0 <= h_s && h_s < 5.0 { 779 RGB(x, 0.0, chr) 780 } else { 781 RGB(chr, 0.0, x) 782 }; 783 784 RGBA { 785 r: col.0 + m, 786 g: col.1 + m, 787 b: col.2 + m, 788 alpha: color.alpha, 789 } 790 } 791 } 792 793 impl From<&Color> for RGBA<u8> { from(color: &Color) -> Self794 fn from(color: &Color) -> Self { 795 let c = RGBA::<f64>::from(color); 796 let r = Scalar::round(255.0 * c.r) as u8; 797 let g = Scalar::round(255.0 * c.g) as u8; 798 let b = Scalar::round(255.0 * c.b) as u8; 799 800 RGBA { 801 r, 802 g, 803 b, 804 alpha: color.alpha, 805 } 806 } 807 } 808 809 impl fmt::Display for RGBA<f64> { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result810 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 811 write!(f, "rgb({r}, {g}, {b})", r = self.r, g = self.g, b = self.b,) 812 } 813 } 814 815 impl fmt::Display for RGBA<u8> { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result816 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 817 write!(f, "rgb({r}, {g}, {b})", r = self.r, g = self.g, b = self.b,) 818 } 819 } 820 821 #[derive(Debug, Clone, PartialEq)] 822 pub struct HSLA { 823 pub h: Scalar, 824 pub s: Scalar, 825 pub l: Scalar, 826 pub alpha: Scalar, 827 } 828 829 impl ColorSpace for HSLA { from_color(c: &Color) -> Self830 fn from_color(c: &Color) -> Self { 831 c.to_hsla() 832 } 833 into_color(&self) -> Color834 fn into_color(&self) -> Color { 835 Color::from_hsla(self.h, self.s, self.l, self.alpha) 836 } 837 mix(&self, other: &Self, fraction: Fraction) -> Self838 fn mix(&self, other: &Self, fraction: Fraction) -> Self { 839 // make sure that the hue is preserved when mixing with gray colors 840 let self_hue = if self.s < 0.0001 { other.h } else { self.h }; 841 let other_hue = if other.s < 0.0001 { self.h } else { other.h }; 842 843 Self { 844 h: interpolate_angle(self_hue, other_hue, fraction), 845 s: interpolate(self.s, other.s, fraction), 846 l: interpolate(self.l, other.l, fraction), 847 alpha: interpolate(self.alpha, other.alpha, fraction), 848 } 849 } 850 } 851 852 impl From<&Color> for HSLA { from(color: &Color) -> Self853 fn from(color: &Color) -> Self { 854 HSLA { 855 h: color.hue.value(), 856 s: color.saturation, 857 l: color.lightness, 858 alpha: color.alpha, 859 } 860 } 861 } 862 863 impl fmt::Display for HSLA { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result864 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 865 write!(f, "hsl({h}, {s}, {l})", h = self.h, s = self.s, l = self.l,) 866 } 867 } 868 869 #[derive(Debug, Clone, PartialEq)] 870 pub struct XYZ { 871 pub x: Scalar, 872 pub y: Scalar, 873 pub z: Scalar, 874 pub alpha: Scalar, 875 } 876 877 impl From<&Color> for XYZ { from(color: &Color) -> Self878 fn from(color: &Color) -> Self { 879 #![allow(clippy::many_single_char_names)] 880 let finv = |c_: f64| { 881 if c_ <= 0.04045 { 882 c_ / 12.92 883 } else { 884 Scalar::powf((c_ + 0.055) / 1.055, 2.4) 885 } 886 }; 887 888 let rec = RGBA::from(color); 889 let r = finv(rec.r); 890 let g = finv(rec.g); 891 let b = finv(rec.b); 892 893 let x = 0.4124 * r + 0.3576 * g + 0.1805 * b; 894 let y = 0.2126 * r + 0.7152 * g + 0.0722 * b; 895 let z = 0.0193 * r + 0.1192 * g + 0.9505 * b; 896 897 XYZ { 898 x, 899 y, 900 z, 901 alpha: color.alpha, 902 } 903 } 904 } 905 906 impl fmt::Display for XYZ { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result907 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 908 write!(f, "XYZ({x}, {y}, {z})", x = self.x, y = self.y, z = self.z,) 909 } 910 } 911 912 /// A color space whose axes correspond to the responsivity spectra of the long-, medium-, and 913 /// short-wavelength cone cells in the human eye. More info 914 /// [here](https://en.wikipedia.org/wiki/LMS_color_space). 915 #[derive(Debug, Clone, PartialEq)] 916 pub struct LMS { 917 pub l: Scalar, 918 pub m: Scalar, 919 pub s: Scalar, 920 pub alpha: Scalar, 921 } 922 923 impl From<&Color> for LMS { from(color: &Color) -> Self924 fn from(color: &Color) -> Self { 925 let XYZ { x, y, z, alpha } = XYZ::from(color); 926 let l = 0.38971 * x + 0.68898 * y - 0.07868 * z; 927 let m = -0.22981 * x + 1.18340 * y + 0.04641 * z; 928 let s = 0.00000 * x + 0.00000 * y + 1.00000 * z; 929 930 LMS { l, m, s, alpha } 931 } 932 } 933 934 impl fmt::Display for LMS { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result935 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 936 write!(f, "LMS({l}, {m}, {s})", l = self.l, m = self.m, s = self.s,) 937 } 938 } 939 940 #[derive(Debug, Clone, PartialEq)] 941 pub struct Lab { 942 pub l: Scalar, 943 pub a: Scalar, 944 pub b: Scalar, 945 pub alpha: Scalar, 946 } 947 948 impl ColorSpace for Lab { from_color(c: &Color) -> Self949 fn from_color(c: &Color) -> Self { 950 c.to_lab() 951 } 952 into_color(&self) -> Color953 fn into_color(&self) -> Color { 954 Color::from_lab(self.l, self.a, self.b, self.alpha) 955 } 956 mix(&self, other: &Self, fraction: Fraction) -> Self957 fn mix(&self, other: &Self, fraction: Fraction) -> Self { 958 Self { 959 l: interpolate(self.l, other.l, fraction), 960 a: interpolate(self.a, other.a, fraction), 961 b: interpolate(self.b, other.b, fraction), 962 alpha: interpolate(self.alpha, other.alpha, fraction), 963 } 964 } 965 } 966 967 impl From<&Color> for Lab { from(color: &Color) -> Self968 fn from(color: &Color) -> Self { 969 let rec = XYZ::from(color); 970 971 let cut = Scalar::powf(6.0 / 29.0, 3.0); 972 let f = |t| { 973 if t > cut { 974 Scalar::powf(t, 1.0 / 3.0) 975 } else { 976 (1.0 / 3.0) * Scalar::powf(29.0 / 6.0, 2.0) * t + 4.0 / 29.0 977 } 978 }; 979 980 let fy = f(rec.y / D65_YN); 981 982 let l = 116.0 * fy - 16.0; 983 let a = 500.0 * (f(rec.x / D65_XN) - fy); 984 let b = 200.0 * (fy - f(rec.z / D65_ZN)); 985 986 Lab { 987 l, 988 a, 989 b, 990 alpha: color.alpha, 991 } 992 } 993 } 994 995 impl fmt::Display for Lab { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result996 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 997 write!(f, "Lab({l}, {a}, {b})", l = self.l, a = self.a, b = self.b,) 998 } 999 } 1000 1001 #[derive(Debug, Clone, PartialEq)] 1002 pub struct LCh { 1003 pub l: Scalar, 1004 pub c: Scalar, 1005 pub h: Scalar, 1006 pub alpha: Scalar, 1007 } 1008 1009 impl ColorSpace for LCh { from_color(c: &Color) -> Self1010 fn from_color(c: &Color) -> Self { 1011 c.to_lch() 1012 } 1013 into_color(&self) -> Color1014 fn into_color(&self) -> Color { 1015 Color::from_lch(self.l, self.c, self.h, self.alpha) 1016 } 1017 mix(&self, other: &Self, fraction: Fraction) -> Self1018 fn mix(&self, other: &Self, fraction: Fraction) -> Self { 1019 // make sure that the hue is preserved when mixing with gray colors 1020 let self_hue = if self.c < 0.1 { other.h } else { self.h }; 1021 let other_hue = if other.c < 0.1 { self.h } else { other.h }; 1022 1023 Self { 1024 l: interpolate(self.l, other.l, fraction), 1025 c: interpolate(self.c, other.c, fraction), 1026 h: interpolate_angle(self_hue, other_hue, fraction), 1027 alpha: interpolate(self.alpha, other.alpha, fraction), 1028 } 1029 } 1030 } 1031 1032 impl From<&Color> for LCh { from(color: &Color) -> Self1033 fn from(color: &Color) -> Self { 1034 let Lab { l, a, b, alpha } = Lab::from(color); 1035 1036 const RAD2DEG: Scalar = 180.0 / std::f64::consts::PI; 1037 1038 let c = Scalar::sqrt(a * a + b * b); 1039 let h = mod_positive(Scalar::atan2(b, a) * RAD2DEG, 360.0); 1040 1041 LCh { l, c, h, alpha } 1042 } 1043 } 1044 1045 impl fmt::Display for LCh { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result1046 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1047 write!(f, "LCh({l}, {c}, {h})", l = self.l, c = self.c, h = self.h,) 1048 } 1049 } 1050 1051 #[derive(Debug, Clone, PartialEq)] 1052 pub struct CMYK { 1053 pub c: Scalar, 1054 pub m: Scalar, 1055 pub y: Scalar, 1056 pub k: Scalar, 1057 } 1058 1059 impl From<&Color> for CMYK { from(color: &Color) -> Self1060 fn from(color: &Color) -> Self { 1061 let rgba = RGBA::<u8>::from(color); 1062 let r = (rgba.r as f64) / 255.0; 1063 let g = (rgba.g as f64) / 255.0; 1064 let b = (rgba.b as f64) / 255.0; 1065 let biggest = if r >= g && r >= b { 1066 r 1067 } else if g >= r && g >= b { 1068 g 1069 } else { 1070 b 1071 }; 1072 let out_k = 1.0 - biggest; 1073 let out_c = (1.0 - r - out_k) / biggest; 1074 let out_m = (1.0 - g - out_k) / biggest; 1075 let out_y = (1.0 - b - out_k) / biggest; 1076 1077 CMYK { 1078 c: if out_c.is_nan() { 0.0 } else { out_c }, 1079 m: if out_m.is_nan() { 0.0 } else { out_m }, 1080 y: if out_y.is_nan() { 0.0 } else { out_y }, 1081 k: out_k, 1082 } 1083 } 1084 } 1085 1086 impl fmt::Display for CMYK { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result1087 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1088 write!( 1089 f, 1090 "cmyk({c}, {m}, {y}, {k})", 1091 c = self.c, 1092 m = self.m, 1093 y = self.y, 1094 k = self.k, 1095 ) 1096 } 1097 } 1098 1099 /// A representation of the different kinds of colorblindness. More info 1100 /// [here](https://en.wikipedia.org/wiki/Color_blindness). 1101 pub enum ColorblindnessType { 1102 /// Protanopic people lack red cones 1103 Protanopia, 1104 /// Deuteranopic people lack green cones 1105 Deuteranopia, 1106 /// Tritanopic people lack blue cones 1107 Tritanopia, 1108 } 1109 1110 #[derive(Debug, Clone, Copy, PartialEq)] 1111 pub enum Format { 1112 Spaces, 1113 NoSpaces, 1114 } 1115 1116 /// The representation of a color stop for a `ColorScale`. 1117 /// The position defines where the color is placed from left (0.0) to right (1.0). 1118 #[derive(Debug, Clone)] 1119 struct ColorStop { 1120 color: Color, 1121 position: Fraction, 1122 } 1123 1124 /// The representation of a color scale. 1125 /// The first `ColorStop` (position 0.0) defines the left end color. 1126 /// The last `ColorStop` (position 1.0) defines the right end color. 1127 #[derive(Debug, Clone)] 1128 pub struct ColorScale { 1129 color_stops: Vec<ColorStop>, 1130 } 1131 1132 impl ColorScale { 1133 /// Create an empty `ColorScale`. empty() -> Self1134 pub fn empty() -> Self { 1135 Self { 1136 color_stops: Vec::new(), 1137 } 1138 } 1139 1140 /// Add a `Color` at the given position. add_stop(&mut self, color: Color, position: Fraction) -> &mut Self1141 pub fn add_stop(&mut self, color: Color, position: Fraction) -> &mut Self { 1142 #![allow(clippy::float_cmp)] 1143 let same_position = self 1144 .color_stops 1145 .iter_mut() 1146 .find(|c| position.value() == c.position.value()); 1147 1148 match same_position { 1149 Some(color_stop) => color_stop.color = color, 1150 None => { 1151 let next_index = self 1152 .color_stops 1153 .iter() 1154 .position(|c| position.value() < c.position.value()); 1155 1156 let index = next_index.unwrap_or_else(|| self.color_stops.len()); 1157 1158 let color_stop = ColorStop { color, position }; 1159 1160 self.color_stops.insert(index, color_stop); 1161 } 1162 }; 1163 1164 self 1165 } 1166 1167 /// Get the color at the given position using the mixing function. 1168 /// 1169 /// Note: 1170 /// - No color is returned if position isn't between two color stops or the `ColorScale` is empty. sample( &self, position: Fraction, mix: &dyn Fn(&Color, &Color, Fraction) -> Color, ) -> Option<Color>1171 pub fn sample( 1172 &self, 1173 position: Fraction, 1174 mix: &dyn Fn(&Color, &Color, Fraction) -> Color, 1175 ) -> Option<Color> { 1176 if self.color_stops.len() < 2 { 1177 return None; 1178 } 1179 1180 let left_stop = self 1181 .color_stops 1182 .iter() 1183 .rev() 1184 .find(|c| position.value() >= c.position.value()); 1185 1186 let right_stop = self 1187 .color_stops 1188 .iter() 1189 .find(|c| position.value() <= c.position.value()); 1190 1191 match (left_stop, right_stop) { 1192 (Some(left_stop), Some(right_stop)) => { 1193 let diff_color_stops = right_stop.position.value() - left_stop.position.value(); 1194 let diff_position = position.value() - left_stop.position.value(); 1195 let local_position = Fraction::from(diff_position / diff_color_stops); 1196 1197 let color = mix(&left_stop.color, &right_stop.color, local_position); 1198 1199 Some(color) 1200 } 1201 _ => None, 1202 } 1203 } 1204 } 1205 1206 #[cfg(test)] 1207 mod tests { 1208 use super::*; 1209 use approx::assert_relative_eq; 1210 assert_almost_equal(c1: &Color, c2: &Color)1211 fn assert_almost_equal(c1: &Color, c2: &Color) { 1212 let c1 = c1.to_rgba(); 1213 let c2 = c2.to_rgba(); 1214 1215 assert!((c1.r as i32 - c2.r as i32).abs() <= 1); 1216 assert!((c1.g as i32 - c2.g as i32).abs() <= 1); 1217 assert!((c1.b as i32 - c2.b as i32).abs() <= 1); 1218 } 1219 1220 #[test] color_partial_eq()1221 fn color_partial_eq() { 1222 assert_eq!( 1223 Color::from_hsl(120.0, 0.3, 0.5), 1224 Color::from_hsl(360.0 + 120.0, 0.3, 0.5), 1225 ); 1226 assert_eq!( 1227 Color::from_rgba(1, 2, 3, 0.3), 1228 Color::from_rgba(1, 2, 3, 0.3), 1229 ); 1230 assert_eq!(Color::black(), Color::from_hsl(123.0, 0.3, 0.0)); 1231 assert_eq!(Color::white(), Color::from_hsl(123.0, 0.3, 1.0)); 1232 1233 assert_ne!( 1234 Color::from_hsl(120.0, 0.3, 0.5), 1235 Color::from_hsl(122.0, 0.3, 0.5), 1236 ); 1237 assert_ne!( 1238 Color::from_hsl(120.0, 0.3, 0.5), 1239 Color::from_hsl(120.0, 0.32, 0.5), 1240 ); 1241 assert_ne!( 1242 Color::from_hsl(120.0, 0.3, 0.5), 1243 Color::from_hsl(120.0, 0.3, 0.52), 1244 ); 1245 assert_ne!( 1246 Color::from_hsla(120.0, 0.3, 0.5, 0.9), 1247 Color::from_hsla(120.0, 0.3, 0.5, 0.901), 1248 ); 1249 assert_ne!( 1250 Color::from_rgba(1, 2, 3, 0.3), 1251 Color::from_rgba(2, 2, 3, 0.3), 1252 ); 1253 assert_ne!( 1254 Color::from_rgba(1, 2, 3, 0.3), 1255 Color::from_rgba(1, 3, 3, 0.3), 1256 ); 1257 assert_ne!( 1258 Color::from_rgba(1, 2, 3, 0.3), 1259 Color::from_rgba(1, 2, 4, 0.3), 1260 ); 1261 } 1262 1263 #[test] rgb_to_hsl_conversion()1264 fn rgb_to_hsl_conversion() { 1265 assert_eq!(Color::white(), Color::from_rgb_float(1.0, 1.0, 1.0)); 1266 assert_eq!(Color::gray(), Color::from_rgb_float(0.5, 0.5, 0.5)); 1267 assert_eq!(Color::black(), Color::from_rgb_float(0.0, 0.0, 0.0)); 1268 assert_eq!(Color::red(), Color::from_rgb_float(1.0, 0.0, 0.0)); 1269 assert_eq!( 1270 Color::from_hsl(60.0, 1.0, 0.375), 1271 Color::from_rgb_float(0.75, 0.75, 0.0) 1272 ); //yellow-green 1273 assert_eq!(Color::green(), Color::from_rgb_float(0.0, 0.5, 0.0)); 1274 assert_eq!( 1275 Color::from_hsl(240.0, 1.0, 0.75), 1276 Color::from_rgb_float(0.5, 0.5, 1.0) 1277 ); // blue-ish 1278 assert_eq!( 1279 Color::from_hsl(49.5, 0.893, 0.497), 1280 Color::from_rgb_float(0.941, 0.785, 0.053) 1281 ); // yellow 1282 assert_eq!( 1283 Color::from_hsl(162.4, 0.779, 0.447), 1284 Color::from_rgb_float(0.099, 0.795, 0.591) 1285 ); // cyan 2 1286 } 1287 1288 #[test] rgb_roundtrip_conversion()1289 fn rgb_roundtrip_conversion() { 1290 let roundtrip = |h, s, l| { 1291 let color1 = Color::from_hsl(h, s, l); 1292 let rgb = color1.to_rgba(); 1293 let color2 = Color::from_rgb(rgb.r, rgb.g, rgb.b); 1294 assert_eq!(color1, color2); 1295 }; 1296 1297 roundtrip(0.0, 0.0, 1.0); 1298 roundtrip(0.0, 0.0, 0.5); 1299 roundtrip(0.0, 0.0, 0.0); 1300 roundtrip(60.0, 1.0, 0.375); 1301 roundtrip(120.0, 1.0, 0.25); 1302 roundtrip(240.0, 1.0, 0.75); 1303 roundtrip(49.5, 0.893, 0.497); 1304 roundtrip(162.4, 0.779, 0.447); 1305 1306 for degree in 0..360 { 1307 roundtrip(Scalar::from(degree), 0.5, 0.8); 1308 } 1309 } 1310 1311 #[test] to_u32()1312 fn to_u32() { 1313 assert_eq!(0, Color::black().to_u32()); 1314 assert_eq!(0xff0000, Color::red().to_u32()); 1315 assert_eq!(0xffffff, Color::white().to_u32()); 1316 assert_eq!(0xf4230f, Color::from_rgb(0xf4, 0x23, 0x0f).to_u32()); 1317 } 1318 1319 #[test] xyz_conversion()1320 fn xyz_conversion() { 1321 assert_eq!(Color::white(), Color::from_xyz(0.9505, 1.0, 1.0890, 1.0)); 1322 assert_eq!(Color::red(), Color::from_xyz(0.4123, 0.2126, 0.01933, 1.0)); 1323 assert_eq!( 1324 Color::from_hsl(109.999, 0.08654, 0.407843), 1325 Color::from_xyz(0.13123, 0.15372, 0.13174, 1.0) 1326 ); 1327 1328 let roundtrip = |h, s, l| { 1329 let color1 = Color::from_hsl(h, s, l); 1330 let xyz1 = color1.to_xyz(); 1331 let color2 = Color::from_xyz(xyz1.x, xyz1.y, xyz1.z, 1.0); 1332 assert_almost_equal(&color1, &color2); 1333 }; 1334 1335 for hue in 0..360 { 1336 roundtrip(Scalar::from(hue), 0.2, 0.8); 1337 } 1338 } 1339 1340 #[test] lms_conversion()1341 fn lms_conversion() { 1342 let roundtrip = |h, s, l| { 1343 let color1 = Color::from_hsl(h, s, l); 1344 let lms1 = color1.to_lms(); 1345 let color2 = Color::from_lms(lms1.l, lms1.m, lms1.s, 1.0); 1346 assert_almost_equal(&color1, &color2); 1347 }; 1348 1349 for hue in 0..360 { 1350 roundtrip(Scalar::from(hue), 0.2, 0.8); 1351 } 1352 } 1353 1354 #[test] lab_conversion()1355 fn lab_conversion() { 1356 assert_eq!(Color::red(), Color::from_lab(53.233, 80.109, 67.22, 1.0)); 1357 1358 let roundtrip = |h, s, l| { 1359 let color1 = Color::from_hsl(h, s, l); 1360 let lab1 = color1.to_lab(); 1361 let color2 = Color::from_lab(lab1.l, lab1.a, lab1.b, 1.0); 1362 assert_almost_equal(&color1, &color2); 1363 }; 1364 1365 for hue in 0..360 { 1366 roundtrip(Scalar::from(hue), 0.2, 0.8); 1367 } 1368 } 1369 1370 #[test] lch_conversion()1371 fn lch_conversion() { 1372 assert_eq!( 1373 Color::from_hsl(0.0, 1.0, 0.245), 1374 Color::from_lch(24.829, 60.093, 38.18, 1.0) 1375 ); 1376 1377 let roundtrip = |h, s, l| { 1378 let color1 = Color::from_hsl(h, s, l); 1379 let lch1 = color1.to_lch(); 1380 let color2 = Color::from_lch(lch1.l, lch1.c, lch1.h, 1.0); 1381 assert_almost_equal(&color1, &color2); 1382 }; 1383 1384 for hue in 0..360 { 1385 roundtrip(Scalar::from(hue), 0.2, 0.8); 1386 } 1387 } 1388 1389 #[test] rotate_hue()1390 fn rotate_hue() { 1391 assert_eq!(Color::lime(), Color::red().rotate_hue(120.0)); 1392 } 1393 1394 #[test] complementary()1395 fn complementary() { 1396 assert_eq!(Color::fuchsia(), Color::lime().complementary()); 1397 assert_eq!(Color::lime(), Color::fuchsia().complementary()); 1398 } 1399 1400 #[test] lighten()1401 fn lighten() { 1402 assert_eq!( 1403 Color::from_hsl(90.0, 0.5, 0.7), 1404 Color::from_hsl(90.0, 0.5, 0.3).lighten(0.4) 1405 ); 1406 assert_eq!( 1407 Color::from_hsl(90.0, 0.5, 1.0), 1408 Color::from_hsl(90.0, 0.5, 0.3).lighten(0.8) 1409 ); 1410 } 1411 1412 #[test] to_gray()1413 fn to_gray() { 1414 let salmon = Color::from_rgb(250, 128, 114); 1415 assert_eq!(0.0, salmon.to_gray().to_hsla().s); 1416 assert_relative_eq!( 1417 salmon.luminance(), 1418 salmon.to_gray().luminance(), 1419 max_relative = 0.01 1420 ); 1421 1422 assert_eq!(Color::graytone(0.3), Color::graytone(0.3).to_gray()); 1423 } 1424 1425 #[test] brightness()1426 fn brightness() { 1427 assert_eq!(0.0, Color::black().brightness()); 1428 assert_eq!(1.0, Color::white().brightness()); 1429 assert_eq!(0.5, Color::graytone(0.5).brightness()); 1430 } 1431 1432 #[test] luminance()1433 fn luminance() { 1434 assert_eq!(1.0, Color::white().luminance()); 1435 let hotpink = Color::from_rgb(255, 105, 180); 1436 assert_relative_eq!(0.347, hotpink.luminance(), max_relative = 0.01); 1437 assert_eq!(0.0, Color::black().luminance()); 1438 } 1439 1440 #[test] contrast_ratio()1441 fn contrast_ratio() { 1442 assert_relative_eq!(21.0, Color::black().contrast_ratio(&Color::white())); 1443 assert_relative_eq!(21.0, Color::white().contrast_ratio(&Color::black())); 1444 1445 assert_relative_eq!(1.0, Color::white().contrast_ratio(&Color::white())); 1446 assert_relative_eq!(1.0, Color::red().contrast_ratio(&Color::red())); 1447 1448 assert_relative_eq!( 1449 4.26, 1450 Color::from_rgb(255, 119, 153).contrast_ratio(&Color::from_rgb(0, 68, 85)), 1451 max_relative = 0.01 1452 ); 1453 } 1454 1455 #[test] text_color()1456 fn text_color() { 1457 assert_eq!(Color::white(), Color::graytone(0.4).text_color()); 1458 assert_eq!(Color::black(), Color::graytone(0.6).text_color()); 1459 } 1460 1461 #[test] distance_delta_e_cie76()1462 fn distance_delta_e_cie76() { 1463 let c = Color::from_rgb(255, 127, 14); 1464 assert_eq!(0.0, c.distance_delta_e_cie76(&c)); 1465 1466 let c1 = Color::from_rgb(50, 100, 200); 1467 let c2 = Color::from_rgb(200, 10, 0); 1468 assert_eq!(123.0, c1.distance_delta_e_cie76(&c2).round()); 1469 } 1470 1471 #[test] to_hsl_string()1472 fn to_hsl_string() { 1473 let c = Color::from_hsl(91.3, 0.541, 0.983); 1474 assert_eq!("hsl(91, 54.1%, 98.3%)", c.to_hsl_string(Format::Spaces)); 1475 } 1476 1477 #[test] to_rgb_string()1478 fn to_rgb_string() { 1479 let c = Color::from_rgb(255, 127, 4); 1480 assert_eq!("rgb(255, 127, 4)", c.to_rgb_string(Format::Spaces)); 1481 } 1482 1483 #[test] to_rgb_float_string()1484 fn to_rgb_float_string() { 1485 assert_eq!( 1486 "rgb(0.000, 0.000, 0.000)", 1487 Color::black().to_rgb_float_string(Format::Spaces) 1488 ); 1489 1490 assert_eq!( 1491 "rgb(1.000, 1.000, 1.000)", 1492 Color::white().to_rgb_float_string(Format::Spaces) 1493 ); 1494 1495 // some minor rounding errors here, but that is to be expected: 1496 let c = Color::from_rgb_float(0.12, 0.45, 0.78); 1497 assert_eq!( 1498 "rgb(0.122, 0.451, 0.780)", 1499 c.to_rgb_float_string(Format::Spaces) 1500 ); 1501 } 1502 1503 #[test] to_rgb_hex_string()1504 fn to_rgb_hex_string() { 1505 let c = Color::from_rgb(255, 127, 4); 1506 assert_eq!("ff7f04", c.to_rgb_hex_string(false)); 1507 assert_eq!("#ff7f04", c.to_rgb_hex_string(true)); 1508 } 1509 1510 #[test] to_lab_string()1511 fn to_lab_string() { 1512 let c = Color::from_lab(41.0, 83.0, -93.0, 1.0); 1513 assert_eq!("Lab(41, 83, -93)", c.to_lab_string(Format::Spaces)); 1514 } 1515 1516 #[test] to_lch_string()1517 fn to_lch_string() { 1518 let c = Color::from_lch(52.0, 44.0, 271.0, 1.0); 1519 assert_eq!("LCh(52, 44, 271)", c.to_lch_string(Format::Spaces)); 1520 } 1521 1522 #[test] mix()1523 fn mix() { 1524 assert_eq!( 1525 Color::purple(), 1526 Color::red().mix::<RGBA<f64>>(&Color::blue(), Fraction::from(0.5)) 1527 ); 1528 assert_eq!( 1529 Color::fuchsia(), 1530 Color::red().mix::<HSLA>(&Color::blue(), Fraction::from(0.5)) 1531 ); 1532 } 1533 1534 #[test] mixing_with_gray_preserves_hue()1535 fn mixing_with_gray_preserves_hue() { 1536 let hue = 123.0; 1537 1538 let input = Color::from_hsla(hue, 0.5, 0.5, 1.0); 1539 1540 let hue_after_mixing = |other| input.mix::<HSLA>(&other, Fraction::from(0.5)).to_hsla().h; 1541 1542 assert_eq!(hue, hue_after_mixing(Color::black())); 1543 assert_eq!(hue, hue_after_mixing(Color::graytone(0.2))); 1544 assert_eq!(hue, hue_after_mixing(Color::graytone(0.7))); 1545 assert_eq!(hue, hue_after_mixing(Color::white())); 1546 } 1547 1548 #[test] color_scale_add_preserves_ordering()1549 fn color_scale_add_preserves_ordering() { 1550 let mut color_scale = ColorScale::empty(); 1551 1552 color_scale 1553 .add_stop(Color::red(), Fraction::from(0.5)) 1554 .add_stop(Color::gray(), Fraction::from(0.0)) 1555 .add_stop(Color::blue(), Fraction::from(1.0)); 1556 1557 assert_eq!(color_scale.color_stops.get(0).unwrap().color, Color::gray()); 1558 assert_eq!(color_scale.color_stops.get(1).unwrap().color, Color::red()); 1559 assert_eq!(color_scale.color_stops.get(2).unwrap().color, Color::blue()); 1560 } 1561 1562 #[test] color_scale_empty_sample_none()1563 fn color_scale_empty_sample_none() { 1564 let mix = Color::mix::<Lab>; 1565 1566 let color_scale = ColorScale::empty(); 1567 1568 let color = color_scale.sample(Fraction::from(0.0), &mix); 1569 1570 assert_eq!(color, None); 1571 } 1572 1573 #[test] color_scale_one_color_sample_none()1574 fn color_scale_one_color_sample_none() { 1575 let mix = Color::mix::<Lab>; 1576 1577 let mut color_scale = ColorScale::empty(); 1578 1579 color_scale.add_stop(Color::red(), Fraction::from(0.0)); 1580 1581 let color = color_scale.sample(Fraction::from(0.0), &mix); 1582 1583 assert_eq!(color, None); 1584 } 1585 1586 #[test] color_scale_sample_same_position()1587 fn color_scale_sample_same_position() { 1588 let mix = Color::mix::<Lab>; 1589 1590 let mut color_scale = ColorScale::empty(); 1591 1592 color_scale 1593 .add_stop(Color::red(), Fraction::from(0.0)) 1594 .add_stop(Color::green(), Fraction::from(1.0)) 1595 .add_stop(Color::blue(), Fraction::from(0.0)) 1596 .add_stop(Color::white(), Fraction::from(1.0)); 1597 1598 let sample_blue = color_scale.sample(Fraction::from(0.0), &mix).unwrap(); 1599 let sample_white = color_scale.sample(Fraction::from(1.0), &mix).unwrap(); 1600 1601 assert_eq!(sample_blue, Color::blue()); 1602 assert_eq!(sample_white, Color::white()); 1603 } 1604 1605 #[test] color_scale_sample()1606 fn color_scale_sample() { 1607 let mix = Color::mix::<Lab>; 1608 1609 let mut color_scale = ColorScale::empty(); 1610 1611 color_scale 1612 .add_stop(Color::green(), Fraction::from(1.0)) 1613 .add_stop(Color::red(), Fraction::from(0.0)); 1614 1615 let sample_red_green = color_scale.sample(Fraction::from(0.5), &mix).unwrap(); 1616 1617 let mix_red_green = mix(&Color::red(), &Color::green(), Fraction::from(0.5)); 1618 1619 assert_eq!(sample_red_green, mix_red_green); 1620 } 1621 1622 #[test] color_scale_sample_position()1623 fn color_scale_sample_position() { 1624 let mix = Color::mix::<Lab>; 1625 1626 let mut color_scale = ColorScale::empty(); 1627 1628 color_scale 1629 .add_stop(Color::green(), Fraction::from(0.5)) 1630 .add_stop(Color::red(), Fraction::from(0.0)) 1631 .add_stop(Color::blue(), Fraction::from(1.0)); 1632 1633 let sample_red = color_scale.sample(Fraction::from(0.0), &mix).unwrap(); 1634 let sample_green = color_scale.sample(Fraction::from(0.5), &mix).unwrap(); 1635 let sample_blue = color_scale.sample(Fraction::from(1.0), &mix).unwrap(); 1636 1637 let sample_red_green = color_scale.sample(Fraction::from(0.25), &mix).unwrap(); 1638 let sample_green_blue = color_scale.sample(Fraction::from(0.75), &mix).unwrap(); 1639 1640 let mix_red_green = mix(&Color::red(), &Color::green(), Fraction::from(0.50)); 1641 let mix_green_blue = mix(&Color::green(), &Color::blue(), Fraction::from(0.50)); 1642 1643 assert_eq!(sample_red, Color::red()); 1644 assert_eq!(sample_green, Color::green()); 1645 assert_eq!(sample_blue, Color::blue()); 1646 1647 assert_eq!(sample_red_green, mix_red_green); 1648 assert_eq!(sample_green_blue, mix_green_blue); 1649 } 1650 1651 #[test] to_cmyk_string()1652 fn to_cmyk_string() { 1653 let white = Color::from_rgb(255, 255, 255); 1654 assert_eq!("cmyk(0, 0, 0, 0)", white.to_cmyk_string(Format::Spaces)); 1655 1656 let black = Color::from_rgb(0, 0, 0); 1657 assert_eq!("cmyk(0, 0, 0, 100)", black.to_cmyk_string(Format::Spaces)); 1658 1659 let c = Color::from_rgb(19, 19, 1); 1660 assert_eq!("cmyk(0, 0, 95, 93)", c.to_cmyk_string(Format::Spaces)); 1661 1662 let c1 = Color::from_rgb(55, 55, 55); 1663 assert_eq!("cmyk(0, 0, 0, 78)", c1.to_cmyk_string(Format::Spaces)); 1664 1665 let c2 = Color::from_rgb(136, 117, 78); 1666 assert_eq!("cmyk(0, 14, 43, 47)", c2.to_cmyk_string(Format::Spaces)); 1667 1668 let c3 = Color::from_rgb(143, 111, 76); 1669 assert_eq!("cmyk(0, 22, 47, 44)", c3.to_cmyk_string(Format::Spaces)); 1670 } 1671 } 1672