convert.rs (29743B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 //! Color conversion algorithms. 6 //! 7 //! Algorithms, matrices and constants are from the [color-4] specification, 8 //! unless otherwise specified: 9 //! 10 //! https://drafts.csswg.org/css-color-4/#color-conversion-code 11 //! 12 //! NOTE: Matrices has to be transposed from the examples in the spec for use 13 //! with the `euclid` library. 14 15 use crate::color::ColorComponents; 16 use crate::values::normalize; 17 18 type Transform = euclid::default::Transform3D<f32>; 19 type Vector = euclid::default::Vector3D<f32>; 20 21 /// Normalize hue into [0, 360). 22 #[inline] 23 pub fn normalize_hue(hue: f32) -> f32 { 24 hue - 360. * (hue / 360.).floor() 25 } 26 27 /// Calculate the hue from RGB components and return it along with the min and 28 /// max RGB values. 29 #[inline] 30 fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { 31 let max = red.max(green).max(blue); 32 let min = red.min(green).min(blue); 33 34 let delta = max - min; 35 36 let hue = if delta != 0.0 { 37 60.0 * if max == red { 38 (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } 39 } else if max == green { 40 (blue - red) / delta + 2.0 41 } else { 42 (red - green) / delta + 4.0 43 } 44 } else { 45 f32::NAN 46 }; 47 48 (hue, min, max) 49 } 50 51 /// Convert from HSL notation to RGB notation. 52 /// https://drafts.csswg.org/css-color-4/#hsl-to-rgb 53 #[inline] 54 pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { 55 fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { 56 let hue = normalize_hue(hue); 57 58 if hue * 6.0 < 360.0 { 59 t1 + (t2 - t1) * hue / 60.0 60 } else if hue * 2.0 < 360.0 { 61 t2 62 } else if hue * 3.0 < 720.0 { 63 t1 + (t2 - t1) * (240.0 - hue) / 60.0 64 } else { 65 t1 66 } 67 } 68 69 // Convert missing components to 0.0. 70 let ColorComponents(hue, saturation, lightness) = from.map(normalize); 71 let saturation = saturation / 100.0; 72 let lightness = lightness / 100.0; 73 74 let t2 = if lightness <= 0.5 { 75 lightness * (saturation + 1.0) 76 } else { 77 lightness + saturation - lightness * saturation 78 }; 79 let t1 = lightness * 2.0 - t2; 80 81 ColorComponents( 82 hue_to_rgb(t1, t2, hue + 120.0), 83 hue_to_rgb(t1, t2, hue), 84 hue_to_rgb(t1, t2, hue - 120.0), 85 ) 86 } 87 88 /// Convert from RGB notation to HSL notation. 89 /// https://drafts.csswg.org/css-color-4/#rgb-to-hsl 90 pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { 91 let ColorComponents(red, green, blue) = *from; 92 93 let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); 94 95 let lightness = (min + max) / 2.0; 96 let delta = max - min; 97 98 let saturation = if delta != 0.0 { 99 if lightness == 0.0 || lightness == 1.0 { 100 0.0 101 } else { 102 (max - lightness) / lightness.min(1.0 - lightness) 103 } 104 } else { 105 0.0 106 }; 107 108 ColorComponents(hue, saturation * 100.0, lightness * 100.0) 109 } 110 111 /// Convert from HWB notation to RGB notation. 112 /// https://drafts.csswg.org/css-color-4/#hwb-to-rgb 113 #[inline] 114 pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { 115 // Convert missing components to 0.0. 116 let ColorComponents(hue, whiteness, blackness) = from.map(normalize); 117 118 let whiteness = whiteness / 100.0; 119 let blackness = blackness / 100.0; 120 121 if whiteness + blackness >= 1.0 { 122 let gray = whiteness / (whiteness + blackness); 123 return ColorComponents(gray, gray, gray); 124 } 125 126 let x = 1.0 - whiteness - blackness; 127 hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness) 128 } 129 130 /// Convert from RGB notation to HWB notation. 131 /// https://drafts.csswg.org/css-color-4/#rgb-to-hwb 132 #[inline] 133 pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { 134 let ColorComponents(red, green, blue) = *from; 135 136 let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); 137 138 let whiteness = min; 139 let blackness = 1.0 - max; 140 141 ColorComponents(hue, whiteness * 100.0, blackness * 100.0) 142 } 143 144 /// Calculate an epsilon for a specified range. 145 #[inline] 146 pub fn epsilon_for_range(min: f32, max: f32) -> f32 { 147 (max - min) / 1.0e5 148 } 149 150 /// Convert from the rectangular orthogonal to the cylindrical polar coordinate 151 /// system. This is used to convert (ok)lab to (ok)lch. 152 /// <https://drafts.csswg.org/css-color-4/#lab-to-lch> 153 #[inline] 154 pub fn orthogonal_to_polar(from: &ColorComponents, e: f32) -> ColorComponents { 155 let ColorComponents(lightness, a, b) = *from; 156 157 let chroma = (a * a + b * b).sqrt(); 158 159 let hue = if a.abs() < e && b.abs() < e { 160 // For extremely small values of a and b ... the reported hue angle 161 // swinging about wildly and being essentially random ... this means 162 // the hue is powerless, and treated as missing when converted into LCH 163 // or Oklch. 164 f32::NAN 165 } else if chroma.abs() < e { 166 // Very small chroma values make the hue component powerless. 167 f32::NAN 168 } else { 169 normalize_hue(b.atan2(a).to_degrees()) 170 }; 171 172 ColorComponents(lightness, chroma, hue) 173 } 174 175 /// Convert from the cylindrical polar to the rectangular orthogonal coordinate 176 /// system. This is used to convert (ok)lch to (ok)lab. 177 /// <https://drafts.csswg.org/css-color-4/#lch-to-lab> 178 #[inline] 179 pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents { 180 let ColorComponents(lightness, chroma, hue) = *from; 181 182 // A missing hue component results in an achromatic color. 183 if hue.is_nan() { 184 return ColorComponents(lightness, 0.0, 0.0); 185 } 186 187 let hue = hue.to_radians(); 188 let a = chroma * hue.cos(); 189 let b = chroma * hue.sin(); 190 191 ColorComponents(lightness, a, b) 192 } 193 194 #[inline] 195 fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { 196 let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); 197 ColorComponents(result.x, result.y, result.z) 198 } 199 200 fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents { 201 #[rustfmt::skip] 202 const MAT: Transform = Transform::new( 203 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0, 204 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0, 205 -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0, 206 0.0, 0.0, 0.0, 1.0, 207 ); 208 209 transform(from, &MAT) 210 } 211 212 fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents { 213 #[rustfmt::skip] 214 const MAT: Transform = Transform::new( 215 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0, 216 -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0, 217 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0, 218 0.0, 0.0, 0.0, 1.0, 219 ); 220 221 transform(from, &MAT) 222 } 223 224 /// A reference white that is used during color conversion. 225 pub enum WhitePoint { 226 /// D50 white reference. 227 D50, 228 /// D65 white reference. 229 D65, 230 } 231 232 impl WhitePoint { 233 const fn values(&self) -> ColorComponents { 234 // <https://drafts.csswg.org/css-color-4/#color-conversion-code> 235 match self { 236 // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585] 237 WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602), 238 // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290] 239 WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784), 240 } 241 } 242 } 243 244 fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) { 245 match (from, to) { 246 (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components), 247 (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components), 248 _ => {}, 249 } 250 } 251 252 /// A trait that allows conversion of color spaces to and from XYZ coordinate 253 /// space with a specified white point. 254 /// 255 /// Allows following the specified method of converting between color spaces: 256 /// - Convert to values to sRGB linear light. 257 /// - Convert to XYZ coordinate space. 258 /// - Adjust white point to target white point. 259 /// - Convert to sRGB linear light in target color space. 260 /// - Convert to sRGB gamma encoded in target color space. 261 /// 262 /// https://drafts.csswg.org/css-color-4/#color-conversion 263 pub trait ColorSpaceConversion { 264 /// The white point that the implementer is represented in. 265 const WHITE_POINT: WhitePoint; 266 267 /// Convert the components from sRGB gamma encoded values to sRGB linear 268 /// light values. 269 fn to_linear_light(from: &ColorComponents) -> ColorComponents; 270 271 /// Convert the components from sRGB linear light values to XYZ coordinate 272 /// space. 273 fn to_xyz(from: &ColorComponents) -> ColorComponents; 274 275 /// Convert the components from XYZ coordinate space to sRGB linear light 276 /// values. 277 fn from_xyz(from: &ColorComponents) -> ColorComponents; 278 279 /// Convert the components from sRGB linear light values to sRGB gamma 280 /// encoded values. 281 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents; 282 } 283 284 /// Convert the color components from the specified color space to XYZ and 285 /// return the components and the white point they are in. 286 pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) { 287 // Convert the color components where in-gamut values are in the range 288 // [0 - 1] to linear light (un-companded) form. 289 let result = From::to_linear_light(from); 290 291 // Convert the color components from the source color space to XYZ. 292 (From::to_xyz(&result), From::WHITE_POINT) 293 } 294 295 /// Convert the color components from XYZ at the given white point to the 296 /// specified color space. 297 pub fn from_xyz<To: ColorSpaceConversion>( 298 from: &ColorComponents, 299 white_point: WhitePoint, 300 ) -> ColorComponents { 301 let mut xyz = from.clone(); 302 303 // Convert the white point if needed. 304 convert_white_point(white_point, To::WHITE_POINT, &mut xyz); 305 306 // Convert the color from XYZ to the target color space. 307 let result = To::from_xyz(&xyz); 308 309 // Convert the color components of linear-light values in the range 310 // [0 - 1] to a gamma corrected form. 311 To::to_gamma_encoded(&result) 312 } 313 314 /// The sRGB color space. 315 /// https://drafts.csswg.org/css-color-4/#predefined-sRGB 316 pub struct Srgb; 317 318 impl Srgb { 319 #[rustfmt::skip] 320 const TO_XYZ: Transform = Transform::new( 321 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0, 322 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0, 323 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0, 324 0.0, 0.0, 0.0, 1.0, 325 ); 326 327 #[rustfmt::skip] 328 const FROM_XYZ: Transform = Transform::new( 329 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0, 330 -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0, 331 -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0, 332 0.0, 0.0, 0.0, 1.0, 333 ); 334 } 335 336 impl ColorSpaceConversion for Srgb { 337 const WHITE_POINT: WhitePoint = WhitePoint::D65; 338 339 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 340 from.clone().map(|value| { 341 let abs = value.abs(); 342 343 if abs < 0.04045 { 344 value / 12.92 345 } else { 346 value.signum() * ((abs + 0.055) / 1.055).powf(2.4) 347 } 348 }) 349 } 350 351 fn to_xyz(from: &ColorComponents) -> ColorComponents { 352 transform(from, &Self::TO_XYZ) 353 } 354 355 fn from_xyz(from: &ColorComponents) -> ColorComponents { 356 transform(from, &Self::FROM_XYZ) 357 } 358 359 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 360 from.clone().map(|value| { 361 let abs = value.abs(); 362 363 if abs > 0.0031308 { 364 value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055) 365 } else { 366 12.92 * value 367 } 368 }) 369 } 370 } 371 372 /// Color specified with hue, saturation and lightness components. 373 pub struct Hsl; 374 375 impl ColorSpaceConversion for Hsl { 376 const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; 377 378 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 379 Srgb::to_linear_light(&hsl_to_rgb(from)) 380 } 381 382 #[inline] 383 fn to_xyz(from: &ColorComponents) -> ColorComponents { 384 Srgb::to_xyz(from) 385 } 386 387 #[inline] 388 fn from_xyz(from: &ColorComponents) -> ColorComponents { 389 Srgb::from_xyz(from) 390 } 391 392 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 393 rgb_to_hsl(&Srgb::to_gamma_encoded(from)) 394 } 395 } 396 397 /// Color specified with hue, whiteness and blackness components. 398 pub struct Hwb; 399 400 impl ColorSpaceConversion for Hwb { 401 const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; 402 403 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 404 Srgb::to_linear_light(&hwb_to_rgb(from)) 405 } 406 407 #[inline] 408 fn to_xyz(from: &ColorComponents) -> ColorComponents { 409 Srgb::to_xyz(from) 410 } 411 412 #[inline] 413 fn from_xyz(from: &ColorComponents) -> ColorComponents { 414 Srgb::from_xyz(from) 415 } 416 417 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 418 rgb_to_hwb(&Srgb::to_gamma_encoded(from)) 419 } 420 } 421 422 /// The same as sRGB color space, except the transfer function is linear light. 423 /// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear 424 pub struct SrgbLinear; 425 426 impl ColorSpaceConversion for SrgbLinear { 427 const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; 428 429 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 430 // Already in linear light form. 431 from.clone() 432 } 433 434 fn to_xyz(from: &ColorComponents) -> ColorComponents { 435 Srgb::to_xyz(from) 436 } 437 438 fn from_xyz(from: &ColorComponents) -> ColorComponents { 439 Srgb::from_xyz(from) 440 } 441 442 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 443 // Stay in linear light form. 444 from.clone() 445 } 446 } 447 448 /// The Display-P3 color space. 449 /// https://drafts.csswg.org/css-color-4/#predefined-display-p3 450 pub struct DisplayP3; 451 452 impl DisplayP3 { 453 #[rustfmt::skip] 454 const TO_XYZ: Transform = Transform::new( 455 0.48657094864821626, 0.22897456406974884, 0.0, 0.0, 456 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0, 457 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0, 458 0.0, 0.0, 0.0, 1.0, 459 ); 460 461 #[rustfmt::skip] 462 const FROM_XYZ: Transform = Transform::new( 463 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0, 464 -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0, 465 -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0, 466 0.0, 0.0, 0.0, 1.0, 467 ); 468 } 469 470 impl ColorSpaceConversion for DisplayP3 { 471 const WHITE_POINT: WhitePoint = WhitePoint::D65; 472 473 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 474 Srgb::to_linear_light(from) 475 } 476 477 fn to_xyz(from: &ColorComponents) -> ColorComponents { 478 transform(from, &Self::TO_XYZ) 479 } 480 481 fn from_xyz(from: &ColorComponents) -> ColorComponents { 482 transform(from, &Self::FROM_XYZ) 483 } 484 485 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 486 Srgb::to_gamma_encoded(from) 487 } 488 } 489 490 /// The Display-P3-linear color space. This is basically display-p3 without gamma encoding. 491 pub struct DisplayP3Linear; 492 impl ColorSpaceConversion for DisplayP3Linear { 493 const WHITE_POINT: WhitePoint = DisplayP3::WHITE_POINT; 494 495 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 496 *from 497 } 498 499 fn to_xyz(from: &ColorComponents) -> ColorComponents { 500 DisplayP3::to_xyz(from) 501 } 502 503 fn from_xyz(from: &ColorComponents) -> ColorComponents { 504 DisplayP3::from_xyz(from) 505 } 506 507 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 508 *from 509 } 510 } 511 512 /// The a98-rgb color space. 513 /// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb 514 pub struct A98Rgb; 515 516 impl A98Rgb { 517 #[rustfmt::skip] 518 const TO_XYZ: Transform = Transform::new( 519 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0, 520 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0, 521 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0, 522 0.0, 0.0, 0.0, 1.0, 523 ); 524 525 #[rustfmt::skip] 526 const FROM_XYZ: Transform = Transform::new( 527 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0, 528 -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0, 529 -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0, 530 0.0, 0.0, 0.0, 1.0, 531 ); 532 } 533 534 impl ColorSpaceConversion for A98Rgb { 535 const WHITE_POINT: WhitePoint = WhitePoint::D65; 536 537 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 538 from.clone().map(|v| v.signum() * v.abs().powf(2.19921875)) 539 } 540 541 fn to_xyz(from: &ColorComponents) -> ColorComponents { 542 transform(from, &Self::TO_XYZ) 543 } 544 545 fn from_xyz(from: &ColorComponents) -> ColorComponents { 546 transform(from, &Self::FROM_XYZ) 547 } 548 549 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 550 from.clone() 551 .map(|v| v.signum() * v.abs().powf(0.4547069271758437)) 552 } 553 } 554 555 /// The ProPhoto RGB color space. 556 /// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb 557 pub struct ProphotoRgb; 558 559 impl ProphotoRgb { 560 #[rustfmt::skip] 561 const TO_XYZ: Transform = Transform::new( 562 0.7977604896723027, 0.2880711282292934, 0.0, 0.0, 563 0.13518583717574031, 0.7118432178101014, 0.0, 0.0, 564 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0, 565 0.0, 0.0, 0.0, 1.0, 566 ); 567 568 #[rustfmt::skip] 569 const FROM_XYZ: Transform = Transform::new( 570 1.3457989731028281, -0.5446224939028347, 0.0, 0.0, 571 -0.25558010007997534, 1.5082327413132781, 0.0, 0.0, 572 -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0, 573 0.0, 0.0, 0.0, 1.0, 574 ); 575 } 576 577 impl ColorSpaceConversion for ProphotoRgb { 578 const WHITE_POINT: WhitePoint = WhitePoint::D50; 579 580 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 581 from.clone().map(|value| { 582 const ET2: f32 = 16.0 / 512.0; 583 584 let abs = value.abs(); 585 586 if abs <= ET2 { 587 value / 16.0 588 } else { 589 value.signum() * abs.powf(1.8) 590 } 591 }) 592 } 593 594 fn to_xyz(from: &ColorComponents) -> ColorComponents { 595 transform(from, &Self::TO_XYZ) 596 } 597 598 fn from_xyz(from: &ColorComponents) -> ColorComponents { 599 transform(from, &Self::FROM_XYZ) 600 } 601 602 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 603 const ET: f32 = 1.0 / 512.0; 604 605 from.clone().map(|v| { 606 let abs = v.abs(); 607 if abs >= ET { 608 v.signum() * abs.powf(1.0 / 1.8) 609 } else { 610 16.0 * v 611 } 612 }) 613 } 614 } 615 616 /// The Rec.2020 color space. 617 /// https://drafts.csswg.org/css-color-4/#predefined-rec2020 618 pub struct Rec2020; 619 620 impl Rec2020 { 621 const ALPHA: f32 = 1.09929682680944; 622 const BETA: f32 = 0.018053968510807; 623 624 #[rustfmt::skip] 625 const TO_XYZ: Transform = Transform::new( 626 0.6369580483012913, 0.26270021201126703, 0.0, 0.0, 627 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0, 628 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0, 629 0.0, 0.0, 0.0, 1.0, 630 ); 631 632 #[rustfmt::skip] 633 const FROM_XYZ: Transform = Transform::new( 634 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0, 635 -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0, 636 -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0, 637 0.0, 0.0, 0.0, 1.0, 638 ); 639 } 640 641 impl ColorSpaceConversion for Rec2020 { 642 const WHITE_POINT: WhitePoint = WhitePoint::D65; 643 644 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 645 from.clone().map(|value| { 646 let abs = value.abs(); 647 648 if abs < Self::BETA * 4.5 { 649 value / 4.5 650 } else { 651 value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45) 652 } 653 }) 654 } 655 656 fn to_xyz(from: &ColorComponents) -> ColorComponents { 657 transform(from, &Self::TO_XYZ) 658 } 659 660 fn from_xyz(from: &ColorComponents) -> ColorComponents { 661 transform(from, &Self::FROM_XYZ) 662 } 663 664 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 665 from.clone().map(|v| { 666 let abs = v.abs(); 667 668 if abs > Self::BETA { 669 v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0)) 670 } else { 671 4.5 * v 672 } 673 }) 674 } 675 } 676 677 /// A color in the XYZ coordinate space with a D50 white reference. 678 /// https://drafts.csswg.org/css-color-4/#predefined-xyz 679 pub struct XyzD50; 680 681 impl ColorSpaceConversion for XyzD50 { 682 const WHITE_POINT: WhitePoint = WhitePoint::D50; 683 684 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 685 from.clone() 686 } 687 688 fn to_xyz(from: &ColorComponents) -> ColorComponents { 689 from.clone() 690 } 691 692 fn from_xyz(from: &ColorComponents) -> ColorComponents { 693 from.clone() 694 } 695 696 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 697 from.clone() 698 } 699 } 700 701 /// A color in the XYZ coordinate space with a D65 white reference. 702 /// https://drafts.csswg.org/css-color-4/#predefined-xyz 703 pub struct XyzD65; 704 705 impl ColorSpaceConversion for XyzD65 { 706 const WHITE_POINT: WhitePoint = WhitePoint::D65; 707 708 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 709 from.clone() 710 } 711 712 fn to_xyz(from: &ColorComponents) -> ColorComponents { 713 from.clone() 714 } 715 716 fn from_xyz(from: &ColorComponents) -> ColorComponents { 717 from.clone() 718 } 719 720 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 721 from.clone() 722 } 723 } 724 725 /// The Lab color space. 726 /// https://drafts.csswg.org/css-color-4/#specifying-lab-lch 727 pub struct Lab; 728 729 impl Lab { 730 const KAPPA: f32 = 24389.0 / 27.0; 731 const EPSILON: f32 = 216.0 / 24389.0; 732 } 733 734 impl ColorSpaceConversion for Lab { 735 const WHITE_POINT: WhitePoint = WhitePoint::D50; 736 737 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 738 // No need for conversion. 739 from.clone() 740 } 741 742 /// Convert a CIELAB color to XYZ as specified in [1] and [2]. 743 /// 744 /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined 745 /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code 746 fn to_xyz(from: &ColorComponents) -> ColorComponents { 747 let ColorComponents(lightness, a, b) = *from; 748 749 let f1 = (lightness + 16.0) / 116.0; 750 let f0 = f1 + a / 500.0; 751 let f2 = f1 - b / 200.0; 752 753 let f0_cubed = f0 * f0 * f0; 754 let x = if f0_cubed > Self::EPSILON { 755 f0_cubed 756 } else { 757 (116.0 * f0 - 16.0) / Self::KAPPA 758 }; 759 760 let y = if lightness > Self::KAPPA * Self::EPSILON { 761 let v = (lightness + 16.0) / 116.0; 762 v * v * v 763 } else { 764 lightness / Self::KAPPA 765 }; 766 767 let f2_cubed = f2 * f2 * f2; 768 let z = if f2_cubed > Self::EPSILON { 769 f2_cubed 770 } else { 771 (116.0 * f2 - 16.0) / Self::KAPPA 772 }; 773 774 ColorComponents(x, y, z) * Self::WHITE_POINT.values() 775 } 776 777 /// Convert an XYZ color to LAB as specified in [1] and [2]. 778 /// 779 /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab 780 /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code 781 fn from_xyz(from: &ColorComponents) -> ColorComponents { 782 let adapted = *from / Self::WHITE_POINT.values(); 783 784 // 4. Convert D50-adapted XYZ to Lab. 785 let ColorComponents(f0, f1, f2) = adapted.map(|v| { 786 if v > Self::EPSILON { 787 v.cbrt() 788 } else { 789 (Self::KAPPA * v + 16.0) / 116.0 790 } 791 }); 792 793 let lightness = 116.0 * f1 - 16.0; 794 let a = 500.0 * (f0 - f1); 795 let b = 200.0 * (f1 - f2); 796 797 ColorComponents(lightness, a, b) 798 } 799 800 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 801 // No need for conversion. 802 from.clone() 803 } 804 } 805 806 /// The Lch color space. 807 /// https://drafts.csswg.org/css-color-4/#specifying-lab-lch 808 pub struct Lch; 809 810 impl ColorSpaceConversion for Lch { 811 const WHITE_POINT: WhitePoint = Lab::WHITE_POINT; 812 813 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 814 // No need for conversion. 815 from.clone() 816 } 817 818 fn to_xyz(from: &ColorComponents) -> ColorComponents { 819 // Convert LCH to Lab first. 820 let lab = polar_to_orthogonal(from); 821 822 // Then convert the Lab to XYZ. 823 Lab::to_xyz(&lab) 824 } 825 826 fn from_xyz(from: &ColorComponents) -> ColorComponents { 827 // First convert the XYZ to LAB. 828 let lab = Lab::from_xyz(&from); 829 830 // Then convert the Lab to LCH. 831 orthogonal_to_polar(&lab, epsilon_for_range(0.0, 100.0)) 832 } 833 834 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 835 // No need for conversion. 836 from.clone() 837 } 838 } 839 840 /// The Oklab color space. 841 /// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch 842 pub struct Oklab; 843 844 impl Oklab { 845 #[rustfmt::skip] 846 const XYZ_TO_LMS: Transform = Transform::new( 847 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0, 848 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0, 849 -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0, 850 0.0, 0.0, 0.0, 1.0, 851 ); 852 853 #[rustfmt::skip] 854 const LMS_TO_OKLAB: Transform = Transform::new( 855 0.2104542553, 1.9779984951, 0.0259040371, 0.0, 856 0.7936177850, -2.4285922050, 0.7827717662, 0.0, 857 -0.0040720468, 0.4505937099, -0.8086757660, 0.0, 858 0.0, 0.0, 0.0, 1.0, 859 ); 860 861 #[rustfmt::skip] 862 const LMS_TO_XYZ: Transform = Transform::new( 863 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0, 864 -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0, 865 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0, 866 0.0, 0.0, 0.0, 1.0, 867 ); 868 869 #[rustfmt::skip] 870 const OKLAB_TO_LMS: Transform = Transform::new( 871 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0, 872 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0, 873 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0, 874 0.0, 0.0, 0.0, 1.0, 875 ); 876 } 877 878 impl ColorSpaceConversion for Oklab { 879 const WHITE_POINT: WhitePoint = WhitePoint::D65; 880 881 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 882 // No need for conversion. 883 from.clone() 884 } 885 886 fn to_xyz(from: &ColorComponents) -> ColorComponents { 887 let lms = transform(&from, &Self::OKLAB_TO_LMS); 888 let lms = lms.map(|v| v * v * v); 889 transform(&lms, &Self::LMS_TO_XYZ) 890 } 891 892 fn from_xyz(from: &ColorComponents) -> ColorComponents { 893 let lms = transform(&from, &Self::XYZ_TO_LMS); 894 let lms = lms.map(|v| v.cbrt()); 895 transform(&lms, &Self::LMS_TO_OKLAB) 896 } 897 898 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 899 // No need for conversion. 900 from.clone() 901 } 902 } 903 904 /// The Oklch color space. 905 /// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch 906 pub struct Oklch; 907 908 impl ColorSpaceConversion for Oklch { 909 const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT; 910 911 fn to_linear_light(from: &ColorComponents) -> ColorComponents { 912 // No need for conversion. 913 from.clone() 914 } 915 916 fn to_xyz(from: &ColorComponents) -> ColorComponents { 917 // First convert OkLCH to Oklab. 918 let oklab = polar_to_orthogonal(from); 919 920 // Then convert Oklab to XYZ. 921 Oklab::to_xyz(&oklab) 922 } 923 924 fn from_xyz(from: &ColorComponents) -> ColorComponents { 925 // First convert XYZ to Oklab. 926 let lab = Oklab::from_xyz(&from); 927 928 // Then convert Oklab to OkLCH. 929 orthogonal_to_polar(&lab, epsilon_for_range(0.0, 1.0)) 930 } 931 932 fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { 933 // No need for conversion. 934 from.clone() 935 } 936 }