Skip to content
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

Merged
merged 17 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/bevy_color/Cargo.toml
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
10 changes: 10 additions & 0 deletions crates/bevy_color/crates/gen_tests/Cargo.toml
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"
viridia marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions crates/bevy_color/crates/gen_tests/README.md
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
```
90 changes: 90 additions & 0 deletions crates/bevy_color/crates/gen_tests/src/main.rs
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)
}
}
128 changes: 128 additions & 0 deletions crates/bevy_color/src/color.rs
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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,
}
}
}
13 changes: 13 additions & 0 deletions crates/bevy_color/src/color_difference.rs
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;
}
50 changes: 50 additions & 0 deletions crates/bevy_color/src/color_ops.rs
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking concern: we need to figure out how this trait relates to Animatable. Our options:

  1. Use and implement Animatable directly, and axe this trait.
  2. Use a blanket impl <T: Mix> Animatable for T that acts as a bridging layer.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Move the stuff to a wrapper type, e.g. PerceputallyLinearMix<T>. Or differentiate perceptually vs mathematically linear by the underlying color space types.

/// 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;
}
42 changes: 42 additions & 0 deletions crates/bevy_color/src/color_range.rs
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);
}
}
Loading
Loading