diff options
Diffstat (limited to 'servo/components/style/color/convert.rs')
-rw-r--r-- | servo/components/style/color/convert.rs | 902 |
1 files changed, 902 insertions, 0 deletions
diff --git a/servo/components/style/color/convert.rs b/servo/components/style/color/convert.rs new file mode 100644 index 0000000000..a6274db39a --- /dev/null +++ b/servo/components/style/color/convert.rs @@ -0,0 +1,902 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color conversion algorithms. +//! +//! Algorithms, matrices and constants are from the [color-4] specification, +//! unless otherwise specified: +//! +//! https://drafts.csswg.org/css-color-4/#color-conversion-code +//! +//! NOTE: Matrices has to be transposed from the examples in the spec for use +//! with the `euclid` library. + +use crate::color::ColorComponents; +use crate::values::normalize; + +type Transform = euclid::default::Transform3D<f32>; +type Vector = euclid::default::Vector3D<f32>; + +/// Normalize hue into [0, 360). +#[inline] +pub fn normalize_hue(hue: f32) -> f32 { + hue - 360. * (hue / 360.).floor() +} + +/// Calculate the hue from RGB components and return it along with the min and +/// max RGB values. +#[inline] +fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + + let delta = max - min; + + let hue = if delta != 0.0 { + 60.0 * if max == red { + (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } + } else if max == green { + (blue - red) / delta + 2.0 + } else { + (red - green) / delta + 4.0 + } + } else { + f32::NAN + }; + + (hue, min, max) +} + +/// Convert from HSL notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb +#[inline] +pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { + fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { + let hue = normalize_hue(hue); + + if hue * 6.0 < 360.0 { + t1 + (t2 - t1) * hue / 60.0 + } else if hue * 2.0 < 360.0 { + t2 + } else if hue * 3.0 < 720.0 { + t1 + (t2 - t1) * (240.0 - hue) / 60.0 + } else { + t1 + } + } + + // Convert missing components to 0.0. + let ColorComponents(hue, saturation, lightness) = from.map(normalize); + let saturation = saturation / 100.0; + let lightness = lightness / 100.0; + + let t2 = if lightness <= 0.5 { + lightness * (saturation + 1.0) + } else { + lightness + saturation - lightness * saturation + }; + let t1 = lightness * 2.0 - t2; + + ColorComponents( + hue_to_rgb(t1, t2, hue + 120.0), + hue_to_rgb(t1, t2, hue), + hue_to_rgb(t1, t2, hue - 120.0), + ) +} + +/// Convert from RGB notation to HSL notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl +pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let lightness = (min + max) / 2.0; + let delta = max - min; + + let saturation = if delta != 0.0 { + if lightness == 0.0 || lightness == 1.0 { + 0.0 + } else { + (max - lightness) / lightness.min(1.0 - lightness) + } + } else { + 0.0 + }; + + ColorComponents(hue, saturation * 100.0, lightness * 100.0) +} + +/// Convert from HWB notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb +#[inline] +pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { + // Convert missing components to 0.0. + let ColorComponents(hue, whiteness, blackness) = from.map(normalize); + + let whiteness = whiteness / 100.0; + let blackness = blackness / 100.0; + + if whiteness + blackness >= 1.0 { + let gray = whiteness / (whiteness + blackness); + return ColorComponents(gray, gray, gray); + } + + let x = 1.0 - whiteness - blackness; + hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness) +} + +/// Convert from RGB notation to HWB notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb +#[inline] +pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let whiteness = min; + let blackness = 1.0 - max; + + ColorComponents(hue, whiteness * 100.0, blackness * 100.0) +} + +/// Convert from the rectangular orthogonal to the cylindrical polar coordinate +/// system. This is used to convert (ok)lab to (ok)lch. +/// <https://drafts.csswg.org/css-color-4/#lab-to-lch> +#[inline] +pub fn orthogonal_to_polar(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let chroma = (a * a + b * b).sqrt(); + + // Very small chroma values make the hue component powerless. + let hue = if chroma.abs() < 1.0e-6 { + f32::NAN + } else { + normalize_hue(b.atan2(a).to_degrees()) + }; + + ColorComponents(lightness, chroma, hue) +} + +/// Convert from the cylindrical polar to the rectangular orthogonal coordinate +/// system. This is used to convert (ok)lch to (ok)lab. +/// <https://drafts.csswg.org/css-color-4/#lch-to-lab> +#[inline] +pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, chroma, hue) = *from; + + // A missing hue component results in an achromatic color. + if hue.is_nan() { + return ColorComponents(lightness, 0.0, 0.0); + } + + let hue = hue.to_radians(); + let a = chroma * hue.cos(); + let b = chroma * hue.sin(); + + ColorComponents(lightness, a, b) +} + +#[inline] +fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { + let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); + ColorComponents(result.x, result.y, result.z) +} + +fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0, + 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0, + -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0, + -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0, + 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +/// A reference white that is used during color conversion. +pub enum WhitePoint { + /// D50 white reference. + D50, + /// D65 white reference. + D65, +} + +impl WhitePoint { + const fn values(&self) -> ColorComponents { + // <https://drafts.csswg.org/css-color-4/#color-conversion-code> + match self { + // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585] + WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602), + // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290] + WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784), + } + } +} + +fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) { + match (from, to) { + (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components), + (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components), + _ => {}, + } +} + +/// A trait that allows conversion of color spaces to and from XYZ coordinate +/// space with a specified white point. +/// +/// Allows following the specified method of converting between color spaces: +/// - Convert to values to sRGB linear light. +/// - Convert to XYZ coordinate space. +/// - Adjust white point to target white point. +/// - Convert to sRGB linear light in target color space. +/// - Convert to sRGB gamma encoded in target color space. +/// +/// https://drafts.csswg.org/css-color-4/#color-conversion +pub trait ColorSpaceConversion { + /// The white point that the implementer is represented in. + const WHITE_POINT: WhitePoint; + + /// Convert the components from sRGB gamma encoded values to sRGB linear + /// light values. + fn to_linear_light(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to XYZ coordinate + /// space. + fn to_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from XYZ coordinate space to sRGB linear light + /// values. + fn from_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to sRGB gamma + /// encoded values. + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents; +} + +/// Convert the color components from the specified color space to XYZ and +/// return the components and the white point they are in. +pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) { + // Convert the color components where in-gamut values are in the range + // [0 - 1] to linear light (un-companded) form. + let result = From::to_linear_light(from); + + // Convert the color components from the source color space to XYZ. + (From::to_xyz(&result), From::WHITE_POINT) +} + +/// Convert the color components from XYZ at the given white point to the +/// specified color space. +pub fn from_xyz<To: ColorSpaceConversion>( + from: &ColorComponents, + white_point: WhitePoint, +) -> ColorComponents { + let mut xyz = from.clone(); + + // Convert the white point if needed. + convert_white_point(white_point, To::WHITE_POINT, &mut xyz); + + // Convert the color from XYZ to the target color space. + let result = To::from_xyz(&xyz); + + // Convert the color components of linear-light values in the range + // [0 - 1] to a gamma corrected form. + To::to_gamma_encoded(&result) +} + +/// The sRGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB +pub struct Srgb; + +impl Srgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0, + 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0, + 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0, + -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0, + -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Srgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < 0.04045 { + value / 12.92 + } else { + value.signum() * ((abs + 0.055) / 1.055).powf(2.4) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs > 0.0031308 { + value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055) + } else { + 12.92 * value + } + }) + } +} + +/// Color specified with hue, saturation and lightness components. +pub struct Hsl; + +impl ColorSpaceConversion for Hsl { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hsl_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hsl(&Srgb::to_gamma_encoded(from)) + } +} + +/// Color specified with hue, whiteness and blackness components. +pub struct Hwb; + +impl ColorSpaceConversion for Hwb { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hwb_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hwb(&Srgb::to_gamma_encoded(from)) + } +} + +/// The same as sRGB color space, except the transfer function is linear light. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear +pub struct SrgbLinear; + +impl ColorSpaceConversion for SrgbLinear { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // Already in linear light form. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // Stay in linear light form. + from.clone() + } +} + +/// The Display-P3 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-display-p3 +pub struct DisplayP3; + +impl DisplayP3 { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.48657094864821626, 0.22897456406974884, 0.0, 0.0, + 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0, + 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0, + -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0, + -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for DisplayP3 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(from) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + Srgb::to_gamma_encoded(from) + } +} + +/// The a98-rgb color space. +/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb +pub struct A98Rgb; + +impl A98Rgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0, + 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0, + 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0, + -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0, + -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for A98Rgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| v.signum() * v.abs().powf(2.19921875)) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + .map(|v| v.signum() * v.abs().powf(0.4547069271758437)) + } +} + +/// The ProPhoto RGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb +pub struct ProphotoRgb; + +impl ProphotoRgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.7977604896723027, 0.2880711282292934, 0.0, 0.0, + 0.13518583717574031, 0.7118432178101014, 0.0, 0.0, + 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.3457989731028281, -0.5446224939028347, 0.0, 0.0, + -0.25558010007997534, 1.5082327413132781, 0.0, 0.0, + -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for ProphotoRgb { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + const ET2: f32 = 16.0 / 512.0; + + let abs = value.abs(); + + if abs <= ET2 { + value / 16.0 + } else { + value.signum() * abs.powf(1.8) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + const ET: f32 = 1.0 / 512.0; + + from.clone().map(|v| { + let abs = v.abs(); + if abs >= ET { + v.signum() * abs.powf(1.0 / 1.8) + } else { + 16.0 * v + } + }) + } +} + +/// The Rec.2020 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-rec2020 +pub struct Rec2020; + +impl Rec2020 { + const ALPHA: f32 = 1.09929682680944; + const BETA: f32 = 0.018053968510807; + + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.6369580483012913, 0.26270021201126703, 0.0, 0.0, + 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0, + 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0, + -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0, + -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Rec2020 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < Self::BETA * 4.5 { + value / 4.5 + } else { + value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| { + let abs = v.abs(); + + if abs > Self::BETA { + v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0)) + } else { + 4.5 * v + } + }) + } +} + +/// A color in the XYZ coordinate space with a D50 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD50; + +impl ColorSpaceConversion for XyzD50 { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// A color in the XYZ coordinate space with a D65 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD65; + +impl ColorSpaceConversion for XyzD65 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// The Lab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lab; + +impl Lab { + const KAPPA: f32 = 24389.0 / 27.0; + const EPSILON: f32 = 216.0 / 24389.0; +} + +impl ColorSpaceConversion for Lab { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + /// Convert a CIELAB color to XYZ as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let f1 = (lightness + 16.0) / 116.0; + let f0 = f1 + a / 500.0; + let f2 = f1 - b / 200.0; + + let f0_cubed = f0 * f0 * f0; + let x = if f0_cubed > Self::EPSILON { + f0_cubed + } else { + (116.0 * f0 - 16.0) / Self::KAPPA + }; + + let y = if lightness > Self::KAPPA * Self::EPSILON { + let v = (lightness + 16.0) / 116.0; + v * v * v + } else { + lightness / Self::KAPPA + }; + + let f2_cubed = f2 * f2 * f2; + let z = if f2_cubed > Self::EPSILON { + f2_cubed + } else { + (116.0 * f2 - 16.0) / Self::KAPPA + }; + + ColorComponents(x, y, z) * Self::WHITE_POINT.values() + } + + /// Convert an XYZ color to LAB as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let adapted = *from / Self::WHITE_POINT.values(); + + // 4. Convert D50-adapted XYZ to Lab. + let ColorComponents(f0, f1, f2) = adapted.map(|v| { + if v > Self::EPSILON { + v.cbrt() + } else { + (Self::KAPPA * v + 16.0) / 116.0 + } + }); + + let lightness = 116.0 * f1 - 16.0; + let a = 500.0 * (f0 - f1); + let b = 200.0 * (f1 - f2); + + ColorComponents(lightness, a, b) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Lch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lch; + +impl ColorSpaceConversion for Lch { + const WHITE_POINT: WhitePoint = Lab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // Convert LCH to Lab first. + let lab = polar_to_orthogonal(from); + + // Then convert the Lab to XYZ. + Lab::to_xyz(&lab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert the XYZ to LAB. + let lab = Lab::from_xyz(&from); + + // Then convert the Lab to LCH. + orthogonal_to_polar(&lab) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklab; + +impl Oklab { + #[rustfmt::skip] + const XYZ_TO_LMS: Transform = Transform::new( + 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0, + 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0, + -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_OKLAB: Transform = Transform::new( + 0.2104542553, 1.9779984951, 0.0259040371, 0.0, + 0.7936177850, -2.4285922050, 0.7827717662, 0.0, + -0.0040720468, 0.4505937099, -0.8086757660, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_XYZ: Transform = Transform::new( + 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0, + -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0, + 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const OKLAB_TO_LMS: Transform = Transform::new( + 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0, + 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0, + 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Oklab { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::OKLAB_TO_LMS); + let lms = lms.map(|v| v * v * v); + transform(&lms, &Self::LMS_TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::XYZ_TO_LMS); + let lms = lms.map(|v| v.cbrt()); + transform(&lms, &Self::LMS_TO_OKLAB) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklch; + +impl ColorSpaceConversion for Oklch { + const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // First convert OkLCH to Oklab. + let oklab = polar_to_orthogonal(from); + + // Then convert Oklab to XYZ. + Oklab::to_xyz(&oklab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert XYZ to Oklab. + let lab = Oklab::from_xyz(&from); + + // Then convert Oklab to OkLCH. + orthogonal_to_polar(&lab) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} |