forked from bevyengine/bevy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Upstreaming bevy_color. (bevyengine#12013)
# Objective This provides a new set of color types and operations for Bevy. Fixes: bevyengine#10986 bevyengine#1402 ## Solution The new crate provides a set of distinct types for various useful color spaces, along with utilities for manipulating and converting colors. This is not a breaking change, as no Bevy APIs are modified (yet). --------- Co-authored-by: François <[email protected]>
- Loading branch information
Showing
17 changed files
with
1,979 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
/// 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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
/// 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 { | ||
/// 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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
/// 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 { | ||
/// 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 { | ||
/// 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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.