use crate::easing::Easing;
use crate::keyframe::{Keyframe, KeyframeAnimation};
use crate::spring::{Spring, SpringConfig};
use crate::timeline::Timeline;
use blinc_core::AnimationAccess;
use slotmap::{new_key_type, SlotMap};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock, Weak};
use std::thread::JoinHandle;
use web_time::Instant;
#[cfg(not(target_arch = "wasm32"))]
use std::thread;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
static GLOBAL_SCHEDULER: OnceLock<SchedulerHandle> = OnceLock::new();
pub fn set_global_scheduler(handle: SchedulerHandle) {
if GLOBAL_SCHEDULER.set(handle).is_err() {
panic!("set_global_scheduler() called more than once");
}
}
pub fn get_scheduler() -> SchedulerHandle {
GLOBAL_SCHEDULER
.get()
.expect("Animation scheduler not initialized. Call set_global_scheduler() at app startup.")
.clone()
}
pub fn try_get_scheduler() -> Option<SchedulerHandle> {
GLOBAL_SCHEDULER.get().cloned()
}
pub fn is_scheduler_initialized() -> bool {
GLOBAL_SCHEDULER.get().is_some()
}
new_key_type! {
pub struct SpringId;
pub struct KeyframeId;
pub struct TimelineId;
}
impl SpringId {
pub fn to_raw(self) -> u64 {
self.0.as_ffi()
}
pub fn from_raw(raw: u64) -> Self {
SpringId::from(slotmap::KeyData::from_ffi(raw))
}
}
pub type TickCallback = Arc<Mutex<dyn FnMut(f32) + Send + Sync>>;
new_key_type! {
pub struct TickCallbackId;
}
impl TickCallbackId {
pub fn to_raw(self) -> u64 {
self.0.as_ffi()
}
pub fn from_raw(raw: u64) -> Self {
TickCallbackId::from(slotmap::KeyData::from_ffi(raw))
}
}
struct SchedulerInner {
springs: SlotMap<SpringId, Spring>,
keyframes: SlotMap<KeyframeId, KeyframeAnimation>,
timelines: SlotMap<TimelineId, Timeline>,
tick_callbacks: SlotMap<TickCallbackId, TickCallback>,
last_frame: Instant,
target_fps: u32,
}
#[cfg(not(target_arch = "wasm32"))]
pub type WakeCallback = Arc<dyn Fn() + Send + Sync>;
#[cfg(target_arch = "wasm32")]
pub type WakeCallback = Arc<dyn Fn()>;
pub struct AnimationScheduler {
inner: Arc<Mutex<SchedulerInner>>,
stop_flag: Arc<AtomicBool>,
needs_redraw: Arc<AtomicBool>,
continuous_redraw: Arc<AtomicBool>,
thread_handle: Option<JoinHandle<()>>,
wake_callback: Option<WakeCallback>,
}
#[cfg(target_arch = "wasm32")]
unsafe impl Send for AnimationScheduler {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for AnimationScheduler {}
impl AnimationScheduler {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(SchedulerInner {
springs: SlotMap::with_key(),
keyframes: SlotMap::with_key(),
timelines: SlotMap::with_key(),
tick_callbacks: SlotMap::with_key(),
last_frame: Instant::now(),
target_fps: 120,
})),
stop_flag: Arc::new(AtomicBool::new(false)),
needs_redraw: Arc::new(AtomicBool::new(false)),
continuous_redraw: Arc::new(AtomicBool::new(false)),
thread_handle: None,
wake_callback: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_wake_callback<F>(&mut self, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
self.wake_callback = Some(Arc::new(callback));
}
#[cfg(target_arch = "wasm32")]
pub fn set_wake_callback<F>(&mut self, callback: F)
where
F: Fn() + 'static,
{
self.wake_callback = Some(Arc::new(callback));
}
fn tick_frame_inner(
inner: &Arc<Mutex<SchedulerInner>>,
needs_redraw: &Arc<AtomicBool>,
wants_continuous: bool,
wake_callback: Option<&WakeCallback>,
) -> (bool, f32) {
let (has_active, tick_callbacks_to_call, dt) = {
let mut inner = inner.lock().unwrap();
let now = Instant::now();
let dt = (now - inner.last_frame).as_secs_f32();
let dt_ms = dt * 1000.0;
inner.last_frame = now;
for (_, spring) in inner.springs.iter_mut() {
spring.step(dt);
}
for (_, keyframe) in inner.keyframes.iter_mut() {
keyframe.tick(dt_ms);
}
for (_, timeline) in inner.timelines.iter_mut() {
timeline.tick(dt_ms);
}
let callbacks: Vec<_> = inner
.tick_callbacks
.iter()
.map(|(_, cb)| Arc::clone(cb))
.collect();
let has_active = inner.springs.iter().any(|(_, s)| !s.is_settled())
|| inner.keyframes.iter().any(|(_, k)| k.is_playing())
|| inner.timelines.iter().any(|(_, t)| t.is_playing())
|| !inner.tick_callbacks.is_empty();
(has_active, callbacks, dt)
};
for callback in tick_callbacks_to_call {
if let Ok(mut cb) = callback.lock() {
cb(dt);
}
}
if has_active || wants_continuous {
needs_redraw.store(true, Ordering::Release);
if let Some(callback) = wake_callback {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
if count % 120 == 0 {
tracing::debug!(
"Animation tick: waking driver (continuous={}, active={})",
wants_continuous,
has_active
);
}
callback();
}
}
(has_active || wants_continuous, dt)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn start_background(&mut self) {
if self.thread_handle.is_some() {
return; }
let inner = Arc::clone(&self.inner);
let stop_flag = Arc::clone(&self.stop_flag);
let needs_redraw = Arc::clone(&self.needs_redraw);
let continuous_redraw = Arc::clone(&self.continuous_redraw);
let wake_callback = self.wake_callback.clone();
self.thread_handle = Some(thread::spawn(move || {
let frame_duration = Duration::from_micros(1_000_000 / 120);
while !stop_flag.load(Ordering::Relaxed) {
let start = Instant::now();
let wants_continuous = continuous_redraw.load(Ordering::Relaxed);
Self::tick_frame_inner(
&inner,
&needs_redraw,
wants_continuous,
wake_callback.as_ref(),
);
let elapsed = start.elapsed();
if elapsed < frame_duration {
thread::sleep(frame_duration - elapsed);
}
}
}));
}
#[cfg(not(target_arch = "wasm32"))]
pub fn stop_background(&mut self) {
self.stop_flag.store(true, Ordering::Relaxed);
if let Some(handle) = self.thread_handle.take() {
let _ = handle.join();
}
self.stop_flag.store(false, Ordering::Relaxed);
}
#[cfg(target_arch = "wasm32")]
pub fn start_raf(&self) {
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
let window = web_sys::window().expect("AnimationScheduler::start_raf needs `window`");
let inner = Arc::clone(&self.inner);
let needs_redraw = Arc::clone(&self.needs_redraw);
let continuous_redraw = Arc::clone(&self.continuous_redraw);
let wake_callback = self.wake_callback.clone();
#[allow(clippy::type_complexity)]
let closure_cell: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
let closure_cell_for_init = Rc::clone(&closure_cell);
let window_for_closure = window.clone();
*closure_cell_for_init.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let wants_continuous = continuous_redraw.load(Ordering::Relaxed);
Self::tick_frame_inner(
&inner,
&needs_redraw,
wants_continuous,
wake_callback.as_ref(),
);
if let Some(ref c) = *closure_cell.borrow() {
let _ = window_for_closure.request_animation_frame(c.as_ref().unchecked_ref());
}
}) as Box<dyn FnMut()>));
if let Some(ref c) = *closure_cell_for_init.borrow() {
let _ = window.request_animation_frame(c.as_ref().unchecked_ref());
}
std::mem::forget(closure_cell_for_init);
}
pub fn is_background_running(&self) -> bool {
self.thread_handle.is_some()
}
pub fn take_needs_redraw(&self) -> bool {
self.needs_redraw.swap(false, Ordering::Acquire)
}
pub fn request_redraw(&self) {
self.needs_redraw.store(true, Ordering::Release);
}
pub fn set_continuous_redraw(&self, enabled: bool) {
tracing::debug!("AnimationScheduler: set_continuous_redraw({})", enabled);
self.continuous_redraw.store(enabled, Ordering::Release);
}
pub fn is_continuous_redraw(&self) -> bool {
self.continuous_redraw.load(Ordering::Relaxed)
}
pub fn handle(&self) -> SchedulerHandle {
SchedulerHandle {
inner: Arc::downgrade(&self.inner),
needs_redraw: Arc::clone(&self.needs_redraw),
}
}
pub fn set_target_fps(&mut self, fps: u32) {
self.inner.lock().unwrap().target_fps = fps;
}
pub fn tick(&self) -> bool {
let mut inner = self.inner.lock().unwrap();
let now = Instant::now();
let dt = (now - inner.last_frame).as_secs_f32();
let dt_ms = dt * 1000.0;
inner.last_frame = now;
for (_, spring) in inner.springs.iter_mut() {
spring.step(dt);
}
for (_, keyframe) in inner.keyframes.iter_mut() {
keyframe.tick(dt_ms);
}
for (_, timeline) in inner.timelines.iter_mut() {
timeline.tick(dt_ms);
}
inner.springs.iter().any(|(_, s)| !s.is_settled())
|| inner.keyframes.iter().any(|(_, k)| k.is_playing())
|| inner.timelines.iter().any(|(_, t)| t.is_playing())
}
pub fn has_active_animations(&self) -> bool {
let inner = self.inner.lock().unwrap();
inner.springs.iter().any(|(_, s)| !s.is_settled())
|| inner.keyframes.iter().any(|(_, k)| k.is_playing())
|| inner.timelines.iter().any(|(_, t)| t.is_playing())
}
pub fn spring_count(&self) -> usize {
self.inner.lock().unwrap().springs.len()
}
pub fn keyframe_count(&self) -> usize {
self.inner.lock().unwrap().keyframes.len()
}
pub fn timeline_count(&self) -> usize {
self.inner.lock().unwrap().timelines.len()
}
pub fn add_spring(&self, spring: Spring) -> SpringId {
self.inner.lock().unwrap().springs.insert(spring)
}
pub fn get_spring(&self, id: SpringId) -> Option<Spring> {
self.inner.lock().unwrap().springs.get(id).copied()
}
pub fn with_spring_mut<F, R>(&self, id: SpringId, f: F) -> Option<R>
where
F: FnOnce(&mut Spring) -> R,
{
self.inner.lock().unwrap().springs.get_mut(id).map(f)
}
pub fn get_spring_value(&self, id: SpringId) -> Option<f32> {
self.inner
.lock()
.unwrap()
.springs
.get(id)
.map(|s| s.value())
}
pub fn set_spring_target(&self, id: SpringId, target: f32) {
if let Some(spring) = self.inner.lock().unwrap().springs.get_mut(id) {
spring.set_target(target);
}
}
pub fn remove_spring(&self, id: SpringId) -> Option<Spring> {
self.inner.lock().unwrap().springs.remove(id)
}
pub fn springs_iter_mut(&self) -> SpringsIterMut<'_> {
SpringsIterMut {
guard: self.inner.lock().unwrap(),
}
}
pub fn add_keyframe(&self, keyframe: KeyframeAnimation) -> KeyframeId {
self.inner.lock().unwrap().keyframes.insert(keyframe)
}
pub fn get_keyframe_value(&self, id: KeyframeId) -> Option<f32> {
self.inner
.lock()
.unwrap()
.keyframes
.get(id)
.map(|k| k.value())
}
pub fn start_keyframe(&self, id: KeyframeId) {
if let Some(keyframe) = self.inner.lock().unwrap().keyframes.get_mut(id) {
keyframe.start();
}
}
pub fn stop_keyframe(&self, id: KeyframeId) {
if let Some(keyframe) = self.inner.lock().unwrap().keyframes.get_mut(id) {
keyframe.stop();
}
}
pub fn remove_keyframe(&self, id: KeyframeId) -> Option<KeyframeAnimation> {
self.inner.lock().unwrap().keyframes.remove(id)
}
pub fn add_timeline(&self, timeline: Timeline) -> TimelineId {
self.inner.lock().unwrap().timelines.insert(timeline)
}
pub fn start_timeline(&self, id: TimelineId) {
if let Some(timeline) = self.inner.lock().unwrap().timelines.get_mut(id) {
timeline.start();
}
}
pub fn stop_timeline(&self, id: TimelineId) {
if let Some(timeline) = self.inner.lock().unwrap().timelines.get_mut(id) {
timeline.stop();
}
}
pub fn remove_timeline(&self, id: TimelineId) -> Option<Timeline> {
self.inner.lock().unwrap().timelines.remove(id)
}
pub fn add_tick_callback<F>(&self, callback: F) -> TickCallbackId
where
F: FnMut(f32) + Send + Sync + 'static,
{
self.inner
.lock()
.unwrap()
.tick_callbacks
.insert(Arc::new(Mutex::new(callback)))
}
pub fn remove_tick_callback(&self, id: TickCallbackId) {
self.inner.lock().unwrap().tick_callbacks.remove(id);
}
pub fn tick_callback_count(&self) -> usize {
self.inner.lock().unwrap().tick_callbacks.len()
}
}
impl Default for AnimationScheduler {
fn default() -> Self {
Self::new()
}
}
pub struct SpringsIterMut<'a> {
guard: std::sync::MutexGuard<'a, SchedulerInner>,
}
impl SpringsIterMut<'_> {
pub fn for_each<F>(&mut self, mut f: F)
where
F: FnMut(SpringId, &mut Spring),
{
for (id, spring) in self.guard.springs.iter_mut() {
f(id, spring);
}
}
}
impl<'a> IntoIterator for &'a mut SpringsIterMut<'_> {
type Item = (SpringId, &'a mut Spring);
type IntoIter = slotmap::basic::IterMut<'a, SpringId, Spring>;
fn into_iter(self) -> Self::IntoIter {
self.guard.springs.iter_mut()
}
}
impl Clone for AnimationScheduler {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
stop_flag: Arc::clone(&self.stop_flag),
needs_redraw: Arc::clone(&self.needs_redraw),
continuous_redraw: Arc::clone(&self.continuous_redraw),
thread_handle: None,
wake_callback: self.wake_callback.clone(),
}
}
}
impl Drop for AnimationScheduler {
fn drop(&mut self) {
#[cfg(not(target_arch = "wasm32"))]
self.stop_background();
}
}
impl AnimationAccess for AnimationScheduler {
fn get_spring_value(&self, id: u64, generation: u32) -> Option<f32> {
let key_data = slotmap::KeyData::from_ffi((id as u32 as u64) | ((generation as u64) << 32));
let spring_id = SpringId::from(key_data);
self.inner
.lock()
.unwrap()
.springs
.get(spring_id)
.map(|s| s.value())
}
fn get_keyframe_value(&self, id: u64) -> Option<f32> {
let key_data = slotmap::KeyData::from_ffi(id);
let keyframe_id = KeyframeId::from(key_data);
self.inner
.lock()
.unwrap()
.keyframes
.get(keyframe_id)
.map(|k| k.value())
}
fn get_timeline_value(&self, timeline_id: u64, _property: &str) -> Option<f32> {
let key_data = slotmap::KeyData::from_ffi(timeline_id);
let tid = TimelineId::from(key_data);
self.inner.lock().unwrap().timelines.get(tid).map(|_t| 0.0) }
}
#[derive(Clone)]
pub struct SchedulerHandle {
inner: Weak<Mutex<SchedulerInner>>,
needs_redraw: Arc<AtomicBool>,
}
impl SchedulerHandle {
pub fn request_redraw(&self) {
self.needs_redraw.store(true, Ordering::Release);
}
pub fn register_spring(&self, spring: Spring) -> Option<SpringId> {
self.inner.upgrade().map(|inner| {
let mut guard = inner.lock().unwrap();
guard.last_frame = Instant::now();
guard.springs.insert(spring)
})
}
pub fn set_spring_target(&self, id: SpringId, target: f32) {
if let Some(inner) = self.inner.upgrade() {
if let Some(spring) = inner.lock().unwrap().springs.get_mut(id) {
spring.set_target(target);
}
}
}
pub fn get_spring_value(&self, id: SpringId) -> Option<f32> {
self.inner
.upgrade()
.and_then(|inner| inner.lock().unwrap().springs.get(id).map(|s| s.value()))
}
pub fn is_spring_settled(&self, id: SpringId) -> bool {
self.inner
.upgrade()
.and_then(|inner| {
inner
.lock()
.unwrap()
.springs
.get(id)
.map(|s| s.is_settled())
})
.unwrap_or(true) }
pub fn remove_spring(&self, id: SpringId) {
if let Some(inner) = self.inner.upgrade() {
inner.lock().unwrap().springs.remove(id);
}
}
pub fn pause_spring(&self, id: SpringId) {
if let Some(inner) = self.inner.upgrade() {
if let Some(spring) = inner.lock().unwrap().springs.get_mut(id) {
spring.pause();
}
}
}
pub fn resume_spring(&self, id: SpringId) {
if let Some(inner) = self.inner.upgrade() {
if let Some(spring) = inner.lock().unwrap().springs.get_mut(id) {
spring.resume();
}
}
}
pub fn register_keyframe(&self, keyframe: KeyframeAnimation) -> Option<KeyframeId> {
self.inner
.upgrade()
.map(|inner| inner.lock().unwrap().keyframes.insert(keyframe))
}
pub fn get_keyframe_value(&self, id: KeyframeId) -> Option<f32> {
self.inner
.upgrade()
.and_then(|inner| inner.lock().unwrap().keyframes.get(id).map(|k| k.value()))
}
pub fn get_keyframe_progress(&self, id: KeyframeId) -> Option<f32> {
self.inner.upgrade().and_then(|inner| {
inner
.lock()
.unwrap()
.keyframes
.get(id)
.map(|k| k.progress())
})
}
pub fn is_keyframe_playing(&self, id: KeyframeId) -> bool {
self.inner
.upgrade()
.and_then(|inner| {
inner
.lock()
.unwrap()
.keyframes
.get(id)
.map(|k| k.is_playing())
})
.unwrap_or(false)
}
pub fn start_keyframe(&self, id: KeyframeId) {
if let Some(inner) = self.inner.upgrade() {
if let Some(keyframe) = inner.lock().unwrap().keyframes.get_mut(id) {
keyframe.start();
}
}
}
pub fn stop_keyframe(&self, id: KeyframeId) {
if let Some(inner) = self.inner.upgrade() {
if let Some(keyframe) = inner.lock().unwrap().keyframes.get_mut(id) {
keyframe.stop();
}
}
}
pub fn remove_keyframe(&self, id: KeyframeId) {
if let Some(inner) = self.inner.upgrade() {
inner.lock().unwrap().keyframes.remove(id);
}
}
pub fn register_timeline(&self, timeline: Timeline) -> Option<TimelineId> {
self.inner
.upgrade()
.map(|inner| inner.lock().unwrap().timelines.insert(timeline))
}
pub fn is_timeline_playing(&self, id: TimelineId) -> bool {
self.inner
.upgrade()
.and_then(|inner| {
inner
.lock()
.unwrap()
.timelines
.get(id)
.map(|t| t.is_playing())
})
.unwrap_or(false)
}
pub fn start_timeline(&self, id: TimelineId) {
if let Some(inner) = self.inner.upgrade() {
if let Some(timeline) = inner.lock().unwrap().timelines.get_mut(id) {
timeline.start();
}
}
}
pub fn stop_timeline(&self, id: TimelineId) {
if let Some(inner) = self.inner.upgrade() {
if let Some(timeline) = inner.lock().unwrap().timelines.get_mut(id) {
timeline.stop();
}
}
}
pub fn remove_timeline(&self, id: TimelineId) {
if let Some(inner) = self.inner.upgrade() {
inner.lock().unwrap().timelines.remove(id);
}
}
pub fn with_timeline<F, R>(&self, id: TimelineId, f: F) -> Option<R>
where
F: FnOnce(&mut Timeline) -> R,
{
self.inner
.upgrade()
.and_then(|inner| inner.lock().unwrap().timelines.get_mut(id).map(f))
}
pub fn is_alive(&self) -> bool {
self.inner.strong_count() > 0
}
pub fn register_tick_callback<F>(&self, callback: F) -> Option<TickCallbackId>
where
F: FnMut(f32) + Send + Sync + 'static,
{
self.inner.upgrade().map(|inner| {
inner
.lock()
.unwrap()
.tick_callbacks
.insert(Arc::new(Mutex::new(callback)))
})
}
pub fn remove_tick_callback(&self, id: TickCallbackId) {
if let Some(inner) = self.inner.upgrade() {
inner.lock().unwrap().tick_callbacks.remove(id);
}
}
}
impl AnimationAccess for SchedulerHandle {
fn get_spring_value(&self, id: u64, generation: u32) -> Option<f32> {
self.inner.upgrade().and_then(|inner| {
let key_data =
slotmap::KeyData::from_ffi((id as u32 as u64) | ((generation as u64) << 32));
let spring_id = SpringId::from(key_data);
inner
.lock()
.unwrap()
.springs
.get(spring_id)
.map(|s| s.value())
})
}
fn get_keyframe_value(&self, id: u64) -> Option<f32> {
self.inner.upgrade().and_then(|inner| {
let key_data = slotmap::KeyData::from_ffi(id);
let keyframe_id = KeyframeId::from(key_data);
inner
.lock()
.unwrap()
.keyframes
.get(keyframe_id)
.map(|k| k.value())
})
}
fn get_timeline_value(&self, _timeline_id: u64, _property: &str) -> Option<f32> {
None
}
}
#[derive(Clone)]
pub struct AnimatedValue {
handle: SchedulerHandle,
spring_id: Option<SpringId>,
config: SpringConfig,
current: f32,
target: f32,
}
impl AnimatedValue {
pub fn new(handle: SchedulerHandle, initial: f32, config: SpringConfig) -> Self {
Self {
handle,
spring_id: None,
config,
current: initial,
target: initial,
}
}
pub fn with_default(handle: SchedulerHandle, initial: f32) -> Self {
Self::new(handle, initial, SpringConfig::stiff())
}
pub fn set_target(&mut self, target: f32) {
self.target = target;
if let Some(id) = self.spring_id {
self.handle.set_spring_target(id, target);
} else {
if (target - self.current).abs() > 0.001 {
let spring = Spring::new(self.config, self.current);
if let Some(id) = self.handle.register_spring(spring) {
self.spring_id = Some(id);
self.handle.set_spring_target(id, target);
crate::suspension::register_spring(id);
}
}
}
}
pub fn get(&self) -> f32 {
if let Some(id) = self.spring_id {
self.handle.get_spring_value(id).unwrap_or(self.target)
} else {
self.current
}
}
pub fn set_immediate(&mut self, value: f32) {
if let Some(id) = self.spring_id.take() {
self.handle.remove_spring(id);
}
self.current = value;
self.target = value;
}
pub fn pause(&mut self) {
if let Some(id) = self.spring_id {
self.handle.pause_spring(id);
}
}
pub fn resume(&mut self) {
if let Some(id) = self.spring_id {
self.handle.resume_spring(id);
}
}
pub fn is_animating(&self) -> bool {
if let Some(id) = self.spring_id {
!self.handle.is_spring_settled(id)
} else {
false
}
}
pub fn snap_to_target(&mut self) {
self.set_immediate(self.target);
}
pub fn target(&self) -> f32 {
self.target
}
}
impl Drop for AnimatedValue {
fn drop(&mut self) {
if let Some(id) = self.spring_id {
crate::suspension::unregister_spring(id);
self.handle.remove_spring(id);
}
}
}
#[derive(Clone)]
pub struct AnimatedKeyframe {
handle: SchedulerHandle,
keyframe_id: Option<KeyframeId>,
duration_ms: u32,
keyframes: Vec<Keyframe>,
auto_start: bool,
iterations: i32,
ping_pong: bool,
current_iteration: i32,
reversed: bool,
delay_ms: u32,
start_time: Option<Instant>,
}
impl AnimatedKeyframe {
pub fn new(handle: SchedulerHandle, duration_ms: u32) -> Self {
Self {
handle,
keyframe_id: None,
duration_ms,
keyframes: Vec::new(),
auto_start: false,
iterations: 1, ping_pong: false,
current_iteration: 0,
reversed: false,
delay_ms: 0,
start_time: None,
}
}
pub fn keyframe(mut self, time: f32, value: f32, easing: Easing) -> Self {
self.keyframes.push(Keyframe {
time,
value,
easing,
});
self
}
pub fn auto_start(mut self, auto: bool) -> Self {
self.auto_start = auto;
self
}
pub fn iterations(mut self, count: i32) -> Self {
self.iterations = count;
self
}
pub fn loop_infinite(mut self) -> Self {
self.iterations = -1;
self
}
pub fn ping_pong(mut self, enabled: bool) -> Self {
self.ping_pong = enabled;
self
}
pub fn delay(mut self, delay_ms: u32) -> Self {
self.delay_ms = delay_ms;
self
}
pub fn build(mut self) -> Self {
self.keyframes.sort_by(|a, b| {
a.time
.partial_cmp(&b.time)
.unwrap_or(std::cmp::Ordering::Equal)
});
let animation = KeyframeAnimation::new(self.duration_ms, self.keyframes.clone());
if let Some(id) = self.handle.register_keyframe(animation) {
self.keyframe_id = Some(id);
}
if self.auto_start {
self.start();
}
self
}
pub fn start(&mut self) {
self.current_iteration = 0;
self.reversed = false;
if self.delay_ms > 0 {
self.start_time = Some(Instant::now());
} else {
self.start_time = None;
}
if let Some(id) = self.keyframe_id {
if self.delay_ms == 0 {
self.handle.start_keyframe(id);
}
} else {
self.keyframes.sort_by(|a, b| {
a.time
.partial_cmp(&b.time)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut animation = KeyframeAnimation::new(self.duration_ms, self.keyframes.clone());
if self.delay_ms == 0 {
animation.start();
}
if let Some(id) = self.handle.register_keyframe(animation) {
self.keyframe_id = Some(id);
}
}
}
pub fn stop(&mut self) {
self.start_time = None;
if let Some(id) = self.keyframe_id {
self.handle.stop_keyframe(id);
}
}
pub fn restart(&mut self) {
self.stop();
self.start();
}
fn check_and_update(&mut self) -> bool {
if let Some(start_time) = self.start_time {
let elapsed = start_time.elapsed().as_millis() as u32;
if elapsed < self.delay_ms {
return true; }
self.start_time = None;
if let Some(id) = self.keyframe_id {
self.handle.start_keyframe(id);
}
}
if let Some(id) = self.keyframe_id {
if !self.handle.is_keyframe_playing(id) {
self.current_iteration += 1;
let should_continue =
self.iterations < 0 || self.current_iteration < self.iterations;
if should_continue {
if self.ping_pong {
self.reversed = !self.reversed;
}
self.handle.start_keyframe(id);
return true;
}
} else {
return true; }
}
false
}
pub fn get(&mut self) -> f32 {
self.check_and_update();
if self.start_time.is_some() {
return self.get_initial_value();
}
if let Some(id) = self.keyframe_id {
let raw_value = self.handle.get_keyframe_value(id).unwrap_or(0.0);
if self.reversed && !self.keyframes.is_empty() {
let first = self.keyframes.first().map(|k| k.value).unwrap_or(0.0);
let last = self.keyframes.last().map(|k| k.value).unwrap_or(0.0);
first + last - raw_value
} else {
raw_value
}
} else {
self.get_initial_value()
}
}
fn get_initial_value(&self) -> f32 {
if !self.keyframes.is_empty() {
self.keyframes[0].value
} else {
0.0
}
}
pub fn progress(&self) -> f32 {
if let Some(id) = self.keyframe_id {
let raw_progress = self.handle.get_keyframe_progress(id).unwrap_or(0.0);
if self.reversed {
1.0 - raw_progress
} else {
raw_progress
}
} else {
0.0
}
}
pub fn is_playing(&mut self) -> bool {
if self.start_time.is_some() {
return true;
}
self.check_and_update();
if let Some(id) = self.keyframe_id {
if self.handle.is_keyframe_playing(id) {
return true;
}
self.iterations < 0 || self.current_iteration < self.iterations
} else {
false
}
}
}
impl Drop for AnimatedKeyframe {
fn drop(&mut self) {
if let Some(id) = self.keyframe_id {
self.handle.remove_keyframe(id);
}
}
}
pub trait ConfigureResult {
fn from_entry_ids(ids: &[crate::timeline::TimelineEntryId]) -> Self;
}
impl ConfigureResult for crate::timeline::TimelineEntryId {
fn from_entry_ids(ids: &[crate::timeline::TimelineEntryId]) -> Self {
ids[0]
}
}
impl ConfigureResult
for (
crate::timeline::TimelineEntryId,
crate::timeline::TimelineEntryId,
)
{
fn from_entry_ids(ids: &[crate::timeline::TimelineEntryId]) -> Self {
(ids[0], ids[1])
}
}
impl ConfigureResult
for (
crate::timeline::TimelineEntryId,
crate::timeline::TimelineEntryId,
crate::timeline::TimelineEntryId,
)
{
fn from_entry_ids(ids: &[crate::timeline::TimelineEntryId]) -> Self {
(ids[0], ids[1], ids[2])
}
}
impl ConfigureResult for Vec<crate::timeline::TimelineEntryId> {
fn from_entry_ids(ids: &[crate::timeline::TimelineEntryId]) -> Self {
ids.to_vec()
}
}
pub struct AnimatedTimeline {
handle: SchedulerHandle,
timeline_id: Option<TimelineId>,
}
impl AnimatedTimeline {
pub fn new(handle: SchedulerHandle) -> Self {
let timeline = Timeline::new();
let timeline_id = handle.register_timeline(timeline);
Self {
handle,
timeline_id,
}
}
pub fn configure<T, F>(&mut self, f: F) -> T
where
F: FnOnce(&mut Self) -> T,
T: ConfigureResult,
{
if self.has_entries() {
T::from_entry_ids(&self.entry_ids())
} else {
f(self)
}
}
pub fn add(
&mut self,
offset_ms: i32,
duration_ms: u32,
start_value: f32,
end_value: f32,
) -> crate::timeline::TimelineEntryId {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| {
timeline.add(offset_ms, duration_ms, start_value, end_value)
})
.expect("Timeline should exist")
} else {
panic!("Timeline not registered - scheduler may have been dropped")
}
}
pub fn add_with_easing(
&mut self,
offset_ms: i32,
duration_ms: u32,
start_value: f32,
end_value: f32,
easing: Easing,
) -> crate::timeline::TimelineEntryId {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| {
timeline.add_with_easing(offset_ms, duration_ms, start_value, end_value, easing)
})
.expect("Timeline should exist")
} else {
panic!("Timeline not registered - scheduler may have been dropped")
}
}
pub fn set_loop(&mut self, count: i32) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.set_loop(count);
});
}
}
pub fn set_alternate(&mut self, enabled: bool) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.set_alternate(enabled);
});
}
}
pub fn set_playback_rate(&mut self, rate: f32) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.set_playback_rate(rate);
});
}
}
pub fn start(&self) {
if let Some(id) = self.timeline_id {
self.handle.start_timeline(id);
}
}
pub fn restart(&self) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.start(); });
}
}
pub fn stop(&self) {
if let Some(id) = self.timeline_id {
self.handle.stop_timeline(id);
}
}
pub fn pause(&self) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.pause();
});
}
}
pub fn resume(&self) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.resume();
});
}
}
pub fn reverse(&self) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.reverse();
});
}
}
pub fn seek(&self, time_ms: f32) {
if let Some(id) = self.timeline_id {
self.handle.with_timeline(id, |timeline| {
timeline.seek(time_ms);
});
}
}
pub fn get(&self, entry_id: crate::timeline::TimelineEntryId) -> Option<f32> {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| timeline.value(entry_id))
.flatten()
} else {
None
}
}
pub fn is_playing(&self) -> bool {
if let Some(id) = self.timeline_id {
self.handle.is_timeline_playing(id)
} else {
false
}
}
pub fn progress(&self) -> f32 {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| timeline.progress())
.unwrap_or(0.0)
} else {
0.0
}
}
pub fn entry_progress(&self, entry_id: crate::timeline::TimelineEntryId) -> Option<f32> {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| timeline.entry_progress(entry_id))
.flatten()
} else {
None
}
}
pub fn has_entries(&self) -> bool {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| timeline.entry_count() > 0)
.unwrap_or(false)
} else {
false
}
}
pub fn entry_ids(&self) -> Vec<crate::timeline::TimelineEntryId> {
if let Some(id) = self.timeline_id {
self.handle
.with_timeline(id, |timeline| timeline.entry_ids())
.unwrap_or_default()
} else {
Vec::new()
}
}
}
impl Drop for AnimatedTimeline {
fn drop(&mut self) {
if let Some(id) = self.timeline_id {
self.handle.remove_timeline(id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scheduler_tick() {
let scheduler = AnimationScheduler::new();
let spring = Spring::new(SpringConfig::stiff(), 0.0);
let id = scheduler.add_spring(spring);
scheduler.set_spring_target(id, 100.0);
assert!(scheduler.tick());
let value = scheduler.get_spring_value(id).unwrap();
assert!(value > 0.0);
}
#[test]
fn test_animated_value() {
let scheduler = AnimationScheduler::new();
let handle = scheduler.handle();
let mut value = AnimatedValue::new(handle, 0.0, SpringConfig::stiff());
assert_eq!(value.get(), 0.0);
assert!(!value.is_animating());
value.set_target(100.0);
assert!(value.is_animating());
scheduler.tick();
assert!(value.get() > 0.0);
}
#[test]
fn test_animated_keyframe() {
let scheduler = AnimationScheduler::new();
let handle = scheduler.handle();
let mut anim = AnimatedKeyframe::new(handle, 1000)
.keyframe(0.0, 0.0, Easing::Linear)
.keyframe(1.0, 100.0, Easing::Linear);
anim.start();
assert!(anim.is_playing());
assert_eq!(anim.get(), 0.0);
scheduler.tick();
assert!(anim.is_playing());
}
#[test]
fn test_animated_timeline() {
let scheduler = AnimationScheduler::new();
let handle = scheduler.handle();
let mut timeline = AnimatedTimeline::new(handle);
let entry = timeline.add(0, 1000, 0.0, 100.0);
timeline.start();
assert!(timeline.is_playing());
assert_eq!(timeline.get(entry), Some(0.0));
scheduler.tick();
assert!(timeline.is_playing());
}
#[test]
fn test_handle_weak_reference() {
let handle = {
let scheduler = AnimationScheduler::new();
scheduler.handle()
};
assert!(!handle.is_alive());
assert!(handle
.register_spring(Spring::new(SpringConfig::stiff(), 0.0))
.is_none());
}
#[test]
fn test_scheduler_counts() {
let scheduler = AnimationScheduler::new();
assert_eq!(scheduler.spring_count(), 0);
assert_eq!(scheduler.keyframe_count(), 0);
assert_eq!(scheduler.timeline_count(), 0);
let spring = Spring::new(SpringConfig::stiff(), 0.0);
scheduler.add_spring(spring);
let mut keyframe = KeyframeAnimation::new(
1000,
vec![Keyframe {
time: 0.0,
value: 0.0,
easing: Easing::Linear,
}],
);
keyframe.start();
scheduler.add_keyframe(keyframe);
let mut timeline = Timeline::new();
timeline.add(0, 1000, 0.0, 100.0);
timeline.start();
scheduler.add_timeline(timeline);
assert_eq!(scheduler.spring_count(), 1);
assert_eq!(scheduler.keyframe_count(), 1);
assert_eq!(scheduler.timeline_count(), 1);
}
}