animation: move a few more steps towards a real generic animation framework

This commit is contained in:
2026-03-09 10:29:40 +01:00
parent 49c201add7
commit 4e5f053c18

View File

@@ -1,8 +1,7 @@
use embassy_time::{Duration, Instant, Timer}; use embassy_time::{Duration, Instant, Timer};
use esp_rtos::CurrentThreadHandle;
use figments::{liber8tion::interpolate::Fract8, surface::Surface}; use figments::{liber8tion::interpolate::Fract8, surface::Surface};
use figments_render::output::Brightness; use figments_render::output::Brightness;
use core::{fmt::Debug, mem::MaybeUninit, ops::{Deref, DerefMut}}; use core::{fmt::Debug, ops::{Deref, DerefMut}};
use log::*; use log::*;
use crate::graphics::display::DisplayControls; use crate::graphics::display::DisplayControls;
@@ -53,6 +52,10 @@ impl<S: Surface> AnimationActor<Fract8> for AnimatedSurface<S> {
} }
} }
trait Tickable {
fn tick(&mut self) -> TickResult;
}
struct Slot<'a, T> { struct Slot<'a, T> {
from: T, from: T,
to: T, to: T,
@@ -62,6 +65,38 @@ struct Slot<'a, T> {
target: &'a mut dyn AnimationActor<T> target: &'a mut dyn AnimationActor<T>
} }
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> { impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Slot") f.debug_struct("Slot")
@@ -73,7 +108,14 @@ impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> {
} }
} }
impl Animation<Fract8> { 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<T> Animation<T> {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
from: None, from: None,
@@ -82,93 +124,21 @@ impl Animation<Fract8> {
} }
} }
pub const fn from(self, from: Fract8) -> Self { async fn execute<'a, const ACTOR_COUNT: usize>(mut animators: [Slot<'a, T>; ACTOR_COUNT]) where Slot<'a, T>: Tickable {
Self { let mut now: Instant = Instant::now();
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<Fract8> {
pub async fn apply<const ACTOR_COUNT: usize>(&self, actors: [&mut dyn AnimationActor<Fract8>; 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<Fract8>; 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:?}");
loop { loop {
// Find the next shortest delay // Find the next shortest delay
let mut next_keyframe_time = animators[0].next_update; let mut next_keyframe_time = animators[0].next_update;
let mut finished = false; let mut finished = false;
for animator in &mut animators { for animator in &mut animators {
if animator.step_time.as_ticks() == 0 { if !animator.is_valid() {
continue; continue;
} }
if animator.next_update <= now { if animator.next_update <= now {
animator.next_update += animator.step_time; finished = match animator.tick() {
animator.cur_step = if animator.to > animator.from { TickResult::Finished => true,
animator.cur_step + Fract8::from_raw(1) TickResult::Continue => finished
} 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;
} }
animator.target.set_value(animator.cur_step);
} }
if next_keyframe_time <= now || animator.next_update < next_keyframe_time { if next_keyframe_time <= now || animator.next_update < next_keyframe_time {
@@ -185,8 +155,75 @@ impl Animation<Fract8> {
Timer::after(keyframe_delay).await; Timer::after(keyframe_delay).await;
now += keyframe_delay; now += keyframe_delay;
} }
}
}
trace!("finished animators={animators:?}"); impl Animation<Fract8> {
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<Fract8> {
pub async fn apply<const ACTOR_COUNT: usize>(&self, actors: [&mut dyn AnimationActor<Fract8>; ACTOR_COUNT]) {
let now = Instant::now();
trace!("start now={now:?} ACTOR_COUNT={ACTOR_COUNT}");
let mut actors = actors.into_iter();
let animators: [Slot<Fract8>; 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;
} }
} }