From 922914aa385084c068ff41f318e95e2e71d59169 Mon Sep 17 00:00:00 2001 From: Tiaan Louw Date: Fri, 27 Jan 2023 12:44:18 +0000 Subject: [PATCH] style: Implement color() function from CSS specification Colors can now be defined in different color spaces with the color() function. https://w3c.github.io/csswg-drafts/css-color-4/#predefined Differential Revision: https://phabricator.services.mozilla.com/D164866 --- Cargo.lock | 5 +- Cargo.toml | 2 +- components/style/color/convert.rs | 649 +++++++++++++++++++++ components/style/color/mod.rs | 19 + components/style/lib.rs | 1 + components/style/values/animated/color.rs | 4 + components/style/values/specified/color.rs | 166 ++++-- 7 files changed, 781 insertions(+), 65 deletions(-) create mode 100644 components/style/color/convert.rs create mode 100644 components/style/color/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0de50631b2a..8048e5524a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,12 +1171,11 @@ dependencies = [ [[package]] name = "cssparser" version = "0.30.0" -source = "git+https://github.com/servo/rust-cssparser?rev=722b30d2f1634714befab967ecae627813fa4cf0#722b30d2f1634714befab967ecae627813fa4cf0" +source = "git+https://github.com/servo/rust-cssparser?rev=d3670a89bae26ba3a8db4758eb7976616113987d#d3670a89bae26ba3a8db4758eb7976616113987d" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "matches", "phf", "proc-macro2", "quote", @@ -1188,7 +1187,7 @@ dependencies = [ [[package]] name = "cssparser-macros" version = "0.6.0" -source = "git+https://github.com/servo/rust-cssparser?rev=722b30d2f1634714befab967ecae627813fa4cf0#722b30d2f1634714befab967ecae627813fa4cf0" +source = "git+https://github.com/servo/rust-cssparser?rev=d3670a89bae26ba3a8db4758eb7976616113987d#d3670a89bae26ba3a8db4758eb7976616113987d" dependencies = [ "quote", "syn 1.0.103", diff --git a/Cargo.toml b/Cargo.toml index 429b60215d6..5b0b61fef6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ compositing_traits = { path = "components/shared/compositing" } content-security-policy = { version = "0.5", features = ["serde"] } cookie = "0.12" crossbeam-channel = "0.5" -cssparser = { version = "0.30", git = "https://github.com/servo/rust-cssparser", rev = "722b30d2f1634714befab967ecae627813fa4cf0" } +cssparser = { version = "0.30", git = "https://github.com/servo/rust-cssparser", rev = "d3670a89bae26ba3a8db4758eb7976616113987d" } darling = { version = "0.14", default-features = false } data-url = "0.1.0" devtools_traits = { path = "components/shared/devtools" } diff --git a/components/style/color/convert.rs b/components/style/color/convert.rs new file mode 100644 index 00000000000..e9735cc3e8d --- /dev/null +++ b/components/style/color/convert.rs @@ -0,0 +1,649 @@ +/* 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/csswg-drafts/css-color-4/#color-conversion-code + +use crate::color::ColorComponents; +use std::f32::consts::PI; + +type Transform = euclid::default::Transform3D; +type Vector = euclid::default::Vector3D; + +const RAD_PER_DEG: f32 = PI / 180.0; +const DEG_PER_RAD: f32 = 180.0 / PI; + +#[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, +} + +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/csswg-drafts/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: &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( + 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/csswg-drafts/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 + } + }) + } +} + +/// The same as sRGB color space, except the transfer function is linear light. +/// https://drafts.csswg.org/csswg-drafts/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/csswg-drafts/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, + ); +} + +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 { + todo!() + } + + fn to_gamma_encoded(_from: &ColorComponents) -> ColorComponents { + todo!() + } +} + +/// The a98-rgb color space. +/// https://drafts.csswg.org/csswg-drafts/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, + ); +} + +impl ColorSpaceConversion for A98Rgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + #[inline] + fn map(value: f32) -> f32 { + value.signum() * value.abs().powf(2.19921875) + } + + ColorComponents(map(from.0), map(from.1), map(from.2)) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(_from: &ColorComponents) -> ColorComponents { + todo!() + } + + fn to_gamma_encoded(_from: &ColorComponents) -> ColorComponents { + todo!() + } +} + +/// The ProPhoto RGB color space. +/// https://drafts.csswg.org/csswg-drafts/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, + ); +} + +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 { + todo!() + } + + fn to_gamma_encoded(_from: &ColorComponents) -> ColorComponents { + todo!() + } +} + +/// The Rec.2020 color space. +/// https://drafts.csswg.org/csswg-drafts/css-color-4/#predefined-rec2020 +pub struct Rec2020; + +impl Rec2020 { + #[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, + ); +} + +impl ColorSpaceConversion for Rec2020 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + const ALPHA: f32 = 1.09929682680944; + const BETA: f32 = 0.018053968510807; + + let abs = value.abs(); + + if abs < BETA * 4.5 { + value / 4.5 + } else { + value.signum() * ((abs + ALPHA - 1.0) / ALPHA).powf(1.0 / 0.45) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(_from: &ColorComponents) -> ColorComponents { + todo!() + } + + fn to_gamma_encoded(_from: &ColorComponents) -> ColorComponents { + todo!() + } +} + +/// A color in the XYZ coordinate space with a D50 white reference. +/// https://drafts.csswg.org/csswg-drafts/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 { + todo!() + } + + fn to_gamma_encoded(_from: &ColorComponents) -> ColorComponents { + todo!() + } +} + +/// A color in the XYZ coordinate space with a D65 white reference. +/// https://drafts.csswg.org/csswg-drafts/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 { + todo!() + } + + fn to_gamma_encoded(_from: &ColorComponents) -> ColorComponents { + todo!() + } +} + +/// The Lab color space. +/// https://drafts.csswg.org/csswg-drafts/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; + const WHITE: ColorComponents = ColorComponents(0.96422, 1.0, 0.82521); +} + +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 f1 = (from.0 + 16.0) / 116.0; + let f0 = (from.1 / 500.0) + f1; + let f2 = f1 - from.2 / 200.0; + + let x = if f0.powf(3.0) > Self::EPSILON { + f0.powf(3.) + } else { + (116.0 * f0 - 16.0) / Self::KAPPA + }; + let y = if from.0 > Self::KAPPA * Self::EPSILON { + ((from.0 + 16.0) / 116.0).powf(3.0) + } else { + from.0 / Self::KAPPA + }; + let z = if f2.powf(3.0) > Self::EPSILON { + f2.powf(3.0) + } else { + (116.0 * f2 - 16.0) / Self::KAPPA + }; + + ColorComponents(x * Self::WHITE.0, y * Self::WHITE.1, z * Self::WHITE.2) + } + + /// 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_xyz(from: &ColorComponents) -> ColorComponents { + macro_rules! compute_f { + ($value:expr) => {{ + if $value > Self::EPSILON { + $value.cbrt() + } else { + (Self::KAPPA * $value + 16.0) / 116.0 + } + }}; + } + + // 4. Convert D50-adapted XYZ to Lab. + let f = [ + compute_f!(from.0 / Self::WHITE.0), + compute_f!(from.1 / Self::WHITE.1), + compute_f!(from.2 / Self::WHITE.2), + ]; + + let lightness = 116.0 * f[1] - 16.0; + let a = 500.0 * (f[0] - f[1]); + let b = 200.0 * (f[1] - f[2]); + + 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/csswg-drafts/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 hue = from.2 * RAD_PER_DEG; + let a = from.1 * hue.cos(); + let b = from.1 * hue.sin(); + + let lab = ColorComponents(from.0, a, b); + + // Then convert the Lab to XYZ. + Lab::to_xyz(&lab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert the XYZ to LAB. + let ColorComponents(lightness, a, b) = Lab::from_xyz(&from); + + // Then conver the Lab to LCH. + let hue = b.atan2(a) * DEG_PER_RAD; + let chroma = (a * a + b * b).sqrt(); + + ColorComponents(lightness, chroma, hue) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklab color space. +/// https://drafts.csswg.org/csswg-drafts/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.powf(3.0)); + 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/csswg-drafts/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 hue = from.2 * RAD_PER_DEG; + let a = from.1 * hue.cos(); + let b = from.1 * hue.sin(); + let oklab = ColorComponents(from.0, a, b); + + // Then convert Oklab to XYZ. + Oklab::to_xyz(&oklab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert XYZ to Oklab. + let ColorComponents(lightness, a, b) = Oklab::from_xyz(&from); + + // Then convert Oklab to OkLCH. + let hue = b.atan2(a) * DEG_PER_RAD; + let chroma = (a * a + b * b).sqrt(); + + ColorComponents(lightness, chroma, hue) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} diff --git a/components/style/color/mod.rs b/components/style/color/mod.rs new file mode 100644 index 00000000000..5a702fb8ff6 --- /dev/null +++ b/components/style/color/mod.rs @@ -0,0 +1,19 @@ +/* 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 support functions. + +/// cbindgen:ignore +pub mod convert; + +/// The 3 components that make up a color. (Does not include the alpha component) +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct ColorComponents(pub f32, pub f32, pub f32); + +impl ColorComponents { + /// Apply a function to each of the 3 components of the color. + pub fn map(self, f: impl Fn(f32) -> f32) -> Self { + Self(f(self.0), f(self.1), f(self.2)) + } +} diff --git a/components/style/lib.rs b/components/style/lib.rs index 46560a109e9..31de0dfcb7a 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -80,6 +80,7 @@ pub mod attr; pub mod author_styles; pub mod bezier; pub mod bloom; +pub mod color; #[path = "properties/computed_value_flags.rs"] pub mod computed_value_flags; pub mod context; diff --git a/components/style/values/animated/color.rs b/components/style/values/animated/color.rs index ddd6bd66783..efc940b92f6 100644 --- a/components/style/values/animated/color.rs +++ b/components/style/values/animated/color.rs @@ -13,6 +13,10 @@ use crate::values::generics::color::{ use euclid::default::{Transform3D, Vector3D}; use std::f32::consts::PI; +// TODO(tlouw): The code does not use the new color conversions that were added +// for new color spaces and needs to be refectored, see: +// https://bugzilla.mozilla.org/show_bug.cgi?id=1812545 + /// An animated RGBA color. /// /// Unlike in computed values, each component value may exceed the diff --git a/components/style/values/specified/color.rs b/components/style/values/specified/color.rs index 74a60e4f8b5..20ef1efa4fe 100644 --- a/components/style/values/specified/color.rs +++ b/components/style/values/specified/color.rs @@ -5,6 +5,7 @@ //! Specified color values. use super::AllowQuirks; +use crate::color::ColorComponents; use crate::media_queries::Device; use crate::parser::{Parse, ParserContext}; use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; @@ -162,10 +163,6 @@ bitflags! { } } -/// The 3 components that make up a color. (Does not include the alpha component) -#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] -struct ColorComponents(f32, f32, f32); - /// An absolutely specified color, using either rgb(), rgba(), lab(), lch(), /// oklab(), oklch() or color(). #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] @@ -196,9 +193,9 @@ macro_rules! color_components_as { impl AbsoluteColor { /// Create a new [AbsoluteColor] with the given [ColorSpace] and components. - pub fn new(color_space: ColorSpace, c1: f32, c2: f32, c3: f32, alpha: f32) -> Self { + pub fn new(color_space: ColorSpace, components: ColorComponents, alpha: f32) -> Self { Self { - components: ColorComponents(c1, c2, c3), + components, alpha, color_space, flags: SerializationFlags::empty(), @@ -211,7 +208,11 @@ impl AbsoluteColor { let green = rgba.green as f32 / 255.0; let blue = rgba.blue as f32 / 255.0; - Self::new(ColorSpace::Srgb, red, green, blue, rgba.alpha) + Self::new( + ColorSpace::Srgb, + ColorComponents(red, green, blue), + rgba.alpha, + ) } /// Return the alpha component. @@ -233,47 +234,58 @@ impl AbsoluteColor { /// Convert this color to the specified color space. pub fn to_color_space(&self, color_space: ColorSpace) -> Self { - use crate::values::animated::color::{AnimatedRGBA, LABA, LCHA, OKLABA, OKLCHA}; + use crate::color::convert; use ColorSpace::*; if self.color_space == color_space { return self.clone(); } - match (self.color_space, color_space) { - (Lab, Srgb) => { - let laba = unsafe { color_components_as!(self, LABA) }; - let rgba = AnimatedRGBA::from(*laba); - Self::new(Srgb, rgba.red, rgba.green, rgba.blue, rgba.alpha) - }, + let (xyz, white_point) = match self.color_space { + Lab => convert::to_xyz::(&self.components), + Lch => convert::to_xyz::(&self.components), + Oklab => convert::to_xyz::(&self.components), + Oklch => convert::to_xyz::(&self.components), + Srgb => convert::to_xyz::(&self.components), + SrgbLinear => convert::to_xyz::(&self.components), + DisplayP3 => convert::to_xyz::(&self.components), + A98Rgb => convert::to_xyz::(&self.components), + ProphotoRgb => convert::to_xyz::(&self.components), + Rec2020 => convert::to_xyz::(&self.components), + XyzD50 => convert::to_xyz::(&self.components), + XyzD65 => convert::to_xyz::(&self.components), + }; - (Oklab, Srgb) => { - let oklaba: &OKLABA = unsafe { color_components_as!(self, OKLABA) }; - let rgba = AnimatedRGBA::from(*oklaba); - Self::new(Srgb, rgba.red, rgba.green, rgba.blue, rgba.alpha) - }, + let result = match color_space { + Lab => convert::from_xyz::(&xyz, white_point), + Lch => convert::from_xyz::(&xyz, white_point), + Oklab => convert::from_xyz::(&xyz, white_point), + Oklch => convert::from_xyz::(&xyz, white_point), + Srgb => convert::from_xyz::(&xyz, white_point), + SrgbLinear => convert::from_xyz::(&xyz, white_point), + DisplayP3 => convert::from_xyz::(&xyz, white_point), + A98Rgb => convert::from_xyz::(&xyz, white_point), + ProphotoRgb => convert::from_xyz::(&xyz, white_point), + Rec2020 => convert::from_xyz::(&xyz, white_point), + XyzD50 => convert::from_xyz::(&xyz, white_point), + XyzD65 => convert::from_xyz::(&xyz, white_point), + }; - (Lch, Srgb) => { - let lcha: &LCHA = unsafe { color_components_as!(self, LCHA) }; - let rgba = AnimatedRGBA::from(*lcha); - Self::new(Srgb, rgba.red, rgba.green, rgba.blue, rgba.alpha) - }, + Self::new(color_space, result, self.alpha) + } +} - (Oklch, Srgb) => { - let oklcha: &OKLCHA = unsafe { color_components_as!(self, OKLCHA) }; - let rgba = AnimatedRGBA::from(*oklcha); - Self::new(Srgb, rgba.red, rgba.green, rgba.blue, rgba.alpha) - }, - - _ => { - // Conversion to other color spaces is not implemented yet. - log::warn!( - "Can not convert from {:?} to {:?}!", - self.color_space, - color_space - ); - Self::from_rgba(RGBA::new(0, 0, 0, 1.0)) - }, +impl From for ColorSpace { + fn from(value: cssparser::PredefinedColorSpace) -> Self { + match value { + cssparser::PredefinedColorSpace::Srgb => ColorSpace::Srgb, + cssparser::PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear, + cssparser::PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3, + cssparser::PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb, + cssparser::PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb, + cssparser::PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020, + cssparser::PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50, + cssparser::PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65, } } } @@ -283,33 +295,43 @@ impl From for AbsoluteColor { match f { cssparser::AbsoluteColor::Rgba(rgba) => Self::from_rgba(rgba), - cssparser::AbsoluteColor::Lab(lab) => { - Self::new(ColorSpace::Lab, lab.lightness, lab.a, lab.b, lab.alpha) - }, + cssparser::AbsoluteColor::Lab(lab) => Self::new( + ColorSpace::Lab, + ColorComponents(lab.lightness, lab.a, lab.b), + lab.alpha, + ), cssparser::AbsoluteColor::Lch(lch) => Self::new( ColorSpace::Lch, - lch.lightness, - lch.chroma, - lch.hue, + ColorComponents(lch.lightness, lch.chroma, lch.hue), lch.alpha, ), cssparser::AbsoluteColor::Oklab(oklab) => Self::new( ColorSpace::Oklab, - oklab.lightness, - oklab.a, - oklab.b, + ColorComponents(oklab.lightness, oklab.a, oklab.b), oklab.alpha, ), cssparser::AbsoluteColor::Oklch(oklch) => Self::new( ColorSpace::Oklch, - oklch.lightness, - oklch.chroma, - oklch.hue, + ColorComponents(oklch.lightness, oklch.chroma, oklch.hue), oklch.alpha, ), + + cssparser::AbsoluteColor::ColorFunction(c) => { + let mut result = AbsoluteColor::new( + c.color_space.into(), + ColorComponents(c.c1, c.c2, c.c3), + c.alpha, + ); + + if matches!(c.color_space, cssparser::PredefinedColorSpace::Srgb) { + result.flags |= SerializationFlags::AS_COLOR_FUNCTION; + } + + result + }, } } } @@ -347,16 +369,38 @@ impl ToCss for AbsoluteColor { unsafe { color_components_as!(self, cssparser::Oklch) }, dest, ), - // Other color spaces are not implemented yet. See: - // https://bugzilla.mozilla.org/show_bug.cgi?id=1128204 - ColorSpace::Srgb | - ColorSpace::SrgbLinear | - ColorSpace::DisplayP3 | - ColorSpace::A98Rgb | - ColorSpace::ProphotoRgb | - ColorSpace::Rec2020 | - ColorSpace::XyzD50 | - ColorSpace::XyzD65 => todo!(), + _ => { + let color_space = match self.color_space { + ColorSpace::Lab | ColorSpace::Lch | ColorSpace::Oklab | ColorSpace::Oklch => { + unreachable!("Handle these in the wrapping match case!!") + }, + ColorSpace::Srgb => { + debug_assert!( + self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION), + "The case without this flag should be handled in the wrapping match case!!" + ); + + cssparser::PredefinedColorSpace::Srgb + }, + ColorSpace::SrgbLinear => cssparser::PredefinedColorSpace::SrgbLinear, + ColorSpace::DisplayP3 => cssparser::PredefinedColorSpace::DisplayP3, + ColorSpace::A98Rgb => cssparser::PredefinedColorSpace::A98Rgb, + ColorSpace::ProphotoRgb => cssparser::PredefinedColorSpace::ProphotoRgb, + ColorSpace::Rec2020 => cssparser::PredefinedColorSpace::Rec2020, + ColorSpace::XyzD50 => cssparser::PredefinedColorSpace::XyzD50, + ColorSpace::XyzD65 => cssparser::PredefinedColorSpace::XyzD65, + }; + + let color_function = cssparser::ColorFunction { + color_space, + c1: self.components.0, + c2: self.components.1, + c3: self.components.2, + alpha: self.alpha, + }; + let color = cssparser::AbsoluteColor::ColorFunction(color_function); + cssparser::ToCss::to_css(&color, dest) + }, } } }