rustwasm / gloo

A modular toolkit for building fast, reliable Web applications and libraries with Rust and WASM
https://gloo-rs.web.app
Apache License 2.0
1.72k stars 141 forks source link

utility crate: requestAnimationFrame #1

Open fitzgen opened 5 years ago

fitzgen commented 5 years ago

We should have an idiomatic Rust utility crate for working with

Pauan commented 5 years ago

A couple more things which I think would be useful:

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.

fitzgen commented 5 years ago

@Pauan are you interested in making a strawman API proposal / draft PR for some of this?

fitzgen commented 5 years ago

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

Aloso commented 5 years ago

@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:

Low-level API

use gloo::ani_frames::Animation;

let af: AnimationIndex<_> = Animation::request_frame(|| { ... });
Animation::cancel_frame(af);

Higher-level API

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

Higher-level API using futures

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 commented 5 years ago

@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.

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.

fitzgen commented 5 years ago

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:

struct 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?

Aloso commented 5 years ago

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()
}
Pauan commented 5 years ago

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.

dakom commented 5 years ago

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())
}
dakom commented 4 years ago

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?

dakom commented 4 years ago

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?

dakom commented 4 years ago

RFC here: https://github.com/rustwasm/gloo/pull/107

ranile commented 3 years ago

1. and 4. were implemented in #126