From f939c09f2c7c257c6b01c8af432c429dbf9df1d6 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Mon, 26 Feb 2024 23:25:49 +1100 Subject: [PATCH] `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 --- .../bevy_color/crates/gen_tests/src/main.rs | 16 +- crates/bevy_color/src/color.rs | 64 ++++- crates/bevy_color/src/hsla.rs | 117 ++++---- crates/bevy_color/src/hsva.rs | 216 +++++++++++++++ crates/bevy_color/src/hwba.rs | 249 ++++++++++++++++++ crates/bevy_color/src/lib.rs | 13 + crates/bevy_color/src/test_colors.rs | 40 ++- crates/bevy_render/src/color/mod.rs | 28 +- 8 files changed, 678 insertions(+), 65 deletions(-) create mode 100644 crates/bevy_color/src/hsva.rs create mode 100644 crates/bevy_color/src/hwba.rs diff --git a/crates/bevy_color/crates/gen_tests/src/main.rs b/crates/bevy_color/crates/gen_tests/src/main.rs index b4d765f4a1..bd26e6921c 100644 --- a/crates/bevy_color/crates/gen_tests/src/main.rs +++ b/crates/bevy_color/crates/gen_tests/src/main.rs @@ -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()), diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 22f98fd502..8cde1b2282 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -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 for Color { } } +impl From for Color { + fn from(value: Hsva) -> Self { + Self::Hsva(value) + } +} + +impl From for Color { + fn from(value: Hwba) -> Self { + Self::Hwba(value) + } +} + impl From for Color { fn from(value: Oklaba) -> Self { Self::Oklaba(value) @@ -108,6 +128,8 @@ impl From 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 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 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 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 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 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 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 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, diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index daede82879..462562a2a3 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -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 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 for Srgba { +impl From for Hsva { fn from( Hsla { hue, @@ -170,48 +137,78 @@ impl From 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 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 for Hsla { + fn from(value: Hwba) -> Self { + Hsva::from(value).into() + } +} + +impl From for Hsla { + fn from(value: Srgba) -> Self { + Hsva::from(value).into() + } +} + +impl From for Srgba { + fn from(value: Hsla) -> Self { + Hsva::from(value).into() + } +} + +impl From for Hwba { + fn from(value: Hsla) -> Self { + Hsva::from(value).into() } } impl From for Hsla { fn from(value: LinearRgba) -> Self { - Srgba::from(value).into() + Hsva::from(value).into() } } impl From for Hsla { fn from(value: Oklaba) -> Self { - Srgba::from(value).into() + Hsva::from(value).into() } } impl From for Hsla { fn from(value: Lcha) -> Self { - Srgba::from(value).into() + Hsva::from(value).into() } } diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs new file mode 100644 index 0000000000..30f204ed1d --- /dev/null +++ b/crates/bevy_color/src/hsva.rs @@ -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 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 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 for Hsva { + fn from(value: Srgba) -> Self { + Hwba::from(value).into() + } +} + +impl From for Srgba { + fn from(value: Hsva) -> Self { + Hwba::from(value).into() + } +} + +impl From for Hsva { + fn from(value: LinearRgba) -> Self { + Hwba::from(value).into() + } +} + +impl From for LinearRgba { + fn from(value: Hsva) -> Self { + Hwba::from(value).into() + } +} + +impl From for Hsva { + fn from(value: Lcha) -> Self { + Hwba::from(value).into() + } +} + +impl From for Lcha { + fn from(value: Hsva) -> Self { + Hwba::from(value).into() + } +} + +impl From for Hsva { + fn from(value: Oklaba) -> Self { + Hwba::from(value).into() + } +} + +impl From for Oklaba { + fn from(value: Hsva) -> Self { + Hwba::from(value).into() + } +} + +impl From for Hsva { + fn from(value: Xyza) -> Self { + Hwba::from(value).into() + } +} + +impl From 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); + } + } +} diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs new file mode 100644 index 0000000000..b6c7003a2a --- /dev/null +++ b/crates/bevy_color/src/hwba.rs @@ -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 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 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 for Hwba { + fn from(value: LinearRgba) -> Self { + Srgba::from(value).into() + } +} + +impl From for LinearRgba { + fn from(value: Hwba) -> Self { + Srgba::from(value).into() + } +} + +impl From for Hwba { + fn from(value: Lcha) -> Self { + Srgba::from(value).into() + } +} + +impl From for Lcha { + fn from(value: Hwba) -> Self { + Srgba::from(value).into() + } +} + +impl From for Hwba { + fn from(value: Oklaba) -> Self { + Srgba::from(value).into() + } +} + +impl From for Oklaba { + fn from(value: Hwba) -> Self { + Srgba::from(value).into() + } +} + +impl From for Hwba { + fn from(value: Xyza) -> Self { + Srgba::from(value).into() + } +} + +impl From 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); + } + } +} diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 857bb2a02d..3bdce2df24 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -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 + Into, Self: From + Into, Self: From + Into, + Self: From + Into, + Self: From + Into, Self: From + Into, Self: From + Into, Self: From + Into, diff --git a/crates/bevy_color/src/test_colors.rs b/crates/bevy_color/src/test_colors.rs index 52e4deb993..d1b1372241 100644 --- a/crates/bevy_color/src/test_colors.rs +++ b/crates/bevy_color/src/test_colors.rs @@ -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), }, ]; diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index 2a5ddd9325..a68204d765 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -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 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 for Hsla { } } +impl From for Hsva { + fn from(value: LegacyColor) -> Self { + Hsla::from(value).into() + } +} + +impl From for LegacyColor { + fn from(value: Hsva) -> Self { + Hsla::from(value).into() + } +} + +impl From for Hwba { + fn from(value: LegacyColor) -> Self { + Hsla::from(value).into() + } +} + +impl From for LegacyColor { + fn from(value: Hwba) -> Self { + Hsla::from(value).into() + } +} + impl From for LegacyColor { fn from( Lcha {