Easing functions (#4630)
This adds most of the "standard" easing functions from https://easings.net/ to `emath::easing`, and adds helpers in `egui` for using them. In particular there is now `ctx.animate_bool_with_easing` and `ctx.animate_bool_responsive`, that uses a cubic easing function. All animations in egui now uses cubic ease-out, for a more responsive feeling (fast at the start, slower towards the end).
This commit is contained in:
parent
3d632cd333
commit
29b12e1760
|
|
@ -519,6 +519,7 @@ impl Prepared {
|
|||
let age =
|
||||
ctx.input(|i| (i.time - self.state.last_became_visible_at) as f32 + i.predicted_dt);
|
||||
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
|
||||
let opacity = emath::easing::cubic_out(opacity); // slow fade-out = quick fade-in
|
||||
ui.multiply_opacity(opacity);
|
||||
if opacity < 1.0 {
|
||||
ctx.request_repaint();
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ impl CollapsingState {
|
|||
if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
1.0
|
||||
} else {
|
||||
ctx.animate_bool(self.id, self.state.open)
|
||||
ctx.animate_bool_responsive(self.id, self.state.open)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@
|
|||
|
||||
use crate::*;
|
||||
|
||||
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
|
||||
ctx.animate_bool_responsive(id, is_expanded)
|
||||
}
|
||||
|
||||
/// State regarding panels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
|
|
@ -387,7 +391,7 @@ impl SidePanel {
|
|||
is_expanded: bool,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let how_expanded = ctx.animate_bool(self.id.with("animation"), is_expanded);
|
||||
let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
None
|
||||
|
|
@ -420,9 +424,7 @@ impl SidePanel {
|
|||
is_expanded: bool,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let how_expanded = ui
|
||||
.ctx()
|
||||
.animate_bool(self.id.with("animation"), is_expanded);
|
||||
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
None
|
||||
|
|
@ -455,7 +457,7 @@ impl SidePanel {
|
|||
expanded_panel: Self,
|
||||
add_contents: impl FnOnce(&mut Ui, f32) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let how_expanded = ctx.animate_bool(expanded_panel.id.with("animation"), is_expanded);
|
||||
let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
|
||||
|
|
@ -487,9 +489,8 @@ impl SidePanel {
|
|||
expanded_panel: Self,
|
||||
add_contents: impl FnOnce(&mut Ui, f32) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let how_expanded = ui
|
||||
.ctx()
|
||||
.animate_bool(expanded_panel.id.with("animation"), is_expanded);
|
||||
let how_expanded =
|
||||
animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
|
||||
|
|
@ -875,7 +876,7 @@ impl TopBottomPanel {
|
|||
is_expanded: bool,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let how_expanded = ctx.animate_bool(self.id.with("animation"), is_expanded);
|
||||
let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
None
|
||||
|
|
@ -910,9 +911,7 @@ impl TopBottomPanel {
|
|||
is_expanded: bool,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let how_expanded = ui
|
||||
.ctx()
|
||||
.animate_bool(self.id.with("animation"), is_expanded);
|
||||
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
None
|
||||
|
|
@ -947,7 +946,7 @@ impl TopBottomPanel {
|
|||
expanded_panel: Self,
|
||||
add_contents: impl FnOnce(&mut Ui, f32) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let how_expanded = ctx.animate_bool(expanded_panel.id.with("animation"), is_expanded);
|
||||
let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
|
||||
|
|
@ -985,9 +984,8 @@ impl TopBottomPanel {
|
|||
expanded_panel: Self,
|
||||
add_contents: impl FnOnce(&mut Ui, f32) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let how_expanded = ui
|
||||
.ctx()
|
||||
.animate_bool(expanded_panel.id.with("animation"), is_expanded);
|
||||
let how_expanded =
|
||||
animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
|
||||
|
||||
if 0.0 == how_expanded {
|
||||
collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
|
||||
|
|
|
|||
|
|
@ -514,8 +514,8 @@ impl ScrollArea {
|
|||
};
|
||||
|
||||
let show_bars_factor = Vec2::new(
|
||||
ctx.animate_bool(id.with("h"), show_bars[0]),
|
||||
ctx.animate_bool(id.with("v"), show_bars[1]),
|
||||
ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
|
||||
ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
|
||||
);
|
||||
|
||||
let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
|
||||
|
|
@ -928,10 +928,10 @@ impl Prepared {
|
|||
|
||||
// Avoid frame delay; start showing scroll bar right away:
|
||||
if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
|
||||
show_bars_factor.x = ui.ctx().animate_bool(id.with("h"), true);
|
||||
show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
|
||||
}
|
||||
if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
|
||||
show_bars_factor.y = ui.ctx().animate_bool(id.with("v"), true);
|
||||
show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
|
||||
}
|
||||
|
||||
let scroll_style = ui.spacing().scroll;
|
||||
|
|
@ -970,7 +970,7 @@ impl Prepared {
|
|||
|| state.scroll_bar_interaction[d];
|
||||
let is_hovering_bar_area_t = ui
|
||||
.ctx()
|
||||
.animate_bool(id.with((d, "bar_hover")), is_hovering_bar_area);
|
||||
.animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
|
||||
let width = show_factor
|
||||
* lerp(
|
||||
scroll_style.floating_width..=scroll_style.bar_width,
|
||||
|
|
@ -1125,7 +1125,7 @@ impl Prepared {
|
|||
if response.hovered() || response.dragged() {
|
||||
scroll_style.interact_handle_opacity
|
||||
} else {
|
||||
let is_hovering_outer_rect_t = ui.ctx().animate_bool(
|
||||
let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
|
||||
id.with((d, "is_hovering_outer_rect")),
|
||||
is_hovering_outer_rect,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -438,7 +438,11 @@ impl<'open> Window<'open> {
|
|||
|
||||
let is_explicitly_closed = matches!(open, Some(false));
|
||||
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
|
||||
let opacity = ctx.animate_bool(area.id.with("fade-out"), is_open);
|
||||
let opacity = ctx.animate_bool_with_easing(
|
||||
area.id.with("fade-out"),
|
||||
is_open,
|
||||
emath::easing::cubic_out,
|
||||
);
|
||||
if opacity <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2442,12 +2442,51 @@ impl Context {
|
|||
#[track_caller] // To track repaint cause
|
||||
pub fn animate_bool(&self, id: Id, value: bool) -> f32 {
|
||||
let animation_time = self.style().animation_time;
|
||||
self.animate_bool_with_time(id, value, animation_time)
|
||||
self.animate_bool_with_time_and_easing(id, value, animation_time, emath::easing::linear)
|
||||
}
|
||||
|
||||
/// Like [`Self::animate_bool`], but uses an easing function that makes the value move
|
||||
/// quickly in the beginning and slow down towards the end.
|
||||
///
|
||||
/// The exact easing function may come to change in future versions of egui.
|
||||
#[track_caller] // To track repaint cause
|
||||
pub fn animate_bool_responsive(&self, id: Id, value: bool) -> f32 {
|
||||
self.animate_bool_with_easing(id, value, emath::easing::cubic_out)
|
||||
}
|
||||
|
||||
/// Like [`Self::animate_bool`] but allows you to control the easing function.
|
||||
#[track_caller] // To track repaint cause
|
||||
pub fn animate_bool_with_easing(&self, id: Id, value: bool, easing: fn(f32) -> f32) -> f32 {
|
||||
let animation_time = self.style().animation_time;
|
||||
self.animate_bool_with_time_and_easing(id, value, animation_time, easing)
|
||||
}
|
||||
|
||||
/// Like [`Self::animate_bool`] but allows you to control the animation time.
|
||||
#[track_caller] // To track repaint cause
|
||||
pub fn animate_bool_with_time(&self, id: Id, target_value: bool, animation_time: f32) -> f32 {
|
||||
self.animate_bool_with_time_and_easing(
|
||||
id,
|
||||
target_value,
|
||||
animation_time,
|
||||
emath::easing::linear,
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`Self::animate_bool`] but allows you to control the animation time and easing function.
|
||||
///
|
||||
/// Use e.g. [`emath::easing::quadratic_out`]
|
||||
/// for a responsive start and a slow end.
|
||||
///
|
||||
/// The easing function flips when `target_value` is `false`,
|
||||
/// so that when going back towards 0.0, we get
|
||||
#[track_caller] // To track repaint cause
|
||||
pub fn animate_bool_with_time_and_easing(
|
||||
&self,
|
||||
id: Id,
|
||||
target_value: bool,
|
||||
animation_time: f32,
|
||||
easing: fn(f32) -> f32,
|
||||
) -> f32 {
|
||||
let animated_value = self.write(|ctx| {
|
||||
ctx.animation_manager.animate_bool(
|
||||
&ctx.viewports.entry(ctx.viewport_id()).or_default().input,
|
||||
|
|
@ -2456,11 +2495,17 @@ impl Context {
|
|||
target_value,
|
||||
)
|
||||
});
|
||||
|
||||
let animation_in_progress = 0.0 < animated_value && animated_value < 1.0;
|
||||
if animation_in_progress {
|
||||
self.request_repaint();
|
||||
}
|
||||
animated_value
|
||||
|
||||
if target_value {
|
||||
easing(animated_value)
|
||||
} else {
|
||||
1.0 - easing(1.0 - animated_value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Smoothly animate an `f32` value.
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
|||
// Let's ask for a simple animation from egui.
|
||||
// egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let how_on = ui.ctx().animate_bool_responsive(response.id, *on);
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
|
|
@ -80,7 +80,7 @@ fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
|||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let how_on = ui.ctx().animate_bool_responsive(response.id, *on);
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
//! Easing functions for animations.
|
||||
//!
|
||||
//! Contains most easing functions from <https://easings.net/>.
|
||||
//!
|
||||
//! All functions take a value in `[0, 1]` and return a value in `[0, 1]`.
|
||||
//!
|
||||
//! Derived from <https://github.com/warrenm/AHEasing/blob/master/AHEasing/easing.c>.
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[inline]
|
||||
fn powf(base: f32, exp: f32) -> f32 {
|
||||
base.powf(exp)
|
||||
}
|
||||
|
||||
/// No easing, just `y = x`
|
||||
#[inline]
|
||||
pub fn linear(t: f32) -> f32 {
|
||||
t
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInQuad>
|
||||
///
|
||||
/// Modeled after the parabola `y = x^2`
|
||||
#[inline]
|
||||
pub fn quadratic_in(t: f32) -> f32 {
|
||||
t * t
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOutQuad>
|
||||
///
|
||||
/// Same as `1.0 - quadratic_in(1.0 - t)`.
|
||||
#[inline]
|
||||
pub fn quadratic_out(t: f32) -> f32 {
|
||||
-(t * (t - 2.))
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutQuad>
|
||||
#[inline]
|
||||
pub fn quadratic_in_out(t: f32) -> f32 {
|
||||
if t < 0.5 {
|
||||
2. * t * t
|
||||
} else {
|
||||
(-2. * t * t) + (4. * t) - 1.
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInCubic>
|
||||
///
|
||||
/// Modeled after the parabola `y = x^3`
|
||||
#[inline]
|
||||
pub fn cubic_in(t: f32) -> f32 {
|
||||
t * t * t
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOutCubic>
|
||||
#[inline]
|
||||
pub fn cubic_out(t: f32) -> f32 {
|
||||
let f = t - 1.;
|
||||
f * f * f + 1.
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutCubic>
|
||||
#[inline]
|
||||
pub fn cubic_in_out(t: f32) -> f32 {
|
||||
if t < 0.5 {
|
||||
4. * t * t * t
|
||||
} else {
|
||||
let f = (2. * t) - 2.;
|
||||
0.5 * f * f * f + 1.
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInSine>
|
||||
///
|
||||
/// Modeled after quarter-cycle of sine wave
|
||||
#[inline]
|
||||
pub fn sin_in(t: f32) -> f32 {
|
||||
((t - 1.) * 2. * PI).sin() + 1.
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOuSine>
|
||||
///
|
||||
/// Modeled after quarter-cycle of sine wave (different phase)
|
||||
#[inline]
|
||||
pub fn sin_out(t: f32) -> f32 {
|
||||
(t * 2. * PI).sin()
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutSine>
|
||||
///
|
||||
/// Modeled after half sine wave
|
||||
#[inline]
|
||||
pub fn sin_in_out(t: f32) -> f32 {
|
||||
0.5 * (1. - (t * PI).cos())
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInCirc>
|
||||
///
|
||||
/// Modeled after shifted quadrant IV of unit circle
|
||||
#[inline]
|
||||
pub fn circular_in(t: f32) -> f32 {
|
||||
1. - (1. - t * t).sqrt()
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOutCirc>
|
||||
///
|
||||
/// Modeled after shifted quadrant II of unit circle
|
||||
#[inline]
|
||||
pub fn circular_out(t: f32) -> f32 {
|
||||
(2. - t).sqrt() * t
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutCirc>
|
||||
#[inline]
|
||||
pub fn circular_in_out(t: f32) -> f32 {
|
||||
if t < 0.5 {
|
||||
0.5 * (1. - (1. - 4. * t * t).sqrt())
|
||||
} else {
|
||||
0.5 * ((-(2. * t - 3.) * (2. * t - 1.)).sqrt() + 1.)
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInExpo>
|
||||
///
|
||||
/// There is a small discontinuity at 0.
|
||||
#[inline]
|
||||
pub fn exponential_in(t: f32) -> f32 {
|
||||
if t == 0. {
|
||||
t
|
||||
} else {
|
||||
powf(2.0, 10. * (t - 1.))
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOutExpo>
|
||||
///
|
||||
/// There is a small discontinuity at 1.
|
||||
#[inline]
|
||||
pub fn exponential_out(t: f32) -> f32 {
|
||||
if t == 1. {
|
||||
t
|
||||
} else {
|
||||
1. - powf(2.0, -10. * t)
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutExpo>
|
||||
///
|
||||
/// There is a small discontinuity at 0 and 1.
|
||||
#[inline]
|
||||
pub fn exponential_in_out(t: f32) -> f32 {
|
||||
if t == 0. || t == 1. {
|
||||
t
|
||||
} else if t < 0.5 {
|
||||
0.5 * powf(2.0, 20. * t - 10.)
|
||||
} else {
|
||||
0.5 * powf(2.0, -20. * t + 10.) + 1.
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInBack>
|
||||
#[inline]
|
||||
pub fn back_in(t: f32) -> f32 {
|
||||
t * t * t - t * (t * PI).sin()
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOutBack>
|
||||
#[inline]
|
||||
pub fn back_out(t: f32) -> f32 {
|
||||
let f = 1. - t;
|
||||
1. - (f * f * f - f * (f * PI).sin())
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutBack>
|
||||
#[inline]
|
||||
pub fn back_in_out(t: f32) -> f32 {
|
||||
if t < 0.5 {
|
||||
let f = 2. * t;
|
||||
0.5 * (f * f * f - f * (f * PI).sin())
|
||||
} else {
|
||||
let f = 1. - (2. * t - 1.);
|
||||
0.5 * (1. - (f * f * f - f * (f * PI).sin())) + 0.5
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInBounce>
|
||||
///
|
||||
/// Each bounce is modelled as a parabola.
|
||||
#[inline]
|
||||
pub fn bounce_in(t: f32) -> f32 {
|
||||
1. - bounce_out(1. - t)
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeOutBounce>
|
||||
///
|
||||
/// Each bounce is modelled as a parabola.
|
||||
#[inline]
|
||||
pub fn bounce_out(t: f32) -> f32 {
|
||||
if t < 4. / 11. {
|
||||
const T2: f32 = 121. / 16.;
|
||||
T2 * t * t
|
||||
} else if t < 8. / 11. {
|
||||
const T2: f32 = 363. / 40.;
|
||||
const T1: f32 = -99. / 10.;
|
||||
const T0: f32 = 17. / 5.;
|
||||
T2 * t * t + T1 * t + T0
|
||||
} else if t < 9. / 10. {
|
||||
const T2: f32 = 4356. / 361.;
|
||||
const T1: f32 = -35442. / 1805.;
|
||||
const T0: f32 = 16061. / 1805.;
|
||||
T2 * t * t + T1 * t + T0
|
||||
} else {
|
||||
const T2: f32 = 54. / 5.;
|
||||
const T1: f32 = -513. / 25.;
|
||||
const T0: f32 = 268. / 25.;
|
||||
T2 * t * t + T1 * t + T0
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://easings.net/#easeInOutBounce>
|
||||
///
|
||||
/// Each bounce is modelled as a parabola.
|
||||
#[inline]
|
||||
pub fn bounce_in_out(t: f32) -> f32 {
|
||||
if t < 0.5 {
|
||||
0.5 * bounce_in(t * 2.)
|
||||
} else {
|
||||
0.5 * bounce_out(t * 2. - 1.) + 0.5
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
|
|||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub mod align;
|
||||
pub mod easing;
|
||||
mod history;
|
||||
mod numeric;
|
||||
mod ordered_float;
|
||||
|
|
|
|||
Loading…
Reference in New Issue