Add hue traits (#12399)

# Objective

Fixes #12200 .

## Solution

I added a Hue Trait with the rotate_hue method to enable hue rotation.
Additionally, I modified the implementation of animations in the
animated_material sample.

---

## Changelog

- Added a  `Hue` trait to `bevy_color/src/color_ops.rs`.
- Added the `Hue` trait implementation to `Hsla`, `Hsva`, `Hwba`,
`Lcha`, and `Oklcha`.
- Updated animated_material sample.

## Migration Guide

Users of Oklcha need to change their usage to use the with_hue method
instead of the with_h method.

---------

Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
oyasumi731 2024-03-22 09:36:46 +09:00 committed by GitHub
parent 887bc27a6f
commit 0950348916
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 137 additions and 37 deletions

View file

@ -62,6 +62,24 @@ pub trait Alpha: Sized {
}
}
/// Trait for manipulating the hue of a color.
pub trait Hue: Sized {
/// Return a new version of this color with the hue channel set to the given value.
fn with_hue(&self, hue: f32) -> Self;
/// Return the hue of this color [0.0, 360.0].
fn hue(&self) -> f32;
/// Sets the hue of this color.
fn set_hue(&mut self, hue: f32);
/// Return a new version of this color with the hue channel rotated by the given degrees.
fn rotate_hue(&self, degrees: f32) -> Self {
let rotated_hue = (self.hue() + degrees).rem_euclid(360.);
self.with_hue(rotated_hue)
}
}
/// 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).
@ -78,3 +96,21 @@ pub trait ClampColor: Sized {
/// Are all the fields of this color in bounds?
fn is_within_bounds(&self) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Hsla;
#[test]
fn test_rotate_hue() {
let hsla = Hsla::hsl(180.0, 1.0, 0.5);
assert_eq!(hsla.rotate_hue(90.0), Hsla::hsl(270.0, 1.0, 0.5));
assert_eq!(hsla.rotate_hue(-90.0), Hsla::hsl(90.0, 1.0, 0.5));
assert_eq!(hsla.rotate_hue(180.0), Hsla::hsl(0.0, 1.0, 0.5));
assert_eq!(hsla.rotate_hue(-180.0), Hsla::hsl(0.0, 1.0, 0.5));
assert_eq!(hsla.rotate_hue(0.0), hsla);
assert_eq!(hsla.rotate_hue(360.0), hsla);
assert_eq!(hsla.rotate_hue(-360.0), hsla);
}
}

View file

@ -1,5 +1,6 @@
use crate::{
Alpha, ClampColor, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza,
Alpha, ClampColor, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor,
Xyza,
};
use bevy_reflect::prelude::*;
use serde::{Deserialize, Serialize};
@ -54,11 +55,6 @@ impl Hsla {
Self::new(hue, saturation, lightness, 1.0)
}
/// Return a copy of this color with the hue channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// 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 }
@ -143,6 +139,23 @@ impl Alpha for Hsla {
}
}
impl Hue for Hsla {
#[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 Luminance for Hsla {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {

View file

@ -1,4 +1,4 @@
use crate::{Alpha, ClampColor, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza};
use crate::{Alpha, ClampColor, Hue, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza};
use bevy_reflect::prelude::*;
use serde::{Deserialize, Serialize};
@ -52,11 +52,6 @@ impl Hsva {
Self::new(hue, saturation, value, 1.0)
}
/// Return a copy of this color with the hue channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// 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 }
@ -91,6 +86,23 @@ impl Alpha for Hsva {
}
}
impl Hue for Hsva {
#[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 ClampColor for Hsva {
fn clamped(&self) -> Self {
Self {

View file

@ -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, ClampColor, Lcha, LinearRgba, Srgba, StandardColor, Xyza};
use crate::{Alpha, ClampColor, Hue, Lcha, LinearRgba, Srgba, StandardColor, Xyza};
use bevy_reflect::prelude::*;
use serde::{Deserialize, Serialize};
@ -56,11 +56,6 @@ impl Hwba {
Self::new(hue, whiteness, blackness, 1.0)
}
/// Return a copy of this color with the hue channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// Return a copy of this color with the whiteness channel set to the given value.
pub const fn with_whiteness(self, whiteness: f32) -> Self {
Self { whiteness, ..self }
@ -95,6 +90,23 @@ impl Alpha for Hwba {
}
}
impl Hue for Hwba {
#[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 ClampColor for Hwba {
fn clamped(&self) -> Self {
Self {

View file

@ -1,4 +1,4 @@
use crate::{Alpha, ClampColor, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza};
use crate::{Alpha, ClampColor, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza};
use bevy_reflect::prelude::*;
use serde::{Deserialize, Serialize};
@ -56,11 +56,6 @@ impl Lcha {
}
}
/// Return a copy of this color with the hue channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// Return a copy of this color with the chroma channel set to the given value.
pub const fn with_chroma(self, chroma: f32) -> Self {
Self { chroma, ..self }
@ -137,6 +132,23 @@ impl Alpha for Lcha {
}
}
impl Hue for Lcha {
#[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 Luminance for Lcha {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {

View file

@ -1,5 +1,5 @@
use crate::{
color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hwba, Laba, Lcha,
color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hue, Hwba, Laba, Lcha,
LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza,
};
use bevy_reflect::prelude::*;
@ -65,11 +65,6 @@ impl Oklcha {
Self { chroma, ..self }
}
/// Return a copy of this color with the 'hue' channel set to the given value.
pub const fn with_hue(self, hue: f32) -> Self {
Self { hue, ..self }
}
/// Generate a deterministic but [quasi-randomly distributed](https://en.wikipedia.org/wiki/Low-discrepancy_sequence)
/// color from a provided `index`.
///
@ -136,6 +131,23 @@ impl Alpha for Oklcha {
}
}
impl Hue for Oklcha {
#[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 Luminance for Oklcha {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {

View file

@ -30,14 +30,19 @@ fn setup(
));
let cube = meshes.add(Cuboid::new(0.5, 0.5, 0.5));
const GOLDEN_ANGLE: f32 = 137.507_77;
let mut hsla = Hsla::hsl(0.0, 1.0, 0.5);
for x in -1..2 {
for z in -1..2 {
commands.spawn(PbrBundle {
mesh: cube.clone(),
material: materials.add(Color::WHITE),
material: materials.add(Color::from(hsla)),
transform: Transform::from_translation(Vec3::new(x as f32, 0.0, z as f32)),
..default()
});
hsla = hsla.rotate_hue(GOLDEN_ANGLE);
}
}
}
@ -47,13 +52,11 @@ fn animate_materials(
time: Res<Time>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
for (i, material_handle) in material_handles.iter().enumerate() {
for material_handle in material_handles.iter() {
if let Some(material) = materials.get_mut(material_handle) {
material.base_color = Color::hsl(
((i as f32 * 2.345 + time.elapsed_seconds_wrapped()) * 100.0) % 360.0,
1.0,
0.5,
);
if let Color::Hsla(ref mut hsla) = material.base_color {
*hsla = hsla.rotate_hue(time.delta_seconds() * 100.0);
}
}
}
}