From ee7c74f3a8b8aafecc0672dc4c01695108400e45 Mon Sep 17 00:00:00 2001 From: Mark <> Date: Sat, 23 Aug 2025 12:26:13 +0200 Subject: [PATCH] feat: use better animations (these now work on low fps too) --- musicdb-client/src/gui_anim.rs | 146 +++++-------------------- musicdb-client/src/gui_base.rs | 2 +- musicdb-client/src/gui_edit_song.rs | 25 +++-- musicdb-client/src/gui_idle_display.rs | 55 ++++------ musicdb-client/src/gui_library.rs | 14 +-- musicdb-client/src/gui_playback.rs | 35 +++--- musicdb-client/src/gui_screen.rs | 2 +- musicdb-client/src/gui_statusbar.rs | 26 ++--- 8 files changed, 105 insertions(+), 200 deletions(-) diff --git a/musicdb-client/src/gui_anim.rs b/musicdb-client/src/gui_anim.rs index db29b12..f2944f4 100644 --- a/musicdb-client/src/gui_anim.rs +++ b/musicdb-client/src/gui_anim.rs @@ -1,130 +1,42 @@ -use std::{ - ops::{Add, AddAssign, Mul, MulAssign, Sub}, - time::{Duration, Instant}, -}; +use std::time::Instant; -pub struct AnimationController { - pub last_updated: Instant, - pub value: F, - pub speed: F, - pub max_speed: F, - pub target: F, - /// while the time remaining to finish the animation is above this, we accelerate (higher -> stop acceleration earlier) - pub accel_until: F, - /// if the time remaining to finish the animation drops below this, we decelerate (higher -> start decelerating earlier) - pub decel_while: F, - pub acceleration: F, +use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator}; + +pub struct AnimationController { + speed: f64, + anim: DefaultAnimatorF64Quadratic, } -pub trait Float: - Sized - + Clone - + Copy - + Add - + Sub - + std::ops::Neg - + Mul - + MulAssign - + AddAssign - + PartialOrd -{ - fn zero() -> Self; - /// 1/1000 - fn milli() -> Self; - fn duration_secs(d: Duration) -> Self; - fn abs(self) -> Self { - if self < Self::zero() { - -self - } else { - self +impl AnimationController { + pub fn new(value: f64, target: f64, speed: f64) -> Self { + let mut anim = DefaultAnimatorF64Quadratic::new(value, speed); + if value != target { + anim.set_target(target, Instant::now()); } + AnimationController { speed, anim } } -} - -impl AnimationController { - pub fn new( - value: F, - target: F, - acceleration: F, - max_speed: F, - accel_until: F, - decel_while: F, - now: Instant, - ) -> Self { - AnimationController { - last_updated: now, - value, - speed: F::zero(), - max_speed, - target, - accel_until, - decel_while, - acceleration, - } + pub fn target(&self) -> f64 { + self.anim.target() } - pub fn ignore_elapsed_time(&mut self, now: Instant) { - self.last_updated = now; + pub fn set_target(&mut self, now: Instant, target: f64) { + self.anim.set_target(target, now); } - pub fn update(&mut self, now: Instant, instant: bool) -> bool { - let changed = if self.target != self.value { + pub fn update(&mut self, now: Instant, instant: bool) -> Result { + if self.anim.target() != self.anim.get_value(now) { if instant { - self.value = self.target; + let target = self.anim.target(); + self.anim = DefaultAnimatorF64Quadratic::new(target, self.speed); + Ok(target) } else { - let inc = self.target > self.value; - let seconds = F::duration_secs(now.duration_since(self.last_updated)); - let ref1 = self.value + self.speed * self.accel_until; - let ref2 = self.value + self.speed * self.decel_while; - let speed_diff = match (ref1 < self.target, ref2 > self.target) { - (true, false) => self.acceleration, - (false, true) => -self.acceleration, - (true, true) | (false, false) => F::zero(), - }; - self.speed += speed_diff; - if self.speed.abs() > self.max_speed { - if self.speed < F::zero() { - self.speed = -self.max_speed; - } else { - self.speed = self.max_speed; - } - } - self.value += self.speed * seconds; - self.speed += speed_diff; - if (self.target - self.value).abs() < self.speed * F::milli() - || inc != (self.target > self.value) - { - // overshoot or target reached - self.value = self.target; - self.speed = F::zero(); - } + Ok(self.anim.get_value(now)) } - true } else { - false - }; - self.last_updated = now; - changed - } -} - -impl Float for f32 { - fn zero() -> Self { - 0.0 - } - fn milli() -> Self { - 0.001 - } - fn duration_secs(d: Duration) -> Self { - d.as_secs_f32().min(0.1) - } -} -impl Float for f64 { - fn zero() -> Self { - 0.0 - } - fn milli() -> Self { - 0.001 - } - fn duration_secs(d: Duration) -> Self { - d.as_secs_f64().min(0.1) + Err(self.anim.target()) + } + } + pub fn value(&mut self, now: Instant) -> f64 { + match self.update(now, false) { + Ok(v) | Err(v) => v, + } } } diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs index d170e72..40a7179 100755 --- a/musicdb-client/src/gui_base.rs +++ b/musicdb-client/src/gui_base.rs @@ -604,7 +604,7 @@ impl GuiElem for Slider { fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) { self.display = !self.display; - self.display_since = Some(Instant::now()); + self.display_since = Some(info.time); self.config.redraw = true; } let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25); diff --git a/musicdb-client/src/gui_edit_song.rs b/musicdb-client/src/gui_edit_song.rs index cf2f975..741fe33 100644 --- a/musicdb-client/src/gui_edit_song.rs +++ b/musicdb-client/src/gui_edit_song.rs @@ -198,7 +198,11 @@ impl GuiElem for EditorForSongs { Event::SetArtist(name, id) => { self.c_scrollbox.children.c_artist.chosen_id = id; self.c_scrollbox.children.c_artist.last_search = name.to_lowercase(); - self.c_scrollbox.children.c_artist.open_prog.target = 1.0; + self.c_scrollbox + .children + .c_artist + .open_prog + .set_target(info.time, 1.0); *self .c_scrollbox .children @@ -232,15 +236,15 @@ impl GuiElem for EditorForSongs { } } // artist sel - if self + if let Ok(val) = self .c_scrollbox .children .c_artist .open_prog - .update(Instant::now(), false) + .update(info.time, false) { if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) { - *v = ELEM_HEIGHT * self.c_scrollbox.children.c_artist.open_prog.value; + *v = ELEM_HEIGHT * val as f32; self.c_scrollbox.config_mut().redraw = true; } if let Some(h) = &info.helper { @@ -272,7 +276,7 @@ pub struct EditorForSongArtistChooser { config: GuiElemCfg, event_sender: std::sync::mpsc::Sender, /// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6) - open_prog: AnimationController, + open_prog: AnimationController, expand_to: f32, chosen_id: Option, c_name: TextField, @@ -285,7 +289,7 @@ impl EditorForSongArtistChooser { Self { config: GuiElemCfg::default(), event_sender, - open_prog: AnimationController::new(1.0, 1.0, 0.3, 8.0, 0.5, 0.6, Instant::now()), + open_prog: AnimationController::new(1.0, 1.0, 4.0), expand_to, chosen_id: None, c_name: TextField::new( @@ -307,10 +311,10 @@ impl EditorForSongArtistChooser { } impl GuiElem for EditorForSongArtistChooser { fn draw(&mut self, info: &mut crate::gui::DrawInfo, _g: &mut speedy2d::Graphics2D) { - let picker_enabled = self.open_prog.value > 1.0; + let picker_enabled = self.open_prog.value(info.time) > 1.0; self.c_picker.config_mut().enabled = picker_enabled; if picker_enabled { - let split = 1.0 / self.open_prog.value; + let split = 1.0 / self.open_prog.value(info.time) as f32; self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, split)); self.c_picker.config_mut().pos = Rectangle::from_tuples((0.0, split), (1.0, 1.0)); } else { @@ -327,9 +331,10 @@ impl GuiElem for EditorForSongArtistChooser { }; if search_changed { self.chosen_id = None; - self.open_prog.target = self.expand_to; if search.is_empty() { - self.open_prog.target = 1.0; + self.open_prog.set_target(info.time, 1.0); + } else { + self.open_prog.set_target(info.time, self.expand_to as f64); } } let artists = info diff --git a/musicdb-client/src/gui_idle_display.rs b/musicdb-client/src/gui_idle_display.rs index 4fbe7f1..1a8a3b0 100644 --- a/musicdb-client/src/gui_idle_display.rs +++ b/musicdb-client/src/gui_idle_display.rs @@ -1,7 +1,4 @@ -use std::{ - sync::{atomic::AtomicBool, Arc}, - time::Instant, -}; +use std::sync::{atomic::AtomicBool, Arc}; use musicdb_lib::data::ArtistId; use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; @@ -27,8 +24,8 @@ pub struct IdleDisplay { pub c_buttons: PlayPause, pub c_buttons_custom_pos: bool, - pub cover_aspect_ratio: AnimationController, - pub artist_image_aspect_ratio: AnimationController, + pub cover_aspect_ratio: AnimationController, + pub artist_image_aspect_ratio: AnimationController, pub cover_pos: Option, pub cover_left: f32, @@ -76,24 +73,8 @@ impl IdleDisplay { is_fav: (false, Arc::clone(&is_fav)), c_buttons: PlayPause::new(GuiElemCfg::default(), is_fav), c_buttons_custom_pos: false, - cover_aspect_ratio: AnimationController::new( - 1.0, - 1.0, - 0.01, - 1.0, - 0.8, - 0.6, - Instant::now(), - ), - artist_image_aspect_ratio: AnimationController::new( - 0.0, - 0.0, - 0.01, - 1.0, - 0.8, - 0.6, - Instant::now(), - ), + cover_aspect_ratio: AnimationController::new(1.0, 1.0, 1.0), + artist_image_aspect_ratio: AnimationController::new(0.0, 0.0, 1.0), cover_pos: None, cover_left: 0.01, cover_top: 0.21, @@ -181,7 +162,7 @@ impl GuiElem for IdleDisplay { .is_some_and(|(a, _)| *a != artist_id) { self.current_artist_image = Some((artist_id, None)); - self.artist_image_aspect_ratio.target = 0.0; + self.artist_image_aspect_ratio.set_target(info.time, 0.0); if let Some(artist) = info.database.artists().get(&artist_id) { for tag in &artist.general.tags { if tag.starts_with("ImageExt=") { @@ -205,7 +186,7 @@ impl GuiElem for IdleDisplay { } else { if self.current_artist_image.is_some() { self.current_artist_image = None; - self.artist_image_aspect_ratio.target = 0.0; + self.artist_image_aspect_ratio.set_target(info.time, 0.0); } } } @@ -213,7 +194,7 @@ impl GuiElem for IdleDisplay { self.current_info.new_cover = false; match self.current_info.current_cover { None | Some((_, Some(None))) => { - self.cover_aspect_ratio.target = 0.0; + self.cover_aspect_ratio.set_target(info.time, 0.0); } Some((_, None)) | Some((_, Some(Some(_)))) => {} } @@ -243,6 +224,7 @@ impl GuiElem for IdleDisplay { info.pos.top_left().x + info.pos.height() * self.cover_left, info.pos.top_left().y + info.pos.height() * self.cover_top, info.pos.top_left().y + info.pos.height() * self.cover_bottom, + info.time, &mut self.cover_aspect_ratio, ); } @@ -260,20 +242,23 @@ impl GuiElem for IdleDisplay { info.pos.top_left().x + info.pos.height() * self.cover_left, top, bottom, - self.cover_aspect_ratio.value, + self.cover_aspect_ratio.value(info.time) as f32, ) + info.pos.height() * self.artist_image_to_cover_margin, top + (bottom - top) * self.artist_image_top, bottom, + info.time, &mut self.artist_image_aspect_ratio, ); } // move children to make space for cover let ar_updated = self .cover_aspect_ratio - .update(info.time.clone(), info.high_performance) + .update(info.time, info.high_performance) + .is_ok() | self .artist_image_aspect_ratio - .update(info.time.clone(), info.high_performance); + .update(info.time, info.high_performance) + .is_ok(); if ar_updated || info.pos.size() != self.config.pixel_pos.size() { if let Some(h) = &info.helper { h.request_redraw(); @@ -281,8 +266,12 @@ impl GuiElem for IdleDisplay { // make thing be relative to width instead of to height by multiplying with this let top = self.cover_top; let bottom = self.cover_bottom; - let left = (get_right_x(self.cover_left, top, bottom, self.cover_aspect_ratio.value) - + self.artist_image_to_cover_margin) + let left = (get_right_x( + self.cover_left, + top, + bottom, + self.cover_aspect_ratio.value(info.time) as f32, + ) + self.artist_image_to_cover_margin) * info.pos.height() / info.pos.width(); let ai_top = top + (bottom - top) * self.artist_image_top; @@ -293,7 +282,7 @@ impl GuiElem for IdleDisplay { left, ai_top * info.pos.height() / info.pos.width(), bottom * info.pos.height() / info.pos.width(), - self.artist_image_aspect_ratio.value, + self.artist_image_aspect_ratio.value(info.time) as f32, ); self.c_side2_label.config_mut().pos = Rectangle::from_tuples((left, ai_top), (max_right, bottom)); diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index cdab90f..856ec49 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -6,7 +6,6 @@ use std::{ atomic::{AtomicBool, AtomicUsize}, mpsc, Mutex, }, - time::Instant, }; use musicdb_lib::data::{ @@ -70,7 +69,7 @@ pub struct LibraryBrowser { search_song: String, search_song_regex: Option, filter_target_state: Arc, - filter_state: AnimationController, + filter_state: AnimationController, library_updated: bool, search_settings_changed: Arc, search_is_case_sensitive: Arc, @@ -203,7 +202,7 @@ impl LibraryBrowser { search_song: String::new(), search_song_regex: None, filter_target_state, - filter_state: AnimationController::new(0.0, 0.0, 0.25, 25.0, 0.1, 0.2, Instant::now()), + filter_state: AnimationController::new(0.0, 0.0, 4.0), library_updated: true, search_settings_changed, search_is_case_sensitive, @@ -380,18 +379,19 @@ impl GuiElem for LibraryBrowser { let filter_target_state = self .filter_target_state .load(std::sync::atomic::Ordering::Relaxed); - self.filter_state.target = if filter_target_state { 1.0 } else { 0.0 }; - if self.filter_state.update(info.time, info.high_performance) { + self.filter_state + .set_target(info.time, if filter_target_state { 1.0 } else { 0.0 }); + if let Ok(val) = self.filter_state.update(info.time, info.high_performance) { if let Some(h) = &info.helper { h.request_redraw(); } - let y = LP_LIB1 + (LP_LIB1S - LP_LIB1) * self.filter_state.value; + let y = LP_LIB1 + (LP_LIB1S - LP_LIB1) * val as f32; self.c_scroll_box.config_mut().pos = Rectangle::new(Vec2::new(0.0, y), Vec2::new(1.0, LP_LIB2)); let filter_panel = &mut self.c_filter_panel; filter_panel.config_mut().pos = Rectangle::new(Vec2::new(0.0, LP_LIB1), Vec2::new(1.0, y)); - filter_panel.config.enabled = self.filter_state.value > 0.0; + filter_panel.config.enabled = val > 0.0; } // - if self.library_updated { diff --git a/musicdb-client/src/gui_playback.rs b/musicdb-client/src/gui_playback.rs index 3449571..51c87b5 100755 --- a/musicdb-client/src/gui_playback.rs +++ b/musicdb-client/src/gui_playback.rs @@ -1,4 +1,7 @@ -use std::{sync::Arc, time::Duration}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; use musicdb_lib::data::{CoverId, SongId}; use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; @@ -151,25 +154,29 @@ pub fn image_display( left: f32, top: f32, bottom: f32, - aspect_ratio: &mut AnimationController, + now: Instant, + aspect_ratio: &mut AnimationController, ) { if let Some(cover) = &img { let cover_size = cover.size(); - aspect_ratio.target = if cover_size.x > 0 && cover_size.y > 0 { - let pos = if let Some(pos) = pos { - pos - } else { - let right_x = get_right_x(left, top, bottom, aspect_ratio.value); - Rectangle::from_tuples((left, top), (right_x, bottom)) - }; - let aspect_ratio = cover_size.x as f32 / cover_size.y as f32; - g.draw_rectangle_image(pos, cover); - aspect_ratio + let pos = if let Some(pos) = pos { + pos } else { - 0.0 + let right_x = get_right_x(left, top, bottom, aspect_ratio.value(now) as f32); + Rectangle::from_tuples((left, top), (right_x, bottom)) }; + g.draw_rectangle_image(pos, cover); + aspect_ratio.set_target( + now, + if cover_size.x > 0 && cover_size.y > 0 { + let aspect_ratio = cover_size.x as f32 / cover_size.y as f32; + aspect_ratio as f64 + } else { + 0.0 + }, + ); } else { - aspect_ratio.target = 0.0; + aspect_ratio.set_target(now, 0.0); } } pub fn get_right_x(left: f32, top: f32, bottom: f32, aspect_ratio: f32) -> f32 { diff --git a/musicdb-client/src/gui_screen.rs b/musicdb-client/src/gui_screen.rs index 776c1f0..1519036 100755 --- a/musicdb-client/src/gui_screen.rs +++ b/musicdb-client/src/gui_screen.rs @@ -403,7 +403,7 @@ impl GuiElem for GuiScreen { false }; // request_redraw for animations - let idle_value = self.idle.get_value(Instant::now()) as f32; + let idle_value = self.idle.get_value(info.time) as f32; let idle_changed = self.idle_prev_val != idle_value; if idle_changed || idle_exit_anim || self.settings.1.is_some() { self.idle_prev_val = idle_value; diff --git a/musicdb-client/src/gui_statusbar.rs b/musicdb-client/src/gui_statusbar.rs index f65ef13..5ef9146 100644 --- a/musicdb-client/src/gui_statusbar.rs +++ b/musicdb-client/src/gui_statusbar.rs @@ -1,7 +1,4 @@ -use std::{ - sync::{atomic::AtomicBool, Arc}, - time::Instant, -}; +use std::sync::{atomic::AtomicBool, Arc}; use speedy2d::{dimen::Vec2, shape::Rectangle}; @@ -17,7 +14,7 @@ pub struct StatusBar { config: GuiElemCfg, pub idle_mode: f32, current_info: CurrentInfo, - cover_aspect_ratio: AnimationController, + cover_aspect_ratio: AnimationController, c_song_label: AdvancedLabel, pub force_reset_texts: bool, c_buttons: PlayPause, @@ -31,15 +28,7 @@ impl StatusBar { config, idle_mode: 0.0, current_info: CurrentInfo::new(), - cover_aspect_ratio: AnimationController::new( - 0.0, - 0.0, - 0.01, - 1.0, - 0.8, - 0.6, - Instant::now(), - ), + cover_aspect_ratio: AnimationController::new(0.0, 0.0, 1.0), c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), force_reset_texts: false, is_fav: (false, Arc::clone(&is_fav)), @@ -82,7 +71,7 @@ impl GuiElem for StatusBar { self.current_info.new_cover = false; match self.current_info.current_cover { None | Some((_, Some(None))) => { - self.cover_aspect_ratio.target = 0.0; + self.cover_aspect_ratio.set_target(info.time, 0.0); } Some((_, None)) | Some((_, Some(Some(_)))) => {} } @@ -90,7 +79,8 @@ impl GuiElem for StatusBar { // move children to make space for cover let ar_updated = self .cover_aspect_ratio - .update(info.time.clone(), info.high_performance); + .update(info.time, info.high_performance) + .is_ok(); if ar_updated || info.pos.size() != self.config.pixel_pos.size() { if let Some(h) = &info.helper { h.request_redraw(); @@ -105,7 +95,8 @@ impl GuiElem for StatusBar { ); self.c_song_label.config_mut().pos = Rectangle::from_tuples( ( - self.cover_aspect_ratio.value * info.pos.height() / info.pos.width(), + self.cover_aspect_ratio.value(info.time) as f32 * info.pos.height() + / info.pos.width(), 0.0, ), (buttons_right_pos - buttons_width, 1.0), @@ -125,6 +116,7 @@ impl GuiElem for StatusBar { info.pos.top_left().x + info.pos.height() * 0.05, info.pos.top_left().y + info.pos.height() * 0.05, info.pos.top_left().y + info.pos.height() * 0.95, + info.time, &mut self.cover_aspect_ratio, ); }