diff --git a/crates/bevy_color/crates/gen_tests/src/main.rs b/crates/bevy_color/crates/gen_tests/src/main.rs index b4d765f4a1801..bd26e6921c844 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 22f98fd5025d9..8cde1b22820c1 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 daede8287903d..462562a2a3593 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,91 +128,87 @@ impl Luminance for Hsla { } } -impl From for Hsla { +impl From for Hsva { fn from( - Srgba { - red, - green, - blue, + Hsla { + hue, + saturation, + lightness, alpha, - }: Srgba, + }: Hsla, ) -> 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) + // 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 { - 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) + 2. * (1. - (lightness / value)) }; - Self::new(hue, saturation, lightness, alpha) + Hsva::new(hue, saturation, value, alpha) } } -impl From for Srgba { +impl From for Hsla { fn from( - Hsla { + Hsva { hue, saturation, - lightness, + value, alpha, - }: Hsla, + }: Hsva, ) -> 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#HSV_to_HSL + let lightness = value * (1. - saturation / 2.); + let saturation = if lightness == 0. || lightness == 1. { + 0. } else { - (chroma, 0.0, largest_component) + (value - lightness) / lightness.min(1. - lightness) }; - 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; + 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() + } +} - Self::new(red, green, blue, alpha) +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 0000000000000..30f204ed1d7ab --- /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 0000000000000..b6c7003a2a345 --- /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 857bb2a02df85..3bdce2df24f27 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 52e4deb9931af..d1b1372241519 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 2a5ddd932518d..a68204d765788 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 {