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
21 changes: 21 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::{MismatchedUnitsError, TryStableInterpolate};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::prelude::*;
use derive_more::derive::From;
Expand Down Expand Up @@ -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<Self, Self::Error> {
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),
}
}
}
58 changes: 57 additions & 1 deletion crates/bevy_math/src/common_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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 derive_more::Display;
use thiserror::Error;
use variadics_please::all_tuples_enumerated;

/// A type that supports the mathematical operations of a real vector space, irrespective of dimension.
Expand Down Expand Up @@ -396,7 +399,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 +541,59 @@ all_tuples_enumerated!(
T
);

/// Error produced when the values to be interpolated are not in the same units.
#[derive(Clone, Debug, Error, Display)]
pub struct MismatchedUnitsError;
Copy link
Contributor

@LikeLakers2 LikeLakers2 Oct 29, 2025

Choose a reason for hiding this comment

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

You'll probably want to impl Error for MismatchedUnitsError. Error requires Display also be implemented, so I'll also include such an impl here:

impl core::error::Error for MismatchedUnitsError {}

impl core::fmt::Display for MismatchedUnitsError {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		write!(f, "cannot interpolate between two values of different units")
	}
}


/// 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 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
/// [`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 {
/// 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<Self, Self::Error>;
}

impl<T: StableInterpolate> TryStableInterpolate for T {
type Error = Infallible;
fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result<Self, Self::Error> {
Ok(self.interpolate_stable(other, t))
}
}

/// A type that has tangents.
pub trait HasTangent {
/// The tangent type.
Expand Down
26 changes: 25 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::{MismatchedUnitsError, 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,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<Self, Self::Error> {
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<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.