requestAnimationFrame woes

I recently ran in to some problems using requestAnimationFrame and thought I'd share the polyfill I made to fix them.

Since Chrome 24 the requestAnimationFrame callback gets passed a DOMHighResTimeStamp instead of a normal timestamp. The high-res timestamp is from what I can tell milliseconds since page load, while the normal timestamp is milliseconds since unix epoch. This causes the following code to break quite badly.

startTime = Date.now()
duration = 1000

animate = (time) ->
  t = (time - startTime) / duration
  # ... dancing unicorn animation redacted ...
  if t < 1
    requestAnimationFrame(animate)

requestAnimationFrame(animate)

If time is smaller than startTime, t will be negative causing the animation to continue indefinitely and all sorts of havoc to any interpolations using it.

To address this I've added a now method to requestAnimationFrame that returns the current time in the format used by requestAnimationFrame. So now, to fix the snippet above you can just replace the call to Date.now() with requestAnimationFrame.now().

The second issue that I had was when using the polyfill found on Paul Irish blog.

..
window.requestAnimationFrame = function(callback, element) {
    var currTime = new Date().getTime();
    var timeToCall = Math.max(0, 16 - (currTime - lastTime));
    var id = window.setTimeout(function() { callback(currTime + timeToCall); },
      timeToCall);
    lastTime = currTime + timeToCall;
    return id;
};
..

This code does not take into account multiple running animation loops. So for every running loop timeToCall is increased by 16ms, resulting in a big fps drop. E.g. running 4 concurrent animations would give you ~15 fps.

A better polyfill

Updated 2013-12-03: Added a workaround for Mobile Safari in iOS 7. It calls frames with a high-res timestamp but does not implement window.performance.

Download raf.coffee or raf.js

do ->
  method = 'native'
  now = Date.now or -> new Date().getTime()

  requestAnimationFrame = window.requestAnimationFrame
  for vendor in ['webkit', 'moz', 'o', 'ms'] when not requestAnimationFrame?
    requestAnimationFrame = window["#{ vendor }RequestAnimationFrame"]

  if not requestAnimationFrame?
    # polyfill using shared timer
    method = 'timer'
    lastFrame = 0
    queue = timer = null
    requestAnimationFrame = (callback) ->
      if queue?
        queue.push callback
        return

      time = now()
      nextFrame = Math.max 0, 16.66 - (time - lastFrame)

      queue = [callback]
      lastFrame = time + nextFrame

      fire = ->
        q = queue
        queue = null
        cb(lastFrame) for cb in q
        return

      timer = setTimeout fire, nextFrame
      return

  # check what timestamp format is being used
  # http://lists.w3.org/Archives/Public/public-web-perf/2012May/0053.html
  requestAnimationFrame (time) ->
    if time < 1e12
      if window.performance?.now?
        requestAnimationFrame.now = -> window.performance.now()
        requestAnimationFrame.method = 'native-highres'
      else
        # iOS7 sends highres timestamps but does not expose a way to access them
        offset = now() - time
        requestAnimationFrame.now = -> now() - offset
        requestAnimationFrame.method = 'native-highres-noperf'
    else
      requestAnimationFrame.now = now
    return

  # there's no way to synchronously detect high-res timestamps :-(
  # naively assume highres until detection finishes if performance.now is present
  requestAnimationFrame.now = if window.performance?.now? then (-> window.performance.now()) else now
  requestAnimationFrame.method = method
  window.requestAnimationFrame = requestAnimationFrame

JavaScript version

(function() {
  var lastFrame, method, now, queue, requestAnimationFrame, timer, vendor, _i, _len, _ref, _ref1;
  method = 'native';
  now = Date.now || function() {
    return new Date().getTime();
  };
  requestAnimationFrame = window.requestAnimationFrame;
  _ref = ['webkit', 'moz', 'o', 'ms'];
  for (_i = 0, _len = _ref.length; _i < _len; _i++) {
    vendor = _ref[_i];
    if (requestAnimationFrame == null) {
      requestAnimationFrame = window[vendor + 'RequestAnimationFrame'];
    }
  }
  if (requestAnimationFrame == null) {
    method = 'timer';
    lastFrame = 0;
    queue = timer = null;
    requestAnimationFrame = function(callback) {
      var fire, nextFrame, time;
      if (queue != null) {
        queue.push(callback);
        return;
      }
      time = now();
      nextFrame = Math.max(0, 16.66 - (time - lastFrame));
      queue = [callback];
      lastFrame = time + nextFrame;
      fire = function() {
        var cb, q, _j, _len1;
        q = queue;
        queue = null;
        for (_j = 0, _len1 = q.length; _j < _len1; _j++) {
          cb = q[_j];
          cb(lastFrame);
        }
      };
      timer = setTimeout(fire, nextFrame);
    };
  }
  requestAnimationFrame(function(time) {
    var offset, _ref1;
    if (time < 1e12) {
      if (((_ref1 = window.performance) != null ? _ref1.now : void 0) != null) {
        requestAnimationFrame.now = function() {
          return window.performance.now();
        };
        requestAnimationFrame.method = 'native-highres';
      } else {
        offset = now() - time;
        requestAnimationFrame.now = function() {
          return now() - offset;
        };
        requestAnimationFrame.method = 'native-highres-noperf';
      }
    } else {
      requestAnimationFrame.now = now;
    }
  });
  requestAnimationFrame.now = ((_ref1 = window.performance) != null ? _ref1.now : void 0) != null ? (function() {
    return window.performance.now();
  }) : now;
  requestAnimationFrame.method = method;
  window.requestAnimationFrame = requestAnimationFrame;
})();