bevy_color: Added Xyza Colour Space (#12079)

# Objective

Add XYZ colour space. This will be most useful as a conversion step when
working with other (more common) colour spaces. See
[Wikipedia](https://en.wikipedia.org/wiki/CIE_1931_color_space) for
details on this space.

## Solution

- Added `Xyza` to `Color` and as its own type.

---

## Changelog

- Added `Xyza` type.
- Added `Color::Xyza` variant.

## Migration Guide

- `Color` enum now has an additional member, `Xyza`. Convert it to any
other type to handle this case in match statements.
This commit is contained in:
Zachary Harrold 2024-02-25 05:49:51 +11:00 committed by GitHub
parent 65267dd1f9
commit 972ca62831
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 312 additions and 4 deletions

View file

@ -1,4 +1,4 @@
use palette::{Hsl, IntoColor, Lch, LinSrgb, Oklab, Srgb};
use palette::{Hsl, IntoColor, Lch, LinSrgb, Oklab, Srgb, Xyz};
const TEST_COLORS: &[(f32, f32, f32, &str)] = &[
(0., 0., 0., "black"),
@ -25,7 +25,7 @@ fn main() {
println!(
"// Generated by gen_tests. Do not edit.
#[cfg(test)]
use crate::{{Hsla, Srgba, LinearRgba, Oklaba, Lcha}};
use crate::{{Hsla, Srgba, LinearRgba, Oklaba, Lcha, Xyza}};
#[cfg(test)]
pub struct TestColor {{
@ -35,6 +35,7 @@ pub struct TestColor {{
pub hsl: Hsla,
pub lch: Lcha,
pub oklab: Oklaba,
pub xyz: Xyza,
}}
"
);
@ -48,6 +49,7 @@ pub struct TestColor {{
let hsl: Hsl = srgb.into_color();
let lch: Lch = srgb.into_color();
let oklab: Oklab = srgb.into_color();
let xyz: Xyz = srgb.into_color();
println!(" // {name}");
println!(
" TestColor {{
@ -57,6 +59,7 @@ pub struct TestColor {{
hsl: Hsla::new({}, {}, {}, 1.0),
lch: Lcha::new({}, {}, {}, 1.0),
oklab: Oklaba::new({}, {}, {}, 1.0),
xyz: Xyza::new({}, {}, {}, 1.0),
}},",
VariablePrecision(srgb.red),
VariablePrecision(srgb.green),
@ -73,6 +76,9 @@ pub struct TestColor {{
VariablePrecision(oklab.l),
VariablePrecision(oklab.a),
VariablePrecision(oklab.b),
VariablePrecision(xyz.x),
VariablePrecision(xyz.y),
VariablePrecision(xyz.z),
);
}
println!("];");

View file

@ -1,4 +1,4 @@
use crate::{Alpha, Hsla, Lcha, LinearRgba, Oklaba, Srgba, StandardColor};
use crate::{Alpha, Hsla, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::Color as LegacyColor;
use serde::{Deserialize, Serialize};
@ -20,6 +20,8 @@ pub enum Color {
Lcha(Lcha),
/// A color in the Oklaba color space with alpha.
Oklaba(Oklaba),
/// A color in the XYZ color space with alpha.
Xyza(Xyza),
}
impl StandardColor for Color {}
@ -33,6 +35,7 @@ impl Color {
Color::Hsla(hsla) => (*hsla).into(),
Color::Lcha(lcha) => (*lcha).into(),
Color::Oklaba(oklab) => (*oklab).into(),
Color::Xyza(xyza) => (*xyza).into(),
}
}
}
@ -53,6 +56,7 @@ impl Alpha for Color {
Color::Hsla(x) => *x = x.with_alpha(alpha),
Color::Lcha(x) => *x = x.with_alpha(alpha),
Color::Oklaba(x) => *x = x.with_alpha(alpha),
Color::Xyza(x) => *x = x.with_alpha(alpha),
}
new
@ -65,6 +69,7 @@ impl Alpha for Color {
Color::Hsla(x) => x.alpha(),
Color::Lcha(x) => x.alpha(),
Color::Oklaba(x) => x.alpha(),
Color::Xyza(x) => x.alpha(),
}
}
}
@ -99,6 +104,12 @@ impl From<Lcha> for Color {
}
}
impl From<Xyza> for Color {
fn from(value: Xyza) -> Self {
Self::Xyza(value)
}
}
impl From<Color> for Srgba {
fn from(value: Color) -> Self {
match value {
@ -107,6 +118,7 @@ impl From<Color> for Srgba {
Color::Hsla(hsla) => hsla.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
@ -119,6 +131,7 @@ impl From<Color> for LinearRgba {
Color::Hsla(hsla) => hsla.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
@ -131,6 +144,7 @@ impl From<Color> for Hsla {
Color::Hsla(hsla) => hsla,
Color::Lcha(lcha) => LinearRgba::from(lcha).into(),
Color::Oklaba(oklab) => LinearRgba::from(oklab).into(),
Color::Xyza(xyza) => LinearRgba::from(xyza).into(),
}
}
}
@ -143,6 +157,7 @@ impl From<Color> for Lcha {
Color::Hsla(hsla) => Srgba::from(hsla).into(),
Color::Lcha(lcha) => lcha,
Color::Oklaba(oklab) => LinearRgba::from(oklab).into(),
Color::Xyza(xyza) => LinearRgba::from(xyza).into(),
}
}
}
@ -155,6 +170,20 @@ impl From<Color> for Oklaba {
Color::Hsla(hsla) => Srgba::from(hsla).into(),
Color::Lcha(lcha) => LinearRgba::from(lcha).into(),
Color::Oklaba(oklab) => oklab,
Color::Xyza(xyza) => LinearRgba::from(xyza).into(),
}
}
}
impl From<Color> for Xyza {
fn from(value: Color) -> Self {
match value {
Color::Srgba(x) => x.into(),
Color::LinearRgba(x) => x.into(),
Color::Hsla(x) => x.into(),
Color::Lcha(x) => x.into(),
Color::Oklaba(x) => x.into(),
Color::Xyza(xyza) => xyza,
}
}
}
@ -178,6 +207,7 @@ impl From<Color> for LegacyColor {
Color::Hsla(x) => x.into(),
Color::Lcha(x) => x.into(),
Color::Oklaba(x) => x.into(),
Color::Xyza(x) => x.into(),
}
}
}

View file

@ -7,6 +7,7 @@
//! - [`Hsla`] (hue, saturation, lightness, alpha)
//! - [`Lcha`] (lightness, chroma, hue, alpha)
//! - [`Oklaba`] (lightness, a-axis, b-axis, alpha)
//! - [`Xyza`] (x-axis, y-axis, z-axis, alpha)
//!
//! Each of these color spaces is represented as a distinct Rust type.
//!
@ -35,6 +36,10 @@
//! for tasks such as color correction and image analysis, where it is important to be able
//! to do things like change color saturation without causing hue shifts.
//!
//! XYZ is a foundational space commonly used in the definition of other more modern color
//! spaces. The space is more formally known as CIE 1931, where the `x` and `z` axes represent
//! a form of chromaticity, while `y` defines an illuminance level.
//!
//! See also the [Wikipedia article on color spaces](https://en.wikipedia.org/wiki/Color_space).
//!
//! # Conversions
@ -78,6 +83,7 @@ mod srgba;
mod test_colors;
#[cfg(test)]
mod testing;
mod xyza;
pub use color::*;
pub use color_ops::*;
@ -87,6 +93,7 @@ pub use lcha::*;
pub use linear_rgba::*;
pub use oklaba::*;
pub use srgba::*;
pub use xyza::*;
use bevy_render::color::Color as LegacyColor;
@ -106,6 +113,7 @@ where
Self: From<Hsla> + Into<Hsla>,
Self: From<Lcha> + Into<Lcha>,
Self: From<Oklaba> + Into<Oklaba>,
Self: From<Xyza> + Into<Xyza>,
Self: Alpha,
{
}

View file

@ -1,6 +1,6 @@
// Generated by gen_tests. Do not edit.
#[cfg(test)]
use crate::{Hsla, Lcha, LinearRgba, Oklaba, Srgba};
use crate::{Hsla, Lcha, LinearRgba, Oklaba, Srgba, Xyza};
#[cfg(test)]
pub struct TestColor {
@ -10,6 +10,7 @@ pub struct TestColor {
pub hsl: Hsla,
pub lch: Lcha,
pub oklab: Oklaba,
pub xyz: Xyza,
}
// Table of equivalent colors in various color spaces
@ -23,6 +24,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(0.0, 0.0, 0.0, 1.0),
lch: Lcha::new(0.0, 0.0, 0.0000136603785, 1.0),
oklab: Oklaba::new(0.0, 0.0, 0.0, 1.0),
xyz: Xyza::new(0.0, 0.0, 0.0, 1.0),
},
// white
TestColor {
@ -32,6 +34,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(0.0, 0.0, 1.0, 1.0),
lch: Lcha::new(1.0, 0.0, 0.0000136603785, 1.0),
oklab: Oklaba::new(1.0, 0.0, 0.000000059604645, 1.0),
xyz: Xyza::new(0.95047, 1.0, 1.08883, 1.0),
},
// red
TestColor {
@ -41,6 +44,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(0.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.53240794, 1.0455177, 39.99901, 1.0),
oklab: Oklaba::new(0.6279554, 0.22486295, 0.1258463, 1.0),
xyz: Xyza::new(0.4124564, 0.2126729, 0.0193339, 1.0),
},
// green
TestColor {
@ -50,6 +54,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(120.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.87734723, 1.1977587, 136.01595, 1.0),
oklab: Oklaba::new(0.8664396, -0.2338874, 0.1794985, 1.0),
xyz: Xyza::new(0.3575761, 0.7151522, 0.119192, 1.0),
},
// blue
TestColor {
@ -59,6 +64,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(240.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.32297012, 1.3380761, 306.28494, 1.0),
oklab: Oklaba::new(0.4520137, -0.032456964, -0.31152815, 1.0),
xyz: Xyza::new(0.1804375, 0.072175, 0.9503041, 1.0),
},
// yellow
TestColor {
@ -68,6 +74,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(60.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.9713927, 0.96905375, 102.85126, 1.0),
oklab: Oklaba::new(0.9679827, -0.07136908, 0.19856972, 1.0),
xyz: Xyza::new(0.7700325, 0.9278251, 0.1385259, 1.0),
},
// magenta
TestColor {
@ -77,6 +84,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(300.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.6032421, 1.1554068, 328.23495, 1.0),
oklab: Oklaba::new(0.7016738, 0.27456632, -0.16915613, 1.0),
xyz: Xyza::new(0.5928939, 0.28484792, 0.969638, 1.0),
},
// cyan
TestColor {
@ -86,6 +94,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(180.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.9111322, 0.50120866, 196.37614, 1.0),
oklab: Oklaba::new(0.90539926, -0.1494439, -0.039398134, 1.0),
xyz: Xyza::new(0.5380136, 0.78732723, 1.069496, 1.0),
},
// gray
TestColor {
@ -95,6 +104,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(0.0, 0.0, 0.5, 1.0),
lch: Lcha::new(0.5338897, 0.00000011920929, 90.0, 1.0),
oklab: Oklaba::new(0.5981807, 0.00000011920929, 0.0, 1.0),
xyz: Xyza::new(0.2034397, 0.21404117, 0.23305441, 1.0),
},
// olive
TestColor {
@ -104,6 +114,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(60.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0),
oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0),
xyz: Xyza::new(0.16481864, 0.19859275, 0.029650241, 1.0),
},
// purple
TestColor {
@ -113,6 +124,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(300.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0),
oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0),
xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0),
},
// teal
TestColor {
@ -122,6 +134,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(180.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0),
oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0),
xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0),
},
// maroon
TestColor {
@ -131,6 +144,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(0.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.2541851, 0.61091745, 38.350803, 1.0),
oklab: Oklaba::new(0.3756308, 0.13450874, 0.07527886, 1.0),
xyz: Xyza::new(0.08828264, 0.045520753, 0.0041382504, 1.0),
},
// lime
TestColor {
@ -140,6 +154,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(120.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.46052113, 0.71647626, 136.01596, 1.0),
oklab: Oklaba::new(0.5182875, -0.13990697, 0.10737252, 1.0),
xyz: Xyza::new(0.076536, 0.153072, 0.025511991, 1.0),
},
// navy
TestColor {
@ -149,6 +164,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(240.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.12890343, 0.8004114, 306.28494, 1.0),
oklab: Oklaba::new(0.27038592, -0.01941514, -0.18635012, 1.0),
xyz: Xyza::new(0.03862105, 0.01544842, 0.20340417, 1.0),
},
// orange
TestColor {
@ -158,6 +174,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(60.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0),
oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0),
xyz: Xyza::new(0.16481864, 0.19859275, 0.029650241, 1.0),
},
// fuchsia
TestColor {
@ -167,6 +184,7 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(300.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0),
oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0),
xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0),
},
// aqua
TestColor {
@ -176,5 +194,6 @@ pub const TEST_COLORS: &[TestColor] = &[
hsl: Hsla::new(180.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0),
oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0),
xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0),
},
];

View file

@ -0,0 +1,245 @@
use crate::{Alpha, Hsla, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::Color;
use serde::{Deserialize, Serialize};
/// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Xyza {
/// The x-axis. [0.0, 1.0]
pub x: f32,
/// The y-axis, intended to represent luminance. [0.0, 1.0]
pub y: f32,
/// The z-axis. [0.0, 1.0]
pub z: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl StandardColor for Xyza {}
impl Xyza {
/// Construct a new [`Xyza`] color from components.
///
/// # Arguments
///
/// * `x` - x-axis. [0.0, 1.0]
/// * `y` - y-axis. [0.0, 1.0]
/// * `z` - z-axis. [0.0, 1.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(x: f32, y: f32, z: f32, alpha: f32) -> Self {
Self { x, y, z, alpha }
}
/// Construct a new [`Xyza`] color from (x, y, z) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `x` - x-axis. [0.0, 1.0]
/// * `y` - y-axis. [0.0, 1.0]
/// * `z` - z-axis. [0.0, 1.0]
pub const fn rgb(x: f32, y: f32, z: f32) -> Self {
Self {
x,
y,
z,
alpha: 1.0,
}
}
}
impl Default for Xyza {
fn default() -> Self {
Self::new(0., 0., 0., 1.)
}
}
impl Alpha for Xyza {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl Luminance for Xyza {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {
Self {
y: lightness,
..*self
}
}
fn luminance(&self) -> f32 {
self.y
}
fn darker(&self, amount: f32) -> Self {
Self {
y: (self.y - amount).clamp(0., 1.),
..*self
}
}
fn lighter(&self, amount: f32) -> Self {
Self {
y: (self.y + amount).min(1.),
..*self
}
}
}
impl Mix for Xyza {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
Self {
x: self.x * n_factor + other.x * factor,
y: self.y * n_factor + other.y * factor,
z: self.z * n_factor + other.z * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl From<LinearRgba> for Xyza {
fn from(
LinearRgba {
red,
green,
blue,
alpha,
}: LinearRgba,
) -> Self {
// Linear sRGB to XYZ
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, RGB to XYZ [M])
let r = red;
let g = green;
let b = blue;
let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
let y = r * 0.2126729 + g * 0.7151522 + b * 0.072175;
let z = r * 0.0193339 + g * 0.119192 + b * 0.9503041;
Xyza::new(x, y, z, alpha)
}
}
impl From<Xyza> for LinearRgba {
fn from(Xyza { x, y, z, alpha }: Xyza) -> Self {
// XYZ to Linear sRGB
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, XYZ to RGB [M]-1)
let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
let g = x * -0.969266 + y * 1.8760108 + z * 0.041556;
let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
LinearRgba::new(r, g, b, alpha)
}
}
impl From<Srgba> for Xyza {
fn from(value: Srgba) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for Srgba {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Hsla> for Xyza {
fn from(value: Hsla) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for Hsla {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Lcha> for Xyza {
fn from(value: Lcha) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for Lcha {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Oklaba> for Xyza {
fn from(value: Oklaba) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for Oklaba {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Color> for Xyza {
fn from(value: Color) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for Color {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
Srgba,
};
#[test]
fn test_to_from_srgba() {
let xyza = Xyza::new(0.5, 0.5, 0.5, 1.0);
let srgba: Srgba = xyza.into();
let xyza2: Xyza = srgba.into();
assert_approx_eq!(xyza.x, xyza2.x, 0.001);
assert_approx_eq!(xyza.y, xyza2.y, 0.001);
assert_approx_eq!(xyza.z, xyza2.z, 0.001);
assert_approx_eq!(xyza.alpha, xyza2.alpha, 0.001);
}
#[test]
fn test_to_from_srgba_2() {
for color in TEST_COLORS.iter() {
let rgb2: Srgba = (color.xyz).into();
let xyz2: Xyza = (color.rgb).into();
assert!(
color.rgb.distance(&rgb2) < 0.00001,
"{}: {:?} != {:?}",
color.name,
color.rgb,
rgb2
);
assert_approx_eq!(color.xyz.x, xyz2.x, 0.001);
assert_approx_eq!(color.xyz.y, xyz2.y, 0.001);
assert_approx_eq!(color.xyz.z, xyz2.z, 0.001);
assert_approx_eq!(color.xyz.alpha, xyz2.alpha, 0.001);
}
}
}