Easing functions as styles

This commit is contained in:
Jonathan Johnson 2023-11-03 13:37:27 -07:00
parent 1bf1b082af
commit 6f5ffd80b4
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
6 changed files with 189 additions and 59 deletions

View file

@ -17,7 +17,7 @@
//! ```rust //! ```rust
//! use std::time::Duration; //! use std::time::Duration;
//! //!
//! use gooey::animation::{AnimationTarget, Spawn}; //! use gooey::animation::{AnimationTarget, EaseInOutElastic, Spawn};
//! use gooey::value::Dynamic; //! use gooey::value::Dynamic;
//! //!
//! let value = Dynamic::new(0); //! let value = Dynamic::new(0);
@ -25,6 +25,7 @@
//! value //! value
//! .transition_to(100) //! .transition_to(100)
//! .over(Duration::from_millis(100)) //! .over(Duration::from_millis(100))
//! .with_easing(EaseInOutElastic)
//! .launch(); //! .launch();
//! //!
//! let mut reader = value.into_reader(); //! let mut reader = value.into_reader();
@ -37,9 +38,9 @@
use std::f32::consts::PI; use std::f32::consts::PI;
use std::fmt::Debug; use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::{ControlFlow, Deref}; use std::ops::{ControlFlow, Deref};
use std::sync::{Condvar, Mutex, MutexGuard, OnceLock, PoisonError}; use std::panic::{RefUnwindSafe, UnwindSafe};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -47,6 +48,7 @@ use alot::{LotId, Lots};
use kempt::Set; use kempt::Set;
use kludgine::Color; use kludgine::Color;
use crate::styles::Component;
use crate::value::Dynamic; use crate::value::Dynamic;
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new()); static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
@ -224,7 +226,7 @@ where
{ {
value: Target, value: Target,
duration: Duration, duration: Duration,
_easing: PhantomData<Easing>, easing: Easing,
} }
impl<T> Animation<T, Linear> impl<T> Animation<T, Linear>
@ -235,16 +237,16 @@ where
Self { Self {
value, value,
duration, duration,
_easing: PhantomData, easing: Linear,
} }
} }
/// Returns this animation with a different easing function. /// Returns this animation with a different easing function.
pub fn with_easing<Easing: self::Easing>(self) -> Animation<T, Easing> { pub fn with_easing<Easing: self::Easing>(self, easing: Easing) -> Animation<T, Easing> {
Animation { Animation {
value: self.value, value: self.value,
duration: self.duration, duration: self.duration,
_easing: PhantomData, easing,
} }
} }
} }
@ -261,7 +263,7 @@ where
target: self.value.begin(), target: self.value.begin(),
duration: self.duration, duration: self.duration,
elapsed: Duration::ZERO, elapsed: Duration::ZERO,
_easing: PhantomData, easing: self.easing,
} }
} }
} }
@ -401,7 +403,7 @@ where
self.target.finish(); self.target.finish();
ControlFlow::Break(remaining_elapsed) ControlFlow::Break(remaining_elapsed)
} else { } else {
let progress = Easing::ease(ZeroToOne::new( let progress = self.easing.ease(ZeroToOne::new(
self.elapsed.as_secs_f32() / self.duration.as_secs_f32(), self.elapsed.as_secs_f32() / self.duration.as_secs_f32(),
)); ));
self.target.update(progress); self.target.update(progress);
@ -421,7 +423,7 @@ pub struct RunningAnimation<T, Easing> {
target: T, target: T,
duration: Duration, duration: Duration,
elapsed: Duration, elapsed: Duration,
_easing: PhantomData<Easing>, easing: Easing,
} }
/// A handle to a spawned animation. When dropped, the associated animation will /// A handle to a spawned animation. When dropped, the associated animation will
@ -653,6 +655,20 @@ impl_lerp_for_int!(i64, u64, f32);
impl_lerp_for_int!(i128, u128, f64); impl_lerp_for_int!(i128, u128, f64);
impl_lerp_for_int!(isize, usize, f64); impl_lerp_for_int!(isize, usize, f64);
impl LinearInterpolate for f32 {
fn lerp(&self, target: &Self, percent: f32) -> Self {
let delta = *target - *self;
*self + delta * percent
}
}
impl LinearInterpolate for f64 {
fn lerp(&self, target: &Self, percent: f32) -> Self {
let delta = *target - *self;
*self + delta * f64::from(percent)
}
}
#[test] #[test]
fn integer_lerps() { fn integer_lerps() {
#[track_caller] #[track_caller]
@ -768,21 +784,57 @@ impl LinearInterpolate for ZeroToOne {
} }
} }
/// Performs easing for value interpolation. /// An easing function for customizing animations.
pub trait Easing: Send + Sync + 'static { #[derive(Debug, Clone)]
/// Eases a value ranging between zero and one. The resulting value does not pub enum EasingFunction {
/// need to be bounded between zero and one. /// A function pointer to use as an easing function.
fn ease(progress: ZeroToOne) -> f32; Fn(fn(ZeroToOne) -> f32),
/// A custom easing implementation.
Custom(Arc<dyn Easing>),
} }
// /// An [`Easing`] function that produces a steady, linear transition. impl Easing for EasingFunction {
// pub enum Linear {} fn ease(&self, progress: ZeroToOne) -> f32 {
match self {
EasingFunction::Fn(func) => func(progress),
EasingFunction::Custom(func) => func.ease(progress),
}
}
}
// impl Easing for Linear { impl From<EasingFunction> for Component {
// fn ease(progress: ZeroToOne) -> f32 { fn from(value: EasingFunction) -> Self {
// *progress Component::Easing(value)
// } }
// } }
impl TryFrom<Component> for EasingFunction {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Easing(easing) => Ok(easing),
other => Err(other),
}
}
}
/// Performs easing for value interpolation.
pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static {
/// Eases a value ranging between zero and one. The resulting value does not
/// need to be bounded between zero and one.
fn ease(&self, progress: ZeroToOne) -> f32;
}
/// An [`Easing`] function that produces a steady, linear transition.
#[derive(Clone, Copy, Debug)]
pub struct Linear;
impl Easing for Linear {
fn ease(&self, progress: ZeroToOne) -> f32 {
*progress
}
}
// Think this macro has a long name? This is to ensure rustfmt wraps the closure // Think this macro has a long name? This is to ensure rustfmt wraps the closure
// onto its own line. Seriously, try shortening the name and see how it changes // onto its own line. Seriously, try shortening the name and see how it changes
@ -793,14 +845,31 @@ macro_rules! declare_easing_function_implementation {
/// An [`Easing`] function that eases /// An [`Easing`] function that eases
#[doc = $description] #[doc = $description]
#[doc = concat!(".\n\nSee <https://easings.net/#", stringify!($anchor_name), "> for a visualization and more information.")] #[doc = concat!(".\n\nSee <https://easings.net/#", stringify!($anchor_name), "> for a visualization and more information.")]
pub enum $name {} #[derive(Clone, Copy, Debug)]
pub struct $name;
impl Easing for $name { impl $name {
fn ease(progress: ZeroToOne) -> f32 { /// Eases
#[doc = $description]
#[doc = concat!(".\n\nSee <https://easings.net/#", stringify!($anchor_name), "> for a visualization and more information.")]
#[must_use]
pub fn ease(progress: ZeroToOne) -> f32 {
let closure = force_closure_type($closure); let closure = force_closure_type($closure);
closure(*progress) closure(*progress)
} }
} }
impl Easing for $name {
fn ease(&self, progress: ZeroToOne) -> f32 {
Self::ease(progress)
}
}
impl From<$name> for EasingFunction {
fn from(_function: $name) -> Self {
Self::Fn($name::ease)
}
}
}; };
} }
@ -809,22 +878,6 @@ fn force_closure_type(f: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
f f
} }
/// An [`Easing`] function that produces a steady, linear transition.
pub enum Linear {}
impl Easing for Linear {
fn ease(progress: ZeroToOne) -> f32 {
*progress
}
}
declare_easing_function_implementation!(
EaseInSine,
easeInSine,
"in using a sine wave",
|percent| 1. - (percent * PI).cos() / 2.
);
declare_easing_function_implementation!( declare_easing_function_implementation!(
EaseOutSine, EaseOutSine,
easeOutSine, easeOutSine,
@ -1088,7 +1141,7 @@ declare_easing_function_implementation!(
EaseInBounce, EaseInBounce,
easeInBounce, easeInBounce,
"in using a curve that bounces progressively closer as it progresses", "in using a curve that bounces progressively closer as it progresses",
|percent| 1. - EaseOutBounce::ease(ZeroToOne(percent)) |percent| 1. - EaseOutBounce.ease(ZeroToOne(percent))
); );
declare_easing_function_implementation!( declare_easing_function_implementation!(

View file

@ -5,6 +5,7 @@ use std::collections::{hash_map, HashMap};
use std::ops::Add; use std::ops::Add;
use std::sync::Arc; use std::sync::Arc;
use crate::animation::{EasingFunction, ZeroToOne};
use crate::names::Name; use crate::names::Name;
use crate::utils::Lazy; use crate::utils::Lazy;
use crate::value::{IntoValue, Value}; use crate::value::{IntoValue, Value};
@ -145,9 +146,11 @@ pub enum Component {
/// A single-dimension measurement. /// A single-dimension measurement.
Dimension(Dimension), Dimension(Dimension),
/// A percentage between 0.0 and 1.0. /// A percentage between 0.0 and 1.0.
Percent(f32), Percent(ZeroToOne),
/// A custom component type. /// A custom component type.
Custom(CustomComponent), Custom(CustomComponent),
/// An easing function for animations.
Easing(EasingFunction),
} }
impl From<Color> for Component { impl From<Color> for Component {

View file

@ -4,6 +4,7 @@ use std::borrow::Cow;
use kludgine::figures::units::Lp; use kludgine::figures::units::Lp;
use kludgine::Color; use kludgine::Color;
use crate::animation::{EaseInQuadradic, EaseOutQuadradic, EasingFunction};
use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent}; use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent};
/// The [`Dimension`] to use as the size to render text. /// The [`Dimension`] to use as the size to render text.
@ -97,3 +98,64 @@ impl ComponentDefinition for IntrinsicPadding {
Dimension::Lp(Lp::points(5)) Dimension::Lp(Lp::points(5))
} }
} }
/// The [`EasingFunction`] to apply to animations that have no inherent
/// directionality.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct Easing;
impl NamedComponent for Easing {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("easing"))
}
}
impl ComponentDefinition for Easing {
type ComponentType = EasingFunction;
fn default_value(&self) -> Self::ComponentType {
EasingFunction::from(EaseInQuadradic)
}
}
/// The [`EasingFunction`] to apply to animations that transition a value from
/// "nothing" to "something". For example, if an widget is animating a color's
/// alpha channel towards opaqueness, it would query for this style component.
/// Otherwise, it would use [`EasingOut`].
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct EasingIn;
impl NamedComponent for EasingIn {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("easing_in"))
}
}
impl ComponentDefinition for EasingIn {
type ComponentType = EasingFunction;
fn default_value(&self) -> Self::ComponentType {
EasingFunction::from(EaseInQuadradic)
}
}
/// The [`EasingFunction`] to apply to animations that transition a value from
/// "something" to "nothing". For example, if an widget is animating a color's
/// alpha channel towards transparency, it would query for this style component.
/// Otherwise, it would use [`EasingIn`].
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct EasingOut;
impl NamedComponent for EasingOut {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("easing_out"))
}
}
impl ComponentDefinition for EasingOut {
type ComponentType = EasingFunction;
fn default_value(&self) -> Self::ComponentType {
EasingFunction::from(EaseOutQuadradic)
}
}

View file

@ -52,7 +52,7 @@ impl Align {
let (left, right, width) = horizontal.measure(available_space.width, content_size.width); let (left, right, width) = horizontal.measure(available_space.width, content_size.width);
let (top, bottom, height) = vertical.measure(available_space.height, content_size.height); let (top, bottom, height) = vertical.measure(available_space.height, content_size.height);
dbg!(Layout { Layout {
margin: Edges { margin: Edges {
left, left,
right, right,
@ -60,7 +60,7 @@ impl Align {
bottom, bottom,
}, },
content: Size::new(width, height), content: Size::new(width, height),
}) }
} }
} }

View file

@ -14,7 +14,7 @@ use kludgine::Color;
use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
use crate::context::{EventContext, GraphicsContext, WidgetContext}; use crate::context::{EventContext, GraphicsContext, WidgetContext};
use crate::names::Name; use crate::names::Name;
use crate::styles::components::{HighlightColor, IntrinsicPadding, TextColor}; use crate::styles::components::{Easing, HighlightColor, IntrinsicPadding, TextColor};
use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent};
use crate::value::{Dynamic, IntoValue, Value}; use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
@ -66,6 +66,7 @@ impl Button {
&ButtonActiveBackground, &ButtonActiveBackground,
&ButtonBackground, &ButtonBackground,
&ButtonHoverBackground, &ButtonHoverBackground,
&Easing,
]); ]);
let background_color = if context.active() { let background_color = if context.active() {
styles.get_or_default(&ButtonActiveBackground) styles.get_or_default(&ButtonActiveBackground)
@ -80,6 +81,7 @@ impl Button {
self.background_color_animation = dynamic self.background_color_animation = dynamic
.transition_to(background_color) .transition_to(background_color)
.over(Duration::from_millis(150)) .over(Duration::from_millis(150))
.with_easing(styles.get_or_default(&Easing))
.spawn(); .spawn();
} }
(true, Some(dynamic)) => { (true, Some(dynamic)) => {

View file

@ -13,6 +13,7 @@ use kludgine::Color;
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
use crate::context::{AsEventContext, EventContext}; use crate::context::{AsEventContext, EventContext};
use crate::styles::components::{EasingIn, EasingOut};
use crate::styles::{ use crate::styles::{
ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent, ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent,
}; };
@ -95,6 +96,23 @@ impl Scroll {
self.scroll.set(clamped); self.scroll.set(clamped);
} }
} }
fn show_scrollbars(&mut self, context: &mut EventContext<'_, '_>) {
let styles = context.query_styles(&[&EasingIn, &EasingOut]);
self.scrollbar_opacity_animation = self
.scrollbar_opacity
.transition_to(ZeroToOne::ONE)
.over(Duration::from_millis(300))
.with_easing(styles.get_or_default(&EasingIn))
.and_then(Duration::from_secs(1))
.and_then(
self.scrollbar_opacity
.transition_to(ZeroToOne::ZERO)
.over(Duration::from_millis(300))
.with_easing(styles.get_or_default(&EasingOut)),
)
.spawn();
}
} }
impl Widget for Scroll { impl Widget for Scroll {
@ -102,25 +120,16 @@ impl Widget for Scroll {
true true
} }
fn hover(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) { fn hover(&mut self, _location: Point<Px>, context: &mut EventContext<'_, '_>) {
self.scrollbar_opacity_animation = self self.show_scrollbars(context);
.scrollbar_opacity
.transition_to(ZeroToOne::ONE)
.over(Duration::from_millis(300))
.and_then(Duration::from_secs(1))
.and_then(
self.scrollbar_opacity
.transition_to(ZeroToOne::ZERO)
.over(Duration::from_millis(300)),
)
.spawn();
} }
fn unhover(&mut self, _context: &mut EventContext<'_, '_>) { fn unhover(&mut self, context: &mut EventContext<'_, '_>) {
self.scrollbar_opacity_animation = self self.scrollbar_opacity_animation = self
.scrollbar_opacity .scrollbar_opacity
.transition_to(ZeroToOne::ZERO) .transition_to(ZeroToOne::ZERO)
.over(Duration::from_millis(300)) .over(Duration::from_millis(300))
.with_easing(context.query_style(&EasingOut))
.spawn(); .spawn();
} }
@ -256,6 +265,7 @@ impl Widget for Scroll {
}; };
self.scroll.map_mut(|scroll| *scroll += amount.cast()); self.scroll.map_mut(|scroll| *scroll += amount.cast());
self.show_scrollbars(context);
context.set_needs_redraw(); context.set_needs_redraw();
// TODO make this only returned handled if we actually scrolled. // TODO make this only returned handled if we actually scrolled.