Skip to content
Open
Show file tree
Hide file tree
Changes from all 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),
}
}
}
54 changes: 53 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,58 @@ 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,
}

/// 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>;
}
Comment on lines +581 to +585
Copy link
Contributor

@LikeLakers2 LikeLakers2 Oct 23, 2025

Choose a reason for hiding this comment

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

Me thinks it's best to let the user specify an error type, since interpolation can fail for reasons specific to the type being interpolated.

Suggested change
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>;
}
pub trait TryStableInterpolate: Clone {
/// The type returned in the event a stable interpolation cannot be performed.
type Error;
/// Attempt to interpolate the value.
fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result<Self, Self::Error>;
}

This is similar to what TryFrom does, since conversion can fail for a variety of reasons, usually reasons specific to the types being converted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making the error type depend on the value type would defeat one of my purposes: to have a single uniform animation clip system that can mix both interpolable and non-interpolable types. Basically something like AnimatableProperty which can work for Val.

To be perfectly honest, we don't actually need an error at all, we could instead just make it an Option and return None. We never actually look at the error code, it's only there for documentation purposes and because the try_ name prefix suggest a Result return type. The animation only cares that the interpolation succeeded, it's not like it actually logs an error or anything.

Copy link
Contributor

@LikeLakers2 LikeLakers2 Oct 23, 2025

Choose a reason for hiding this comment

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

Could you elaborate? I'm not sure how an error type that can be specified by each impl would defeat that purpose.

Also, I would recommend keeping it as a Result, even if our error type is (). This is because Option implies that trying will always succeed but may sometimes return nothing, whereas Result tells the user that the operation may fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess that it could work, since the error value is ignored, but it seems a bit dodgy. Here's the way the animation code looks now:

match self.start.try_interpolate(&self.end, t) {
    Ok(value) => value,
    Err(_) => self.end, // If we can't interpolate, then just skip to the end
}

Where start and end are generics of type TransitionProperty::ValueType:

/// Represents an animatable property such as `BackgroundColor` or `Width`.
pub trait TransitionProperty {
    /// The data type of the animated property.
    type ValueType: Copy + Send + Sync + PartialEq + 'static + TryStableInterpolate;

    /// The type of component that contains the animated property.
    type ComponentType: Component<Mutability = Mutable>;

    /// Read the value of the animatable property from the component.
    fn get(component: &Self::ComponentType) -> Self::ValueType;

    /// Update the value of the animatable property in the component.
    fn set(component: &mut Mut<Self::ComponentType>, value: Self::ValueType);
}

Adding an extra generic parameter for error type would mean that I wouldn't be able to treat different value types quite as uniformly as before.

Copy link
Contributor

Choose a reason for hiding this comment

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

Adding an extra generic parameter for error type would mean that I wouldn't be able to treat different value types quite as uniformly as before.

I'm not sure what's meant by this. Can you give me an example of what is meant here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Poking @viridia - I wonder if you saw my comment just above this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I was waiting to see if anyone else had any thoughts on this issue.

For now I would like to keep this as simple as possible. I don't want to add an extra generic parameter unless there's an actual need for it, especially with the latest change where we only have one error type.

Copy link
Contributor

@LikeLakers2 LikeLakers2 Oct 26, 2025

Choose a reason for hiding this comment

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

You don't need an extra generic parameter to do this. The only time you'd need to specify the Error type as part of the type signature, is if you're targeting a specific error type.


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 +587 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.


/// 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.