Skip to content

Commit f939c09

Browse files
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 <[email protected]>
1 parent 8ec6552 commit f939c09

File tree

8 files changed

+672
-59
lines changed

8 files changed

+672
-59
lines changed

crates/bevy_color/crates/gen_tests/src/main.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use palette::{Hsl, IntoColor, Lch, LinSrgb, Oklab, Srgb, Xyz};
1+
use palette::{Hsl, Hsv, Hwb, IntoColor, Lch, LinSrgb, Oklab, Srgb, Xyz};
22

33
const TEST_COLORS: &[(f32, f32, f32, &str)] = &[
44
(0., 0., 0., "black"),
@@ -25,14 +25,16 @@ fn main() {
2525
println!(
2626
"// Generated by gen_tests. Do not edit.
2727
#[cfg(test)]
28-
use crate::{{Hsla, Srgba, LinearRgba, Oklaba, Lcha, Xyza}};
28+
use crate::{{Hsla, Hsva, Hwba, Srgba, LinearRgba, Oklaba, Lcha, Xyza}};
2929
3030
#[cfg(test)]
3131
pub struct TestColor {{
3232
pub name: &'static str,
3333
pub rgb: Srgba,
3434
pub linear_rgb: LinearRgba,
3535
pub hsl: Hsla,
36+
pub hsv: Hsva,
37+
pub hwb: Hwba,
3638
pub lch: Lcha,
3739
pub oklab: Oklaba,
3840
pub xyz: Xyza,
@@ -47,6 +49,8 @@ pub struct TestColor {{
4749
let srgb = Srgb::new(*r, *g, *b);
4850
let linear_rgb: LinSrgb = srgb.into_color();
4951
let hsl: Hsl = srgb.into_color();
52+
let hsv: Hsv = srgb.into_color();
53+
let hwb: Hwb = srgb.into_color();
5054
let lch: Lch = srgb.into_color();
5155
let oklab: Oklab = srgb.into_color();
5256
let xyz: Xyz = srgb.into_color();
@@ -57,6 +61,8 @@ pub struct TestColor {{
5761
rgb: Srgba::new({}, {}, {}, 1.0),
5862
linear_rgb: LinearRgba::new({}, {}, {}, 1.0),
5963
hsl: Hsla::new({}, {}, {}, 1.0),
64+
hsv: Hsva::new({}, {}, {}, 1.0),
65+
hwb: Hwba::new({}, {}, {}, 1.0),
6066
lch: Lcha::new({}, {}, {}, 1.0),
6167
oklab: Oklaba::new({}, {}, {}, 1.0),
6268
xyz: Xyza::new({}, {}, {}, 1.0),
@@ -70,6 +76,12 @@ pub struct TestColor {{
7076
VariablePrecision(hsl.hue.into_positive_degrees()),
7177
VariablePrecision(hsl.saturation),
7278
VariablePrecision(hsl.lightness),
79+
VariablePrecision(hsv.hue.into_positive_degrees()),
80+
VariablePrecision(hsv.saturation),
81+
VariablePrecision(hsv.value),
82+
VariablePrecision(hwb.hue.into_positive_degrees()),
83+
VariablePrecision(hwb.whiteness),
84+
VariablePrecision(hwb.blackness),
7385
VariablePrecision(lch.l / 100.0),
7486
VariablePrecision(lch.chroma / 100.0),
7587
VariablePrecision(lch.hue.into_positive_degrees()),

crates/bevy_color/src/color.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Alpha, Hsla, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
1+
use crate::{Alpha, Hsla, Hsva, Hwba, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
22
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
33
use serde::{Deserialize, Serialize};
44

@@ -15,6 +15,10 @@ pub enum Color {
1515
LinearRgba(LinearRgba),
1616
/// A color in the HSL color space with alpha.
1717
Hsla(Hsla),
18+
/// A color in the HSV color space with alpha.
19+
Hsva(Hsva),
20+
/// A color in the HWB color space with alpha.
21+
Hwba(Hwba),
1822
/// A color in the LCH color space with alpha.
1923
Lcha(Lcha),
2024
/// A color in the Oklaba color space with alpha.
@@ -46,6 +50,8 @@ impl Alpha for Color {
4650
Color::Srgba(x) => *x = x.with_alpha(alpha),
4751
Color::LinearRgba(x) => *x = x.with_alpha(alpha),
4852
Color::Hsla(x) => *x = x.with_alpha(alpha),
53+
Color::Hsva(x) => *x = x.with_alpha(alpha),
54+
Color::Hwba(x) => *x = x.with_alpha(alpha),
4955
Color::Lcha(x) => *x = x.with_alpha(alpha),
5056
Color::Oklaba(x) => *x = x.with_alpha(alpha),
5157
Color::Xyza(x) => *x = x.with_alpha(alpha),
@@ -59,6 +65,8 @@ impl Alpha for Color {
5965
Color::Srgba(x) => x.alpha(),
6066
Color::LinearRgba(x) => x.alpha(),
6167
Color::Hsla(x) => x.alpha(),
68+
Color::Hsva(x) => x.alpha(),
69+
Color::Hwba(x) => x.alpha(),
6270
Color::Lcha(x) => x.alpha(),
6371
Color::Oklaba(x) => x.alpha(),
6472
Color::Xyza(x) => x.alpha(),
@@ -84,6 +92,18 @@ impl From<Hsla> for Color {
8492
}
8593
}
8694

95+
impl From<Hsva> for Color {
96+
fn from(value: Hsva) -> Self {
97+
Self::Hsva(value)
98+
}
99+
}
100+
101+
impl From<Hwba> for Color {
102+
fn from(value: Hwba) -> Self {
103+
Self::Hwba(value)
104+
}
105+
}
106+
87107
impl From<Oklaba> for Color {
88108
fn from(value: Oklaba) -> Self {
89109
Self::Oklaba(value)
@@ -108,6 +128,8 @@ impl From<Color> for Srgba {
108128
Color::Srgba(srgba) => srgba,
109129
Color::LinearRgba(linear) => linear.into(),
110130
Color::Hsla(hsla) => hsla.into(),
131+
Color::Hsva(hsva) => hsva.into(),
132+
Color::Hwba(hwba) => hwba.into(),
111133
Color::Lcha(lcha) => lcha.into(),
112134
Color::Oklaba(oklab) => oklab.into(),
113135
Color::Xyza(xyza) => xyza.into(),
@@ -121,6 +143,8 @@ impl From<Color> for LinearRgba {
121143
Color::Srgba(srgba) => srgba.into(),
122144
Color::LinearRgba(linear) => linear,
123145
Color::Hsla(hsla) => hsla.into(),
146+
Color::Hsva(hsva) => hsva.into(),
147+
Color::Hwba(hwba) => hwba.into(),
124148
Color::Lcha(lcha) => lcha.into(),
125149
Color::Oklaba(oklab) => oklab.into(),
126150
Color::Xyza(xyza) => xyza.into(),
@@ -134,6 +158,38 @@ impl From<Color> for Hsla {
134158
Color::Srgba(srgba) => srgba.into(),
135159
Color::LinearRgba(linear) => linear.into(),
136160
Color::Hsla(hsla) => hsla,
161+
Color::Hsva(hsva) => hsva.into(),
162+
Color::Hwba(hwba) => hwba.into(),
163+
Color::Lcha(lcha) => lcha.into(),
164+
Color::Oklaba(oklab) => oklab.into(),
165+
Color::Xyza(xyza) => xyza.into(),
166+
}
167+
}
168+
}
169+
170+
impl From<Color> for Hsva {
171+
fn from(value: Color) -> Self {
172+
match value {
173+
Color::Srgba(srgba) => srgba.into(),
174+
Color::LinearRgba(linear) => linear.into(),
175+
Color::Hsla(hsla) => hsla.into(),
176+
Color::Hsva(hsva) => hsva,
177+
Color::Hwba(hwba) => hwba.into(),
178+
Color::Lcha(lcha) => lcha.into(),
179+
Color::Oklaba(oklab) => oklab.into(),
180+
Color::Xyza(xyza) => xyza.into(),
181+
}
182+
}
183+
}
184+
185+
impl From<Color> for Hwba {
186+
fn from(value: Color) -> Self {
187+
match value {
188+
Color::Srgba(srgba) => srgba.into(),
189+
Color::LinearRgba(linear) => linear.into(),
190+
Color::Hsla(hsla) => hsla.into(),
191+
Color::Hsva(hsva) => hsva.into(),
192+
Color::Hwba(hwba) => hwba,
137193
Color::Lcha(lcha) => lcha.into(),
138194
Color::Oklaba(oklab) => oklab.into(),
139195
Color::Xyza(xyza) => xyza.into(),
@@ -147,6 +203,8 @@ impl From<Color> for Lcha {
147203
Color::Srgba(srgba) => srgba.into(),
148204
Color::LinearRgba(linear) => linear.into(),
149205
Color::Hsla(hsla) => hsla.into(),
206+
Color::Hsva(hsva) => hsva.into(),
207+
Color::Hwba(hwba) => hwba.into(),
150208
Color::Lcha(lcha) => lcha,
151209
Color::Oklaba(oklab) => oklab.into(),
152210
Color::Xyza(xyza) => xyza.into(),
@@ -160,6 +218,8 @@ impl From<Color> for Oklaba {
160218
Color::Srgba(srgba) => srgba.into(),
161219
Color::LinearRgba(linear) => linear.into(),
162220
Color::Hsla(hsla) => hsla.into(),
221+
Color::Hsva(hsva) => hsva.into(),
222+
Color::Hwba(hwba) => hwba.into(),
163223
Color::Lcha(lcha) => lcha.into(),
164224
Color::Oklaba(oklab) => oklab,
165225
Color::Xyza(xyza) => xyza.into(),
@@ -173,6 +233,8 @@ impl From<Color> for Xyza {
173233
Color::Srgba(x) => x.into(),
174234
Color::LinearRgba(x) => x.into(),
175235
Color::Hsla(x) => x.into(),
236+
Color::Hsva(hsva) => hsva.into(),
237+
Color::Hwba(hwba) => hwba.into(),
176238
Color::Lcha(x) => x.into(),
177239
Color::Oklaba(x) => x.into(),
178240
Color::Xyza(xyza) => xyza,

crates/bevy_color/src/hsla.rs

Lines changed: 51 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use crate::{Alpha, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
1+
use crate::{Alpha, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
22
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
33
use serde::{Deserialize, Serialize};
44

5-
/// Color in Hue-Saturation-Lightness color space with alpha
5+
/// Color in Hue-Saturation-Lightness (HSL) color space with alpha.
6+
/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV).
67
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
78
#[reflect(PartialEq, Serialize, Deserialize)]
89
pub struct Hsla {
@@ -127,91 +128,87 @@ impl Luminance for Hsla {
127128
}
128129
}
129130

130-
impl From<Srgba> for Hsla {
131+
impl From<Hsla> for Hsva {
131132
fn from(
132-
Srgba {
133-
red,
134-
green,
135-
blue,
133+
Hsla {
134+
hue,
135+
saturation,
136+
lightness,
136137
alpha,
137-
}: Srgba,
138+
}: Hsla,
138139
) -> Self {
139-
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
140-
let x_max = red.max(green.max(blue));
141-
let x_min = red.min(green.min(blue));
142-
let chroma = x_max - x_min;
143-
let lightness = (x_max + x_min) / 2.0;
144-
let hue = if chroma == 0.0 {
145-
0.0
146-
} else if red == x_max {
147-
60.0 * (green - blue) / chroma
148-
} else if green == x_max {
149-
60.0 * (2.0 + (blue - red) / chroma)
140+
// Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
141+
let value = lightness + saturation * lightness.min(1. - lightness);
142+
let saturation = if value == 0. {
143+
0.
150144
} else {
151-
60.0 * (4.0 + (red - green) / chroma)
152-
};
153-
let hue = if hue < 0.0 { 360.0 + hue } else { hue };
154-
let saturation = if lightness <= 0.0 || lightness >= 1.0 {
155-
0.0
156-
} else {
157-
(x_max - lightness) / lightness.min(1.0 - lightness)
145+
2. * (1. - (lightness / value))
158146
};
159147

160-
Self::new(hue, saturation, lightness, alpha)
148+
Hsva::new(hue, saturation, value, alpha)
161149
}
162150
}
163151

164-
impl From<Hsla> for Srgba {
152+
impl From<Hsva> for Hsla {
165153
fn from(
166-
Hsla {
154+
Hsva {
167155
hue,
168156
saturation,
169-
lightness,
157+
value,
170158
alpha,
171-
}: Hsla,
159+
}: Hsva,
172160
) -> Self {
173-
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
174-
let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
175-
let hue_prime = hue / 60.0;
176-
let largest_component = chroma * (1.0 - (hue_prime % 2.0 - 1.0).abs());
177-
let (r_temp, g_temp, b_temp) = if hue_prime < 1.0 {
178-
(chroma, largest_component, 0.0)
179-
} else if hue_prime < 2.0 {
180-
(largest_component, chroma, 0.0)
181-
} else if hue_prime < 3.0 {
182-
(0.0, chroma, largest_component)
183-
} else if hue_prime < 4.0 {
184-
(0.0, largest_component, chroma)
185-
} else if hue_prime < 5.0 {
186-
(largest_component, 0.0, chroma)
161+
// Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
162+
let lightness = value * (1. - saturation / 2.);
163+
let saturation = if lightness == 0. || lightness == 1. {
164+
0.
187165
} else {
188-
(chroma, 0.0, largest_component)
166+
(value - lightness) / lightness.min(1. - lightness)
189167
};
190-
let lightness_match = lightness - chroma / 2.0;
191168

192-
let red = r_temp + lightness_match;
193-
let green = g_temp + lightness_match;
194-
let blue = b_temp + lightness_match;
169+
Hsla::new(hue, saturation, lightness, alpha)
170+
}
171+
}
172+
173+
impl From<Hwba> for Hsla {
174+
fn from(value: Hwba) -> Self {
175+
Hsva::from(value).into()
176+
}
177+
}
178+
179+
impl From<Srgba> for Hsla {
180+
fn from(value: Srgba) -> Self {
181+
Hsva::from(value).into()
182+
}
183+
}
184+
185+
impl From<Hsla> for Srgba {
186+
fn from(value: Hsla) -> Self {
187+
Hsva::from(value).into()
188+
}
189+
}
195190

196-
Self::new(red, green, blue, alpha)
191+
impl From<Hsla> for Hwba {
192+
fn from(value: Hsla) -> Self {
193+
Hsva::from(value).into()
197194
}
198195
}
199196

200197
impl From<LinearRgba> for Hsla {
201198
fn from(value: LinearRgba) -> Self {
202-
Srgba::from(value).into()
199+
Hsva::from(value).into()
203200
}
204201
}
205202

206203
impl From<Oklaba> for Hsla {
207204
fn from(value: Oklaba) -> Self {
208-
Srgba::from(value).into()
205+
Hsva::from(value).into()
209206
}
210207
}
211208

212209
impl From<Lcha> for Hsla {
213210
fn from(value: Lcha) -> Self {
214-
Srgba::from(value).into()
211+
Hsva::from(value).into()
215212
}
216213
}
217214

0 commit comments

Comments
 (0)