use crate::widget_id::WidgetId;
use ahash::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnimatableProperty {
Opacity,
PositionX,
PositionY,
Width,
Height,
Rotation,
ScaleX,
ScaleY,
ColorR,
ColorG,
ColorB,
ColorA,
BorderRadius,
Padding,
TranslateX,
TranslateY,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EasingFunction {
Linear,
EaseIn,
EaseOut,
EaseInOut,
Bounce,
Elastic,
QuadIn,
QuadOut,
QuadInOut,
CubicIn,
CubicOut,
CubicInOut,
}
impl EasingFunction {
pub fn apply(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
EasingFunction::Linear => t,
EasingFunction::EaseIn => t * t,
EasingFunction::EaseOut => t * (2.0 - t),
EasingFunction::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
-1.0 + (4.0 - 2.0 * t) * t
}
}
EasingFunction::Bounce => {
if t < 1.0 / 2.75 {
7.5625 * t * t
} else if t < 2.0 / 2.75 {
let t = t - 1.5 / 2.75;
7.5625 * t * t + 0.75
} else if t < 2.5 / 2.75 {
let t = t - 2.25 / 2.75;
7.5625 * t * t + 0.9375
} else {
let t = t - 2.625 / 2.75;
7.5625 * t * t + 0.984375
}
}
EasingFunction::Elastic => {
if t == 0.0 || t == 1.0 {
t
} else {
let p = 0.3;
let s = p / 4.0;
let t = t - 1.0;
-(2.0f32.powf(10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin())
}
}
EasingFunction::QuadIn => t * t,
EasingFunction::QuadOut => t * (2.0 - t),
EasingFunction::QuadInOut => {
if t < 0.5 {
2.0 * t * t
} else {
-1.0 + (4.0 - 2.0 * t) * t
}
}
EasingFunction::CubicIn => t * t * t,
EasingFunction::CubicOut => {
let t = t - 1.0;
t * t * t + 1.0
}
EasingFunction::CubicInOut => {
let t = t * 2.0;
if t < 1.0 {
0.5 * t * t * t
} else {
let t = t - 2.0;
0.5 * (t * t * t + 2.0)
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnimationState {
Running,
Paused,
Completed,
}
#[derive(Debug, Clone)]
pub struct Animation {
property: AnimatableProperty,
from: f32,
to: f32,
duration: f32,
elapsed: f32,
easing: EasingFunction,
state: AnimationState,
looping: bool,
yoyo: bool,
direction: f32,
delay: f32,
delay_elapsed: f32,
}
impl Animation {
pub fn new(property: AnimatableProperty) -> Self {
Self {
property,
from: 0.0,
to: 1.0,
duration: 1.0,
elapsed: 0.0,
easing: EasingFunction::Linear,
state: AnimationState::Running,
looping: false,
yoyo: false,
direction: 1.0,
delay: 0.0,
delay_elapsed: 0.0,
}
}
pub fn from(mut self, value: f32) -> Self {
self.from = value;
self
}
pub fn to(mut self, value: f32) -> Self {
self.to = value;
self
}
pub fn duration(mut self, duration: f32) -> Self {
self.duration = duration;
self
}
pub fn easing(mut self, easing: EasingFunction) -> Self {
self.easing = easing;
self
}
pub fn looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
pub fn yoyo(mut self, yoyo: bool) -> Self {
self.yoyo = yoyo;
self
}
pub fn delay(mut self, delay: f32) -> Self {
self.delay = delay;
self
}
pub fn property(&self) -> AnimatableProperty {
self.property
}
pub fn value(&self) -> f32 {
if self.delay_elapsed < self.delay {
return self.from;
}
let t = (self.elapsed / self.duration).clamp(0.0, 1.0);
let eased_t = self.easing.apply(t);
self.from + (self.to - self.from) * eased_t
}
pub fn state(&self) -> AnimationState {
self.state
}
pub fn pause(&mut self) {
self.state = AnimationState::Paused;
}
pub fn resume(&mut self) {
if self.state == AnimationState::Paused {
self.state = AnimationState::Running;
}
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.delay_elapsed = 0.0;
self.state = AnimationState::Running;
self.direction = 1.0;
}
pub fn update(&mut self, delta_time: f32) -> bool {
if self.state != AnimationState::Running {
return self.state != AnimationState::Completed;
}
if self.delay_elapsed < self.delay {
self.delay_elapsed += delta_time;
if self.delay_elapsed < self.delay {
return true;
}
let _ = delta_time - (self.delay - self.delay_elapsed);
}
self.elapsed += delta_time * self.direction;
if self.direction > 0.0 && self.elapsed >= self.duration {
if self.yoyo {
self.direction = -1.0;
self.elapsed = self.duration;
} else if self.looping {
self.elapsed = 0.0;
} else {
self.elapsed = self.duration;
self.state = AnimationState::Completed;
return false;
}
} else if self.direction < 0.0 && self.elapsed <= 0.0 {
if self.looping {
self.direction = 1.0;
self.elapsed = 0.0;
} else {
self.elapsed = 0.0;
self.state = AnimationState::Completed;
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct WidgetAnimations {
animations: HashMap<AnimatableProperty, Animation>,
}
impl WidgetAnimations {
pub fn new() -> Self {
Self {
animations: HashMap::default(),
}
}
pub fn add(&mut self, animation: Animation) {
self.animations.insert(animation.property(), animation);
}
pub fn remove(&mut self, property: AnimatableProperty) {
self.animations.remove(&property);
}
pub fn get(&self, property: AnimatableProperty) -> Option<&Animation> {
self.animations.get(&property)
}
pub fn get_mut(&mut self, property: AnimatableProperty) -> Option<&mut Animation> {
self.animations.get_mut(&property)
}
pub fn update(&mut self, delta_time: f32) -> bool {
let mut any_running = false;
self.animations.retain(|_, animation| {
let running = animation.update(delta_time);
any_running |= running;
running
});
any_running
}
pub fn values(&self) -> HashMap<AnimatableProperty, f32> {
self.animations
.iter()
.map(|(prop, anim)| (*prop, anim.value()))
.collect()
}
pub fn is_empty(&self) -> bool {
self.animations.is_empty()
}
pub fn clear(&mut self) {
self.animations.clear();
}
}
impl Default for WidgetAnimations {
fn default() -> Self {
Self::new()
}
}
pub struct AnimationSystem {
widget_animations: HashMap<WidgetId, WidgetAnimations>,
}
impl AnimationSystem {
pub fn new() -> Self {
Self {
widget_animations: HashMap::default(),
}
}
pub fn animate(&mut self, widget_id: WidgetId, animation: Animation) {
self.widget_animations
.entry(widget_id)
.or_default()
.add(animation);
}
pub fn remove_animation(&mut self, widget_id: WidgetId, property: AnimatableProperty) {
if let Some(animations) = self.widget_animations.get_mut(&widget_id) {
animations.remove(property);
if animations.is_empty() {
self.widget_animations.remove(&widget_id);
}
}
}
pub fn remove_widget_animations(&mut self, widget_id: WidgetId) {
self.widget_animations.remove(&widget_id);
}
pub fn get_animations(&self, widget_id: WidgetId) -> Option<&WidgetAnimations> {
self.widget_animations.get(&widget_id)
}
pub fn get_animations_mut(&mut self, widget_id: WidgetId) -> Option<&mut WidgetAnimations> {
self.widget_animations.get_mut(&widget_id)
}
pub fn update(&mut self, delta_time: f32) {
self.widget_animations.retain(|_, animations| {
animations.update(delta_time);
!animations.is_empty()
});
}
pub fn animated_values(&self) -> HashMap<WidgetId, HashMap<AnimatableProperty, f32>> {
self.widget_animations
.iter()
.map(|(id, animations)| (*id, animations.values()))
.collect()
}
pub fn clear(&mut self) {
self.widget_animations.clear();
}
pub fn widget_count(&self) -> usize {
self.widget_animations.len()
}
pub fn has_active(&self) -> bool {
!self.widget_animations.is_empty()
}
pub fn tick(&mut self, delta_time: f32, ui: &mut crate::UiCore) {
self.widget_animations.retain(|&widget_id, animations| {
animations.update(delta_time);
for (prop, value) in animations.values() {
match prop {
AnimatableProperty::Opacity => {
ui.update_opacity(widget_id, value);
}
AnimatableProperty::TranslateX | AnimatableProperty::PositionX => {
ui.update_translate_x(widget_id, value);
}
AnimatableProperty::TranslateY | AnimatableProperty::PositionY => {
ui.update_translate_y(widget_id, value);
}
AnimatableProperty::ScaleX => {
ui.update_scale_x(widget_id, value);
}
AnimatableProperty::ScaleY => {
ui.update_scale_y(widget_id, value);
}
_ => {}
}
}
!animations.is_empty()
});
}
pub fn tick_system(&mut self, delta_time: f32, ui: &mut crate::UiSystem) {
self.tick(delta_time, ui.core_mut());
}
}
impl Default for AnimationSystem {
fn default() -> Self {
Self::new()
}
}
pub fn fade_in(duration: f32) -> Animation {
Animation::new(AnimatableProperty::Opacity)
.from(0.0)
.to(1.0)
.duration(duration)
.easing(EasingFunction::EaseInOut)
}
pub fn fade_out(duration: f32) -> Animation {
Animation::new(AnimatableProperty::Opacity)
.from(1.0)
.to(0.0)
.duration(duration)
.easing(EasingFunction::EaseInOut)
}
pub fn slide_in_left(from_x: f32, to_x: f32, duration: f32) -> Animation {
Animation::new(AnimatableProperty::PositionX)
.from(from_x)
.to(to_x)
.duration(duration)
.easing(EasingFunction::EaseOut)
}
pub fn slide_in_top(from_y: f32, to_y: f32, duration: f32) -> Animation {
Animation::new(AnimatableProperty::PositionY)
.from(from_y)
.to(to_y)
.duration(duration)
.easing(EasingFunction::EaseOut)
}
pub fn scale(from: f32, to: f32, duration: f32) -> Animation {
Animation::new(AnimatableProperty::ScaleX)
.from(from)
.to(to)
.duration(duration)
.easing(EasingFunction::EaseInOut)
}
pub fn bounce(duration: f32) -> Animation {
Animation::new(AnimatableProperty::ScaleY)
.from(1.0)
.to(1.2)
.duration(duration)
.easing(EasingFunction::Bounce)
.yoyo(true)
.looping(true)
}
pub fn translate_x(from: f32, to: f32, duration: f32) -> Animation {
Animation::new(AnimatableProperty::TranslateX)
.from(from)
.to(to)
.duration(duration)
.easing(EasingFunction::EaseOut)
}
pub fn translate_y(from: f32, to: f32, duration: f32) -> Animation {
Animation::new(AnimatableProperty::TranslateY)
.from(from)
.to(to)
.duration(duration)
.easing(EasingFunction::EaseOut)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_easing() {
let easing = EasingFunction::Linear;
assert_eq!(easing.apply(0.0), 0.0);
assert_eq!(easing.apply(0.5), 0.5);
assert_eq!(easing.apply(1.0), 1.0);
}
#[test]
fn test_animation_update() {
let mut anim = Animation::new(AnimatableProperty::Opacity)
.from(0.0)
.to(1.0)
.duration(1.0);
assert_eq!(anim.value(), 0.0);
assert!(anim.update(0.5));
assert!((anim.value() - 0.5).abs() < 0.01);
assert!(!anim.update(0.5));
assert_eq!(anim.value(), 1.0);
assert_eq!(anim.state(), AnimationState::Completed);
}
#[test]
fn test_animation_system() {
let mut system = AnimationSystem::new();
let widget_id = WidgetId::new("test_widget");
system.animate(widget_id, fade_in(1.0));
assert_eq!(system.widget_count(), 1);
system.update(1.0);
assert_eq!(system.widget_count(), 0);
}
#[test]
fn test_looping_animation() {
let mut anim = Animation::new(AnimatableProperty::Opacity)
.from(0.0)
.to(1.0)
.duration(1.0)
.looping(true);
anim.update(1.0);
assert_eq!(anim.state(), AnimationState::Running);
assert_eq!(anim.value(), 0.0);
anim.update(0.5);
assert!((anim.value() - 0.5).abs() < 0.01);
}
#[test]
fn test_yoyo_animation() {
let mut anim = Animation::new(AnimatableProperty::Opacity)
.from(0.0)
.to(1.0)
.duration(1.0)
.yoyo(true);
assert!(anim.update(1.0));
assert!((anim.value() - 1.0).abs() < 0.01);
assert_eq!(anim.state(), AnimationState::Running);
assert!(!anim.update(1.0));
assert!((anim.value() - 0.0).abs() < 0.01);
assert_eq!(anim.state(), AnimationState::Completed);
}
#[test]
fn test_translate_animation_helpers() {
let anim_x = translate_x(-100.0, 0.0, 0.5);
assert_eq!(anim_x.property(), AnimatableProperty::TranslateX);
assert_eq!(anim_x.value(), -100.0);
let anim_y = translate_y(-50.0, 0.0, 0.3);
assert_eq!(anim_y.property(), AnimatableProperty::TranslateY);
assert_eq!(anim_y.value(), -50.0); }
#[test]
fn test_has_active() {
let mut system = AnimationSystem::new();
assert!(!system.has_active());
let widget_id = WidgetId::new("test");
system.animate(widget_id, fade_in(1.0));
assert!(system.has_active());
system.update(2.0); assert!(!system.has_active());
}
#[test]
fn test_easing_boundary_values() {
let easings = [
EasingFunction::Linear,
EasingFunction::EaseIn,
EasingFunction::EaseOut,
EasingFunction::EaseInOut,
EasingFunction::QuadIn,
EasingFunction::QuadOut,
EasingFunction::QuadInOut,
EasingFunction::CubicIn,
EasingFunction::CubicOut,
EasingFunction::CubicInOut,
];
for easing in &easings {
assert!(
(easing.apply(0.0) - 0.0).abs() < 0.001,
"{easing:?} failed at t=0"
);
assert!(
(easing.apply(1.0) - 1.0).abs() < 0.001,
"{easing:?} failed at t=1"
);
}
}
}