bevy_color: Added Hsva and Hwba Models (#12114)

# Objective

- Improve compatibility with CSS Module 4
- Simplify `Hsla` conversion functions

## Solution

- Added `Hsva` which implements the HSV color model.
- Added `Hwba` which implements the HWB color model.
- Updated `Color` and `LegacyColor` accordingly.

## Migration Guide

- Convert `Hsva` / `Hwba` to either `Hsla` or `Srgba` using the provided
`From` implementations and then handle accordingly.

## Notes

While the HSL color space is older than HWB, the formulation for HWB is
more directly related to RGB. Likewise, HSV is more closely related to
HWB than HSL. This makes the conversion of HSL to/from RGB more
naturally represented as the compound operation HSL <-> HSV <-> HWB <->
RGB. All `From` implementations for HSL, HSV, and HWB have been designed
to take the shortest path between itself and the target space.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Zachary Harrold 2024-02-26 23:25:49 +11:00 committed by GitHub
parent 8ec65525ab
commit f939c09f2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 678 additions and 65 deletions

View file

@ -1,4 +1,4 @@
use palette::{Hsl, IntoColor, Lch, LinSrgb, Oklab, Srgb, Xyz};
use palette::{Hsl, Hsv, Hwb, 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, Xyza}};
use crate::{{Hsla, Hsva, Hwba, Srgba, LinearRgba, Oklaba, Lcha, Xyza}};
#[cfg(test)]
pub struct TestColor {{
@ -33,6 +33,8 @@ pub struct TestColor {{
pub rgb: Srgba,
pub linear_rgb: LinearRgba,
pub hsl: Hsla,
pub hsv: Hsva,
pub hwb: Hwba,
pub lch: Lcha,
pub oklab: Oklaba,
pub xyz: Xyza,
@ -47,6 +49,8 @@ pub struct TestColor {{
let srgb = Srgb::new(*r, *g, *b);
let linear_rgb: LinSrgb = srgb.into_color();
let hsl: Hsl = srgb.into_color();
let hsv: Hsv = srgb.into_color();
let hwb: Hwb = srgb.into_color();
let lch: Lch = srgb.into_color();
let oklab: Oklab = srgb.into_color();
let xyz: Xyz = srgb.into_color();
@ -57,6 +61,8 @@ pub struct TestColor {{
rgb: Srgba::new({}, {}, {}, 1.0),
linear_rgb: LinearRgba::new({}, {}, {}, 1.0),
hsl: Hsla::new({}, {}, {}, 1.0),
hsv: Hsva::new({}, {}, {}, 1.0),
hwb: Hwba::new({}, {}, {}, 1.0),
lch: Lcha::new({}, {}, {}, 1.0),
oklab: Oklaba::new({}, {}, {}, 1.0),
xyz: Xyza::new({}, {}, {}, 1.0),
@ -70,6 +76,12 @@ pub struct TestColor {{
VariablePrecision(hsl.hue.into_positive_degrees()),
VariablePrecision(hsl.saturation),
VariablePrecision(hsl.lightness),
VariablePrecision(hsv.hue.into_positive_degrees()),
VariablePrecision(hsv.saturation),
VariablePrecision(hsv.value),
VariablePrecision(hwb.hue.into_positive_degrees()),
VariablePrecision(hwb.whiteness),
VariablePrecision(hwb.blackness),
VariablePrecision(lch.l / 100.0),
VariablePrecision(lch.chroma / 100.0),
VariablePrecision(lch.hue.into_positive_degrees()),

View file

@ -1,4 +1,4 @@
use crate::{Alpha, Hsla, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
use crate::{Alpha, Hsla, Hsva, Hwba, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
@ -15,6 +15,10 @@ pub enum Color {
LinearRgba(LinearRgba),
/// A color in the HSL color space with alpha.
Hsla(Hsla),
/// A color in the HSV color space with alpha.
Hsva(Hsva),
/// A color in the HWB color space with alpha.
Hwba(Hwba),
/// A color in the LCH color space with alpha.
Lcha(Lcha),
/// A color in the Oklaba color space with alpha.
@ -46,6 +50,8 @@ impl Alpha for Color {
Color::Srgba(x) => *x = x.with_alpha(alpha),
Color::LinearRgba(x) => *x = x.with_alpha(alpha),
Color::Hsla(x) => *x = x.with_alpha(alpha),
Color::Hsva(x) => *x = x.with_alpha(alpha),
Color::Hwba(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),
@ -59,6 +65,8 @@ impl Alpha for Color {
Color::Srgba(x) => x.alpha(),
Color::LinearRgba(x) => x.alpha(),
Color::Hsla(x) => x.alpha(),
Color::Hsva(x) => x.alpha(),
Color::Hwba(x) => x.alpha(),
Color::Lcha(x) => x.alpha(),
Color::Oklaba(x) => x.alpha(),
Color::Xyza(x) => x.alpha(),
@ -84,6 +92,18 @@ impl From<Hsla> for Color {
}
}
impl From<Hsva> for Color {
fn from(value: Hsva) -> Self {
Self::Hsva(value)
}
}
impl From<Hwba> for Color {
fn from(value: Hwba) -> Self {
Self::Hwba(value)
}
}
impl From<Oklaba> for Color {
fn from(value: Oklaba) -> Self {
Self::Oklaba(value)
@ -108,6 +128,8 @@ impl From<Color> for Srgba {
Color::Srgba(srgba) => srgba,
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => hsla.into(),
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
@ -121,6 +143,8 @@ impl From<Color> for LinearRgba {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear,
Color::Hsla(hsla) => hsla.into(),
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
@ -134,6 +158,38 @@ impl From<Color> for Hsla {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => hsla,
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
impl From<Color> for Hsva {
fn from(value: Color) -> Self {
match value {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => hsla.into(),
Color::Hsva(hsva) => hsva,
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
impl From<Color> for Hwba {
fn from(value: Color) -> Self {
match value {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => hsla.into(),
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba,
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
@ -147,6 +203,8 @@ impl From<Color> for Lcha {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => hsla.into(),
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(lcha) => lcha,
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
@ -160,6 +218,8 @@ impl From<Color> for Oklaba {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => hsla.into(),
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab,
Color::Xyza(xyza) => xyza.into(),
@ -173,6 +233,8 @@ impl From<Color> for Xyza {
Color::Srgba(x) => x.into(),
Color::LinearRgba(x) => x.into(),
Color::Hsla(x) => x.into(),
Color::Hsva(hsva) => hsva.into(),
Color::Hwba(hwba) => hwba.into(),
Color::Lcha(x) => x.into(),
Color::Oklaba(x) => x.into(),
Color::Xyza(xyza) => xyza,

View file

@ -1,8 +1,9 @@
use crate::{Alpha, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use crate::{Alpha, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
/// Color in Hue-Saturation-Lightness color space with alpha
/// Color in Hue-Saturation-Lightness (HSL) color space with alpha.
/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Hsla {
@ -127,41 +128,7 @@ impl Luminance for Hsla {
}
}
impl From<Srgba> for Hsla {
fn from(
Srgba {
red,
green,
blue,
alpha,
}: Srgba,
) -> Self {
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
let x_max = red.max(green.max(blue));
let x_min = red.min(green.min(blue));
let chroma = x_max - x_min;
let lightness = (x_max + x_min) / 2.0;
let hue = if chroma == 0.0 {
0.0
} else if red == x_max {
60.0 * (green - blue) / chroma
} else if green == x_max {
60.0 * (2.0 + (blue - red) / chroma)
} else {
60.0 * (4.0 + (red - green) / chroma)
};
let hue = if hue < 0.0 { 360.0 + hue } else { hue };
let saturation = if lightness <= 0.0 || lightness >= 1.0 {
0.0
} else {
(x_max - lightness) / lightness.min(1.0 - lightness)
};
Self::new(hue, saturation, lightness, alpha)
}
}
impl From<Hsla> for Srgba {
impl From<Hsla> for Hsva {
fn from(
Hsla {
hue,
@ -170,48 +137,78 @@ impl From<Hsla> for Srgba {
alpha,
}: Hsla,
) -> Self {
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
let hue_prime = hue / 60.0;
let largest_component = chroma * (1.0 - (hue_prime % 2.0 - 1.0).abs());
let (r_temp, g_temp, b_temp) = if hue_prime < 1.0 {
(chroma, largest_component, 0.0)
} else if hue_prime < 2.0 {
(largest_component, chroma, 0.0)
} else if hue_prime < 3.0 {
(0.0, chroma, largest_component)
} else if hue_prime < 4.0 {
(0.0, largest_component, chroma)
} else if hue_prime < 5.0 {
(largest_component, 0.0, chroma)
// Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
let value = lightness + saturation * lightness.min(1. - lightness);
let saturation = if value == 0. {
0.
} else {
(chroma, 0.0, largest_component)
2. * (1. - (lightness / value))
};
let lightness_match = lightness - chroma / 2.0;
let red = r_temp + lightness_match;
let green = g_temp + lightness_match;
let blue = b_temp + lightness_match;
Hsva::new(hue, saturation, value, alpha)
}
}
Self::new(red, green, blue, alpha)
impl From<Hsva> for Hsla {
fn from(
Hsva {
hue,
saturation,
value,
alpha,
}: Hsva,
) -> Self {
// Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
let lightness = value * (1. - saturation / 2.);
let saturation = if lightness == 0. || lightness == 1. {
0.
} else {
(value - lightness) / lightness.min(1. - lightness)
};
Hsla::new(hue, saturation, lightness, alpha)
}
}
impl From<Hwba> for Hsla {
fn from(value: Hwba) -> Self {
Hsva::from(value).into()
}
}
impl From<Srgba> for Hsla {
fn from(value: Srgba) -> Self {
Hsva::from(value).into()
}
}
impl From<Hsla> for Srgba {
fn from(value: Hsla) -> Self {
Hsva::from(value).into()
}
}
impl From<Hsla> for Hwba {
fn from(value: Hsla) -> Self {
Hsva::from(value).into()
}
}
impl From<LinearRgba> for Hsla {
fn from(value: LinearRgba) -> Self {
Srgba::from(value).into()
Hsva::from(value).into()
}
}
impl From<Oklaba> for Hsla {
fn from(value: Oklaba) -> Self {
Srgba::from(value).into()
Hsva::from(value).into()
}
}
impl From<Lcha> for Hsla {
fn from(value: Lcha) -> Self {
Srgba::from(value).into()
Hsva::from(value).into()
}
}

View file

@ -0,0 +1,216 @@
use crate::{Alpha, Hwba, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
/// Color in Hue-Saturation-Value (HSV) color space with alpha.
/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Hsva {
/// The hue channel. [0.0, 360.0]
pub hue: f32,
/// The saturation channel. [0.0, 1.0]
pub saturation: f32,
/// The value channel. [0.0, 1.0]
pub value: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl StandardColor for Hsva {}
impl Hsva {
/// Construct a new [`Hsva`] color from components.
///
/// # Arguments
///
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `saturation` - Saturation channel. [0.0, 1.0]
/// * `value` - Value channel. [0.0, 1.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self {
Self {
hue,
saturation,
value,
alpha,
}
}
/// Construct a new [`Hsva`] color from (h, s, v) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `saturation` - Saturation channel. [0.0, 1.0]
/// * `value` - Value channel. [0.0, 1.0]
pub const fn hsv(hue: f32, saturation: f32, value: f32) -> Self {
Self::new(hue, saturation, value, 1.0)
}
/// Return a copy of this color with the hue channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// Return a copy of this color with the saturation channel set to the given value.
pub const fn with_saturation(self, saturation: f32) -> Self {
Self { saturation, ..self }
}
/// Return a copy of this color with the value channel set to the given value.
pub const fn with_value(self, value: f32) -> Self {
Self { value, ..self }
}
}
impl Default for Hsva {
fn default() -> Self {
Self::new(0., 0., 1., 1.)
}
}
impl Alpha for Hsva {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl From<Hsva> for Hwba {
fn from(
Hsva {
hue,
saturation,
value,
alpha,
}: Hsva,
) -> Self {
// Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion
let whiteness = (1. - saturation) * value;
let blackness = 1. - value;
Hwba::new(hue, whiteness, blackness, alpha)
}
}
impl From<Hwba> for Hsva {
fn from(
Hwba {
hue,
whiteness,
blackness,
alpha,
}: Hwba,
) -> Self {
// Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion
let value = 1. - blackness;
let saturation = 1. - (whiteness / value);
Hsva::new(hue, saturation, value, alpha)
}
}
impl From<Srgba> for Hsva {
fn from(value: Srgba) -> Self {
Hwba::from(value).into()
}
}
impl From<Hsva> for Srgba {
fn from(value: Hsva) -> Self {
Hwba::from(value).into()
}
}
impl From<LinearRgba> for Hsva {
fn from(value: LinearRgba) -> Self {
Hwba::from(value).into()
}
}
impl From<Hsva> for LinearRgba {
fn from(value: Hsva) -> Self {
Hwba::from(value).into()
}
}
impl From<Lcha> for Hsva {
fn from(value: Lcha) -> Self {
Hwba::from(value).into()
}
}
impl From<Hsva> for Lcha {
fn from(value: Hsva) -> Self {
Hwba::from(value).into()
}
}
impl From<Oklaba> for Hsva {
fn from(value: Oklaba) -> Self {
Hwba::from(value).into()
}
}
impl From<Hsva> for Oklaba {
fn from(value: Hsva) -> Self {
Hwba::from(value).into()
}
}
impl From<Xyza> for Hsva {
fn from(value: Xyza) -> Self {
Hwba::from(value).into()
}
}
impl From<Hsva> for Xyza {
fn from(value: Hsva) -> Self {
Hwba::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 hsva = Hsva::new(180., 0.5, 0.5, 1.0);
let srgba: Srgba = hsva.into();
let hsva2: Hsva = srgba.into();
assert_approx_eq!(hsva.hue, hsva2.hue, 0.001);
assert_approx_eq!(hsva.saturation, hsva2.saturation, 0.001);
assert_approx_eq!(hsva.value, hsva2.value, 0.001);
assert_approx_eq!(hsva.alpha, hsva2.alpha, 0.001);
}
#[test]
fn test_to_from_srgba_2() {
for color in TEST_COLORS.iter() {
let rgb2: Srgba = (color.hsv).into();
let hsv2: Hsva = (color.rgb).into();
assert!(
color.rgb.distance(&rgb2) < 0.00001,
"{}: {:?} != {:?}",
color.name,
color.rgb,
rgb2
);
assert_approx_eq!(color.hsv.hue, hsv2.hue, 0.001);
assert_approx_eq!(color.hsv.saturation, hsv2.saturation, 0.001);
assert_approx_eq!(color.hsv.value, hsv2.value, 0.001);
assert_approx_eq!(color.hsv.alpha, hsv2.alpha, 0.001);
}
}
}

View file

@ -0,0 +1,249 @@
//! Implementation of the Hue-Whiteness-Blackness (HWB) color model as described
//! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_.
//!
//! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
use crate::{Alpha, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
/// Color in Hue-Whiteness-Blackness (HWB) color space with alpha.
/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HWB_color_model).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Hwba {
/// The hue channel. [0.0, 360.0]
pub hue: f32,
/// The whiteness channel. [0.0, 1.0]
pub whiteness: f32,
/// The blackness channel. [0.0, 1.0]
pub blackness: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl StandardColor for Hwba {}
impl Hwba {
/// Construct a new [`Hwba`] color from components.
///
/// # Arguments
///
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `whiteness` - Whiteness channel. [0.0, 1.0]
/// * `blackness` - Blackness channel. [0.0, 1.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(hue: f32, whiteness: f32, blackness: f32, alpha: f32) -> Self {
Self {
hue,
whiteness,
blackness,
alpha,
}
}
/// Construct a new [`Hwba`] color from (h, s, l) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `whiteness` - Whiteness channel. [0.0, 1.0]
/// * `blackness` - Blackness channel. [0.0, 1.0]
pub const fn hwb(hue: f32, whiteness: f32, blackness: f32) -> Self {
Self::new(hue, whiteness, blackness, 1.0)
}
/// Return a copy of this color with the hue channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// Return a copy of this color with the whiteness channel set to the given value.
pub const fn with_whiteness(self, whiteness: f32) -> Self {
Self { whiteness, ..self }
}
/// Return a copy of this color with the blackness channel set to the given value.
pub const fn with_blackness(self, blackness: f32) -> Self {
Self { blackness, ..self }
}
}
impl Default for Hwba {
fn default() -> Self {
Self::new(0., 0., 1., 1.)
}
}
impl Alpha for Hwba {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl From<Srgba> for Hwba {
fn from(
Srgba {
red,
green,
blue,
alpha,
}: Srgba,
) -> Self {
// Based on "HWB - A More Intuitive Hue-Based Color Model" Appendix B
let x_max = 0f32.max(red).max(green).max(blue);
let x_min = 1f32.min(red).min(green).min(blue);
let chroma = x_max - x_min;
let hue = if chroma == 0.0 {
0.0
} else if red == x_max {
60.0 * (green - blue) / chroma
} else if green == x_max {
60.0 * (2.0 + (blue - red) / chroma)
} else {
60.0 * (4.0 + (red - green) / chroma)
};
let hue = if hue < 0.0 { 360.0 + hue } else { hue };
let whiteness = x_min;
let blackness = 1.0 - x_max;
Hwba {
hue,
whiteness,
blackness,
alpha,
}
}
}
impl From<Hwba> for Srgba {
fn from(
Hwba {
hue,
whiteness,
blackness,
alpha,
}: Hwba,
) -> Self {
// Based on "HWB - A More Intuitive Hue-Based Color Model" Appendix B
let w = whiteness;
let v = 1. - blackness;
let h = (hue % 360.) / 60.;
let i = h.floor();
let f = h - i;
let i = i as u8;
let f = if i % 2 == 0 { f } else { 1. - f };
let n = w + f * (v - w);
let (red, green, blue) = match i {
0 => (v, n, w),
1 => (n, v, w),
2 => (w, v, n),
3 => (w, n, v),
4 => (n, w, v),
5 => (v, w, n),
_ => unreachable!("i is bounded in [0, 6)"),
};
Srgba::new(red, green, blue, alpha)
}
}
impl From<LinearRgba> for Hwba {
fn from(value: LinearRgba) -> Self {
Srgba::from(value).into()
}
}
impl From<Hwba> for LinearRgba {
fn from(value: Hwba) -> Self {
Srgba::from(value).into()
}
}
impl From<Lcha> for Hwba {
fn from(value: Lcha) -> Self {
Srgba::from(value).into()
}
}
impl From<Hwba> for Lcha {
fn from(value: Hwba) -> Self {
Srgba::from(value).into()
}
}
impl From<Oklaba> for Hwba {
fn from(value: Oklaba) -> Self {
Srgba::from(value).into()
}
}
impl From<Hwba> for Oklaba {
fn from(value: Hwba) -> Self {
Srgba::from(value).into()
}
}
impl From<Xyza> for Hwba {
fn from(value: Xyza) -> Self {
Srgba::from(value).into()
}
}
impl From<Hwba> for Xyza {
fn from(value: Hwba) -> Self {
Srgba::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 hwba = Hwba::new(0.0, 0.5, 0.5, 1.0);
let srgba: Srgba = hwba.into();
let hwba2: Hwba = srgba.into();
assert_approx_eq!(hwba.hue, hwba2.hue, 0.001);
assert_approx_eq!(hwba.whiteness, hwba2.whiteness, 0.001);
assert_approx_eq!(hwba.blackness, hwba2.blackness, 0.001);
assert_approx_eq!(hwba.alpha, hwba2.alpha, 0.001);
}
#[test]
fn test_to_from_srgba_2() {
for color in TEST_COLORS.iter() {
let rgb2: Srgba = (color.hwb).into();
let hwb2: Hwba = (color.rgb).into();
assert!(
color.rgb.distance(&rgb2) < 0.00001,
"{}: {:?} != {:?}",
color.name,
color.rgb,
rgb2
);
assert_approx_eq!(color.hwb.hue, hwb2.hue, 0.001);
assert_approx_eq!(color.hwb.whiteness, hwb2.whiteness, 0.001);
assert_approx_eq!(color.hwb.blackness, hwb2.blackness, 0.001);
assert_approx_eq!(color.hwb.alpha, hwb2.alpha, 0.001);
}
}
}

View file

@ -5,6 +5,8 @@
//! - [`Srgba`] (standard RGBA, with gamma correction)
//! - [`LinearRgba`] (linear RGBA, without gamma correction)
//! - [`Hsla`] (hue, saturation, lightness, alpha)
//! - [`Hsva`] (hue, saturation, value, alpha)
//! - [`Hwba`] (hue, whiteness, blackness, alpha)
//! - [`Lcha`] (lightness, chroma, hue, alpha)
//! - [`Oklaba`] (lightness, a-axis, b-axis, alpha)
//! - [`Xyza`] (x-axis, y-axis, z-axis, alpha)
@ -31,6 +33,11 @@
//! A gradient in HSL space from red to violet will produce a rainbow. The LCH color space is
//! more perceptually accurate than HSL, but is less intuitive to work with.
//!
//! HSV and HWB are very closely related to HSL in their derivation, having identical definitions for
//! hue. Where HSL uses saturation and lightness, HSV uses a slightly modified definition of saturation,
//! and an analog of lightness in the form of value. In contrast, HWB instead uses whiteness and blackness
//! parameters, which can be used to lighten and darken a particular hue respectively.
//!
//! Oklab is a perceptually uniform color space that is designed to be used for tasks such
//! as image processing. It is not as widely used as the other color spaces, but it is useful
//! for tasks such as color correction and image analysis, where it is important to be able
@ -74,6 +81,8 @@ pub mod color_difference;
mod color_ops;
mod color_range;
mod hsla;
mod hsva;
mod hwba;
mod lcha;
mod linear_rgba;
mod oklaba;
@ -89,6 +98,8 @@ pub use color::*;
pub use color_ops::*;
pub use color_range::*;
pub use hsla::*;
pub use hsva::*;
pub use hwba::*;
pub use lcha::*;
pub use linear_rgba::*;
pub use oklaba::*;
@ -109,6 +120,8 @@ where
Self: From<Srgba> + Into<Srgba>,
Self: From<LinearRgba> + Into<LinearRgba>,
Self: From<Hsla> + Into<Hsla>,
Self: From<Hsva> + Into<Hsva>,
Self: From<Hwba> + Into<Hwba>,
Self: From<Lcha> + Into<Lcha>,
Self: From<Oklaba> + Into<Oklaba>,
Self: From<Xyza> + Into<Xyza>,

View file

@ -1,6 +1,6 @@
// Generated by gen_tests. Do not edit.
#[cfg(test)]
use crate::{Hsla, Lcha, LinearRgba, Oklaba, Srgba, Xyza};
use crate::{Hsla, Hsva, Hwba, Lcha, LinearRgba, Oklaba, Srgba, Xyza};
#[cfg(test)]
pub struct TestColor {
@ -8,6 +8,8 @@ pub struct TestColor {
pub rgb: Srgba,
pub linear_rgb: LinearRgba,
pub hsl: Hsla,
pub hsv: Hsva,
pub hwb: Hwba,
pub lch: Lcha,
pub oklab: Oklaba,
pub xyz: Xyza,
@ -23,6 +25,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.0, 0.0, 0.0, 1.0),
hsl: Hsla::new(0.0, 0.0, 0.0, 1.0),
lch: Lcha::new(0.0, 0.0, 0.0000136603785, 1.0),
hsv: Hsva::new(0.0, 0.0, 0.0, 1.0),
hwb: Hwba::new(0.0, 0.0, 1.0, 1.0),
oklab: Oklaba::new(0.0, 0.0, 0.0, 1.0),
xyz: Xyza::new(0.0, 0.0, 0.0, 1.0),
},
@ -33,6 +37,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(1.0, 1.0, 1.0, 1.0),
hsl: Hsla::new(0.0, 0.0, 1.0, 1.0),
lch: Lcha::new(1.0, 0.0, 0.0000136603785, 1.0),
hsv: Hsva::new(0.0, 0.0, 1.0, 1.0),
hwb: Hwba::new(0.0, 1.0, 0.0, 1.0),
oklab: Oklaba::new(1.0, 0.0, 0.000000059604645, 1.0),
xyz: Xyza::new(0.95047, 1.0, 1.08883, 1.0),
},
@ -44,6 +50,8 @@ 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),
hsv: Hsva::new(0.0, 1.0, 1.0, 1.0),
hwb: Hwba::new(0.0, 0.0, 0.0, 1.0),
xyz: Xyza::new(0.4124564, 0.2126729, 0.0193339, 1.0),
},
// green
@ -53,6 +61,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.0, 1.0, 0.0, 1.0),
hsl: Hsla::new(120.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.87734723, 1.1977587, 136.01595, 1.0),
hsv: Hsva::new(120.0, 1.0, 1.0, 1.0),
hwb: Hwba::new(120.0, 0.0, 0.0, 1.0),
oklab: Oklaba::new(0.8664396, -0.2338874, 0.1794985, 1.0),
xyz: Xyza::new(0.3575761, 0.7151522, 0.119192, 1.0),
},
@ -64,6 +74,8 @@ 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),
hsv: Hsva::new(240.0, 1.0, 1.0, 1.0),
hwb: Hwba::new(240.0, 0.0, 0.0, 1.0),
xyz: Xyza::new(0.1804375, 0.072175, 0.9503041, 1.0),
},
// yellow
@ -74,6 +86,8 @@ 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),
hsv: Hsva::new(60.0, 1.0, 1.0, 1.0),
hwb: Hwba::new(60.0, 0.0, 0.0, 1.0),
xyz: Xyza::new(0.7700325, 0.9278251, 0.1385259, 1.0),
},
// magenta
@ -82,6 +96,8 @@ pub const TEST_COLORS: &[TestColor] = &[
rgb: Srgba::new(1.0, 0.0, 1.0, 1.0),
linear_rgb: LinearRgba::new(1.0, 0.0, 1.0, 1.0),
hsl: Hsla::new(300.0, 1.0, 0.5, 1.0),
hsv: Hsva::new(300.0, 1.0, 1.0, 1.0),
hwb: Hwba::new(300.0, 0.0, 0.0, 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),
@ -94,6 +110,8 @@ 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),
hsv: Hsva::new(180.0, 1.0, 1.0, 1.0),
hwb: Hwba::new(180.0, 0.0, 0.0, 1.0),
xyz: Xyza::new(0.5380136, 0.78732723, 1.069496, 1.0),
},
// gray
@ -104,6 +122,8 @@ 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),
hsv: Hsva::new(0.0, 0.0, 0.5, 1.0),
hwb: Hwba::new(0.0, 0.5, 0.5, 1.0),
xyz: Xyza::new(0.2034397, 0.21404117, 0.23305441, 1.0),
},
// olive
@ -113,6 +133,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.0, 1.0),
hsl: Hsla::new(60.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0),
hsv: Hsva::new(60.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(60.0, 0.0, 0.5, 1.0),
oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0),
xyz: Xyza::new(0.16481864, 0.19859275, 0.029650241, 1.0),
},
@ -123,6 +145,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.21404114, 1.0),
hsl: Hsla::new(300.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0),
hsv: Hsva::new(300.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(300.0, 0.0, 0.5, 1.0),
oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0),
xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0),
},
@ -134,6 +158,8 @@ 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),
hsv: Hsva::new(180.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(180.0, 0.0, 0.5, 1.0),
xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0),
},
// maroon
@ -142,6 +168,8 @@ pub const TEST_COLORS: &[TestColor] = &[
rgb: Srgba::new(0.5, 0.0, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.0, 1.0),
hsl: Hsla::new(0.0, 1.0, 0.25, 1.0),
hsv: Hsva::new(0.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(0.0, 0.0, 0.5, 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),
@ -152,6 +180,8 @@ pub const TEST_COLORS: &[TestColor] = &[
rgb: Srgba::new(0.0, 0.5, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.21404114, 0.0, 1.0),
hsl: Hsla::new(120.0, 1.0, 0.25, 1.0),
hsv: Hsva::new(120.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(120.0, 0.0, 0.5, 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),
@ -163,6 +193,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.0, 0.0, 0.21404114, 1.0),
hsl: Hsla::new(240.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.12890343, 0.8004114, 306.28494, 1.0),
hsv: Hsva::new(240.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(240.0, 0.0, 0.5, 1.0),
oklab: Oklaba::new(0.27038592, -0.01941514, -0.18635012, 1.0),
xyz: Xyza::new(0.03862105, 0.01544842, 0.20340417, 1.0),
},
@ -173,6 +205,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.0, 1.0),
hsl: Hsla::new(60.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0),
hsv: Hsva::new(60.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(60.0, 0.0, 0.5, 1.0),
oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0),
xyz: Xyza::new(0.16481864, 0.19859275, 0.029650241, 1.0),
},
@ -183,6 +217,8 @@ pub const TEST_COLORS: &[TestColor] = &[
linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.21404114, 1.0),
hsl: Hsla::new(300.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0),
hsv: Hsva::new(300.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(300.0, 0.0, 0.5, 1.0),
oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0),
xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0),
},
@ -194,6 +230,8 @@ 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),
hsv: Hsva::new(180.0, 1.0, 0.5, 1.0),
hwb: Hwba::new(180.0, 0.0, 0.5, 1.0),
xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0),
},
];

View file

@ -1,4 +1,4 @@
use bevy_color::{Color, HexColorError, Hsla, Lcha, LinearRgba, Oklaba, Srgba, Xyza};
use bevy_color::{Color, HexColorError, Hsla, Hsva, Hwba, Lcha, LinearRgba, Oklaba, Srgba, Xyza};
use bevy_math::{Vec3, Vec4};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
@ -922,6 +922,8 @@ impl From<Color> for LegacyColor {
Color::Srgba(x) => x.into(),
Color::LinearRgba(x) => x.into(),
Color::Hsla(x) => x.into(),
Color::Hsva(x) => x.into(),
Color::Hwba(x) => x.into(),
Color::Lcha(x) => x.into(),
Color::Oklaba(x) => x.into(),
Color::Xyza(x) => x.into(),
@ -1006,6 +1008,30 @@ impl From<LegacyColor> for Hsla {
}
}
impl From<LegacyColor> for Hsva {
fn from(value: LegacyColor) -> Self {
Hsla::from(value).into()
}
}
impl From<Hsva> for LegacyColor {
fn from(value: Hsva) -> Self {
Hsla::from(value).into()
}
}
impl From<LegacyColor> for Hwba {
fn from(value: LegacyColor) -> Self {
Hsla::from(value).into()
}
}
impl From<Hwba> for LegacyColor {
fn from(value: Hwba) -> Self {
Hsla::from(value).into()
}
}
impl From<Lcha> for LegacyColor {
fn from(
Lcha {