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:
parent
e5994a4e55
commit
5e63f6815b
@ -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
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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>>(),
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user