Adding callback's timestamp parameter to requestAnimationFrame - is this right?

As luck would have it, this morphed from a “how do I?” question into an “is this right?” question. This is my first go at writing an effectful binding, so I want to make sure that even if it works, I’m not introducing some subtle misbehavior.

Here is a gist with the main module, foreign module, and compiled output: https://gist.github.com/smilack/b2026158f0b35e5dbc4d271009d87614.


Motivation: The callback given to Window.requestAnimationFrame can take an argument - the timestamp marking the beginning of the animation frame - but the current FFI to requestAnimationFrame (JS implementation) doesn’t support this.

I started by changing the Effect Unit types to Number -> Effect Unit and using the same JavaScript implementation.

foreign import _requestAnimationFrame :: Effect Unit -> Window -> Effect Int
requestAnimationFrame :: Effect Unit -> Window -> Effect RequestAnimationFrameId

foreign import _requestAnimationFrameWithTime :: (Number -> Effect Unit) -> Window -> Effect Int
requestAnimationFrameWithTime :: (Number -> Effect Unit) -> Window -> Effect RequestAnimationFrameId

However, the callback for requestAnimationFrameWithTime never ran. I looked at the compiled code (I didn’t realize how great readable output is until now!), and found that the callback with an argument is wrapped in an extra function:

var callbackWithTime = function (time) {
    return Effect_Console.log("callback with time: " + Data_Show.show(Data_Show.showNumber)(time));
};
var callback = Effect_Console.log("callback without time");

So requestAnimationFrame was calling callbackWithTime but discarding the return value - which is what I actually wanted it to call.

I wrestled with the type checker for a while but couldn’t reconcile the fact that the issue was occurring within requestAnimationFrame, so I realized the problem had to be on the JS side.

To solve it I added an anonymous function which is passed as the callback to requestAnimationFrame. It receives the timestamp value, passes it into the provided callback, and then calls the returned function:

exports._requestAnimationFrameWithTime = function(fn) {
  return function(window) {
    return function() {
      return window.requestAnimationFrame(function(time) {
        fn(time)();
      });
    };
  };
};

So I guess my only question is - is there anything wrong with this?


A note on DOMHighResTimeStamp: MDN says the callback is passed a DOMHighResTimeStamp, but I couldn’t find a definition for it anywhere in PureScript. I looked at the High Resolution Time spec and found that it’s an alias for double, just like JavaScript’s Number. I also double-checked with the Web Interface Definition Language spec, which says typedefs are for use in writing specs only, and are not part of the language described. So I think Number is the correct type for the callback’s parameter.

1 Like

What was your previous FFI code? Did you not call fn(time) in requestAnimationFrame before? Or did you pass fn directly? For your question, this is the best and simplest way to do this that I know of. See @hdgarrood’s reply below.

Yep, that code all looks good to me! You might also like to take a look at https://pursuit.purescript.org/packages/purescript-effect/2.0.1/docs/Effect.Uncurried which is another option for dealing with cases like this.

2 Likes

Oh, I forgot there are functions for that. @smilack you probably want to use them as they’re more readable.

I passed fn to requestAnimationFrame, so I never directly called fn(time):

exports._requestAnimationFrameWithTime = function(fn) {
  return function(window) {
    return function() {
      return window.requestAnimationFrame(fn);
    };
  };
};

Excellent! So the JS code goes back to the original (above), and the PureScript side becomes:

foreign import _requestAnimationFrameWithTime :: EffectFn1 Number Unit -> Window -> Effect Int

requestAnimationFrameWithTime :: (Number -> Effect Unit) -> Window -> Effect RequestAnimationFrameId
requestAnimationFrameWithTime fn = map wrap <<< _requestAnimationFrame (mkEffectFn1 fn)

And it works! Hooray!

4 Likes