From 043041f3aaa49d15295e9ed7422a152336cbe480 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Thu, 29 Feb 2024 08:45:48 +1100 Subject: [PATCH] `bevy_color`: Add `sequence_dispersed` to `Hsla`, `Lcha`, `Oklcha` (#12173) # Objective - Fixes #12170 ## Solution - Moved the existing `color_from_entity` internals into `Hsla::sequence_dispersed` which generates a randomly distributed but deterministic color sequence based. - Replicated the method for `Lcha` and `Oklcha` as well. ## Examples ### Getting a few colours for a quick palette ```rust let palette = Hsla::sequence_dispersed().take(5).collect::>(); /*[ Hsla::hsl(0.0, 1., 0.5), Hsla::hsl(222.49225, 1., 0.5), Hsla::hsl(84.984474, 1., 0.5), Hsla::hsl(307.4767, 1., 0.5), Hsla::hsl(169.96895, 1., 0.5), ]*/ ``` ### Getting a colour from an `Entity` ```rust let color = Oklcha::sequence_dispersed().nth(entity.index() as u32).unwrap(); ``` ## Notes This was previously a private function exclusively for `Entity` types. I've decided it should instead be public and operate on a `u32` directly, since this function may have broader uses for debugging purposes. --------- Co-authored-by: Alice Cecile --- crates/bevy_color/src/hsla.rs | 46 +++++++++++++++++++++++++++++++++ crates/bevy_color/src/lcha.rs | 29 +++++++++++++++++++++ crates/bevy_color/src/oklcha.rs | 29 +++++++++++++++++++++ crates/bevy_gizmos/src/aabb.rs | 14 ++-------- 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index b0774b1322dab..0559274427195 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -66,6 +66,35 @@ impl Hsla { 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 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::hsl(hue, 1., 0.5) + } } impl Default for Hsla { @@ -306,4 +335,21 @@ mod tests { assert_approx_eq!(hsla2.mix(&hsla0, 0.5).hue, 0., 0.001); assert_approx_eq!(hsla2.mix(&hsla0, 0.75).hue, 5., 0.001); } + + #[test] + fn test_from_index() { + let references = [ + Hsla::hsl(0.0, 1., 0.5), + Hsla::hsl(222.49225, 1., 0.5), + Hsla::hsl(84.984474, 1., 0.5), + Hsla::hsl(307.4767, 1., 0.5), + Hsla::hsl(169.96895, 1., 0.5), + ]; + + for (index, reference) in references.into_iter().enumerate() { + let color = Hsla::sequential_dispersed(index as u32); + + assert_approx_eq!(color.hue, reference.hue, 0.001); + } + } } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index 4e9192146b5ce..7a14ce854d3ac 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -70,6 +70,35 @@ impl Lcha { 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::Lcha; + /// // Unique color for an entity + /// # let entity_index = 123; + /// // let entity_index = entity.index(); + /// let color = Lcha::sequential_dispersed(entity_index); + /// + /// // Palette with 5 distinct hues + /// let palette = (0..5).map(Lcha::sequential_dispersed).collect::>(); + /// ``` + pub 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::lch(0.75, 0.35, hue) + } } impl Default for Lcha { diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 8fd2512b5a940..052b4b9edf7f5 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -69,6 +69,35 @@ impl Oklcha { pub const fn with_h(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`. + /// + /// This can be helpful for generating debug colors. + /// + /// # Examples + /// + /// ```rust + /// # use bevy_color::Oklcha; + /// // Unique color for an entity + /// # let entity_index = 123; + /// // let entity_index = entity.index(); + /// let color = Oklcha::sequential_dispersed(entity_index); + /// + /// // Palette with 5 distinct hues + /// let palette = (0..5).map(Oklcha::sequential_dispersed).collect::>(); + /// ``` + pub 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::lch(0.75, 0.1, hue) + } } impl Default for Oklcha { diff --git a/crates/bevy_gizmos/src/aabb.rs b/crates/bevy_gizmos/src/aabb.rs index 65b726bea9994..d264051703806 100644 --- a/crates/bevy_gizmos/src/aabb.rs +++ b/crates/bevy_gizmos/src/aabb.rs @@ -3,6 +3,7 @@ use crate as bevy_gizmos; use bevy_app::{Plugin, PostUpdate}; +use bevy_color::Oklcha; use bevy_ecs::{ component::Component, entity::Entity, @@ -97,18 +98,7 @@ fn draw_all_aabbs( } fn color_from_entity(entity: Entity) -> LegacyColor { - let index = entity.index(); - - // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ - // - // See https://en.wikipedia.org/wiki/Low-discrepancy_sequence - // 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. - const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up - const RATIO_360: f32 = 360.0 / u32::MAX as f32; - let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360; - - LegacyColor::hsl(hue, 1., 0.5) + Oklcha::sequential_dispersed(entity.index()).into() } fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform {