diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index efa3529905..d06e337012 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -61,3 +61,20 @@ pub trait Alpha: Sized { self.alpha() >= 1.0 } } + +/// Trait with methods for asserting a colorspace is within bounds. +/// +/// During ordinary usage (e.g. reading images from disk, rendering images, picking colors for UI), colors should always be within their ordinary bounds (such as 0 to 1 for RGB colors). +/// However, some applications, such as high dynamic range rendering or bloom rely on unbounded colors to naturally represent a wider array of choices. +pub trait ClampColor: Sized { + /// Return a new version of this color clamped, with all fields in bounds. + fn clamped(&self) -> Self; + + /// Changes all the fields of this color to ensure they are within bounds. + fn clamp(&mut self) { + *self = self.clamped(); + } + + /// Are all the fields of this color in bounds? + fn is_within_bounds(&self) -> bool; +} diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 5e49b4dbff..b908afd035 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,4 +1,6 @@ -use crate::{Alpha, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; +use crate::{ + Alpha, ClampColor, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, +}; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -166,6 +168,24 @@ impl Luminance for Hsla { } } +impl ClampColor for Hsla { + fn clamped(&self) -> Self { + Self { + hue: self.hue.rem_euclid(360.), + saturation: self.saturation.clamp(0., 1.), + lightness: self.lightness.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.saturation) + && (0. ..=1.).contains(&self.lightness) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Hsva { fn from( Hsla { @@ -357,4 +377,21 @@ mod tests { assert_approx_eq!(color.hue, reference.hue, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Hsla::hsl(361., 2., -1.); + let color_2 = Hsla::hsl(250.2762, 1., 0.67); + let mut color_3 = Hsla::hsl(-50., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Hsla::hsl(1., 1., 0.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Hsla::hsl(310., 1., 1.)); + } } diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index d895b157a5..8f45a476d6 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,4 +1,4 @@ -use crate::{Alpha, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; +use crate::{Alpha, ClampColor, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -91,6 +91,24 @@ impl Alpha for Hsva { } } +impl ClampColor for Hsva { + fn clamped(&self) -> Self { + Self { + hue: self.hue.rem_euclid(360.), + saturation: self.saturation.clamp(0., 1.), + value: self.value.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.saturation) + && (0. ..=1.).contains(&self.value) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Hwba { fn from( Hsva { @@ -212,4 +230,21 @@ mod tests { assert_approx_eq!(color.hsv.alpha, hsv2.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Hsva::hsv(361., 2., -1.); + let color_2 = Hsva::hsv(250.2762, 1., 0.67); + let mut color_3 = Hsva::hsv(-50., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Hsva::hsv(1., 1., 0.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Hsva::hsv(310., 1., 1.)); + } } diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs index a85f01b107..bfde45d8dc 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,7 +2,7 @@ //! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_. //! //! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf -use crate::{Alpha, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; +use crate::{Alpha, ClampColor, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -95,6 +95,24 @@ impl Alpha for Hwba { } } +impl ClampColor for Hwba { + fn clamped(&self) -> Self { + Self { + hue: self.hue.rem_euclid(360.), + whiteness: self.whiteness.clamp(0., 1.), + blackness: self.blackness.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.whiteness) + && (0. ..=1.).contains(&self.blackness) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Hwba { fn from( Srgba { @@ -245,4 +263,21 @@ mod tests { assert_approx_eq!(color.hwb.alpha, hwb2.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Hwba::hwb(361., 2., -1.); + let color_2 = Hwba::hwb(250.2762, 1., 0.67); + let mut color_3 = Hwba::hwb(-50., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Hwba::hwb(1., 1., 0.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Hwba::hwb(310., 1., 1.)); + } } diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 5b636665dc..10decc0504 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,5 +1,6 @@ use crate::{ - Alpha, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, + Xyza, }; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -110,6 +111,24 @@ impl Alpha for Laba { } } +impl ClampColor for Laba { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.5), + a: self.a.clamp(-1.5, 1.5), + b: self.b.clamp(-1.5, 1.5), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.5).contains(&self.lightness) + && (-1.5..=1.5).contains(&self.a) + && (-1.5..=1.5).contains(&self.b) + && (0. ..=1.).contains(&self.alpha) + } +} + impl Luminance for Laba { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -353,4 +372,21 @@ mod tests { assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Laba::lab(-1., 2., -2.); + let color_2 = Laba::lab(1., 1.5, -1.2); + let mut color_3 = Laba::lab(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Laba::lab(0., 1.5, -1.5)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Laba::lab(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index ea77ec49d7..cceaa43eea 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,4 +1,4 @@ -use crate::{Alpha, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; +use crate::{Alpha, ClampColor, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -166,6 +166,24 @@ impl Luminance for Lcha { } } +impl ClampColor for Lcha { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.5), + chroma: self.chroma.clamp(0., 1.5), + hue: self.hue.rem_euclid(360.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.5).contains(&self.lightness) + && (0. ..=1.5).contains(&self.chroma) + && (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Laba { fn from( Lcha { @@ -313,4 +331,21 @@ mod tests { assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Lcha::lch(-1., 2., 400.); + let color_2 = Lcha::lch(1., 1.5, 249.54); + let mut color_3 = Lcha::lch(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Lcha::lch(0., 1.5, 40.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Lcha::lch(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index 78856f36bd..864b402a28 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,6 +1,8 @@ use std::ops::{Div, Mul}; -use crate::{color_difference::EuclideanDistance, Alpha, Luminance, Mix, StandardColor}; +use crate::{ + color_difference::EuclideanDistance, Alpha, ClampColor, Luminance, Mix, StandardColor, +}; use bevy_math::Vec4; use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; @@ -256,6 +258,24 @@ impl EuclideanDistance for LinearRgba { } } +impl ClampColor for LinearRgba { + fn clamped(&self) -> Self { + Self { + red: self.red.clamp(0., 1.), + green: self.green.clamp(0., 1.), + blue: self.blue.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.red) + && (0. ..=1.).contains(&self.green) + && (0. ..=1.).contains(&self.blue) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for [f32; 4] { fn from(color: LinearRgba) -> Self { [color.red, color.green, color.blue, color.alpha] @@ -455,4 +475,21 @@ mod tests { let twice_as_light = color.lighter(0.2); assert!(lighter2.distance_squared(&twice_as_light) < 0.0001); } + + #[test] + fn test_clamp() { + let color_1 = LinearRgba::rgb(2., -1., 0.4); + let color_2 = LinearRgba::rgb(0.031, 0.749, 1.); + let mut color_3 = LinearRgba::rgb(-1., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), LinearRgba::rgb(1., 0., 0.4)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, LinearRgba::rgb(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 2281fe6538..2ec202a2fa 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,6 +1,6 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, - Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hwba, Lcha, LinearRgba, + Luminance, Mix, Srgba, StandardColor, Xyza, }; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -149,6 +149,24 @@ impl EuclideanDistance for Oklaba { } } +impl ClampColor for Oklaba { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.), + a: self.a.clamp(-1., 1.), + b: self.b.clamp(-1., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.lightness) + && (-1. ..=1.).contains(&self.a) + && (-1. ..=1.).contains(&self.b) + && (0. ..=1.).contains(&self.alpha) + } +} + #[allow(clippy::excessive_precision)] impl From for Oklaba { fn from(value: LinearRgba) -> Self { @@ -326,4 +344,21 @@ mod tests { assert_approx_eq!(oklaba.b, oklaba2.b, 0.001); assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001); } + + #[test] + fn test_clamp() { + let color_1 = Oklaba::lab(-1., 2., -2.); + let color_2 = Oklaba::lab(1., 0.42, -0.4); + let mut color_3 = Oklaba::lab(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Oklaba::lab(0., 1., -1.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Oklaba::lab(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 75f31325bb..4185990e5d 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,6 +1,6 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, - Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hwba, Laba, Lcha, + LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -209,6 +209,24 @@ impl From for Oklaba { } } +impl ClampColor for Oklcha { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.), + chroma: self.chroma.clamp(0., 1.), + hue: self.hue.rem_euclid(360.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.lightness) + && (0. ..=1.).contains(&self.chroma) + && (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.alpha) + } +} + // Derived Conversions impl From for Oklcha { @@ -355,4 +373,21 @@ mod tests { assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001); assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001); } + + #[test] + fn test_clamp() { + let color_1 = Oklcha::lch(-1., 2., 400.); + let color_2 = Oklcha::lch(1., 1., 249.54); + let mut color_3 = Oklcha::lch(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Oklcha::lch(0., 1., 40.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Oklcha::lch(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index 2101c5e5f2..606a6e2363 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,7 +1,7 @@ use std::ops::{Div, Mul}; use crate::color_difference::EuclideanDistance; -use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor, Xyza}; +use crate::{Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, Xyza}; use bevy_math::Vec4; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -307,6 +307,24 @@ impl EuclideanDistance for Srgba { } } +impl ClampColor for Srgba { + fn clamped(&self) -> Self { + Self { + red: self.red.clamp(0., 1.), + green: self.green.clamp(0., 1.), + blue: self.blue.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.red) + && (0. ..=1.).contains(&self.green) + && (0. ..=1.).contains(&self.blue) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Srgba { #[inline] fn from(value: LinearRgba) -> Self { @@ -490,4 +508,21 @@ mod tests { assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_)))); assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_)))); } + + #[test] + fn test_clamp() { + let color_1 = Srgba::rgb(2., -1., 0.4); + let color_2 = Srgba::rgb(0.031, 0.749, 1.); + let mut color_3 = Srgba::rgb(-1., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Srgba::rgb(1., 0., 0.4)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Srgba::rgb(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index ff10c5eab2..caee5e705c 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,4 +1,4 @@ -use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor}; +use crate::{Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor}; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -134,6 +134,24 @@ impl Mix for Xyza { } } +impl ClampColor for Xyza { + fn clamped(&self) -> Self { + Self { + x: self.x.clamp(0., 1.), + y: self.y.clamp(0., 1.), + z: self.z.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.x) + && (0. ..=1.).contains(&self.y) + && (0. ..=1.).contains(&self.z) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Xyza { fn from( LinearRgba { @@ -208,4 +226,21 @@ mod tests { assert_approx_eq!(color.xyz.alpha, xyz2.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Xyza::xyz(2., -1., 0.4); + let color_2 = Xyza::xyz(0.031, 0.749, 1.); + let mut color_3 = Xyza::xyz(-1., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Xyza::xyz(1., 0., 0.4)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Xyza::xyz(0., 1., 1.)); + } }