
# Objective Simplify the API surrounding easing curves. Broaden the base of types that support easing. ## Solution There is now a single library function, `easing_curve`, which constructs a unit-parametrized easing curve between two values based on an `EaseFunction`: ```rust /// Given a `start` and `end` value, create a curve parametrized over [the unit interval] /// that connects them, using the given [ease function] to determine the form of the /// curve in between. /// /// [the unit interval]: Interval::UNIT /// [ease function]: EaseFunction pub fn easing_curve<T: Ease>(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve<T> { //... } ``` As this shows, the type of the output curve is generic only in `T`. In particular, as long as `T` is `Reflect` (and `FromReflect` etc. — i.e., a standard "well-behaved" reflectable type), `EasingCurve<T>` is also `Reflect`, and there is no special field handling nonsense. Therefore, `EasingCurve` is the kind of thing that would be able to be easily changed in an editor. This is made possible by storing the actual `EaseFunction` on `EasingCurve<T>` instead of indirecting through some kind of function type (which generally leads to issues with reflection). The types that can be eased are those that implement a trait `Ease`: ```rust /// A type whose values can be eased between. /// /// This requires the construction of an interpolation curve that actually extends /// beyond the curve segment that connects two values, because an easing curve may /// extrapolate before the starting value and after the ending value. This is /// especially common in easing functions that mimic elastic or springlike behavior. pub trait Ease: Sized { /// Given `start` and `end` values, produce a curve with [unlimited domain] /// that: /// - takes a value equivalent to `start` at `t = 0` /// - takes a value equivalent to `end` at `t = 1` /// - has constant speed everywhere, including outside of `[0, 1]` /// /// [unlimited domain]: Interval::EVERYWHERE fn interpolating_curve_unbounded(start: &Self, end: &Self) -> impl Curve<Self>; } ``` (I know, I know, yet *another* interpolation trait. See 'Future direction'.) The other existing easing functions from the previous version of this module have also become new members of `EaseFunction`: `Linear`, `Steps`, and `Elastic` (which maybe needs a different name). The latter two are parametrized. ## Testing Tested using the `easing_functions` example. I also axed the `cubic_curve` example which was of questionable value and replaced it with `eased_motion`, which uses this API in the context of animation: https://github.com/user-attachments/assets/3c802992-6b9b-4b56-aeb1-a47501c29ce2 --- ## Future direction Morally speaking, `Ease` is incredibly similar to `StableInterpolate`. Probably, we should just merge `StableInterpolate` into `Ease`, and then make `SmoothNudge` an automatic extension trait of `Ease`. The reason I didn't do that is that `StableInterpolate` is not implemented for `VectorSpace` because of concerns about the `Color` types, and I wanted to avoid controversy. I think that may be a good idea though. As Alice mentioned before, we should also probably get rid of the `interpolation` dependency. The parametrized `Elastic` variant probably also needs some additional work (e.g. renaming, in/out/in-out variants, etc.) if we want to keep it.
182 lines
6.0 KiB
Rust
182 lines
6.0 KiB
Rust
//! Demonstrates the behavior of the built-in easing functions.
|
|
|
|
use bevy::{prelude::*, sprite::Anchor};
|
|
|
|
#[derive(Component)]
|
|
struct SelectedEaseFunction(EaseFunction, Color);
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, display_curves)
|
|
.run();
|
|
}
|
|
|
|
fn setup(mut commands: Commands) {
|
|
commands.spawn(Camera2d);
|
|
|
|
let text_style = TextStyle {
|
|
font_size: 10.0,
|
|
..default()
|
|
};
|
|
|
|
for (i, functions) in [
|
|
EaseFunction::SineIn,
|
|
EaseFunction::SineOut,
|
|
EaseFunction::SineInOut,
|
|
EaseFunction::QuadraticIn,
|
|
EaseFunction::QuadraticOut,
|
|
EaseFunction::QuadraticInOut,
|
|
EaseFunction::CubicIn,
|
|
EaseFunction::CubicOut,
|
|
EaseFunction::CubicInOut,
|
|
EaseFunction::QuarticIn,
|
|
EaseFunction::QuarticOut,
|
|
EaseFunction::QuarticInOut,
|
|
EaseFunction::QuinticIn,
|
|
EaseFunction::QuinticOut,
|
|
EaseFunction::QuinticInOut,
|
|
EaseFunction::CircularIn,
|
|
EaseFunction::CircularOut,
|
|
EaseFunction::CircularInOut,
|
|
EaseFunction::ExponentialIn,
|
|
EaseFunction::ExponentialOut,
|
|
EaseFunction::ExponentialInOut,
|
|
EaseFunction::ElasticIn,
|
|
EaseFunction::ElasticOut,
|
|
EaseFunction::ElasticInOut,
|
|
EaseFunction::BackIn,
|
|
EaseFunction::BackOut,
|
|
EaseFunction::BackInOut,
|
|
EaseFunction::BounceIn,
|
|
EaseFunction::BounceOut,
|
|
EaseFunction::BounceInOut,
|
|
EaseFunction::Linear,
|
|
EaseFunction::Steps(4),
|
|
EaseFunction::Elastic(50.0),
|
|
]
|
|
.chunks(3)
|
|
.enumerate()
|
|
{
|
|
for (j, function) in functions.iter().enumerate() {
|
|
let color = Hsla::hsl(i as f32 / 11.0 * 360.0, 0.8, 0.75).into();
|
|
commands
|
|
.spawn((
|
|
Text2dBundle {
|
|
text: Text::from_section(
|
|
format!("{:?}", function),
|
|
TextStyle {
|
|
color,
|
|
..text_style.clone()
|
|
},
|
|
),
|
|
transform: Transform::from_xyz(
|
|
i as f32 * 113.0 - 1280.0 / 2.0 + 25.0,
|
|
-100.0 - ((j as f32 * 250.0) - 300.0),
|
|
0.0,
|
|
),
|
|
text_anchor: Anchor::TopLeft,
|
|
..default()
|
|
},
|
|
SelectedEaseFunction(*function, color),
|
|
))
|
|
.with_children(|p| {
|
|
p.spawn(SpriteBundle {
|
|
sprite: Sprite {
|
|
custom_size: Some(Vec2::new(5.0, 5.0)),
|
|
color,
|
|
..default()
|
|
},
|
|
transform: Transform::from_xyz(110.0, 15.0, 0.0),
|
|
..default()
|
|
});
|
|
p.spawn(SpriteBundle {
|
|
sprite: Sprite {
|
|
custom_size: Some(Vec2::new(4.0, 4.0)),
|
|
color,
|
|
..default()
|
|
},
|
|
transform: Transform::from_xyz(0.0, 0.0, 0.0),
|
|
..default()
|
|
});
|
|
});
|
|
}
|
|
}
|
|
commands.spawn(
|
|
TextBundle::from_section("", TextStyle::default()).with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
bottom: Val::Px(12.0),
|
|
left: Val::Px(12.0),
|
|
..default()
|
|
}),
|
|
);
|
|
}
|
|
|
|
fn display_curves(
|
|
mut gizmos: Gizmos,
|
|
ease_functions: Query<(&SelectedEaseFunction, &Transform, &Children)>,
|
|
mut transforms: Query<&mut Transform, Without<SelectedEaseFunction>>,
|
|
mut ui: Query<&mut Text, With<Node>>,
|
|
time: Res<Time>,
|
|
) {
|
|
let samples = 100;
|
|
let size = 95.0;
|
|
let duration = 2.5;
|
|
let time_margin = 0.5;
|
|
|
|
let now = ((time.elapsed_seconds() % (duration + time_margin * 2.0) - time_margin) / duration)
|
|
.clamp(0.0, 1.0);
|
|
|
|
ui.single_mut().sections[0].value = format!("Progress: {:.2}", now);
|
|
|
|
for (SelectedEaseFunction(function, color), transform, children) in &ease_functions {
|
|
// Draw a box around the curve
|
|
gizmos.linestrip_2d(
|
|
[
|
|
Vec2::new(transform.translation.x, transform.translation.y + 15.0),
|
|
Vec2::new(
|
|
transform.translation.x + size,
|
|
transform.translation.y + 15.0,
|
|
),
|
|
Vec2::new(
|
|
transform.translation.x + size,
|
|
transform.translation.y + 15.0 + size,
|
|
),
|
|
Vec2::new(
|
|
transform.translation.x,
|
|
transform.translation.y + 15.0 + size,
|
|
),
|
|
Vec2::new(transform.translation.x, transform.translation.y + 15.0),
|
|
],
|
|
color.darker(0.4),
|
|
);
|
|
|
|
// Draw the curve
|
|
let f = easing_curve(0.0, 1.0, *function);
|
|
let drawn_curve = f.by_ref().graph().map(|(x, y)| {
|
|
Vec2::new(
|
|
x * size + transform.translation.x,
|
|
y * size + transform.translation.y + 15.0,
|
|
)
|
|
});
|
|
gizmos.curve_2d(
|
|
&drawn_curve,
|
|
drawn_curve.domain().spaced_points(samples).unwrap(),
|
|
*color,
|
|
);
|
|
|
|
// Show progress along the curve for the current time
|
|
let y = f.sample(now).unwrap() * size + 15.0;
|
|
transforms.get_mut(children[0]).unwrap().translation.y = y;
|
|
transforms.get_mut(children[1]).unwrap().translation = Vec3::new(now * size, y, 0.0);
|
|
gizmos.linestrip_2d(
|
|
[
|
|
Vec2::new(transform.translation.x, transform.translation.y + y),
|
|
Vec2::new(transform.translation.x + size, transform.translation.y + y),
|
|
],
|
|
color.darker(0.2),
|
|
);
|
|
}
|
|
}
|