Wasm web-sys: how to use window.request_animation_frame (resolved)

After finishing the rustwasm Game of Life tutorial I'm trying to implement the whole example in rust using web-sys, including event handlers. I've been able to port the rendering, and straightforward button handling, the only javascript that is left is the animation based on window.requestAnimationFrame:

import { Universe, Cell, Renderer, ... } from "wasm-game-of-life";
const universe = Universe.new();
const canvas = document.getElementById("game-of-life-canvas");
const renderer = Renderer.new(canvas, universe);
let animationId = null;
const renderLoop = () => {
  universe.tick();
  renderer.draw();
  animationId = requestAnimationFrame(renderLoop);
};
const isPaused = () => {
  return animationId === null;
};
const playPauseButton = document.getElementById("play-pause");
const play = () => {
  playPauseButton.textContent = "⏸";
  renderLoop();
};
const pause = () => {
  playPauseButton.textContent = "▶";
  cancelAnimationFrame(animationId);
  animationId = null;
};
playPauseButton.addEventListener("click", event => {
  if (isPaused()) {
    play();
  } else {
    pause();
  }
});
play();

The problem, for me, is to implement this using Closure without leaking every frame while recreating it. The closure examples in the guide are all about event handlers that are called repeatedly. However, in this case it is necessary to re-set the event handler after every frame.

Ideally the closure itself needs to be able to pass itself again into requestAnimationFrame. But it eludes me how to do this, say, what to put in the here:

    // renderLoop
    {
        let closure: Closure<FnMut()> = Closure::wrap(Box::new(move || {
            log("animation frame");
            let window = web_sys::window().expect("no global `window` exists");
            window.request_animation_frame(…);
        }));
        let window = web_sys::window().expect("no global `window` exists");
        window.request_animation_frame(closure.as_ref().unchecked_ref());
        closure.forget();
    }

(I've tried to think of various ways to wrap using Cell, Rc, Box, or wrapping the Closure into my own struct, but nothing worked… I suspect I'm thinking in the wrong direction completely !)

1 Like

Oh, shortly after writing this down I think I got somewhere, by using RefCell to a state structure, that wraps the closure as well;

struct RenderLoop {
    animation_id: Option<i32>,
    pub closure: Option<Closure<Fn()>>,
}
…

    // Render loop handling
    let render_loop: Rc<RefCell<RenderLoop>> = Rc::new(RefCell::new(RenderLoop::new()));
    {
        let closure: Closure<Fn()> = {
            let render_loop = render_loop.clone();
            Closure::wrap(Box::new(move || {
                ...
                let mut render_loop = render_loop.borrow_mut();
                render_loop.animation_id = if let Some(ref closure) = render_loop.closure {
                    Some(window.request_animation_frame(closure.as_ref().unchecked_ref()).expect("cannot set animation frame"))
                } else {
                    None
                }
            }))
        };
        let mut render_loop = render_loop.borrow_mut();
        render_loop.animation_id = Some(window.request_animation_frame(closure.as_ref().unchecked_ref()).expect("cannot set animation frame"));
        render_loop.closure = Some(closure);
    }

it's probably overkill but it seems to work !

1 Like

General comment: Often, the effort to express a problem and explain it to someone else – in this case the readers of this forum – provides sufficient clarity to see past the impasse. That's one reason it's good to have a colleague to discuss issues with, even when working on what is primarily a solo task.

hehe yes, this is certainly a textbook example of that

in case anyone is interested, i managed to move over virtually everything to rust, and put up the source here: mara / wasm-game-of-life · GitLab