Skip to content

Commit

Permalink
bevy_color: Added Hsva and Hwba Models (bevyengine#12114)
Browse files Browse the repository at this point in the history
# 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 <[email protected]>
  • Loading branch information
bushrat011899 and alice-i-cecile authored Feb 26, 2024
1 parent 8ec6552 commit f939c09
Show file tree
Hide file tree
Showing 8 changed files with 672 additions and 59 deletions.
16 changes: 14 additions & 2 deletions crates/bevy_color/crates/gen_tests/src/main.rs
Original file line number Diff line number Diff line change
@@ -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"),
Expand All @@ -25,14 +25,16 @@ 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 {{
pub name: &'static str,
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,
Expand All @@ -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();
Expand All @@ -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),
Expand All @@ -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()),
Expand Down
64 changes: 63 additions & 1 deletion crates/bevy_color/src/color.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand All @@ -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(),
Expand All @@ -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)
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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,
Expand Down
105 changes: 51 additions & 54 deletions crates/bevy_color/src/hsla.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -127,91 +128,87 @@ impl Luminance for Hsla {
}
}

impl From<Srgba> for Hsla {
impl From<Hsla> 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<Hsla> for Srgba {
impl From<Hsva> 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<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()
}
}

Self::new(red, green, blue, alpha)
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()
}
}

Expand Down
Loading

0 comments on commit f939c09

Please sign in to comment.