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
//! use std::time::Duration;
//!
//! use gooey::animation::{AnimationTarget, Spawn};
//! use gooey::animation::{AnimationTarget, EaseInOutElastic, Spawn};
//! use gooey::value::Dynamic;
//!
//! let value = Dynamic::new(0);
@ -25,6 +25,7 @@
//! value
//! .transition_to(100)
//! .over(Duration::from_millis(100))
//! .with_easing(EaseInOutElastic)
//! .launch();
//!
//! let mut reader = value.into_reader();
@ -37,9 +38,9 @@
use std::f32::consts::PI;
use std::fmt::Debug;
use std::marker::PhantomData;
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::time::{Duration, Instant};
@ -47,6 +48,7 @@ use alot::{LotId, Lots};
use kempt::Set;
use kludgine::Color;
use crate::styles::Component;
use crate::value::Dynamic;
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
@ -224,7 +226,7 @@ where
{
value: Target,
duration: Duration,
_easing: PhantomData<Easing>,
easing: Easing,
}
impl<T> Animation<T, Linear>
@ -235,16 +237,16 @@ where
Self {
value,
duration,
_easing: PhantomData,
easing: Linear,
}
}
/// 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 {
value: self.value,
duration: self.duration,
_easing: PhantomData,
easing,
}
}
}
@ -261,7 +263,7 @@ where
target: self.value.begin(),
duration: self.duration,
elapsed: Duration::ZERO,
_easing: PhantomData,
easing: self.easing,
}
}
}
@ -401,7 +403,7 @@ where
self.target.finish();
ControlFlow::Break(remaining_elapsed)
} 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.target.update(progress);
@ -421,7 +423,7 @@ pub struct RunningAnimation<T, Easing> {
target: T,
duration: Duration,
elapsed: Duration,
_easing: PhantomData<Easing>,
easing: Easing,
}
/// 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!(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]
fn integer_lerps() {
#[track_caller]
@ -768,21 +784,57 @@ impl LinearInterpolate for ZeroToOne {
}
}
/// Performs easing for value interpolation.
pub trait Easing: Send + Sync + 'static {
/// Eases a value ranging between zero and one. The resulting value does not
/// need to be bounded between zero and one.
fn ease(progress: ZeroToOne) -> f32;
/// An easing function for customizing animations.
#[derive(Debug, Clone)]
pub enum EasingFunction {
/// A function pointer to use as an easing function.
Fn(fn(ZeroToOne) -> f32),
/// A custom easing implementation.
Custom(Arc<dyn Easing>),
}
// /// An [`Easing`] function that produces a steady, linear transition.
// pub enum Linear {}
impl Easing for EasingFunction {
fn ease(&self, progress: ZeroToOne) -> f32 {
match self {
EasingFunction::Fn(func) => func(progress),
EasingFunction::Custom(func) => func.ease(progress),
}
}
}
// impl Easing for Linear {
// fn ease(progress: ZeroToOne) -> f32 {
// *progress
// }
// }
impl From<EasingFunction> for Component {
fn from(value: EasingFunction) -> Self {
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
// 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
#[doc = $description]
#[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 {
fn ease(progress: ZeroToOne) -> f32 {
impl $name {
/// 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);
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
}
/// 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!(
EaseOutSine,
easeOutSine,
@ -1088,7 +1141,7 @@ declare_easing_function_implementation!(
EaseInBounce,
easeInBounce,
"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!(

View file

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

View file

@ -4,6 +4,7 @@ use std::borrow::Cow;
use kludgine::figures::units::Lp;
use kludgine::Color;
use crate::animation::{EaseInQuadradic, EaseOutQuadradic, EasingFunction};
use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent};
/// The [`Dimension`] to use as the size to render text.
@ -97,3 +98,64 @@ impl ComponentDefinition for IntrinsicPadding {
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 (top, bottom, height) = vertical.measure(available_space.height, content_size.height);
dbg!(Layout {
Layout {
margin: Edges {
left,
right,
@ -60,7 +60,7 @@ impl Align {
bottom,
},
content: Size::new(width, height),
})
}
}
}

View file

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

View file

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