diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index a5f8a6f0ac..908198a915 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -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 diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 106ccfa6ff..22f98fd502 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -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 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 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 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 for Xyza { } } } - -impl From 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 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(), - } - } -} diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 07e350b6b4..daede82879 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -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 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 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 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 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) } } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index c2ed3d1018..c56995a397 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -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 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 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 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 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 for LinearRgba { } } -impl From 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 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 for Lcha { fn from(value: Oklaba) -> Self { Srgba::from(value).into() diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 627cd6f195..857bb2a02d 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -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 + Into, - Self: From + Into, Self: From + Into, Self: From + Into, Self: From + Into, diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index 347e6b295d..0c3375b5a8 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -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 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 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 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 for [f32; 4] { fn from(color: LinearRgba) -> Self { [color.red, color.green, color.blue, color.alpha] diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index f109d7529c..dceac2157e 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -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 for Oklaba { } } -impl From for Oklaba { - fn from(value: LegacyColor) -> Self { - LinearRgba::from(value).into() - } -} - -impl From for LegacyColor { - fn from(value: Oklaba) -> Self { - LinearRgba::from(value).into() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index dcfc954a6b..761496d334 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -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 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 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 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 for Srgba { } } -impl From 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 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 for [f32; 4] { fn from(color: Srgba) -> Self { [color.red, color.green, color.blue, color.alpha] @@ -311,6 +315,20 @@ impl From 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; diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index 9445e1705e..6a11749852 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -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 for Hsla { } } -impl From for Xyza { - fn from(value: Lcha) -> Self { - LinearRgba::from(value).into() - } -} - -impl From for Lcha { - fn from(value: Xyza) -> Self { - LinearRgba::from(value).into() - } -} - impl From for Xyza { fn from(value: Oklaba) -> Self { LinearRgba::from(value).into() @@ -208,18 +198,6 @@ impl From for Oklaba { } } -impl From for Xyza { - fn from(value: LegacyColor) -> Self { - LinearRgba::from(value).into() - } -} - -impl From for LegacyColor { - fn from(value: Xyza) -> Self { - LinearRgba::from(value).into() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index a89bf813ed..7903dd8d08 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -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" } diff --git a/crates/bevy_render/src/color/colorspace.rs b/crates/bevy_render/src/color/colorspace.rs deleted file mode 100644 index 362a25d2a4..0000000000 --- a/crates/bevy_render/src/color/colorspace.rs +++ /dev/null @@ -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); - } -} diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index cf92cf54f8..2a5ddd9325 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -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>(hex: T) -> Result { - 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 for LegacyColor { } } +impl From 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 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 for LegacyColor { + fn from( + LinearRgba { + red, + green, + blue, + alpha, + }: LinearRgba, + ) -> Self { + LegacyColor::RgbaLinear { + red, + green, + blue, + alpha, + } + } +} + +impl From for Xyza { + fn from(value: LegacyColor) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for LegacyColor { + fn from(value: Xyza) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for LinearRgba { + fn from(value: LegacyColor) -> Self { + Color::from(value).into() + } +} + +impl From for LegacyColor { + fn from( + Srgba { + red, + green, + blue, + alpha, + }: Srgba, + ) -> Self { + LegacyColor::Rgba { + red, + green, + blue, + alpha, + } + } +} + +impl From for Srgba { + fn from(value: LegacyColor) -> Self { + Color::from(value).into() + } +} + +impl From for LegacyColor { + fn from(value: Hsla) -> Self { + LegacyColor::Hsla { + hue: value.hue, + saturation: value.saturation, + lightness: value.lightness, + alpha: value.alpha, + } + } +} + +impl From for Hsla { + fn from(value: LegacyColor) -> Self { + Color::from(value).into() + } +} + +impl From for LegacyColor { + fn from( + Lcha { + lightness, + chroma, + hue, + alpha, + }: Lcha, + ) -> Self { + LegacyColor::Lcha { + hue, + chroma, + lightness, + alpha, + } + } +} + +impl From for Lcha { + fn from(value: LegacyColor) -> Self { + Color::from(value).into() + } +} + +impl From for Oklaba { + fn from(value: LegacyColor) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for LegacyColor { + fn from(value: Oklaba) -> Self { + LinearRgba::from(value).into() + } +} + impl From 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(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 { - 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] diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_render/src/texture/ktx2.rs index 2ebf4d6c7e..8318a6fad7 100644 --- a/crates/bevy_render/src/texture/ktx2.rs +++ b/crates/bevy_render/src/texture/ktx2.rs @@ -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::>(), ); @@ -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::>(), );