mirror of
https://github.com/servo/servo.git
synced 2025-08-03 12:40:06 +01:00
style: Make the color interpolation code more generic
It's really piece-wise premultiplied interpolation, with a special-case for hue, so centralize the implementation. Differential Revision: https://phabricator.services.mozilla.com/D147003
This commit is contained in:
parent
0fc3267d2e
commit
95e9898db4
1 changed files with 160 additions and 260 deletions
|
@ -15,6 +15,7 @@ use euclid::default::{Transform3D, Vector3D};
|
||||||
/// Unlike in computed values, each component value may exceed the
|
/// Unlike in computed values, each component value may exceed the
|
||||||
/// range `[0.0, 1.0]`.
|
/// range `[0.0, 1.0]`.
|
||||||
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero)]
|
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero)]
|
||||||
|
#[repr(C)]
|
||||||
pub struct RGBA {
|
pub struct RGBA {
|
||||||
/// The red component.
|
/// The red component.
|
||||||
pub red: f32,
|
pub red: f32,
|
||||||
|
@ -179,7 +180,7 @@ impl Color {
|
||||||
let left_bg = S::from(left_color.scaled_rgba());
|
let left_bg = S::from(left_color.scaled_rgba());
|
||||||
let right_bg = S::from(right_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_interpolation);
|
let color = S::lerp(&left_bg, left_weight, &right_bg, right_weight, hue_interpolation);
|
||||||
let rgba: RGBA = color.into();
|
let rgba: RGBA = color.into();
|
||||||
let rgba = if !rgba.in_gamut() {
|
let rgba = if !rgba.in_gamut() {
|
||||||
// TODO: Better gamut mapping.
|
// TODO: Better gamut mapping.
|
||||||
|
@ -367,48 +368,175 @@ trait ModelledColor: Clone + Copy + From<RGBA> + Into<RGBA> {
|
||||||
/// The HueInterpolationMethod parameter is only for color spaces where the hue is
|
/// The HueInterpolationMethod parameter is only for color spaces where the hue is
|
||||||
/// represented as an angle (e.g., CIE LCH).
|
/// represented as an angle (e.g., CIE LCH).
|
||||||
fn lerp(
|
fn lerp(
|
||||||
left_bg: Self,
|
left_bg: &Self,
|
||||||
left_weight: f32,
|
left_weight: f32,
|
||||||
right_bg: Self,
|
right_bg: &Self,
|
||||||
right_weight: f32,
|
right_weight: f32,
|
||||||
hue_interpolation: HueInterpolationMethod,
|
hue_interpolation: HueInterpolationMethod,
|
||||||
) -> Self;
|
) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModelledColor for RGBA {
|
fn interpolate_premultiplied_component(
|
||||||
fn lerp(
|
left: f32,
|
||||||
left_bg: Self,
|
left_weight: f32,
|
||||||
left_weight: f32,
|
left_alpha: f32,
|
||||||
right_bg: Self,
|
right: f32,
|
||||||
right_weight: f32,
|
right_weight: f32,
|
||||||
_: HueInterpolationMethod,
|
right_alpha: f32,
|
||||||
) -> Self {
|
inverse_of_result_alpha: f32,
|
||||||
// Interpolation with alpha, as per
|
) -> f32 {
|
||||||
// https://drafts.csswg.org/css-color/#interpolation-alpha.
|
(left * left_weight * left_alpha + right * right_weight * right_alpha) * inverse_of_result_alpha
|
||||||
let mut red = 0.;
|
}
|
||||||
let mut green = 0.;
|
|
||||||
let mut blue = 0.;
|
|
||||||
|
|
||||||
// sRGB is a rectangular othogonal color space, so all component values
|
fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
|
||||||
// are multiplied by the alpha value.
|
use std::f32::consts::{PI, TAU};
|
||||||
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.);
|
// Adjust the hue angle as per
|
||||||
if alpha <= 0. {
|
// https://drafts.csswg.org/css-color/#hue-interpolation.
|
||||||
RGBA::transparent()
|
//
|
||||||
|
// 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.is_nan() {
|
||||||
|
if right.is_nan() {
|
||||||
|
*left = 0.;
|
||||||
|
*right = 0.;
|
||||||
} else {
|
} else {
|
||||||
let inv = 1. / alpha;
|
*left = *right;
|
||||||
RGBA::new(red * inv, green * inv, blue * inv, alpha)
|
}
|
||||||
|
} else if right.is_nan() {
|
||||||
|
*right = *left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hue_interpolation == HueInterpolationMethod::Specified {
|
||||||
|
// Angles are not adjusted. They are interpolated like any other
|
||||||
|
// component.
|
||||||
|
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);
|
||||||
|
|
||||||
|
match hue_interpolation {
|
||||||
|
// https://drafts.csswg.org/css-color/#shorter
|
||||||
|
HueInterpolationMethod::Shorter => {
|
||||||
|
let delta = *right - *left;
|
||||||
|
|
||||||
|
if delta > PI {
|
||||||
|
*left += PI;
|
||||||
|
} else if delta < -1. * PI {
|
||||||
|
*right += PI;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// https://drafts.csswg.org/css-color/#longer
|
||||||
|
HueInterpolationMethod::Longer => {
|
||||||
|
let delta = *right - *left;
|
||||||
|
if 0. < delta && delta < PI {
|
||||||
|
*left += TAU;
|
||||||
|
} else if -1. * PI < delta && delta < 0. {
|
||||||
|
*right += TAU;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// https://drafts.csswg.org/css-color/#increasing
|
||||||
|
HueInterpolationMethod::Increasing => {
|
||||||
|
if *right < *left {
|
||||||
|
*right += TAU;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// https://drafts.csswg.org/css-color/#decreasing
|
||||||
|
HueInterpolationMethod::Decreasing => {
|
||||||
|
if *left < *right {
|
||||||
|
*left += TAU;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HueInterpolationMethod::Specified => unreachable!("Handled above"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_hue(
|
||||||
|
mut left: f32,
|
||||||
|
left_weight: f32,
|
||||||
|
mut right: f32,
|
||||||
|
right_weight: f32,
|
||||||
|
hue_interpolation: HueInterpolationMethod,
|
||||||
|
) -> f32 {
|
||||||
|
adjust_hue(&mut left, &mut right, hue_interpolation);
|
||||||
|
left * left_weight + right * right_weight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_premultiplied(
|
||||||
|
left: &[f32; 4],
|
||||||
|
left_weight: f32,
|
||||||
|
right: &[f32; 4],
|
||||||
|
right_weight: f32,
|
||||||
|
hue_index: Option<usize>,
|
||||||
|
hue_interpolation: HueInterpolationMethod,
|
||||||
|
) -> [f32; 4] {
|
||||||
|
let left_alpha = left[3];
|
||||||
|
let right_alpha = right[3];
|
||||||
|
let result_alpha = (left_alpha * left_weight + right_alpha * right_weight).min(1.);
|
||||||
|
let mut result = [0.; 4];
|
||||||
|
if result_alpha <= 0. {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inverse_of_result_alpha = 1. / result_alpha;
|
||||||
|
for i in 0..3 {
|
||||||
|
let is_hue = hue_index == Some(i);
|
||||||
|
result[i] = if is_hue {
|
||||||
|
interpolate_hue(left[i], left_weight, right[i], right_weight, hue_interpolation)
|
||||||
|
} else {
|
||||||
|
interpolate_premultiplied_component(left[i], left_weight, left_alpha, right[i], right_weight, right_alpha, inverse_of_result_alpha)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
result[3] = result_alpha;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_lerp {
|
||||||
|
($ty:ident, $hue_index:expr) => {
|
||||||
|
// These ensure the transmutes below are sound.
|
||||||
|
const_assert_eq!(std::mem::size_of::<$ty>(), std::mem::size_of::<f32>() * 4);
|
||||||
|
const_assert_eq!(std::mem::align_of::<$ty>(), std::mem::align_of::<f32>());
|
||||||
|
impl ModelledColor for $ty {
|
||||||
|
fn lerp(
|
||||||
|
left: &Self,
|
||||||
|
left_weight: f32,
|
||||||
|
right: &Self,
|
||||||
|
right_weight: f32,
|
||||||
|
hue_interpolation: HueInterpolationMethod,
|
||||||
|
) -> Self {
|
||||||
|
use std::mem::transmute;
|
||||||
|
unsafe {
|
||||||
|
transmute::<[f32; 4], Self>(interpolate_premultiplied(
|
||||||
|
transmute::<&Self, &[f32; 4]>(left),
|
||||||
|
left_weight,
|
||||||
|
transmute::<&Self, &[f32; 4]>(right),
|
||||||
|
right_weight,
|
||||||
|
$hue_index,
|
||||||
|
hue_interpolation,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_lerp!(RGBA, None);
|
||||||
|
|
||||||
/// An animated XYZA colour.
|
/// An animated XYZA colour.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
#[repr(C)]
|
||||||
pub struct XYZA {
|
pub struct XYZA {
|
||||||
/// The x component.
|
/// The x component.
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
|
@ -420,58 +548,11 @@ pub struct XYZA {
|
||||||
pub alpha: f32,
|
pub alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XYZA {
|
impl_lerp!(XYZA, None);
|
||||||
/// 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,
|
|
||||||
_: HueInterpolationMethod,
|
|
||||||
) -> 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.
|
/// An animated LABA colour.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
#[repr(C)]
|
||||||
pub struct LABA {
|
pub struct LABA {
|
||||||
/// The lightness component.
|
/// The lightness component.
|
||||||
pub lightness: f32,
|
pub lightness: f32,
|
||||||
|
@ -483,55 +564,7 @@ pub struct LABA {
|
||||||
pub alpha: f32,
|
pub alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LABA {
|
impl_lerp!(LABA, None);
|
||||||
/// 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,
|
|
||||||
_: HueInterpolationMethod,
|
|
||||||
) -> 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.
|
/// An animated LCHA colour.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
@ -546,140 +579,7 @@ pub struct LCHA {
|
||||||
pub alpha: f32,
|
pub alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LCHA {
|
impl_lerp!(LCHA, Some(2));
|
||||||
/// 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_interpolation: HueInterpolationMethod) -> (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_interpolation != HueInterpolationMethod::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_interpolation {
|
|
||||||
HueInterpolationMethod::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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
HueInterpolationMethod::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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
HueInterpolationMethod::Increasing => {
|
|
||||||
if right_bg.hue < left_bg.hue {
|
|
||||||
right_bg.hue += TAU;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
HueInterpolationMethod::Decreasing => {
|
|
||||||
if left_bg.hue < right_bg.hue {
|
|
||||||
left_bg.hue += TAU;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//Angles are not adjusted. They are interpolated like any other
|
|
||||||
//component.
|
|
||||||
HueInterpolationMethod::Specified => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
(left_bg, right_bg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModelledColor for LCHA {
|
|
||||||
fn lerp(
|
|
||||||
left_bg: Self,
|
|
||||||
left_weight: f32,
|
|
||||||
right_bg: Self,
|
|
||||||
right_weight: f32,
|
|
||||||
hue_interpolation: HueInterpolationMethod,
|
|
||||||
) -> 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_interpolation);
|
|
||||||
|
|
||||||
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 {
|
impl From<RGBA> for XYZA {
|
||||||
/// Convert an RGBA colour to XYZ as specified in [1].
|
/// Convert an RGBA colour to XYZ as specified in [1].
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue