From a5066ab12bc9d9a028ad0a46cf84d40f63050afc Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 21:05:52 +0200 Subject: [PATCH 01/15] clamp `ColorRange` mix factor to valid range --- crates/bevy_color/src/color_range.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index 16d6f04866670..f0f77fa1b135f 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -15,7 +15,9 @@ pub trait ColorRange { impl ColorRange for Range { fn at(&self, factor: f32) -> T { - self.start.mix(&self.end, factor) + self.start.mix(&self.end, factor.clamp(0.0, 1.0)) + } +} } } From b7c5af355524cf1a4541302c1a3d8d87f5e184c2 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 22:02:16 +0200 Subject: [PATCH 02/15] implement `ColorGradient` struct --- crates/bevy_color/src/color_range.rs | 101 ++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index f0f77fa1b135f..cb8cde62b40a3 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -6,7 +6,7 @@ use crate::Mix; /// end point which must be in the same color space. It works for any color type that /// implements [`Mix`]. /// -/// This is useful for defining gradients or animated color transitions. +/// This is useful for defining simple gradients or animated color transitions. pub trait ColorRange { /// Get the color value at the given interpolation factor, which should be between 0.0 (start) /// and 1.0 (end). @@ -18,9 +18,78 @@ impl ColorRange for Range { self.start.mix(&self.end, factor.clamp(0.0, 1.0)) } } + +/// Represents a gradient of a minimum of 1 up to arbitrary many colors. Supported colors have to +/// implement [`Mix`] and have to be from the same color space. +/// +/// By default the color values are linearly interpolated. +/// +/// This is useful for defining complex gradients or animated color transitions. +pub struct ColorGradient { + colors: Vec, +} + +impl ColorGradient { + /// Create a new [`ColorGradient`] from a collection of [mixable] types. + /// + /// This fails if there's not at least one mixable type in the collection. + /// + /// [mixable]: `Mix` + /// + /// # Example + /// + /// ``` + /// # use bevy_color::palettes::basic::*; + /// # use bevy_color::ColorGradient; + /// let gradient = ColorGradient::new([RED, GREEN, BLUE]); + /// assert!(gradient.is_ok()); + /// ``` + pub fn new(colors: impl IntoIterator) -> Result { + let colors = colors.into_iter().collect::>(); + let len = colors.len(); + (!colors.is_empty()) + .then(|| Self { colors }) + .ok_or_else(|| ColorGradientError(len)) + } +} + +impl ColorRange for ColorGradient { + fn at(&self, factor: f32) -> T { + match self.colors.len() { + len if len == 0 => { + unreachable!("at least 1 by construction") + } + len if len == 1 => { + // This weirdness exists to prevent adding a `Clone` bound on the type `T` and instead + // work with what we already have here + self.colors[0].mix(&self.colors[0], 0.0) + } + len => { + // clamp to range of valid indices + let factor = factor.clamp(0.0, (len - 1) as f32); + let fract = factor.fract(); + if fract == 0.0 { + // doesn't need clamping since it already was clamped to valid indices + let exact_n = factor as usize; + // weirdness again + self.colors[exact_n].mix(&self.colors[exact_n], 0.0) + } else { + // SAFETY: we know that `len != 0` and `len != 1` here so `len >= 2` + let below = (factor.floor() as usize).min(len - 2); + self.colors[below].mix(&self.colors[below + 1], fract) + } + } + } } } +/// Error related to violations of invariants of [`ColorGradient`] +#[derive(Debug, thiserror::Error)] +#[error( + "Couldn't construct a ColorGradient since there were too few colors. Got {0}, expected >=1" +)] +pub struct ColorGradientError(usize); + #[cfg(test)] mod tests { use super::*; @@ -42,4 +111,34 @@ mod tests { assert_eq!(range.at(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0)); assert_eq!(range.at(1.0), lblue); } + + #[test] + fn test_color_gradient() { + let gradient = + ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).expect("Valid gradient"); + assert_eq!(gradient.at(-1.5), basic::RED); + assert_eq!(gradient.at(-1.0), basic::RED); + assert_eq!(gradient.at(0.0), basic::RED); + assert_eq!(gradient.at(0.5), Srgba::new(0.5, 0.5, 0.0, 1.0)); + assert_eq!(gradient.at(1.0), basic::LIME); + assert_eq!(gradient.at(1.5), Srgba::new(0.0, 0.5, 0.5, 1.0)); + assert_eq!(gradient.at(2.0), basic::BLUE); + assert_eq!(gradient.at(2.5), basic::BLUE); + assert_eq!(gradient.at(3.0), basic::BLUE); + + let lred: LinearRgba = basic::RED.into(); + let lgreen: LinearRgba = basic::LIME.into(); + let lblue: LinearRgba = basic::BLUE.into(); + + let gradient = ColorGradient::new([lred, lgreen, lblue]).expect("Valid gradient"); + assert_eq!(gradient.at(-1.5), lred); + assert_eq!(gradient.at(-1.0), lred); + assert_eq!(gradient.at(0.0), lred); + assert_eq!(gradient.at(0.5), LinearRgba::new(0.5, 0.5, 0.0, 1.0)); + assert_eq!(gradient.at(1.0), lgreen); + assert_eq!(gradient.at(1.5), LinearRgba::new(0.0, 0.5, 0.5, 1.0)); + assert_eq!(gradient.at(2.0), lblue); + assert_eq!(gradient.at(2.5), lblue); + assert_eq!(gradient.at(3.0), lblue); + } } From 89f4cad9fab570809d938cf746565fae31accd83 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 22:16:46 +0200 Subject: [PATCH 03/15] implement [`ColorCurve`] --- crates/bevy_color/src/color_range.rs | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index cb8cde62b40a3..06cedb9ffd6c5 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -1,5 +1,7 @@ use std::ops::Range; +use bevy_math::curve::{Curve, Interval}; + use crate::Mix; /// Represents a range of colors that can be linearly interpolated, defined by a start and @@ -25,6 +27,7 @@ impl ColorRange for Range { /// By default the color values are linearly interpolated. /// /// This is useful for defining complex gradients or animated color transitions. +#[derive(Debug, Clone)] pub struct ColorGradient { colors: Vec, } @@ -51,6 +54,33 @@ impl ColorGradient { .then(|| Self { colors }) .ok_or_else(|| ColorGradientError(len)) } + + /// Converts the [`ColorGradient`] to a [`ColorCurve`] which implements the [`Curve`] trait + /// along with its adaptor methods + /// # Example + /// + /// ``` + /// # use bevy_color::palettes::basic::*; + /// # use bevy_color::ColorGradient; + /// # use bevy_color::Srgba; + /// # use bevy_color::Mix; + /// # use bevy_math::curve::Curve; + /// let gradient = ColorGradient::new([RED, GREEN, BLUE]).unwrap(); + /// let curve = gradient.to_curve(); + /// + /// // you can then apply useful methods ontop of the gradient + /// let brighter_curve = curve.map(|c| c.mix(&WHITE, 0.25)); + /// + /// assert_eq!(brighter_curve.sample_unchecked(0.0), Srgba::new(1.0, 0.25, 0.25, 1.0)); + /// ``` + pub fn to_curve(self) -> ColorCurve { + let domain = + Interval::new(0.0, (self.colors.len() - 1) as f32).expect("at least 1 by construction"); + ColorCurve { + domain, + gradient: self, + } + } } impl ColorRange for ColorGradient { @@ -90,6 +120,24 @@ impl ColorRange for ColorGradient { )] pub struct ColorGradientError(usize); +/// A curve whose samples are defined by a [`ColorGradient`]. Curves of this type are produced by +/// calling [`ColorGradien::to_curve`]. +#[derive(Clone, Debug)] +pub struct ColorCurve { + domain: Interval, + gradient: ColorGradient, +} + +impl Curve for ColorCurve { + fn domain(&self) -> Interval { + self.domain + } + + fn sample_unchecked(&self, t: f32) -> T { + self.gradient.at(t) + } +} + #[cfg(test)] mod tests { use super::*; From 32739002e0b4fa77ff0bb293ebcf5ba759062063 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 22:34:32 +0200 Subject: [PATCH 04/15] give ColorGradient it's own module --- crates/bevy_color/src/color_gradient.rs | 156 ++++++++++++++++++++++++ crates/bevy_color/src/color_range.rs | 149 ---------------------- crates/bevy_color/src/lib.rs | 2 + 3 files changed, 158 insertions(+), 149 deletions(-) create mode 100644 crates/bevy_color/src/color_gradient.rs diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs new file mode 100644 index 0000000000000..758b8ab609a1e --- /dev/null +++ b/crates/bevy_color/src/color_gradient.rs @@ -0,0 +1,156 @@ +use crate::{ColorRange, Mix}; +use bevy_math::curve::{Curve, Interval}; + +/// Represents a gradient of a minimum of 1 up to arbitrary many colors. Supported colors have to +/// implement [`Mix`] and have to be from the same color space. +/// +/// By default the color values are linearly interpolated. +/// +/// This is useful for defining complex gradients or animated color transitions. +#[derive(Debug, Clone)] +pub struct ColorGradient { + colors: Vec, +} + +impl ColorGradient { + /// Create a new [`ColorGradient`] from a collection of [mixable] types. + /// + /// This fails if there's not at least one mixable type in the collection. + /// + /// [mixable]: `Mix` + /// + /// # Example + /// + /// ``` + /// # use bevy_color::palettes::basic::*; + /// # use bevy_color::ColorGradient; + /// let gradient = ColorGradient::new([RED, GREEN, BLUE]); + /// assert!(gradient.is_ok()); + /// ``` + pub fn new(colors: impl IntoIterator) -> Result { + let colors = colors.into_iter().collect::>(); + let len = colors.len(); + (!colors.is_empty()) + .then(|| Self { colors }) + .ok_or_else(|| ColorGradientError(len)) + } + + /// Converts the [`ColorGradient`] to a [`ColorCurve`] which implements the [`Curve`] trait + /// along with its adaptor methods + /// # Example + /// + /// ``` + /// # use bevy_color::palettes::basic::*; + /// # use bevy_color::ColorGradient; + /// # use bevy_color::Srgba; + /// # use bevy_color::Mix; + /// # use bevy_math::curve::Curve; + /// let gradient = ColorGradient::new([RED, GREEN, BLUE]).unwrap(); + /// let curve = gradient.to_curve(); + /// + /// // you can then apply useful methods ontop of the gradient + /// let brighter_curve = curve.map(|c| c.mix(&WHITE, 0.25)); + /// + /// assert_eq!(brighter_curve.sample_unchecked(0.0), Srgba::new(1.0, 0.25, 0.25, 1.0)); + /// ``` + pub fn to_curve(self) -> ColorCurve { + let domain = + Interval::new(0.0, (self.colors.len() - 1) as f32).expect("at least 1 by construction"); + ColorCurve { + domain, + gradient: self, + } + } +} + +impl ColorRange for ColorGradient { + fn at(&self, factor: f32) -> T { + match self.colors.len() { + len if len == 0 => { + unreachable!("at least 1 by construction") + } + len if len == 1 => { + // This weirdness exists to prevent adding a `Clone` bound on the type `T` and instead + // work with what we already have here + self.colors[0].mix(&self.colors[0], 0.0) + } + len => { + // clamp to range of valid indices + let factor = factor.clamp(0.0, (len - 1) as f32); + let fract = factor.fract(); + if fract == 0.0 { + // doesn't need clamping since it already was clamped to valid indices + let exact_n = factor as usize; + // weirdness again + self.colors[exact_n].mix(&self.colors[exact_n], 0.0) + } else { + // SAFETY: we know that `len != 0` and `len != 1` here so `len >= 2` + let below = (factor.floor() as usize).min(len - 2); + self.colors[below].mix(&self.colors[below + 1], fract) + } + } + } + } +} + +/// Error related to violations of invariants of [`ColorGradient`] +#[derive(Debug, thiserror::Error)] +#[error( + "Couldn't construct a ColorGradient since there were too few colors. Got {0}, expected >=1" +)] +pub struct ColorGradientError(usize); + +/// A curve whose samples are defined by a [`ColorGradient`]. Curves of this type are produced by +/// calling [`ColorGradien::to_curve`]. +#[derive(Clone, Debug)] +pub struct ColorCurve { + domain: Interval, + gradient: ColorGradient, +} + +impl Curve for ColorCurve { + fn domain(&self) -> Interval { + self.domain + } + + fn sample_unchecked(&self, t: f32) -> T { + self.gradient.at(t) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::palettes::basic; + use crate::{LinearRgba, Srgba}; + + #[test] + fn test_color_gradient() { + let gradient = + ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).expect("Valid gradient"); + assert_eq!(gradient.at(-1.5), basic::RED); + assert_eq!(gradient.at(-1.0), basic::RED); + assert_eq!(gradient.at(0.0), basic::RED); + assert_eq!(gradient.at(0.5), Srgba::new(0.5, 0.5, 0.0, 1.0)); + assert_eq!(gradient.at(1.0), basic::LIME); + assert_eq!(gradient.at(1.5), Srgba::new(0.0, 0.5, 0.5, 1.0)); + assert_eq!(gradient.at(2.0), basic::BLUE); + assert_eq!(gradient.at(2.5), basic::BLUE); + assert_eq!(gradient.at(3.0), basic::BLUE); + + let lred: LinearRgba = basic::RED.into(); + let lgreen: LinearRgba = basic::LIME.into(); + let lblue: LinearRgba = basic::BLUE.into(); + + let gradient = ColorGradient::new([lred, lgreen, lblue]).expect("Valid gradient"); + assert_eq!(gradient.at(-1.5), lred); + assert_eq!(gradient.at(-1.0), lred); + assert_eq!(gradient.at(0.0), lred); + assert_eq!(gradient.at(0.5), LinearRgba::new(0.5, 0.5, 0.0, 1.0)); + assert_eq!(gradient.at(1.0), lgreen); + assert_eq!(gradient.at(1.5), LinearRgba::new(0.0, 0.5, 0.5, 1.0)); + assert_eq!(gradient.at(2.0), lblue); + assert_eq!(gradient.at(2.5), lblue); + assert_eq!(gradient.at(3.0), lblue); + } +} diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index 06cedb9ffd6c5..14f494781b811 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -1,7 +1,5 @@ use std::ops::Range; -use bevy_math::curve::{Curve, Interval}; - use crate::Mix; /// Represents a range of colors that can be linearly interpolated, defined by a start and @@ -21,123 +19,6 @@ impl ColorRange for Range { } } -/// Represents a gradient of a minimum of 1 up to arbitrary many colors. Supported colors have to -/// implement [`Mix`] and have to be from the same color space. -/// -/// By default the color values are linearly interpolated. -/// -/// This is useful for defining complex gradients or animated color transitions. -#[derive(Debug, Clone)] -pub struct ColorGradient { - colors: Vec, -} - -impl ColorGradient { - /// Create a new [`ColorGradient`] from a collection of [mixable] types. - /// - /// This fails if there's not at least one mixable type in the collection. - /// - /// [mixable]: `Mix` - /// - /// # Example - /// - /// ``` - /// # use bevy_color::palettes::basic::*; - /// # use bevy_color::ColorGradient; - /// let gradient = ColorGradient::new([RED, GREEN, BLUE]); - /// assert!(gradient.is_ok()); - /// ``` - pub fn new(colors: impl IntoIterator) -> Result { - let colors = colors.into_iter().collect::>(); - let len = colors.len(); - (!colors.is_empty()) - .then(|| Self { colors }) - .ok_or_else(|| ColorGradientError(len)) - } - - /// Converts the [`ColorGradient`] to a [`ColorCurve`] which implements the [`Curve`] trait - /// along with its adaptor methods - /// # Example - /// - /// ``` - /// # use bevy_color::palettes::basic::*; - /// # use bevy_color::ColorGradient; - /// # use bevy_color::Srgba; - /// # use bevy_color::Mix; - /// # use bevy_math::curve::Curve; - /// let gradient = ColorGradient::new([RED, GREEN, BLUE]).unwrap(); - /// let curve = gradient.to_curve(); - /// - /// // you can then apply useful methods ontop of the gradient - /// let brighter_curve = curve.map(|c| c.mix(&WHITE, 0.25)); - /// - /// assert_eq!(brighter_curve.sample_unchecked(0.0), Srgba::new(1.0, 0.25, 0.25, 1.0)); - /// ``` - pub fn to_curve(self) -> ColorCurve { - let domain = - Interval::new(0.0, (self.colors.len() - 1) as f32).expect("at least 1 by construction"); - ColorCurve { - domain, - gradient: self, - } - } -} - -impl ColorRange for ColorGradient { - fn at(&self, factor: f32) -> T { - match self.colors.len() { - len if len == 0 => { - unreachable!("at least 1 by construction") - } - len if len == 1 => { - // This weirdness exists to prevent adding a `Clone` bound on the type `T` and instead - // work with what we already have here - self.colors[0].mix(&self.colors[0], 0.0) - } - len => { - // clamp to range of valid indices - let factor = factor.clamp(0.0, (len - 1) as f32); - let fract = factor.fract(); - if fract == 0.0 { - // doesn't need clamping since it already was clamped to valid indices - let exact_n = factor as usize; - // weirdness again - self.colors[exact_n].mix(&self.colors[exact_n], 0.0) - } else { - // SAFETY: we know that `len != 0` and `len != 1` here so `len >= 2` - let below = (factor.floor() as usize).min(len - 2); - self.colors[below].mix(&self.colors[below + 1], fract) - } - } - } - } -} - -/// Error related to violations of invariants of [`ColorGradient`] -#[derive(Debug, thiserror::Error)] -#[error( - "Couldn't construct a ColorGradient since there were too few colors. Got {0}, expected >=1" -)] -pub struct ColorGradientError(usize); - -/// A curve whose samples are defined by a [`ColorGradient`]. Curves of this type are produced by -/// calling [`ColorGradien::to_curve`]. -#[derive(Clone, Debug)] -pub struct ColorCurve { - domain: Interval, - gradient: ColorGradient, -} - -impl Curve for ColorCurve { - fn domain(&self) -> Interval { - self.domain - } - - fn sample_unchecked(&self, t: f32) -> T { - self.gradient.at(t) - } -} - #[cfg(test)] mod tests { use super::*; @@ -159,34 +40,4 @@ mod tests { assert_eq!(range.at(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0)); assert_eq!(range.at(1.0), lblue); } - - #[test] - fn test_color_gradient() { - let gradient = - ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).expect("Valid gradient"); - assert_eq!(gradient.at(-1.5), basic::RED); - assert_eq!(gradient.at(-1.0), basic::RED); - assert_eq!(gradient.at(0.0), basic::RED); - assert_eq!(gradient.at(0.5), Srgba::new(0.5, 0.5, 0.0, 1.0)); - assert_eq!(gradient.at(1.0), basic::LIME); - assert_eq!(gradient.at(1.5), Srgba::new(0.0, 0.5, 0.5, 1.0)); - assert_eq!(gradient.at(2.0), basic::BLUE); - assert_eq!(gradient.at(2.5), basic::BLUE); - assert_eq!(gradient.at(3.0), basic::BLUE); - - let lred: LinearRgba = basic::RED.into(); - let lgreen: LinearRgba = basic::LIME.into(); - let lblue: LinearRgba = basic::BLUE.into(); - - let gradient = ColorGradient::new([lred, lgreen, lblue]).expect("Valid gradient"); - assert_eq!(gradient.at(-1.5), lred); - assert_eq!(gradient.at(-1.0), lred); - assert_eq!(gradient.at(0.0), lred); - assert_eq!(gradient.at(0.5), LinearRgba::new(0.5, 0.5, 0.0, 1.0)); - assert_eq!(gradient.at(1.0), lgreen); - assert_eq!(gradient.at(1.5), LinearRgba::new(0.0, 0.5, 0.5, 1.0)); - assert_eq!(gradient.at(2.0), lblue); - assert_eq!(gradient.at(2.5), lblue); - assert_eq!(gradient.at(3.0), lblue); - } } diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 95c51a494db14..c03c1dd3480e8 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -92,6 +92,7 @@ mod color; pub mod color_difference; +mod color_gradient; mod color_ops; mod color_range; mod hsla; @@ -127,6 +128,7 @@ pub mod prelude { } pub use color::*; +pub use color_gradient::*; pub use color_ops::*; pub use color_range::*; pub use hsla::*; From a2430b26dabd40602aa330ae57a9eb926b79190e Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 22:35:16 +0200 Subject: [PATCH 05/15] extend ColorRange tests --- crates/bevy_color/src/color_range.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index 14f494781b811..5b83514298721 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -28,16 +28,20 @@ mod tests { #[test] fn test_color_range() { let range = basic::RED..basic::BLUE; + assert_eq!(range.at(-0.5), basic::RED); assert_eq!(range.at(0.0), basic::RED); assert_eq!(range.at(0.5), Srgba::new(0.5, 0.0, 0.5, 1.0)); assert_eq!(range.at(1.0), basic::BLUE); + assert_eq!(range.at(1.5), basic::BLUE); let lred: LinearRgba = basic::RED.into(); let lblue: LinearRgba = basic::BLUE.into(); let range = lred..lblue; + assert_eq!(range.at(-0.5), lred); assert_eq!(range.at(0.0), lred); assert_eq!(range.at(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0)); assert_eq!(range.at(1.0), lblue); + assert_eq!(range.at(1.5), lblue); } } From 4329d2ca517b54e5936b622f336cfbc8fc3a9a7d Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 22:47:38 +0200 Subject: [PATCH 06/15] add tests for curves --- crates/bevy_color/src/color_gradient.rs | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index 758b8ab609a1e..3f86e2b89487a 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -153,4 +153,33 @@ mod tests { assert_eq!(gradient.at(2.5), lblue); assert_eq!(gradient.at(3.0), lblue); } + + #[test] + fn test_color_curve() { + let gradient = ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).unwrap(); + let curve = gradient.to_curve(); + + assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap()); + + // you can then apply useful methods ontop of the gradient + let brighter_curve = curve.map(|c| c.mix(&basic::WHITE, 0.5)); + + [ + (-0.1, None), + (0.0, Some([1.0, 0.5, 0.5, 1.0])), + (0.5, Some([0.75, 0.75, 0.5, 1.0])), + (1.0, Some([0.5, 1.0, 0.5, 1.0])), + (1.5, Some([0.5, 0.75, 0.75, 1.0])), + (2.0, Some([0.5, 0.5, 1.0, 1.0])), + (2.1, None), + ] + .map(|(t, maybe_rgba)| { + let maybe_srgba = maybe_rgba.map(|[r, g, b, a]| Srgba::new(r, g, b, a)); + (t, maybe_srgba) + }) + .into_iter() + .for_each(|(t, maybe_color)| { + assert_eq!(brighter_curve.sample(t), maybe_color); + }); + } } From 5559af2d5f32a3d2bc445d767dcf8363bb941648 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 22:54:25 +0200 Subject: [PATCH 07/15] make clippy happy --- crates/bevy_color/src/color_gradient.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index 3f86e2b89487a..c425a99bcbabb 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -32,7 +32,7 @@ impl ColorGradient { let len = colors.len(); (!colors.is_empty()) .then(|| Self { colors }) - .ok_or_else(|| ColorGradientError(len)) + .ok_or(ColorGradientError(len)) } /// Converts the [`ColorGradient`] to a [`ColorCurve`] which implements the [`Curve`] trait @@ -66,10 +66,10 @@ impl ColorGradient { impl ColorRange for ColorGradient { fn at(&self, factor: f32) -> T { match self.colors.len() { - len if len == 0 => { + 0 => { unreachable!("at least 1 by construction") } - len if len == 1 => { + 1 => { // This weirdness exists to prevent adding a `Clone` bound on the type `T` and instead // work with what we already have here self.colors[0].mix(&self.colors[0], 0.0) @@ -161,7 +161,6 @@ mod tests { assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap()); - // you can then apply useful methods ontop of the gradient let brighter_curve = curve.map(|c| c.mix(&basic::WHITE, 0.5)); [ From 0b8009e411b1998bf2e0ae1f6704732bbef23846 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 29 Aug 2024 23:04:16 +0200 Subject: [PATCH 08/15] fix typo in docs --- crates/bevy_color/src/color_gradient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index c425a99bcbabb..c4646440cc037 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -101,7 +101,7 @@ impl ColorRange for ColorGradient { pub struct ColorGradientError(usize); /// A curve whose samples are defined by a [`ColorGradient`]. Curves of this type are produced by -/// calling [`ColorGradien::to_curve`]. +/// calling [`ColorGradient::to_curve`]. #[derive(Clone, Debug)] pub struct ColorCurve { domain: Interval, From f54e3f4f5322025faa001ef5bb3d54604f81e8c3 Mon Sep 17 00:00:00 2001 From: aviac Date: Sun, 1 Sep 2024 16:11:41 +0200 Subject: [PATCH 09/15] more docs + doc tests --- crates/bevy_color/src/color_gradient.rs | 182 +++++++----------------- 1 file changed, 54 insertions(+), 128 deletions(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index c4646440cc037..23610954e81f5 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -1,21 +1,22 @@ -use crate::{ColorRange, Mix}; -use bevy_math::curve::{Curve, Interval}; +use crate::Mix; +use bevy_math::curve::{cores::EvenCoreError, Curve, Interval, SampleCurve}; -/// Represents a gradient of a minimum of 1 up to arbitrary many colors. Supported colors have to -/// implement [`Mix`] and have to be from the same color space. -/// -/// By default the color values are linearly interpolated. -/// -/// This is useful for defining complex gradients or animated color transitions. -#[derive(Debug, Clone)] -pub struct ColorGradient { - colors: Vec, +/// A curve whose samples are defined by a collection of colors. Curves of this type are produced +/// by calling [`ColorGradient::to_curve`]. +#[derive(Clone, Debug)] +pub struct ColorCurve { + curve: SampleCurve, } -impl ColorGradient { - /// Create a new [`ColorGradient`] from a collection of [mixable] types. +impl ColorCurve +where + T: Mix + Clone, +{ + /// Create a new [`ColorCurve`] from a collection of [mixable] types and a mixing function. The + /// domain of this curve will always be `[0.0, len - 1]` where `len` is the amount of mixable + /// objects in the collection. /// - /// This fails if there's not at least one mixable type in the collection. + /// This fails if there's not at least two mixable things in the collection. /// /// [mixable]: `Mix` /// @@ -23,98 +24,50 @@ impl ColorGradient { /// /// ``` /// # use bevy_color::palettes::basic::*; - /// # use bevy_color::ColorGradient; - /// let gradient = ColorGradient::new([RED, GREEN, BLUE]); - /// assert!(gradient.is_ok()); - /// ``` - pub fn new(colors: impl IntoIterator) -> Result { - let colors = colors.into_iter().collect::>(); - let len = colors.len(); - (!colors.is_empty()) - .then(|| Self { colors }) - .ok_or(ColorGradientError(len)) - } - - /// Converts the [`ColorGradient`] to a [`ColorCurve`] which implements the [`Curve`] trait - /// along with its adaptor methods - /// # Example - /// - /// ``` - /// # use bevy_color::palettes::basic::*; - /// # use bevy_color::ColorGradient; - /// # use bevy_color::Srgba; /// # use bevy_color::Mix; + /// # use bevy_color::Srgba; + /// # use bevy_color::ColorCurve; + /// # use bevy_math::curve::Interval; /// # use bevy_math::curve::Curve; - /// let gradient = ColorGradient::new([RED, GREEN, BLUE]).unwrap(); - /// let curve = gradient.to_curve(); - /// - /// // you can then apply useful methods ontop of the gradient - /// let brighter_curve = curve.map(|c| c.mix(&WHITE, 0.25)); - /// - /// assert_eq!(brighter_curve.sample_unchecked(0.0), Srgba::new(1.0, 0.25, 0.25, 1.0)); + /// let broken = ColorCurve::new([RED], Srgba::mix); + /// assert!(broken.is_err()); + /// let gradient = ColorCurve::new([RED, GREEN, BLUE], Srgba::mix); + /// assert!(gradient.is_ok()); + /// assert_eq!(gradient.unwrap().domain(), Interval::new(0.0, 2.0).unwrap()); /// ``` - pub fn to_curve(self) -> ColorCurve { - let domain = - Interval::new(0.0, (self.colors.len() - 1) as f32).expect("at least 1 by construction"); - ColorCurve { - domain, - gradient: self, - } + pub fn new( + colors: impl IntoIterator>, + interpolation: I, + ) -> Result + where + I: Fn(&T, &T, f32) -> T, + { + let colors = colors.into_iter().map(|ic| ic.into()).collect::>(); + Interval::new(0.0, colors.len().saturating_sub(1) as f32) + .map_err(|_| EvenCoreError::NotEnoughSamples { + samples: colors.len(), + }) + .and_then(|domain| SampleCurve::new(domain, colors, interpolation)) + .map(|curve| Self { curve }) } } -impl ColorRange for ColorGradient { - fn at(&self, factor: f32) -> T { - match self.colors.len() { - 0 => { - unreachable!("at least 1 by construction") - } - 1 => { - // This weirdness exists to prevent adding a `Clone` bound on the type `T` and instead - // work with what we already have here - self.colors[0].mix(&self.colors[0], 0.0) - } - len => { - // clamp to range of valid indices - let factor = factor.clamp(0.0, (len - 1) as f32); - let fract = factor.fract(); - if fract == 0.0 { - // doesn't need clamping since it already was clamped to valid indices - let exact_n = factor as usize; - // weirdness again - self.colors[exact_n].mix(&self.colors[exact_n], 0.0) - } else { - // SAFETY: we know that `len != 0` and `len != 1` here so `len >= 2` - let below = (factor.floor() as usize).min(len - 2); - self.colors[below].mix(&self.colors[below + 1], fract) - } - } - } - } -} - -/// Error related to violations of invariants of [`ColorGradient`] +/// Error related to violations of invariants of [`ColorCurve`] #[derive(Debug, thiserror::Error)] -#[error( - "Couldn't construct a ColorGradient since there were too few colors. Got {0}, expected >=1" -)] -pub struct ColorGradientError(usize); - -/// A curve whose samples are defined by a [`ColorGradient`]. Curves of this type are produced by -/// calling [`ColorGradient::to_curve`]. -#[derive(Clone, Debug)] -pub struct ColorCurve { - domain: Interval, - gradient: ColorGradient, -} - -impl Curve for ColorCurve { +#[error("Couldn't construct a ColorCurve since there were too few colors. Got {0}, expected >=2")] +pub struct ColorCurveError(usize); + +impl Curve for ColorCurve +where + T: Mix + Clone, + F: Fn(&T, &T, f32) -> T, +{ fn domain(&self) -> Interval { - self.domain + self.curve.domain() } fn sample_unchecked(&self, t: f32) -> T { - self.gradient.at(t) + self.curve.sample_unchecked(t) } } @@ -122,42 +75,15 @@ impl Curve for ColorCurve { mod tests { use super::*; use crate::palettes::basic; - use crate::{LinearRgba, Srgba}; - - #[test] - fn test_color_gradient() { - let gradient = - ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).expect("Valid gradient"); - assert_eq!(gradient.at(-1.5), basic::RED); - assert_eq!(gradient.at(-1.0), basic::RED); - assert_eq!(gradient.at(0.0), basic::RED); - assert_eq!(gradient.at(0.5), Srgba::new(0.5, 0.5, 0.0, 1.0)); - assert_eq!(gradient.at(1.0), basic::LIME); - assert_eq!(gradient.at(1.5), Srgba::new(0.0, 0.5, 0.5, 1.0)); - assert_eq!(gradient.at(2.0), basic::BLUE); - assert_eq!(gradient.at(2.5), basic::BLUE); - assert_eq!(gradient.at(3.0), basic::BLUE); - - let lred: LinearRgba = basic::RED.into(); - let lgreen: LinearRgba = basic::LIME.into(); - let lblue: LinearRgba = basic::BLUE.into(); - - let gradient = ColorGradient::new([lred, lgreen, lblue]).expect("Valid gradient"); - assert_eq!(gradient.at(-1.5), lred); - assert_eq!(gradient.at(-1.0), lred); - assert_eq!(gradient.at(0.0), lred); - assert_eq!(gradient.at(0.5), LinearRgba::new(0.5, 0.5, 0.0, 1.0)); - assert_eq!(gradient.at(1.0), lgreen); - assert_eq!(gradient.at(1.5), LinearRgba::new(0.0, 0.5, 0.5, 1.0)); - assert_eq!(gradient.at(2.0), lblue); - assert_eq!(gradient.at(2.5), lblue); - assert_eq!(gradient.at(3.0), lblue); - } + use crate::Srgba; #[test] fn test_color_curve() { - let gradient = ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).unwrap(); - let curve = gradient.to_curve(); + let broken = ColorCurve::new([basic::RED], Srgba::mix); + assert!(broken.is_err()); + + let gradient = [basic::RED, basic::LIME, basic::BLUE]; + let curve = ColorCurve::new(gradient, Srgba::mix).unwrap(); assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap()); From 66d285912d6cf9305958b124951d94a6ca8aa239 Mon Sep 17 00:00:00 2001 From: aviac Date: Sun, 1 Sep 2024 16:12:47 +0200 Subject: [PATCH 10/15] undo wording change --- crates/bevy_color/src/color_range.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index 5b83514298721..72260b27f4a7e 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -6,7 +6,7 @@ use crate::Mix; /// end point which must be in the same color space. It works for any color type that /// implements [`Mix`]. /// -/// This is useful for defining simple gradients or animated color transitions. +/// This is useful for defining gradients or animated color transitions. pub trait ColorRange { /// Get the color value at the given interpolation factor, which should be between 0.0 (start) /// and 1.0 (end). From 8f36f09508e028440e03ecb55df580f04bd599da Mon Sep 17 00:00:00 2001 From: aviac Date: Sun, 1 Sep 2024 16:27:10 +0200 Subject: [PATCH 11/15] cleanup --- crates/bevy_color/src/color_gradient.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index 23610954e81f5..c93346e4d8292 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -1,8 +1,7 @@ use crate::Mix; use bevy_math::curve::{cores::EvenCoreError, Curve, Interval, SampleCurve}; -/// A curve whose samples are defined by a collection of colors. Curves of this type are produced -/// by calling [`ColorGradient::to_curve`]. +/// A curve whose samples are defined by a collection of colors. #[derive(Clone, Debug)] pub struct ColorCurve { curve: SampleCurve, @@ -52,11 +51,6 @@ where } } -/// Error related to violations of invariants of [`ColorCurve`] -#[derive(Debug, thiserror::Error)] -#[error("Couldn't construct a ColorCurve since there were too few colors. Got {0}, expected >=2")] -pub struct ColorCurveError(usize); - impl Curve for ColorCurve where T: Mix + Clone, From 63f6827dad99144a03ca29532e165b5afebdb70e Mon Sep 17 00:00:00 2001 From: aviac Date: Sun, 1 Sep 2024 16:27:49 +0200 Subject: [PATCH 12/15] clippy --- crates/bevy_color/src/color_gradient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index c93346e4d8292..aa21cb39f75d7 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -41,7 +41,7 @@ where where I: Fn(&T, &T, f32) -> T, { - let colors = colors.into_iter().map(|ic| ic.into()).collect::>(); + let colors = colors.into_iter().map(Into::into).collect::>(); Interval::new(0.0, colors.len().saturating_sub(1) as f32) .map_err(|_| EvenCoreError::NotEnoughSamples { samples: colors.len(), From 45edfd3de9bb58fbac23e928ab290c441d86233f Mon Sep 17 00:00:00 2001 From: aviac Date: Mon, 2 Sep 2024 05:59:21 +0200 Subject: [PATCH 13/15] simplify API to use `Mix::mix` by default Co-Authored-By: Zachary Harrold --- crates/bevy_color/src/color_gradient.rs | 33 +++++++++++-------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index aa21cb39f75d7..8b30d2167bf6d 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -3,11 +3,12 @@ use bevy_math::curve::{cores::EvenCoreError, Curve, Interval, SampleCurve}; /// A curve whose samples are defined by a collection of colors. #[derive(Clone, Debug)] -pub struct ColorCurve { - curve: SampleCurve, +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct ColorCurve { + curve: SampleCurve T>, } -impl ColorCurve +impl ColorCurve where T: Mix + Clone, { @@ -28,33 +29,27 @@ where /// # use bevy_color::ColorCurve; /// # use bevy_math::curve::Interval; /// # use bevy_math::curve::Curve; - /// let broken = ColorCurve::new([RED], Srgba::mix); + /// let broken = ColorCurve::new([RED]); /// assert!(broken.is_err()); - /// let gradient = ColorCurve::new([RED, GREEN, BLUE], Srgba::mix); + /// let gradient = ColorCurve::new([RED, GREEN, BLUE]); /// assert!(gradient.is_ok()); /// assert_eq!(gradient.unwrap().domain(), Interval::new(0.0, 2.0).unwrap()); /// ``` - pub fn new( - colors: impl IntoIterator>, - interpolation: I, - ) -> Result - where - I: Fn(&T, &T, f32) -> T, - { - let colors = colors.into_iter().map(Into::into).collect::>(); + pub fn new(colors: impl IntoIterator) -> Result { + let colors = colors.into_iter().collect::>(); + let mix: fn(&T, &T, f32) -> T = ::mix; Interval::new(0.0, colors.len().saturating_sub(1) as f32) .map_err(|_| EvenCoreError::NotEnoughSamples { samples: colors.len(), }) - .and_then(|domain| SampleCurve::new(domain, colors, interpolation)) + .and_then(|domain| SampleCurve::new(domain, colors, mix)) .map(|curve| Self { curve }) } } -impl Curve for ColorCurve +impl Curve for ColorCurve where T: Mix + Clone, - F: Fn(&T, &T, f32) -> T, { fn domain(&self) -> Interval { self.curve.domain() @@ -73,15 +68,15 @@ mod tests { #[test] fn test_color_curve() { - let broken = ColorCurve::new([basic::RED], Srgba::mix); + let broken = ColorCurve::new([basic::RED]); assert!(broken.is_err()); let gradient = [basic::RED, basic::LIME, basic::BLUE]; - let curve = ColorCurve::new(gradient, Srgba::mix).unwrap(); + let curve = ColorCurve::new(gradient).unwrap(); assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap()); - let brighter_curve = curve.map(|c| c.mix(&basic::WHITE, 0.5)); + let brighter_curve = curve.map(|c: Srgba| c.mix(&basic::WHITE, 0.5)); [ (-0.1, None), From 062f781fa53f798165345973d542bbd2db40eff8 Mon Sep 17 00:00:00 2001 From: aviac Date: Mon, 2 Sep 2024 06:16:44 +0200 Subject: [PATCH 14/15] adjust docs --- crates/bevy_color/src/color_gradient.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index 8b30d2167bf6d..0f230484d7a5f 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -12,9 +12,9 @@ impl ColorCurve where T: Mix + Clone, { - /// Create a new [`ColorCurve`] from a collection of [mixable] types and a mixing function. The - /// domain of this curve will always be `[0.0, len - 1]` where `len` is the amount of mixable - /// objects in the collection. + /// Create a new [`ColorCurve`] from a collection of [mixable] types. The domain of this curve + /// will always be `[0.0, len - 1]` where `len` is the amount of mixable objects in the + /// collection. /// /// This fails if there's not at least two mixable things in the collection. /// From aeca4d46af0d50d17b469968cec7838f54d98bdd Mon Sep 17 00:00:00 2001 From: aviac Date: Mon, 2 Sep 2024 16:20:17 +0200 Subject: [PATCH 15/15] refactor to EvenCore Co-authored-by: Matty --- crates/bevy_color/src/color_gradient.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index 0f230484d7a5f..60920d6ca3576 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -1,11 +1,15 @@ use crate::Mix; -use bevy_math::curve::{cores::EvenCoreError, Curve, Interval, SampleCurve}; +use bevy_math::curve::{ + cores::{EvenCore, EvenCoreError}, + Curve, Interval, +}; /// A curve whose samples are defined by a collection of colors. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct ColorCurve { - curve: SampleCurve T>, +pub struct ColorCurve { + core: EvenCore, } impl ColorCurve @@ -37,13 +41,12 @@ where /// ``` pub fn new(colors: impl IntoIterator) -> Result { let colors = colors.into_iter().collect::>(); - let mix: fn(&T, &T, f32) -> T = ::mix; Interval::new(0.0, colors.len().saturating_sub(1) as f32) .map_err(|_| EvenCoreError::NotEnoughSamples { samples: colors.len(), }) - .and_then(|domain| SampleCurve::new(domain, colors, mix)) - .map(|curve| Self { curve }) + .and_then(|domain| EvenCore::new(domain, colors)) + .map(|core| Self { core }) } } @@ -52,11 +55,11 @@ where T: Mix + Clone, { fn domain(&self) -> Interval { - self.curve.domain() + self.core.domain() } fn sample_unchecked(&self, t: f32) -> T { - self.curve.sample_unchecked(t) + self.core.sample_with(t, T::mix) } }