use crate::error::{JsResult, js_error, non_negative};
use crate::keyframe::{KeyframeTrack, KeyframeTrack2D, KeyframeTrack3D, KeyframeTrack4D};
use crate::path::MotionPath;
use crate::tween::{Tween, Tween2D, Tween3D, Tween4D, lock};
use crate::types::parse_loop_mode;
use animato_core::{Playable, Update};
use animato_timeline::{At, Timeline as CoreTimeline, TimelineState};
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::*;
fn state_name(state: TimelineState) -> &'static str {
match state {
TimelineState::Idle => "idle",
TimelineState::Playing => "playing",
TimelineState::Paused => "paused",
TimelineState::Completed => "completed",
}
}
fn parse_at(input: &str) -> JsResult<At<'_>> {
let at = input.trim();
if at.eq_ignore_ascii_case("start") {
return Ok(At::Start);
}
if at.eq_ignore_ascii_case("end") {
return Ok(At::End);
}
if let Some(label) = at.strip_prefix("label:") {
return Ok(At::Label(label));
}
if let Some(offset) = at.strip_prefix('+') {
return Ok(At::Offset(offset.parse::<f32>().map_err(|_| {
js_error(format!("invalid timeline offset `{input}`"))
})?));
}
if at.starts_with('-') {
return Ok(At::Offset(at.parse::<f32>().map_err(|_| {
js_error(format!("invalid timeline offset `{input}`"))
})?));
}
if let Some(abs) = at.strip_prefix('@') {
return Ok(At::Absolute(non_negative(
abs.parse::<f32>()
.map_err(|_| js_error(format!("invalid absolute timeline time `{input}`")))?,
0.0,
)));
}
if let Ok(abs) = at.parse::<f32>() {
return Ok(At::Absolute(non_negative(abs, 0.0)));
}
Ok(At::Label(at))
}
#[wasm_bindgen(js_name = Timeline)]
#[derive(Clone, Debug)]
pub struct Timeline {
inner: Arc<Mutex<CoreTimeline>>,
}
#[wasm_bindgen(js_class = Timeline)]
impl Timeline {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(CoreTimeline::new())),
}
}
#[wasm_bindgen(js_name = addTween)]
pub fn add_tween(&self, label: &str, tween: &Tween, at: &str) -> Result<(), JsValue> {
self.add_playable(label, tween.shared(), at)
}
#[wasm_bindgen(js_name = addTween2D)]
pub fn add_tween_2d(&self, label: &str, tween: &Tween2D, at: &str) -> Result<(), JsValue> {
self.add_playable(label, tween.shared(), at)
}
#[wasm_bindgen(js_name = addTween3D)]
pub fn add_tween_3d(&self, label: &str, tween: &Tween3D, at: &str) -> Result<(), JsValue> {
self.add_playable(label, tween.shared(), at)
}
#[wasm_bindgen(js_name = addTween4D)]
pub fn add_tween_4d(&self, label: &str, tween: &Tween4D, at: &str) -> Result<(), JsValue> {
self.add_playable(label, tween.shared(), at)
}
#[wasm_bindgen(js_name = addKeyframes)]
pub fn add_keyframes(
&self,
label: &str,
track: &KeyframeTrack,
at: &str,
) -> Result<(), JsValue> {
self.add_playable(label, track.shared(), at)
}
#[wasm_bindgen(js_name = addKeyframes2D)]
pub fn add_keyframes_2d(
&self,
label: &str,
track: &KeyframeTrack2D,
at: &str,
) -> Result<(), JsValue> {
self.add_playable(label, track.shared(), at)
}
#[wasm_bindgen(js_name = addKeyframes3D)]
pub fn add_keyframes_3d(
&self,
label: &str,
track: &KeyframeTrack3D,
at: &str,
) -> Result<(), JsValue> {
self.add_playable(label, track.shared(), at)
}
#[wasm_bindgen(js_name = addKeyframes4D)]
pub fn add_keyframes_4d(
&self,
label: &str,
track: &KeyframeTrack4D,
at: &str,
) -> Result<(), JsValue> {
self.add_playable(label, track.shared(), at)
}
#[wasm_bindgen(js_name = addMotionPath)]
pub fn add_motion_path(
&self,
label: &str,
motion: &MotionPath,
at: &str,
) -> Result<(), JsValue> {
self.add_playable(label, motion.shared(), at)
}
pub fn play(&self) {
lock(&self.inner).play();
}
pub fn pause(&self) {
lock(&self.inner).pause();
}
pub fn resume(&self) {
lock(&self.inner).resume();
}
pub fn reset(&self) {
lock(&self.inner).reset();
}
pub fn update(&self, dt: f32) -> bool {
lock(&self.inner).update(dt)
}
pub fn seek(&self, progress: f32) {
lock(&self.inner).seek(progress);
}
#[wasm_bindgen(js_name = seekAbs)]
pub fn seek_abs(&self, seconds: f32) {
lock(&self.inner).seek_abs(seconds);
}
pub fn duration(&self) -> f32 {
lock(&self.inner).duration()
}
pub fn progress(&self) -> f32 {
lock(&self.inner).progress()
}
pub fn state(&self) -> String {
state_name(lock(&self.inner).state()).to_owned()
}
#[wasm_bindgen(js_name = isComplete)]
pub fn is_complete(&self) -> bool {
lock(&self.inner).is_complete()
}
#[wasm_bindgen(js_name = entryCount)]
pub fn entry_count(&self) -> usize {
lock(&self.inner).entry_count()
}
#[wasm_bindgen(js_name = setTimeScale)]
pub fn set_time_scale(&self, scale: f32) {
lock(&self.inner).set_time_scale(non_negative(scale, 1.0));
}
#[wasm_bindgen(js_name = setLoopMode)]
pub fn set_loop_mode(&self, mode: &str) -> Result<(), JsValue> {
lock(&self.inner).looping = parse_loop_mode(mode)?;
Ok(())
}
fn add_playable<A>(&self, label: &str, animation: A, at: &str) -> Result<(), JsValue>
where
A: Playable + Send + 'static,
{
let at = parse_at(at)?;
let mut timeline = lock(&self.inner);
let next = core::mem::take(&mut *timeline).add(label, animation, at);
*timeline = next;
Ok(())
}
}
impl Default for Timeline {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub(crate) struct SharedTimeline {
inner: Arc<Mutex<CoreTimeline>>,
}
impl SharedTimeline {
pub(crate) fn new(inner: Arc<Mutex<CoreTimeline>>) -> Self {
Self { inner }
}
}
impl Update for SharedTimeline {
fn update(&mut self, dt: f32) -> bool {
lock(&self.inner).update(dt)
}
}
impl Timeline {
pub(crate) fn shared(&self) -> SharedTimeline {
SharedTimeline::new(Arc::clone(&self.inner))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timeline_updates_shared_tween() {
let tween = Tween::new(0.0, 100.0, 1.0);
let timeline = Timeline::new();
timeline.add_tween("x", &tween, "start").unwrap();
timeline.play();
timeline.update(0.5);
assert_eq!(tween.value(), 50.0);
}
}