Bug 1770616 - Make the color interpolation code more generic. r=barret

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:
Emilio Cobos Álvarez 2022-05-31 08:33:47 +00:00
Родитель 711d458368
Коммит 817ea22aaf
1 изменённых файлов: 160 добавлений и 260 удалений

Просмотреть файл

@ -15,6 +15,7 @@ use euclid::default::{Transform3D, Vector3D};
/// Unlike in computed values, each component value may exceed the
/// range `[0.0, 1.0]`.
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero)]
#[repr(C)]
pub struct RGBA {
/// The red component.
pub red: f32,
@ -179,7 +180,7 @@ impl Color {
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_interpolation);
let color = S::lerp(&left_bg, left_weight, &right_bg, right_weight, hue_interpolation);
let rgba: RGBA = color.into();
let rgba = if !rgba.in_gamut() {
// 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
/// represented as an angle (e.g., CIE LCH).
fn lerp(
left_bg: Self,
left_bg: &Self,
left_weight: f32,
right_bg: Self,
right_bg: &Self,
right_weight: f32,
hue_interpolation: HueInterpolationMethod,
) -> Self;
}
impl ModelledColor for RGBA {
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 red = 0.;
let mut green = 0.;
let mut blue = 0.;
fn interpolate_premultiplied_component(
left: f32,
left_weight: f32,
left_alpha: f32,
right: f32,
right_weight: f32,
right_alpha: f32,
inverse_of_result_alpha: f32,
) -> f32 {
(left * left_weight * left_alpha + right * right_weight * right_alpha) * inverse_of_result_alpha
}
// 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;
}
fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
use std::f32::consts::{PI, TAU};
let alpha = (left_bg.alpha * left_weight + right_bg.alpha * right_weight).min(1.);
if alpha <= 0. {
RGBA::transparent()
// 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.is_nan() {
if right.is_nan() {
*left = 0.;
*right = 0.;
} else {
let inv = 1. / alpha;
RGBA::new(red * inv, green * inv, blue * inv, alpha)
*left = *right;
}
} 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.
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct XYZA {
/// The x component.
pub x: f32,
@ -420,58 +548,11 @@ pub struct XYZA {
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,
_: 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,
}
}
}
}
impl_lerp!(XYZA, None);
/// An animated LABA colour.
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct LABA {
/// The lightness component.
pub lightness: f32,
@ -483,55 +564,7 @@ pub struct LABA {
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,
_: 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,
}
}
}
}
impl_lerp!(LABA, None);
/// An animated LCHA colour.
#[derive(Clone, Copy, Debug)]
@ -546,140 +579,7 @@ pub struct LCHA {
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_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_lerp!(LCHA, Some(2));
impl From<RGBA> for XYZA {
/// Convert an RGBA colour to XYZ as specified in [1].