use crate::{
Animation, AnimationError, AnimationState, Easing, Keyframe, Playback, Repeat, RepeatMode,
Spring, Target,
};
use std::{
any::Any,
fmt::Debug,
marker::PhantomData,
sync::atomic::{AtomicU64, Ordering},
time::Duration,
};
pub trait MotionData<const N: usize>: Copy + Debug + PartialEq + 'static {
fn channels(self) -> [f64; N];
fn with_channels(self, values: [f64; N]) -> Self;
fn compatible(self, other: Self) -> bool;
}
impl MotionData<1> for f64 {
fn channels(self) -> [f64; 1] {
[self]
}
fn with_channels(self, values: [f64; 1]) -> Self {
values[0]
}
fn compatible(self, _: Self) -> bool {
true
}
}
impl MotionData<6> for Target {
fn channels(self) -> [f64; 6] {
self.values()
}
fn with_channels(self, values: [f64; 6]) -> Self {
Self::from_values(values, self.mask())
}
fn compatible(self, other: Self) -> bool {
self.mask() == other.mask()
}
}
#[derive(Clone, Copy, Debug)]
pub struct TypedKeyframe<T> {
at: f64,
value: Option<T>,
easing: Easing,
}
impl<T> TypedKeyframe<T> {
pub fn new(at: f64, value: T) -> Self {
Self {
at,
value: Some(value),
easing: Easing::Linear,
}
}
pub fn current(at: f64) -> Self {
Self {
at,
value: None,
easing: Easing::Linear,
}
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
}
fn scalar(value: f64) -> Target {
Target::from_values([value, 0.0, 1.0, 0.0, 0.0, 0.0], 1)
}
#[derive(Clone, Debug)]
pub struct TypedAnimation<T, const N: usize> {
channels: [Animation; N],
template: T,
}
impl<T: MotionData<N>, const N: usize> TypedAnimation<T, N> {
pub fn keyframes(
duration: Duration,
frames: impl IntoIterator<Item = TypedKeyframe<T>>,
) -> Result<Self, AnimationError> {
let frames: Vec<_> = frames.into_iter().collect();
let template = frames
.iter()
.find_map(|frame| frame.value)
.ok_or(AnimationError::Keyframes)?;
if N == 0
|| frames.iter().filter_map(|frame| frame.value).any(|value| {
!template.compatible(value) || value.channels().iter().any(|v| !v.is_finite())
})
{
return Err(AnimationError::Keyframes);
}
let channels = (0..N)
.map(|i| {
Animation::keyframes(
duration,
frames.iter().map(|frame| {
let key = frame.value.map_or_else(
|| Keyframe::current(frame.at),
|value| Keyframe::new(frame.at, scalar(value.channels()[i])),
);
key.easing(frame.easing)
}),
)
})
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap_or_else(|_| unreachable!());
Ok(Self { channels, template })
}
pub fn to(value: T, duration: Duration, easing: Easing) -> Result<Self, AnimationError> {
let values = value.channels();
if N == 0 || values.iter().any(|value| !value.is_finite()) {
return Err(AnimationError::Keyframes);
}
let channels = std::array::from_fn(|i| Animation::to(scalar(values[i]), duration, easing));
Ok(Self {
channels,
template: value,
})
}
pub fn spring(value: T, spring: Spring, deadline: Duration) -> Result<Self, AnimationError> {
if N == 0 || value.channels().iter().any(|v| !v.is_finite()) {
return Err(AnimationError::Keyframes);
}
let channels = std::array::from_fn(|i| {
Animation::spring(scalar(value.channels()[i]), spring, deadline)
});
Ok(Self {
channels,
template: value,
})
}
pub fn delay(mut self, duration: Duration) -> Self {
self.channels = self.channels.map(|channel| channel.delay(duration));
self
}
pub fn repeat(
mut self,
repeat: Repeat,
mode: RepeatMode,
delay: Duration,
) -> Result<Self, AnimationError> {
self.channels = self
.channels
.into_iter()
.map(|channel| channel.repeat(repeat, mode, delay))
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap_or_else(|_| unreachable!());
Ok(self)
}
pub fn duration(&self) -> Option<Duration> {
self.channels[0].duration()
}
}
#[derive(Clone, Copy, Debug)]
pub struct TypedFrame<T> {
value: T,
active: bool,
completed: bool,
}
impl<T: Copy> TypedFrame<T> {
pub fn value(self) -> T {
self.value
}
pub fn is_animating(self) -> bool {
self.active
}
pub fn just_completed(self) -> bool {
self.completed
}
}
#[derive(Debug)]
pub struct TypedState<T, const N: usize> {
states: [AnimationState; N],
frame: TypedFrame<T>,
#[cfg(feature = "gpui")]
epoch: Option<std::time::Instant>,
}
impl<T: MotionData<N>, const N: usize> TypedState<T, N> {
pub fn new(value: T) -> Self {
assert!(
N > 0 && value.channels().iter().all(|v| v.is_finite()),
"invalid typed seed"
);
Self {
states: std::array::from_fn(|i| AnimationState::new(scalar(value.channels()[i]))),
frame: TypedFrame {
value,
active: false,
completed: false,
},
#[cfg(feature = "gpui")]
epoch: None,
}
}
pub fn sample(
&mut self,
clip: &TypedAnimation<T, N>,
now: Duration,
reduced: bool,
) -> TypedFrame<T> {
assert!(
self.frame.value.compatible(clip.template),
"typed property masks must match"
);
let samples = std::array::from_fn::<_, N, _>(|i| {
self.states[i].sample(&clip.channels[i], now, reduced)
});
let active = samples.iter().any(|frame| frame.is_animating());
self.frame = TypedFrame {
value: clip
.template
.with_channels(samples.map(|frame| frame.target().values()[0])),
active,
completed: !active && samples.iter().any(|frame| frame.just_completed()),
};
self.frame
}
pub fn value(&self) -> T {
self.frame.value
}
pub fn velocities(&self) -> [f64; N] {
std::array::from_fn(|i| self.states[i].current_velocity())
}
pub fn pause(&mut self, now: Duration) {
for state in &mut self.states {
state.pause(now);
}
}
pub fn play(&mut self, now: Duration) {
for state in &mut self.states {
state.play(now);
}
}
pub fn seek(&mut self, time: Duration, now: Duration) {
for state in &mut self.states {
state.seek(time, now);
}
}
pub fn restart(&mut self, now: Duration) {
for state in &mut self.states {
state.restart(now);
}
}
pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
if !speed.is_finite() {
return Err(AnimationError::Speed);
}
for state in &mut self.states {
state.set_speed(speed, now)?;
}
Ok(())
}
pub fn finish(&mut self) {
for state in &mut self.states {
state.finish();
}
}
pub fn completion(&mut self) -> crate::Completion {
self.states[0].completion()
}
pub fn is_animating(&self) -> bool {
self.frame.active
}
#[cfg(feature = "gpui")]
pub fn now(&self, cx: &gpui::App) -> Duration {
self.epoch.map_or(Duration::ZERO, |epoch| {
cx.background_executor()
.now()
.saturating_duration_since(epoch)
})
}
#[cfg(feature = "gpui")]
pub fn frame(
&mut self,
clip: &TypedAnimation<T, N>,
window: &mut gpui::Window,
cx: &gpui::App,
) -> TypedFrame<T> {
let instant = cx.background_executor().now();
let epoch = *self.epoch.get_or_insert(instant);
let frame = self.sample(
clip,
instant.saturating_duration_since(epoch),
cx.reduce_motion(),
);
if frame.active {
window.request_animation_frame();
}
frame
}
}
trait ErasedTrack: Debug {
fn sample(&mut self, time: Duration, now: Duration, speed: f64, reduced: bool) -> bool;
fn restart(&mut self);
fn as_any(&self) -> &dyn Any;
}
#[derive(Debug)]
struct Track<T, const N: usize> {
clip: TypedAnimation<T, N>,
state: TypedState<T, N>,
start: Duration,
}
impl<T: MotionData<N>, const N: usize> ErasedTrack for Track<T, N> {
fn sample(&mut self, time: Duration, now: Duration, speed: f64, reduced: bool) -> bool {
self.state
.set_speed(speed, now)
.expect("validated timeline speed");
self.state.seek(time.saturating_sub(self.start), now);
self.state.sample(&self.clip, now, reduced);
(time < self.start && !reduced)
|| !self.state.states.iter().all(|state| state.is_complete())
}
fn restart(&mut self) {
self.state.restart(Duration::ZERO);
}
fn as_any(&self) -> &dyn Any {
self
}
}
static NEXT_TIMELINE: AtomicU64 = AtomicU64::new(1);
#[derive(Debug)]
pub struct TrackId<T, const N: usize> {
owner: u64,
index: usize,
marker: PhantomData<T>,
}
impl<T, const N: usize> Copy for TrackId<T, N> {}
impl<T, const N: usize> Clone for TrackId<T, N> {
fn clone(&self) -> Self {
*self
}
}
#[derive(Debug)]
pub struct TypedTimeline {
owner: u64,
tracks: Vec<Box<dyn ErasedTrack>>,
clock: Playback,
cursor: Option<Duration>,
labels: Vec<(String, Duration)>,
active: bool,
completed: bool,
observers: crate::completion::CompletionSource,
#[cfg(feature = "gpui")]
epoch: Option<std::time::Instant>,
}
impl Default for TypedTimeline {
fn default() -> Self {
Self {
owner: NEXT_TIMELINE.fetch_add(1, Ordering::Relaxed),
tracks: Vec::new(),
clock: Playback::default(),
cursor: Some(Duration::ZERO),
labels: Vec::new(),
active: false,
completed: false,
observers: Default::default(),
#[cfg(feature = "gpui")]
epoch: None,
}
}
}
impl TypedTimeline {
pub fn at<T: MotionData<N>, const N: usize>(
&mut self,
start: Duration,
initial: T,
clip: TypedAnimation<T, N>,
) -> TrackId<T, N> {
assert!(
initial.compatible(clip.template),
"typed property masks must match"
);
self.observers.resolve(crate::CompletionStatus::Cancelled);
self.cursor = clip
.duration()
.map(|duration| start.saturating_add(duration));
let id = TrackId {
owner: self.owner,
index: self.tracks.len(),
marker: PhantomData,
};
let mut state = TypedState::new(initial);
state.sample(&clip, Duration::ZERO, false);
self.tracks.push(Box::new(Track { state, clip, start }));
self.completed = false;
id
}
fn offset(base: Option<Duration>, seconds: f64) -> Result<Duration, AnimationError> {
let base = base.ok_or(AnimationError::Times)?;
let value = base.as_secs_f64() + seconds;
Duration::try_from_secs_f64(value).map_err(|_| AnimationError::Times)
}
pub fn after<T: MotionData<N>, const N: usize>(
&mut self,
seconds: f64,
initial: T,
clip: TypedAnimation<T, N>,
) -> Result<TrackId<T, N>, AnimationError> {
let at = Self::offset(self.cursor, seconds)?;
Ok(self.at(at, initial, clip))
}
pub fn label(&mut self, name: impl Into<String>, at: Duration) -> Result<(), AnimationError> {
let name = name.into();
if self.labels.iter().any(|(old, _)| *old == name) {
return Err(AnimationError::Label);
}
self.labels.push((name, at));
Ok(())
}
pub fn at_label<T: MotionData<N>, const N: usize>(
&mut self,
name: &str,
seconds: f64,
initial: T,
clip: TypedAnimation<T, N>,
) -> Result<TrackId<T, N>, AnimationError> {
let at = self
.labels
.iter()
.find(|(label, _)| label == name)
.map(|(_, at)| *at)
.ok_or(AnimationError::Label)?;
Ok(self.at(Self::offset(Some(at), seconds)?, initial, clip))
}
pub fn sample(&mut self, now: Duration, reduced: bool) {
let time = self.clock.time(now);
let mut unfinished = false;
let speed = if self.clock.is_playing() {
self.clock.speed()
} else {
0.0
};
for track in &mut self.tracks {
unfinished |= track.sample(time, now, speed, reduced);
}
if speed < 0.0 && time.is_zero() {
unfinished = false;
}
self.active = unfinished && self.clock.is_playing();
if !unfinished && !self.completed {
self.observers.resolve(crate::CompletionStatus::Completed);
}
self.completed = !unfinished;
}
pub fn value<T: MotionData<N>, const N: usize>(&self, id: TrackId<T, N>) -> Option<T> {
if id.owner != self.owner {
return None;
}
self.tracks
.get(id.index)?
.as_any()
.downcast_ref::<Track<T, N>>()
.map(|track| track.state.value())
}
pub fn pause(&mut self, now: Duration) {
self.clock.pause(now);
}
pub fn play(&mut self, now: Duration) {
self.clock.play(now);
}
pub fn seek(&mut self, time: Duration, now: Duration) {
self.clock.seek(time, now);
}
pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
self.clock.set_speed(speed, now)
}
pub fn restart(&mut self, now: Duration) {
self.observers.resolve(crate::CompletionStatus::Cancelled);
self.clock.seek(Duration::ZERO, now);
self.clock.play(now);
for track in &mut self.tracks {
track.restart();
}
self.completed = false;
}
pub fn completion(&mut self) -> crate::Completion {
self.observers.subscribe(self.completed)
}
pub fn is_animating(&self) -> bool {
self.active
}
#[cfg(feature = "gpui")]
pub fn now(&self, cx: &gpui::App) -> Duration {
self.epoch.map_or(Duration::ZERO, |epoch| {
cx.background_executor()
.now()
.saturating_duration_since(epoch)
})
}
#[cfg(feature = "gpui")]
pub fn frame(&mut self, window: &mut gpui::Window, cx: &gpui::App) {
let instant = cx.background_executor().now();
let epoch = *self.epoch.get_or_insert(instant);
self.sample(instant.saturating_duration_since(epoch), cx.reduce_motion());
if self.active {
window.request_animation_frame();
}
}
}
impl<const N: usize> MotionData<N> for [f64; N] {
fn channels(self) -> [f64; N] {
self
}
fn with_channels(self, values: [f64; N]) -> Self {
values
}
fn compatible(self, _: Self) -> bool {
true
}
}