Skip to content

Commit

Permalink
refactor(animation): new animations engine
Browse files Browse the repository at this point in the history
This commit is comprised of the following interactively rebased commits
from PR #1002 by @thearturca.

1a184a4
refactor(animation): move animations to its own mod

First step for more rusty version animations. The goal is to make
animations more generic so its easier to add new animations to komorebi!

d3ac6b7
refactor(animation): reduce mutex calls on `ANIMATION_STYLE`

8a42b73
refactor(animation): introduce `Lerp` trait

e449861
refactor(animation): generalized ANIMATION_MANAGER

Instead of a isize key for the ANIMATION_MANAGER HashMap, now we use a
String key. For window move animation, the key would be
`window_move:{hwnd}`.

This allows us to use single manager for more types of animations.

67b2a7a
feat(animation): introduce `AnimationPrefix` enum

8290f14
feat(animation): introduce `RenderDispatcher` trait

2400d75
feat(animation): implement window transparency animation

This commit also fixes graceful shutdown of animations by disabling them
before exit and wait for all remaining animations for 20 seconds.

44189d8
refactor(animation): move generation of `animation key` to `RenderDispatcher`

e502cb3
refactor(animation): rename `animation` mod to `engine`

Linter was upset about this:
> error: module has the same name as its containing module

369107f
feat(config): adds per animation configuration options

Originally static config only allowed global config for animations.

Since this refactor introduces the abilty to add more type of
animations, this change allows us to configure `enabled`, `duration` and
`style` state per animation type.

Now each of them take either the raw value or a JSON object where keys
are the animation types and values are desired config value. Also adds
support for per animation configuration for komorebic commands.
  • Loading branch information
thearturca authored and LGUG2Z committed Nov 26, 2024
1 parent 1d00196 commit 414860c
Show file tree
Hide file tree
Showing 17 changed files with 764 additions and 347 deletions.
1 change: 1 addition & 0 deletions komorebi-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]

pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
Expand Down
115 changes: 115 additions & 0 deletions komorebi/src/animation/animation_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;

use super::prefix::AnimationPrefix;

#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}

#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<String, AnimationState>,
}

impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}

impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}

pub fn is_cancelled(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.pending_cancel_count > 0
} else {
false
}
}

pub fn in_progress(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.in_progress
} else {
false
}
}

pub fn init_cancel(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;

// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}

pub fn latest_cancel_idx(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.cancel_idx_counter
} else {
0
}
}

pub fn end_cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count -= 1;
}
}

pub fn cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
}
}

pub fn start(&mut self, animation_key: &str) {
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});

return;
}

if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = true;
}
}

pub fn end(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;

if animation_state.pending_cancel_count == 0 {
self.animations.remove(animation_key);
}
}
}

pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
self.animations
.keys()
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
.count()
}

pub fn count(&self) -> usize {
self.animations.len()
}
}
122 changes: 122 additions & 0 deletions komorebi/src/animation/engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use color_eyre::Result;

use schemars::JsonSchema;

use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;

use super::RenderDispatcher;
use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER;

#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AnimationEngine;

impl AnimationEngine {
pub fn wait_for_all_animations() {
let max_duration = Duration::from_secs(20);
let spent_duration = Instant::now();

while ANIMATION_MANAGER.lock().count() > 0 {
if spent_duration.elapsed() >= max_duration {
break;
}

std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
));
}
}

/// Returns true if the animation needs to continue
pub fn cancel(animation_key: &str) -> bool {
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
let max_duration = Duration::from_secs(5);
let spent_duration = Instant::now();

while ANIMATION_MANAGER.lock().in_progress(animation_key) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(animation_key);
}

std::thread::sleep(Duration::from_millis(250 / 2));
}

let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);

ANIMATION_MANAGER.lock().end_cancel(animation_key);

latest_cancel_idx == cancel_idx
}

#[allow(clippy::cast_precision_loss)]
pub fn animate(
render_dispatcher: (impl RenderDispatcher + Send + 'static),
duration: Duration,
) -> Result<()> {
std::thread::spawn(move || {
let animation_key = render_dispatcher.get_animation_key();
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
let should_animate = Self::cancel(animation_key.as_str());

if !should_animate {
return Ok(());
}
}

render_dispatcher.pre_render()?;

ANIMATION_MANAGER.lock().start(animation_key.as_str());

let target_frame_time =
Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();

// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER
.lock()
.is_cancelled(animation_key.as_str())
{
// cancel animation
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
return Ok(());
}

let frame_start = Instant::now();
// calculate progress
progress =
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_dispatcher.render(progress).ok();

// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();

if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}

ANIMATION_MANAGER.lock().end(animation_key.as_str());

// limit progress to 1.0 if animation took longer
if progress != 1.0 {
progress = 1.0;

// process animation for 1.0 to set target position
render_dispatcher.render(progress).ok();
}

render_dispatcher.post_render()
});

Ok(())
}
}
42 changes: 42 additions & 0 deletions komorebi/src/animation/lerp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::core::Rect;
use crate::AnimationStyle;

use super::style::apply_ease_func;

pub trait Lerp<T = Self> {
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
}

impl Lerp for i32 {
#[allow(clippy::cast_possible_truncation)]
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
let time = apply_ease_func(time, style);

f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
}
}

impl Lerp for f64 {
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
let time = apply_ease_func(time, style);

(end - self).mul_add(time, self)
}
}

impl Lerp for u8 {
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
(self as f64).lerp(end as f64, time, style) as u8
}
}

impl Lerp for Rect {
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
Rect {
left: self.left.lerp(end.left, time, style),
top: self.top.lerp(end.top, time, style),
right: self.right.lerp(end.right, time, style),
bottom: self.bottom.lerp(end.bottom, time, style),
}
}
}
54 changes: 54 additions & 0 deletions komorebi/src/animation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::animation::animation_manager::AnimationManager;
use crate::core::animation::AnimationStyle;

use lazy_static::lazy_static;
use prefix::AnimationPrefix;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;

use parking_lot::Mutex;

pub use engine::AnimationEngine;
pub mod animation_manager;
pub mod engine;
pub mod lerp;
pub mod prefix;
pub mod render_dispatcher;
pub use render_dispatcher::RenderDispatcher;
pub mod style;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;

#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum PerAnimationPrefixConfig<T> {
Prefix(HashMap<AnimationPrefix, T>),
Global(T),
}

pub const DEFAULT_ANIMATION_ENABLED: bool = false;
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
pub const DEFAULT_ANIMATION_FPS: u64 = 60;

lazy_static! {
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
Arc::new(Mutex::new(HashMap::new()));
}

pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);
31 changes: 31 additions & 0 deletions komorebi/src/animation/prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;

#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum AnimationPrefix {
WindowMove,
WindowTransparency,
}

pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{}:{}", prefix, key)
}
8 changes: 8 additions & 0 deletions komorebi/src/animation/render_dispatcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use color_eyre::Result;

pub trait RenderDispatcher {
fn get_animation_key(&self) -> String;
fn pre_render(&self) -> Result<()>;
fn render(&self, delta: f64) -> Result<()>;
fn post_render(&self) -> Result<()>;
}
Loading

0 comments on commit 414860c

Please sign in to comment.