From 6197d90f543586c3586c05719a60c456cac3c494 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 5 May 2026 19:43:08 +0800 Subject: [PATCH 01/11] Add Okhsla and Okhsva to Color --- crates/bevy_color/src/color.rs | 93 +++- crates/bevy_color/src/lib.rs | 5 + crates/bevy_color/src/okhsla.rs | 774 ++++++++++++++++++++++++++++++++ crates/bevy_color/src/okhsva.rs | 469 +++++++++++++++++++ 4 files changed, 1339 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_color/src/okhsla.rs create mode 100644 crates/bevy_color/src/okhsva.rs diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 7e185ece653d6..9134a900a5d2a 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -1,6 +1,7 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, - Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, okhsla::Okhsla, okhsva::Okhsva, Alpha, Hsla, Hsva, Hue, + Hwba, Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, + Xyza, }; use bevy_math::{MismatchedUnitsError, TryStableInterpolate}; #[cfg(feature = "bevy_reflect")] @@ -74,6 +75,10 @@ pub enum Color { Oklcha(Oklcha), /// A color in the XYZ color space with alpha. Xyza(Xyza), + /// + Okhsla(Okhsla), + /// + Okhsva(Okhsva), } impl StandardColor for Color {} @@ -531,6 +536,8 @@ impl Alpha for Color { Color::Oklaba(x) => *x = x.with_alpha(alpha), Color::Oklcha(x) => *x = x.with_alpha(alpha), Color::Xyza(x) => *x = x.with_alpha(alpha), + Color::Okhsla(x) => *x = x.with_alpha(alpha), + Color::Okhsva(x) => *x = x.with_alpha(alpha), } new @@ -548,6 +555,8 @@ impl Alpha for Color { Color::Oklaba(x) => x.alpha(), Color::Oklcha(x) => x.alpha(), Color::Xyza(x) => x.alpha(), + Color::Okhsla(x) => x.alpha(), + Color::Okhsva(x) => x.alpha(), } } @@ -563,6 +572,8 @@ impl Alpha for Color { Color::Oklaba(x) => x.set_alpha(alpha), Color::Oklcha(x) => x.set_alpha(alpha), Color::Xyza(x) => x.set_alpha(alpha), + Color::Okhsla(x) => x.set_alpha(alpha), + Color::Okhsva(x) => x.set_alpha(alpha), } } } @@ -580,6 +591,8 @@ impl From for Srgba { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -597,6 +610,8 @@ impl From for LinearRgba { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -614,6 +629,8 @@ impl From for Hsla { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -631,6 +648,8 @@ impl From for Hsva { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -648,6 +667,8 @@ impl From for Hwba { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -665,6 +686,8 @@ impl From for Laba { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -682,6 +705,8 @@ impl From for Lcha { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -699,6 +724,8 @@ impl From for Oklaba { Color::Oklaba(oklab) => oklab, Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -716,6 +743,8 @@ impl From for Oklcha { Color::Oklaba(oklab) => oklab.into(), Color::Oklcha(oklch) => oklch, Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), } } } @@ -733,6 +762,46 @@ impl From for Xyza { Color::Oklaba(x) => x.into(), Color::Oklcha(oklch) => oklch.into(), Color::Xyza(xyza) => xyza, + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv.into(), + } + } +} + +impl From for Okhsla { + fn from(value: Color) -> Self { + match value { + Color::Srgba(x) => x.into(), + Color::LinearRgba(x) => x.into(), + Color::Hsla(x) => x.into(), + Color::Hsva(hsva) => hsva.into(), + Color::Hwba(hwba) => hwba.into(), + Color::Laba(laba) => laba.into(), + Color::Lcha(x) => x.into(), + Color::Oklaba(x) => x.into(), + Color::Oklcha(oklch) => oklch.into(), + Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl, + Color::Okhsva(okhsv) => okhsv.into(), + } + } +} + +impl From for Okhsva { + fn from(value: Color) -> Self { + match value { + Color::Srgba(x) => x.into(), + Color::LinearRgba(x) => x.into(), + Color::Hsla(x) => x.into(), + Color::Hsva(hsva) => hsva.into(), + Color::Hwba(hwba) => hwba.into(), + Color::Laba(laba) => laba.into(), + Color::Lcha(x) => x.into(), + Color::Oklaba(x) => x.into(), + Color::Oklcha(oklch) => oklch.into(), + Color::Xyza(xyza) => xyza.into(), + Color::Okhsla(okhsl) => okhsl.into(), + Color::Okhsva(okhsv) => okhsv, } } } @@ -753,6 +822,8 @@ impl Luminance for Color { Color::Oklaba(x) => x.luminance(), Color::Oklcha(x) => x.luminance(), Color::Xyza(x) => x.luminance(), + Color::Okhsla(x) => x.luminance(), + Color::Okhsva(x) => ChosenColorSpace::from(*x).luminance(), } } @@ -770,6 +841,8 @@ impl Luminance for Color { Color::Oklaba(x) => *x = x.with_luminance(value), Color::Oklcha(x) => *x = x.with_luminance(value), Color::Xyza(x) => *x = x.with_luminance(value), + Color::Okhsla(x) => *x = x.with_luminance(value), + Color::Okhsva(x) => *x = ChosenColorSpace::from(*x).with_luminance(value).into(), } new @@ -789,6 +862,8 @@ impl Luminance for Color { Color::Oklaba(x) => *x = x.darker(amount), Color::Oklcha(x) => *x = x.darker(amount), Color::Xyza(x) => *x = x.darker(amount), + Color::Okhsla(x) => *x = x.darker(amount), + Color::Okhsva(x) => *x = ChosenColorSpace::from(*x).darker(amount).into(), } new @@ -808,6 +883,8 @@ impl Luminance for Color { Color::Oklaba(x) => *x = x.lighter(amount), Color::Oklcha(x) => *x = x.lighter(amount), Color::Xyza(x) => *x = x.lighter(amount), + Color::Okhsla(x) => *x = x.lighter(amount), + Color::Okhsva(x) => *x = ChosenColorSpace::from(*x).lighter(amount).into(), } new @@ -829,6 +906,8 @@ impl Hue for Color { Color::Oklaba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), Color::Oklcha(x) => *x = x.with_hue(hue), Color::Xyza(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), + Color::Okhsla(x) => *x = x.with_hue(hue).into(), + Color::Okhsva(x) => *x = x.with_hue(hue).into(), } new @@ -846,6 +925,8 @@ impl Hue for Color { Color::Oklaba(x) => ChosenColorSpace::from(*x).hue(), Color::Oklcha(x) => x.hue(), Color::Xyza(x) => ChosenColorSpace::from(*x).hue(), + Color::Okhsla(x) => x.hue(), + Color::Okhsva(x) => x.hue(), } } @@ -869,6 +950,8 @@ impl Saturation for Color { Color::Oklaba(x) => Hsla::from(*x).with_saturation(saturation).into(), Color::Oklcha(x) => Hsla::from(*x).with_saturation(saturation).into(), Color::Xyza(x) => Hsla::from(*x).with_saturation(saturation).into(), + Color::Okhsla(x) => x.with_saturation(saturation).into(), + Color::Okhsva(x) => x.with_saturation(saturation).into(), } } @@ -884,6 +967,8 @@ impl Saturation for Color { Color::Oklaba(x) => Hsla::from(*x).saturation(), Color::Oklcha(x) => Hsla::from(*x).saturation(), Color::Xyza(x) => Hsla::from(*x).saturation(), + Color::Okhsla(x) => x.saturation(), + Color::Okhsva(x) => x.saturation(), } } @@ -907,6 +992,8 @@ impl Mix for Color { Color::Oklaba(x) => *x = x.mix(&(*other).into(), factor), Color::Oklcha(x) => *x = x.mix(&(*other).into(), factor), Color::Xyza(x) => *x = x.mix(&(*other).into(), factor), + Color::Okhsla(x) => *x = x.mix(&(*other).into(), factor), + Color::Okhsva(x) => *x = x.mix(&(*other).into(), factor), } new @@ -926,6 +1013,8 @@ impl EuclideanDistance for Color { Color::Oklaba(x) => x.distance_squared(&(*other).into()), Color::Oklcha(x) => x.distance_squared(&(*other).into()), Color::Xyza(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Okhsla(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Okhsva(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), } } } diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 230fce5befecc..60f8cd72b23d9 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -109,6 +109,8 @@ mod interpolate; mod laba; mod lcha; mod linear_rgba; +mod okhsla; +mod okhsva; mod oklaba; mod oklcha; pub mod palettes; @@ -171,6 +173,7 @@ where Self: From + Into, Self: From + Into, Self: From + Into, + Self: From + Into, Self: Alpha, { } @@ -277,3 +280,5 @@ macro_rules! impl_componentwise_vector_space { } pub(crate) use impl_componentwise_vector_space; + +use crate::okhsla::Okhsla; diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs new file mode 100644 index 0000000000000..c279037bce49e --- /dev/null +++ b/crates/bevy_color/src/okhsla.rs @@ -0,0 +1,774 @@ +use crate::{ + impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, + Lcha, LinearRgba, Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, +}; +use bevy_math::{ops, Vec3, Vec4}; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::prelude::*; + +/// Color in Okhsl color space, with alpha +#[doc = include_str!("../docs/conversion.md")] +///
+#[doc = include_str!("../docs/diagrams/model_graph.svg")] +///
+#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Clone, PartialEq, Default) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Okhsla { + /// The hue channel. [0.0, 360.0] + pub hue: f32, + /// The saturation channel. [0.0, 1.0] + pub saturation: f32, + /// The lightness channel. [0.0, 1.0] + pub lightness: f32, + /// The alpha channel. [0.0, 1.0] + pub alpha: f32, +} + +impl StandardColor for Okhsla {} + +impl_componentwise_vector_space!(Okhsla, [hue, saturation, lightness, alpha]); + +impl Okhsla { + /// Construct a new [`Okhsla`] color from components. + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `lightness` - Lightness channel. [0.0, 1.0] + /// * `alpha` - Alpha channel. [0.0, 1.0] + pub const fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { + Self { + hue, + saturation, + lightness, + alpha, + } + } + + /// Construct a new [`Hsla`] color from (h, s, l) components, with the default alpha (1.0). + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `lightness` - Lightness channel. [0.0, 1.0] + pub const fn okhsl(hue: f32, saturation: f32, lightness: f32) -> Self { + Self::new(hue, saturation, lightness, 1.0) + } + + /// Return a copy of this color with the saturation channel set to the given value. + pub const fn with_saturation(self, saturation: f32) -> Self { + Self { saturation, ..self } + } + + /// Return a copy of this color with the lightness channel set to the given value. + pub const fn with_lightness(self, lightness: f32) -> Self { + Self { lightness, ..self } + } + + /// Generate a deterministic but [quasi-randomly distributed](https://en.wikipedia.org/wiki/Low-discrepancy_sequence) + /// color from a provided `index`. + /// + /// This can be helpful for generating debug colors. + /// + /// # Examples + /// + /// ```rust + /// # use bevy_color::Hsla; + /// // Unique color for an entity + /// # let entity_index = 123; + /// // let entity_index = entity.index(); + /// let color = Hsla::sequential_dispersed(entity_index); + /// + /// // Palette with 5 distinct hues + /// let palette = (0..5).map(Hsla::sequential_dispersed).collect::>(); + /// ``` + pub const fn sequential_dispersed(index: u32) -> Self { + const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up + const RATIO_360: f32 = 360.0 / u32::MAX as f32; + + // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ + // + // Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range, + // so that the closer the numbers are, the larger the difference of their image. + let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360; + Self::okhsl(hue, 1., 0.5) + } +} + +impl Default for Okhsla { + fn default() -> Self { + Self::new(0., 0., 1., 1.) + } +} + +impl Mix for Okhsla { + #[inline] + fn mix(&self, other: &Self, factor: f32) -> Self { + let n_factor = 1.0 - factor; + Self { + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), + saturation: self.saturation * n_factor + other.saturation * factor, + lightness: self.lightness * n_factor + other.lightness * factor, + alpha: self.alpha * n_factor + other.alpha * factor, + } + } +} + +impl Gray for Okhsla { + const BLACK: Self = Self::new(0., 0., 0., 1.); + const WHITE: Self = Self::new(0., 0., 1., 1.); +} + +impl Alpha for Okhsla { + #[inline] + fn with_alpha(&self, alpha: f32) -> Self { + Self { alpha, ..*self } + } + + #[inline] + fn alpha(&self) -> f32 { + self.alpha + } + + #[inline] + fn set_alpha(&mut self, alpha: f32) { + self.alpha = alpha; + } +} + +impl Hue for Okhsla { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + +impl Saturation for Okhsla { + #[inline] + fn with_saturation(&self, saturation: f32) -> Self { + Self { + saturation, + ..*self + } + } + + #[inline] + fn saturation(&self) -> f32 { + self.saturation + } + + #[inline] + fn set_saturation(&mut self, saturation: f32) { + self.saturation = saturation; + } +} + +impl Luminance for Okhsla { + #[inline] + fn with_luminance(&self, lightness: f32) -> Self { + Self { lightness, ..*self } + } + + fn luminance(&self) -> f32 { + self.lightness + } + + fn darker(&self, amount: f32) -> Self { + Self { + lightness: (self.lightness - amount).clamp(0., 1.), + ..*self + } + } + + fn lighter(&self, amount: f32) -> Self { + Self { + lightness: (self.lightness + amount).min(1.), + ..*self + } + } +} + +impl ColorToComponents for Okhsla { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.saturation, self.lightness, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.saturation, self.lightness] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.saturation, self.lightness, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.saturation, self.lightness) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: 1.0, + } + } +} + +#[cfg(feature = "wgpu-types")] +impl From for wgpu_types::Color { + fn from(color: Okhsla) -> Self { + wgpu_types::Color { + r: color.hue as f64, + g: color.saturation as f64, + b: color.lightness as f64, + a: color.alpha as f64, + } + } +} + +#[derive(Clone, Copy)] +pub(crate) struct LC { + pub(crate) L: f32, + pub(crate) C: f32, +} + +#[derive(Clone, Copy)] +pub(crate) struct ST { + pub(crate) S: f32, + pub(crate) T: f32, +} + +pub(crate) fn to_ST(cusp: LC) -> ST { + let L = cusp.L; + let C = cusp.C; + return ST { + S: C / L, + T: C / (1. - L), + }; +} + +// Finds the maximum saturation possible for a given hue that fits in sRGB +// Saturation here is defined as S = C/L +// a and b must be normalized so a^2 + b^2 == 1 +fn compute_max_saturation(a: f32, b: f32) -> f32 { + // Max saturation will be when one of r, g or b goes below zero. + + // Select different coefficients depending on which component goes below zero first + let (k0, k1, k2, k3, k4, wl, wm, ws); + + if (-1.88170328 * a - 0.80936493 * b > 1.) { + // Red component + k0 = 1.19086277; + k1 = 1.76576728; + k2 = 0.59662641; + k3 = 0.75515197; + k4 = 0.56771245; + wl = 4.0767416621; + wm = -3.3077115913; + ws = 0.2309699292; + } else if (1.81444104 * a - 1.19445276 * b > 1.) { + // Green component + k0 = 0.73956515; + k1 = -0.45954404; + k2 = 0.08285427; + k3 = 0.12541070; + k4 = 0.14503204; + wl = -1.2684380046; + wm = 2.6097574011; + ws = -0.3413193965; + } else { + // Blue component + k0 = 1.35733652; + k1 = -0.00915799; + k2 = -1.15130210; + k3 = -0.50559606; + k4 = 0.00692167; + wl = -0.0041960863; + wm = -0.7034186147; + ws = 1.7076147010; + } + + // Approximate max saturation using a polynomial: + let mut S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; + + // Do one step Halley's method to get closer + // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite + // this should be sufficient for most applications, otherwise do two/three steps + + let k_l = 0.3963377774 * a + 0.2158037573 * b; + let k_m = -0.1055613458 * a - 0.0638541728 * b; + let k_s = -0.0894841775 * a - 1.2914855480 * b; + + { + let l_ = 1. + S * k_l; + let m_ = 1. + S * k_m; + let s_ = 1. + S * k_s; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let l_dS = 3. * k_l * l_ * l_; + let m_dS = 3. * k_m * m_ * m_; + let s_dS = 3. * k_s * s_ * s_; + + let l_dS2 = 6. * k_l * k_l * l_; + let m_dS2 = 6. * k_m * k_m * m_; + let s_dS2 = 6. * k_s * k_s * s_; + + let f = wl * l + wm * m + ws * s; + let f1 = wl * l_dS + wm * m_dS + ws * s_dS; + let f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; + + S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); + } + + return S; +} + +// finds L_cusp and C_cusp for a given hue +// a and b must be normalized so a^2 + b^2 == 1 +pub(crate) fn find_cusp(a: f32, b: f32) -> LC { + // First, find the maximum saturation (saturation S = C/L) + let S_cusp = compute_max_saturation(a, b); + + // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: + let rgb_at_max: LinearRgba = Oklaba::lab(1., S_cusp * a, S_cusp * b).into(); + let L_cusp = ops::cbrt((1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue)))); + let C_cusp = L_cusp * S_cusp; + + return LC { + L: L_cusp, + C: C_cusp, + }; +} + +// Finds intersection of the line defined by +// L = L0 * (1 - t) + t * L1; +// C = t * C1; +// a and b must be normalized so a^2 + b^2 == 1 +fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) -> f32 { + // Find the intersection for upper and lower half seprately + let mut t; + if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.) { + // Lower half + + t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); + } else { + // Upper half + + // First intersect with triangle + t = cusp.C * (L0 - 1.) / (C1 * (cusp.L - 1.) + cusp.C * (L0 - L1)); + + // Then one step Halley's method + { + let dL = L1 - L0; + let dC = C1; + + let k_l = 0.3963377774 * a + 0.2158037573 * b; + let k_m = -0.1055613458 * a - 0.0638541728 * b; + let k_s = -0.0894841775 * a - 1.2914855480 * b; + + let l_dt = dL + dC * k_l; + let m_dt = dL + dC * k_m; + let s_dt = dL + dC * k_s; + + // If higher accuracy is required, 2 or 3 iterations of the following block can be used: + { + let L = L0 * (1. - t) + t * L1; + let C = t * C1; + + let l_ = L + C * k_l; + let m_ = L + C * k_m; + let s_ = L + C * k_s; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let ldt = 3. * l_dt * l_ * l_; + let mdt = 3. * m_dt * m_ * m_; + let sdt = 3. * s_dt * s_ * s_; + + let ldt2 = 6. * l_dt * l_dt * l_; + let mdt2 = 6. * m_dt * m_dt * m_; + let sdt2 = 6. * s_dt * s_dt * s_; + + let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1.; + let r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; + let r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; + + let u_r = r1 / (r1 * r1 - 0.5 * r * r2); + let mut t_r = -r * u_r; + + let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1.; + let g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; + let g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; + + let u_g = g1 / (g1 * g1 - 0.5 * g * g2); + let mut t_g = -g * u_g; + + let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1.; + let b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; + let b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; + + let u_b = b1 / (b1 * b1 - 0.5 * b * b2); + let mut t_b = -b * u_b; + + t_r = if u_r >= 0. { t_r } else { core::f32::MAX }; + t_g = if u_g >= 0. { t_g } else { core::f32::MAX }; + t_b = if u_b >= 0. { t_b } else { core::f32::MAX }; + + t += (t_r.min(t_g.min(t_b))); + } + } + } + + return t; +} + +#[derive(Clone, Copy)] +struct Cs { + C_0: f32, + C_mid: f32, + C_max: f32, +} + +fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { + let cusp = find_cusp(a_, b_); + + let C_max = find_gamut_intersection(a_, b_, L, 1., L, cusp); + let ST_max = to_ST(cusp); + + // Scale factor to compensate for the curved part of gamut shape: + let k = C_max / ((L * ST_max.S).min((1. - L) * ST_max.T)); + + let C_mid; + { + let ST_mid = get_ST_mid(a_, b_); + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + let C_a = L * ST_mid.S; + let C_b = (1. - L) * ST_mid.T; + C_mid = 0.9 + * k + * (1. / (1. / (C_a * C_a * C_a * C_a) + 1. / (C_b * C_b * C_b * C_b))) + .sqrt() + .sqrt(); + } + + let C_0; + { + // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. + let C_a = L * 0.4; + let C_b = (1. - L) * 0.8; + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + C_0 = (1. / (1. / (C_a * C_a) + 1. / (C_b * C_b))).sqrt(); + } + + return Cs { C_0, C_mid, C_max }; +} + +// Returns a smooth approximation of the location of the cusp +// This polynomial was created by an optimization process +// It has been designed so that S_mid < S_max and T_mid < T_max +fn get_ST_mid(a_: f32, b_: f32) -> ST { + let S = 0.11516993 + + 1. / (7.44778970 + + 4.15901240 * b_ + + a_ * (-2.19557347 + + 1.75198401 * b_ + + a_ * (-2.13704948 - 10.02301043 * b_ + + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_)))); + + let T = 0.11239642 + + 1. / (1.61320320 - 0.68124379 * b_ + + a_ * (0.40370612 + + 0.90148123 * b_ + + a_ * (-0.27087943 + + 0.61223990 * b_ + + a_ * (0.00299215 - 0.45399568 * b_ - 0.14661872 * a_)))); + + return ST { S, T }; +} + +pub(crate) fn toe(x: f32) -> f32 { + let k_1: f32 = 0.206; + let k_2: f32 = 0.03; + let k_3: f32 = (1. + k_1) / (1. + k_2); + return 0.5 * (k_3 * x - k_1 + ((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x).sqrt()); +} + +pub(crate) fn toe_inv(x: f32) -> f32 { + let k_1 = 0.206; + let k_2 = 0.03; + let k_3 = (1. + k_1) / (1. + k_2); + return (x * x + k_1 * x) / (k_3 * (x + k_2)); +} + +impl From for Okhsla { + fn from(value: Oklaba) -> Self { + let Oklaba { + lightness: lab_l, + a: lab_a, + b: lab_b, + alpha, + } = value; + let C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let a_ = lab_a / C; + let b_ = lab_b / C; + + let L = lab_l; + let h = 0.5 + 0.5 * ops::atan2(-lab_b, -lab_a) / core::f32::consts::PI; + + let cs = get_Cs(L, a_, b_); + let C_0 = cs.C_0; + let C_mid = cs.C_mid; + let C_max = cs.C_max; + + // Inverse of the interpolation in okhsl_to_srgb: + + let mid = 0.8; + let mid_inv = 1.25; + + let s; + if (C < C_mid) { + let k_1 = mid * C_0; + let k_2 = (1. - k_1 / C_mid); + + let t = C / (k_1 + k_2 * C); + s = t * mid; + } else { + let k_0 = C_mid; + let k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + let k_2 = (1. - (k_1) / (C_max - C_mid)); + + let t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + s = mid + (1. - mid) * t; + } + + let l = toe(L); + return Okhsla { + hue: h, + saturation: s, + lightness: l, + alpha, + }; + } +} + +impl From for Oklaba { + fn from(value: Okhsla) -> Self { + let Okhsla { + hue: h, + saturation: s, + lightness: l, + alpha, + } = value; + + if (l == 1.) { + return Oklaba::new(1., 1., 1., alpha); + } else if (l == 0.) { + return Oklaba::new(0., 0., 0., alpha); + } + + let a_ = (2. * core::f32::consts::PI * h).cos(); + let b_ = (2. * core::f32::consts::PI * h).sin(); + let L = toe_inv(l); + + let cs = get_Cs(L, a_, b_); + let C_0 = cs.C_0; + let C_mid = cs.C_mid; + let C_max = cs.C_max; + + let mid = 0.8; + let mid_inv = 1.25; + + let (C, t, k_0, k_1, k_2); + + if (s < mid) { + t = mid_inv * s; + + k_1 = mid * C_0; + k_2 = (1. - k_1 / C_mid); + + C = t * k_1 / (1. - k_2 * t); + } else { + t = (s - mid) / (1. - mid); + + k_0 = C_mid; + k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + k_2 = (1. - (k_1) / (C_max - C_mid)); + + C = k_0 + t * k_1 / (1. - k_2 * t); + } + + Oklaba::new(L, C * a_, C * b_, alpha) + } +} + +// Derived Conversions + +impl From for Okhsla { + fn from(value: LinearRgba) -> Self { + Oklaba::from(value).into() + } +} + +impl From for LinearRgba { + fn from(value: Okhsla) -> Self { + Oklaba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Hsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Hsla { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Hsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Hsva { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Hwba) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Hwba { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Lcha) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Lcha { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Srgba) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Srgba { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Xyza) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Xyza { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Laba) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Laba { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Oklcha) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Oklcha { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq}; +} diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs new file mode 100644 index 0000000000000..6deb7c660f4ef --- /dev/null +++ b/crates/bevy_color/src/okhsva.rs @@ -0,0 +1,469 @@ +use crate::{ + okhsla::{find_cusp, to_ST, toe, toe_inv, Okhsla}, + Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, Mix, Oklaba, + Oklcha, Saturation, Srgba, StandardColor, Xyza, +}; +use bevy_math::{ops, Vec3, Vec4}; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::prelude::*; + +/// Color in Okhsv color space with alpha. +/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV). +#[doc = include_str!("../docs/conversion.md")] +///
+#[doc = include_str!("../docs/diagrams/model_graph.svg")] +///
+#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Clone, PartialEq, Default) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Okhsva { + /// The hue channel. [0.0, 360.0] + pub hue: f32, + /// The saturation channel. [0.0, 1.0] + pub saturation: f32, + /// The value channel. [0.0, 1.0] + pub value: f32, + /// The alpha channel. [0.0, 1.0] + pub alpha: f32, +} + +impl StandardColor for Okhsva {} + +impl Okhsva { + /// Construct a new [`Okhsva`] color from components. + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `value` - Value channel. [0.0, 1.0] + /// * `alpha` - Alpha channel. [0.0, 1.0] + pub const fn new(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self { + Self { + hue, + saturation, + value, + alpha, + } + } + + /// Construct a new [`Okhsva`] color from (h, s, v) components, with the default alpha (1.0). + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `value` - Value channel. [0.0, 1.0] + pub const fn hsv(hue: f32, saturation: f32, value: f32) -> Self { + Self::new(hue, saturation, value, 1.0) + } + + /// Return a copy of this color with the saturation channel set to the given value. + pub const fn with_saturation(self, saturation: f32) -> Self { + Self { saturation, ..self } + } + + /// Return a copy of this color with the value channel set to the given value. + pub const fn with_value(self, value: f32) -> Self { + Self { value, ..self } + } +} + +impl Default for Okhsva { + fn default() -> Self { + Self::new(0., 0., 1., 1.) + } +} + +impl Mix for Okhsva { + #[inline] + fn mix(&self, other: &Self, factor: f32) -> Self { + let n_factor = 1.0 - factor; + Self { + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), + saturation: self.saturation * n_factor + other.saturation * factor, + value: self.value * n_factor + other.value * factor, + alpha: self.alpha * n_factor + other.alpha * factor, + } + } +} + +impl Gray for Okhsva { + const BLACK: Self = Self::new(0., 0., 0., 1.); + const WHITE: Self = Self::new(0., 0., 1., 1.); +} + +impl Alpha for Okhsva { + #[inline] + fn with_alpha(&self, alpha: f32) -> Self { + Self { alpha, ..*self } + } + + #[inline] + fn alpha(&self) -> f32 { + self.alpha + } + + #[inline] + fn set_alpha(&mut self, alpha: f32) { + self.alpha = alpha; + } +} + +impl Hue for Okhsva { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + +impl Saturation for Okhsva { + #[inline] + fn with_saturation(&self, saturation: f32) -> Self { + Self { + saturation, + ..*self + } + } + + #[inline] + fn saturation(&self) -> f32 { + self.saturation + } + + #[inline] + fn set_saturation(&mut self, saturation: f32) { + self.saturation = saturation; + } +} + +impl From for Hwba { + fn from( + Okhsva { + hue, + saturation, + value, + alpha, + }: Okhsva, + ) -> Self { + // Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion + let whiteness = (1. - saturation) * value; + let blackness = 1. - value; + + Hwba::new(hue, whiteness, blackness, alpha) + } +} + +impl From for Okhsva { + fn from( + Hwba { + hue, + whiteness, + blackness, + alpha, + }: Hwba, + ) -> Self { + // Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion + let value = 1. - blackness; + let saturation = if value != 0. { + 1. - (whiteness / value) + } else { + 0. + }; + + Okhsva::new(hue, saturation, value, alpha) + } +} + +impl ColorToComponents for Okhsva { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.saturation, self.value, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.saturation, self.value] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.saturation, self.value, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.saturation, self.value) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: 1.0, + } + } +} + +impl From for Okhsva { + fn from(value: Oklaba) -> Self { + let Oklaba { + lightness: lab_l, + a: lab_a, + b: lab_b, + alpha, + } = value; + let mut C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let a_ = lab_a / C; + let b_ = lab_b / C; + + let mut L = lab_l; + let h = 0.5 + 0.5 * ops::atan2(-lab_b, -lab_a) / core::f32::consts::PI; + + let cusp = find_cusp(a_, b_); + let ST_max = to_ST(cusp); + let S_max = ST_max.S; + let T_max = ST_max.T; + let S_0 = 0.5; + let k = 1. - S_0 / S_max; + + // first we find L_v, C_v, L_vt and C_vt + + let t = T_max / (C + L * T_max); + let L_v = t * L; + let C_v = t * C; + + let L_vt = toe_inv(L_v); + let C_vt = C_v * L_vt / L_v; + + // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle: + let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); + let scale_L = + ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); + + L = L / scale_L; + C = C / scale_L; + + C = C * toe(L) / L; + L = toe(L); + + // we can now compute v and s: + + let v = L / L_v; + let s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); + + return Okhsva { + hue: h, + saturation: s, + value: v, + alpha, + }; + } +} + +impl From for Oklaba { + fn from(value: Okhsva) -> Self { + let Okhsva { + hue: h, + saturation: s, + value: v, + alpha, + } = value; + + let a_ = (2. * core::f32::consts::PI * h).cos(); + let b_ = (2. * core::f32::consts::PI * h).sin(); + + let cusp = find_cusp(a_, b_); + let ST_max = to_ST(cusp); + let S_max = ST_max.S; + let T_max = ST_max.T; + let S_0 = 0.5; + let k = 1. - S_0 / S_max; + + // first we compute L and V as if the gamut is a perfect triangle: + + // L, C when v==1: + let L_v = 1. - s * S_0 / (S_0 + T_max - T_max * k * s); + let C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); + + let mut L = v * L_v; + let mut C = v * C_v; + + // then we compensate for both toe and the curved top part of the triangle: + let L_vt = toe_inv(L_v); + let C_vt = C_v * L_vt / L_v; + + let L_new = toe_inv(L); + C = C * L_new / L; + L = L_new; + + let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); + let scale_L = + ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max((rgb_scale.blue.max(0.))))); + + L = L * scale_L; + C = C * scale_L; + + Oklaba::new(L, C * a_, C * b_, alpha) + } +} + +// Derived Conversions + +impl From for Okhsva { + fn from(value: LinearRgba) -> Self { + Oklaba::from(value).into() + } +} + +impl From for LinearRgba { + fn from(value: Okhsva) -> Self { + Oklaba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Srgba) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Srgba { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Lcha) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Lcha { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Xyza) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Xyza { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Okhsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsla { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Hsla) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Hsla { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Hsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Hsva { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Laba) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Laba { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Okhsva { + fn from(value: Oklcha) -> Self { + LinearRgba::from(value).into() + } +} + +impl From for Oklcha { + fn from(value: Okhsva) -> Self { + LinearRgba::from(value).into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq, + }; +} From 88f2d13c7fe211f8e1e64f0aa7b69bc13c35e8fa Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 5 May 2026 23:33:58 +0800 Subject: [PATCH 02/11] Test pass --- .../bevy_color/crates/gen_tests/src/main.rs | 60 ++- crates/bevy_color/src/lib.rs | 3 + crates/bevy_color/src/okcolor_convert.rs | 485 ++++++++++++++++++ crates/bevy_color/src/okhsla.rs | 431 ++-------------- crates/bevy_color/src/okhsva.rs | 144 ++---- crates/bevy_color/src/test_colors.rs | 130 +++-- 6 files changed, 736 insertions(+), 517 deletions(-) create mode 100644 crates/bevy_color/src/okcolor_convert.rs diff --git a/crates/bevy_color/crates/gen_tests/src/main.rs b/crates/bevy_color/crates/gen_tests/src/main.rs index 7cd614f966fea..7dbd40af3aceb 100644 --- a/crates/bevy_color/crates/gen_tests/src/main.rs +++ b/crates/bevy_color/crates/gen_tests/src/main.rs @@ -20,12 +20,56 @@ const TEST_COLORS: &[(f32, f32, f32, &str)] = &[ (0.5, 0., 0.5, "fuchsia"), (0., 0.5, 0.5, "aqua"), ]; +/// Pre-computed okhsl results of [`TEST_COLORS`]. +const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ + [0., 0., 0.], // The result of original javascript implemention is [0, NaN, 0]. + [89.87556309590242, 0.5582831888483675, 0.9999999923961898], + [29.23388519234263, 1.0000000001433997, 0.5680846525040862], + [142.49533888780996, 0.9999999700728788, 0.8445289645307816], + [264.052020638055, 0.9999999948631134, 0.3665653394260194], + [109.76923207652122, 1.0000000336324515, 0.9627043968088945], + [328.36341792345144, 1.000039018792486, 0.6532987448472438], + [194.76894793196382, 0.9999999858415329, 0.889848301619521], + [89.87556282857139, 1.1616558204531687e-7, 0.5337598228073358], + [109.76923207652133, 1.0000004489650085, 0.5117162225399476], + [328.36341792345144, 1.0006210729018223, 0.33011042396630463], + [194.7689479319638, 0.9999994637526137, 0.4687233442820504], + [29.23388519234263, 0.9996788048327606, 0.28080442778247217], + [142.49533888780996, 0.9999999579156374, 0.4420348389571636], + [264.052020638055, 0.9999976560886634, 0.1673431798736403], + [109.76923207652133, 1.0000004489650085, 0.5117162225399476], + [328.36341792345144, 1.0006210729018223, 0.33011042396630463], + [194.7689479319638, 0.9999994637526137, 0.4687233442820504], +]; +/// Pre-computed okhsv results of [`TEST_COLORS`]. +const TEST_COLORS_OKHSV: &[[f32; 3]] = &[ + [0., 0., 0.], // The result of original javascript implemention is [0, NaN, NaN]. + [89.87556309590242, 1.0347523928230576e-7, 1.000000027003774], + [29.23388519234263, 0.9995219692256989, 1.0000000001685625], + [142.49533888780996, 0.9999997210415695, 0.9999999884428648], + [264.052020638055, 0.9999910912349018, 0.9999999646150918], + [109.76923207652122, 1.0000004467649033, 1.0000000319591924], + [328.36341792345144, 1.000122136833694, 0.9999999978651268], + [194.76894793196382, 0.9999994264606504, 0.9999999950078027], + [89.87556282857139, 1.0347523926099617e-7, 0.5337598416065157], + [109.76923207652133, 1.0000004467649042, 0.5318634934435551], + [328.36341792345144, 1.0001221368336943, 0.5106197804980503], + [194.7689479319638, 0.999999426460651, 0.5278241496179404], + [29.23388519234263, 0.9995219692256992, 0.5022645128400224], + [142.49533888780996, 0.9999997210415695, 0.5250592582180916], + [264.052020638055, 0.9999910912349016, 0.47496664028144414], + [109.76923207652133, 1.0000004467649042, 0.5318634934435551], + [328.36341792345144, 1.0001221368336943, 0.5106197804980503], + [194.7689479319638, 0.999999426460651, 0.5278241496179404], +]; fn main() { println!( "// Generated by gen_tests. Do not edit. #[cfg(test)] -use crate::{{Hsla, Hsva, Hwba, Srgba, LinearRgba, Oklaba, Oklcha, Laba, Lcha, Xyza}}; +use crate::{{ + okhsla::Okhsla, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Okhsva, Oklaba, Oklcha, Srgba, Xyza, +}}; #[cfg(test)] pub struct TestColor {{ @@ -40,6 +84,8 @@ pub struct TestColor {{ pub oklab: Oklaba, pub oklch: Oklcha, pub xyz: Xyza, + pub okhsl: Okhsla, + pub okhsv: Okhsva, }} " ); @@ -47,7 +93,7 @@ pub struct TestColor {{ println!("// Table of equivalent colors in various color spaces"); println!("#[cfg(test)]"); println!("pub const TEST_COLORS: &[TestColor] = &["); - for (r, g, b, name) in TEST_COLORS { + for (i, (r, g, b, name)) in TEST_COLORS.iter().enumerate() { let srgb = Srgb::new(*r, *g, *b); let linear_rgb: LinSrgb = srgb.into_color(); let hsl: Hsl = srgb.into_color(); @@ -58,6 +104,8 @@ pub struct TestColor {{ let oklab: Oklab = srgb.into_color(); let oklch: Oklch = srgb.into_color(); let xyz: Xyz = srgb.into_color(); + let okhsl = TEST_COLORS_OKHSL[i]; + let okhsv = TEST_COLORS_OKHSV[i]; println!(" // {name}"); println!( " TestColor {{ @@ -72,6 +120,8 @@ pub struct TestColor {{ oklab: Oklaba::new({}, {}, {}, 1.0), oklch: Oklcha::new({}, {}, {}, 1.0), xyz: Xyza::new({}, {}, {}, 1.0), + okhsl: Okhsla::new({}, {}, {}, 1.0), + okhsv: Okhsva::new({}, {}, {}, 1.0), }},", VariablePrecision(srgb.red), VariablePrecision(srgb.green), @@ -103,6 +153,12 @@ pub struct TestColor {{ VariablePrecision(xyz.x), VariablePrecision(xyz.y), VariablePrecision(xyz.z), + VariablePrecision(okhsl[0]), + VariablePrecision(okhsl[1]), + VariablePrecision(okhsl[2]), + VariablePrecision(okhsv[0]), + VariablePrecision(okhsv[1]), + VariablePrecision(okhsv[2]), ); } println!("];"); diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 60f8cd72b23d9..cead0cb5943b3 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -109,6 +109,7 @@ mod interpolate; mod laba; mod lcha; mod linear_rgba; +mod okcolor_convert; mod okhsla; mod okhsva; mod oklaba; @@ -142,6 +143,8 @@ pub use hwba::*; pub use laba::*; pub use lcha::*; pub use linear_rgba::*; +pub use okhsla::*; +pub use okhsva::*; pub use oklaba::*; pub use oklcha::*; pub use srgba::*; diff --git a/crates/bevy_color/src/okcolor_convert.rs b/crates/bevy_color/src/okcolor_convert.rs new file mode 100644 index 0000000000000..746959145b4e4 --- /dev/null +++ b/crates/bevy_color/src/okcolor_convert.rs @@ -0,0 +1,485 @@ +use crate::{okhsla::Okhsla, LinearRgba, Okhsva, Oklaba}; +use bevy_math::ops; + +#[derive(Clone, Copy)] +pub(crate) struct LC { + pub(crate) L: f32, + pub(crate) C: f32, +} + +#[derive(Clone, Copy)] +pub(crate) struct ST { + pub(crate) S: f32, + pub(crate) T: f32, +} + +pub(crate) fn to_ST(cusp: LC) -> ST { + let L = cusp.L; + let C = cusp.C; + return ST { + S: C / L, + T: C / (1. - L), + }; +} + +// Finds the maximum saturation possible for a given hue that fits in sRGB +// Saturation here is defined as S = C/L +// a and b must be normalized so a^2 + b^2 == 1 +fn compute_max_saturation(a: f32, b: f32) -> f32 { + // Max saturation will be when one of r, g or b goes below zero. + + // Select different coefficients depending on which component goes below zero first + let (k0, k1, k2, k3, k4, wl, wm, ws); + + if (-1.88170328 * a - 0.80936493 * b > 1.) { + // Red component + k0 = 1.19086277; + k1 = 1.76576728; + k2 = 0.59662641; + k3 = 0.75515197; + k4 = 0.56771245; + wl = 4.0767416621; + wm = -3.3077115913; + ws = 0.2309699292; + } else if (1.81444104 * a - 1.19445276 * b > 1.) { + // Green component + k0 = 0.73956515; + k1 = -0.45954404; + k2 = 0.08285427; + k3 = 0.12541070; + k4 = 0.14503204; + wl = -1.2684380046; + wm = 2.6097574011; + ws = -0.3413193965; + } else { + // Blue component + k0 = 1.35733652; + k1 = -0.00915799; + k2 = -1.15130210; + k3 = -0.50559606; + k4 = 0.00692167; + wl = -0.0041960863; + wm = -0.7034186147; + ws = 1.7076147010; + } + + // Approximate max saturation using a polynomial: + let mut S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; + + // Do one step Halley's method to get closer + // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite + // this should be sufficient for most applications, otherwise do two/three steps + + let k_l = 0.3963377774 * a + 0.2158037573 * b; + let k_m = -0.1055613458 * a - 0.0638541728 * b; + let k_s = -0.0894841775 * a - 1.2914855480 * b; + + { + let l_ = 1. + S * k_l; + let m_ = 1. + S * k_m; + let s_ = 1. + S * k_s; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let l_dS = 3. * k_l * l_ * l_; + let m_dS = 3. * k_m * m_ * m_; + let s_dS = 3. * k_s * s_ * s_; + + let l_dS2 = 6. * k_l * k_l * l_; + let m_dS2 = 6. * k_m * k_m * m_; + let s_dS2 = 6. * k_s * k_s * s_; + + let f = wl * l + wm * m + ws * s; + let f1 = wl * l_dS + wm * m_dS + ws * s_dS; + let f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; + + S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); + } + + return S; +} + +// finds L_cusp and C_cusp for a given hue +// a and b must be normalized so a^2 + b^2 == 1 +pub(crate) fn find_cusp(a: f32, b: f32) -> LC { + // First, find the maximum saturation (saturation S = C/L) + let S_cusp = compute_max_saturation(a, b); + + // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: + let rgb_at_max: LinearRgba = Oklaba::lab(1., S_cusp * a, S_cusp * b).into(); + let L_cusp = ops::cbrt((1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue)))); + let C_cusp = L_cusp * S_cusp; + + return LC { + L: L_cusp, + C: C_cusp, + }; +} + +// Finds intersection of the line defined by +// L = L0 * (1 - t) + t * L1; +// C = t * C1; +// a and b must be normalized so a^2 + b^2 == 1 +fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) -> f32 { + // Find the intersection for upper and lower half seprately + let mut t; + if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.) { + // Lower half + + t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); + } else { + // Upper half + + // First intersect with triangle + t = cusp.C * (L0 - 1.) / (C1 * (cusp.L - 1.) + cusp.C * (L0 - L1)); + + // Then one step Halley's method + { + let dL = L1 - L0; + let dC = C1; + + let k_l = 0.3963377774 * a + 0.2158037573 * b; + let k_m = -0.1055613458 * a - 0.0638541728 * b; + let k_s = -0.0894841775 * a - 1.2914855480 * b; + + let l_dt = dL + dC * k_l; + let m_dt = dL + dC * k_m; + let s_dt = dL + dC * k_s; + + // If higher accuracy is required, 2 or 3 iterations of the following block can be used: + { + let L = L0 * (1. - t) + t * L1; + let C = t * C1; + + let l_ = L + C * k_l; + let m_ = L + C * k_m; + let s_ = L + C * k_s; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let ldt = 3. * l_dt * l_ * l_; + let mdt = 3. * m_dt * m_ * m_; + let sdt = 3. * s_dt * s_ * s_; + + let ldt2 = 6. * l_dt * l_dt * l_; + let mdt2 = 6. * m_dt * m_dt * m_; + let sdt2 = 6. * s_dt * s_dt * s_; + + let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1.; + let r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; + let r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; + + let u_r = r1 / (r1 * r1 - 0.5 * r * r2); + let mut t_r = -r * u_r; + + let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1.; + let g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; + let g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; + + let u_g = g1 / (g1 * g1 - 0.5 * g * g2); + let mut t_g = -g * u_g; + + let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1.; + let b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; + let b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; + + let u_b = b1 / (b1 * b1 - 0.5 * b * b2); + let mut t_b = -b * u_b; + + t_r = if u_r >= 0. { t_r } else { core::f32::MAX }; + t_g = if u_g >= 0. { t_g } else { core::f32::MAX }; + t_b = if u_b >= 0. { t_b } else { core::f32::MAX }; + + t += (t_r.min(t_g.min(t_b))); + } + } + } + + return t; +} + +#[derive(Clone, Copy)] +struct Cs { + C_0: f32, + C_mid: f32, + C_max: f32, +} + +fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { + let cusp = find_cusp(a_, b_); + + let C_max = find_gamut_intersection(a_, b_, L, 1., L, cusp); + let ST_max = to_ST(cusp); + + // Scale factor to compensate for the curved part of gamut shape: + let k = C_max / ((L * ST_max.S).min((1. - L) * ST_max.T)); + + let C_mid; + { + let ST_mid = get_ST_mid(a_, b_); + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + let C_a = L * ST_mid.S; + let C_b = (1. - L) * ST_mid.T; + C_mid = 0.9 + * k + * (1. / (1. / (C_a * C_a * C_a * C_a) + 1. / (C_b * C_b * C_b * C_b))) + .sqrt() + .sqrt(); + } + + let C_0; + { + // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. + let C_a = L * 0.4; + let C_b = (1. - L) * 0.8; + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + C_0 = (1. / (1. / (C_a * C_a) + 1. / (C_b * C_b))).sqrt(); + } + + return Cs { C_0, C_mid, C_max }; +} + +// Returns a smooth approximation of the location of the cusp +// This polynomial was created by an optimization process +// It has been designed so that S_mid < S_max and T_mid < T_max +fn get_ST_mid(a_: f32, b_: f32) -> ST { + let S = 0.11516993 + + 1. / (7.44778970 + + 4.15901240 * b_ + + a_ * (-2.19557347 + + 1.75198401 * b_ + + a_ * (-2.13704948 - 10.02301043 * b_ + + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_)))); + + let T = 0.11239642 + + 1. / (1.61320320 - 0.68124379 * b_ + + a_ * (0.40370612 + + 0.90148123 * b_ + + a_ * (-0.27087943 + + 0.61223990 * b_ + + a_ * (0.00299215 - 0.45399568 * b_ - 0.14661872 * a_)))); + + return ST { S, T }; +} + +pub(crate) fn toe(x: f32) -> f32 { + let k_1: f32 = 0.206; + let k_2: f32 = 0.03; + let k_3: f32 = (1. + k_1) / (1. + k_2); + return 0.5 * (k_3 * x - k_1 + ((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x).sqrt()); +} + +pub(crate) fn toe_inv(x: f32) -> f32 { + let k_1 = 0.206; + let k_2 = 0.03; + let k_3 = (1. + k_1) / (1. + k_2); + return (x * x + k_1 * x) / (k_3 * (x + k_2)); +} + +pub(crate) fn oklab_to_okhsl(value: Oklaba) -> Okhsla { + let Oklaba { + lightness: lab_l, + a: lab_a, + b: lab_b, + alpha, + } = value; + let C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let a_ = lab_a / C; + let b_ = lab_b / C; + + let L = lab_l; + let h = 0.5 + 0.5 * ops::atan2(-lab_b, -lab_a) / core::f32::consts::PI; + + let cs = get_Cs(L, a_, b_); + let C_0 = cs.C_0; + let C_mid = cs.C_mid; + let C_max = cs.C_max; + + // Inverse of the interpolation in okhsl_to_srgb: + + let mid = 0.8; + let mid_inv = 1.25; + + let s; + if (C < C_mid) { + let k_1 = mid * C_0; + let k_2 = (1. - k_1 / C_mid); + + let t = C / (k_1 + k_2 * C); + s = t * mid; + } else { + let k_0 = C_mid; + let k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + let k_2 = (1. - (k_1) / (C_max - C_mid)); + + let t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + s = mid + (1. - mid) * t; + } + + let l = toe(L); + return Okhsla { + hue: h * 360., + saturation: s, + lightness: l, + alpha, + }; +} + +pub(crate) fn okhsl_to_oklab(value: Okhsla) -> Oklaba { + let Okhsla { + hue: h, + saturation: s, + lightness: l, + alpha, + } = value; + let h = h / 360.; + + if (l == 1.) { + return LinearRgba::new(1., 1., 1., alpha).into(); + } else if (l == 0.) { + return LinearRgba::new(0., 0., 0., alpha).into(); + } + + let a_ = (2. * core::f32::consts::PI * h).cos(); + let b_ = (2. * core::f32::consts::PI * h).sin(); + let L = toe_inv(l); + + let cs = get_Cs(L, a_, b_); + let C_0 = cs.C_0; + let C_mid = cs.C_mid; + let C_max = cs.C_max; + + let mid = 0.8; + let mid_inv = 1.25; + + let (C, t, k_0, k_1, k_2); + + if (s < mid) { + t = mid_inv * s; + + k_1 = mid * C_0; + k_2 = (1. - k_1 / C_mid); + + C = t * k_1 / (1. - k_2 * t); + } else { + t = (s - mid) / (1. - mid); + + k_0 = C_mid; + k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + k_2 = (1. - (k_1) / (C_max - C_mid)); + + C = k_0 + t * k_1 / (1. - k_2 * t); + } + + Oklaba::new(L, C * a_, C * b_, alpha) +} + +pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { + let Oklaba { + lightness: lab_l, + a: lab_a, + b: lab_b, + alpha, + } = value; + let mut C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let a_ = lab_a / C; + let b_ = lab_b / C; + + let mut L = lab_l; + let h = 0.5 + 0.5 * ops::atan2(-lab_b, -lab_a) / core::f32::consts::PI; + + let cusp = find_cusp(a_, b_); + let ST_max = to_ST(cusp); + let S_max = ST_max.S; + let T_max = ST_max.T; + let S_0 = 0.5; + let k = 1. - S_0 / S_max; + + // first we find L_v, C_v, L_vt and C_vt + + let t = T_max / (C + L * T_max); + let L_v = t * L; + let C_v = t * C; + + let L_vt = toe_inv(L_v); + let C_vt = C_v * L_vt / L_v; + + // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle: + let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); + let scale_L = + ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); + + L = L / scale_L; + C = C / scale_L; + + C = C * toe(L) / L; + L = toe(L); + + // we can now compute v and s: + + let v = L / L_v; + let s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); + + return Okhsva { + hue: h * 360., + saturation: s, + value: v, + alpha, + }; +} + +pub(crate) fn okhsv_to_oklab(value: Okhsva) -> Oklaba { + let Okhsva { + hue: h, + saturation: s, + value: v, + alpha, + } = value; + let h = h / 360.; + + if (v == 0.) { + return LinearRgba::new(0., 0., 0., alpha).into(); + } + + let a_ = (2. * core::f32::consts::PI * h).cos(); + let b_ = (2. * core::f32::consts::PI * h).sin(); + + let cusp = find_cusp(a_, b_); + let ST_max = to_ST(cusp); + let S_max = ST_max.S; + let T_max = ST_max.T; + let S_0 = 0.5; + let k = 1. - S_0 / S_max; + + // first we compute L and V as if the gamut is a perfect triangle: + + // L, C when v==1: + let L_v = 1. - s * S_0 / (S_0 + T_max - T_max * k * s); + let C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); + + let mut L = v * L_v; + let mut C = v * C_v; + + // then we compensate for both toe and the curved top part of the triangle: + let L_vt = toe_inv(L_v); + let C_vt = C_v * L_vt / L_v; + + let L_new = toe_inv(L); + C = C * L_new / L; + L = L_new; + + let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); + let scale_L = + ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max((rgb_scale.blue.max(0.))))); + + L = L * scale_L; + C = C * scale_L; + + Oklaba::new(L, C * a_, C * b_, alpha) +} diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index c279037bce49e..917f7412115ec 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -1,6 +1,8 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, - Lcha, LinearRgba, Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, + impl_componentwise_vector_space, + okcolor_convert::{okhsl_to_oklab, oklab_to_okhsl}, + Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, Luminance, Mix, + Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, }; use bevy_math::{ops, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] @@ -275,385 +277,15 @@ impl From for wgpu_types::Color { } } -#[derive(Clone, Copy)] -pub(crate) struct LC { - pub(crate) L: f32, - pub(crate) C: f32, -} - -#[derive(Clone, Copy)] -pub(crate) struct ST { - pub(crate) S: f32, - pub(crate) T: f32, -} - -pub(crate) fn to_ST(cusp: LC) -> ST { - let L = cusp.L; - let C = cusp.C; - return ST { - S: C / L, - T: C / (1. - L), - }; -} - -// Finds the maximum saturation possible for a given hue that fits in sRGB -// Saturation here is defined as S = C/L -// a and b must be normalized so a^2 + b^2 == 1 -fn compute_max_saturation(a: f32, b: f32) -> f32 { - // Max saturation will be when one of r, g or b goes below zero. - - // Select different coefficients depending on which component goes below zero first - let (k0, k1, k2, k3, k4, wl, wm, ws); - - if (-1.88170328 * a - 0.80936493 * b > 1.) { - // Red component - k0 = 1.19086277; - k1 = 1.76576728; - k2 = 0.59662641; - k3 = 0.75515197; - k4 = 0.56771245; - wl = 4.0767416621; - wm = -3.3077115913; - ws = 0.2309699292; - } else if (1.81444104 * a - 1.19445276 * b > 1.) { - // Green component - k0 = 0.73956515; - k1 = -0.45954404; - k2 = 0.08285427; - k3 = 0.12541070; - k4 = 0.14503204; - wl = -1.2684380046; - wm = 2.6097574011; - ws = -0.3413193965; - } else { - // Blue component - k0 = 1.35733652; - k1 = -0.00915799; - k2 = -1.15130210; - k3 = -0.50559606; - k4 = 0.00692167; - wl = -0.0041960863; - wm = -0.7034186147; - ws = 1.7076147010; - } - - // Approximate max saturation using a polynomial: - let mut S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; - - // Do one step Halley's method to get closer - // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite - // this should be sufficient for most applications, otherwise do two/three steps - - let k_l = 0.3963377774 * a + 0.2158037573 * b; - let k_m = -0.1055613458 * a - 0.0638541728 * b; - let k_s = -0.0894841775 * a - 1.2914855480 * b; - - { - let l_ = 1. + S * k_l; - let m_ = 1. + S * k_m; - let s_ = 1. + S * k_s; - - let l = l_ * l_ * l_; - let m = m_ * m_ * m_; - let s = s_ * s_ * s_; - - let l_dS = 3. * k_l * l_ * l_; - let m_dS = 3. * k_m * m_ * m_; - let s_dS = 3. * k_s * s_ * s_; - - let l_dS2 = 6. * k_l * k_l * l_; - let m_dS2 = 6. * k_m * k_m * m_; - let s_dS2 = 6. * k_s * k_s * s_; - - let f = wl * l + wm * m + ws * s; - let f1 = wl * l_dS + wm * m_dS + ws * s_dS; - let f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; - - S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); - } - - return S; -} - -// finds L_cusp and C_cusp for a given hue -// a and b must be normalized so a^2 + b^2 == 1 -pub(crate) fn find_cusp(a: f32, b: f32) -> LC { - // First, find the maximum saturation (saturation S = C/L) - let S_cusp = compute_max_saturation(a, b); - - // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: - let rgb_at_max: LinearRgba = Oklaba::lab(1., S_cusp * a, S_cusp * b).into(); - let L_cusp = ops::cbrt((1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue)))); - let C_cusp = L_cusp * S_cusp; - - return LC { - L: L_cusp, - C: C_cusp, - }; -} - -// Finds intersection of the line defined by -// L = L0 * (1 - t) + t * L1; -// C = t * C1; -// a and b must be normalized so a^2 + b^2 == 1 -fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) -> f32 { - // Find the intersection for upper and lower half seprately - let mut t; - if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.) { - // Lower half - - t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); - } else { - // Upper half - - // First intersect with triangle - t = cusp.C * (L0 - 1.) / (C1 * (cusp.L - 1.) + cusp.C * (L0 - L1)); - - // Then one step Halley's method - { - let dL = L1 - L0; - let dC = C1; - - let k_l = 0.3963377774 * a + 0.2158037573 * b; - let k_m = -0.1055613458 * a - 0.0638541728 * b; - let k_s = -0.0894841775 * a - 1.2914855480 * b; - - let l_dt = dL + dC * k_l; - let m_dt = dL + dC * k_m; - let s_dt = dL + dC * k_s; - - // If higher accuracy is required, 2 or 3 iterations of the following block can be used: - { - let L = L0 * (1. - t) + t * L1; - let C = t * C1; - - let l_ = L + C * k_l; - let m_ = L + C * k_m; - let s_ = L + C * k_s; - - let l = l_ * l_ * l_; - let m = m_ * m_ * m_; - let s = s_ * s_ * s_; - - let ldt = 3. * l_dt * l_ * l_; - let mdt = 3. * m_dt * m_ * m_; - let sdt = 3. * s_dt * s_ * s_; - - let ldt2 = 6. * l_dt * l_dt * l_; - let mdt2 = 6. * m_dt * m_dt * m_; - let sdt2 = 6. * s_dt * s_dt * s_; - - let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1.; - let r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; - let r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; - - let u_r = r1 / (r1 * r1 - 0.5 * r * r2); - let mut t_r = -r * u_r; - - let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1.; - let g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; - let g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; - - let u_g = g1 / (g1 * g1 - 0.5 * g * g2); - let mut t_g = -g * u_g; - - let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1.; - let b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; - let b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; - - let u_b = b1 / (b1 * b1 - 0.5 * b * b2); - let mut t_b = -b * u_b; - - t_r = if u_r >= 0. { t_r } else { core::f32::MAX }; - t_g = if u_g >= 0. { t_g } else { core::f32::MAX }; - t_b = if u_b >= 0. { t_b } else { core::f32::MAX }; - - t += (t_r.min(t_g.min(t_b))); - } - } - } - - return t; -} - -#[derive(Clone, Copy)] -struct Cs { - C_0: f32, - C_mid: f32, - C_max: f32, -} - -fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { - let cusp = find_cusp(a_, b_); - - let C_max = find_gamut_intersection(a_, b_, L, 1., L, cusp); - let ST_max = to_ST(cusp); - - // Scale factor to compensate for the curved part of gamut shape: - let k = C_max / ((L * ST_max.S).min((1. - L) * ST_max.T)); - - let C_mid; - { - let ST_mid = get_ST_mid(a_, b_); - - // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - let C_a = L * ST_mid.S; - let C_b = (1. - L) * ST_mid.T; - C_mid = 0.9 - * k - * (1. / (1. / (C_a * C_a * C_a * C_a) + 1. / (C_b * C_b * C_b * C_b))) - .sqrt() - .sqrt(); - } - - let C_0; - { - // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. - let C_a = L * 0.4; - let C_b = (1. - L) * 0.8; - - // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - C_0 = (1. / (1. / (C_a * C_a) + 1. / (C_b * C_b))).sqrt(); - } - - return Cs { C_0, C_mid, C_max }; -} - -// Returns a smooth approximation of the location of the cusp -// This polynomial was created by an optimization process -// It has been designed so that S_mid < S_max and T_mid < T_max -fn get_ST_mid(a_: f32, b_: f32) -> ST { - let S = 0.11516993 - + 1. / (7.44778970 - + 4.15901240 * b_ - + a_ * (-2.19557347 - + 1.75198401 * b_ - + a_ * (-2.13704948 - 10.02301043 * b_ - + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_)))); - - let T = 0.11239642 - + 1. / (1.61320320 - 0.68124379 * b_ - + a_ * (0.40370612 - + 0.90148123 * b_ - + a_ * (-0.27087943 - + 0.61223990 * b_ - + a_ * (0.00299215 - 0.45399568 * b_ - 0.14661872 * a_)))); - - return ST { S, T }; -} - -pub(crate) fn toe(x: f32) -> f32 { - let k_1: f32 = 0.206; - let k_2: f32 = 0.03; - let k_3: f32 = (1. + k_1) / (1. + k_2); - return 0.5 * (k_3 * x - k_1 + ((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x).sqrt()); -} - -pub(crate) fn toe_inv(x: f32) -> f32 { - let k_1 = 0.206; - let k_2 = 0.03; - let k_3 = (1. + k_1) / (1. + k_2); - return (x * x + k_1 * x) / (k_3 * (x + k_2)); -} - impl From for Okhsla { fn from(value: Oklaba) -> Self { - let Oklaba { - lightness: lab_l, - a: lab_a, - b: lab_b, - alpha, - } = value; - let C = (lab_a * lab_a + lab_b * lab_b).sqrt(); - let a_ = lab_a / C; - let b_ = lab_b / C; - - let L = lab_l; - let h = 0.5 + 0.5 * ops::atan2(-lab_b, -lab_a) / core::f32::consts::PI; - - let cs = get_Cs(L, a_, b_); - let C_0 = cs.C_0; - let C_mid = cs.C_mid; - let C_max = cs.C_max; - - // Inverse of the interpolation in okhsl_to_srgb: - - let mid = 0.8; - let mid_inv = 1.25; - - let s; - if (C < C_mid) { - let k_1 = mid * C_0; - let k_2 = (1. - k_1 / C_mid); - - let t = C / (k_1 + k_2 * C); - s = t * mid; - } else { - let k_0 = C_mid; - let k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; - let k_2 = (1. - (k_1) / (C_max - C_mid)); - - let t = (C - k_0) / (k_1 + k_2 * (C - k_0)); - s = mid + (1. - mid) * t; - } - - let l = toe(L); - return Okhsla { - hue: h, - saturation: s, - lightness: l, - alpha, - }; + oklab_to_okhsl(value) } } impl From for Oklaba { fn from(value: Okhsla) -> Self { - let Okhsla { - hue: h, - saturation: s, - lightness: l, - alpha, - } = value; - - if (l == 1.) { - return Oklaba::new(1., 1., 1., alpha); - } else if (l == 0.) { - return Oklaba::new(0., 0., 0., alpha); - } - - let a_ = (2. * core::f32::consts::PI * h).cos(); - let b_ = (2. * core::f32::consts::PI * h).sin(); - let L = toe_inv(l); - - let cs = get_Cs(L, a_, b_); - let C_0 = cs.C_0; - let C_mid = cs.C_mid; - let C_max = cs.C_max; - - let mid = 0.8; - let mid_inv = 1.25; - - let (C, t, k_0, k_1, k_2); - - if (s < mid) { - t = mid_inv * s; - - k_1 = mid * C_0; - k_2 = (1. - k_1 / C_mid); - - C = t * k_1 / (1. - k_2 * t); - } else { - t = (s - mid) / (1. - mid); - - k_0 = C_mid; - k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; - k_2 = (1. - (k_1) / (C_max - C_mid)); - - C = k_0 + t * k_1 / (1. - k_2 * t); - } - - Oklaba::new(L, C * a_, C * b_, alpha) + okhsl_to_oklab(value) } } @@ -770,5 +402,54 @@ impl From for Oklcha { #[cfg(test)] mod tests { use super::*; - use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq}; + use crate::{ + color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq, + }; + + #[test] + fn test_to_from_srgba() { + let okhsla = Okhsla::new(180.0, 0.5, 0.5, 1.0); + let srgba: Srgba = okhsla.into(); + let okhsla2: Okhsla = srgba.into(); + assert_approx_eq!(okhsla.hue, okhsla2.hue, 0.001); + assert_approx_eq!(okhsla.saturation, okhsla2.saturation, 0.001); + assert_approx_eq!(okhsla.lightness, okhsla2.lightness, 0.001); + assert_approx_eq!(okhsla.alpha, okhsla2.alpha, 0.001); + } + + #[test] + fn test_to_from_srgba_2() { + for color in TEST_COLORS.iter() { + let rgb2: Srgba = (color.okhsl).into(); + let okhsl: Okhsla = (color.rgb).into(); + assert!( + color.rgb.distance(&rgb2) < 0.001, + "{}: {:?} != {:?}", + color.name, + color.rgb, + rgb2 + ); + // If lightness is approximately equal to 1.0, hue and saturation are arbitrary. + if color.okhsl.lightness < 0.999 { + // If saturation is approximately equal to 0.0, hue is arbitrary. + if color.okhsl.saturation > 0.001 { + assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001); + } + assert_approx_eq!(color.okhsl.saturation, okhsl.saturation, 0.001); + } + assert_approx_eq!(color.okhsl.lightness, okhsl.lightness, 0.001); + assert_approx_eq!(color.okhsl.alpha, okhsl.alpha, 0.001); + } + } + + #[test] + fn test_to_from_linear() { + let okhsla = Okhsla::new(180.0, 0.5, 0.5, 1.0); + let linear: LinearRgba = okhsla.into(); + let okhsla2: Okhsla = linear.into(); + assert_approx_eq!(okhsla.hue, okhsla2.hue, 0.001); + assert_approx_eq!(okhsla.saturation, okhsla2.saturation, 0.001); + assert_approx_eq!(okhsla.lightness, okhsla2.lightness, 0.001); + assert_approx_eq!(okhsla.alpha, okhsla2.alpha, 0.001); + } } diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index 6deb7c660f4ef..5631fd750a633 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -1,9 +1,10 @@ use crate::{ - okhsla::{find_cusp, to_ST, toe, toe_inv, Okhsla}, + okcolor_convert::{okhsv_to_oklab, oklab_to_okhsv}, + okhsla::Okhsla, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, }; -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -249,104 +250,13 @@ impl ColorToComponents for Okhsva { impl From for Okhsva { fn from(value: Oklaba) -> Self { - let Oklaba { - lightness: lab_l, - a: lab_a, - b: lab_b, - alpha, - } = value; - let mut C = (lab_a * lab_a + lab_b * lab_b).sqrt(); - let a_ = lab_a / C; - let b_ = lab_b / C; - - let mut L = lab_l; - let h = 0.5 + 0.5 * ops::atan2(-lab_b, -lab_a) / core::f32::consts::PI; - - let cusp = find_cusp(a_, b_); - let ST_max = to_ST(cusp); - let S_max = ST_max.S; - let T_max = ST_max.T; - let S_0 = 0.5; - let k = 1. - S_0 / S_max; - - // first we find L_v, C_v, L_vt and C_vt - - let t = T_max / (C + L * T_max); - let L_v = t * L; - let C_v = t * C; - - let L_vt = toe_inv(L_v); - let C_vt = C_v * L_vt / L_v; - - // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle: - let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); - let scale_L = - ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); - - L = L / scale_L; - C = C / scale_L; - - C = C * toe(L) / L; - L = toe(L); - - // we can now compute v and s: - - let v = L / L_v; - let s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); - - return Okhsva { - hue: h, - saturation: s, - value: v, - alpha, - }; + oklab_to_okhsv(value) } } impl From for Oklaba { fn from(value: Okhsva) -> Self { - let Okhsva { - hue: h, - saturation: s, - value: v, - alpha, - } = value; - - let a_ = (2. * core::f32::consts::PI * h).cos(); - let b_ = (2. * core::f32::consts::PI * h).sin(); - - let cusp = find_cusp(a_, b_); - let ST_max = to_ST(cusp); - let S_max = ST_max.S; - let T_max = ST_max.T; - let S_0 = 0.5; - let k = 1. - S_0 / S_max; - - // first we compute L and V as if the gamut is a perfect triangle: - - // L, C when v==1: - let L_v = 1. - s * S_0 / (S_0 + T_max - T_max * k * s); - let C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); - - let mut L = v * L_v; - let mut C = v * C_v; - - // then we compensate for both toe and the curved top part of the triangle: - let L_vt = toe_inv(L_v); - let C_vt = C_v * L_vt / L_v; - - let L_new = toe_inv(L); - C = C * L_new / L; - L = L_new; - - let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); - let scale_L = - ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max((rgb_scale.blue.max(0.))))); - - L = L * scale_L; - C = C * scale_L; - - Oklaba::new(L, C * a_, C * b_, alpha) + okhsv_to_oklab(value) } } @@ -466,4 +376,48 @@ mod tests { use crate::{ color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq, }; + + #[test] + fn test_to_from_srgba() { + let okhsva = Okhsva::new(180.0, 0.5, 0.5, 1.0); + let srgba: Srgba = okhsva.into(); + let okhsva2: Okhsva = srgba.into(); + assert_approx_eq!(okhsva.hue, okhsva2.hue, 0.001); + assert_approx_eq!(okhsva.saturation, okhsva2.saturation, 0.001); + assert_approx_eq!(okhsva.value, okhsva2.value, 0.001); + assert_approx_eq!(okhsva.alpha, okhsva2.alpha, 0.001); + } + + #[test] + fn test_to_from_srgba_2() { + for color in TEST_COLORS.iter() { + let rgb2: Srgba = (color.okhsv).into(); + let okhsv: Okhsva = (color.rgb).into(); + assert!( + color.rgb.distance(&rgb2) < 0.01, + "{}: {:?} != {:?}", + color.name, + color.rgb, + rgb2, + ); + // If saturation is approximately equal to 0.0, hue is arbitrary. + if color.okhsv.saturation > 0.001 { + assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001); + } + assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001); + assert_approx_eq!(color.okhsv.value, okhsv.value, 0.001); + assert_approx_eq!(color.okhsv.alpha, okhsv.alpha, 0.001); + } + } + + #[test] + fn test_to_from_linear() { + let okhsva = Okhsva::new(0.5, 0.5, 0.5, 1.0); + let linear: LinearRgba = okhsva.into(); + let okhsva2: Okhsva = linear.into(); + assert_approx_eq!(okhsva.hue, okhsva2.hue, 0.001); + assert_approx_eq!(okhsva.saturation, okhsva2.saturation, 0.001); + assert_approx_eq!(okhsva.value, okhsva2.value, 0.001); + assert_approx_eq!(okhsva.alpha, okhsva2.alpha, 0.001); + } } diff --git a/crates/bevy_color/src/test_colors.rs b/crates/bevy_color/src/test_colors.rs index 9fd479c0af5e3..348df6b49fef3 100644 --- a/crates/bevy_color/src/test_colors.rs +++ b/crates/bevy_color/src/test_colors.rs @@ -1,6 +1,8 @@ // Generated by gen_tests. Do not edit. #[cfg(test)] -use crate::{Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, Xyza}; +use crate::{ + okhsla::Okhsla, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Okhsva, Oklaba, Oklcha, Srgba, Xyza, +}; #[cfg(test)] pub struct TestColor { @@ -15,6 +17,8 @@ pub struct TestColor { pub oklab: Oklaba, pub oklch: Oklcha, pub xyz: Xyza, + pub okhsl: Okhsla, + pub okhsv: Okhsva, } // Table of equivalent colors in various color spaces @@ -26,13 +30,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 0.0, 0.0, 1.0), linear_rgb: LinearRgba::new(0.0, 0.0, 0.0, 1.0), hsl: Hsla::new(0.0, 0.0, 0.0, 1.0), - lch: Lcha::new(0.0, 0.0, 0.0000136603785, 1.0), hsv: Hsva::new(0.0, 0.0, 0.0, 1.0), hwb: Hwba::new(0.0, 0.0, 1.0, 1.0), lab: Laba::new(0.0, 0.0, 0.0, 1.0), + lch: Lcha::new(0.0, 0.0, 0.0, 1.0), oklab: Oklaba::new(0.0, 0.0, 0.0, 1.0), oklch: Oklcha::new(0.0, 0.0, 0.0, 1.0), xyz: Xyza::new(0.0, 0.0, 0.0, 1.0), + okhsl: Okhsla::new(0.0, 0.0, 0.0, 1.0), + okhsv: Okhsva::new(0.0, 0.0, 0.0, 1.0), }, // white TestColor { @@ -40,13 +46,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(1.0, 1.0, 1.0, 1.0), linear_rgb: LinearRgba::new(1.0, 1.0, 1.0, 1.0), hsl: Hsla::new(0.0, 0.0, 1.0, 1.0), - lch: Lcha::new(1.0, 0.0, 0.0000136603785, 1.0), hsv: Hsva::new(0.0, 0.0, 1.0, 1.0), hwb: Hwba::new(0.0, 1.0, 0.0, 1.0), lab: Laba::new(1.0, 0.0, 0.0, 1.0), + lch: Lcha::new(1.0, 0.0, 0.0, 1.0), oklab: Oklaba::new(1.0, 0.0, 0.000000059604645, 1.0), oklch: Oklcha::new(1.0, 0.000000059604645, 90.0, 1.0), xyz: Xyza::new(0.95047, 1.0, 1.08883, 1.0), + okhsl: Okhsla::new(89.875565, 0.5582832, 1.0, 1.0), + okhsv: Okhsva::new(89.875565, 0.00000010347524, 1.0, 1.0), }, // red TestColor { @@ -54,13 +62,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(1.0, 0.0, 0.0, 1.0), linear_rgb: LinearRgba::new(1.0, 0.0, 0.0, 1.0), hsl: Hsla::new(0.0, 1.0, 0.5, 1.0), - lch: Lcha::new(0.53240794, 1.0455177, 39.99901, 1.0), - oklab: Oklaba::new(0.6279554, 0.22486295, 0.1258463, 1.0), hsv: Hsva::new(0.0, 1.0, 1.0, 1.0), hwb: Hwba::new(0.0, 0.0, 0.0, 1.0), - lab: Laba::new(0.532408, 0.8009243, 0.6720321, 1.0), - oklch: Oklcha::new(0.6279554, 0.2576833, 29.233892, 1.0), + lab: Laba::new(0.53240794, 0.8009246, 0.67203194, 1.0), + lch: Lcha::new(0.53240794, 1.0455177, 39.99901, 1.0), + oklab: Oklaba::new(0.6279554, 0.22486295, 0.1258463, 1.0), + oklch: Oklcha::new(0.6279554, 0.25768322, 29.233906, 1.0), xyz: Xyza::new(0.4124564, 0.2126729, 0.0193339, 1.0), + okhsl: Okhsla::new(29.233885, 1.0, 0.56808466, 1.0), + okhsv: Okhsva::new(29.233885, 0.999522, 1.0, 1.0), }, // green TestColor { @@ -68,13 +78,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 1.0, 0.0, 1.0), linear_rgb: LinearRgba::new(0.0, 1.0, 0.0, 1.0), hsl: Hsla::new(120.0, 1.0, 0.5, 1.0), - lch: Lcha::new(0.87734723, 1.1977587, 136.01595, 1.0), hsv: Hsva::new(120.0, 1.0, 1.0, 1.0), hwb: Hwba::new(120.0, 0.0, 0.0, 1.0), - lab: Laba::new(0.8773472, -0.86182654, 0.8317931, 1.0), + lab: Laba::new(0.87734723, -0.86182714, 0.8317932, 1.0), + lch: Lcha::new(0.87734723, 1.1977587, 136.01595, 1.0), oklab: Oklaba::new(0.8664396, -0.2338874, 0.1794985, 1.0), oklch: Oklcha::new(0.8664396, 0.2948271, 142.49532, 1.0), xyz: Xyza::new(0.3575761, 0.7151522, 0.119192, 1.0), + okhsl: Okhsla::new(142.49535, 0.99999994, 0.844529, 1.0), + okhsv: Okhsva::new(142.49535, 0.9999997, 1.0, 1.0), }, // blue TestColor { @@ -82,13 +94,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 0.0, 1.0, 1.0), linear_rgb: LinearRgba::new(0.0, 0.0, 1.0, 1.0), hsl: Hsla::new(240.0, 1.0, 0.5, 1.0), - lch: Lcha::new(0.32297012, 1.3380761, 306.28494, 1.0), - oklab: Oklaba::new(0.4520137, -0.032456964, -0.31152815, 1.0), hsv: Hsva::new(240.0, 1.0, 1.0, 1.0), hwb: Hwba::new(240.0, 0.0, 0.0, 1.0), - lab: Laba::new(0.32297015, 0.7918751, -1.0786015, 1.0), - oklch: Oklcha::new(0.45201376, 0.31321433, 264.05203, 1.0), + lab: Laba::new(0.32297012, 0.7918753, -1.0786016, 1.0), + lch: Lcha::new(0.32297012, 1.3380761, 306.28494, 1.0), + oklab: Oklaba::new(0.4520137, -0.032456964, -0.31152815, 1.0), + oklch: Oklcha::new(0.4520137, 0.31321436, 264.05203, 1.0), xyz: Xyza::new(0.1804375, 0.072175, 0.9503041, 1.0), + okhsl: Okhsla::new(264.05203, 1.0, 0.36656535, 1.0), + okhsv: Okhsva::new(264.05203, 0.9999911, 0.99999994, 1.0), }, // yellow TestColor { @@ -96,13 +110,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(1.0, 1.0, 0.0, 1.0), linear_rgb: LinearRgba::new(1.0, 1.0, 0.0, 1.0), hsl: Hsla::new(60.0, 1.0, 0.5, 1.0), - lch: Lcha::new(0.9713927, 0.96905375, 102.85126, 1.0), - oklab: Oklaba::new(0.9679827, -0.07136908, 0.19856972, 1.0), hsv: Hsva::new(60.0, 1.0, 1.0, 1.0), hwb: Hwba::new(60.0, 0.0, 0.0, 1.0), - lab: Laba::new(0.9713927, -0.21553725, 0.94477975, 1.0), - oklch: Oklcha::new(0.9679827, 0.21100593, 109.76923, 1.0), + lab: Laba::new(0.9713927, -0.21553755, 0.94477975, 1.0), + lch: Lcha::new(0.9713927, 0.96905375, 102.85126, 1.0), + oklab: Oklaba::new(0.9679827, -0.07136908, 0.19856972, 1.0), + oklch: Oklcha::new(0.9679827, 0.21100587, 109.76924, 1.0), xyz: Xyza::new(0.7700325, 0.9278251, 0.1385259, 1.0), + okhsl: Okhsla::new(109.76923, 1.0, 0.9627044, 1.0), + okhsv: Okhsva::new(109.76923, 1.0000005, 1.0, 1.0), }, // magenta TestColor { @@ -115,8 +131,10 @@ pub const TEST_COLORS: &[TestColor] = &[ lab: Laba::new(0.6032421, 0.9823433, -0.60824895, 1.0), lch: Lcha::new(0.6032421, 1.1554068, 328.23495, 1.0), oklab: Oklaba::new(0.7016738, 0.27456632, -0.16915613, 1.0), - oklch: Oklcha::new(0.7016738, 0.32249108, 328.36343, 1.0), + oklch: Oklcha::new(0.7016738, 0.32249102, 328.36343, 1.0), xyz: Xyza::new(0.5928939, 0.28484792, 0.969638, 1.0), + okhsl: Okhsla::new(328.3634, 1.000039, 0.65329874, 1.0), + okhsv: Okhsva::new(328.3634, 1.0001222, 1.0, 1.0), }, // cyan TestColor { @@ -124,13 +142,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 1.0, 1.0, 1.0), linear_rgb: LinearRgba::new(0.0, 1.0, 1.0, 1.0), hsl: Hsla::new(180.0, 1.0, 0.5, 1.0), - lch: Lcha::new(0.9111322, 0.50120866, 196.37614, 1.0), - oklab: Oklaba::new(0.90539926, -0.1494439, -0.039398134, 1.0), hsv: Hsva::new(180.0, 1.0, 1.0, 1.0), hwb: Hwba::new(180.0, 0.0, 0.0, 1.0), - lab: Laba::new(0.9111321, -0.4808751, -0.14131188, 1.0), - oklch: Oklcha::new(0.9053992, 0.15454963, 194.76901, 1.0), + lab: Laba::new(0.9111322, -0.48087537, -0.14131176, 1.0), + lch: Lcha::new(0.9111322, 0.50120866, 196.37614, 1.0), + oklab: Oklaba::new(0.90539926, -0.1494439, -0.039398134, 1.0), + oklch: Oklcha::new(0.90539926, 0.15454996, 194.76895, 1.0), xyz: Xyza::new(0.5380136, 0.78732723, 1.069496, 1.0), + okhsl: Okhsla::new(194.76895, 1.0, 0.8898483, 1.0), + okhsv: Okhsva::new(194.76895, 0.9999994, 1.0, 1.0), }, // gray TestColor { @@ -138,13 +158,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.5, 0.5, 0.5, 1.0), linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.21404114, 1.0), hsl: Hsla::new(0.0, 0.0, 0.5, 1.0), - lch: Lcha::new(0.5338897, 0.00000011920929, 90.0, 1.0), - oklab: Oklaba::new(0.5981807, 0.00000011920929, 0.0, 1.0), hsv: Hsva::new(0.0, 0.0, 0.5, 1.0), hwb: Hwba::new(0.0, 0.5, 0.5, 1.0), - lab: Laba::new(0.5338897, 0.0, 0.0, 1.0), - oklch: Oklcha::new(0.5981808, 0.00000023841858, 0.0, 1.0), + lab: Laba::new(0.5338897, 0.0, 0.00000011920929, 1.0), + lch: Lcha::new(0.5338897, 0.00000011920929, 90.0, 1.0), + oklab: Oklaba::new(0.5981807, 0.00000011920929, 0.0, 1.0), + oklch: Oklcha::new(0.5981807, 0.00000011920929, 0.0, 1.0), xyz: Xyza::new(0.2034397, 0.21404117, 0.23305441, 1.0), + okhsl: Okhsla::new(89.875565, 0.00000011616558, 0.53375983, 1.0), + okhsv: Okhsva::new(89.875565, 0.00000010347524, 0.53375983, 1.0), }, // olive TestColor { @@ -152,13 +174,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.5, 0.5, 0.0, 1.0), linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.0, 1.0), hsl: Hsla::new(60.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0), hsv: Hsva::new(60.0, 1.0, 0.5, 1.0), hwb: Hwba::new(60.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.51677734, -0.1289308, 0.5651491, 1.0), + lab: Laba::new(0.51677734, -0.12893051, 0.5651491, 1.0), + lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0), oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0), oklch: Oklcha::new(0.57902855, 0.12621966, 109.76922, 1.0), xyz: Xyza::new(0.16481864, 0.19859275, 0.029650241, 1.0), + okhsl: Okhsla::new(109.76923, 1.0000005, 0.51171625, 1.0), + okhsv: Okhsva::new(109.76923, 1.0000005, 0.5318635, 1.0), }, // purple TestColor { @@ -166,13 +190,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.5, 0.0, 0.5, 1.0), linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.21404114, 1.0), hsl: Hsla::new(300.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0), hsv: Hsva::new(300.0, 1.0, 0.5, 1.0), hwb: Hwba::new(300.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.29655674, 0.58761847, -0.3638428, 1.0), + lab: Laba::new(0.29655674, 0.58761877, -0.3638428, 1.0), + lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0), oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0), oklch: Oklcha::new(0.41972777, 0.19290791, 328.36343, 1.0), xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0), + okhsl: Okhsla::new(328.3634, 1.0006211, 0.33011043, 1.0), + okhsv: Okhsva::new(328.3634, 1.0001222, 0.51061976, 1.0), }, // teal TestColor { @@ -180,13 +206,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 0.5, 0.5, 1.0), linear_rgb: LinearRgba::new(0.0, 0.21404114, 0.21404114, 1.0), hsl: Hsla::new(180.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0), - oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0), hsv: Hsva::new(180.0, 1.0, 0.5, 1.0), hwb: Hwba::new(180.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.4807306, -0.28765023, -0.084530115, 1.0), - oklch: Oklcha::new(0.54159236, 0.092448615, 194.76903, 1.0), + lab: Laba::new(0.48073065, -0.28765038, -0.08452999, 1.0), + lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0), + oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0), + oklch: Oklcha::new(0.54159236, 0.09244873, 194.769, 1.0), xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0), + okhsl: Okhsla::new(194.76895, 0.99999946, 0.46872336, 1.0), + okhsv: Okhsva::new(194.76895, 0.9999994, 0.52782416, 1.0), }, // maroon TestColor { @@ -201,6 +229,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.3756308, 0.13450874, 0.07527886, 1.0), oklch: Oklcha::new(0.3756308, 0.1541412, 29.233906, 1.0), xyz: Xyza::new(0.08828264, 0.045520753, 0.0041382504, 1.0), + okhsl: Okhsla::new(29.233885, 0.9996788, 0.28080443, 1.0), + okhsv: Okhsva::new(29.233885, 0.999522, 0.5022645, 1.0), }, // lime TestColor { @@ -213,8 +243,10 @@ pub const TEST_COLORS: &[TestColor] = &[ lab: Laba::new(0.46052113, -0.5155285, 0.4975627, 1.0), lch: Lcha::new(0.46052113, 0.71647626, 136.01596, 1.0), oklab: Oklaba::new(0.5182875, -0.13990697, 0.10737252, 1.0), - oklch: Oklcha::new(0.5182875, 0.17635989, 142.49535, 1.0), + oklch: Oklcha::new(0.5182875, 0.17635992, 142.49535, 1.0), xyz: Xyza::new(0.076536, 0.153072, 0.025511991, 1.0), + okhsl: Okhsla::new(142.49535, 0.99999994, 0.44203484, 1.0), + okhsv: Okhsva::new(142.49535, 0.9999997, 0.5250593, 1.0), }, // navy TestColor { @@ -222,13 +254,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 0.0, 0.5, 1.0), linear_rgb: LinearRgba::new(0.0, 0.0, 0.21404114, 1.0), hsl: Hsla::new(240.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.12890343, 0.8004114, 306.28494, 1.0), hsv: Hsva::new(240.0, 1.0, 0.5, 1.0), hwb: Hwba::new(240.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.12890343, 0.4736844, -0.64519864, 1.0), + lab: Laba::new(0.12890343, 0.4736845, -0.64519864, 1.0), + lch: Lcha::new(0.12890343, 0.8004114, 306.28494, 1.0), oklab: Oklaba::new(0.27038592, -0.01941514, -0.18635012, 1.0), oklch: Oklcha::new(0.27038592, 0.18735878, 264.05203, 1.0), xyz: Xyza::new(0.03862105, 0.01544842, 0.20340417, 1.0), + okhsl: Okhsla::new(264.05203, 0.9999977, 0.16734318, 1.0), + okhsv: Okhsva::new(264.05203, 0.9999911, 0.47496665, 1.0), }, // orange TestColor { @@ -236,13 +270,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.5, 0.5, 0.0, 1.0), linear_rgb: LinearRgba::new(0.21404114, 0.21404114, 0.0, 1.0), hsl: Hsla::new(60.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0), hsv: Hsva::new(60.0, 1.0, 0.5, 1.0), hwb: Hwba::new(60.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.51677734, -0.1289308, 0.5651491, 1.0), + lab: Laba::new(0.51677734, -0.12893051, 0.5651491, 1.0), + lch: Lcha::new(0.51677734, 0.57966936, 102.851265, 1.0), oklab: Oklaba::new(0.57902855, -0.042691574, 0.11878061, 1.0), oklch: Oklcha::new(0.57902855, 0.12621966, 109.76922, 1.0), xyz: Xyza::new(0.16481864, 0.19859275, 0.029650241, 1.0), + okhsl: Okhsla::new(109.76923, 1.0000005, 0.51171625, 1.0), + okhsv: Okhsva::new(109.76923, 1.0000005, 0.5318635, 1.0), }, // fuchsia TestColor { @@ -250,13 +286,15 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.5, 0.0, 0.5, 1.0), linear_rgb: LinearRgba::new(0.21404114, 0.0, 0.21404114, 1.0), hsl: Hsla::new(300.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0), hsv: Hsva::new(300.0, 1.0, 0.5, 1.0), hwb: Hwba::new(300.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.29655674, 0.58761847, -0.3638428, 1.0), + lab: Laba::new(0.29655674, 0.58761877, -0.3638428, 1.0), + lch: Lcha::new(0.29655674, 0.69114214, 328.23495, 1.0), oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0), oklch: Oklcha::new(0.41972777, 0.19290791, 328.36343, 1.0), xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0), + okhsl: Okhsla::new(328.3634, 1.0006211, 0.33011043, 1.0), + okhsv: Okhsva::new(328.3634, 1.0001222, 0.51061976, 1.0), }, // aqua TestColor { @@ -264,12 +302,14 @@ pub const TEST_COLORS: &[TestColor] = &[ rgb: Srgba::new(0.0, 0.5, 0.5, 1.0), linear_rgb: LinearRgba::new(0.0, 0.21404114, 0.21404114, 1.0), hsl: Hsla::new(180.0, 1.0, 0.25, 1.0), - lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0), - oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0), hsv: Hsva::new(180.0, 1.0, 0.5, 1.0), hwb: Hwba::new(180.0, 0.0, 0.5, 1.0), - lab: Laba::new(0.4807306, -0.28765023, -0.084530115, 1.0), - oklch: Oklcha::new(0.54159236, 0.092448615, 194.76903, 1.0), + lab: Laba::new(0.48073065, -0.28765038, -0.08452999, 1.0), + lch: Lcha::new(0.48073065, 0.29981336, 196.37614, 1.0), + oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0), + oklch: Oklcha::new(0.54159236, 0.09244873, 194.769, 1.0), xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0), + okhsl: Okhsla::new(194.76895, 0.99999946, 0.46872336, 1.0), + okhsv: Okhsva::new(194.76895, 0.9999994, 0.52782416, 1.0), }, ]; From 7bd914a65a55f065fce4fa8139a3a08a1b754f68 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 5 May 2026 23:46:43 +0800 Subject: [PATCH 03/11] clippy --- crates/bevy_color/src/color.rs | 8 +- crates/bevy_color/src/lib.rs | 2 - crates/bevy_color/src/okcolor_convert.rs | 178 ++++++++++++----------- crates/bevy_color/src/okhsla.rs | 2 +- 4 files changed, 96 insertions(+), 94 deletions(-) diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 9134a900a5d2a..d7c478601fb89 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -75,9 +75,9 @@ pub enum Color { Oklcha(Oklcha), /// A color in the XYZ color space with alpha. Xyza(Xyza), - /// + /// A color in the Okhsl color space with alpha. Okhsla(Okhsla), - /// + /// A color in the Okhsv color space with alpha. Okhsva(Okhsva), } @@ -906,8 +906,8 @@ impl Hue for Color { Color::Oklaba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), Color::Oklcha(x) => *x = x.with_hue(hue), Color::Xyza(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), - Color::Okhsla(x) => *x = x.with_hue(hue).into(), - Color::Okhsva(x) => *x = x.with_hue(hue).into(), + Color::Okhsla(x) => *x = x.with_hue(hue), + Color::Okhsva(x) => *x = x.with_hue(hue), } new diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index cead0cb5943b3..731a4f1254a95 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -283,5 +283,3 @@ macro_rules! impl_componentwise_vector_space { } pub(crate) use impl_componentwise_vector_space; - -use crate::okhsla::Okhsla; diff --git a/crates/bevy_color/src/okcolor_convert.rs b/crates/bevy_color/src/okcolor_convert.rs index 746959145b4e4..6827b9589a249 100644 --- a/crates/bevy_color/src/okcolor_convert.rs +++ b/crates/bevy_color/src/okcolor_convert.rs @@ -1,3 +1,10 @@ +//! Functions for Okhsl/Okhsv <-> Oklab conversion. See + +#![expect( + non_snake_case, + reason = "The code is translated from a C implementation." +)] + use crate::{okhsla::Okhsla, LinearRgba, Okhsva, Oklaba}; use bevy_math::ops; @@ -16,10 +23,10 @@ pub(crate) struct ST { pub(crate) fn to_ST(cusp: LC) -> ST { let L = cusp.L; let C = cusp.C; - return ST { + ST { S: C / L, T: C / (1. - L), - }; + } } // Finds the maximum saturation possible for a given hue that fits in sRGB @@ -31,36 +38,36 @@ fn compute_max_saturation(a: f32, b: f32) -> f32 { // Select different coefficients depending on which component goes below zero first let (k0, k1, k2, k3, k4, wl, wm, ws); - if (-1.88170328 * a - 0.80936493 * b > 1.) { + if -1.881_703_3 * a - 0.809_364_9 * b > 1. { // Red component - k0 = 1.19086277; - k1 = 1.76576728; - k2 = 0.59662641; - k3 = 0.75515197; - k4 = 0.56771245; - wl = 4.0767416621; - wm = -3.3077115913; - ws = 0.2309699292; - } else if (1.81444104 * a - 1.19445276 * b > 1.) { + k0 = 1.190_862_8; + k1 = 1.765_767_3; + k2 = 0.596_626_4; + k3 = 0.755_152; + k4 = 0.567_712_4; + wl = 4.076_741_7; + wm = -3.307_711_6; + ws = 0.230_969_94; + } else if 1.814_441_1 * a - 1.194_452_8 * b > 1. { // Green component k0 = 0.73956515; k1 = -0.45954404; k2 = 0.08285427; - k3 = 0.12541070; + k3 = 0.125_410_7; k4 = 0.14503204; - wl = -1.2684380046; - wm = 2.6097574011; - ws = -0.3413193965; + wl = -1.268_438; + wm = 2.609_757_4; + ws = -0.341_319_38; } else { // Blue component - k0 = 1.35733652; + k0 = 1.357_336_5; k1 = -0.00915799; - k2 = -1.15130210; + k2 = -1.151_302_1; k3 = -0.50559606; k4 = 0.00692167; wl = -0.0041960863; - wm = -0.7034186147; - ws = 1.7076147010; + wm = -0.703_418_6; + ws = 1.707_614_7; } // Approximate max saturation using a polynomial: @@ -70,9 +77,9 @@ fn compute_max_saturation(a: f32, b: f32) -> f32 { // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite // this should be sufficient for most applications, otherwise do two/three steps - let k_l = 0.3963377774 * a + 0.2158037573 * b; - let k_m = -0.1055613458 * a - 0.0638541728 * b; - let k_s = -0.0894841775 * a - 1.2914855480 * b; + let k_l = 0.396_337_78 * a + 0.215_803_76 * b; + let k_m = -0.105_561_346 * a - 0.063_854_17 * b; + let k_s = -0.089_484_18 * a - 1.291_485_5 * b; { let l_ = 1. + S * k_l; @@ -95,10 +102,10 @@ fn compute_max_saturation(a: f32, b: f32) -> f32 { let f1 = wl * l_dS + wm * m_dS + ws * s_dS; let f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; - S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); + S -= f * f1 / (f1 * f1 - 0.5 * f * f2); } - return S; + S } // finds L_cusp and C_cusp for a given hue @@ -109,13 +116,13 @@ pub(crate) fn find_cusp(a: f32, b: f32) -> LC { // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: let rgb_at_max: LinearRgba = Oklaba::lab(1., S_cusp * a, S_cusp * b).into(); - let L_cusp = ops::cbrt((1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue)))); + let L_cusp = ops::cbrt(1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue))); let C_cusp = L_cusp * S_cusp; - return LC { + LC { L: L_cusp, C: C_cusp, - }; + } } // Finds intersection of the line defined by @@ -125,7 +132,7 @@ pub(crate) fn find_cusp(a: f32, b: f32) -> LC { fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) -> f32 { // Find the intersection for upper and lower half seprately let mut t; - if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.) { + if ((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0. { // Lower half t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); @@ -140,9 +147,9 @@ fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) let dL = L1 - L0; let dC = C1; - let k_l = 0.3963377774 * a + 0.2158037573 * b; - let k_m = -0.1055613458 * a - 0.0638541728 * b; - let k_s = -0.0894841775 * a - 1.2914855480 * b; + let k_l = 0.396_337_78 * a + 0.215_803_76 * b; + let k_m = -0.105_561_346 * a - 0.063_854_17 * b; + let k_s = -0.089_484_18 * a - 1.291_485_5 * b; let l_dt = dL + dC * k_l; let m_dt = dL + dC * k_m; @@ -169,37 +176,37 @@ fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) let mdt2 = 6. * m_dt * m_dt * m_; let sdt2 = 6. * s_dt * s_dt * s_; - let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1.; - let r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; - let r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; + let r = 4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s - 1.; + let r1 = 4.076_741_7 * ldt - 3.307_711_6 * mdt + 0.230_969_94 * sdt; + let r2 = 4.076_741_7 * ldt2 - 3.307_711_6 * mdt2 + 0.230_969_94 * sdt2; let u_r = r1 / (r1 * r1 - 0.5 * r * r2); let mut t_r = -r * u_r; - let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1.; - let g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; - let g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; + let g = -1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s - 1.; + let g1 = -1.268_438 * ldt + 2.609_757_4 * mdt - 0.341_319_38 * sdt; + let g2 = -1.268_438 * ldt2 + 2.609_757_4 * mdt2 - 0.341_319_38 * sdt2; let u_g = g1 / (g1 * g1 - 0.5 * g * g2); let mut t_g = -g * u_g; - let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1.; - let b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; - let b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; + let b = -0.0041960863 * l - 0.703_418_6 * m + 1.707_614_7 * s - 1.; + let b1 = -0.0041960863 * ldt - 0.703_418_6 * mdt + 1.707_614_7 * sdt; + let b2 = -0.0041960863 * ldt2 - 0.703_418_6 * mdt2 + 1.707_614_7 * sdt2; let u_b = b1 / (b1 * b1 - 0.5 * b * b2); let mut t_b = -b * u_b; - t_r = if u_r >= 0. { t_r } else { core::f32::MAX }; - t_g = if u_g >= 0. { t_g } else { core::f32::MAX }; - t_b = if u_b >= 0. { t_b } else { core::f32::MAX }; + t_r = if u_r >= 0. { t_r } else { f32::MAX }; + t_g = if u_g >= 0. { t_g } else { f32::MAX }; + t_b = if u_b >= 0. { t_b } else { f32::MAX }; - t += (t_r.min(t_g.min(t_b))); + t += t_r.min(t_g.min(t_b)); } } } - return t; + t } #[derive(Clone, Copy)] @@ -242,7 +249,7 @@ fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { C_0 = (1. / (1. / (C_a * C_a) + 1. / (C_b * C_b))).sqrt(); } - return Cs { C_0, C_mid, C_max }; + Cs { C_0, C_mid, C_max } } // Returns a smooth approximation of the location of the cusp @@ -250,36 +257,36 @@ fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { // It has been designed so that S_mid < S_max and T_mid < T_max fn get_ST_mid(a_: f32, b_: f32) -> ST { let S = 0.11516993 - + 1. / (7.44778970 - + 4.15901240 * b_ - + a_ * (-2.19557347 - + 1.75198401 * b_ - + a_ * (-2.13704948 - 10.02301043 * b_ - + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_)))); + + 1. / (7.447_789_7 + + 4.159_012_3 * b_ + + a_ * (-2.195_573_6 + + 1.751_984 * b_ + + a_ * (-2.137_049_4 - 10.023_01 * b_ + + a_ * (-4.248_945_7 + 5.387_708 * b_ + 4.698_91 * a_)))); let T = 0.11239642 - + 1. / (1.61320320 - 0.68124379 * b_ + + 1. / (1.613_203_2 - 0.681_243_8 * b_ + a_ * (0.40370612 - + 0.90148123 * b_ + + 0.901_481_2 * b_ + a_ * (-0.27087943 - + 0.61223990 * b_ + + 0.612_239_9 * b_ + a_ * (0.00299215 - 0.45399568 * b_ - 0.14661872 * a_)))); - return ST { S, T }; + ST { S, T } } pub(crate) fn toe(x: f32) -> f32 { let k_1: f32 = 0.206; let k_2: f32 = 0.03; let k_3: f32 = (1. + k_1) / (1. + k_2); - return 0.5 * (k_3 * x - k_1 + ((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x).sqrt()); + 0.5 * (k_3 * x - k_1 + ((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x).sqrt()) } pub(crate) fn toe_inv(x: f32) -> f32 { let k_1 = 0.206; let k_2 = 0.03; let k_3 = (1. + k_1) / (1. + k_2); - return (x * x + k_1 * x) / (k_3 * (x + k_2)); + (x * x + k_1 * x) / (k_3 * (x + k_2)) } pub(crate) fn oklab_to_okhsl(value: Oklaba) -> Okhsla { @@ -306,29 +313,28 @@ pub(crate) fn oklab_to_okhsl(value: Oklaba) -> Okhsla { let mid = 0.8; let mid_inv = 1.25; - let s; - if (C < C_mid) { + let s = if C < C_mid { let k_1 = mid * C_0; - let k_2 = (1. - k_1 / C_mid); + let k_2 = 1. - k_1 / C_mid; let t = C / (k_1 + k_2 * C); - s = t * mid; + t * mid } else { let k_0 = C_mid; let k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; - let k_2 = (1. - (k_1) / (C_max - C_mid)); + let k_2 = 1. - (k_1) / (C_max - C_mid); let t = (C - k_0) / (k_1 + k_2 * (C - k_0)); - s = mid + (1. - mid) * t; - } + mid + (1. - mid) * t + }; let l = toe(L); - return Okhsla { + Okhsla { hue: h * 360., saturation: s, lightness: l, alpha, - }; + } } pub(crate) fn okhsl_to_oklab(value: Okhsla) -> Oklaba { @@ -340,14 +346,14 @@ pub(crate) fn okhsl_to_oklab(value: Okhsla) -> Oklaba { } = value; let h = h / 360.; - if (l == 1.) { + if l == 1. { return LinearRgba::new(1., 1., 1., alpha).into(); - } else if (l == 0.) { + } else if l == 0. { return LinearRgba::new(0., 0., 0., alpha).into(); } - let a_ = (2. * core::f32::consts::PI * h).cos(); - let b_ = (2. * core::f32::consts::PI * h).sin(); + let a_ = ops::cos(2. * core::f32::consts::PI * h); + let b_ = ops::sin(2. * core::f32::consts::PI * h); let L = toe_inv(l); let cs = get_Cs(L, a_, b_); @@ -360,11 +366,11 @@ pub(crate) fn okhsl_to_oklab(value: Okhsla) -> Oklaba { let (C, t, k_0, k_1, k_2); - if (s < mid) { + if s < mid { t = mid_inv * s; k_1 = mid * C_0; - k_2 = (1. - k_1 / C_mid); + k_2 = 1. - k_1 / C_mid; C = t * k_1 / (1. - k_2 * t); } else { @@ -372,7 +378,7 @@ pub(crate) fn okhsl_to_oklab(value: Okhsla) -> Oklaba { k_0 = C_mid; k_1 = (1. - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; - k_2 = (1. - (k_1) / (C_max - C_mid)); + k_2 = 1. - (k_1) / (C_max - C_mid); C = k_0 + t * k_1 / (1. - k_2 * t); } @@ -387,7 +393,7 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { b: lab_b, alpha, } = value; - let mut C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let C = (lab_a * lab_a + lab_b * lab_b).sqrt(); let a_ = lab_a / C; let b_ = lab_b / C; @@ -415,10 +421,8 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { let scale_L = ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); - L = L / scale_L; - C = C / scale_L; + L /= scale_L; - C = C * toe(L) / L; L = toe(L); // we can now compute v and s: @@ -426,12 +430,12 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { let v = L / L_v; let s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); - return Okhsva { + Okhsva { hue: h * 360., saturation: s, value: v, alpha, - }; + } } pub(crate) fn okhsv_to_oklab(value: Okhsva) -> Oklaba { @@ -443,12 +447,12 @@ pub(crate) fn okhsv_to_oklab(value: Okhsva) -> Oklaba { } = value; let h = h / 360.; - if (v == 0.) { + if v == 0. { return LinearRgba::new(0., 0., 0., alpha).into(); } - let a_ = (2. * core::f32::consts::PI * h).cos(); - let b_ = (2. * core::f32::consts::PI * h).sin(); + let a_ = ops::cos(2. * core::f32::consts::PI * h); + let b_ = ops::sin(2. * core::f32::consts::PI * h); let cusp = find_cusp(a_, b_); let ST_max = to_ST(cusp); @@ -476,10 +480,10 @@ pub(crate) fn okhsv_to_oklab(value: Okhsva) -> Oklaba { let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); let scale_L = - ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max((rgb_scale.blue.max(0.))))); + ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); - L = L * scale_L; - C = C * scale_L; + L *= scale_L; + C *= scale_L; Oklaba::new(L, C * a_, C * b_, alpha) } diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index 917f7412115ec..934cb826b0daf 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -4,7 +4,7 @@ use crate::{ Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, }; -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; From 04e26a4961227e31602a022fb39235afead9debc Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 5 May 2026 23:50:07 +0800 Subject: [PATCH 04/11] Fix StandardColor and docs --- crates/bevy_color/src/lib.rs | 1 + crates/bevy_color/src/okhsla.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 731a4f1254a95..d8cb691053506 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -177,6 +177,7 @@ where Self: From + Into, Self: From + Into, Self: From + Into, + Self: From + Into, Self: Alpha, { } diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index 934cb826b0daf..9ec5be780a1cd 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -57,7 +57,7 @@ impl Okhsla { } } - /// Construct a new [`Hsla`] color from (h, s, l) components, with the default alpha (1.0). + /// Construct a new [`Okhsla`] color from (h, s, l) components, with the default alpha (1.0). /// /// # Arguments /// @@ -93,7 +93,7 @@ impl Okhsla { /// let color = Hsla::sequential_dispersed(entity_index); /// /// // Palette with 5 distinct hues - /// let palette = (0..5).map(Hsla::sequential_dispersed).collect::>(); + /// let palette = (0..5).map(Okhsla::sequential_dispersed).collect::>(); /// ``` pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up From 27efc4bbd80352ee3417c317c2f7148e2b3e4ba3 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 6 May 2026 00:55:45 +0800 Subject: [PATCH 05/11] CI --- .../bevy_color/crates/gen_tests/src/main.rs | 16 ++++++++++++++-- crates/bevy_color/src/okcolor_convert.rs | 19 ++++++++++--------- crates/bevy_color/src/okhsla.rs | 8 ++++---- crates/bevy_color/src/okhsva.rs | 13 ++++++++----- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/crates/bevy_color/crates/gen_tests/src/main.rs b/crates/bevy_color/crates/gen_tests/src/main.rs index 7dbd40af3aceb..d890cdce72fbc 100644 --- a/crates/bevy_color/crates/gen_tests/src/main.rs +++ b/crates/bevy_color/crates/gen_tests/src/main.rs @@ -20,9 +20,15 @@ const TEST_COLORS: &[(f32, f32, f32, &str)] = &[ (0.5, 0., 0.5, "fuchsia"), (0., 0.5, 0.5, "aqua"), ]; + /// Pre-computed okhsl results of [`TEST_COLORS`]. +/// See for how this was computed. +#[expect( + clippy::excessive_precision, + reason = "The results are copied from output of script" +)] const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ - [0., 0., 0.], // The result of original javascript implemention is [0, NaN, 0]. + [0., 0., 0.], // The result of original javascript implementation is [0, NaN, 0]. [89.87556309590242, 0.5582831888483675, 0.9999999923961898], [29.23388519234263, 1.0000000001433997, 0.5680846525040862], [142.49533888780996, 0.9999999700728788, 0.8445289645307816], @@ -41,9 +47,15 @@ const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ [328.36341792345144, 1.0006210729018223, 0.33011042396630463], [194.7689479319638, 0.9999994637526137, 0.4687233442820504], ]; + /// Pre-computed okhsv results of [`TEST_COLORS`]. +/// See for how this was computed. +#[expect( + clippy::excessive_precision, + reason = "The results are copied from output of script" +)] const TEST_COLORS_OKHSV: &[[f32; 3]] = &[ - [0., 0., 0.], // The result of original javascript implemention is [0, NaN, NaN]. + [0., 0., 0.], // The result of original javascript implementation is [0, NaN, NaN]. [89.87556309590242, 1.0347523928230576e-7, 1.000000027003774], [29.23388519234263, 0.9995219692256989, 1.0000000001685625], [142.49533888780996, 0.9999997210415695, 0.9999999884428648], diff --git a/crates/bevy_color/src/okcolor_convert.rs b/crates/bevy_color/src/okcolor_convert.rs index 6827b9589a249..d94b0cbb7fd05 100644 --- a/crates/bevy_color/src/okcolor_convert.rs +++ b/crates/bevy_color/src/okcolor_convert.rs @@ -1,4 +1,5 @@ -//! Functions for Okhsl/Okhsv <-> Oklab conversion. See +//! Functions for Okhsl/Okhsv <-> Oklab conversion. +//! See #![expect( non_snake_case, @@ -130,7 +131,7 @@ pub(crate) fn find_cusp(a: f32, b: f32) -> LC { // C = t * C1; // a and b must be normalized so a^2 + b^2 == 1 fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) -> f32 { - // Find the intersection for upper and lower half seprately + // Find the intersection for upper and lower half separately let mut t; if ((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0. { // Lower half @@ -234,9 +235,9 @@ fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { let C_b = (1. - L) * ST_mid.T; C_mid = 0.9 * k - * (1. / (1. / (C_a * C_a * C_a * C_a) + 1. / (C_b * C_b * C_b * C_b))) - .sqrt() - .sqrt(); + * ops::sqrt(ops::sqrt( + 1. / (1. / (C_a * C_a * C_a * C_a) + 1. / (C_b * C_b * C_b * C_b)), + )); } let C_0; @@ -246,7 +247,7 @@ fn get_Cs(L: f32, a_: f32, b_: f32) -> Cs { let C_b = (1. - L) * 0.8; // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - C_0 = (1. / (1. / (C_a * C_a) + 1. / (C_b * C_b))).sqrt(); + C_0 = ops::sqrt(1. / (1. / (C_a * C_a) + 1. / (C_b * C_b))); } Cs { C_0, C_mid, C_max } @@ -279,7 +280,7 @@ pub(crate) fn toe(x: f32) -> f32 { let k_1: f32 = 0.206; let k_2: f32 = 0.03; let k_3: f32 = (1. + k_1) / (1. + k_2); - 0.5 * (k_3 * x - k_1 + ((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x).sqrt()) + 0.5 * (k_3 * x - k_1 + ops::sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4. * k_2 * k_3 * x)) } pub(crate) fn toe_inv(x: f32) -> f32 { @@ -296,7 +297,7 @@ pub(crate) fn oklab_to_okhsl(value: Oklaba) -> Okhsla { b: lab_b, alpha, } = value; - let C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let C = ops::sqrt(lab_a * lab_a + lab_b * lab_b); let a_ = lab_a / C; let b_ = lab_b / C; @@ -393,7 +394,7 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { b: lab_b, alpha, } = value; - let C = (lab_a * lab_a + lab_b * lab_b).sqrt(); + let C = ops::sqrt(lab_a * lab_a + lab_b * lab_b); let a_ = lab_a / C; let b_ = lab_b / C; diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index 9ec5be780a1cd..cb821800462c0 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -86,11 +86,11 @@ impl Okhsla { /// # Examples /// /// ```rust - /// # use bevy_color::Hsla; + /// # use bevy_color::Okhsla; /// // Unique color for an entity /// # let entity_index = 123; /// // let entity_index = entity.index(); - /// let color = Hsla::sequential_dispersed(entity_index); + /// let color = Okhsla::sequential_dispersed(entity_index); /// /// // Palette with 5 distinct hues /// let palette = (0..5).map(Okhsla::sequential_dispersed).collect::>(); @@ -429,8 +429,8 @@ mod tests { color.rgb, rgb2 ); - // If lightness is approximately equal to 1.0, hue and saturation are arbitrary. - if color.okhsl.lightness < 0.999 { + // If lightness is approximately equal to 0.0 or 1.0, hue and saturation are arbitrary. + if color.okhsl.lightness < 0.999 && color.okhsl.lightness > 0.001 { // If saturation is approximately equal to 0.0, hue is arbitrary. if color.okhsl.saturation > 0.001 { assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001); diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index 5631fd750a633..bc35e29797e2e 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -394,17 +394,20 @@ mod tests { let rgb2: Srgba = (color.okhsv).into(); let okhsv: Okhsva = (color.rgb).into(); assert!( - color.rgb.distance(&rgb2) < 0.01, + color.rgb.distance(&rgb2) < 0.003, "{}: {:?} != {:?}", color.name, color.rgb, rgb2, ); - // If saturation is approximately equal to 0.0, hue is arbitrary. - if color.okhsv.saturation > 0.001 { - assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001); + // If value is approximately equal to 0.0, hue and saturation are arbitrary. + if color.okhsv.value > 0.001 { + // If saturation is approximately equal to 0.0, hue is arbitrary. + if color.okhsv.saturation > 0.001 { + assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001); + } + assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001); } - assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001); assert_approx_eq!(color.okhsv.value, okhsv.value, 0.001); assert_approx_eq!(color.okhsv.alpha, okhsv.alpha, 0.001); } From ae4a2efcbb6d4997009c728f8889fc13875473cc Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 6 May 2026 01:52:43 +0800 Subject: [PATCH 06/11] Add docs --- .../bevy_color/docs/diagrams/model_graph.mmd | 6 ++- .../bevy_color/docs/diagrams/model_graph.svg | 2 +- crates/bevy_color/src/okhsla.rs | 42 +++++++++--------- crates/bevy_color/src/okhsva.rs | 43 ++++++++++--------- crates/bevy_color/src/testing.rs | 10 +++++ 5 files changed, 60 insertions(+), 43 deletions(-) diff --git a/crates/bevy_color/docs/diagrams/model_graph.mmd b/crates/bevy_color/docs/diagrams/model_graph.mmd index d7fc5e1b248f4..8f602aa8f00db 100644 --- a/crates/bevy_color/docs/diagrams/model_graph.mmd +++ b/crates/bevy_color/docs/diagrams/model_graph.mmd @@ -3,6 +3,8 @@ flowchart LR lRGB(Linear
sRGB
) Oklab(Oklab) Oklch(Oklch) + Okhsl(Okhsl) + Okhsv(Okhsv) XYZ(XYZ) Lab(Lab) Lch(Lch) @@ -13,10 +15,12 @@ flowchart LR GPU <--> lRGB lRGB <--Conversion--> Oklab Oklab <--Conversion--> Oklch + Oklab <--Conversion--> Okhsl + Oklab <--Conversion--> Okhsv lRGB <--Conversion--> XYZ XYZ <--Conversion--> Lab Lab <--Conversion--> Lch lRGB <--Conversion--> sRGB sRGB <--Conversion--> HWB HWB <--Conversion--> HSV - HSV <--Conversion--> HSL \ No newline at end of file + HSV <--Conversion--> HSL diff --git a/crates/bevy_color/docs/diagrams/model_graph.svg b/crates/bevy_color/docs/diagrams/model_graph.svg index 5c1d3dceb77d8..817fa7b840e13 100644 --- a/crates/bevy_color/docs/diagrams/model_graph.svg +++ b/crates/bevy_color/docs/diagrams/model_graph.svg @@ -1 +1 @@ -
GPU
\ No newline at end of file +

GPU

\ No newline at end of file diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index cb821800462c0..d7d683d291643 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -9,6 +9,7 @@ use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Okhsl color space, with alpha +/// Further information on this color model can be found on . #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] @@ -305,97 +306,97 @@ impl From for LinearRgba { impl From for Okhsla { fn from(value: Hsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Hsla { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Hsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Hsva { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Hwba) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Hwba { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Lcha) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Lcha { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Srgba) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Srgba { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Xyza) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Xyza { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Laba) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Laba { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Oklcha) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Oklcha { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } @@ -429,16 +430,17 @@ mod tests { color.rgb, rgb2 ); + let msg = alloc::format!(", color {:?}, got {:?}", color.okhsl, okhsl); // If lightness is approximately equal to 0.0 or 1.0, hue and saturation are arbitrary. if color.okhsl.lightness < 0.999 && color.okhsl.lightness > 0.001 { // If saturation is approximately equal to 0.0, hue is arbitrary. if color.okhsl.saturation > 0.001 { - assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001); + assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001, msg); } - assert_approx_eq!(color.okhsl.saturation, okhsl.saturation, 0.001); + assert_approx_eq!(color.okhsl.saturation, okhsl.saturation, 0.001, msg); } - assert_approx_eq!(color.okhsl.lightness, okhsl.lightness, 0.001); - assert_approx_eq!(color.okhsl.alpha, okhsl.alpha, 0.001); + assert_approx_eq!(color.okhsl.lightness, okhsl.lightness, 0.001, msg); + assert_approx_eq!(color.okhsl.alpha, okhsl.alpha, 0.001, msg); } } diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index bc35e29797e2e..13c39881b3c8a 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -9,7 +9,7 @@ use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Okhsv color space with alpha. -/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV). +/// Further information on this color model can be found on . #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] @@ -276,97 +276,97 @@ impl From for LinearRgba { impl From for Okhsva { fn from(value: Srgba) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Srgba { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Lcha) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Lcha { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Xyza) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Xyza { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Okhsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsla { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Hsla) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Hsla { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Hsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Hsva { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Laba) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Laba { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Okhsva { fn from(value: Oklcha) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } impl From for Oklcha { fn from(value: Okhsva) -> Self { - LinearRgba::from(value).into() + Oklaba::from(value).into() } } @@ -400,16 +400,17 @@ mod tests { color.rgb, rgb2, ); + let msg = alloc::format!(", color {:?}, got {:?}", color.okhsv, okhsv); // If value is approximately equal to 0.0, hue and saturation are arbitrary. if color.okhsv.value > 0.001 { // If saturation is approximately equal to 0.0, hue is arbitrary. if color.okhsv.saturation > 0.001 { - assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001); + assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001, msg); } - assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001); + assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001, msg); } - assert_approx_eq!(color.okhsv.value, okhsv.value, 0.001); - assert_approx_eq!(color.okhsv.alpha, okhsv.alpha, 0.001); + assert_approx_eq!(color.okhsv.value, okhsv.value, 0.001, msg); + assert_approx_eq!(color.okhsv.alpha, okhsv.alpha, 0.001, msg); } } diff --git a/crates/bevy_color/src/testing.rs b/crates/bevy_color/src/testing.rs index 6c7747e2a540b..5c22132a5e849 100644 --- a/crates/bevy_color/src/testing.rs +++ b/crates/bevy_color/src/testing.rs @@ -9,6 +9,16 @@ macro_rules! assert_approx_eq { ); } }; + + ($x:expr, $y:expr, $d:expr, $msg:expr) => { + if ($x - $y).abs() >= $d { + panic!( + "assertion failed: `(left !== right)` \ + (left: `{}`, right: `{}`, tolerance: `{}`){:?}", + $x, $y, $d, $msg + ); + } + }; } #[cfg(test)] From d6d3385c8d1c9b83adaa1ce5c1223daa75e688ce Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 6 May 2026 02:28:49 +0800 Subject: [PATCH 07/11] work around windows ci --- crates/bevy_color/src/okhsla.rs | 7 ++++++- crates/bevy_color/src/okhsva.rs | 13 +++++++++++-- crates/bevy_color/src/testing.rs | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index d7d683d291643..cbf3abf557ab5 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -430,7 +430,12 @@ mod tests { color.rgb, rgb2 ); - let msg = alloc::format!(", color {:?}, got {:?}", color.okhsl, okhsl); + let msg = alloc::format!( + "{}: expected {:?}, got {:?}", + color.name, + color.okhsl, + okhsl + ); // If lightness is approximately equal to 0.0 or 1.0, hue and saturation are arbitrary. if color.okhsl.lightness < 0.999 && color.okhsl.lightness > 0.001 { // If saturation is approximately equal to 0.0, hue is arbitrary. diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index 13c39881b3c8a..132bef9712544 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -400,14 +400,23 @@ mod tests { color.rgb, rgb2, ); - let msg = alloc::format!(", color {:?}, got {:?}", color.okhsv, okhsv); + let msg = alloc::format!( + "{}: expected {:?}, got {:?}", + color.name, + color.okhsv, + okhsv + ); // If value is approximately equal to 0.0, hue and saturation are arbitrary. if color.okhsv.value > 0.001 { // If saturation is approximately equal to 0.0, hue is arbitrary. if color.okhsv.saturation > 0.001 { assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001, msg); } - assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001, msg); + // TODO: blue color has a large error on windows. + // Expected: [264.05203, 0.9999911, 0.99999994], + // got [264.05203, 0.9239708, 1.0000002] on windows, + // got [264.05203, 0.9999911, 0.9999998] on linux + assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.08, msg); } assert_approx_eq!(color.okhsv.value, okhsv.value, 0.001, msg); assert_approx_eq!(color.okhsv.alpha, okhsv.alpha, 0.001, msg); diff --git a/crates/bevy_color/src/testing.rs b/crates/bevy_color/src/testing.rs index 5c22132a5e849..605510fcd45ba 100644 --- a/crates/bevy_color/src/testing.rs +++ b/crates/bevy_color/src/testing.rs @@ -14,7 +14,7 @@ macro_rules! assert_approx_eq { if ($x - $y).abs() >= $d { panic!( "assertion failed: `(left !== right)` \ - (left: `{}`, right: `{}`, tolerance: `{}`){:?}", + (left: `{}`, right: `{}`, tolerance: `{}`). {}", $x, $y, $d, $msg ); } From 9f36128d7a18c997befea7506a5a58cdb1f92f0d Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 6 May 2026 10:27:26 +0800 Subject: [PATCH 08/11] Fix NaN and test colors and try improving precision --- .../bevy_color/crates/gen_tests/src/main.rs | 12 ++++---- crates/bevy_color/src/okcolor_convert.rs | 28 +++++++++++++++++-- crates/bevy_color/src/okhsla.rs | 10 ++----- crates/bevy_color/src/okhsva.rs | 14 ++-------- crates/bevy_color/src/test_colors.rs | 8 +++--- 5 files changed, 39 insertions(+), 33 deletions(-) diff --git a/crates/bevy_color/crates/gen_tests/src/main.rs b/crates/bevy_color/crates/gen_tests/src/main.rs index d890cdce72fbc..d0197f367b596 100644 --- a/crates/bevy_color/crates/gen_tests/src/main.rs +++ b/crates/bevy_color/crates/gen_tests/src/main.rs @@ -28,15 +28,15 @@ const TEST_COLORS: &[(f32, f32, f32, &str)] = &[ reason = "The results are copied from output of script" )] const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ - [0., 0., 0.], // The result of original javascript implementation is [0, NaN, 0]. - [89.87556309590242, 0.5582831888483675, 0.9999999923961898], + [0., 0., 0.], // Fixed. [0, NaN, 0] + [0., 0., 1.], // Fixed. [89.87556309590242, 0.5582831888483675, 0.9999999923961898] [29.23388519234263, 1.0000000001433997, 0.5680846525040862], [142.49533888780996, 0.9999999700728788, 0.8445289645307816], [264.052020638055, 0.9999999948631134, 0.3665653394260194], [109.76923207652122, 1.0000000336324515, 0.9627043968088945], [328.36341792345144, 1.000039018792486, 0.6532987448472438], [194.76894793196382, 0.9999999858415329, 0.889848301619521], - [89.87556282857139, 1.1616558204531687e-7, 0.5337598228073358], + [0., 6.210822e-7, 0.5337598], // Fixed. [89.87556282857139, 1.1616558204531687e-7, 0.5337598228073358], [109.76923207652133, 1.0000004489650085, 0.5117162225399476], [328.36341792345144, 1.0006210729018223, 0.33011042396630463], [194.7689479319638, 0.9999994637526137, 0.4687233442820504], @@ -55,15 +55,15 @@ const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ reason = "The results are copied from output of script" )] const TEST_COLORS_OKHSV: &[[f32; 3]] = &[ - [0., 0., 0.], // The result of original javascript implementation is [0, NaN, NaN]. - [89.87556309590242, 1.0347523928230576e-7, 1.000000027003774], + [0., 0., 0.], // Fixed. [0, NaN, NaN] + [0., 0., 1.], // Fixed. [89.87556309590242, 1.0347523928230576e-7, 1.000000027003774] [29.23388519234263, 0.9995219692256989, 1.0000000001685625], [142.49533888780996, 0.9999997210415695, 0.9999999884428648], [264.052020638055, 0.9999910912349018, 0.9999999646150918], [109.76923207652122, 1.0000004467649033, 1.0000000319591924], [328.36341792345144, 1.000122136833694, 0.9999999978651268], [194.76894793196382, 0.9999994264606504, 0.9999999950078027], - [89.87556282857139, 1.0347523926099617e-7, 0.5337598416065157], + [0.0, 6.65956e-7, 0.5337598], // Fixed. [89.87556282857139, 1.0347523926099617e-7, 0.5337598416065157], [109.76923207652133, 1.0000004467649042, 0.5318634934435551], [328.36341792345144, 1.0001221368336943, 0.5106197804980503], [194.7689479319638, 0.999999426460651, 0.5278241496179404], diff --git a/crates/bevy_color/src/okcolor_convert.rs b/crates/bevy_color/src/okcolor_convert.rs index d94b0cbb7fd05..325dba781250c 100644 --- a/crates/bevy_color/src/okcolor_convert.rs +++ b/crates/bevy_color/src/okcolor_convert.rs @@ -81,8 +81,8 @@ fn compute_max_saturation(a: f32, b: f32) -> f32 { let k_l = 0.396_337_78 * a + 0.215_803_76 * b; let k_m = -0.105_561_346 * a - 0.063_854_17 * b; let k_s = -0.089_484_18 * a - 1.291_485_5 * b; - - { + // Patch: Do two steps + for _ in 0..2 { let l_ = 1. + S * k_l; let m_ = 1. + S * k_m; let s_ = 1. + S * k_s; @@ -157,7 +157,8 @@ fn find_gamut_intersection(a: f32, b: f32, L1: f32, C1: f32, L0: f32, cusp: LC) let s_dt = dL + dC * k_s; // If higher accuracy is required, 2 or 3 iterations of the following block can be used: - { + // Patch: Do two steps + for _ in 0..2 { let L = L0 * (1. - t) + t * L1; let C = t * C1; @@ -298,6 +299,16 @@ pub(crate) fn oklab_to_okhsl(value: Oklaba) -> Okhsla { alpha, } = value; let C = ops::sqrt(lab_a * lab_a + lab_b * lab_b); + // Patch: Fixes NaN for pure black and white colors. + if C < core::f32::EPSILON { + let l = toe(lab_l); + return Okhsla { + hue: 0., + saturation: 0., + lightness: l, + alpha, + }; + } let a_ = lab_a / C; let b_ = lab_b / C; @@ -395,6 +406,17 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { alpha, } = value; let C = ops::sqrt(lab_a * lab_a + lab_b * lab_b); + // Patch: Fixes NaN for pure black and white colors. + if C < core::f32::EPSILON { + // In this case, value is equal to lightness. + let l = toe(lab_l); + return Okhsva { + hue: 0., + saturation: 0., + value: l, + alpha, + }; + } let a_ = lab_a / C; let b_ = lab_b / C; diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index cbf3abf557ab5..d56a74c4f9929 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -436,14 +436,8 @@ mod tests { color.okhsl, okhsl ); - // If lightness is approximately equal to 0.0 or 1.0, hue and saturation are arbitrary. - if color.okhsl.lightness < 0.999 && color.okhsl.lightness > 0.001 { - // If saturation is approximately equal to 0.0, hue is arbitrary. - if color.okhsl.saturation > 0.001 { - assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001, msg); - } - assert_approx_eq!(color.okhsl.saturation, okhsl.saturation, 0.001, msg); - } + assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001, msg); + assert_approx_eq!(color.okhsl.saturation, okhsl.saturation, 0.001, msg); assert_approx_eq!(color.okhsl.lightness, okhsl.lightness, 0.001, msg); assert_approx_eq!(color.okhsl.alpha, okhsl.alpha, 0.001, msg); } diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index 132bef9712544..d1fb6cf45cd25 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -406,18 +406,8 @@ mod tests { color.okhsv, okhsv ); - // If value is approximately equal to 0.0, hue and saturation are arbitrary. - if color.okhsv.value > 0.001 { - // If saturation is approximately equal to 0.0, hue is arbitrary. - if color.okhsv.saturation > 0.001 { - assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001, msg); - } - // TODO: blue color has a large error on windows. - // Expected: [264.05203, 0.9999911, 0.99999994], - // got [264.05203, 0.9239708, 1.0000002] on windows, - // got [264.05203, 0.9999911, 0.9999998] on linux - assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.08, msg); - } + assert_approx_eq!(color.okhsv.hue, okhsv.hue, 0.001, msg); + assert_approx_eq!(color.okhsv.saturation, okhsv.saturation, 0.001, msg); assert_approx_eq!(color.okhsv.value, okhsv.value, 0.001, msg); assert_approx_eq!(color.okhsv.alpha, okhsv.alpha, 0.001, msg); } diff --git a/crates/bevy_color/src/test_colors.rs b/crates/bevy_color/src/test_colors.rs index 348df6b49fef3..2fbe1a0fd1206 100644 --- a/crates/bevy_color/src/test_colors.rs +++ b/crates/bevy_color/src/test_colors.rs @@ -53,8 +53,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(1.0, 0.0, 0.000000059604645, 1.0), oklch: Oklcha::new(1.0, 0.000000059604645, 90.0, 1.0), xyz: Xyza::new(0.95047, 1.0, 1.08883, 1.0), - okhsl: Okhsla::new(89.875565, 0.5582832, 1.0, 1.0), - okhsv: Okhsva::new(89.875565, 0.00000010347524, 1.0, 1.0), + okhsl: Okhsla::new(0.0, 0.0, 1.0, 1.0), + okhsv: Okhsva::new(0.0, 0.0, 1.0, 1.0), }, // red TestColor { @@ -165,8 +165,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.5981807, 0.00000011920929, 0.0, 1.0), oklch: Oklcha::new(0.5981807, 0.00000011920929, 0.0, 1.0), xyz: Xyza::new(0.2034397, 0.21404117, 0.23305441, 1.0), - okhsl: Okhsla::new(89.875565, 0.00000011616558, 0.53375983, 1.0), - okhsv: Okhsva::new(89.875565, 0.00000010347524, 0.53375983, 1.0), + okhsl: Okhsla::new(0.0, 0.0000006210822, 0.5337598, 1.0), + okhsv: Okhsva::new(0.0, 0.000000665956, 0.5337598, 1.0), }, // olive TestColor { From 8df62616be18f4fbb276eac3558f956dfa8f08d6 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Wed, 6 May 2026 12:56:29 +0800 Subject: [PATCH 09/11] Fix pre-computed test colors and use `libm::cbrtf` to fix windows testing (#4) The pre-compute results for testing is from `node colorconversion.js` with increased Halley steps: [colorconversion.js.zip](https://github.com/user-attachments/files/27423193/colorconversion.js.zip) Use `libm`'s `cbrtf` implemention to fix windows testing --- .../bevy_color/crates/gen_tests/src/main.rs | 70 +++++++++---------- crates/bevy_color/src/okcolor_convert.rs | 67 ++++++++++++++++-- crates/bevy_color/src/okhsva.rs | 2 +- crates/bevy_color/src/oklaba.rs | 13 ++-- crates/bevy_color/src/test_colors.rs | 44 ++++++------ 5 files changed, 127 insertions(+), 69 deletions(-) diff --git a/crates/bevy_color/crates/gen_tests/src/main.rs b/crates/bevy_color/crates/gen_tests/src/main.rs index d0197f367b596..5a19675b804aa 100644 --- a/crates/bevy_color/crates/gen_tests/src/main.rs +++ b/crates/bevy_color/crates/gen_tests/src/main.rs @@ -28,24 +28,24 @@ const TEST_COLORS: &[(f32, f32, f32, &str)] = &[ reason = "The results are copied from output of script" )] const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ - [0., 0., 0.], // Fixed. [0, NaN, 0] - [0., 0., 1.], // Fixed. [89.87556309590242, 0.5582831888483675, 0.9999999923961898] - [29.23388519234263, 1.0000000001433997, 0.5680846525040862], - [142.49533888780996, 0.9999999700728788, 0.8445289645307816], - [264.052020638055, 0.9999999948631134, 0.3665653394260194], + [0., 0., 0.], + [0., 0., 0.9999999923961898], + [29.23388519234263, 1.0000000052082665, 0.5680846525040862], + [142.49533888780996, 0.999999970072876, 0.8445289645307816], + [264.052020638055, 0.9999999966883574, 0.3665653394260194], [109.76923207652122, 1.0000000336324515, 0.9627043968088945], - [328.36341792345144, 1.000039018792486, 0.6532987448472438], - [194.76894793196382, 0.9999999858415329, 0.889848301619521], - [0., 6.210822e-7, 0.5337598], // Fixed. [89.87556282857139, 1.1616558204531687e-7, 0.5337598228073358], - [109.76923207652133, 1.0000004489650085, 0.5117162225399476], - [328.36341792345144, 1.0006210729018223, 0.33011042396630463], - [194.7689479319638, 0.9999994637526137, 0.4687233442820504], - [29.23388519234263, 0.9996788048327606, 0.28080442778247217], - [142.49533888780996, 0.9999999579156374, 0.4420348389571636], - [264.052020638055, 0.9999976560886634, 0.1673431798736403], - [109.76923207652133, 1.0000004489650085, 0.5117162225399476], - [328.36341792345144, 1.0006210729018223, 0.33011042396630463], - [194.7689479319638, 0.9999994637526137, 0.4687233442820504], + [328.36341792345144, 0.9999999982842802, 0.6532987448472438], + [194.76894793196382, 0.9999999858415309, 0.889848301619521], + [0., 0., 0.5337598228073358], + [109.76923207652133, 1.0000004293546714, 0.5117162225399476], + [328.36341792345144, 0.9999999247891016, 0.33011042396630463], + [194.7689479319638, 0.9999998087565706, 0.4687233442820504], + [29.23388519234263, 1.0000000079893017, 0.28080442778247217], + [142.49533888780996, 0.9999999780939269, 0.4420348389571636], + [264.052020638055, 0.9999999965721178, 0.1673431798736403], + [109.76923207652133, 1.0000004293546714, 0.5117162225399476], + [328.36341792345144, 0.9999999247891016, 0.33011042396630463], + [194.7689479319638, 0.9999998087565706, 0.4687233442820504], ]; /// Pre-computed okhsv results of [`TEST_COLORS`]. @@ -55,24 +55,24 @@ const TEST_COLORS_OKHSL: &[[f32; 3]] = &[ reason = "The results are copied from output of script" )] const TEST_COLORS_OKHSV: &[[f32; 3]] = &[ - [0., 0., 0.], // Fixed. [0, NaN, NaN] - [0., 0., 1.], // Fixed. [89.87556309590242, 1.0347523928230576e-7, 1.000000027003774] - [29.23388519234263, 0.9995219692256989, 1.0000000001685625], - [142.49533888780996, 0.9999997210415695, 0.9999999884428648], - [264.052020638055, 0.9999910912349018, 0.9999999646150918], - [109.76923207652122, 1.0000004467649033, 1.0000000319591924], - [328.36341792345144, 1.000122136833694, 0.9999999978651268], - [194.76894793196382, 0.9999994264606504, 0.9999999950078027], - [0.0, 6.65956e-7, 0.5337598], // Fixed. [89.87556282857139, 1.0347523926099617e-7, 0.5337598416065157], - [109.76923207652133, 1.0000004467649042, 0.5318634934435551], - [328.36341792345144, 1.0001221368336943, 0.5106197804980503], - [194.7689479319638, 0.999999426460651, 0.5278241496179404], - [29.23388519234263, 0.9995219692256992, 0.5022645128400224], - [142.49533888780996, 0.9999997210415695, 0.5250592582180916], - [264.052020638055, 0.9999910912349016, 0.47496664028144414], - [109.76923207652133, 1.0000004467649042, 0.5318634934435551], - [328.36341792345144, 1.0001221368336943, 0.5106197804980503], - [194.7689479319638, 0.999999426460651, 0.5278241496179404], + [0., 0., 0.], + [0., 0., 0.9999999923961898], + [29.23388519234263, 1.0000000118548495, 1.0000000001685643], + [142.49533888780996, 0.9999998547944557, 0.9999999884428643], + [264.052020638055, 0.9999999869716024, 0.9999999646150842], + [109.76923207652122, 1.0000004272507257, 1.0000000319591922], + [328.36341792345144, 0.9999999851480317, 0.9999999978651299], + [194.76894793196382, 0.9999997954575895, 0.9999999950078031], + [0., 0., 0.5337598228073358], + [109.76923207652133, 1.0000004272507266, 0.5318634934374442], + [328.36341792345144, 0.9999999851480317, 0.5106204722384122], + [194.7689479319638, 0.9999997954575874, 0.527824149473251], + [29.23388519234263, 1.0000000118548495, 0.5022602924281349], + [142.49533888780996, 0.999999854794456, 0.525059257999103], + [264.052020638055, 0.9999999869716024, 0.4749665533554043], + [109.76923207652133, 1.0000004272507266, 0.5318634934374442], + [328.36341792345144, 0.9999999851480317, 0.5106204722384122], + [194.7689479319638, 0.9999997954575874, 0.527824149473251], ]; fn main() { diff --git a/crates/bevy_color/src/okcolor_convert.rs b/crates/bevy_color/src/okcolor_convert.rs index 325dba781250c..1ec68420aa966 100644 --- a/crates/bevy_color/src/okcolor_convert.rs +++ b/crates/bevy_color/src/okcolor_convert.rs @@ -1,6 +1,8 @@ //! Functions for Okhsl/Okhsv <-> Oklab conversion. //! See +// See comments start with `Patch` for how this differs from original `ok_color.h` + #![expect( non_snake_case, reason = "The code is translated from a C implementation." @@ -117,7 +119,7 @@ pub(crate) fn find_cusp(a: f32, b: f32) -> LC { // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: let rgb_at_max: LinearRgba = Oklaba::lab(1., S_cusp * a, S_cusp * b).into(); - let L_cusp = ops::cbrt(1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue))); + let L_cusp = libm_cbrtf(1. / ((rgb_at_max.red.max(rgb_at_max.green)).max(rgb_at_max.blue))); let C_cusp = L_cusp * S_cusp; LC { @@ -300,7 +302,7 @@ pub(crate) fn oklab_to_okhsl(value: Oklaba) -> Okhsla { } = value; let C = ops::sqrt(lab_a * lab_a + lab_b * lab_b); // Patch: Fixes NaN for pure black and white colors. - if C < core::f32::EPSILON { + if C < f32::EPSILON { let l = toe(lab_l); return Okhsla { hue: 0., @@ -407,7 +409,7 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { } = value; let C = ops::sqrt(lab_a * lab_a + lab_b * lab_b); // Patch: Fixes NaN for pure black and white colors. - if C < core::f32::EPSILON { + if C < f32::EPSILON { // In this case, value is equal to lightness. let l = toe(lab_l); return Okhsva { @@ -442,7 +444,7 @@ pub(crate) fn oklab_to_okhsv(value: Oklaba) -> Okhsva { // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle: let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); let scale_L = - ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); + libm_cbrtf(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); L /= scale_L; @@ -503,10 +505,65 @@ pub(crate) fn okhsv_to_oklab(value: Okhsva) -> Oklaba { let rgb_scale: LinearRgba = Oklaba::lab(L_vt, a_ * C_vt, b_ * C_vt).into(); let scale_L = - ops::cbrt(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); + libm_cbrtf(1. / ((rgb_scale.red.max(rgb_scale.green)).max(rgb_scale.blue.max(0.)))); L *= scale_L; C *= scale_L; Oklaba::new(L, C * a_, C * b_, alpha) } + +// Note: This is copied from `libm` to fix precision issue on Windows CI testing. + +const B1: u32 = 709958130; /* B1 = (127-127.0/3-0.03306235651)*2**23 */ +const B2: u32 = 642849266; /* B2 = (127-127.0/3-24/3-0.03306235651)*2**23 */ +/// Cube root (f32) +/// +/// Computes the cube root of the argument. +pub(crate) fn libm_cbrtf(x: f32) -> f32 { + let x1p24 = f32::from_bits(0x4b800000); // 0x1p24f === 2 ^ 24 + + let mut r: f64; + let mut t: f64; + let mut ui: u32 = x.to_bits(); + let mut hx: u32 = ui & 0x7fffffff; + + if hx >= 0x7f800000 { + /* cbrt(NaN,INF) is itself */ + return x + x; + } + + /* rough cbrt to 5 bits */ + if hx < 0x00800000 { + /* zero or subnormal? */ + if hx == 0 { + return x; /* cbrt(+-0) is itself */ + } + ui = (x * x1p24).to_bits(); + hx = ui & 0x7fffffff; + hx = hx / 3 + B2; + } else { + hx = hx / 3 + B1; + } + ui &= 0x80000000; + ui |= hx; + + /* + * First step Newton iteration (solving t*t-x/t == 0) to 16 bits. In + * double precision so that its terms can be arranged for efficiency + * without causing overflow or underflow. + */ + t = f32::from_bits(ui) as f64; + r = t * t * t; + t = t * (x as f64 + x as f64 + r) / (x as f64 + r + r); + + /* + * Second step Newton iteration to 47 bits. In double precision for + * efficiency and accuracy. + */ + r = t * t * t; + t = t * (x as f64 + x as f64 + r) / (x as f64 + r + r); + + /* rounding to 24 bits is perfect in round-to-nearest mode */ + t as f32 +} diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index d1fb6cf45cd25..971c8f6a762a3 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -394,7 +394,7 @@ mod tests { let rgb2: Srgba = (color.okhsv).into(); let okhsv: Okhsva = (color.rgb).into(); assert!( - color.rgb.distance(&rgb2) < 0.003, + color.rgb.distance(&rgb2) < 0.001, "{}: {:?} != {:?}", color.name, color.rgb, diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 12118ca6b4704..533fac545cb9e 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,8 +1,9 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, - Gray, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, impl_componentwise_vector_space, + okcolor_convert::libm_cbrtf, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hwba, Lcha, + LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; -use bevy_math::{ops, FloatPow, Vec3, Vec4}; +use bevy_math::{FloatPow, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -245,9 +246,9 @@ impl From for Oklaba { let l = 0.41222146 * red + 0.53633255 * green + 0.051445995 * blue; let m = 0.2119035 * red + 0.6806995 * green + 0.10739696 * blue; let s = 0.08830246 * red + 0.28171885 * green + 0.6299787 * blue; - let l_ = ops::cbrt(l); - let m_ = ops::cbrt(m); - let s_ = ops::cbrt(s); + let l_ = libm_cbrtf(l); + let m_ = libm_cbrtf(m); + let s_ = libm_cbrtf(s); let l = 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_; let a = 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_; let b = 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_; diff --git a/crates/bevy_color/src/test_colors.rs b/crates/bevy_color/src/test_colors.rs index 2fbe1a0fd1206..4cb68feb87351 100644 --- a/crates/bevy_color/src/test_colors.rs +++ b/crates/bevy_color/src/test_colors.rs @@ -70,7 +70,7 @@ pub const TEST_COLORS: &[TestColor] = &[ oklch: Oklcha::new(0.6279554, 0.25768322, 29.233906, 1.0), xyz: Xyza::new(0.4124564, 0.2126729, 0.0193339, 1.0), okhsl: Okhsla::new(29.233885, 1.0, 0.56808466, 1.0), - okhsv: Okhsva::new(29.233885, 0.999522, 1.0, 1.0), + okhsv: Okhsva::new(29.233885, 1.0, 1.0, 1.0), }, // green TestColor { @@ -86,7 +86,7 @@ pub const TEST_COLORS: &[TestColor] = &[ oklch: Oklcha::new(0.8664396, 0.2948271, 142.49532, 1.0), xyz: Xyza::new(0.3575761, 0.7151522, 0.119192, 1.0), okhsl: Okhsla::new(142.49535, 0.99999994, 0.844529, 1.0), - okhsv: Okhsva::new(142.49535, 0.9999997, 1.0, 1.0), + okhsv: Okhsva::new(142.49535, 0.9999999, 1.0, 1.0), }, // blue TestColor { @@ -102,7 +102,7 @@ pub const TEST_COLORS: &[TestColor] = &[ oklch: Oklcha::new(0.4520137, 0.31321436, 264.05203, 1.0), xyz: Xyza::new(0.1804375, 0.072175, 0.9503041, 1.0), okhsl: Okhsla::new(264.05203, 1.0, 0.36656535, 1.0), - okhsv: Okhsva::new(264.05203, 0.9999911, 0.99999994, 1.0), + okhsv: Okhsva::new(264.05203, 1.0, 0.99999994, 1.0), }, // yellow TestColor { @@ -133,8 +133,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.7016738, 0.27456632, -0.16915613, 1.0), oklch: Oklcha::new(0.7016738, 0.32249102, 328.36343, 1.0), xyz: Xyza::new(0.5928939, 0.28484792, 0.969638, 1.0), - okhsl: Okhsla::new(328.3634, 1.000039, 0.65329874, 1.0), - okhsv: Okhsva::new(328.3634, 1.0001222, 1.0, 1.0), + okhsl: Okhsla::new(328.3634, 1.0, 0.65329874, 1.0), + okhsv: Okhsva::new(328.3634, 1.0, 1.0, 1.0), }, // cyan TestColor { @@ -150,7 +150,7 @@ pub const TEST_COLORS: &[TestColor] = &[ oklch: Oklcha::new(0.90539926, 0.15454996, 194.76895, 1.0), xyz: Xyza::new(0.5380136, 0.78732723, 1.069496, 1.0), okhsl: Okhsla::new(194.76895, 1.0, 0.8898483, 1.0), - okhsv: Okhsva::new(194.76895, 0.9999994, 1.0, 1.0), + okhsv: Okhsva::new(194.76895, 0.9999998, 1.0, 1.0), }, // gray TestColor { @@ -165,8 +165,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.5981807, 0.00000011920929, 0.0, 1.0), oklch: Oklcha::new(0.5981807, 0.00000011920929, 0.0, 1.0), xyz: Xyza::new(0.2034397, 0.21404117, 0.23305441, 1.0), - okhsl: Okhsla::new(0.0, 0.0000006210822, 0.5337598, 1.0), - okhsv: Okhsva::new(0.0, 0.000000665956, 0.5337598, 1.0), + okhsl: Okhsla::new(0.0, 0.0, 0.53375983, 1.0), + okhsv: Okhsva::new(0.0, 0.0, 0.53375983, 1.0), }, // olive TestColor { @@ -197,8 +197,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0), oklch: Oklcha::new(0.41972777, 0.19290791, 328.36343, 1.0), xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0), - okhsl: Okhsla::new(328.3634, 1.0006211, 0.33011043, 1.0), - okhsv: Okhsva::new(328.3634, 1.0001222, 0.51061976, 1.0), + okhsl: Okhsla::new(328.3634, 0.99999994, 0.33011043, 1.0), + okhsv: Okhsva::new(328.3634, 1.0, 0.5106205, 1.0), }, // teal TestColor { @@ -213,8 +213,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0), oklch: Oklcha::new(0.54159236, 0.09244873, 194.769, 1.0), xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0), - okhsl: Okhsla::new(194.76895, 0.99999946, 0.46872336, 1.0), - okhsv: Okhsva::new(194.76895, 0.9999994, 0.52782416, 1.0), + okhsl: Okhsla::new(194.76895, 0.9999998, 0.46872336, 1.0), + okhsv: Okhsva::new(194.76895, 0.9999998, 0.52782416, 1.0), }, // maroon TestColor { @@ -229,8 +229,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.3756308, 0.13450874, 0.07527886, 1.0), oklch: Oklcha::new(0.3756308, 0.1541412, 29.233906, 1.0), xyz: Xyza::new(0.08828264, 0.045520753, 0.0041382504, 1.0), - okhsl: Okhsla::new(29.233885, 0.9996788, 0.28080443, 1.0), - okhsv: Okhsva::new(29.233885, 0.999522, 0.5022645, 1.0), + okhsl: Okhsla::new(29.233885, 1.0, 0.28080443, 1.0), + okhsv: Okhsva::new(29.233885, 1.0, 0.50226027, 1.0), }, // lime TestColor { @@ -245,8 +245,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.5182875, -0.13990697, 0.10737252, 1.0), oklch: Oklcha::new(0.5182875, 0.17635992, 142.49535, 1.0), xyz: Xyza::new(0.076536, 0.153072, 0.025511991, 1.0), - okhsl: Okhsla::new(142.49535, 0.99999994, 0.44203484, 1.0), - okhsv: Okhsva::new(142.49535, 0.9999997, 0.5250593, 1.0), + okhsl: Okhsla::new(142.49535, 1.0, 0.44203484, 1.0), + okhsv: Okhsva::new(142.49535, 0.9999999, 0.5250593, 1.0), }, // navy TestColor { @@ -261,8 +261,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.27038592, -0.01941514, -0.18635012, 1.0), oklch: Oklcha::new(0.27038592, 0.18735878, 264.05203, 1.0), xyz: Xyza::new(0.03862105, 0.01544842, 0.20340417, 1.0), - okhsl: Okhsla::new(264.05203, 0.9999977, 0.16734318, 1.0), - okhsv: Okhsva::new(264.05203, 0.9999911, 0.47496665, 1.0), + okhsl: Okhsla::new(264.05203, 1.0, 0.16734318, 1.0), + okhsv: Okhsva::new(264.05203, 1.0, 0.47496656, 1.0), }, // orange TestColor { @@ -293,8 +293,8 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.41972777, 0.1642403, -0.10118592, 1.0), oklch: Oklcha::new(0.41972777, 0.19290791, 328.36343, 1.0), xyz: Xyza::new(0.12690368, 0.060969174, 0.20754242, 1.0), - okhsl: Okhsla::new(328.3634, 1.0006211, 0.33011043, 1.0), - okhsv: Okhsva::new(328.3634, 1.0001222, 0.51061976, 1.0), + okhsl: Okhsla::new(328.3634, 0.99999994, 0.33011043, 1.0), + okhsv: Okhsva::new(328.3634, 1.0, 0.5106205, 1.0), }, // aqua TestColor { @@ -309,7 +309,7 @@ pub const TEST_COLORS: &[TestColor] = &[ oklab: Oklaba::new(0.54159236, -0.08939436, -0.02356726, 1.0), oklch: Oklcha::new(0.54159236, 0.09244873, 194.769, 1.0), xyz: Xyza::new(0.11515705, 0.16852042, 0.22891617, 1.0), - okhsl: Okhsla::new(194.76895, 0.99999946, 0.46872336, 1.0), - okhsv: Okhsva::new(194.76895, 0.9999994, 0.52782416, 1.0), + okhsl: Okhsla::new(194.76895, 0.9999998, 0.46872336, 1.0), + okhsv: Okhsva::new(194.76895, 0.9999998, 0.52782416, 1.0), }, ]; From 82f1107b9e5890f99150330d5251447a40765f75 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Thu, 7 May 2026 09:27:05 +0800 Subject: [PATCH 10/11] Add methods --- crates/bevy_color/src/color.rs | 66 +++++++++++++++++++++++++++++++++ crates/bevy_color/src/okhsla.rs | 6 +-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index d7c478601fb89..df6a5db1fb9b7 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -504,6 +504,72 @@ impl Color { }) } + /// Creates a new [`Color`] object storing a [`Okhsla`] color. + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `lightness` - Lightness channel. [0.0, 1.0] + /// * `alpha` - Alpha channel. [0.0, 1.0] + pub const fn okhsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { + Self::Okhsla(Okhsla { + hue, + saturation, + lightness, + alpha, + }) + } + + /// Creates a new [`Color`] object storing a [`Okhsla`] color with an alpha of 1.0. + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `lightness` - Lightness channel. [0.0, 1.0] + pub const fn okhsl(hue: f32, saturation: f32, lightness: f32) -> Self { + Self::Okhsla(Okhsla { + hue, + saturation, + lightness, + alpha: 1.0, + }) + } + + /// Creates a new [`Color`] object storing a [`Okhsva`] color. + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `value` - Value channel. [0.0, 1.0] + /// * `alpha` - Alpha channel. [0.0, 1.0] + pub const fn okhsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self { + Self::Okhsva(Okhsva { + hue, + saturation, + value, + alpha, + }) + } + + /// Creates a new [`Color`] object storing a [`Okhsva`] color with an alpha of 1.0. + /// + /// # Arguments + /// + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `saturation` - Saturation channel. [0.0, 1.0] + /// * `value` - Value channel. [0.0, 1.0] + pub const fn okhsv(hue: f32, saturation: f32, value: f32) -> Self { + Self::Okhsva(Okhsva { + hue, + saturation, + value, + alpha: 1.0, + }) + } + /// A fully white [`Color::LinearRgba`] color with an alpha of 1.0. pub const WHITE: Self = Self::linear_rgb(1.0, 1.0, 1.0); diff --git a/crates/bevy_color/src/okhsla.rs b/crates/bevy_color/src/okhsla.rs index d56a74c4f9929..ea2cdd821995e 100644 --- a/crates/bevy_color/src/okhsla.rs +++ b/crates/bevy_color/src/okhsla.rs @@ -8,7 +8,7 @@ use bevy_math::{Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; -/// Color in Okhsl color space, with alpha +/// Color in Okhsl color space with alpha /// Further information on this color model can be found on . #[doc = include_str!("../docs/conversion.md")] ///
@@ -65,7 +65,7 @@ impl Okhsla { /// * `hue` - Hue channel. [0.0, 360.0] /// * `saturation` - Saturation channel. [0.0, 1.0] /// * `lightness` - Lightness channel. [0.0, 1.0] - pub const fn okhsl(hue: f32, saturation: f32, lightness: f32) -> Self { + pub const fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self { Self::new(hue, saturation, lightness, 1.0) } @@ -105,7 +105,7 @@ impl Okhsla { // Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range, // so that the closer the numbers are, the larger the difference of their image. let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360; - Self::okhsl(hue, 1., 0.5) + Self::hsl(hue, 1., 0.5) } } From 64f71bfff76d826d968836a3aca43cb5def81b46 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Thu, 7 May 2026 09:29:36 +0800 Subject: [PATCH 11/11] Fix wrong Hwba Okhsva conversion --- crates/bevy_color/src/okhsva.rs | 50 ++++++++------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/crates/bevy_color/src/okhsva.rs b/crates/bevy_color/src/okhsva.rs index 971c8f6a762a3..5eb236b0f8dde 100644 --- a/crates/bevy_color/src/okhsva.rs +++ b/crates/bevy_color/src/okhsva.rs @@ -156,44 +156,6 @@ impl Saturation for Okhsva { } } -impl From for Hwba { - fn from( - Okhsva { - hue, - saturation, - value, - alpha, - }: Okhsva, - ) -> Self { - // Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion - let whiteness = (1. - saturation) * value; - let blackness = 1. - value; - - Hwba::new(hue, whiteness, blackness, alpha) - } -} - -impl From for Okhsva { - fn from( - Hwba { - hue, - whiteness, - blackness, - alpha, - }: Hwba, - ) -> Self { - // Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion - let value = 1. - blackness; - let saturation = if value != 0. { - 1. - (whiteness / value) - } else { - 0. - }; - - Okhsva::new(hue, saturation, value, alpha) - } -} - impl ColorToComponents for Okhsva { fn to_f32_array(self) -> [f32; 4] { [self.hue, self.saturation, self.value, self.alpha] @@ -286,6 +248,18 @@ impl From for Srgba { } } +impl From for Okhsva { + fn from(value: Hwba) -> Self { + Oklaba::from(value).into() + } +} + +impl From for Hwba { + fn from(value: Okhsva) -> Self { + Oklaba::from(value).into() + } +} + impl From for Okhsva { fn from(value: Lcha) -> Self { Oklaba::from(value).into()