use js_sys::{Array, Object, Reflect};
use std::borrow::Cow;
use std::future::Future;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::Element;
#[derive(Clone, Debug)]
pub struct Keyframe {
pub props: Vec<(&'static str, String)>,
}
impl Keyframe {
#[allow(clippy::should_implement_trait)]
pub fn from_iter<I, V>(iter: I) -> Self
where
I: IntoIterator<Item = (&'static str, V)>,
V: Into<String>,
{
Self {
props: iter.into_iter().map(|(k, v)| (k, v.into())).collect(),
}
}
}
#[derive(Clone, Debug)]
pub struct AnimateOptions {
pub duration_ms: f64,
pub easing: Cow<'static, str>,
pub delay_ms: f64,
pub fill: &'static str,
pub respect_motion_preference: bool,
}
impl Default for AnimateOptions {
fn default() -> Self {
Self {
duration_ms: 200.0,
easing: Cow::Borrowed("cubic-bezier(0, 0, 0.2, 1)"),
delay_ms: 0.0,
fill: "forwards",
respect_motion_preference: true,
}
}
}
pub struct AnimationHandle {
inner: web_sys::Animation,
}
impl AnimationHandle {
pub fn cancel(&self) {
self.inner.cancel();
}
pub fn finish(&self) {
let _ = self.inner.finish();
}
pub fn pause(&self) {
let _ = self.inner.pause();
}
pub fn play(&self) {
let _ = self.inner.play();
}
pub fn set_playback_rate(&self, rate: f64) {
self.inner.set_playback_rate(rate);
}
pub fn current_time(&self) -> Option<f64> {
self.inner.current_time()
}
pub fn raw(&self) -> &web_sys::Animation {
&self.inner
}
pub fn on_finish<F: FnOnce() + 'static>(&self, cb: F) {
let closure = Closure::once_into_js(cb);
self.inner.set_onfinish(Some(closure.unchecked_ref()));
}
pub fn finished(&self) -> impl Future<Output = ()> {
let promise: js_sys::Promise =
match Reflect::get(self.inner.as_ref(), &JsValue::from_str("finished")) {
Ok(v) if !v.is_undefined() => v.unchecked_into(),
_ => js_sys::Promise::resolve(&JsValue::UNDEFINED),
};
async move {
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
}
}
}
pub fn animate(el: &Element, keyframes: &[Keyframe], opts: AnimateOptions) -> AnimationHandle {
let effective_duration = if opts.respect_motion_preference
&& super::motion::effective_for(el) == super::motion::MotionPreference::Reduced
{
1.0
} else {
opts.duration_ms
};
let kf_array = Array::new();
for kf in keyframes {
let obj = Object::new();
for (k, v) in &kf.props {
let _ = Reflect::set(&obj, &JsValue::from_str(k), &JsValue::from_str(v));
}
kf_array.push(&obj);
}
let opt_obj = Object::new();
let _ = Reflect::set(
&opt_obj,
&JsValue::from_str("duration"),
&JsValue::from_f64(effective_duration),
);
let _ = Reflect::set(
&opt_obj,
&JsValue::from_str("easing"),
&JsValue::from_str(opts.easing.as_ref()),
);
if opts.delay_ms > 0.0 {
let _ = Reflect::set(
&opt_obj,
&JsValue::from_str("delay"),
&JsValue::from_f64(opts.delay_ms),
);
}
let _ = Reflect::set(
&opt_obj,
&JsValue::from_str("fill"),
&JsValue::from_str(opts.fill),
);
let animate_fn = match Reflect::get(el.as_ref(), &JsValue::from_str("animate")) {
Ok(v) if v.is_function() => v.unchecked_into::<js_sys::Function>(),
_ => {
return AnimationHandle {
inner: fallback_animation(),
};
}
};
let args = Array::new();
args.push(&kf_array);
args.push(&opt_obj);
let result = animate_fn
.apply(el.as_ref(), &args)
.unwrap_or(JsValue::NULL);
let anim = result
.dyn_into::<web_sys::Animation>()
.unwrap_or_else(|_| fallback_animation());
AnimationHandle { inner: anim }
}
fn fallback_animation() -> web_sys::Animation {
match js_sys::Reflect::construct(
&js_sys::Function::new_no_args("return new Animation();"),
&Array::new(),
) {
Ok(v) => v.unchecked_into(),
Err(_) => Object::new().unchecked_into(),
}
}