Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/bevy_color/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{InterpolationError, TryStableInterpolate};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::prelude::*;
use derive_more::derive::From;
Expand Down Expand Up @@ -889,3 +890,21 @@ impl EuclideanDistance for Color {
}
}
}

impl TryStableInterpolate for Color {
fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result<Self, InterpolationError> {
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(InterpolationError::MismatchedUnits),
}
}
}
64 changes: 63 additions & 1 deletion crates/bevy_math/src/common_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,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
/// / \
Expand Down Expand Up @@ -538,6 +538,68 @@ all_tuples_enumerated!(
T
);

/// Why the interpolation failed.
#[derive(Clone, Debug)]
pub enum InterpolationError {
/// The values to be interpolated are not in the same units.
MismatchedUnits,
/// Data type cannot be interpolated because it is not contiguous
NonContiguous,
}

/// 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.
///
/// The motivating case for this trait is animating UI entities with [`Val`]
/// properties, which, because they are enums, cannot be interpolated in the normal way. This same
/// concept can be extended to other types as well.
///
/// Fallible interpolation can be used for animated transitions, which can be set up to fail
/// gracefully if there's a mismatch of units. 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 is has more predictable performance.
///
/// [`Val::Px`]: https://docs.rs/bevy/latest/bevy/ui/enum.Val.html
/// [`Val::Percent`]: https://docs.rs/bevy/latest/bevy/ui/enum.Val.html
/// [`Val`]: https://docs.rs/bevy/latest/bevy/ui/struct.enum.html
/// [`Color`]: https://docs.rs/bevy/latest/bevy/color/enum.Color.html
pub trait TryStableInterpolate: Clone {
/// 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<Self, InterpolationError>;
}

impl<T: StableInterpolate> TryStableInterpolate for T {
fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result<Self, InterpolationError> {
Ok(self.interpolate_stable(other, t))
}
}
Comment on lines 586 to 591
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, so this ended up working? I'm curious what was different about how you initially tested it, that caused the error you were reporting.

Copy link
Contributor Author

@viridia viridia Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it has to do with the fact that the impls are in the same module as the target types instead of being in the same module as the trait definition. But to be honest, I'm not really sure why it works.


/// Boolean values can never be interpolated, but they can be animatable parameters (for things
/// like enabling and disabling lighting).
impl TryStableInterpolate for bool {
fn try_interpolate_stable(&self, _other: &Self, _t: f32) -> Result<Self, InterpolationError> {
Err(InterpolationError::NonContiguous)
}
}

/// A type that has tangents.
pub trait HasTangent {
/// The tangent type.
Expand Down
24 changes: 23 additions & 1 deletion crates/bevy_ui/src/geometry.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bevy_math::Vec2;
use bevy_math::{InterpolationError, StableInterpolate as _, TryStableInterpolate, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_utils::default;
use core::ops::{Div, DivAssign, Mul, MulAssign, Neg};
Expand Down Expand Up @@ -418,6 +418,28 @@ impl Val {
}
}

impl TryStableInterpolate for Val {
/// # 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<Self, InterpolationError> {
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(InterpolationError::MismatchedUnits),
}
}
}

/// All the types that should be able to be used in the [`Val`] enum should implement this trait.
///
/// Instead of just implementing `Into<Val>` a custom trait is added.
Expand Down
26 changes: 26 additions & 0 deletions release-content/release-notes/fallible_interpolation.md
Original file line number Diff line number Diff line change
@@ -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.