Made bevy_color a dependency of bevy_render (#12105)

# Objective

- Fixes #12068

## Solution

- Split `bevy_render::color::colorspace` across the various space
implementations in `bevy_color` as appropriate.
- Moved `From` implementations involving
`bevy_render::color::LegacyColor` into `bevy_render::color`

## Migration Guide

###
`bevy_render::color::colorspace::SrgbColorSpace::<f32>::linear_to_nonlinear_srgb`

Use `bevy_color::color::gamma_function_inverse`

###
`bevy_render::color::colorspace::SrgbColorSpace::<f32>::nonlinear_to_linear_srgb`

Use `bevy_color::color::gamma_function`

###
`bevy_render::color::colorspace::SrgbColorSpace::<u8>::linear_to_nonlinear_srgb`

Modify the `u8` value to instead be an `f32` (`|x| x as f32 / 255.`),
use `bevy_color::color::gamma_function_inverse`, and back again.

###
`bevy_render::color::colorspace::SrgbColorSpace::<u8>::nonlinear_to_linear_srgb`

Modify the `u8` value to instead be an `f32` (`|x| x as f32 / 255.`),
use `bevy_color::color::gamma_function`, and back again.

###
`bevy_render::color::colorspace::HslRepresentation::hsl_to_nonlinear_srgb`

Use `Hsla`'s implementation of `Into<Srgba>`

###
`bevy_render::color::colorspace::HslRepresentation::nonlinear_srgb_to_hsl`

Use `Srgba`'s implementation of `Into<Hsla>`

###
`bevy_render::color::colorspace::LchRepresentation::lch_to_nonlinear_srgb`

Use `Lcha`'s implementation of `Into<Srgba>`

###
`bevy_render::color::colorspace::LchRepresentation::nonlinear_srgb_to_lch`

Use `Srgba`'s implementation of `Into<Lcha>`
This commit is contained in:
Zachary Harrold 2024-02-26 09:35:00 +11:00 committed by GitHub
parent e5994a4e55
commit 5e63f6815b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 462 additions and 1209 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_color"
version = "0.13.0"
version = "0.14.0-dev"
edition = "2021"
description = "Types for representing and manipulating color values"
homepage = "https://bevyengine.org"
@ -13,8 +13,8 @@ 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"
thiserror = "1.0"
[lints]
workspace = true

View File

@ -1,6 +1,5 @@
use crate::{Alpha, Hsla, Lcha, LinearRgba, Oklaba, Srgba, StandardColor, Xyza};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::LegacyColor;
use serde::{Deserialize, Serialize};
/// An enumerated type that can represent any of the color types in this crate.
@ -29,14 +28,7 @@ impl StandardColor for Color {}
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(),
Color::Xyza(xyza) => (*xyza).into(),
}
(*self).into()
}
}
@ -142,9 +134,9 @@ impl From<Color> for Hsla {
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(),
Color::Xyza(xyza) => LinearRgba::from(xyza).into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
@ -154,10 +146,10 @@ impl From<Color> for Lcha {
match value {
Color::Srgba(srgba) => srgba.into(),
Color::LinearRgba(linear) => linear.into(),
Color::Hsla(hsla) => Srgba::from(hsla).into(),
Color::Hsla(hsla) => hsla.into(),
Color::Lcha(lcha) => lcha,
Color::Oklaba(oklab) => LinearRgba::from(oklab).into(),
Color::Xyza(xyza) => LinearRgba::from(xyza).into(),
Color::Oklaba(oklab) => oklab.into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
@ -167,10 +159,10 @@ impl From<Color> for Oklaba {
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::Hsla(hsla) => hsla.into(),
Color::Lcha(lcha) => lcha.into(),
Color::Oklaba(oklab) => oklab,
Color::Xyza(xyza) => LinearRgba::from(xyza).into(),
Color::Xyza(xyza) => xyza.into(),
}
}
}
@ -187,27 +179,3 @@ impl From<Color> for Xyza {
}
}
}
impl From<LegacyColor> for Color {
fn from(value: LegacyColor) -> Self {
match value {
LegacyColor::Rgba { .. } => Srgba::from(value).into(),
LegacyColor::RgbaLinear { .. } => LinearRgba::from(value).into(),
LegacyColor::Hsla { .. } => Hsla::from(value).into(),
LegacyColor::Lcha { .. } => Lcha::from(value).into(),
}
}
}
impl From<Color> for LegacyColor {
fn from(value: Color) -> Self {
match value {
Color::Srgba(x) => x.into(),
Color::LinearRgba(x) => x.into(),
Color::Hsla(x) => x.into(),
Color::Lcha(x) => x.into(),
Color::Oklaba(x) => x.into(),
Color::Xyza(x) => x.into(),
}
}
}

View File

@ -1,6 +1,5 @@
use crate::{Alpha, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::HslRepresentation;
use serde::{Deserialize, Serialize};
/// Color in Hue-Saturation-Lightness color space with alpha
@ -129,35 +128,72 @@ impl Luminance for Hsla {
}
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)
fn from(
Srgba {
red,
green,
blue,
alpha,
}: Srgba,
) -> Self {
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
let x_max = red.max(green.max(blue));
let x_min = red.min(green.min(blue));
let chroma = x_max - x_min;
let lightness = (x_max + x_min) / 2.0;
let hue = if chroma == 0.0 {
0.0
} else if red == x_max {
60.0 * (green - blue) / chroma
} else if green == x_max {
60.0 * (2.0 + (blue - red) / chroma)
} else {
60.0 * (4.0 + (red - green) / chroma)
};
let hue = if hue < 0.0 { 360.0 + hue } else { hue };
let saturation = if lightness <= 0.0 || lightness >= 1.0 {
0.0
} else {
(x_max - lightness) / lightness.min(1.0 - lightness)
};
Self::new(hue, saturation, lightness, alpha)
}
}
impl From<Hsla> for bevy_render::color::LegacyColor {
fn from(value: Hsla) -> Self {
bevy_render::color::LegacyColor::Hsla {
hue: value.hue,
saturation: value.saturation,
lightness: value.lightness,
alpha: value.alpha,
}
}
}
impl From<Hsla> for Srgba {
fn from(
Hsla {
hue,
saturation,
lightness,
alpha,
}: Hsla,
) -> Self {
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
let hue_prime = hue / 60.0;
let largest_component = chroma * (1.0 - (hue_prime % 2.0 - 1.0).abs());
let (r_temp, g_temp, b_temp) = if hue_prime < 1.0 {
(chroma, largest_component, 0.0)
} else if hue_prime < 2.0 {
(largest_component, chroma, 0.0)
} else if hue_prime < 3.0 {
(0.0, chroma, largest_component)
} else if hue_prime < 4.0 {
(0.0, largest_component, chroma)
} else if hue_prime < 5.0 {
(largest_component, 0.0, chroma)
} else {
(chroma, 0.0, largest_component)
};
let lightness_match = lightness - chroma / 2.0;
impl From<bevy_render::color::LegacyColor> for Hsla {
fn from(value: bevy_render::color::LegacyColor) -> Self {
match value.as_hsla() {
bevy_render::color::LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => Hsla::new(hue, saturation, lightness, alpha),
_ => unreachable!(),
}
let red = r_temp + lightness_match;
let green = g_temp + lightness_match;
let blue = b_temp + lightness_match;
Self::new(red, green, blue, alpha)
}
}

View File

@ -1,6 +1,5 @@
use crate::{Alpha, Hsla, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use crate::{Alpha, Hsla, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::LchRepresentation;
use serde::{Deserialize, Serialize};
/// Color in LCH color space, with alpha
@ -67,6 +66,16 @@ impl Lcha {
pub const fn with_lightness(self, lightness: f32) -> Self {
Self { lightness, ..self }
}
/// CIE Epsilon Constant
///
/// See [Continuity (16) (17)](http://brucelindbloom.com/index.html?LContinuity.html)
pub const CIE_EPSILON: f32 = 216.0 / 24389.0;
/// CIE Kappa Constant
///
/// See [Continuity (16) (17)](http://brucelindbloom.com/index.html?LContinuity.html)
pub const CIE_KAPPA: f32 = 24389.0 / 27.0;
}
impl Default for Lcha {
@ -129,19 +138,116 @@ impl Luminance for Lcha {
}
}
impl From<Lcha> for Xyza {
fn from(
Lcha {
lightness,
chroma,
hue,
alpha,
}: Lcha,
) -> Self {
let lightness = lightness * 100.0;
let chroma = chroma * 100.0;
// convert LCH to Lab
// http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html
let l = lightness;
let a = chroma * hue.to_radians().cos();
let b = chroma * hue.to_radians().sin();
// convert Lab to XYZ
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
let fy = (l + 16.0) / 116.0;
let fx = a / 500.0 + fy;
let fz = fy - b / 200.0;
let xr = {
let fx3 = fx.powf(3.0);
if fx3 > Lcha::CIE_EPSILON {
fx3
} else {
(116.0 * fx - 16.0) / Lcha::CIE_KAPPA
}
};
let yr = if l > Lcha::CIE_EPSILON * Lcha::CIE_KAPPA {
((l + 16.0) / 116.0).powf(3.0)
} else {
l / Lcha::CIE_KAPPA
};
let zr = {
let fz3 = fz.powf(3.0);
if fz3 > Lcha::CIE_EPSILON {
fz3
} else {
(116.0 * fz - 16.0) / Lcha::CIE_KAPPA
}
};
let x = xr * Xyza::D65_WHITE.x;
let y = yr * Xyza::D65_WHITE.y;
let z = zr * Xyza::D65_WHITE.z;
Xyza::new(x, y, z, alpha)
}
}
impl From<Xyza> for Lcha {
fn from(Xyza { x, y, z, alpha }: Xyza) -> Self {
// XYZ to Lab
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
let xr = x / Xyza::D65_WHITE.x;
let yr = y / Xyza::D65_WHITE.y;
let zr = z / Xyza::D65_WHITE.z;
let fx = if xr > Lcha::CIE_EPSILON {
xr.cbrt()
} else {
(Lcha::CIE_KAPPA * xr + 16.0) / 116.0
};
let fy = if yr > Lcha::CIE_EPSILON {
yr.cbrt()
} else {
(Lcha::CIE_KAPPA * yr + 16.0) / 116.0
};
let fz = if yr > Lcha::CIE_EPSILON {
zr.cbrt()
} else {
(Lcha::CIE_KAPPA * zr + 16.0) / 116.0
};
let l = 116.0 * fy - 16.0;
let a = 500.0 * (fx - fy);
let b = 200.0 * (fy - fz);
// Lab to LCH
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
let c = (a.powf(2.0) + b.powf(2.0)).sqrt();
let h = {
let h = b.to_radians().atan2(a.to_radians()).to_degrees();
if h < 0.0 {
h + 360.0
} else {
h
}
};
let lightness = (l / 100.0).clamp(0.0, 1.5);
let chroma = (c / 100.0).clamp(0.0, 1.5);
let hue = h;
Lcha::new(lightness, chroma, hue, 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)
Xyza::from(value).into()
}
}
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)
Xyza::from(value).into()
}
}
@ -157,31 +263,6 @@ impl From<Lcha> for LinearRgba {
}
}
impl From<Lcha> for bevy_render::color::LegacyColor {
fn from(value: Lcha) -> Self {
bevy_render::color::LegacyColor::Lcha {
hue: value.hue,
chroma: value.chroma,
lightness: value.lightness,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::LegacyColor> for Lcha {
fn from(value: bevy_render::color::LegacyColor) -> Self {
match value.as_lcha() {
bevy_render::color::LegacyColor::Lcha {
hue,
chroma,
lightness,
alpha,
} => Lcha::new(hue, chroma, lightness, alpha),
_ => unreachable!(),
}
}
}
impl From<Oklaba> for Lcha {
fn from(value: Oklaba) -> Self {
Srgba::from(value).into()

View File

@ -95,8 +95,6 @@ pub use oklaba::*;
pub use srgba::*;
pub use xyza::*;
use bevy_render::color::LegacyColor;
/// Describes the traits that a color should implement for consistency.
#[allow(dead_code)] // This is an internal marker trait used to ensure that our color types impl the required traits
pub(crate) trait StandardColor
@ -108,7 +106,6 @@ where
Self: bevy_reflect::Reflect,
Self: Default,
Self: From<Color> + Into<Color>,
Self: From<LegacyColor> + Into<LegacyColor>,
Self: From<Srgba> + Into<Srgba>,
Self: From<LinearRgba> + Into<LinearRgba>,
Self: From<Hsla> + Into<Hsla>,

View File

@ -4,7 +4,6 @@ use crate::{
};
use bevy_math::Vec4;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::SrgbColorSpace;
use serde::{Deserialize, Serialize};
/// Linear RGB color with alpha.
@ -160,43 +159,6 @@ impl EuclideanDistance for LinearRgba {
}
}
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::LegacyColor {
fn from(value: LinearRgba) -> Self {
bevy_render::color::LegacyColor::RgbaLinear {
red: value.red,
green: value.green,
blue: value.blue,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::LegacyColor> for LinearRgba {
fn from(value: bevy_render::color::LegacyColor) -> Self {
match value.as_rgba_linear() {
bevy_render::color::LegacyColor::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]

View File

@ -3,7 +3,6 @@ use crate::{
StandardColor,
};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::LegacyColor;
use serde::{Deserialize, Serialize};
/// Color in Oklaba color space, with alpha
@ -165,18 +164,6 @@ impl From<Hsla> for Oklaba {
}
}
impl From<LegacyColor> for Oklaba {
fn from(value: LegacyColor) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Oklaba> for LegacyColor {
fn from(value: Oklaba) -> Self {
LinearRgba::from(value).into()
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,10 +1,10 @@
use crate::color_difference::EuclideanDistance;
use crate::oklaba::Oklaba;
use crate::{Alpha, Hsla, LinearRgba, Luminance, Mix, StandardColor};
use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor};
use bevy_math::Vec4;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::{HexColorError, HslRepresentation, SrgbColorSpace};
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Non-linear standard RGB with alpha.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
@ -177,6 +177,31 @@ impl Srgba {
a as f32 / u8::MAX as f32,
)
}
/// Converts a non-linear sRGB value to a linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
pub fn gamma_function(value: f32) -> f32 {
if value <= 0.0 {
return value;
}
if value <= 0.04045 {
value / 12.92 // linear falloff in dark values
} else {
((value + 0.055) / 1.055).powf(2.4) // gamma curve in other area
}
}
/// Converts a linear sRGB value to a non-linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
pub fn gamma_function_inverse(value: f32) -> f32 {
if value <= 0.0 {
return value;
}
if value <= 0.0031308 {
value * 12.92 // linear falloff in dark values
} else {
(1.055 * value.powf(1.0 / 2.4)) - 0.055 // gamma curve in other area
}
}
}
impl Default for Srgba {
@ -196,7 +221,7 @@ impl Luminance for Srgba {
fn with_luminance(&self, luminance: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear
.with_luminance(luminance.nonlinear_to_linear_srgb())
.with_luminance(Srgba::gamma_function(luminance))
.into()
}
@ -252,19 +277,23 @@ 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(),
red: Srgba::gamma_function_inverse(value.red),
green: Srgba::gamma_function_inverse(value.green),
blue: Srgba::gamma_function_inverse(value.blue),
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<Srgba> for LinearRgba {
#[inline]
fn from(value: Srgba) -> Self {
Self {
red: Srgba::gamma_function(value.red),
green: Srgba::gamma_function(value.green),
blue: Srgba::gamma_function(value.blue),
alpha: value.alpha,
}
}
}
@ -274,31 +303,6 @@ impl From<Oklaba> for Srgba {
}
}
impl From<Srgba> for bevy_render::color::LegacyColor {
fn from(value: Srgba) -> Self {
bevy_render::color::LegacyColor::Rgba {
red: value.red,
green: value.green,
blue: value.blue,
alpha: value.alpha,
}
}
}
impl From<bevy_render::color::LegacyColor> for Srgba {
fn from(value: bevy_render::color::LegacyColor) -> Self {
match value.as_rgba() {
bevy_render::color::LegacyColor::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]
@ -311,6 +315,20 @@ impl From<Srgba> for Vec4 {
}
}
/// Error returned if a hex string could not be parsed as a color.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum HexColorError {
/// Parsing error.
#[error("Invalid hex string")]
Parse(#[from] std::num::ParseIntError),
/// Invalid length.
#[error("Unexpected length of hex string")]
Length,
/// Invalid character.
#[error("Invalid hex char")]
Char(char),
}
#[cfg(test)]
mod tests {
use crate::testing::assert_approx_eq;

View File

@ -1,6 +1,5 @@
use crate::{Alpha, Hsla, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use crate::{Alpha, Hsla, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::color::LegacyColor;
use serde::{Deserialize, Serialize};
/// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel.
@ -62,6 +61,9 @@ impl Xyza {
pub const fn with_z(self, z: f32) -> Self {
Self { z, ..self }
}
/// [D65 White Point](https://en.wikipedia.org/wiki/Illuminant_D65#Definition)
pub const D65_WHITE: Self = Self::xyz(0.95047, 1.0, 1.08883);
}
impl Default for Xyza {
@ -184,18 +186,6 @@ impl From<Xyza> for Hsla {
}
}
impl From<Lcha> for Xyza {
fn from(value: Lcha) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for Lcha {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Oklaba> for Xyza {
fn from(value: Oklaba) -> Self {
LinearRgba::from(value).into()
@ -208,18 +198,6 @@ impl From<Xyza> for Oklaba {
}
}
impl From<LegacyColor> for Xyza {
fn from(value: LegacyColor) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for LegacyColor {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -40,6 +40,7 @@ ios_simulator = []
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }

View File

@ -1,430 +0,0 @@
pub trait SrgbColorSpace {
fn linear_to_nonlinear_srgb(self) -> Self;
fn nonlinear_to_linear_srgb(self) -> Self;
}
// source: https://entropymine.com/imageworsener/srgbformula/
impl SrgbColorSpace for f32 {
#[inline]
fn linear_to_nonlinear_srgb(self) -> f32 {
if self <= 0.0 {
return self;
}
if self <= 0.0031308 {
self * 12.92 // linear falloff in dark values
} else {
(1.055 * self.powf(1.0 / 2.4)) - 0.055 // gamma curve in other area
}
}
#[inline]
fn nonlinear_to_linear_srgb(self) -> f32 {
if self <= 0.0 {
return self;
}
if self <= 0.04045 {
self / 12.92 // linear falloff in dark values
} else {
((self + 0.055) / 1.055).powf(2.4) // gamma curve in other area
}
}
}
impl SrgbColorSpace for u8 {
#[inline]
fn linear_to_nonlinear_srgb(self) -> Self {
((self as f32 / u8::MAX as f32).linear_to_nonlinear_srgb() * u8::MAX as f32) as u8
}
#[inline]
fn nonlinear_to_linear_srgb(self) -> Self {
((self as f32 / u8::MAX as f32).nonlinear_to_linear_srgb() * u8::MAX as f32) as u8
}
}
pub struct HslRepresentation;
impl HslRepresentation {
/// converts a color in HLS space to sRGB space
#[inline]
pub fn hsl_to_nonlinear_srgb(hue: f32, saturation: f32, lightness: f32) -> [f32; 3] {
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
let hue_prime = hue / 60.0;
let largest_component = chroma * (1.0 - (hue_prime % 2.0 - 1.0).abs());
let (r_temp, g_temp, b_temp) = if hue_prime < 1.0 {
(chroma, largest_component, 0.0)
} else if hue_prime < 2.0 {
(largest_component, chroma, 0.0)
} else if hue_prime < 3.0 {
(0.0, chroma, largest_component)
} else if hue_prime < 4.0 {
(0.0, largest_component, chroma)
} else if hue_prime < 5.0 {
(largest_component, 0.0, chroma)
} else {
(chroma, 0.0, largest_component)
};
let lightness_match = lightness - chroma / 2.0;
[
r_temp + lightness_match,
g_temp + lightness_match,
b_temp + lightness_match,
]
}
/// converts a color in sRGB space to HLS space
#[inline]
pub fn nonlinear_srgb_to_hsl([red, green, blue]: [f32; 3]) -> (f32, f32, f32) {
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
let x_max = red.max(green.max(blue));
let x_min = red.min(green.min(blue));
let chroma = x_max - x_min;
let lightness = (x_max + x_min) / 2.0;
let hue = if chroma == 0.0 {
0.0
} else if red == x_max {
60.0 * (green - blue) / chroma
} else if green == x_max {
60.0 * (2.0 + (blue - red) / chroma)
} else {
60.0 * (4.0 + (red - green) / chroma)
};
let hue = if hue < 0.0 { 360.0 + hue } else { hue };
let saturation = if lightness <= 0.0 || lightness >= 1.0 {
0.0
} else {
(x_max - lightness) / lightness.min(1.0 - lightness)
};
(hue, saturation, lightness)
}
}
pub struct LchRepresentation;
impl LchRepresentation {
// References available at http://brucelindbloom.com/ in the "Math" section
// CIE Constants
// http://brucelindbloom.com/index.html?LContinuity.html (16) (17)
const CIE_EPSILON: f32 = 216.0 / 24389.0;
const CIE_KAPPA: f32 = 24389.0 / 27.0;
// D65 White Reference:
// https://en.wikipedia.org/wiki/Illuminant_D65#Definition
const D65_WHITE_X: f32 = 0.95047;
const D65_WHITE_Y: f32 = 1.0;
const D65_WHITE_Z: f32 = 1.08883;
/// converts a color in LCH space to sRGB space
#[inline]
pub fn lch_to_nonlinear_srgb(lightness: f32, chroma: f32, hue: f32) -> [f32; 3] {
let lightness = lightness * 100.0;
let chroma = chroma * 100.0;
// convert LCH to Lab
// http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html
let l = lightness;
let a = chroma * hue.to_radians().cos();
let b = chroma * hue.to_radians().sin();
// convert Lab to XYZ
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
let fy = (l + 16.0) / 116.0;
let fx = a / 500.0 + fy;
let fz = fy - b / 200.0;
let xr = {
let fx3 = fx.powf(3.0);
if fx3 > Self::CIE_EPSILON {
fx3
} else {
(116.0 * fx - 16.0) / Self::CIE_KAPPA
}
};
let yr = if l > Self::CIE_EPSILON * Self::CIE_KAPPA {
((l + 16.0) / 116.0).powf(3.0)
} else {
l / Self::CIE_KAPPA
};
let zr = {
let fz3 = fz.powf(3.0);
if fz3 > Self::CIE_EPSILON {
fz3
} else {
(116.0 * fz - 16.0) / Self::CIE_KAPPA
}
};
let x = xr * Self::D65_WHITE_X;
let y = yr * Self::D65_WHITE_Y;
let z = zr * Self::D65_WHITE_Z;
// XYZ to sRGB
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, XYZ to RGB [M]-1)
let red = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
let green = x * -0.969266 + y * 1.8760108 + z * 0.041556;
let blue = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
[
red.linear_to_nonlinear_srgb().clamp(0.0, 1.0),
green.linear_to_nonlinear_srgb().clamp(0.0, 1.0),
blue.linear_to_nonlinear_srgb().clamp(0.0, 1.0),
]
}
/// converts a color in sRGB space to LCH space
#[inline]
pub fn nonlinear_srgb_to_lch([red, green, blue]: [f32; 3]) -> (f32, f32, f32) {
// RGB to XYZ
// http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
let red = red.nonlinear_to_linear_srgb();
let green = green.nonlinear_to_linear_srgb();
let blue = blue.nonlinear_to_linear_srgb();
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, RGB to XYZ [M])
let x = red * 0.4124564 + green * 0.3575761 + blue * 0.1804375;
let y = red * 0.2126729 + green * 0.7151522 + blue * 0.072175;
let z = red * 0.0193339 + green * 0.119192 + blue * 0.9503041;
// XYZ to Lab
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
let xr = x / Self::D65_WHITE_X;
let yr = y / Self::D65_WHITE_Y;
let zr = z / Self::D65_WHITE_Z;
let fx = if xr > Self::CIE_EPSILON {
xr.cbrt()
} else {
(Self::CIE_KAPPA * xr + 16.0) / 116.0
};
let fy = if yr > Self::CIE_EPSILON {
yr.cbrt()
} else {
(Self::CIE_KAPPA * yr + 16.0) / 116.0
};
let fz = if yr > Self::CIE_EPSILON {
zr.cbrt()
} else {
(Self::CIE_KAPPA * zr + 16.0) / 116.0
};
let l = 116.0 * fy - 16.0;
let a = 500.0 * (fx - fy);
let b = 200.0 * (fy - fz);
// Lab to LCH
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
let c = (a.powf(2.0) + b.powf(2.0)).sqrt();
let h = {
let h = b.to_radians().atan2(a.to_radians()).to_degrees();
if h < 0.0 {
h + 360.0
} else {
h
}
};
((l / 100.0).clamp(0.0, 1.5), (c / 100.0).clamp(0.0, 1.5), h)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn srgb_linear_full_roundtrip() {
let u8max: f32 = u8::MAX as f32;
for color in 0..u8::MAX {
let color01 = color as f32 / u8max;
let color_roundtrip = color01
.linear_to_nonlinear_srgb()
.nonlinear_to_linear_srgb();
// roundtrip is not perfect due to numeric precision, even with f64
// so ensure the error is at least ready for u8 (where sRGB is used)
assert_eq!(
(color01 * u8max).round() as u8,
(color_roundtrip * u8max).round() as u8
);
}
}
#[test]
fn hsl_to_srgb() {
// "truth" from https://en.wikipedia.org/wiki/HSL_and_HSV#Examples
// black
let (hue, saturation, lightness) = (0.0, 0.0, 0.0);
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
assert_eq!((r * 100.0).round() as u32, 0);
assert_eq!((g * 100.0).round() as u32, 0);
assert_eq!((b * 100.0).round() as u32, 0);
// white
let (hue, saturation, lightness) = (0.0, 0.0, 1.0);
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
assert_eq!((r * 100.0).round() as u32, 100);
assert_eq!((g * 100.0).round() as u32, 100);
assert_eq!((b * 100.0).round() as u32, 100);
let (hue, saturation, lightness) = (300.0, 0.5, 0.5);
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
assert_eq!((r * 100.0).round() as u32, 75);
assert_eq!((g * 100.0).round() as u32, 25);
assert_eq!((b * 100.0).round() as u32, 75);
// a red
let (hue, saturation, lightness) = (283.7, 0.775, 0.543);
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
assert_eq!((r * 100.0).round() as u32, 70);
assert_eq!((g * 100.0).round() as u32, 19);
assert_eq!((b * 100.0).round() as u32, 90);
// a green
let (hue, saturation, lightness) = (162.4, 0.779, 0.447);
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
assert_eq!((r * 100.0).round() as u32, 10);
assert_eq!((g * 100.0).round() as u32, 80);
assert_eq!((b * 100.0).round() as u32, 59);
// a blue
let (hue, saturation, lightness) = (251.1, 0.832, 0.511);
let [r, g, b] = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
assert_eq!((r * 100.0).round() as u32, 25);
assert_eq!((g * 100.0).round() as u32, 10);
assert_eq!((b * 100.0).round() as u32, 92);
}
#[test]
fn srgb_to_hsl() {
// "truth" from https://en.wikipedia.org/wiki/HSL_and_HSV#Examples
// black
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([0.0, 0.0, 0.0]);
assert_eq!(hue.round() as u32, 0);
assert_eq!((saturation * 100.0).round() as u32, 0);
assert_eq!((lightness * 100.0).round() as u32, 0);
// white
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([1.0, 1.0, 1.0]);
assert_eq!(hue.round() as u32, 0);
assert_eq!((saturation * 100.0).round() as u32, 0);
assert_eq!((lightness * 100.0).round() as u32, 100);
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([0.75, 0.25, 0.75]);
assert_eq!(hue.round() as u32, 300);
assert_eq!((saturation * 100.0).round() as u32, 50);
assert_eq!((lightness * 100.0).round() as u32, 50);
// a red
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([0.704, 0.187, 0.897]);
assert_eq!(hue.round() as u32, 284);
assert_eq!((saturation * 100.0).round() as u32, 78);
assert_eq!((lightness * 100.0).round() as u32, 54);
// a green
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([0.099, 0.795, 0.591]);
assert_eq!(hue.round() as u32, 162);
assert_eq!((saturation * 100.0).round() as u32, 78);
assert_eq!((lightness * 100.0).round() as u32, 45);
// a blue
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([0.255, 0.104, 0.918]);
assert_eq!(hue.round() as u32, 251);
assert_eq!((saturation * 100.0).round() as u32, 83);
assert_eq!((lightness * 100.0).round() as u32, 51);
}
#[test]
fn lch_to_srgb() {
// "truth" from http://www.brucelindbloom.com/ColorCalculator.html
// black
let (lightness, chroma, hue) = (0.0, 0.0, 0.0);
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
assert_eq!((r * 100.0).round() as u32, 0);
assert_eq!((g * 100.0).round() as u32, 0);
assert_eq!((b * 100.0).round() as u32, 0);
// white
let (lightness, chroma, hue) = (1.0, 0.0, 0.0);
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
assert_eq!((r * 100.0).round() as u32, 100);
assert_eq!((g * 100.0).round() as u32, 100);
assert_eq!((b * 100.0).round() as u32, 100);
let (lightness, chroma, hue) = (0.501236, 0.777514, 327.6608);
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
assert_eq!((r * 100.0).round() as u32, 75);
assert_eq!((g * 100.0).round() as u32, 25);
assert_eq!((b * 100.0).round() as u32, 75);
// a red
let (lightness, chroma, hue) = (0.487122, 0.999531, 318.7684);
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
assert_eq!((r * 100.0).round() as u32, 70);
assert_eq!((g * 100.0).round() as u32, 19);
assert_eq!((b * 100.0).round() as u32, 90);
// a green
let (lightness, chroma, hue) = (0.732929, 0.560925, 164.3216);
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
assert_eq!((r * 100.0).round() as u32, 10);
assert_eq!((g * 100.0).round() as u32, 80);
assert_eq!((b * 100.0).round() as u32, 59);
// a blue
let (lightness, chroma, hue) = (0.335030, 1.176923, 306.7828);
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
assert_eq!((r * 100.0).round() as u32, 25);
assert_eq!((g * 100.0).round() as u32, 10);
assert_eq!((b * 100.0).round() as u32, 92);
}
#[test]
fn srgb_to_lch() {
// "truth" from http://www.brucelindbloom.com/ColorCalculator.html
// black
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.0, 0.0, 0.0]);
assert_eq!((lightness * 100.0).round() as u32, 0);
assert_eq!((chroma * 100.0).round() as u32, 0);
assert_eq!(hue.round() as u32, 0);
// white
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([1.0, 1.0, 1.0]);
assert_eq!((lightness * 100.0).round() as u32, 100);
assert_eq!((chroma * 100.0).round() as u32, 0);
assert_eq!(hue.round() as u32, 0);
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.75, 0.25, 0.75]);
assert_eq!((lightness * 100.0).round() as u32, 50);
assert_eq!((chroma * 100.0).round() as u32, 78);
assert_eq!(hue.round() as u32, 328);
// a red
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.70, 0.19, 0.90]);
assert_eq!((lightness * 100.0).round() as u32, 49);
assert_eq!((chroma * 100.0).round() as u32, 100);
assert_eq!(hue.round() as u32, 319);
// a green
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.10, 0.80, 0.59]);
assert_eq!((lightness * 100.0).round() as u32, 73);
assert_eq!((chroma * 100.0).round() as u32, 56);
assert_eq!(hue.round() as u32, 164);
// a blue
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.25, 0.10, 0.92]);
assert_eq!((lightness * 100.0).round() as u32, 34);
assert_eq!((chroma * 100.0).round() as u32, 118);
assert_eq!(hue.round() as u32, 307);
}
}

View File

@ -1,12 +1,9 @@
mod colorspace;
pub use colorspace::*;
use bevy_color::{Color, HexColorError, Hsla, Lcha, LinearRgba, Oklaba, Srgba, Xyza};
use bevy_math::{Vec3, Vec4};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
use std::ops::{Add, Mul, MulAssign};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
@ -304,32 +301,7 @@ impl LegacyColor {
/// ```
///
pub fn hex<T: AsRef<str>>(hex: T) -> Result<LegacyColor, 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(LegacyColor::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(LegacyColor::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(LegacyColor::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(LegacyColor::rgba_u8(r, g, b, a))
}
_ => Err(HexColorError::Length),
}
Srgba::hex(hex).map(|color| color.into())
}
/// New `Color` from sRGB colorspace.
@ -570,211 +542,22 @@ impl LegacyColor {
/// Converts a `Color` to variant `LegacyColor::Rgba`
pub fn as_rgba(self: &LegacyColor) -> LegacyColor {
match self {
LegacyColor::Rgba { .. } => *self,
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => LegacyColor::Rgba {
red: red.linear_to_nonlinear_srgb(),
green: green.linear_to_nonlinear_srgb(),
blue: blue.linear_to_nonlinear_srgb(),
alpha: *alpha,
},
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let [red, green, blue] =
HslRepresentation::hsl_to_nonlinear_srgb(*hue, *saturation, *lightness);
LegacyColor::Rgba {
red,
green,
blue,
alpha: *alpha,
}
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let [red, green, blue] =
LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue);
LegacyColor::Rgba {
red,
green,
blue,
alpha: *alpha,
}
}
}
Srgba::from(*self).into()
}
/// Converts a `Color` to variant `LegacyColor::RgbaLinear`
pub fn as_rgba_linear(self: &LegacyColor) -> LegacyColor {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => LegacyColor::RgbaLinear {
red: red.nonlinear_to_linear_srgb(),
green: green.nonlinear_to_linear_srgb(),
blue: blue.nonlinear_to_linear_srgb(),
alpha: *alpha,
},
LegacyColor::RgbaLinear { .. } => *self,
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let [red, green, blue] =
HslRepresentation::hsl_to_nonlinear_srgb(*hue, *saturation, *lightness);
LegacyColor::RgbaLinear {
red: red.nonlinear_to_linear_srgb(),
green: green.nonlinear_to_linear_srgb(),
blue: blue.nonlinear_to_linear_srgb(),
alpha: *alpha,
}
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let [red, green, blue] =
LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue);
LegacyColor::RgbaLinear {
red: red.nonlinear_to_linear_srgb(),
green: green.nonlinear_to_linear_srgb(),
blue: blue.nonlinear_to_linear_srgb(),
alpha: *alpha,
}
}
}
LinearRgba::from(*self).into()
}
/// Converts a `Color` to variant `LegacyColor::Hsla`
pub fn as_hsla(self: &LegacyColor) -> LegacyColor {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => {
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([*red, *green, *blue]);
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha: *alpha,
}
}
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => {
let (hue, saturation, lightness) = HslRepresentation::nonlinear_srgb_to_hsl([
red.linear_to_nonlinear_srgb(),
green.linear_to_nonlinear_srgb(),
blue.linear_to_nonlinear_srgb(),
]);
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha: *alpha,
}
}
LegacyColor::Hsla { .. } => *self,
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let rgb = LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue);
let (hue, saturation, lightness) = HslRepresentation::nonlinear_srgb_to_hsl(rgb);
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha: *alpha,
}
}
}
Hsla::from(*self).into()
}
/// Converts a `Color` to variant `LegacyColor::Lcha`
pub fn as_lcha(self: &LegacyColor) -> LegacyColor {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => {
let (lightness, chroma, hue) =
LchRepresentation::nonlinear_srgb_to_lch([*red, *green, *blue]);
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha: *alpha,
}
}
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => {
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([
red.linear_to_nonlinear_srgb(),
green.linear_to_nonlinear_srgb(),
blue.linear_to_nonlinear_srgb(),
]);
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha: *alpha,
}
}
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let rgb = HslRepresentation::hsl_to_nonlinear_srgb(*hue, *saturation, *lightness);
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch(rgb);
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha: *alpha,
}
}
LegacyColor::Lcha { .. } => *self,
}
Lcha::from(*self).into()
}
/// Converts a `Color` to a `[u8; 4]` from sRGB colorspace
@ -790,193 +573,47 @@ impl LegacyColor {
/// Converts a `Color` to a `[f32; 4]` from sRGB colorspace
pub fn as_rgba_f32(self: LegacyColor) -> [f32; 4] {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => [red, green, blue, alpha],
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => [
red.linear_to_nonlinear_srgb(),
green.linear_to_nonlinear_srgb(),
blue.linear_to_nonlinear_srgb(),
alpha,
],
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let [red, green, blue] =
HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
[red, green, blue, alpha]
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let [red, green, blue] =
LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
[red, green, blue, alpha]
}
}
let Srgba {
red,
green,
blue,
alpha,
} = Srgba::from(self);
[red, green, blue, alpha]
}
/// Converts a `Color` to a `[f32; 4]` from linear RGB colorspace
#[inline]
pub fn as_linear_rgba_f32(self: LegacyColor) -> [f32; 4] {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => [
red.nonlinear_to_linear_srgb(),
green.nonlinear_to_linear_srgb(),
blue.nonlinear_to_linear_srgb(),
alpha,
],
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => [red, green, blue, alpha],
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let [red, green, blue] =
HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
[
red.nonlinear_to_linear_srgb(),
green.nonlinear_to_linear_srgb(),
blue.nonlinear_to_linear_srgb(),
alpha,
]
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let [red, green, blue] =
LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
[
red.nonlinear_to_linear_srgb(),
green.nonlinear_to_linear_srgb(),
blue.nonlinear_to_linear_srgb(),
alpha,
]
}
}
let LinearRgba {
red,
green,
blue,
alpha,
} = LinearRgba::from(self);
[red, green, blue, alpha]
}
/// Converts a `Color` to a `[f32; 4]` from HSL colorspace
pub fn as_hsla_f32(self: LegacyColor) -> [f32; 4] {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => {
let (hue, saturation, lightness) =
HslRepresentation::nonlinear_srgb_to_hsl([red, green, blue]);
[hue, saturation, lightness, alpha]
}
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => {
let (hue, saturation, lightness) = HslRepresentation::nonlinear_srgb_to_hsl([
red.linear_to_nonlinear_srgb(),
green.linear_to_nonlinear_srgb(),
blue.linear_to_nonlinear_srgb(),
]);
[hue, saturation, lightness, alpha]
}
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => [hue, saturation, lightness, alpha],
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let rgb = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
let (hue, saturation, lightness) = HslRepresentation::nonlinear_srgb_to_hsl(rgb);
[hue, saturation, lightness, alpha]
}
}
let Hsla {
hue,
saturation,
lightness,
alpha,
} = Hsla::from(self);
[hue, saturation, lightness, alpha]
}
/// Converts a `Color` to a `[f32; 4]` from LCH colorspace
pub fn as_lcha_f32(self: LegacyColor) -> [f32; 4] {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => {
let (lightness, chroma, hue) =
LchRepresentation::nonlinear_srgb_to_lch([red, green, blue]);
[lightness, chroma, hue, alpha]
}
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => {
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([
red.linear_to_nonlinear_srgb(),
green.linear_to_nonlinear_srgb(),
blue.linear_to_nonlinear_srgb(),
]);
[lightness, chroma, hue, alpha]
}
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let rgb = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch(rgb);
[lightness, chroma, hue, alpha]
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => [lightness, chroma, hue, alpha],
}
let Lcha {
lightness,
chroma,
hue,
alpha,
} = Lcha::from(self);
[lightness, chroma, hue, alpha]
}
/// Converts `Color` to a `u32` from sRGB colorspace.
@ -984,61 +621,7 @@ impl LegacyColor {
/// Maps the RGBA channels in RGBA order to a little-endian byte array (GPUs are little-endian).
/// `A` will be the most significant byte and `R` the least significant.
pub fn as_rgba_u32(self: LegacyColor) -> u32 {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => u32::from_le_bytes([
(red * 255.0) as u8,
(green * 255.0) as u8,
(blue * 255.0) as u8,
(alpha * 255.0) as u8,
]),
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => u32::from_le_bytes([
(red.linear_to_nonlinear_srgb() * 255.0) as u8,
(green.linear_to_nonlinear_srgb() * 255.0) as u8,
(blue.linear_to_nonlinear_srgb() * 255.0) as u8,
(alpha * 255.0) as u8,
]),
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let [red, green, blue] =
HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
u32::from_le_bytes([
(red * 255.0) as u8,
(green * 255.0) as u8,
(blue * 255.0) as u8,
(alpha * 255.0) as u8,
])
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let [red, green, blue] =
LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
u32::from_le_bytes([
(red * 255.0) as u8,
(green * 255.0) as u8,
(blue * 255.0) as u8,
(alpha * 255.0) as u8,
])
}
}
u32::from_le_bytes(self.as_rgba_u8())
}
/// Converts Color to a u32 from linear RGB colorspace.
@ -1046,61 +629,18 @@ impl LegacyColor {
/// Maps the RGBA channels in RGBA order to a little-endian byte array (GPUs are little-endian).
/// `A` will be the most significant byte and `R` the least significant.
pub fn as_linear_rgba_u32(self: LegacyColor) -> u32 {
match self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => u32::from_le_bytes([
(red.nonlinear_to_linear_srgb() * 255.0) as u8,
(green.nonlinear_to_linear_srgb() * 255.0) as u8,
(blue.nonlinear_to_linear_srgb() * 255.0) as u8,
(alpha * 255.0) as u8,
]),
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => u32::from_le_bytes([
(red * 255.0) as u8,
(green * 255.0) as u8,
(blue * 255.0) as u8,
(alpha * 255.0) as u8,
]),
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => {
let [red, green, blue] =
HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness);
u32::from_le_bytes([
(red.nonlinear_to_linear_srgb() * 255.0) as u8,
(green.nonlinear_to_linear_srgb() * 255.0) as u8,
(blue.nonlinear_to_linear_srgb() * 255.0) as u8,
(alpha * 255.0) as u8,
])
}
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => {
let [red, green, blue] =
LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
u32::from_le_bytes([
(red.nonlinear_to_linear_srgb() * 255.0) as u8,
(green.nonlinear_to_linear_srgb() * 255.0) as u8,
(blue.nonlinear_to_linear_srgb() * 255.0) as u8,
(alpha * 255.0) as u8,
])
}
}
let LinearRgba {
red,
green,
blue,
alpha,
} = self.into();
u32::from_le_bytes([
(red * 255.0) as u8,
(green * 255.0) as u8,
(blue * 255.0) as u8,
(alpha * 255.0) as u8,
])
}
/// New `Color` from `[f32; 4]` (or a type that can be converted into them) with RGB representation in sRGB colorspace.
@ -1345,6 +885,163 @@ impl Add<LegacyColor> for LegacyColor {
}
}
impl From<LegacyColor> for Color {
fn from(value: LegacyColor) -> Self {
match value {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
} => Srgba::new(red, green, blue, alpha).into(),
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
} => LinearRgba::new(red, green, blue, alpha).into(),
LegacyColor::Hsla {
hue,
saturation,
lightness,
alpha,
} => Hsla::new(hue, saturation, lightness, alpha).into(),
LegacyColor::Lcha {
lightness,
chroma,
hue,
alpha,
} => Lcha::new(lightness, chroma, hue, alpha).into(),
}
}
}
impl From<Color> for LegacyColor {
fn from(value: Color) -> Self {
match value {
Color::Srgba(x) => x.into(),
Color::LinearRgba(x) => x.into(),
Color::Hsla(x) => x.into(),
Color::Lcha(x) => x.into(),
Color::Oklaba(x) => x.into(),
Color::Xyza(x) => x.into(),
}
}
}
impl From<LinearRgba> for LegacyColor {
fn from(
LinearRgba {
red,
green,
blue,
alpha,
}: LinearRgba,
) -> Self {
LegacyColor::RgbaLinear {
red,
green,
blue,
alpha,
}
}
}
impl From<LegacyColor> for Xyza {
fn from(value: LegacyColor) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Xyza> for LegacyColor {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<LegacyColor> for LinearRgba {
fn from(value: LegacyColor) -> Self {
Color::from(value).into()
}
}
impl From<Srgba> for LegacyColor {
fn from(
Srgba {
red,
green,
blue,
alpha,
}: Srgba,
) -> Self {
LegacyColor::Rgba {
red,
green,
blue,
alpha,
}
}
}
impl From<LegacyColor> for Srgba {
fn from(value: LegacyColor) -> Self {
Color::from(value).into()
}
}
impl From<Hsla> for LegacyColor {
fn from(value: Hsla) -> Self {
LegacyColor::Hsla {
hue: value.hue,
saturation: value.saturation,
lightness: value.lightness,
alpha: value.alpha,
}
}
}
impl From<LegacyColor> for Hsla {
fn from(value: LegacyColor) -> Self {
Color::from(value).into()
}
}
impl From<Lcha> for LegacyColor {
fn from(
Lcha {
lightness,
chroma,
hue,
alpha,
}: Lcha,
) -> Self {
LegacyColor::Lcha {
hue,
chroma,
lightness,
alpha,
}
}
}
impl From<LegacyColor> for Lcha {
fn from(value: LegacyColor) -> Self {
Color::from(value).into()
}
}
impl From<LegacyColor> for Oklaba {
fn from(value: LegacyColor) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Oklaba> for LegacyColor {
fn from(value: Oklaba) -> Self {
LinearRgba::from(value).into()
}
}
impl From<LegacyColor> for wgpu::Color {
fn from(color: LegacyColor) -> Self {
if let LegacyColor::RgbaLinear {
@ -1904,56 +1601,10 @@ impl encase::private::CreateFrom for LegacyColor {
impl encase::ShaderSize for LegacyColor {}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum HexColorError {
#[error("Invalid hex string")]
Parse(#[from] std::num::ParseIntError),
#[error("Unexpected length of hex string")]
Length,
#[error("Invalid hex char")]
Char(char),
}
/// 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 std::num::ParseIntError;
use super::*;
#[test]
@ -1971,7 +1622,9 @@ mod tests {
Ok(LegacyColor::rgb_u8(3, 169, 244))
);
assert_eq!(LegacyColor::hex("yy"), Err(HexColorError::Length));
assert_eq!(LegacyColor::hex("yyy"), Err(HexColorError::Char('y')));
let Err(HexColorError::Parse(ParseIntError { .. })) = LegacyColor::hex("yyy") else {
panic!("Expected Parse Int Error")
};
assert_eq!(
LegacyColor::hex("#f2a"),
Ok(LegacyColor::rgb_u8(255, 34, 170))
@ -1981,7 +1634,9 @@ mod tests {
Ok(LegacyColor::rgb_u8(226, 48, 48))
);
assert_eq!(LegacyColor::hex("#ff"), Err(HexColorError::Length));
assert_eq!(LegacyColor::hex("##fff"), Err(HexColorError::Char('#')));
let Err(HexColorError::Parse(ParseIntError { .. })) = LegacyColor::hex("##fff") else {
panic!("Expected Parse Int Error")
};
}
#[test]

View File

@ -1,11 +1,11 @@
#[cfg(any(feature = "flate2", feature = "ruzstd"))]
use std::io::Read;
use crate::color::SrgbColorSpace;
#[cfg(feature = "basis-universal")]
use basis_universal::{
DecodeFlags, LowLevelUastcTranscoder, SliceParametersUastc, TranscoderBlockFormat,
};
use bevy_color::Srgba;
use bevy_utils::default;
#[cfg(any(feature = "flate2", feature = "ruzstd"))]
use ktx2::SupercompressionScheme;
@ -95,7 +95,7 @@ pub fn ktx2_buffer_to_image(
level_data
.iter()
.copied()
.map(|v| v.nonlinear_to_linear_srgb())
.map(|v| (Srgba::gamma_function(v as f32 / 255.) * 255.).floor() as u8)
.collect::<Vec<u8>>(),
);
@ -114,7 +114,7 @@ pub fn ktx2_buffer_to_image(
level_data
.iter()
.copied()
.map(|v| v.nonlinear_to_linear_srgb())
.map(|v| (Srgba::gamma_function(v as f32 / 255.) * 255.).floor() as u8)
.collect::<Vec<u8>>(),
);