use std::borrow::Cow;
use js_sys::Reflect;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{Animation, DomRect, Element};
use super::waapi::{animate, AnimateOptions, Keyframe};
const FLIP_ANIM_KEY: &str = "__pp_flip_anim";
#[derive(Clone, Debug)]
pub struct FlipOptions {
pub duration_ms: f64,
pub easing: Cow<'static, str>,
pub min_delta_px: f64,
}
impl Default for FlipOptions {
fn default() -> Self {
Self {
duration_ms: 320.0,
easing: Cow::Borrowed("cubic-bezier(0.16, 1, 0.3, 1)"),
min_delta_px: 2.0,
}
}
}
pub struct FlipTarget {
pub element: Element,
pub old_rect: DomRect,
pub new_rect: DomRect,
}
pub fn flip_from_snapshot(el: &Element, old_rect: DomRect, opts: FlipOptions) {
let new_rect = el.get_bounding_client_rect();
flip_with_new_rect(el, old_rect, new_rect, opts);
}
pub fn flip_with_new_rect(el: &Element, old_rect: DomRect, new_rect: DomRect, opts: FlipOptions) {
let dx = old_rect.left() - new_rect.left();
let dy = old_rect.top() - new_rect.top();
if dx.abs() < opts.min_delta_px && dy.abs() < opts.min_delta_px {
return;
}
cancel_prior(el);
let from = format!("translate({dx}px, {dy}px)");
let handle = animate(
el,
&[
Keyframe::from_iter([("transform", from.as_str())]),
Keyframe::from_iter([("transform", "translate(0, 0)")]),
],
AnimateOptions {
duration_ms: opts.duration_ms,
easing: opts.easing,
delay_ms: 0.0,
fill: "none",
respect_motion_preference: true,
},
);
stash(el, handle.raw());
}
pub fn flip_batch<I>(targets: I, opts: FlipOptions)
where
I: IntoIterator<Item = FlipTarget>,
{
for t in targets {
flip_with_new_rect(&t.element, t.old_rect, t.new_rect, opts.clone());
}
}
pub fn flip(el: &Element, mutate: impl FnOnce(), opts: FlipOptions) {
let rect = el.get_bounding_client_rect();
mutate();
flip_from_snapshot(el, rect, opts);
}
pub fn measure_layout_rect(el: &Element) -> DomRect {
el.get_bounding_client_rect()
}
fn cancel_prior(el: &Element) {
if let Some(anim) = stashed(el) {
anim.cancel();
}
let _ = Reflect::set(
el.as_ref(),
&JsValue::from_str(FLIP_ANIM_KEY),
&JsValue::UNDEFINED,
);
}
fn stash(el: &Element, anim: &Animation) {
let _ = Reflect::set(
el.as_ref(),
&JsValue::from_str(FLIP_ANIM_KEY),
anim.as_ref(),
);
}
fn stashed(el: &Element) -> Option<Animation> {
let raw = Reflect::get(el.as_ref(), &JsValue::from_str(FLIP_ANIM_KEY)).ok()?;
if raw.is_undefined() || raw.is_null() {
return None;
}
raw.dyn_into::<Animation>().ok()
}