style: Implement more color-mix() color-spaces

We had code to convert between these and the latest draft supports them so...

Differential Revision: https://phabricator.services.mozilla.com/D147004
This commit is contained in:
Emilio Cobos Álvarez 2023-08-15 00:47:38 +02:00 committed by Martin Robinson
parent 95e9898db4
commit 6fc4355dc2
2 changed files with 305 additions and 168 deletions

View file

@ -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::<RGBA>(
left_color,
left_weight,
right_color,
right_weight,
interpolation.hue,
),
ColorSpace::Xyz => Self::mix_in::<XYZA>(
left_color,
left_weight,
right_color,
right_weight,
interpolation.hue,
),
ColorSpace::Lab => Self::mix_in::<LABA>(
left_color,
left_weight,
right_color,
right_weight,
interpolation.hue,
),
ColorSpace::Lch => Self::mix_in::<LCHA>(
left_color,
left_weight,
right_color,
right_weight,
interpolation.hue,
),
}
let mix_function = match interpolation.space {
ColorSpace::Srgb => Self::mix_in::<RGBA>,
ColorSpace::LinearSrgb => Self::mix_in::<LinearRGBA>,
ColorSpace::Xyz => Self::mix_in::<XYZD65A>,
ColorSpace::XyzD50 => Self::mix_in::<XYZD50A>,
ColorSpace::Lab => Self::mix_in::<LABA>,
ColorSpace::Hwb => Self::mix_in::<HWBA>,
ColorSpace::Hsl => Self::mix_in::<HSLA>,
ColorSpace::Lch => Self::mix_in::<LCHA>,
};
mix_function(left_color, left_weight, right_color, right_weight, interpolation.hue)
}
fn mix_in<S>(
@ -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<RGBA> 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<RGBA> for HSLA {
fn from(rgba: RGBA) -> Self {
rgb_to_hsl(rgba).0
}
}
impl From<HSLA> 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<RGBA> 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<HWBA> 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<RGBA> 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<RGBA> 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<f32> = 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<LinearRGBA> 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<XYZD65A> 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<f32> = Transform3D::new(
1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.,
@ -611,34 +719,100 @@ impl From<RGBA> 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<XYZA> for LABA {
impl From<XYZD50A> 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<f32> = 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<LinearRGBA> 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<f32> = 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<XYZD65A> 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<f32> = 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<XYZD65A> for RGBA {
fn from(d65: XYZD65A) -> Self {
Self::from(LinearRGBA::from(d65))
}
}
impl From<RGBA> 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<XYZD50A> 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<LCHA> for LABA {
}
}
impl From<LABA> for XYZA {
impl From<LABA> 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<LABA> 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<LABA> for XYZA {
}
}
impl From<XYZA> 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<f32> = 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<XYZD50A> for RGBA {
fn from(d50: XYZD50A) -> Self {
Self::from(XYZD65A::from(d50))
}
}
#[cfg_attr(rustfmt, rustfmt_skip)]
const XYZ_TO_SRGB: Transform3D<f32> = 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<RGBA> for XYZD50A {
fn from(rgba: RGBA) -> Self {
Self::from(XYZD65A::from(rgba))
}
}
impl From<RGBA> for LABA {
fn from(rgba: RGBA) -> Self {
let xyza: XYZA = rgba.into();
xyza.into()
Self::from(XYZD50A::from(rgba))
}
}
impl From<LABA> for RGBA {
fn from(laba: LABA) -> Self {
let xyza: XYZA = laba.into();
xyza.into()
Self::from(XYZD50A::from(laba))
}
}
impl From<RGBA> 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<LCHA> for RGBA {
fn from(lcha: LCHA) -> Self {
let laba: LABA = lcha.into();
let xyza: XYZA = laba.into();
xyza.into()
Self::from(LABA::from(lcha))
}
}

View file

@ -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 `<polar-color-space>`.
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,
}
}
}