Skip to content

Commit

Permalink
Add ways to configure EasingFunction::Steps via new StepConfig (#…
Browse files Browse the repository at this point in the history
…17752)

# Objective

- In #17743, attention was raised to the fact that we supported an
unusual kind of step easing function. The author of the fix kindly
provided some links to standards used in CSS. It would be desirable to
support generally agreed upon standards so this PR here tries to
implement an extra configuration option of the step easing function
- Resolve #17744

## Solution

- Introduce `StepConfig`
- `StepConfig` can configure both the number of steps and the jumping
behavior of the function
- `StepConfig` replaces the raw `usize` parameter of the
`EasingFunction::Steps(usize)` construct.
- `StepConfig`s default jumping behavior is `end`, so in that way it
follows #17743

## Testing

- I added a new test per `JumpAt` jumping behavior. These tests
replicate the visuals that can be found at
https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/steps#description

## Migration Guide

- `EasingFunction::Steps` now uses a `StepConfig` instead of a raw
`usize`. You can replicate the previous behavior by replaceing
`EasingFunction::Steps(10)` with
`EasingFunction::Steps(StepConfig::new(10))`.

---------

Co-authored-by: François Mockers <[email protected]>
Co-authored-by: Alice Cecile <[email protected]>
  • Loading branch information
3 people authored Feb 11, 2025
1 parent 98dcee2 commit aa8793f
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 18 deletions.
5 changes: 5 additions & 0 deletions crates/bevy_math/images/easefunction/BothSteps.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions crates/bevy_math/images/easefunction/EndSteps.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions crates/bevy_math/images/easefunction/NoneSteps.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions crates/bevy_math/images/easefunction/StartSteps.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions crates/bevy_math/images/easefunction/Steps.svg

This file was deleted.

151 changes: 144 additions & 7 deletions crates/bevy_math/src/curve/easing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,52 @@ where
}
}

/// Configuration options for the [`EaseFunction::Steps`] curves. This closely replicates the
/// [CSS step function specification].
///
/// [CSS step function specification]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/steps#description
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub enum JumpAt {
/// Indicates that the first step happens when the animation begins.
///
#[doc = include_str!("../../images/easefunction/StartSteps.svg")]
Start,
/// Indicates that the last step happens when the animation ends.
///
#[doc = include_str!("../../images/easefunction/EndSteps.svg")]
#[default]
End,
/// Indicates neither early nor late jumps happen.
///
#[doc = include_str!("../../images/easefunction/NoneSteps.svg")]
None,
/// Indicates both early and late jumps happen.
///
#[doc = include_str!("../../images/easefunction/BothSteps.svg")]
Both,
}

impl JumpAt {
#[inline]
pub(crate) fn eval(self, num_steps: usize, t: f32) -> f32 {
use crate::ops;

let (a, b) = match self {
JumpAt::Start => (1.0, 0),
JumpAt::End => (0.0, 0),
JumpAt::None => (0.0, -1),
JumpAt::Both => (1.0, 1),
};

let current_step = ops::floor(t * num_steps as f32) + a;
let step_size = (num_steps as isize + b).max(1) as f32;

(current_step / step_size).clamp(0.0, 1.0)
}
}

/// Curve functions over the [unit interval], commonly used for easing transitions.
///
/// `EaseFunction` can be used on its own to interpolate between `0.0` and `1.0`.
Expand Down Expand Up @@ -538,10 +584,9 @@ pub enum EaseFunction {
#[doc = include_str!("../../images/easefunction/BounceInOut.svg")]
BounceInOut,

/// `n` steps connecting the start and the end
///
#[doc = include_str!("../../images/easefunction/Steps.svg")]
Steps(usize),
/// `n` steps connecting the start and the end. Jumping behavior is customizable via
/// [`JumpAt`]. See [`JumpAt`] for all the options and visual examples.
Steps(usize, JumpAt),

/// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega`
///
Expand Down Expand Up @@ -794,8 +839,8 @@ mod easing_functions {
}

#[inline]
pub(crate) fn steps(num_steps: usize, t: f32) -> f32 {
ops::floor(t * num_steps as f32) / num_steps.max(1) as f32
pub(crate) fn steps(num_steps: usize, jump_at: super::JumpAt, t: f32) -> f32 {
jump_at.eval(num_steps, t)
}

#[inline]
Expand Down Expand Up @@ -844,7 +889,9 @@ impl EaseFunction {
EaseFunction::BounceIn => easing_functions::bounce_in(t),
EaseFunction::BounceOut => easing_functions::bounce_out(t),
EaseFunction::BounceInOut => easing_functions::bounce_in_out(t),
EaseFunction::Steps(num_steps) => easing_functions::steps(*num_steps, t),
EaseFunction::Steps(num_steps, jump_at) => {
easing_functions::steps(*num_steps, *jump_at, t)
}
EaseFunction::Elastic(omega) => easing_functions::elastic(*omega, t),
}
}
Expand All @@ -865,6 +912,7 @@ impl Curve<f32> for EaseFunction {
#[cfg(test)]
#[cfg(feature = "approx")]
mod tests {

use crate::{Vec2, Vec3, Vec3A};
use approx::assert_abs_diff_eq;

Expand Down Expand Up @@ -1027,6 +1075,95 @@ mod tests {
});
}

#[test]
fn jump_at_start() {
let jump_at = JumpAt::Start;
let num_steps = 4;

[
(0.0, 0.25),
(0.249, 0.25),
(0.25, 0.5),
(0.499, 0.5),
(0.5, 0.75),
(0.749, 0.75),
(0.75, 1.0),
(1.0, 1.0),
]
.into_iter()
.for_each(|(t, expected)| {
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
});
}

#[test]
fn jump_at_end() {
let jump_at = JumpAt::End;
let num_steps = 4;

[
(0.0, 0.0),
(0.249, 0.0),
(0.25, 0.25),
(0.499, 0.25),
(0.5, 0.5),
(0.749, 0.5),
(0.75, 0.75),
(0.999, 0.75),
(1.0, 1.0),
]
.into_iter()
.for_each(|(t, expected)| {
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
});
}

#[test]
fn jump_at_none() {
let jump_at = JumpAt::None;
let num_steps = 5;

[
(0.0, 0.0),
(0.199, 0.0),
(0.2, 0.25),
(0.399, 0.25),
(0.4, 0.5),
(0.599, 0.5),
(0.6, 0.75),
(0.799, 0.75),
(0.8, 1.0),
(0.999, 1.0),
(1.0, 1.0),
]
.into_iter()
.for_each(|(t, expected)| {
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
});
}

#[test]
fn jump_at_both() {
let jump_at = JumpAt::Both;
let num_steps = 4;

[
(0.0, 0.2),
(0.249, 0.2),
(0.25, 0.4),
(0.499, 0.4),
(0.5, 0.6),
(0.749, 0.6),
(0.75, 0.8),
(0.999, 0.8),
(1.0, 1.0),
]
.into_iter()
.for_each(|(t, expected)| {
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
});
}

#[test]
fn ease_function_curve() {
// Test that using `EaseFunction` directly is equivalent to `EasingCurve::new(0.0, 1.0, ...)`.
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_math/src/curve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1061,7 +1061,7 @@ mod tests {
let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0);

let curve = EasingCurve::new(start, end, EaseFunction::Steps(4));
let curve = EasingCurve::new(start, end, EaseFunction::Steps(4, JumpAt::End));
[
(0.0, start),
(0.249, start),
Expand Down
5 changes: 4 additions & 1 deletion examples/animation/easing_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ fn setup(mut commands: Commands) {
EaseFunction::BounceInOut,
// "Other" row
EaseFunction::Linear,
EaseFunction::Steps(4),
EaseFunction::Steps(4, JumpAt::End),
EaseFunction::Steps(4, JumpAt::Start),
EaseFunction::Steps(4, JumpAt::Both),
EaseFunction::Steps(4, JumpAt::None),
EaseFunction::Elastic(50.0),
]
.chunks(COLS);
Expand Down
16 changes: 12 additions & 4 deletions tools/build-easefunction-graphs/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Generates graphs for the `EaseFunction` docs.
use std::path::PathBuf;

use bevy_math::curve::{CurveExt, EaseFunction, EasingCurve};
use bevy_math::curve::{CurveExt, EaseFunction, EasingCurve, JumpAt};
use svg::{
node::element::{self, path::Data},
Document,
Expand Down Expand Up @@ -55,7 +55,10 @@ fn main() {
EaseFunction::BounceOut,
EaseFunction::BounceInOut,
EaseFunction::Linear,
EaseFunction::Steps(4),
EaseFunction::Steps(4, JumpAt::Start),
EaseFunction::Steps(4, JumpAt::End),
EaseFunction::Steps(4, JumpAt::None),
EaseFunction::Steps(4, JumpAt::Both),
EaseFunction::Elastic(50.0),
] {
let curve = EasingCurve::new(0.0, 1.0, function);
Expand All @@ -71,7 +74,7 @@ fn main() {

// Curve can go out past endpoints
let mut min = 0.0f32;
let mut max = 0.0f32;
let mut max = 1.0f32;
for &(_, y) in &samples {
min = min.min(y);
max = max.max(y);
Expand Down Expand Up @@ -104,7 +107,12 @@ fn main() {
data
});

let name = format!("{function:?}");
let opt_tag = match function {
EaseFunction::Steps(_n, jump_at) => format!("{jump_at:?}"),
_ => String::new(),
};

let name = format!("{opt_tag}{function:?}");
let tooltip = element::Title::new(&name);

const MARGIN: f32 = 0.04;
Expand Down

0 comments on commit aa8793f

Please sign in to comment.