-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Upstreaming bevy_color. #12013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upstreaming bevy_color. #12013
Changes from all commits
a01f513
e9ed79c
6fb4d38
f7e058b
bc9c7da
b8fcf07
0b7bcf6
814aa39
8a4e7aa
8793198
5738695
8775270
e48e255
44e8146
1247d7e
f2156e0
320237b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "bevy_color" | ||
version = "0.13.0" | ||
edition = "2021" | ||
description = "Types for representing and manipulating color values" | ||
homepage = "https://bevyengine.org" | ||
repository = "https://github.com/bevyengine/bevy" | ||
license = "MIT OR Apache-2.0" | ||
keywords = ["bevy", "color"] | ||
|
||
[dependencies] | ||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } | ||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ | ||
"bevy", | ||
] } | ||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" } | ||
serde = "1.0" | ||
|
||
[lints] | ||
workspace = true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[package] | ||
name = "gen_tests" | ||
version = "0.1.0" | ||
edition = "2021" | ||
publish = false | ||
|
||
[workspace] | ||
|
||
[dependencies] | ||
palette = "0.7.4" | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# gen_tests for bevy_color | ||
|
||
The purpose of this crate is to generate test data for validating the color conversion | ||
functions. It is not part of the Bevy library and should only be run by developers | ||
working on Bevy. | ||
|
||
To generate the file: | ||
|
||
```sh | ||
cargo run > ../../src/test_colors.rs | ||
viridia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
use palette::{Hsl, IntoColor, Lch, LinSrgb, Oklab, Srgb}; | ||
|
||
const TEST_COLORS: &[(f32, f32, f32, &str)] = &[ | ||
(0., 0., 0., "black"), | ||
(1., 1., 1., "white"), | ||
(1., 0., 0., "red"), | ||
(0., 1., 0., "green"), | ||
(0., 0., 1., "blue"), | ||
(1., 1., 0., "yellow"), | ||
(1., 0., 1., "magenta"), | ||
(0., 1., 1., "cyan"), | ||
(0.5, 0.5, 0.5, "gray"), | ||
(0.5, 0.5, 0., "olive"), | ||
(0.5, 0., 0.5, "purple"), | ||
(0., 0.5, 0.5, "teal"), | ||
(0.5, 0., 0., "maroon"), | ||
(0., 0.5, 0., "lime"), | ||
(0., 0., 0.5, "navy"), | ||
(0.5, 0.5, 0., "orange"), | ||
(0.5, 0., 0.5, "fuchsia"), | ||
(0., 0.5, 0.5, "aqua"), | ||
]; | ||
|
||
fn main() { | ||
println!( | ||
"// Generated by gen_tests. Do not edit. | ||
#[cfg(test)] | ||
use crate::{{Hsla, Srgba, LinearRgba, Oklaba, Lcha}}; | ||
|
||
#[cfg(test)] | ||
pub struct TestColor {{ | ||
pub name: &'static str, | ||
pub rgb: Srgba, | ||
pub linear_rgb: LinearRgba, | ||
pub hsl: Hsla, | ||
pub lch: Lcha, | ||
pub oklab: Oklaba, | ||
}} | ||
" | ||
); | ||
|
||
println!("// Table of equivalent colors in various color spaces"); | ||
println!("#[cfg(test)]"); | ||
println!("pub const TEST_COLORS: &[TestColor] = &["); | ||
for (r, g, b, name) in TEST_COLORS { | ||
let srgb = Srgb::new(*r, *g, *b); | ||
let linear_rgb: LinSrgb = srgb.into_color(); | ||
let hsl: Hsl = srgb.into_color(); | ||
let lch: Lch = srgb.into_color(); | ||
let oklab: Oklab = srgb.into_color(); | ||
println!(" // {name}"); | ||
println!( | ||
" TestColor {{ | ||
name: \"{name}\", | ||
rgb: Srgba::new({}, {}, {}, 1.0), | ||
linear_rgb: LinearRgba::new({}, {}, {}, 1.0), | ||
hsl: Hsla::new({}, {}, {}, 1.0), | ||
lch: Lcha::new({}, {}, {}, 1.0), | ||
oklab: Oklaba::new({}, {}, {}, 1.0), | ||
}},", | ||
VariablePrecision(srgb.red), | ||
VariablePrecision(srgb.green), | ||
VariablePrecision(srgb.blue), | ||
VariablePrecision(linear_rgb.red), | ||
VariablePrecision(linear_rgb.green), | ||
VariablePrecision(linear_rgb.blue), | ||
VariablePrecision(hsl.hue.into_positive_degrees()), | ||
VariablePrecision(hsl.saturation), | ||
VariablePrecision(hsl.lightness), | ||
VariablePrecision(lch.l / 100.0), | ||
VariablePrecision(lch.chroma / 100.0), | ||
VariablePrecision(lch.hue.into_positive_degrees()), | ||
VariablePrecision(oklab.l), | ||
VariablePrecision(oklab.a), | ||
VariablePrecision(oklab.b), | ||
); | ||
} | ||
println!("];"); | ||
} | ||
|
||
struct VariablePrecision(f32); | ||
|
||
impl std::fmt::Display for VariablePrecision { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
if self.0.fract() == 0.0 { | ||
return write!(f, "{}.0", self.0); | ||
} | ||
write!(f, "{}", self.0) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
use crate::{Hsla, Lcha, LinearRgba, Oklaba, Srgba}; | ||
|
||
/// An enumerated type that can represent any of the color types in this crate. | ||
/// | ||
/// This is useful when you need to store a color in a data structure that can't be generic over | ||
/// the color type. | ||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
pub enum Color { | ||
/// A color in the sRGB color space with alpha. | ||
Srgba(Srgba), | ||
/// A color in the linear sRGB color space with alpha. | ||
LinearRgba(LinearRgba), | ||
/// A color in the HSL color space with alpha. | ||
Hsla(Hsla), | ||
/// A color in the LCH color space with alpha. | ||
Lcha(Lcha), | ||
/// A color in the Oklaba color space with alpha. | ||
Oklaba(Oklaba), | ||
} | ||
|
||
impl Color { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect if we swap our current Color type for this, that we will want to add a bunch of convenience constructors here. But that can wait! |
||
/// Return the color as a linear RGBA color. | ||
pub fn linear(&self) -> LinearRgba { | ||
match self { | ||
Color::Srgba(srgba) => (*srgba).into(), | ||
Color::LinearRgba(linear) => *linear, | ||
Color::Hsla(hsla) => (*hsla).into(), | ||
Color::Lcha(lcha) => (*lcha).into(), | ||
Color::Oklaba(oklab) => (*oklab).into(), | ||
} | ||
} | ||
} | ||
|
||
impl Default for Color { | ||
fn default() -> Self { | ||
Self::Srgba(Srgba::WHITE) | ||
} | ||
} | ||
|
||
impl From<Srgba> for Color { | ||
fn from(value: Srgba) -> Self { | ||
Self::Srgba(value) | ||
} | ||
} | ||
|
||
impl From<LinearRgba> for Color { | ||
fn from(value: LinearRgba) -> Self { | ||
Self::LinearRgba(value) | ||
} | ||
} | ||
|
||
impl From<Hsla> for Color { | ||
fn from(value: Hsla) -> Self { | ||
Self::Hsla(value) | ||
} | ||
} | ||
|
||
impl From<Oklaba> for Color { | ||
fn from(value: Oklaba) -> Self { | ||
Self::Oklaba(value) | ||
} | ||
} | ||
|
||
impl From<Lcha> for Color { | ||
fn from(value: Lcha) -> Self { | ||
Self::Lcha(value) | ||
} | ||
} | ||
|
||
impl From<Color> for Srgba { | ||
fn from(value: Color) -> Self { | ||
match value { | ||
Color::Srgba(srgba) => srgba, | ||
Color::LinearRgba(linear) => linear.into(), | ||
Color::Hsla(hsla) => hsla.into(), | ||
Color::Lcha(lcha) => lcha.into(), | ||
Color::Oklaba(oklab) => oklab.into(), | ||
} | ||
} | ||
} | ||
|
||
impl From<Color> for LinearRgba { | ||
fn from(value: Color) -> Self { | ||
match value { | ||
Color::Srgba(srgba) => srgba.into(), | ||
Color::LinearRgba(linear) => linear, | ||
Color::Hsla(hsla) => hsla.into(), | ||
Color::Lcha(lcha) => lcha.into(), | ||
Color::Oklaba(oklab) => oklab.into(), | ||
} | ||
} | ||
} | ||
|
||
impl From<Color> for Hsla { | ||
fn from(value: Color) -> Self { | ||
match value { | ||
Color::Srgba(srgba) => srgba.into(), | ||
Color::LinearRgba(linear) => linear.into(), | ||
Color::Hsla(hsla) => hsla, | ||
Color::Lcha(lcha) => LinearRgba::from(lcha).into(), | ||
Color::Oklaba(oklab) => LinearRgba::from(oklab).into(), | ||
} | ||
} | ||
} | ||
|
||
impl From<Color> for Lcha { | ||
fn from(value: Color) -> Self { | ||
match value { | ||
Color::Srgba(srgba) => srgba.into(), | ||
Color::LinearRgba(linear) => linear.into(), | ||
Color::Hsla(hsla) => Srgba::from(hsla).into(), | ||
Color::Lcha(lcha) => lcha, | ||
Color::Oklaba(oklab) => LinearRgba::from(oklab).into(), | ||
} | ||
} | ||
} | ||
|
||
impl From<Color> for Oklaba { | ||
fn from(value: Color) -> Self { | ||
match value { | ||
Color::Srgba(srgba) => srgba.into(), | ||
Color::LinearRgba(linear) => linear.into(), | ||
Color::Hsla(hsla) => Srgba::from(hsla).into(), | ||
Color::Lcha(lcha) => LinearRgba::from(lcha).into(), | ||
Color::Oklaba(oklab) => oklab, | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
//! Module for calculating distance between two colors in the same color space. | ||
viridia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// Calculate the distance between this and another color as if they were coordinates | ||
/// in a Euclidean space. Alpha is not considered in the distance calculation. | ||
pub trait EuclideanDistance: Sized { | ||
viridia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Distance from `self` to `other`. | ||
fn distance(&self, other: &Self) -> f32 { | ||
self.distance_squared(other).sqrt() | ||
} | ||
|
||
/// Distance squared from `self` to `other`. | ||
fn distance_squared(&self, other: &Self) -> f32; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/// Methods for changing the luminance of a color. Note that these methods are not | ||
/// guaranteed to produce consistent results across color spaces, | ||
/// but will be within a given space. | ||
pub trait Luminance: Sized { | ||
/// Return the luminance of this color (0.0 - 1.0). | ||
fn luminance(&self) -> f32; | ||
|
||
/// Return a new version of this color with the given luminance. The resulting color will | ||
/// be clamped to the valid range for the color space; for some color spaces, clamping | ||
/// may cause the hue or chroma to change. | ||
fn with_luminance(&self, value: f32) -> Self; | ||
|
||
/// Return a darker version of this color. The `amount` should be between 0.0 and 1.0. | ||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// The amount represents an absolute decrease in luminance, and is distributive: | ||
/// `color.darker(a).darker(b) == color.darker(a + b)`. Colors are clamped to black | ||
/// if the amount would cause them to go below black. | ||
/// | ||
/// For a relative decrease in luminance, you can simply `mix()` with black. | ||
fn darker(&self, amount: f32) -> Self; | ||
|
||
/// Return a lighter version of this color. The `amount` should be between 0.0 and 1.0. | ||
/// The amount represents an absolute increase in luminance, and is distributive: | ||
/// `color.lighter(a).lighter(b) == color.lighter(a + b)`. Colors are clamped to white | ||
/// if the amount would cause them to go above white. | ||
/// | ||
/// For a relative increase in luminance, you can simply `mix()` with white. | ||
fn lighter(&self, amount: f32) -> Self; | ||
} | ||
|
||
/// Linear interpolation of two colors within a given color space. | ||
pub trait Mix: Sized { | ||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking concern: we need to figure out how this trait relates to
I think that 1 is clearer and results in less duplication, but will complicate our dependency tree and make this crate effectively useless externally. As a result, I think that 2 is the way to go. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
/// Linearly interpolate between this and another color, by factor. | ||
/// Factor should be between 0.0 and 1.0. | ||
fn mix(&self, other: &Self, factor: f32) -> Self; | ||
|
||
/// Linearly interpolate between this and another color, by factor, storing the result | ||
/// in this color. Factor should be between 0.0 and 1.0. | ||
fn mix_assign(&mut self, other: Self, factor: f32) { | ||
*self = self.mix(&other, factor); | ||
} | ||
} | ||
|
||
/// Methods for manipulating alpha values. | ||
pub trait Alpha: Sized { | ||
viridia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Return a new version of this color with the given alpha value. | ||
fn with_alpha(&self, alpha: f32) -> Self; | ||
|
||
/// Return a the alpha component of this color. | ||
fn alpha(&self) -> f32; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
use std::ops::Range; | ||
|
||
use crate::Mix; | ||
|
||
/// Represents a range of colors that can be linearly interpolated, defined by a start and | ||
/// end point which must be in the same color space. It works for any color type that | ||
/// implements [`Mix`]. | ||
/// | ||
/// This is useful for defining gradients or animated color transitions. | ||
pub trait ColorRange<T: Mix> { | ||
/// Get the color value at the given interpolation factor, which should be between 0.0 (start) | ||
/// and 1.0 (end). | ||
fn at(&self, factor: f32) -> T; | ||
} | ||
|
||
impl<T: Mix> ColorRange<T> for Range<T> { | ||
fn at(&self, factor: f32) -> T { | ||
self.start.mix(&self.end, factor) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::{LinearRgba, Srgba}; | ||
|
||
#[test] | ||
fn test_color_range() { | ||
let range = Srgba::RED..Srgba::BLUE; | ||
assert_eq!(range.at(0.0), Srgba::RED); | ||
assert_eq!(range.at(0.5), Srgba::new(0.5, 0.0, 0.5, 1.0)); | ||
assert_eq!(range.at(1.0), Srgba::BLUE); | ||
|
||
let lred: LinearRgba = Srgba::RED.into(); | ||
let lblue: LinearRgba = Srgba::BLUE.into(); | ||
|
||
let range = lred..lblue; | ||
assert_eq!(range.at(0.0), lred); | ||
assert_eq!(range.at(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0)); | ||
assert_eq!(range.at(1.0), lblue); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.