mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 14:10:19 +00:00
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:
parent
54e2b2ea07
commit
31d7fcd9cb
17 changed files with 1979 additions and 0 deletions
20
crates/bevy_color/Cargo.toml
Normal file
20
crates/bevy_color/Cargo.toml
Normal 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
|
10
crates/bevy_color/crates/gen_tests/Cargo.toml
Normal file
10
crates/bevy_color/crates/gen_tests/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "gen_tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
palette = "0.7.4"
|
11
crates/bevy_color/crates/gen_tests/README.md
Normal file
11
crates/bevy_color/crates/gen_tests/README.md
Normal 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
|
||||
```
|
90
crates/bevy_color/crates/gen_tests/src/main.rs
Normal file
90
crates/bevy_color/crates/gen_tests/src/main.rs
Normal 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)
|
||||
}
|
||||
}
|
128
crates/bevy_color/src/color.rs
Normal file
128
crates/bevy_color/src/color.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
13
crates/bevy_color/src/color_difference.rs
Normal file
13
crates/bevy_color/src/color_difference.rs
Normal 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;
|
||||
}
|
50
crates/bevy_color/src/color_ops.rs
Normal file
50
crates/bevy_color/src/color_ops.rs
Normal 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;
|
||||
}
|
42
crates/bevy_color/src/color_range.rs
Normal file
42
crates/bevy_color/src/color_range.rs
Normal 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);
|
||||
}
|
||||
}
|
223
crates/bevy_color/src/hsla.rs
Normal file
223
crates/bevy_color/src/hsla.rs
Normal 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);
|
||||
}
|
||||
}
|
231
crates/bevy_color/src/lcha.rs
Normal file
231
crates/bevy_color/src/lcha.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
88
crates/bevy_color/src/lib.rs
Normal file
88
crates/bevy_color/src/lib.rs
Normal 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::*;
|
264
crates/bevy_color/src/linear_rgba.rs
Normal file
264
crates/bevy_color/src/linear_rgba.rs
Normal 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);
|
||||
}
|
||||
}
|
183
crates/bevy_color/src/oklaba.rs
Normal file
183
crates/bevy_color/src/oklaba.rs
Normal 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);
|
||||
}
|
||||
}
|
430
crates/bevy_color/src/srgba.rs
Normal file
430
crates/bevy_color/src/srgba.rs
Normal 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('#')));
|
||||
}
|
||||
}
|
180
crates/bevy_color/src/test_colors.rs
Normal file
180
crates/bevy_color/src/test_colors.rs
Normal 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),
|
||||
},
|
||||
];
|
15
crates/bevy_color/src/testing.rs
Normal file
15
crates/bevy_color/src/testing.rs
Normal 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;
|
|
@ -43,6 +43,7 @@ crates=(
|
|||
bevy_winit
|
||||
bevy_internal
|
||||
bevy_dylib
|
||||
bevy_color
|
||||
)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
|
|
Loading…
Reference in a new issue