Open fitzgen opened 5 years ago
A couple more things which I think would be useful:
For performance reasons, it's probably faster to use a single rAF
and then use a Rust VecDeque
to support multiple listeners. I've used this strategy in dominator, and I can explain in more detail if needed.
rAF
is an API which works excellently with my futures-signals
crate.
For example, dominator has an API called timestamps()
which returns a Signal
of timestamps (which automatically updates every 60 FPS, by using rAF
).
Of course it supports automatic cancellation and automatic frame dropping, which makes it ideal for silky-smooth synchronized animations.
The above was pioneered by dominator, but it is absolutely not tied to dominator at all, so it can easily be put into a separate crate.
@Pauan are you interested in making a strawman API proposal / draft PR for some of this?
FWIW, when I said "debouncing rAF", this is the kind of thing I was thinking of: https://github.com/fitzgen/dodrio/blob/master/src/vdom.rs#L599-L625
@fitzgen I don't think that dodrio::VdomWeak::render
uses debouncing. If I understand it correctly, you want an API for requesting animation frames that returns a Future
or a Stream
.
I'd like to give this a try. Here's my design idea:
use gloo::ani_frames::Animation;
let af: AnimationIndex<_> = Animation::request_frame(|| { ... });
Animation::cancel_frame(af);
struct T; // zero-sized type to distinguish animations
// create and start a requestAnimationFrame() loop!
// use Animation::<T>::paused() if you don't want to start it
let mut ani = Animation::<T>::new();
// add closure that is executed on every frame:
ani.add(|state| { ... });
// closures can be removed (only once, because AnimationIndex isn't Copy):
let index: AnimationIndex<T> = ani.add(|_| { ... });
ani.remove(index);
ani.pause(); // pauses the animation loop, using cancelAnimationFrame()
ani.once(); // requests one frame, if it's paused
ani.start(); // starts the animation loop, if it's paused
std::mem::drop(ani); // cancels the animation
// or
ani.forget(); // leaks the memory
struct T;
let mut ani: AnimationStream<T, &AnimationState> = Animation::stream(); // or paused_stream()
// add closure that is executed on every frame:
ani.add(|state| { ... });
// or
let ani = ani.map(|state| { ... });
Please tell me what you think!
This is roughly how I would implement the higher-level API:
pub struct Animation<C> {
state: AnimationState,
next_index: AnimationIndex<C>,
callbacks: Vec<(AnimationIndex<C>, Box<dyn FnMut(AnimationState)>)>,
// this could be done with a HashMap, but I think we want deterministic iteration order
}
pub enum AnimationState {
Paused,
Running(i64),
RunningOnce(i64),
}
pub struct AnimationIndex<C> {
index: i64,
_marker: PhantomData<C>,
}
@fitzgen I don't think that
dodrio::VdomWeak::render
uses debouncing. If I understand it correctly, you want an API for requesting animation frames that returns aFuture
or aStream
.
If there is not an existing promise, then an animation frame is scheduled that will resolve the promise, and the promise is saved. If there is an existing promise, then it is re-used and no new animation frame is scheduled. Thus the requestAnimationFrame
calls are "debounced" or "de-duplicated" or "reused"; whatever you want to call it.
For the low-level API, I would expect something very similar to the gloo_events
API that we have a WIP PR for. That is:
forget
method to make it uncancelablestruct T; // zero-sized type to distinguish animations
Is this the state
passed to each callback? Wait it seems like this is a separate AnimationState
thing. Is that user-defined or provided by the library?
Is this the state passed to each callback? Wait it seems like this is a separate AnimationState thing. Is that user-defined or provided by the library?
The state isn't user-defined, it's one of Paused
, Running(i32)
or RunningOnce(i32)
. I thought it might be useful to pass it to the callbacks.
The struct T
is just a way to allow distinguish animations, so the following doesn't compile:
struct T;
struct U;
let mut first = Animation::<T>::new();
let mut second = Animation::<U>::new();
let ix = first.add(|_| { ... });
second.remove(ix); // expected AnimationIndex<U>, found AnimationIndex<T>
Instead of T and U, you could also write [(); 1]
and [(); 2]
, or &()
and &&()
.
I'm trying to implement the whole thing ATM and struggling with the borrow checker. I fear I'll have to use a Rc<Mutex<Animation>>
in the loop.
Here is my helper function. It compiles, but when I try to use it, I get all sorts of errors because of 'static
:
fn request_af<F: FnOnce() + 'static>(callback: F) -> i32 {
web_sys::window().unwrap_throw()
.request_animation_frame(Closure::once(callback).as_ref().unchecked_ref())
.unwrap_throw()
}
I fear I'll have to use a
Rc<Mutex<Animation>>
in the loop.
Yes, if you want looping in Rust, that is necessary.
If the looping is done in JS it isn't necessary, but then you need a local .js
snippet.
Nit: it should be RefCell
, not Mutex
.
Fwiw below is what I'm currently using for a cancellable rAF loop.
At a high level it takes a closure (which gets the timestamp) and gives back a function that can be used to cancel.
This allows wrapping the closure in order to support debouncing (or in this demo, turning it into a struct with elapsedTime
and deltaTime
)
It's not much more than a modified version of the reference implementation:
/// Kick off a rAF loop. The returned function can be called to cancel it
pub fn start_raf_ticker<F>(mut on_tick:F) -> Result<impl (FnOnce() -> ()), JsValue>
where F: (FnMut(f64) -> ()) + 'static
{
let f = Rc::new(RefCell::new(None));
let g = f.clone();
//the main closure must be static - and so it needs to take in its deps via move
//but keep_alive also exists in cancel() - so we're left with multiple owners
let keep_alive = Rc::new(Cell::new(true));
let mut raf_id:Option<i32> = None;
//this window is passed into the loop
let window = web_sys::window().expect("couldn't get window!");
{
let keep_alive = Rc::clone(&keep_alive);
*g.borrow_mut() = Some(Closure::wrap(Box::new(move |time| {
if !keep_alive.get() {
if let Some(id) = raf_id {
info!("clearing raf id: {}", id);
window.cancel_animation_frame(id).unwrap();
}
//stopping tick loop
f.borrow_mut().take();
} else {
on_tick(time);
raf_id = request_animation_frame(&window, f.borrow().as_ref().unwrap()).ok();
}
}) as Box<FnMut(f64)-> ()>));
}
//this is just used to create the first invocation
let window = web_sys::window().expect("couldn't get window!");
request_animation_frame(&window, g.borrow().as_ref().unwrap())?;
let cancel = move || keep_alive.set(false);
Ok(cancel)
}
fn request_animation_frame(window:&Window, f: &Closure<FnMut(f64) -> ()>) -> Result<i32, JsValue> {
window.request_animation_frame(f.as_ref().unchecked_ref())
}
Now that work has been done to bring the other crates in line with std::Future, perhaps this can be resurrected... rAF could give a Stream?
Btw looking at @Pauan 's Raf struct here it looks like it would be really straightforward to bring as-is into gloo... any reason not to do this now?
1. and 4. were implemented in #126
We should have an idiomatic Rust utility crate for working with
requestAnimationFrame
requestAnimationFrame
loop (reusing the same function)requestAnimationFrame
s