diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 832394449bc4f..b28f2ca05e3d7 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -2,6 +2,7 @@ use crate::{ color_difference::EuclideanDistance, 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")] use bevy_reflect::prelude::*; use derive_more::derive::From; @@ -889,3 +890,23 @@ impl EuclideanDistance for Color { } } } + +impl TryStableInterpolate for Color { + type Error = MismatchedUnitsError; + + fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result { + match (self, other) { + (Color::Srgba(a), Color::Srgba(b)) => Ok(Color::Srgba(a.mix(b, t))), + (Color::LinearRgba(a), Color::LinearRgba(b)) => Ok(Color::LinearRgba(a.mix(b, t))), + (Color::Hsla(a), Color::Hsla(b)) => Ok(Color::Hsla(a.mix(b, t))), + (Color::Hsva(a), Color::Hsva(b)) => Ok(Color::Hsva(a.mix(b, t))), + (Color::Hwba(a), Color::Hwba(b)) => Ok(Color::Hwba(a.mix(b, t))), + (Color::Laba(a), Color::Laba(b)) => Ok(Color::Laba(a.mix(b, t))), + (Color::Lcha(a), Color::Lcha(b)) => Ok(Color::Lcha(a.mix(b, t))), + (Color::Oklaba(a), Color::Oklaba(b)) => Ok(Color::Oklaba(a.mix(b, t))), + (Color::Oklcha(a), Color::Oklcha(b)) => Ok(Color::Oklcha(a.mix(b, t))), + (Color::Xyza(a), Color::Xyza(b)) => Ok(Color::Xyza(a.mix(b, t))), + _ => Err(MismatchedUnitsError), + } + } +} diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index b249b34618ae9..d02e71b4861d0 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -2,9 +2,11 @@ use crate::{ops, DVec2, DVec3, DVec4, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use core::{ + convert::Infallible, fmt::Debug, ops::{Add, Div, Mul, Neg, Sub}, }; +use thiserror::Error; use variadics_please::all_tuples_enumerated; /// A type that supports the mathematical operations of a real vector space, irrespective of dimension. @@ -396,7 +398,7 @@ impl NormedVectorSpace for f64 { /// ```text /// top curve = u.interpolate_stable(v, t) /// -/// t0 => p t1 => q +/// t0 => p t1 => q /// |-------------|---------|-------------| /// 0 => u / \ 1 => v /// / \ @@ -538,6 +540,56 @@ all_tuples_enumerated!( T ); +/// Error produced when the values to be interpolated are not in the same units. +#[derive(Clone, Debug, Error)] +#[error("cannot interpolate between two values of different units")] +pub struct MismatchedUnitsError; + +/// A trait that indicates that a value _may_ be interpolable via [`StableInterpolate`]. An +/// interpolation may fail if the values have different units - for example, attempting to +/// interpolate between [`Val::Px`] and [`Val::Percent`] will fail, +/// even though they are the same Rust type. +/// +/// Fallible interpolation can be used for animated transitions, which can be set up to fail +/// gracefully if the the values cannot be interpolated. For example, the a transition could smoothly +/// go from `Val::Px(10)` to `Val::Px(20)`, but if the user attempts to go from `Val::Px(10)` to +/// `Val::Percent(10)`, the animation player can detect the failure and simply snap to the new +/// value without interpolating. +/// +/// An animation clip system can incorporate fallible interpolation to support a broad set of +/// sequenced parameter values. This can include numeric types, which always interpolate, +/// enum types, which may or may not interpolate depending on the units, and non-interpolable +/// types, which always jump immediately to the new value without interpolation. This meaas, for +/// example, that you can have an animation track whose value type is a boolean or a string. +/// +/// Interpolation for simple number and coordinate types will always succeed, as will any type +/// that implements [`StableInterpolate`]. Types which have different variants such as +/// [`Val`] and [`Color`] will only fail if the units are different. +/// Note that [`Color`] has its own, non-fallible mixing methods, but those entail +/// automatically converting between different color spaces, and is both expensive and complex. +/// [`TryStableInterpolate`] is more conservative, and doesn't automatically convert between +/// color spaces. This produces a color interpolation that has more predictable performance. +/// +/// [`Val::Px`]: https://docs.rs/bevy/latest/bevy/ui/enum.Val.html#variant.Px +/// [`Val::Percent`]: https://docs.rs/bevy/latest/bevy/ui/enum.Val.html#variant.Percent +/// [`Val`]: https://docs.rs/bevy/latest/bevy/ui/enum.Val.html +/// [`Color`]: https://docs.rs/bevy/latest/bevy/color/enum.Color.html +pub trait TryStableInterpolate: Clone { + /// Error produced when the value cannot be interpolated. + type Error; + + /// Attempt to interpolate the value. This may fail if the two interpolation values have + /// different units, or if the type is not interpolable. + fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result; +} + +impl TryStableInterpolate for T { + type Error = Infallible; + fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result { + Ok(self.interpolate_stable(other, t)) + } +} + /// A type that has tangents. pub trait HasTangent { /// The tangent type. diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 36296e748fa9f..70ff8f23ac4d2 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -1,4 +1,4 @@ -use bevy_math::Vec2; +use bevy_math::{MismatchedUnitsError, StableInterpolate as _, TryStableInterpolate, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_utils::default; use core::ops::{Div, DivAssign, Mul, MulAssign, Neg}; @@ -418,6 +418,30 @@ impl Val { } } +impl TryStableInterpolate for Val { + type Error = MismatchedUnitsError; + + /// # Example + /// + /// ``` + /// # use bevy_ui::Val; + /// # use bevy_math::TryStableInterpolate; + /// assert!(matches!(Val::Px(0.0).try_interpolate_stable(&Val::Px(10.0), 0.5), Ok(Val::Px(5.0)))); + /// ``` + fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result { + match (self, other) { + (Val::Px(a), Val::Px(b)) => Ok(Val::Px(a.interpolate_stable(b, t))), + (Val::Percent(a), Val::Percent(b)) => Ok(Val::Percent(a.interpolate_stable(b, t))), + (Val::Vw(a), Val::Vw(b)) => Ok(Val::Vw(a.interpolate_stable(b, t))), + (Val::Vh(a), Val::Vh(b)) => Ok(Val::Vh(a.interpolate_stable(b, t))), + (Val::VMin(a), Val::VMin(b)) => Ok(Val::VMin(a.interpolate_stable(b, t))), + (Val::VMax(a), Val::VMax(b)) => Ok(Val::VMax(a.interpolate_stable(b, t))), + (Val::Auto, Val::Auto) => Ok(Val::Auto), + _ => Err(MismatchedUnitsError), + } + } +} + /// All the types that should be able to be used in the [`Val`] enum should implement this trait. /// /// Instead of just implementing `Into` a custom trait is added. diff --git a/release-content/release-notes/fallible_interpolation.md b/release-content/release-notes/fallible_interpolation.md new file mode 100644 index 0000000000000..4d7674c5ec54b --- /dev/null +++ b/release-content/release-notes/fallible_interpolation.md @@ -0,0 +1,26 @@ +--- +title: Fallible Interpolation +authors: ["@viridia"] +pull_requests: [21633] +--- + +## Fallible Interpolation + +The `StableInterpolate` trait is great, but sadly there's one important type that it doesn't work +with: The `Val` type from `bevy_ui`. The reason is that `Val` is an enum, representing different +length units such as pixels and percentages, and it's not generally possible or even meaningful to +try and interpolate between different units. + +However, the use cases for wanting to animate `Val` don't require mixing units: often we just want +to slide or stretch the length of a widget such as a toggle switch. We can do this so long as we +check at runtime that both interpolation control points are in the same units. + +The new `TryStableInterpolate` trait introduces the idea of interpolation that can fail, by returning +a `Result`. Note that "failure" in this case is not necessarily bad: it just means that the +animation player will need to modify the parameter in some other way, such as "snapping" or +"jumping" to the new keyframe without smoothly interpolating. This lets us create complex animations +that incorporate both kinds of parameters: ones that interpolate, and ones that don't. + +There's a blanket implementation of `TryStableInterpolate` for all types that impl +`StableInterpolate`, and these can never fail. There are additional impls for `Color` and `Val` +which can fail if the control points are not in the same units / color space.