mirror of
https://github.com/danbulant/cushy
synced 2026-06-18 14:01:10 +00:00
Easing functions as styles
This commit is contained in:
parent
1bf1b082af
commit
6f5ffd80b4
6 changed files with 189 additions and 59 deletions
137
src/animation.rs
137
src/animation.rs
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) => {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue