From 4e5f053c18f84bd93654ec27c35f869da7bf66d9 Mon Sep 17 00:00:00 2001 From: Victoria Fischer Date: Mon, 9 Mar 2026 10:29:40 +0100 Subject: [PATCH] animation: move a few more steps towards a real generic animation framework --- src/animation.rs | 201 ++++++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 82 deletions(-) diff --git a/src/animation.rs b/src/animation.rs index da73d72..5e36d39 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,8 +1,7 @@ use embassy_time::{Duration, Instant, Timer}; -use esp_rtos::CurrentThreadHandle; use figments::{liber8tion::interpolate::Fract8, surface::Surface}; use figments_render::output::Brightness; -use core::{fmt::Debug, mem::MaybeUninit, ops::{Deref, DerefMut}}; +use core::{fmt::Debug, ops::{Deref, DerefMut}}; use log::*; use crate::graphics::display::DisplayControls; @@ -53,6 +52,10 @@ impl AnimationActor for AnimatedSurface { } } +trait Tickable { + fn tick(&mut self) -> TickResult; +} + struct Slot<'a, T> { from: T, to: T, @@ -62,6 +65,38 @@ struct Slot<'a, T> { target: &'a mut dyn AnimationActor } +enum TickResult { + Finished, + Continue +} + +impl<'a, T> Slot<'a, T> { + fn is_valid(&self) -> bool { + self.step_time.as_ticks() != 0 + } +} + +impl<'a> Tickable for Slot<'a, Fract8> { + /// Advances the animation by one tick, and then returns whether or not the animation should continue or not + fn tick(&mut self) -> TickResult { + self.next_update += self.step_time; + self.cur_step = if self.to > self.from { + self.cur_step + Fract8::from_raw(1) + } else { + self.cur_step - Fract8::from_raw(1) + }; + + self.target.set_value(self.cur_step); + + if (self.to > self.from && self.cur_step >= self.to) || + (self.to <= self.from && self.cur_step <= self.to) { + TickResult::Finished + } else { + TickResult::Continue + } + } +} + impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Slot") @@ -73,7 +108,14 @@ impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> { } } -impl Animation { +struct Animator<'a, T, const ACTOR_COUNT: usize> { + animators: [Slot<'a, T>; ACTOR_COUNT] +} + +impl<'a, T, const ACTOR_COUNT: usize> Animator<'a, T, ACTOR_COUNT> { +} + +impl Animation { pub const fn new() -> Self { Self { from: None, @@ -82,93 +124,21 @@ impl Animation { } } - pub const fn from(self, from: Fract8) -> Self { - Self { - from: Some(from), - ..self - } - } - - pub const fn to(self, to: Fract8) -> Self { - Self { - to: Some(to), - ..self - } - } - - pub const fn duration(self, duration: Duration) -> Self { - Self { - duration, - ..self - } - } -} - - -const MIN_ANIMATION_RATE: Duration = Duration::from_millis(5); - -impl Animation { - pub async fn apply(&self, actors: [&mut dyn AnimationActor; ACTOR_COUNT]) { - - let mut now = Instant::now(); - trace!("start now={now:?} ACTOR_COUNT={ACTOR_COUNT}"); - - let mut actors = actors.into_iter(); - let mut animators: [Slot; ACTOR_COUNT] = core::array::from_fn(|_| { - let target = actors.next().unwrap(); - let from = if let Some(val) = self.from { - val - } else { - target.get_value() - }; - let to = if let Some(val) = self.to { - val - } else { - target.get_value() - }; - let steps = to.abs_diff(from); - - let step_time = if steps == Fract8::MIN { - // Zero ticks is an 'invalid' animator that shouldn't get processed because start == end already - Duration::from_ticks(0) - } else { - // FIXME: if the resulting duration is less than the animation rate, we also need to re-scale the number added to animator.cur_step further down below. Otherwise a 0-255 animation with a 100ms duration actually ends up running for 255ms - (self.duration / steps.to_raw().into()).max(MIN_ANIMATION_RATE) - }; - Slot { - from, - to, - cur_step: from, - step_time, - next_update: now, - target - } - }); - - trace!("animators={animators:?}"); - + async fn execute<'a, const ACTOR_COUNT: usize>(mut animators: [Slot<'a, T>; ACTOR_COUNT]) where Slot<'a, T>: Tickable { + let mut now: Instant = Instant::now(); loop { // Find the next shortest delay let mut next_keyframe_time = animators[0].next_update; let mut finished = false; for animator in &mut animators { - if animator.step_time.as_ticks() == 0 { + if !animator.is_valid() { continue; } if animator.next_update <= now { - animator.next_update += animator.step_time; - animator.cur_step = if animator.to > animator.from { - animator.cur_step + Fract8::from_raw(1) - } else { - animator.cur_step - Fract8::from_raw(1) - }; - - if (animator.to > animator.from && animator.cur_step >= animator.to) || - (animator.to <= animator.from && animator.cur_step <= animator.to) { - finished = true; + finished = match animator.tick() { + TickResult::Finished => true, + TickResult::Continue => finished } - - animator.target.set_value(animator.cur_step); } if next_keyframe_time <= now || animator.next_update < next_keyframe_time { @@ -185,8 +155,75 @@ impl Animation { Timer::after(keyframe_delay).await; now += keyframe_delay; } + } +} - trace!("finished animators={animators:?}"); +impl Animation { + pub const fn duration(self, duration: Duration) -> Self { + Self { + duration, + ..self + } + } + + pub const fn from(self, from: Fract8) -> Self { + Self { + from: Some(from), + ..self + } + } + + pub const fn to(self, to: Fract8) -> Self { + Self { + to: Some(to), + ..self + } + } +} + +const MIN_ANIMATION_RATE: Duration = Duration::from_millis(5); + +impl Animation { + pub async fn apply(&self, actors: [&mut dyn AnimationActor; ACTOR_COUNT]) { + + let now = Instant::now(); + trace!("start now={now:?} ACTOR_COUNT={ACTOR_COUNT}"); + + let mut actors = actors.into_iter(); + let animators: [Slot; ACTOR_COUNT] = core::array::from_fn(|_| { + let target = actors.next().unwrap(); + let from = if let Some(val) = self.from { + val + } else { + target.get_value() + }; + let to = if let Some(val) = self.to { + val + } else { + target.get_value() + }; + + let step_time = if to == from { + // Zero ticks is an 'invalid' animator that shouldn't get processed because start == end already + Duration::from_ticks(0) + } else { + let steps = to.abs_diff(from); + // FIXME: if the resulting duration is less than the animation rate, we also need to re-scale the number added to animator.cur_step further down below. Otherwise a 0-255 animation with a 100ms duration actually ends up running for 255ms + (self.duration / steps.to_raw().into()).max(MIN_ANIMATION_RATE) + }; + Slot { + from, + to, + cur_step: from, + step_time, + next_update: now, + target + } + }); + + trace!("animators={animators:?}"); + + Self::execute(animators).await; } }