color spaces and representation (#1572)
`Color` can now be from different color spaces or representation: - sRGB - linear RGB - HSL This fixes #1193 by allowing the creation of const colors of all types, and writing it to the linear RGB color space for rendering. I went with an enum after trying with two different types (`Color` and `LinearColor`) to be able to use the different variants in all place where a `Color` is expected. I also added the HLS representation because: - I like it - it's useful for some case, see example `contributors`: I can just change the saturation and lightness while keeping the hue of the color - I think adding another variant not using `red`, `green`, `blue` makes it clearer there are differences Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
ab0165d20d
commit
bcd5318247
File diff suppressed because it is too large
Load Diff
@ -29,19 +29,173 @@ impl SrgbColorSpace for f32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_srgb_full_roundtrip() {
|
||||
let u8max: f32 = u8::max_value() as f32;
|
||||
for color in 0..u8::max_value() {
|
||||
let color01 = color as f32 / u8max;
|
||||
let color_roundtrip = color01
|
||||
.linear_to_nonlinear_srgb()
|
||||
.nonlinear_to_linear_srgb();
|
||||
// roundtrip is not perfect due to numeric precision, even with f64
|
||||
// so ensure the error is at least ready for u8 (where sRGB is used)
|
||||
assert_eq!(
|
||||
(color01 * u8max).round() as u8,
|
||||
(color_roundtrip * u8max).round() as u8
|
||||
);
|
||||
pub struct HslRepresentation;
|
||||
impl HslRepresentation {
|
||||
/// converts a color in HLS space to sRGB space
|
||||
pub fn hsl_to_nonlinear_srgb(hue: f32, saturation: f32, lightness: f32) -> [f32; 3] {
|
||||
// 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)
|
||||
} else {
|
||||
(chroma, 0.0, largest_component)
|
||||
};
|
||||
let lightness_match = lightness - chroma / 2.0;
|
||||
|
||||
[
|
||||
r_temp + lightness_match,
|
||||
g_temp + lightness_match,
|
||||
b_temp + lightness_match,
|
||||
]
|
||||
}
|
||||
|
||||
/// converts a color in sRGB space to HLS space
|
||||
pub fn nonlinear_srgb_to_hsl([red, green, blue]: [f32; 3]) -> (f32, f32, f32) {
|
||||
// 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 > green && red > blue {
|
||||
60.0 * (green - blue) / chroma
|
||||
} else if green > red && green > blue {
|
||||
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)
|
||||
};
|
||||
|
||||
(hue, saturation, lightness)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn srgb_linear_full_roundtrip() {
|
||||
let u8max: f32 = u8::max_value() as f32;
|
||||
for color in 0..u8::max_value() {
|
||||
let color01 = color as f32 / u8max;
|
||||
let color_roundtrip = color01
|
||||
.linear_to_nonlinear_srgb()
|
||||
.nonlinear_to_linear_srgb();
|
||||
// roundtrip is not perfect due to numeric precision, even with f64
|
||||
// so ensure the error is at least ready for u8 (where sRGB is used)
|
||||
assert_eq!(
|
||||
(color01 * u8max).round() as u8,
|
||||
(color_roundtrip * u8max).round() as u8
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hsl_to_srgb() {
|
||||
// "truth" from https://en.wikipedia.org/wiki/HSL_and_HSV#Examples
|
||||
|
||||
// black
|
||||
let (hue, saturation, lightness) = (0.0, 0.0, 0.0);
|
||||
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
|
||||
assert_eq!((r * 100.0).round() as u32, 0);
|
||||
assert_eq!((g * 100.0).round() as u32, 0);
|
||||
assert_eq!((b * 100.0).round() as u32, 0);
|
||||
|
||||
// white
|
||||
let (hue, saturation, lightness) = (0.0, 0.0, 1.0);
|
||||
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
|
||||
assert_eq!((r * 100.0).round() as u32, 100);
|
||||
assert_eq!((g * 100.0).round() as u32, 100);
|
||||
assert_eq!((b * 100.0).round() as u32, 100);
|
||||
|
||||
let (hue, saturation, lightness) = (300.0, 0.5, 0.5);
|
||||
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
|
||||
assert_eq!((r * 100.0).round() as u32, 75);
|
||||
assert_eq!((g * 100.0).round() as u32, 25);
|
||||
assert_eq!((b * 100.0).round() as u32, 75);
|
||||
|
||||
// a red
|
||||
let (hue, saturation, lightness) = (283.7, 0.775, 0.543);
|
||||
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
|
||||
assert_eq!((r * 100.0).round() as u32, 70);
|
||||
assert_eq!((g * 100.0).round() as u32, 19);
|
||||
assert_eq!((b * 100.0).round() as u32, 90);
|
||||
|
||||
// a green
|
||||
let (hue, saturation, lightness) = (162.4, 0.779, 0.447);
|
||||
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
|
||||
assert_eq!((r * 100.0).round() as u32, 10);
|
||||
assert_eq!((g * 100.0).round() as u32, 80);
|
||||
assert_eq!((b * 100.0).round() as u32, 59);
|
||||
|
||||
// a blue
|
||||
let (hue, saturation, lightness) = (251.1, 0.832, 0.511);
|
||||
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
|
||||
assert_eq!((r * 100.0).round() as u32, 25);
|
||||
assert_eq!((g * 100.0).round() as u32, 10);
|
||||
assert_eq!((b * 100.0).round() as u32, 92);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn srgb_to_hsl() {
|
||||
// "truth" from https://en.wikipedia.org/wiki/HSL_and_HSV#Examples
|
||||
|
||||
// black
|
||||
let (hue, saturation, lightness) =
|
||||
HslRepresentation::nonlinear_srgb_to_hsl([0.0, 0.0, 0.0]);
|
||||
assert_eq!(hue.round() as u32, 0);
|
||||
assert_eq!((saturation * 100.0).round() as u32, 0);
|
||||
assert_eq!((lightness * 100.0).round() as u32, 0);
|
||||
|
||||
// white
|
||||
let (hue, saturation, lightness) =
|
||||
HslRepresentation::nonlinear_srgb_to_hsl([1.0, 1.0, 1.0]);
|
||||
assert_eq!(hue.round() as u32, 0);
|
||||
assert_eq!((saturation * 100.0).round() as u32, 0);
|
||||
assert_eq!((lightness * 100.0).round() as u32, 100);
|
||||
|
||||
let (hue, saturation, lightness) =
|
||||
HslRepresentation::nonlinear_srgb_to_hsl([0.75, 0.25, 0.75]);
|
||||
assert_eq!(hue.round() as u32, 300);
|
||||
assert_eq!((saturation * 100.0).round() as u32, 50);
|
||||
assert_eq!((lightness * 100.0).round() as u32, 50);
|
||||
|
||||
// a red
|
||||
let (hue, saturation, lightness) =
|
||||
HslRepresentation::nonlinear_srgb_to_hsl([0.704, 0.187, 0.897]);
|
||||
assert_eq!(hue.round() as u32, 284);
|
||||
assert_eq!((saturation * 100.0).round() as u32, 78);
|
||||
assert_eq!((lightness * 100.0).round() as u32, 54);
|
||||
|
||||
// a green
|
||||
let (hue, saturation, lightness) =
|
||||
HslRepresentation::nonlinear_srgb_to_hsl([0.099, 0.795, 0.591]);
|
||||
assert_eq!(hue.round() as u32, 162);
|
||||
assert_eq!((saturation * 100.0).round() as u32, 78);
|
||||
assert_eq!((lightness * 100.0).round() as u32, 45);
|
||||
|
||||
// a blue
|
||||
let (hue, saturation, lightness) =
|
||||
HslRepresentation::nonlinear_srgb_to_hsl([0.255, 0.104, 0.918]);
|
||||
assert_eq!(hue.round() as u32, 251);
|
||||
assert_eq!((saturation * 100.0).round() as u32, 83);
|
||||
assert_eq!((lightness * 100.0).round() as u32, 51);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
color::Color,
|
||||
texture::{Extent3d, Texture, TextureDimension, TextureFormat},
|
||||
};
|
||||
use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat};
|
||||
|
||||
#[derive(Debug, TypeUuid)]
|
||||
#[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"]
|
||||
@ -28,25 +25,12 @@ impl Font {
|
||||
});
|
||||
|
||||
// TODO: make this texture grayscale
|
||||
let color = Color::WHITE;
|
||||
let color_u8 = [
|
||||
(color.r() * 255.0) as u8,
|
||||
(color.g() * 255.0) as u8,
|
||||
(color.b() * 255.0) as u8,
|
||||
];
|
||||
Texture::new(
|
||||
Extent3d::new(width as u32, height as u32, 1),
|
||||
TextureDimension::D2,
|
||||
alpha
|
||||
.iter()
|
||||
.map(|a| {
|
||||
vec![
|
||||
color_u8[0],
|
||||
color_u8[1],
|
||||
color_u8[2],
|
||||
(color.a() * a * 255.0) as u8,
|
||||
]
|
||||
})
|
||||
.map(|a| vec![255, 255, 255, (*a * 255.0) as u8])
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>(),
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
|
@ -127,11 +127,12 @@ impl<'a> From<&'a OwnedWgpuVertexBufferLayout> for wgpu::VertexBufferLayout<'a>
|
||||
|
||||
impl WgpuFrom<Color> for wgpu::Color {
|
||||
fn from(color: Color) -> Self {
|
||||
let linear = color.as_linear_rgba_f32();
|
||||
wgpu::Color {
|
||||
r: color.r_linear() as f64,
|
||||
g: color.g_linear() as f64,
|
||||
b: color.b_linear() as f64,
|
||||
a: color.a() as f64,
|
||||
r: linear[0] as f64,
|
||||
g: linear[1] as f64,
|
||||
b: linear[2] as f64,
|
||||
a: linear[3] as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ struct SelectTimer;
|
||||
struct ContributorDisplay;
|
||||
|
||||
struct Contributor {
|
||||
color: [f32; 3],
|
||||
hue: f32,
|
||||
}
|
||||
|
||||
struct Velocity {
|
||||
@ -40,8 +40,11 @@ struct Velocity {
|
||||
const GRAVITY: f32 = -9.821 * 100.0;
|
||||
const SPRITE_SIZE: f32 = 75.0;
|
||||
|
||||
const COL_DESELECTED: Color = Color::rgba_linear(0.03, 0.03, 0.03, 0.92);
|
||||
const COL_SELECTED: Color = Color::WHITE;
|
||||
const SATURATION_DESELECTED: f32 = 0.3;
|
||||
const LIGHTNESS_DESELECTED: f32 = 0.2;
|
||||
const SATURATION_SELECTED: f32 = 0.9;
|
||||
const LIGHTNESS_SELECTED: f32 = 0.7;
|
||||
const ALPHA: f32 = 0.92;
|
||||
|
||||
const SHOWCASE_TIMER_SECS: f32 = 3.0;
|
||||
|
||||
@ -69,7 +72,7 @@ fn setup(
|
||||
let pos = (rnd.gen_range(-400.0..400.0), rnd.gen_range(0.0..400.0));
|
||||
let dir = rnd.gen_range(-1.0..1.0);
|
||||
let velocity = Vec3::new(dir * 500.0, 0.0, 0.0);
|
||||
let col = gen_color(&mut rnd);
|
||||
let hue = rnd.gen_range(0.0..=360.0);
|
||||
|
||||
// some sprites should be flipped
|
||||
let flipped = rnd.gen_bool(0.5);
|
||||
@ -77,7 +80,7 @@ fn setup(
|
||||
let transform = Transform::from_xyz(pos.0, pos.1, 0.0);
|
||||
|
||||
commands
|
||||
.spawn((Contributor { color: col },))
|
||||
.spawn((Contributor { hue },))
|
||||
.with(Velocity {
|
||||
translation: velocity,
|
||||
rotation: -dir * 5.0,
|
||||
@ -90,12 +93,12 @@ fn setup(
|
||||
..Default::default()
|
||||
},
|
||||
material: materials.add(ColorMaterial {
|
||||
color: COL_DESELECTED * col,
|
||||
color: Color::hsla(hue, SATURATION_DESELECTED, LIGHTNESS_DESELECTED, ALPHA),
|
||||
texture: Some(texture_handle.clone()),
|
||||
}),
|
||||
transform,
|
||||
..Default::default()
|
||||
})
|
||||
.with(transform);
|
||||
});
|
||||
|
||||
let e = commands.current_entity().unwrap();
|
||||
|
||||
@ -180,15 +183,8 @@ fn select_system(
|
||||
let (name, e) = &sel.order[sel.idx];
|
||||
|
||||
if let Ok((c, handle, mut tr)) = q.get_mut(*e) {
|
||||
for mut text in dq.iter_mut() {
|
||||
select(
|
||||
&mut *materials,
|
||||
handle.clone(),
|
||||
c,
|
||||
&mut *tr,
|
||||
&mut *text,
|
||||
name,
|
||||
);
|
||||
if let Some(mut text) = dq.iter_mut().next() {
|
||||
select(&mut *materials, handle, c, &mut *tr, &mut *text, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,14 +193,14 @@ fn select_system(
|
||||
/// bring the object to the front and display the name.
|
||||
fn select(
|
||||
materials: &mut Assets<ColorMaterial>,
|
||||
mat_handle: Handle<ColorMaterial>,
|
||||
mat_handle: &Handle<ColorMaterial>,
|
||||
cont: &Contributor,
|
||||
trans: &mut Transform,
|
||||
text: &mut Text,
|
||||
name: &str,
|
||||
) -> Option<()> {
|
||||
let mat = materials.get_mut(mat_handle)?;
|
||||
mat.color = COL_SELECTED * cont.color;
|
||||
mat.color = Color::hsla(cont.hue, SATURATION_SELECTED, LIGHTNESS_SELECTED, ALPHA);
|
||||
|
||||
trans.translation.z = 100.0;
|
||||
|
||||
@ -224,7 +220,7 @@ fn deselect(
|
||||
trans: &mut Transform,
|
||||
) -> Option<()> {
|
||||
let mat = materials.get_mut(mat_handle)?;
|
||||
mat.color = COL_DESELECTED * cont.color;
|
||||
mat.color = Color::hsla(cont.hue, SATURATION_DESELECTED, LIGHTNESS_DESELECTED, ALPHA);
|
||||
|
||||
trans.translation.z = 0.0;
|
||||
|
||||
@ -321,20 +317,3 @@ fn contributors() -> Contributors {
|
||||
.filter_map(|x| x.ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a color modulation
|
||||
///
|
||||
/// Because there is no `Mul<Color> for Color` instead `[f32; 3]` is
|
||||
/// used.
|
||||
fn gen_color(rng: &mut impl Rng) -> [f32; 3] {
|
||||
loop {
|
||||
let rgb = rng.gen();
|
||||
if luminance(rgb) >= 0.6 {
|
||||
break rgb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn luminance([r, g, b]: [f32; 3]) -> f32 {
|
||||
0.299 * r + 0.587 * g + 0.114 * b
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use bevy::{
|
||||
use rand::Rng;
|
||||
|
||||
const BIRDS_PER_SECOND: u32 = 1000;
|
||||
const BASE_COLOR: Color = Color::rgb_linear(5.0, 5.0, 5.0);
|
||||
const BASE_COLOR: Color = Color::rgb(5.0, 5.0, 5.0);
|
||||
const GRAVITY: f32 = -9.8 * 100.0;
|
||||
const MAX_VELOCITY: f32 = 750.;
|
||||
const BIRD_SCALE: f32 = 0.15;
|
||||
|
@ -106,11 +106,11 @@ fn text_color_system(time: Res<Time>, mut query: Query<&mut Text, With<ColorText
|
||||
let seconds = time.seconds_since_startup() as f32;
|
||||
// We used the `Text::with_section` helper method, but it is still just a `Text`,
|
||||
// so to update it, we are still updating the one and only section
|
||||
text.sections[0]
|
||||
.style
|
||||
.color
|
||||
.set_r((1.25 * seconds).sin() / 2.0 + 0.5)
|
||||
.set_g((0.75 * seconds).sin() / 2.0 + 0.5)
|
||||
.set_b((0.50 * seconds).sin() / 2.0 + 0.5);
|
||||
text.sections[0].style.color = Color::Rgba {
|
||||
red: (1.25 * seconds).sin() / 2.0 + 0.5,
|
||||
green: (0.75 * seconds).sin() / 2.0 + 0.5,
|
||||
blue: (0.50 * seconds).sin() / 2.0 + 0.5,
|
||||
alpha: 1.0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user