diff --git a/components/style/values/animated/color.rs b/components/style/values/animated/color.rs index e6d16134428..8baa2497a01 100644 --- a/components/style/values/animated/color.rs +++ b/components/style/values/animated/color.rs @@ -9,6 +9,7 @@ use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; use crate::values::generics::color::{Color as GenericColor, ComplexColorRatios}; use crate::values::specified::color::{ColorInterpolationMethod, ColorSpace, HueInterpolationMethod}; use euclid::default::{Transform3D, Vector3D}; +use std::f32::consts::{PI, TAU}; /// An animated RGBA color. /// @@ -27,6 +28,8 @@ pub struct RGBA { pub alpha: f32, } +const RAD_PER_DEG: f32 = PI / 180.0; + impl RGBA { /// Returns a transparent color. #[inline] @@ -135,36 +138,17 @@ impl Color { right_color: &Color, right_weight: f32, ) -> Self { - match interpolation.space { - ColorSpace::Srgb => Self::mix_in::( - left_color, - left_weight, - right_color, - right_weight, - interpolation.hue, - ), - ColorSpace::Xyz => Self::mix_in::( - left_color, - left_weight, - right_color, - right_weight, - interpolation.hue, - ), - ColorSpace::Lab => Self::mix_in::( - left_color, - left_weight, - right_color, - right_weight, - interpolation.hue, - ), - ColorSpace::Lch => Self::mix_in::( - left_color, - left_weight, - right_color, - right_weight, - interpolation.hue, - ), - } + let mix_function = match interpolation.space { + ColorSpace::Srgb => Self::mix_in::, + ColorSpace::LinearSrgb => Self::mix_in::, + ColorSpace::Xyz => Self::mix_in::, + ColorSpace::XyzD50 => Self::mix_in::, + ColorSpace::Lab => Self::mix_in::, + ColorSpace::Hwb => Self::mix_in::, + ColorSpace::Hsl => Self::mix_in::, + ColorSpace::Lch => Self::mix_in::, + }; + mix_function(left_color, left_weight, right_color, right_weight, interpolation.hue) } fn mix_in( @@ -388,9 +372,18 @@ fn interpolate_premultiplied_component( (left * left_weight * left_alpha + right * right_weight * right_alpha) * inverse_of_result_alpha } -fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { - use std::f32::consts::{PI, TAU}; +// Normalize hue into [0, 2 * PI) +fn normalize_hue(mut v: f32) -> f32 { + while v < 0. { + v += TAU; + } + while v >= TAU { + v -= TAU; + } + v +} +fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { // Adjust the hue angle as per // https://drafts.csswg.org/css-color/#hue-interpolation. // @@ -413,18 +406,8 @@ fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolati return; } - // Normalize hue into [0, 2 * PI) - fn normalize(v: &mut f32) { - while *v < 0. { - *v += TAU; - } - while *v > TAU { - *v -= TAU; - } - } - - normalize(left); - normalize(right); + *left = normalize_hue(*left); + *right = normalize_hue(*right); match hue_interpolation { // https://drafts.csswg.org/css-color/#shorter @@ -534,57 +517,158 @@ macro_rules! impl_lerp { impl_lerp!(RGBA, None); -/// An animated XYZA colour. #[derive(Clone, Copy, Debug)] #[repr(C)] -pub struct XYZA { - /// The x component. - pub x: f32, - /// The y component. - pub y: f32, - /// The z component. - pub z: f32, - /// The alpha component. - pub alpha: f32, +struct LinearRGBA { + red: f32, + green: f32, + blue: f32, + alpha: f32, } -impl_lerp!(XYZA, None); +impl_lerp!(LinearRGBA, None); -/// An animated LABA colour. +/// An animated XYZ D65 colour. #[derive(Clone, Copy, Debug)] #[repr(C)] -pub struct LABA { - /// The lightness component. - pub lightness: f32, - /// The a component. - pub a: f32, - /// The b component. - pub b: f32, - /// The alpha component. - pub alpha: f32, +struct XYZD65A { + x: f32, + y: f32, + z: f32, + alpha: f32, +} + +impl_lerp!(XYZD65A, None); + +/// An animated XYZ D50 colour. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct XYZD50A { + x: f32, + y: f32, + z: f32, + alpha: f32, +} + +impl_lerp!(XYZD50A, None); + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct LABA { + lightness: f32, + a: f32, + b: f32, + alpha: f32, } impl_lerp!(LABA, None); /// An animated LCHA colour. #[derive(Clone, Copy, Debug)] -pub struct LCHA { - /// The lightness component. - pub lightness: f32, - /// The chroma component. - pub chroma: f32, - /// The hua component. - pub hue: f32, - /// The alpha component. - pub alpha: f32, +#[repr(C)] +struct LCHA { + lightness: f32, + chroma: f32, + hue: f32, + alpha: f32, } impl_lerp!(LCHA, Some(2)); -impl From for XYZA { - /// Convert an RGBA colour to XYZ as specified in [1]. - /// - /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab +/// An animated hwb() color. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct HWBA { + hue: f32, + white: f32, + black: f32, + alpha: f32, +} + +impl_lerp!(HWBA, Some(0)); + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct HSLA { + hue: f32, + sat: f32, + light: f32, + alpha: f32, +} + +impl_lerp!(HSLA, Some(0)); + +// https://drafts.csswg.org/css-color/#rgb-to-hsl +// +// We also return min/max for the hwb conversion. +fn rgb_to_hsl(rgba: RGBA) -> (HSLA, f32, f32) { + let RGBA { red, green, blue, alpha } = rgba; + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + let mut hue = std::f32::NAN; + let mut sat = 0.; + let light = (min + max) / 2.; + let d = max - min; + + if d != 0. { + sat = if light == 0.0 || light == 1.0 { + 0. + } else { + (max - light) / light.min(1. - light) + }; + + if max == red { + hue = (green - blue) / d + if green < blue { 6. } else { 0. } + } else if max == green { + hue = (blue - red) / d + 2.; + } else { + hue = (red - green) / d + 4.; + } + + hue *= 60.; + hue *= RAD_PER_DEG; + } + + (HSLA { hue, sat, light, alpha }, min, max) +} + +impl From for HSLA { + fn from(rgba: RGBA) -> Self { + rgb_to_hsl(rgba).0 + } +} + +impl From for RGBA { + fn from(hsla: HSLA) -> Self { + // cssparser expects hue in the 0..1 range. + let hue_normalized = normalize_hue(hsla.hue) / TAU; + let (r, g, b) = cssparser::hsl_to_rgb(hue_normalized, hsla.sat, hsla.light); + RGBA::new(r, g, b, hsla.alpha) + } +} + +impl From for HWBA { + // https://drafts.csswg.org/css-color/#rgb-to-hwb + fn from(rgba: RGBA) -> Self { + let (hsl, min, max) = rgb_to_hsl(rgba); + Self { + hue: hsl.hue, + white: min, + black: 1. - max, + alpha: rgba.alpha, + } + } +} + +impl From for RGBA { + fn from(hwba: HWBA) -> Self { + let hue_normalized = normalize_hue(hwba.hue) / TAU; + let (r, g, b) = cssparser::hwb_to_rgb(hue_normalized, hwba.white, hwba.black); + RGBA::new(r, g, b, hwba.alpha) + } +} + +impl From for LinearRGBA { fn from(rgba: RGBA) -> Self { fn linearize(value: f32) -> f32 { let sign = if value < 0. { -1. } else { 1. }; @@ -595,15 +679,39 @@ impl From for XYZA { sign * ((abs + 0.055) / 1.055).powf(2.4) } + Self { + red: linearize(rgba.red), + green: linearize(rgba.green), + blue: linearize(rgba.blue), + alpha: rgba.alpha, + } + } +} - #[cfg_attr(rustfmt, rustfmt_skip)] - const SRGB_TO_XYZ: Transform3D = Transform3D::new( - 0.41239079926595934, 0.21263900587151027, 0.01933081871559182, 0., - 0.357584339383878, 0.715168678767756, 0.11919477979462598, 0., - 0.1804807884018343, 0.07219231536073371, 0.9505321522496607, 0., - 0., 0., 0., 1., - ); +impl From for RGBA { + fn from(lrgba: LinearRGBA) -> Self { + fn delinearize(value: f32) -> f32 { + let sign = if value < 0. { -1. } else { 1. }; + let abs = value.abs(); + if abs > 0.0031308 { + sign * (1.055 * abs.powf(1. / 2.4) - 0.055) + } else { + 12.92 * value + } + } + Self { + red: delinearize(lrgba.red), + green: delinearize(lrgba.green), + blue: delinearize(lrgba.blue), + alpha: lrgba.alpha, + } + } +} + +impl From for XYZD50A { + fn from(d65: XYZD65A) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code #[cfg_attr(rustfmt, rustfmt_skip)] const BRADFORD: Transform3D = Transform3D::new( 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0., @@ -611,34 +719,100 @@ impl From for XYZA { -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0., 0., 0., 0., 1., ); - - // 1. Convert from sRGB to linear-light sRGB (undo gamma encoding). - let rgb = Vector3D::new( - linearize(rgba.red), - linearize(rgba.green), - linearize(rgba.blue), - ); - - // 2. Convert from linear sRGB to CIE XYZ. - // 3. Convert from a D65 whitepoint (used by sRGB) to the D50 whitepoint used in XYZ - // with the Bradford transform. - let xyz = SRGB_TO_XYZ.then(&BRADFORD).transform_vector3d(rgb); - - XYZA { - x: xyz.x, - y: xyz.y, - z: xyz.z, - alpha: rgba.alpha, + let d50 = BRADFORD.transform_vector3d(Vector3D::new(d65.x, d65.y, d65.z)); + Self { + x: d50.x, + y: d50.y, + z: d50.z, + alpha: d65.alpha, } } } -impl From for LABA { +impl From for XYZD65A { + fn from(d50: XYZD50A) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const BRADFORD_INVERSE: Transform3D = Transform3D::new( + 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0., + -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0., + 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0., + 0., 0., 0., 1., + ); + let d65 = BRADFORD_INVERSE.transform_vector3d(Vector3D::new(d50.x, d50.y, d50.z)); + Self { + x: d65.x, + y: d65.y, + z: d65.z, + alpha: d50.alpha, + } + } +} + +impl From for XYZD65A { + fn from(lrgba: LinearRGBA) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const LSRGB_TO_XYZ: Transform3D = Transform3D::new( + 0.41239079926595934, 0.21263900587151027, 0.01933081871559182, 0., + 0.357584339383878, 0.715168678767756, 0.11919477979462598, 0., + 0.1804807884018343, 0.07219231536073371, 0.9505321522496607, 0., + 0., 0., 0., 1., + ); + let linear_rgb = Vector3D::new(lrgba.red, lrgba.green, lrgba.blue); + let xyz = LSRGB_TO_XYZ.transform_vector3d(linear_rgb); + Self { + x: xyz.x, + y: xyz.y, + z: xyz.z, + alpha: lrgba.alpha, + } + } +} + +impl From for LinearRGBA { + fn from(d65: XYZD65A) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const XYZ_TO_LSRGB: Transform3D = Transform3D::new( + 3.2409699419045226, -0.9692436362808796, 0.05563007969699366, 0., + -1.537383177570094, 1.8759675015077202, -0.20397695888897652, 0., + -0.4986107602930034, 0.04155505740717559, 1.0569715142428786, 0., + 0., 0., 0., 1., + ); + + let xyz = Vector3D::new(d65.x, d65.y, d65.z); + let rgb = XYZ_TO_LSRGB.transform_vector3d(xyz); + Self { + red: rgb.x, + green: rgb.y, + blue: rgb.z, + alpha: d65.alpha, + } + } +} + +impl From for RGBA { + fn from(d65: XYZD65A) -> Self { + Self::from(LinearRGBA::from(d65)) + } +} + +impl From for XYZD65A { + /// Convert an RGBA colour to XYZ as specified in [1]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + fn from(rgba: RGBA) -> Self { + Self::from(LinearRGBA::from(rgba)) + } +} + +impl From for LABA { /// Convert an XYZ colour 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(xyza: XYZA) -> Self { + fn from(xyza: XYZD50A) -> Self { const WHITE: [f32; 3] = [0.96422, 1., 0.82521]; fn compute_f(value: f32) -> f32 { @@ -704,7 +878,7 @@ impl From for LABA { } } -impl From for XYZA { +impl From for XYZD50A { /// Convert a CIELAB color to XYZ as specified in [1] and [2]. /// /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined @@ -735,7 +909,7 @@ impl From for XYZA { (116. * f2 - 16.) / KAPPA }; - XYZA { + Self { x: x * WHITE[0], y: y * WHITE[1], z: z * WHITE[2], @@ -744,85 +918,38 @@ impl From for XYZA { } } -impl From for RGBA { - /// Convert an XYZ color to sRGB as specified in [1] and [2]. - /// - /// [1]: https://www.w3.org/TR/css-color-4/#lab-to-predefined - /// [2]: https://www.w3.org/TR/css-color-4/#color-conversion-code - fn from(xyza: XYZA) -> Self { - #[cfg_attr(rustfmt, rustfmt_skip)] - const BRADFORD_INVERSE: Transform3D = Transform3D::new( - 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0., - -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0., - 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0., - 0., 0., 0., 1., - ); +impl From for RGBA { + fn from(d50: XYZD50A) -> Self { + Self::from(XYZD65A::from(d50)) + } +} - #[cfg_attr(rustfmt, rustfmt_skip)] - const XYZ_TO_SRGB: Transform3D = Transform3D::new( - 3.2409699419045226, -0.9692436362808796, 0.05563007969699366, 0., - -1.537383177570094, 1.8759675015077202, -0.20397695888897652, 0., - -0.4986107602930034, 0.04155505740717559, 1.0569715142428786, 0., - 0., 0., 0., 1., - ); - - // 2. Convert from a D50 whitepoint (used by Lab) to the D65 whitepoint - // used in sRGB, with the Bradford transform. - // 3. Convert from (D65-adapted) CIE XYZ to linear-light srgb - let xyz = Vector3D::new(xyza.x, xyza.y, xyza.z); - let linear_rgb = BRADFORD_INVERSE.then(&XYZ_TO_SRGB).transform_vector3d(xyz); - - // 4. Convert from linear-light srgb to srgb (do gamma encoding). - fn delinearize(value: f32) -> f32 { - let sign = if value < 0. { -1. } else { 1. }; - let abs = value.abs(); - - if abs > 0.0031308 { - sign * (1.055 * abs.powf(1. / 2.4) - 0.055) - } else { - 12.92 * value - } - } - - let red = delinearize(linear_rgb.x); - let green = delinearize(linear_rgb.y); - let blue = delinearize(linear_rgb.z); - - RGBA { - red, - green, - blue, - alpha: xyza.alpha, - } +impl From for XYZD50A { + fn from(rgba: RGBA) -> Self { + Self::from(XYZD65A::from(rgba)) } } impl From for LABA { fn from(rgba: RGBA) -> Self { - let xyza: XYZA = rgba.into(); - xyza.into() + Self::from(XYZD50A::from(rgba)) } } impl From for RGBA { fn from(laba: LABA) -> Self { - let xyza: XYZA = laba.into(); - xyza.into() + Self::from(XYZD50A::from(laba)) } } impl From for LCHA { fn from(rgba: RGBA) -> Self { - let xyza: XYZA = rgba.into(); - let laba: LABA = xyza.into(); - laba.into() + Self::from(LABA::from(rgba)) } } impl From for RGBA { fn from(lcha: LCHA) -> Self { - let laba: LABA = lcha.into(); - let xyza: XYZA = laba.into(); - xyza.into() + Self::from(LABA::from(lcha)) } } diff --git a/components/style/values/specified/color.rs b/components/style/values/specified/color.rs index c786ada2c7b..82805bf1efa 100644 --- a/components/style/values/specified/color.rs +++ b/components/style/values/specified/color.rs @@ -26,20 +26,30 @@ use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; pub enum ColorSpace { /// The sRGB color space. Srgb, + /// The linear-sRGB color space. + LinearSrgb, /// The CIEXYZ color space. + #[parse(aliases = "xyz-d65")] Xyz, + /// https://drafts.csswg.org/css-color-4/#valdef-color-xyz + XyzD50, /// The CIELAB color space. Lab, + /// https://drafts.csswg.org/css-color-4/#valdef-hsl-hsl + Hsl, + /// https://drafts.csswg.org/css-color-4/#valdef-hwb-hwb + Hwb, /// The CIELAB color space, expressed in cylindrical coordinates. Lch, + // TODO: Oklab, Lch } impl ColorSpace { /// Returns whether this is a ``. pub fn is_polar(self) -> bool { match self { - Self::Srgb | Self::Xyz | Self::Lab => false, - Self::Lch => true, + Self::Srgb | Self::LinearSrgb | Self::Xyz | Self::XyzD50 | Self::Lab => false, + Self::Hsl | Self::Hwb | Self::Lch => true, } } }