Upstreaming bevy_color. (#12013)

# 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>
This commit is contained in:
Talin 2024-02-23 09:51:31 -08:00 committed by GitHub
parent 54e2b2ea07
commit 31d7fcd9cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1979 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,10 @@
[package]
name = "gen_tests"
version = "0.1.0"
edition = "2021"
publish = false
[workspace]
[dependencies]
palette = "0.7.4"

View file

@ -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
```

View file

@ -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)
}
}

View file

@ -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,
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -0,0 +1,223 @@
use crate::{Alpha, LinearRgba, Luminance, Mix, Srgba};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::HslRepresentation;
use serde::{Deserialize, Serialize};
/// Color in Hue-Saturation-Lightness color space with alpha
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Hsla {
/// The hue channel. [0.0, 360.0]
pub hue: f32,
/// The saturation channel. [0.0, 1.0]
pub saturation: f32,
/// The lightness channel. [0.0, 1.0]
pub lightness: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl Hsla {
/// Construct a new [`Hsla`] color from components.
///
/// # Arguments
///
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `saturation` - Saturation channel. [0.0, 1.0]
/// * `lightness` - Lightness channel. [0.0, 1.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
Self {
hue,
saturation,
lightness,
alpha,
}
}
/// Construct a new [`Hsla`] color from (h, s, l) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `saturation` - Saturation channel. [0.0, 1.0]
/// * `lightness` - Lightness channel. [0.0, 1.0]
pub const fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self {
Self::new(hue, saturation, lightness, 1.0)
}
}
impl Default for Hsla {
fn default() -> Self {
Self::new(0., 0., 1., 1.)
}
}
impl Mix for Hsla {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
// TODO: Refactor this into EuclideanModulo::lerp_modulo
let shortest_angle = ((((other.hue - self.hue) % 360.) + 540.) % 360.) - 180.;
let mut hue = self.hue + shortest_angle * factor;
if hue < 0. {
hue += 360.;
} else if hue >= 360. {
hue -= 360.;
}
Self {
hue,
saturation: self.saturation * n_factor + other.saturation * factor,
lightness: self.lightness * n_factor + other.lightness * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl Alpha for Hsla {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl Luminance for Hsla {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {
Self { lightness, ..*self }
}
fn luminance(&self) -> f32 {
self.lightness
}
fn darker(&self, amount: f32) -> Self {
Self {
lightness: (self.lightness - amount).clamp(0., 1.),
..*self
}
}
fn lighter(&self, amount: f32) -> Self {
Self {
lightness: (self.lightness + amount).min(1.),
..*self
}
}
}
impl From<Srgba> for Hsla {
fn from(value: Srgba) -> Self {
let (h, s, l) =
HslRepresentation::nonlinear_srgb_to_hsl([value.red, value.green, value.blue]);
Self::new(h, s, l, value.alpha)
}
}
impl From<LinearRgba> for Hsla {
fn from(value: LinearRgba) -> Self {
Hsla::from(Srgba::from(value))
}
}
impl From<Hsla> for bevy_render::color::Color {
fn from(value: Hsla) -> Self {
bevy_render::color::Color::Hsla {
hue: value.hue,
saturation: value.saturation,
lightness: value.lightness,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::Color> for Hsla {
fn from(value: bevy_render::color::Color) -> Self {
match value.as_hsla() {
bevy_render::color::Color::Hsla {
hue,
saturation,
lightness,
alpha,
} => Hsla::new(hue, saturation, lightness, alpha),
_ => unreachable!(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
Srgba,
};
#[test]
fn test_to_from_srgba() {
let hsla = Hsla::new(0.5, 0.5, 0.5, 1.0);
let srgba: Srgba = hsla.into();
let hsla2: Hsla = srgba.into();
assert_approx_eq!(hsla.hue, hsla2.hue, 0.001);
assert_approx_eq!(hsla.saturation, hsla2.saturation, 0.001);
assert_approx_eq!(hsla.lightness, hsla2.lightness, 0.001);
assert_approx_eq!(hsla.alpha, hsla2.alpha, 0.001);
}
#[test]
fn test_to_from_srgba_2() {
for color in TEST_COLORS.iter() {
let rgb2: Srgba = (color.hsl).into();
let hsl2: Hsla = (color.rgb).into();
assert!(
color.rgb.distance(&rgb2) < 0.000001,
"{}: {:?} != {:?}",
color.name,
color.rgb,
rgb2
);
assert_approx_eq!(color.hsl.hue, hsl2.hue, 0.001);
assert_approx_eq!(color.hsl.saturation, hsl2.saturation, 0.001);
assert_approx_eq!(color.hsl.lightness, hsl2.lightness, 0.001);
assert_approx_eq!(color.hsl.alpha, hsl2.alpha, 0.001);
}
}
#[test]
fn test_to_from_linear() {
let hsla = Hsla::new(0.5, 0.5, 0.5, 1.0);
let linear: LinearRgba = hsla.into();
let hsla2: Hsla = linear.into();
assert_approx_eq!(hsla.hue, hsla2.hue, 0.001);
assert_approx_eq!(hsla.saturation, hsla2.saturation, 0.001);
assert_approx_eq!(hsla.lightness, hsla2.lightness, 0.001);
assert_approx_eq!(hsla.alpha, hsla2.alpha, 0.001);
}
#[test]
fn test_mix_wrap() {
let hsla0 = Hsla::new(10., 0.5, 0.5, 1.0);
let hsla1 = Hsla::new(20., 0.5, 0.5, 1.0);
let hsla2 = Hsla::new(350., 0.5, 0.5, 1.0);
assert_approx_eq!(hsla0.mix(&hsla1, 0.25).hue, 12.5, 0.001);
assert_approx_eq!(hsla0.mix(&hsla1, 0.5).hue, 15., 0.001);
assert_approx_eq!(hsla0.mix(&hsla1, 0.75).hue, 17.5, 0.001);
assert_approx_eq!(hsla1.mix(&hsla0, 0.25).hue, 17.5, 0.001);
assert_approx_eq!(hsla1.mix(&hsla0, 0.5).hue, 15., 0.001);
assert_approx_eq!(hsla1.mix(&hsla0, 0.75).hue, 12.5, 0.001);
assert_approx_eq!(hsla0.mix(&hsla2, 0.25).hue, 5., 0.001);
assert_approx_eq!(hsla0.mix(&hsla2, 0.5).hue, 0., 0.001);
assert_approx_eq!(hsla0.mix(&hsla2, 0.75).hue, 355., 0.001);
assert_approx_eq!(hsla2.mix(&hsla0, 0.25).hue, 355., 0.001);
assert_approx_eq!(hsla2.mix(&hsla0, 0.5).hue, 0., 0.001);
assert_approx_eq!(hsla2.mix(&hsla0, 0.75).hue, 5., 0.001);
}
}

View file

@ -0,0 +1,231 @@
use crate::{Alpha, LinearRgba, Luminance, Mix, Srgba};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::LchRepresentation;
use serde::{Deserialize, Serialize};
/// Color in LCH color space, with alpha
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Lcha {
/// The lightness channel. [0.0, 1.5]
pub lightness: f32,
/// The chroma channel. [0.0, 1.5]
pub chroma: f32,
/// The hue channel. [0.0, 360.0]
pub hue: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl Lcha {
/// Construct a new [`Lcha`] color from components.
///
/// # Arguments
///
/// * `lightness` - Lightness channel. [0.0, 1.5]
/// * `chroma` - Chroma channel. [0.0, 1.5]
/// * `hue` - Hue channel. [0.0, 360.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
Self {
lightness,
chroma,
hue,
alpha,
}
}
/// Construct a new [`Lcha`] color from (h, s, l) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `lightness` - Lightness channel. [0.0, 1.5]
/// * `chroma` - Chroma channel. [0.0, 1.5]
/// * `hue` - Hue channel. [0.0, 360.0]
pub const fn lch(lightness: f32, chroma: f32, hue: f32) -> Self {
Self {
lightness,
chroma,
hue,
alpha: 1.0,
}
}
}
impl Default for Lcha {
fn default() -> Self {
Self::new(1., 0., 0., 1.)
}
}
impl Mix for Lcha {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
Self {
lightness: self.lightness * n_factor + other.lightness * factor,
chroma: self.chroma * n_factor + other.chroma * factor,
hue: self.hue * n_factor + other.hue * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl Alpha for Lcha {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl Luminance for Lcha {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {
Self { lightness, ..*self }
}
fn luminance(&self) -> f32 {
self.lightness
}
fn darker(&self, amount: f32) -> Self {
Self::new(
(self.lightness - amount).max(0.),
self.chroma,
self.hue,
self.alpha,
)
}
fn lighter(&self, amount: f32) -> Self {
Self::new(
(self.lightness + amount).min(1.),
self.chroma,
self.hue,
self.alpha,
)
}
}
impl From<Srgba> for Lcha {
fn from(value: Srgba) -> Self {
let (l, c, h) =
LchRepresentation::nonlinear_srgb_to_lch([value.red, value.green, value.blue]);
Lcha::new(l, c, h, value.alpha)
}
}
impl From<Lcha> for Srgba {
fn from(value: Lcha) -> Self {
let [r, g, b] =
LchRepresentation::lch_to_nonlinear_srgb(value.lightness, value.chroma, value.hue);
Srgba::new(r, g, b, value.alpha)
}
}
impl From<LinearRgba> for Lcha {
fn from(value: LinearRgba) -> Self {
Srgba::from(value).into()
}
}
impl From<Lcha> for LinearRgba {
fn from(value: Lcha) -> Self {
LinearRgba::from(Srgba::from(value))
}
}
impl From<Lcha> for bevy_render::color::Color {
fn from(value: Lcha) -> Self {
bevy_render::color::Color::Lcha {
hue: value.hue,
chroma: value.chroma,
lightness: value.lightness,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::Color> for Lcha {
fn from(value: bevy_render::color::Color) -> Self {
match value.as_lcha() {
bevy_render::color::Color::Lcha {
hue,
chroma,
lightness,
alpha,
} => Lcha::new(hue, chroma, lightness, alpha),
_ => unreachable!(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
Srgba,
};
#[test]
fn test_to_from_srgba() {
for color in TEST_COLORS.iter() {
let rgb2: Srgba = (color.lch).into();
let lcha: Lcha = (color.rgb).into();
assert!(
color.rgb.distance(&rgb2) < 0.0001,
"{}: {:?} != {:?}",
color.name,
color.rgb,
rgb2
);
assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
if lcha.lightness > 0.01 {
assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
}
if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
assert!(
(color.lch.hue - lcha.hue).abs() < 1.7,
"{:?} != {:?}",
color.lch,
lcha
);
}
assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
}
}
#[test]
fn test_to_from_linear() {
for color in TEST_COLORS.iter() {
let rgb2: LinearRgba = (color.lch).into();
let lcha: Lcha = (color.linear_rgb).into();
assert!(
color.linear_rgb.distance(&rgb2) < 0.0001,
"{}: {:?} != {:?}",
color.name,
color.linear_rgb,
rgb2
);
assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
if lcha.lightness > 0.01 {
assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
}
if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
assert!(
(color.lch.hue - lcha.hue).abs() < 1.7,
"{:?} != {:?}",
color.lch,
lcha
);
}
assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
}
}
}

View file

@ -0,0 +1,88 @@
//! Representations of colors in various color spaces.
//!
//! This crate provides a number of color representations, including:
//!
//! - [`Srgba`] (standard RGBA, with gamma correction)
//! - [`LinearRgba`] (linear RGBA, without gamma correction)
//! - [`Hsla`] (hue, saturation, lightness, alpha)
//! - [`Lcha`] (lightness, chroma, hue, alpha)
//! - [`Oklaba`] (lightness, a-axis, b-axis, alpha)
//!
//! Each of these color spaces is represented as a distinct Rust type.
//!
//! # Color Space Usage
//!
//! Rendering engines typically use linear RGBA colors, which allow for physically accurate
//! lighting calculations. However, linear RGBA colors are not perceptually uniform, because
//! both human eyes and computer monitors have non-linear responses to light. "Standard" RGBA
//! represents an industry-wide compromise designed to encode colors in a way that looks good to
//! humans in as few bits as possible, but it is not suitable for lighting calculations.
//!
//! Most image file formats and scene graph formats use standard RGBA, because graphic design
//! tools are intended to be used by humans. However, 3D lighting calculations operate in linear
//! RGBA, so it is important to convert standard colors to linear before sending them to the GPU.
//! Most Bevy APIs will handle this conversion automatically, but if you are writing a custom
//! shader, you will need to do this conversion yourself.
//!
//! HSL and LCH are "cylindrical" color spaces, which means they represent colors as a combination
//! of hue, saturation, and lightness (or chroma). These color spaces are useful for working
//! with colors in an artistic way - for example, when creating gradients or color palettes.
//! A gradient in HSL space from red to violet will produce a rainbow. The LCH color space is
//! more perceptually accurate than HSL, but is less intuitive to work with.
//!
//! Oklab is a perceptually uniform color space that is designed to be used for tasks such
//! as image processing. It is not as widely used as the other color spaces, but it is useful
//! for tasks such as color correction and image analysis, where it is important to be able
//! to do things like change color saturation without causing hue shifts.
//!
//! See also the [Wikipedia article on color spaces](https://en.wikipedia.org/wiki/Color_space).
//!
//! # Conversions
//!
//! Each color space can be converted to and from the others using the [`From`] trait. Not all
//! possible combinations of conversions are provided, but every color space has a converstion to
//! and from [`Srgba`] and [`LinearRgba`].
//!
//! # Other Utilities
//!
//! The crate also provides a number of color operations, such as blending, color difference,
//! and color range operations.
//!
//! In addition, there is a [`Color`] enum 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.
//!
//! # Example
//!
//! ```
//! use bevy_color::{Srgba, Hsla};
//!
//! let srgba = Srgba::new(0.5, 0.2, 0.8, 1.0);
//! let hsla: Hsla = srgba.into();
//!
//! println!("Srgba: {:?}", srgba);
//! println!("Hsla: {:?}", hsla);
//! ```
mod color;
pub mod color_difference;
mod color_ops;
mod color_range;
mod hsla;
mod lcha;
mod linear_rgba;
mod oklaba;
mod srgba;
#[cfg(test)]
mod test_colors;
#[cfg(test)]
mod testing;
pub use color::*;
pub use color_ops::*;
pub use color_range::*;
pub use hsla::*;
pub use lcha::*;
pub use linear_rgba::*;
pub use oklaba::*;
pub use srgba::*;

View file

@ -0,0 +1,264 @@
use crate::{
color_difference::EuclideanDistance, oklaba::Oklaba, Alpha, Hsla, Luminance, Mix, Srgba,
};
use bevy_math::Vec4;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::SrgbColorSpace;
use serde::{Deserialize, Serialize};
/// Linear RGB color with alpha.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct LinearRgba {
/// The red channel. [0.0, 1.0]
pub red: f32,
/// The green channel. [0.0, 1.0]
pub green: f32,
/// The blue channel. [0.0, 1.0]
pub blue: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl LinearRgba {
/// Construct a new [`LinearRgba`] color from components.
pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
/// Construct a new [`LinearRgba`] color from (r, g, b) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `red` - Red channel. [0.0, 1.0]
/// * `green` - Green channel. [0.0, 1.0]
/// * `blue` - Blue channel. [0.0, 1.0]
pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
Self {
red,
green,
blue,
alpha: 1.0,
}
}
/// Make the color lighter or darker by some amount
fn adjust_lightness(&mut self, amount: f32) {
let luminance = self.luminance();
let target_luminance = (luminance + amount).clamp(0.0, 1.0);
if target_luminance < luminance {
let adjustment = (luminance - target_luminance) / luminance;
self.mix_assign(Self::new(0.0, 0.0, 0.0, self.alpha), adjustment);
} else if target_luminance > luminance {
let adjustment = (target_luminance - luminance) / (1. - luminance);
self.mix_assign(Self::new(1.0, 1.0, 1.0, self.alpha), adjustment);
}
}
}
impl Default for LinearRgba {
/// Construct a new [`LinearRgba`] color with the default values (white with full alpha).
fn default() -> Self {
Self {
red: 1.,
green: 1.,
blue: 1.,
alpha: 1.,
}
}
}
impl Luminance for LinearRgba {
/// Luminance calculated using the [CIE XYZ formula](https://en.wikipedia.org/wiki/Relative_luminance).
#[inline]
fn luminance(&self) -> f32 {
self.red * 0.2126 + self.green * 0.7152 + self.blue * 0.0722
}
#[inline]
fn with_luminance(&self, luminance: f32) -> Self {
let current_luminance = self.luminance();
let adjustment = luminance / current_luminance;
Self {
red: (self.red * adjustment).clamp(0., 1.),
green: (self.green * adjustment).clamp(0., 1.),
blue: (self.blue * adjustment).clamp(0., 1.),
alpha: self.alpha,
}
}
#[inline]
fn darker(&self, amount: f32) -> Self {
let mut result = *self;
result.adjust_lightness(-amount);
result
}
#[inline]
fn lighter(&self, amount: f32) -> Self {
let mut result = *self;
result.adjust_lightness(amount);
result
}
}
impl Mix for LinearRgba {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
Self {
red: self.red * n_factor + other.red * factor,
green: self.green * n_factor + other.green * factor,
blue: self.blue * n_factor + other.blue * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl Alpha for LinearRgba {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl EuclideanDistance for LinearRgba {
#[inline]
fn distance_squared(&self, other: &Self) -> f32 {
let dr = self.red - other.red;
let dg = self.green - other.green;
let db = self.blue - other.blue;
dr * dr + dg * dg + db * db
}
}
impl From<Srgba> for LinearRgba {
#[inline]
fn from(value: Srgba) -> Self {
Self {
red: value.red.nonlinear_to_linear_srgb(),
green: value.green.nonlinear_to_linear_srgb(),
blue: value.blue.nonlinear_to_linear_srgb(),
alpha: value.alpha,
}
}
}
impl From<LinearRgba> for bevy_render::color::Color {
fn from(value: LinearRgba) -> Self {
bevy_render::color::Color::RgbaLinear {
red: value.red,
green: value.green,
blue: value.blue,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::Color> for LinearRgba {
fn from(value: bevy_render::color::Color) -> Self {
match value.as_rgba_linear() {
bevy_render::color::Color::RgbaLinear {
red,
green,
blue,
alpha,
} => LinearRgba::new(red, green, blue, alpha),
_ => unreachable!(),
}
}
}
impl From<LinearRgba> for [f32; 4] {
fn from(color: LinearRgba) -> Self {
[color.red, color.green, color.blue, color.alpha]
}
}
impl From<LinearRgba> for Vec4 {
fn from(color: LinearRgba) -> Self {
Vec4::new(color.red, color.green, color.blue, color.alpha)
}
}
#[allow(clippy::excessive_precision)]
impl From<Oklaba> for LinearRgba {
fn from(value: Oklaba) -> Self {
let Oklaba { l, a, b, alpha } = value;
// From https://github.com/Ogeon/palette/blob/e75eab2fb21af579353f51f6229a510d0d50a311/palette/src/oklab.rs#L312-L332
let l_ = l + 0.3963377774 * a + 0.2158037573 * b;
let m_ = l - 0.1055613458 * a - 0.0638541728 * b;
let s_ = l - 0.0894841775 * a - 1.2914855480 * b;
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
let red = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
let green = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
let blue = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
Self {
red,
green,
blue,
alpha,
}
}
}
impl From<Hsla> for LinearRgba {
#[inline]
fn from(value: Hsla) -> Self {
LinearRgba::from(Srgba::from(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn euclidean_distance() {
// White to black
let a = LinearRgba::new(0.0, 0.0, 0.0, 1.0);
let b = LinearRgba::new(1.0, 1.0, 1.0, 1.0);
assert_eq!(a.distance_squared(&b), 3.0);
// Alpha shouldn't matter
let a = LinearRgba::new(0.0, 0.0, 0.0, 1.0);
let b = LinearRgba::new(1.0, 1.0, 1.0, 0.0);
assert_eq!(a.distance_squared(&b), 3.0);
// Red to green
let a = LinearRgba::new(0.0, 0.0, 0.0, 1.0);
let b = LinearRgba::new(1.0, 0.0, 0.0, 1.0);
assert_eq!(a.distance_squared(&b), 1.0);
}
#[test]
fn darker_lighter() {
// Darker and lighter should be commutative.
let color = LinearRgba::new(0.4, 0.5, 0.6, 1.0);
let darker1 = color.darker(0.1);
let darker2 = darker1.darker(0.1);
let twice_as_dark = color.darker(0.2);
assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
let lighter1 = color.lighter(0.1);
let lighter2 = lighter1.lighter(0.1);
let twice_as_light = color.lighter(0.2);
assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
}
}

View file

@ -0,0 +1,183 @@
use crate::{color_difference::EuclideanDistance, Alpha, LinearRgba, Luminance, Mix, Srgba};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
/// Color in Oklaba color space, with alpha
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Oklaba {
/// The 'l' channel. [0.0, 1.0]
pub l: f32,
/// The 'a' channel. [-1.0, 1.0]
pub a: f32,
/// The 'b' channel. [-1.0, 1.0]
pub b: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl Oklaba {
/// Construct a new [`Oklaba`] color from components.
///
/// # Arguments
///
/// * `l` - Lightness channel. [0.0, 1.0]
/// * `a` - Green-red channel. [-1.0, 1.0]
/// * `b` - Blue-yellow channel. [-1.0, 1.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(l: f32, a: f32, b: f32, alpha: f32) -> Self {
Self { l, a, b, alpha }
}
/// Construct a new [`Oklaba`] color from (l, a, b) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `l` - Lightness channel. [0.0, 1.0]
/// * `a` - Green-red channel. [-1.0, 1.0]
/// * `b` - Blue-yellow channel. [-1.0, 1.0]
pub const fn lch(l: f32, a: f32, b: f32) -> Self {
Self {
l,
a,
b,
alpha: 1.0,
}
}
}
impl Default for Oklaba {
fn default() -> Self {
Self::new(1., 0., 0., 1.)
}
}
impl Mix for Oklaba {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
Self {
l: self.l * n_factor + other.l * factor,
a: self.a * n_factor + other.a * factor,
b: self.b * n_factor + other.b * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl Alpha for Oklaba {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl Luminance for Oklaba {
#[inline]
fn with_luminance(&self, l: f32) -> Self {
Self { l, ..*self }
}
fn luminance(&self) -> f32 {
self.l
}
fn darker(&self, amount: f32) -> Self {
Self::new((self.l - amount).max(0.), self.a, self.b, self.alpha)
}
fn lighter(&self, amount: f32) -> Self {
Self::new((self.l + amount).min(1.), self.a, self.b, self.alpha)
}
}
impl EuclideanDistance for Oklaba {
#[inline]
fn distance_squared(&self, other: &Self) -> f32 {
(self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2)
}
}
#[allow(clippy::excessive_precision)]
impl From<LinearRgba> for Oklaba {
fn from(value: LinearRgba) -> Self {
let LinearRgba {
red,
green,
blue,
alpha,
} = value;
// From https://github.com/DougLau/pix
let l = 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue;
let m = 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue;
let s = 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue;
let l_ = l.cbrt();
let m_ = m.cbrt();
let s_ = s.cbrt();
let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
Oklaba::new(l, a, b, alpha)
}
}
impl From<Srgba> for Oklaba {
fn from(value: Srgba) -> Self {
Oklaba::from(LinearRgba::from(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq, Srgba};
#[test]
fn test_to_from_srgba() {
let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0);
let srgba: Srgba = oklaba.into();
let oklaba2: Oklaba = srgba.into();
assert_approx_eq!(oklaba.l, oklaba2.l, 0.001);
assert_approx_eq!(oklaba.a, oklaba2.a, 0.001);
assert_approx_eq!(oklaba.b, oklaba2.b, 0.001);
assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
}
#[test]
fn test_to_from_srgba_2() {
for color in TEST_COLORS.iter() {
let rgb2: Srgba = (color.oklab).into();
let oklab: Oklaba = (color.rgb).into();
assert!(
color.rgb.distance(&rgb2) < 0.0001,
"{}: {:?} != {:?}",
color.name,
color.rgb,
rgb2
);
assert!(
color.oklab.distance(&oklab) < 0.0001,
"{}: {:?} != {:?}",
color.name,
color.oklab,
oklab
);
}
}
#[test]
fn test_to_from_linear() {
let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0);
let linear: LinearRgba = oklaba.into();
let oklaba2: Oklaba = linear.into();
assert_approx_eq!(oklaba.l, oklaba2.l, 0.001);
assert_approx_eq!(oklaba.a, oklaba2.a, 0.001);
assert_approx_eq!(oklaba.b, oklaba2.b, 0.001);
assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
}
}

View file

@ -0,0 +1,430 @@
use crate::color_difference::EuclideanDistance;
use crate::oklaba::Oklaba;
use crate::{Alpha, Hsla, LinearRgba, Luminance, Mix};
use bevy_math::Vec4;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::{HexColorError, HslRepresentation, SrgbColorSpace};
use serde::{Deserialize, Serialize};
/// Non-linear standard RGB with alpha.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct Srgba {
/// The red channel. [0.0, 1.0]
pub red: f32,
/// The green channel. [0.0, 1.0]
pub green: f32,
/// The blue channel. [0.0, 1.0]
pub blue: f32,
/// The alpha channel. [0.0, 1.0]
pub alpha: f32,
}
impl Srgba {
// The standard VGA colors, with alpha set to 1.0.
// https://en.wikipedia.org/wiki/Web_colors#Basic_colors
/// <div style="background-color:rgb(0%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const BLACK: Srgba = Srgba::new(0.0, 0.0, 0.0, 1.0);
/// <div style="background-color:rgb(0%, 0%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const BLUE: Srgba = Srgba::new(0.0, 0.0, 1.0, 1.0);
/// <div style="background-color:rgb(0%, 100%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const CYAN: Srgba = Srgba::new(0.0, 1.0, 1.0, 1.0);
/// <div style="background-color:rgb(25%, 25%, 25%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const DARK_GRAY: Srgba = Srgba::new(0.25, 0.25, 0.25, 1.0);
/// <div style="background-color:rgb(0%, 50%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const GREEN: Srgba = Srgba::new(0.0, 0.5, 0.0, 1.0);
/// <div style="background-color:rgb(100%, 0%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const FUCHSIA: Srgba = Srgba::new(1.0, 0.0, 1.0, 1.0);
/// <div style="background-color:rgb(50%, 50%, 50%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const GRAY: Srgba = Srgba::new(0.5, 0.5, 0.5, 1.0);
/// <div style="background-color:rgb(0%, 100%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const LIME: Srgba = Srgba::new(0.0, 1.0, 0.0, 1.0);
/// <div style="background-color:rgb(50%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const MAROON: Srgba = Srgba::new(0.5, 0.0, 0.0, 1.0);
/// <div style="background-color:rgb(0%, 0%, 50%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const NAVY: Srgba = Srgba::new(0.0, 0.0, 0.5, 1.0);
/// <div style="background-color:rgba(0%, 0%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
#[doc(alias = "transparent")]
pub const NONE: Srgba = Srgba::new(0.0, 0.0, 0.0, 0.0);
/// <div style="background-color:rgb(50%, 50%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const OLIVE: Srgba = Srgba::new(0.5, 0.5, 0.0, 1.0);
/// <div style="background-color:rgb(50%, 0%, 50%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const PURPLE: Srgba = Srgba::new(0.5, 0.0, 0.5, 1.0);
/// <div style="background-color:rgb(100%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const RED: Srgba = Srgba::new(1.0, 0.0, 0.0, 1.0);
/// <div style="background-color:rgb(75%, 75%, 75%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const SILVER: Srgba = Srgba::new(0.75, 0.75, 0.75, 1.0);
/// <div style="background-color:rgb(0%, 50%, 50%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const TEAL: Srgba = Srgba::new(0.0, 0.5, 0.5, 1.0);
/// <div style="background-color:rgb(100%, 100%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const WHITE: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
/// <div style="background-color:rgb(100%, 100%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const YELLOW: Srgba = Srgba::new(1.0, 1.0, 0.0, 1.0);
/// Construct a new [`Srgba`] color from components.
///
/// # Arguments
///
/// * `red` - Red channel. [0.0, 1.0]
/// * `green` - Green channel. [0.0, 1.0]
/// * `blue` - Blue channel. [0.0, 1.0]
/// * `alpha` - Alpha channel. [0.0, 1.0]
pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
/// Construct a new [`Srgba`] color from (r, g, b) components, with the default alpha (1.0).
///
/// # Arguments
///
/// * `red` - Red channel. [0.0, 1.0]
/// * `green` - Green channel. [0.0, 1.0]
/// * `blue` - Blue channel. [0.0, 1.0]
pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
Self {
red,
green,
blue,
alpha: 1.0,
}
}
/// New `Srgba` from a CSS-style hexadecimal string.
///
/// # Examples
///
/// ```
/// # use bevy_color::Srgba;
/// let color = Srgba::hex("FF00FF").unwrap(); // fuchsia
/// let color = Srgba::hex("FF00FF7F").unwrap(); // partially transparent fuchsia
///
/// // A standard hex color notation is also available
/// assert_eq!(Srgba::hex("#FFFFFF").unwrap(), Srgba::new(1.0, 1.0, 1.0, 1.0));
/// ```
pub fn hex<T: AsRef<str>>(hex: T) -> Result<Self, HexColorError> {
let hex = hex.as_ref();
let hex = hex.strip_prefix('#').unwrap_or(hex);
match *hex.as_bytes() {
// RGB
[r, g, b] => {
let [r, g, b, ..] = decode_hex([r, r, g, g, b, b])?;
Ok(Self::rgb_u8(r, g, b))
}
// RGBA
[r, g, b, a] => {
let [r, g, b, a, ..] = decode_hex([r, r, g, g, b, b, a, a])?;
Ok(Self::rgba_u8(r, g, b, a))
}
// RRGGBB
[r1, r2, g1, g2, b1, b2] => {
let [r, g, b, ..] = decode_hex([r1, r2, g1, g2, b1, b2])?;
Ok(Self::rgb_u8(r, g, b))
}
// RRGGBBAA
[r1, r2, g1, g2, b1, b2, a1, a2] => {
let [r, g, b, a, ..] = decode_hex([r1, r2, g1, g2, b1, b2, a1, a2])?;
Ok(Self::rgba_u8(r, g, b, a))
}
_ => Err(HexColorError::Length),
}
}
/// Convert this color to CSS-style hexadecimal notation.
pub fn to_hex(&self) -> String {
let r = (self.red * 255.0).round() as u8;
let g = (self.green * 255.0).round() as u8;
let b = (self.blue * 255.0).round() as u8;
let a = (self.alpha * 255.0).round() as u8;
match a {
255 => format!("#{:02X}{:02X}{:02X}", r, g, b),
_ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a),
}
}
/// New `Srgba` from sRGB colorspace.
///
/// # Arguments
///
/// * `r` - Red channel. [0, 255]
/// * `g` - Green channel. [0, 255]
/// * `b` - Blue channel. [0, 255]
///
/// See also [`Srgba::new`], [`Srgba::rgba_u8`], [`Srgba::hex`].
///
pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
Self::rgba_u8(r, g, b, u8::MAX)
}
// Float operations in const fn are not stable yet
// see https://github.com/rust-lang/rust/issues/57241
/// New `Srgba` from sRGB colorspace.
///
/// # Arguments
///
/// * `r` - Red channel. [0, 255]
/// * `g` - Green channel. [0, 255]
/// * `b` - Blue channel. [0, 255]
/// * `a` - Alpha channel. [0, 255]
///
/// See also [`Srgba::new`], [`Srgba::rgb_u8`], [`Srgba::hex`].
///
pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self::new(
r as f32 / u8::MAX as f32,
g as f32 / u8::MAX as f32,
b as f32 / u8::MAX as f32,
a as f32 / u8::MAX as f32,
)
}
}
impl Default for Srgba {
fn default() -> Self {
Self::WHITE
}
}
impl Luminance for Srgba {
#[inline]
fn luminance(&self) -> f32 {
let linear: LinearRgba = (*self).into();
linear.luminance()
}
#[inline]
fn with_luminance(&self, luminance: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear
.with_luminance(luminance.nonlinear_to_linear_srgb())
.into()
}
#[inline]
fn darker(&self, amount: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear.darker(amount).into()
}
#[inline]
fn lighter(&self, amount: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear.lighter(amount).into()
}
}
impl Mix for Srgba {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
Self {
red: self.red * n_factor + other.red * factor,
green: self.green * n_factor + other.green * factor,
blue: self.blue * n_factor + other.blue * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl Alpha for Srgba {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
}
impl EuclideanDistance for Srgba {
#[inline]
fn distance_squared(&self, other: &Self) -> f32 {
let dr = self.red - other.red;
let dg = self.green - other.green;
let db = self.blue - other.blue;
dr * dr + dg * dg + db * db
}
}
impl From<LinearRgba> for Srgba {
#[inline]
fn from(value: LinearRgba) -> Self {
Self {
red: value.red.linear_to_nonlinear_srgb(),
green: value.green.linear_to_nonlinear_srgb(),
blue: value.blue.linear_to_nonlinear_srgb(),
alpha: value.alpha,
}
}
}
impl From<Hsla> for Srgba {
fn from(value: Hsla) -> Self {
let [r, g, b] =
HslRepresentation::hsl_to_nonlinear_srgb(value.hue, value.saturation, value.lightness);
Self::new(r, g, b, value.alpha)
}
}
impl From<Oklaba> for Srgba {
fn from(value: Oklaba) -> Self {
Srgba::from(LinearRgba::from(value))
}
}
impl From<Srgba> for bevy_render::color::Color {
fn from(value: Srgba) -> Self {
bevy_render::color::Color::Rgba {
red: value.red,
green: value.green,
blue: value.blue,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::Color> for Srgba {
fn from(value: bevy_render::color::Color) -> Self {
match value.as_rgba() {
bevy_render::color::Color::Rgba {
red,
green,
blue,
alpha,
} => Srgba::new(red, green, blue, alpha),
_ => unreachable!(),
}
}
}
impl From<Srgba> for [f32; 4] {
fn from(color: Srgba) -> Self {
[color.red, color.green, color.blue, color.alpha]
}
}
impl From<Srgba> for Vec4 {
fn from(color: Srgba) -> Self {
Vec4::new(color.red, color.green, color.blue, color.alpha)
}
}
/// Converts hex bytes to an array of RGB\[A\] components
///
/// # Example
/// For RGB: *b"ffffff" -> [255, 255, 255, ..]
/// For RGBA: *b"E2E2E2FF" -> [226, 226, 226, 255, ..]
const fn decode_hex<const N: usize>(mut bytes: [u8; N]) -> Result<[u8; N], HexColorError> {
let mut i = 0;
while i < bytes.len() {
// Convert single hex digit to u8
let val = match hex_value(bytes[i]) {
Ok(val) => val,
Err(byte) => return Err(HexColorError::Char(byte as char)),
};
bytes[i] = val;
i += 1;
}
// Modify the original bytes to give an `N / 2` length result
i = 0;
while i < bytes.len() / 2 {
// Convert pairs of u8 to R/G/B/A
// e.g `ff` -> [102, 102] -> [15, 15] = 255
bytes[i] = bytes[i * 2] * 16 + bytes[i * 2 + 1];
i += 1;
}
Ok(bytes)
}
/// Parse a single hex digit (a-f/A-F/0-9) as a `u8`
const fn hex_value(b: u8) -> Result<u8, u8> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'A'..=b'F' => Ok(b - b'A' + 10),
b'a'..=b'f' => Ok(b - b'a' + 10),
// Wrong hex digit
_ => Err(b),
}
}
#[cfg(test)]
mod tests {
use crate::testing::assert_approx_eq;
use super::*;
#[test]
fn test_to_from_linear() {
let srgba = Srgba::new(0.0, 0.5, 1.0, 1.0);
let linear_rgba: LinearRgba = srgba.into();
assert_eq!(linear_rgba.red, 0.0);
assert_approx_eq!(linear_rgba.green, 0.2140, 0.0001);
assert_approx_eq!(linear_rgba.blue, 1.0, 0.0001);
assert_eq!(linear_rgba.alpha, 1.0);
let srgba2: Srgba = linear_rgba.into();
assert_eq!(srgba2.red, 0.0);
assert_approx_eq!(srgba2.green, 0.5, 0.0001);
assert_approx_eq!(srgba2.blue, 1.0, 0.0001);
assert_eq!(srgba2.alpha, 1.0);
}
#[test]
fn euclidean_distance() {
// White to black
let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
let b = Srgba::new(1.0, 1.0, 1.0, 1.0);
assert_eq!(a.distance_squared(&b), 3.0);
// Alpha shouldn't matter
let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
let b = Srgba::new(1.0, 1.0, 1.0, 0.0);
assert_eq!(a.distance_squared(&b), 3.0);
// Red to green
let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
let b = Srgba::new(1.0, 0.0, 0.0, 1.0);
assert_eq!(a.distance_squared(&b), 1.0);
}
#[test]
fn darker_lighter() {
// Darker and lighter should be commutative.
let color = Srgba::new(0.4, 0.5, 0.6, 1.0);
let darker1 = color.darker(0.1);
let darker2 = darker1.darker(0.1);
let twice_as_dark = color.darker(0.2);
assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
let lighter1 = color.lighter(0.1);
let lighter2 = lighter1.lighter(0.1);
let twice_as_light = color.lighter(0.2);
assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
}
#[test]
fn hex_color() {
assert_eq!(Srgba::hex("FFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("FFFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("FFFFFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("FFFFFFFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("000"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("000F"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("000000"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("000000FF"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("03a9f4"), Ok(Srgba::rgb_u8(3, 169, 244)));
assert_eq!(Srgba::hex("yy"), Err(HexColorError::Length));
assert_eq!(Srgba::hex("yyy"), Err(HexColorError::Char('y')));
assert_eq!(Srgba::hex("#f2a"), Ok(Srgba::rgb_u8(255, 34, 170)));
assert_eq!(Srgba::hex("#e23030"), Ok(Srgba::rgb_u8(226, 48, 48)));
assert_eq!(Srgba::hex("#ff"), Err(HexColorError::Length));
assert_eq!(Srgba::hex("##fff"), Err(HexColorError::Char('#')));
}
}

View file

@ -0,0 +1,180 @@
// Generated by gen_tests. Do not edit.
#[cfg(test)]
use crate::{Hsla, Lcha, LinearRgba, Oklaba, Srgba};
#[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,
}
// Table of equivalent colors in various color spaces
#[cfg(test)]
pub const TEST_COLORS: &[TestColor] = &[
// black
TestColor {
name: "black",
rgb: Srgba::new(0.0, 0.0, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.0, 0.0, 1.0),
hsl: Hsla::new(0.0, 0.0, 0.0, 1.0),
lch: Lcha::new(0.0, 0.0, 0.0000136603785, 1.0),
oklab: Oklaba::new(0.0, 0.0, 0.0, 1.0),
},
// white
TestColor {
name: "white",
rgb: Srgba::new(1.0, 1.0, 1.0, 1.0),
linear_rgb: LinearRgba::new(1.0, 1.0, 1.0, 1.0),
hsl: Hsla::new(0.0, 0.0, 1.0, 1.0),
lch: Lcha::new(1.0, 0.0, 0.0000136603785, 1.0),
oklab: Oklaba::new(1.0, 0.0, 0.000000059604645, 1.0),
},
// red
TestColor {
name: "red",
rgb: Srgba::new(1.0, 0.0, 0.0, 1.0),
linear_rgb: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
hsl: Hsla::new(0.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.53240794, 1.0455177, 39.99901, 1.0),
oklab: Oklaba::new(0.6279554, 0.22486295, 0.1258463, 1.0),
},
// green
TestColor {
name: "green",
rgb: Srgba::new(0.0, 1.0, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.0, 1.0, 0.0, 1.0),
hsl: Hsla::new(120.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.87734723, 1.1977587, 136.01595, 1.0),
oklab: Oklaba::new(0.8664396, -0.2338874, 0.1794985, 1.0),
},
// blue
TestColor {
name: "blue",
rgb: Srgba::new(0.0, 0.0, 1.0, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.0, 1.0, 1.0),
hsl: Hsla::new(240.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.32297012, 1.3380761, 306.28494, 1.0),
oklab: Oklaba::new(0.4520137, -0.032456964, -0.31152815, 1.0),
},
// yellow
TestColor {
name: "yellow",
rgb: Srgba::new(1.0, 1.0, 0.0, 1.0),
linear_rgb: LinearRgba::new(1.0, 1.0, 0.0, 1.0),
hsl: Hsla::new(60.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.9713927, 0.96905375, 102.85126, 1.0),
oklab: Oklaba::new(0.9679827, -0.07136908, 0.19856972, 1.0),
},
// magenta
TestColor {
name: "magenta",
rgb: Srgba::new(1.0, 0.0, 1.0, 1.0),
linear_rgb: LinearRgba::new(1.0, 0.0, 1.0, 1.0),
hsl: Hsla::new(300.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.6032421, 1.1554068, 328.23495, 1.0),
oklab: Oklaba::new(0.7016738, 0.27456632, -0.16915613, 1.0),
},
// cyan
TestColor {
name: "cyan",
rgb: Srgba::new(0.0, 1.0, 1.0, 1.0),
linear_rgb: LinearRgba::new(0.0, 1.0, 1.0, 1.0),
hsl: Hsla::new(180.0, 1.0, 0.5, 1.0),
lch: Lcha::new(0.9111322, 0.50120866, 196.37614, 1.0),
oklab: Oklaba::new(0.90539926, -0.1494439, -0.039398134, 1.0),
},
// gray
TestColor {
name: "gray",
rgb: Srgba::new(0.5, 0.5, 0.5, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.21404114, 1.0),
hsl: Hsla::new(0.0, 0.0, 0.5, 1.0),
lch: Lcha::new(0.5338897, 0.00000011920929, 90.0, 1.0),
oklab: Oklaba::new(0.5981807, 0.00000011920929, 0.0, 1.0),
},
// olive
TestColor {
name: "olive",
rgb: Srgba::new(0.5, 0.5, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.0, 1.0),
hsl: Hsla::new(60.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0),
oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0),
},
// purple
TestColor {
name: "purple",
rgb: Srgba::new(0.5, 0.0, 0.5, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.21404114, 1.0),
hsl: Hsla::new(300.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0),
oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0),
},
// teal
TestColor {
name: "teal",
rgb: Srgba::new(0.0, 0.5, 0.5, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.21404114, 0.21404114, 1.0),
hsl: Hsla::new(180.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0),
oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0),
},
// maroon
TestColor {
name: "maroon",
rgb: Srgba::new(0.5, 0.0, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.0, 1.0),
hsl: Hsla::new(0.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.2541851, 0.61091745, 38.350803, 1.0),
oklab: Oklaba::new(0.3756308, 0.13450874, 0.07527886, 1.0),
},
// lime
TestColor {
name: "lime",
rgb: Srgba::new(0.0, 0.5, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.21404114, 0.0, 1.0),
hsl: Hsla::new(120.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.46052113, 0.71647626, 136.01596, 1.0),
oklab: Oklaba::new(0.5182875, -0.13990697, 0.10737252, 1.0),
},
// navy
TestColor {
name: "navy",
rgb: Srgba::new(0.0, 0.0, 0.5, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.0, 0.21404114, 1.0),
hsl: Hsla::new(240.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.12890343, 0.8004114, 306.28494, 1.0),
oklab: Oklaba::new(0.27038592, -0.01941514, -0.18635012, 1.0),
},
// orange
TestColor {
name: "orange",
rgb: Srgba::new(0.5, 0.5, 0.0, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.0, 1.0),
hsl: Hsla::new(60.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0),
oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0),
},
// fuchsia
TestColor {
name: "fuchsia",
rgb: Srgba::new(0.5, 0.0, 0.5, 1.0),
linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.21404114, 1.0),
hsl: Hsla::new(300.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0),
oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0),
},
// aqua
TestColor {
name: "aqua",
rgb: Srgba::new(0.0, 0.5, 0.5, 1.0),
linear_rgb: LinearRgba::new(0.0, 0.21404114, 0.21404114, 1.0),
hsl: Hsla::new(180.0, 1.0, 0.25, 1.0),
lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0),
oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0),
},
];

View file

@ -0,0 +1,15 @@
#[cfg(test)]
macro_rules! assert_approx_eq {
($x:expr, $y:expr, $d:expr) => {
if ($x - $y).abs() >= $d {
panic!(
"assertion failed: `(left !== right)` \
(left: `{:?}`, right: `{:?}`, tolerance: `{:?}`)",
$x, $y, $d
);
}
};
}
#[cfg(test)]
pub(crate) use assert_approx_eq;

View file

@ -43,6 +43,7 @@ crates=(
bevy_winit bevy_winit
bevy_internal bevy_internal
bevy_dylib bevy_dylib
bevy_color
) )
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then