mirror of
https://github.com/servo/servo.git
synced 2025-06-24 17:14:33 +01:00
style: Support color-mix() in non-sRGB color spaces
Out of gamut colours are currently clipped into sRGB. Differential Revision: https://phabricator.services.mozilla.com/D120561
This commit is contained in:
parent
d564f200aa
commit
c241182d09
2 changed files with 716 additions and 36 deletions
|
@ -7,6 +7,8 @@
|
|||
use crate::values::animated::{Animate, Procedure, ToAnimatedZero};
|
||||
use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
|
||||
use crate::values::generics::color::{Color as GenericColor, ComplexColorRatios};
|
||||
use crate::values::specified::color::{ColorSpaceKind, HueAdjuster};
|
||||
use euclid::default::{Transform3D, Vector3D};
|
||||
|
||||
/// An animated RGBA color.
|
||||
///
|
||||
|
@ -41,6 +43,26 @@ impl RGBA {
|
|||
alpha,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not the colour is in gamut for sRGB.
|
||||
pub fn in_gamut(&self) -> bool {
|
||||
0. <= self.red &&
|
||||
self.red <= 1. &&
|
||||
0. <= self.green &&
|
||||
self.green <= 1. &&
|
||||
0. <= self.blue &&
|
||||
self.blue <= 1.
|
||||
}
|
||||
|
||||
/// Returns the colour with coordinates clamped to the sRGB range.
|
||||
pub fn clamp(&self) -> Self {
|
||||
Self {
|
||||
red: self.red.max(0.).min(1.),
|
||||
green: self.green.max(0.).min(1.),
|
||||
blue: self.blue.max(0.).min(1.),
|
||||
alpha: self.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Animate for RGBA {
|
||||
|
@ -106,43 +128,69 @@ impl Color {
|
|||
|
||||
/// Mix two colors into one.
|
||||
pub fn mix(
|
||||
color_space: ColorSpaceKind,
|
||||
left_color: &Color,
|
||||
left_weight: f32,
|
||||
right_color: &Color,
|
||||
right_weight: f32,
|
||||
hue_adjuster: HueAdjuster,
|
||||
) -> Self {
|
||||
let left_bg = left_color.scaled_rgba();
|
||||
let right_bg = right_color.scaled_rgba();
|
||||
let alpha = (left_bg.alpha * left_weight +
|
||||
right_bg.alpha * right_weight)
|
||||
.min(1.);
|
||||
|
||||
let mut fg = 0.;
|
||||
let mut red = 0.;
|
||||
let mut green = 0.;
|
||||
let mut blue = 0.;
|
||||
|
||||
let colors = [
|
||||
(left_color, &left_bg, left_weight),
|
||||
(right_color, &right_bg, right_weight),
|
||||
];
|
||||
|
||||
for &(color, bg, weight) in &colors {
|
||||
fg += color.ratios.fg * weight;
|
||||
|
||||
red += bg.red * bg.alpha * weight;
|
||||
green += bg.green * bg.alpha * weight;
|
||||
blue += bg.blue * bg.alpha * weight;
|
||||
match color_space {
|
||||
ColorSpaceKind::Srgb => Self::mix_in::<RGBA>(
|
||||
left_color,
|
||||
left_weight,
|
||||
right_color,
|
||||
right_weight,
|
||||
hue_adjuster,
|
||||
),
|
||||
ColorSpaceKind::Xyz => Self::mix_in::<XYZA>(
|
||||
left_color,
|
||||
left_weight,
|
||||
right_color,
|
||||
right_weight,
|
||||
hue_adjuster,
|
||||
),
|
||||
ColorSpaceKind::Lab => Self::mix_in::<LABA>(
|
||||
left_color,
|
||||
left_weight,
|
||||
right_color,
|
||||
right_weight,
|
||||
hue_adjuster,
|
||||
),
|
||||
ColorSpaceKind::Lch => Self::mix_in::<LCHA>(
|
||||
left_color,
|
||||
left_weight,
|
||||
right_color,
|
||||
right_weight,
|
||||
hue_adjuster,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let color = if alpha <= 0. {
|
||||
RGBA::transparent()
|
||||
fn mix_in<S>(
|
||||
left_color: &Color,
|
||||
left_weight: f32,
|
||||
right_color: &Color,
|
||||
right_weight: f32,
|
||||
hue_adjuster: HueAdjuster,
|
||||
) -> Self
|
||||
where
|
||||
S: ModelledColor,
|
||||
{
|
||||
let left_bg = S::from(left_color.scaled_rgba());
|
||||
let right_bg = S::from(right_color.scaled_rgba());
|
||||
|
||||
let color = S::lerp(left_bg, left_weight, right_bg, right_weight, hue_adjuster);
|
||||
let rgba: RGBA = color.into();
|
||||
let rgba = if !rgba.in_gamut() {
|
||||
// TODO: Better gamut mapping.
|
||||
rgba.clamp()
|
||||
} else {
|
||||
let inv = 1. / alpha;
|
||||
RGBA::new(red * inv, green * inv, blue * inv, alpha)
|
||||
rgba
|
||||
};
|
||||
|
||||
Self::new(color, ComplexColorRatios { bg: 1., fg })
|
||||
let fg = left_color.ratios.fg * left_weight + right_color.ratios.fg * right_weight;
|
||||
Self::new(rgba, ComplexColorRatios { bg: 1., fg })
|
||||
}
|
||||
|
||||
fn scaled_rgba(&self) -> RGBA {
|
||||
|
@ -309,3 +357,573 @@ impl ToAnimatedZero for Color {
|
|||
Ok(RGBA::transparent().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A color modelled in a specific color space (such as sRGB or CIE XYZ).
|
||||
///
|
||||
/// For now, colors modelled in other spaces need to be convertible to and from
|
||||
/// `RGBA` because we use sRGB for displaying colors.
|
||||
trait ModelledColor: Clone + Copy + From<RGBA> + Into<RGBA> {
|
||||
/// Linearly interpolate between the left and right colors.
|
||||
///
|
||||
/// The HueAdjuster parameter is only for color spaces where the hue is
|
||||
/// represented as an angle (e.g., CIE LCH).
|
||||
fn lerp(
|
||||
left_bg: Self,
|
||||
left_weight: f32,
|
||||
right_bg: Self,
|
||||
right_weight: f32,
|
||||
hue_adjuster: HueAdjuster,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
impl ModelledColor for RGBA {
|
||||
fn lerp(
|
||||
left_bg: Self,
|
||||
left_weight: f32,
|
||||
right_bg: Self,
|
||||
right_weight: f32,
|
||||
_: HueAdjuster,
|
||||
) -> Self {
|
||||
// Interpolation with alpha, as per
|
||||
// https://drafts.csswg.org/css-color/#interpolation-alpha.
|
||||
let mut red = 0.;
|
||||
let mut green = 0.;
|
||||
let mut blue = 0.;
|
||||
|
||||
// sRGB is a rectangular othogonal color space, so all component values
|
||||
// are multiplied by the alpha value.
|
||||
for &(bg, weight) in &[(left_bg, left_weight), (right_bg, right_weight)] {
|
||||
red += bg.red * bg.alpha * weight;
|
||||
green += bg.green * bg.alpha * weight;
|
||||
blue += bg.blue * bg.alpha * weight;
|
||||
}
|
||||
|
||||
let alpha = (left_bg.alpha * left_weight + right_bg.alpha * right_weight).min(1.);
|
||||
if alpha <= 0. {
|
||||
RGBA::transparent()
|
||||
} else {
|
||||
let inv = 1. / alpha;
|
||||
RGBA::new(red * inv, green * inv, blue * inv, alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An animated XYZA colour.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl XYZA {
|
||||
/// Returns a transparent color.
|
||||
#[inline]
|
||||
pub fn transparent() -> Self {
|
||||
Self {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
z: 0.,
|
||||
alpha: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelledColor for XYZA {
|
||||
fn lerp(
|
||||
left_bg: Self,
|
||||
left_weight: f32,
|
||||
right_bg: Self,
|
||||
right_weight: f32,
|
||||
_: HueAdjuster,
|
||||
) -> Self {
|
||||
// Interpolation with alpha, as per
|
||||
// https://drafts.csswg.org/css-color/#interpolation-alpha.
|
||||
let mut x = 0.;
|
||||
let mut y = 0.;
|
||||
let mut z = 0.;
|
||||
|
||||
// CIE XYZ is a rectangular othogonal color space, so all component
|
||||
// values are multiplied by the alpha value.
|
||||
for &(bg, weight) in &[(left_bg, left_weight), (right_bg, right_weight)] {
|
||||
x += bg.x * bg.alpha * weight;
|
||||
y += bg.y * bg.alpha * weight;
|
||||
z += bg.z * bg.alpha * weight;
|
||||
}
|
||||
|
||||
let alpha = (left_bg.alpha * left_weight + right_bg.alpha * right_weight).min(1.);
|
||||
if alpha <= 0. {
|
||||
Self::transparent()
|
||||
} else {
|
||||
let inv = 1. / alpha;
|
||||
Self {
|
||||
x: x * inv,
|
||||
y: y * inv,
|
||||
z: z * inv,
|
||||
alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An animated LABA colour.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl LABA {
|
||||
/// Returns a transparent color.
|
||||
#[inline]
|
||||
pub fn transparent() -> Self {
|
||||
Self {
|
||||
lightness: 0.,
|
||||
a: 0.,
|
||||
b: 0.,
|
||||
alpha: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelledColor for LABA {
|
||||
fn lerp(
|
||||
left_bg: Self,
|
||||
left_weight: f32,
|
||||
right_bg: Self,
|
||||
right_weight: f32,
|
||||
_: HueAdjuster,
|
||||
) -> Self {
|
||||
// Interpolation with alpha, as per
|
||||
// https://drafts.csswg.org/css-color/#interpolation-alpha.
|
||||
let mut lightness = 0.;
|
||||
let mut a = 0.;
|
||||
let mut b = 0.;
|
||||
|
||||
// CIE LAB is a rectangular othogonal color space, so all component
|
||||
// values are multiplied by the alpha value.
|
||||
for &(bg, weight) in &[(left_bg, left_weight), (right_bg, right_weight)] {
|
||||
lightness += bg.lightness * bg.alpha * weight;
|
||||
a += bg.a * bg.alpha * weight;
|
||||
b += bg.b * bg.alpha * weight;
|
||||
}
|
||||
|
||||
let alpha = (left_bg.alpha * left_weight + right_bg.alpha * right_weight).min(1.);
|
||||
if alpha <= 0. {
|
||||
Self::transparent()
|
||||
} else {
|
||||
let inv = 1. / alpha;
|
||||
Self {
|
||||
lightness: lightness * inv,
|
||||
a: a * inv,
|
||||
b: b * inv,
|
||||
alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl LCHA {
|
||||
/// Returns a transparent color.
|
||||
#[inline]
|
||||
pub fn transparent() -> Self {
|
||||
Self {
|
||||
lightness: 0.,
|
||||
chroma: 0.,
|
||||
hue: 0.,
|
||||
alpha: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LCHA {
|
||||
fn adjust(left_bg: Self, right_bg: Self, hue_adjuster: HueAdjuster) -> (Self, Self) {
|
||||
use std::f32::consts::{PI, TAU};
|
||||
|
||||
let mut left_bg = left_bg;
|
||||
let mut right_bg = right_bg;
|
||||
|
||||
// Adjust the hue angle as per
|
||||
// https://drafts.csswg.org/css-color/#hue-interpolation.
|
||||
//
|
||||
// If both hue angles are NAN, they should be set to 0. Otherwise, if a
|
||||
// single hue angle is NAN, it should use the other hue angle.
|
||||
if left_bg.hue.is_nan() || right_bg.hue.is_nan() {
|
||||
if left_bg.hue.is_nan() && right_bg.hue.is_nan() {
|
||||
left_bg.hue = 0.;
|
||||
right_bg.hue = 0.;
|
||||
} else if left_bg.hue.is_nan() {
|
||||
left_bg.hue = right_bg.hue;
|
||||
} else if right_bg.hue.is_nan() {
|
||||
right_bg.hue = left_bg.hue;
|
||||
}
|
||||
}
|
||||
|
||||
if hue_adjuster != HueAdjuster::Specified {
|
||||
// Normalize hue into [0, 2 * PI)
|
||||
while left_bg.hue < 0. {
|
||||
left_bg.hue += TAU;
|
||||
}
|
||||
while left_bg.hue > TAU {
|
||||
left_bg.hue -= TAU;
|
||||
}
|
||||
|
||||
while right_bg.hue < 0. {
|
||||
right_bg.hue += TAU;
|
||||
}
|
||||
while right_bg.hue >= TAU {
|
||||
right_bg.hue -= TAU;
|
||||
}
|
||||
}
|
||||
|
||||
match hue_adjuster {
|
||||
HueAdjuster::Shorter => {
|
||||
let delta = right_bg.hue - left_bg.hue;
|
||||
|
||||
if delta > PI {
|
||||
left_bg.hue += PI;
|
||||
} else if delta < -1. * PI {
|
||||
right_bg.hue += PI;
|
||||
}
|
||||
},
|
||||
|
||||
HueAdjuster::Longer => {
|
||||
let delta = right_bg.hue - left_bg.hue;
|
||||
if 0. < delta && delta < PI {
|
||||
left_bg.hue += TAU;
|
||||
} else if -1. * PI < delta && delta < 0. {
|
||||
right_bg.hue += TAU;
|
||||
}
|
||||
},
|
||||
|
||||
HueAdjuster::Increasing => {
|
||||
if right_bg.hue < left_bg.hue {
|
||||
right_bg.hue += TAU;
|
||||
}
|
||||
},
|
||||
|
||||
HueAdjuster::Decreasing => {
|
||||
if left_bg.hue < right_bg.hue {
|
||||
left_bg.hue += TAU;
|
||||
}
|
||||
},
|
||||
|
||||
//Angles are not adjusted. They are interpolated like any other
|
||||
//component.
|
||||
HueAdjuster::Specified => {},
|
||||
}
|
||||
|
||||
(left_bg, right_bg)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelledColor for LCHA {
|
||||
fn lerp(
|
||||
left_bg: Self,
|
||||
left_weight: f32,
|
||||
right_bg: Self,
|
||||
right_weight: f32,
|
||||
hue_adjuster: HueAdjuster,
|
||||
) -> Self {
|
||||
// Interpolation with alpha, as per
|
||||
// https://drafts.csswg.org/css-color/#interpolation-alpha.
|
||||
let (left_bg, right_bg) = Self::adjust(left_bg, right_bg, hue_adjuster);
|
||||
|
||||
let mut lightness = 0.;
|
||||
let mut chroma = 0.;
|
||||
let mut hue = 0.;
|
||||
|
||||
// CIE LCH is a cylindical polar color space, so all component values
|
||||
// are multiplied by the alpha value.
|
||||
for &(bg, weight) in &[(left_bg, left_weight), (right_bg, right_weight)] {
|
||||
lightness += bg.lightness * bg.alpha * weight;
|
||||
chroma += bg.chroma * bg.alpha * weight;
|
||||
// LCHA is a cylindrical color space so the hue coordinate is not
|
||||
// pre-multipled by the alpha component when interpolating.
|
||||
hue += bg.hue * weight;
|
||||
}
|
||||
|
||||
let alpha = (left_bg.alpha * left_weight + right_bg.alpha * right_weight).min(1.);
|
||||
if alpha <= 0. {
|
||||
Self::transparent()
|
||||
} else {
|
||||
let inv = 1. / alpha;
|
||||
Self {
|
||||
lightness: lightness * inv,
|
||||
chroma: chroma * inv,
|
||||
hue,
|
||||
alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
fn from(rgba: RGBA) -> Self {
|
||||
fn linearize(value: f32) -> f32 {
|
||||
let sign = if value < 0. { -1. } else { 1. };
|
||||
let abs = value.abs();
|
||||
if abs < 0.04045 {
|
||||
return value / 12.92;
|
||||
}
|
||||
|
||||
sign * ((abs + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
|
||||
#[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.,
|
||||
);
|
||||
|
||||
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||
const BRADFORD: Transform3D<f32> = Transform3D::new(
|
||||
1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.,
|
||||
0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.,
|
||||
-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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<XYZA> 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 {
|
||||
const WHITE: [f32; 3] = [0.96422, 1., 0.82521];
|
||||
|
||||
fn compute_f(value: f32) -> f32 {
|
||||
const EPSILON: f32 = 216. / 24389.;
|
||||
const KAPPA: f32 = 24389. / 27.;
|
||||
|
||||
if value > EPSILON {
|
||||
value.cbrt()
|
||||
} else {
|
||||
(KAPPA * value + 16.) / 116.
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Convert D50-adapted XYZ to Lab.
|
||||
let f = [
|
||||
compute_f(xyza.x / WHITE[0]),
|
||||
compute_f(xyza.y / WHITE[1]),
|
||||
compute_f(xyza.z / WHITE[2]),
|
||||
];
|
||||
|
||||
let lightness = 116. * f[1] - 16.;
|
||||
let a = 500. * (f[0] - f[1]);
|
||||
let b = 200. * (f[1] - f[2]);
|
||||
|
||||
LABA {
|
||||
lightness,
|
||||
a,
|
||||
b,
|
||||
alpha: xyza.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LABA> for LCHA {
|
||||
/// Convert a LAB color to LCH as specified in [1].
|
||||
///
|
||||
/// [1]: https://drafts.csswg.org/css-color/#color-conversion-code
|
||||
fn from(laba: LABA) -> Self {
|
||||
let hue = laba.b.atan2(laba.a);
|
||||
let chroma = (laba.a * laba.a + laba.b * laba.b).sqrt();
|
||||
LCHA {
|
||||
lightness: laba.lightness,
|
||||
chroma,
|
||||
hue,
|
||||
alpha: laba.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LCHA> for LABA {
|
||||
/// Convert a LCH color to LAB as specified in [1].
|
||||
///
|
||||
/// [1]: https://drafts.csswg.org/css-color/#color-conversion-code
|
||||
fn from(lcha: LCHA) -> Self {
|
||||
let a = lcha.chroma * lcha.hue.cos();
|
||||
let b = lcha.chroma * lcha.hue.sin();
|
||||
LABA {
|
||||
lightness: lcha.lightness,
|
||||
a,
|
||||
b,
|
||||
alpha: lcha.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LABA> for XYZA {
|
||||
/// 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 from(laba: LABA) -> Self {
|
||||
// 1. Convert LAB to (D50-adapated) XYZ.
|
||||
const KAPPA: f32 = 24389. / 27.;
|
||||
const EPSILON: f32 = 216. / 24389.;
|
||||
const WHITE: [f32; 3] = [0.96422, 1., 0.82521];
|
||||
|
||||
let f1 = (laba.lightness + 16f32) / 116f32;
|
||||
let f0 = (laba.a / 500.) + f1;
|
||||
let f2 = f1 - laba.b / 200.;
|
||||
|
||||
let x = if f0.powf(3.) > EPSILON {
|
||||
f0.powf(3.)
|
||||
} else {
|
||||
(116. * f0 - 16.) / KAPPA
|
||||
};
|
||||
let y = if laba.lightness > KAPPA * EPSILON {
|
||||
((laba.lightness + 16.) / 116.).powf(3.)
|
||||
} else {
|
||||
laba.lightness / KAPPA
|
||||
};
|
||||
let z = if f2.powf(3.) > EPSILON {
|
||||
f2.powf(3.)
|
||||
} else {
|
||||
(116. * f2 - 16.) / KAPPA
|
||||
};
|
||||
|
||||
XYZA {
|
||||
x: x * WHITE[0],
|
||||
y: y * WHITE[1],
|
||||
z: z * WHITE[2],
|
||||
alpha: laba.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.,
|
||||
);
|
||||
|
||||
#[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 LABA {
|
||||
fn from(rgba: RGBA) -> Self {
|
||||
let xyza: XYZA = rgba.into();
|
||||
xyza.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LABA> for RGBA {
|
||||
fn from(laba: LABA) -> Self {
|
||||
let xyza: XYZA = laba.into();
|
||||
xyza.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RGBA> for LCHA {
|
||||
fn from(rgba: RGBA) -> Self {
|
||||
let xyza: XYZA = rgba.into();
|
||||
let laba: LABA = xyza.into();
|
||||
laba.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LCHA> for RGBA {
|
||||
fn from(lcha: LCHA) -> Self {
|
||||
let laba: LABA = lcha.into();
|
||||
let xyza: XYZA = laba.into();
|
||||
xyza.into()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use super::AllowQuirks;
|
|||
use crate::gecko_bindings::structs::nscolor;
|
||||
use crate::parser::{Parse, ParserContext};
|
||||
use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue};
|
||||
use crate::values::generics::color::{GenericColorOrAuto, GenericCaretColor};
|
||||
use crate::values::generics::color::{GenericCaretColor, GenericColorOrAuto};
|
||||
use crate::values::specified::calc::CalcNode;
|
||||
use crate::values::specified::Percentage;
|
||||
use crate::values::CustomIdent;
|
||||
|
@ -21,17 +21,51 @@ use std::io::Write as IoWrite;
|
|||
use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind};
|
||||
use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind};
|
||||
|
||||
/// A color space as defined in [1].
|
||||
///
|
||||
/// [1]: https://drafts.csswg.org/css-color-5/#typedef-colorspace
|
||||
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
|
||||
pub enum ColorSpaceKind {
|
||||
/// The sRGB color space.
|
||||
Srgb,
|
||||
/// The CIEXYZ color space.
|
||||
Xyz,
|
||||
/// The CIELAB color space.
|
||||
Lab,
|
||||
/// The CIELAB color space, expressed in cylindrical coordinates.
|
||||
Lch,
|
||||
}
|
||||
|
||||
/// A hue adjuster as defined in [1].
|
||||
///
|
||||
/// [1]: https://drafts.csswg.org/css-color-5/#typedef-hue-adjuster
|
||||
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
|
||||
pub enum HueAdjuster {
|
||||
/// The "shorter" angle adjustment.
|
||||
Shorter,
|
||||
/// The "longer" angle adjustment.
|
||||
Longer,
|
||||
/// The "increasing" angle adjustment.
|
||||
Increasing,
|
||||
/// The "decreasing" angle adjustment.
|
||||
Decreasing,
|
||||
/// The "specified" angle adjustment.
|
||||
Specified,
|
||||
}
|
||||
|
||||
/// A restricted version of the css `color-mix()` function, which only supports
|
||||
/// percentages and sRGB color-space interpolation.
|
||||
/// percentages.
|
||||
///
|
||||
/// https://drafts.csswg.org/css-color-5/#color-mix
|
||||
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ColorMix {
|
||||
pub color_space: ColorSpaceKind,
|
||||
pub left: Color,
|
||||
pub left_percentage: Percentage,
|
||||
pub right: Color,
|
||||
pub right_percentage: Percentage,
|
||||
pub hue_adjuster: HueAdjuster,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gecko")]
|
||||
|
@ -62,6 +96,9 @@ impl Parse for ColorMix {
|
|||
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
|
||||
}
|
||||
|
||||
let color_spaces_enabled = context.chrome_rules_enabled() ||
|
||||
static_prefs::pref!("layout.css.color-mix.color-spaces.enabled");
|
||||
|
||||
input.expect_function_matching("color-mix")?;
|
||||
|
||||
// NOTE(emilio): This implements the syntax described here for now,
|
||||
|
@ -70,12 +107,18 @@ impl Parse for ColorMix {
|
|||
// https://github.com/w3c/csswg-drafts/issues/6066#issuecomment-789836765
|
||||
input.parse_nested_block(|input| {
|
||||
input.expect_ident_matching("in")?;
|
||||
// TODO: support multiple interpolation spaces.
|
||||
input.expect_ident_matching("srgb")?;
|
||||
let color_space = if color_spaces_enabled {
|
||||
ColorSpaceKind::parse(input)?
|
||||
} else {
|
||||
input.expect_ident_matching("srgb")?;
|
||||
ColorSpaceKind::Srgb
|
||||
};
|
||||
input.expect_comma()?;
|
||||
|
||||
let left = Color::parse(context, input)?;
|
||||
let left_percentage = input.try_parse(|input| Percentage::parse(context, input)).ok();
|
||||
let left_percentage = input
|
||||
.try_parse(|input| Percentage::parse(context, input))
|
||||
.ok();
|
||||
|
||||
input.expect_comma()?;
|
||||
|
||||
|
@ -88,11 +131,18 @@ impl Parse for ColorMix {
|
|||
|
||||
let left_percentage =
|
||||
left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get()));
|
||||
|
||||
let hue_adjuster = input
|
||||
.try_parse(|input| HueAdjuster::parse(input))
|
||||
.unwrap_or(HueAdjuster::Shorter);
|
||||
|
||||
Ok(ColorMix {
|
||||
color_space,
|
||||
left,
|
||||
left_percentage,
|
||||
right,
|
||||
right_percentage,
|
||||
hue_adjuster,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -116,7 +166,9 @@ impl ToCss for ColorMix {
|
|||
(1.0 - percent.get() - other.get()).abs() <= f32::EPSILON
|
||||
}
|
||||
|
||||
dest.write_str("color-mix(in srgb, ")?;
|
||||
dest.write_str("color-mix(in ")?;
|
||||
self.color_space.to_css(dest)?;
|
||||
dest.write_str(", ")?;
|
||||
self.left.to_css(dest)?;
|
||||
if !can_omit(&self.left_percentage, &self.right_percentage, true) {
|
||||
dest.write_str(" ")?;
|
||||
|
@ -128,6 +180,12 @@ impl ToCss for ColorMix {
|
|||
dest.write_str(" ")?;
|
||||
self.right_percentage.to_css(dest)?;
|
||||
}
|
||||
|
||||
if self.hue_adjuster != HueAdjuster::Shorter {
|
||||
dest.write_str(" ")?;
|
||||
self.hue_adjuster.to_css(dest)?;
|
||||
}
|
||||
|
||||
dest.write_str(")")
|
||||
}
|
||||
}
|
||||
|
@ -433,7 +491,7 @@ impl SystemColor {
|
|||
return ComputedColor::currentcolor();
|
||||
}
|
||||
color
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -552,7 +610,9 @@ impl Parse for Color {
|
|||
}
|
||||
|
||||
if context.chrome_rules_enabled() {
|
||||
if let Ok((color, scheme)) = input.try_parse(|i| parse_moz_system_color(context, i)) {
|
||||
if let Ok((color, scheme)) =
|
||||
input.try_parse(|i| parse_moz_system_color(context, i))
|
||||
{
|
||||
return Ok(Color::System(color, scheme));
|
||||
}
|
||||
}
|
||||
|
@ -603,7 +663,7 @@ impl ToCss for Color {
|
|||
scheme.to_css(dest)?;
|
||||
dest.write_char(')')
|
||||
}
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "gecko")]
|
||||
Color::InheritFromBodyQuirk => Ok(()),
|
||||
}
|
||||
|
@ -762,10 +822,12 @@ impl Color {
|
|||
let left = mix.left.to_computed_color(context)?.to_animated_value();
|
||||
let right = mix.right.to_computed_color(context)?.to_animated_value();
|
||||
ToAnimatedValue::from_animated_value(AnimatedColor::mix(
|
||||
mix.color_space,
|
||||
&left,
|
||||
mix.left_percentage.get(),
|
||||
&right,
|
||||
mix.right_percentage.get(),
|
||||
mix.hue_adjuster,
|
||||
))
|
||||
},
|
||||
#[cfg(feature = "gecko")]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue