Skip to content

Commit

Permalink
Upstreaming bevy_color. (#12013)
Browse files Browse the repository at this point in the history
# Objective

This provides a new set of color types and operations for Bevy.

Fixes: #10986 #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 <mockersf@gmail.com>
  • Loading branch information
viridia and mockersf authored Feb 23, 2024
1 parent 54e2b2e commit 31d7fcd
Show file tree
Hide file tree
Showing 17 changed files with 1,979 additions and 0 deletions.
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"
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
```
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 {
/// 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.

/// 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;
}
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.
/// 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;
}
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

0 comments on commit 31d7fcd

Please sign in to comment.