Question

I am trying to update a javascript canvas by drawing pixels directly onto the imageData buffer. Basically I am updating all the pixels on the imageData buffer after each mousemove / touchmove event, and trying to get the best possible performance.

Background : I am developing an application based on emscripten, where the drawing on the canvas is fully drawn pixel by pixel by the "native" code. The example I give in this question is a simpler example where I reproduced my issue.

I currently have encoutered two performance issues :

  • on iOS safari (tested on an iPad air) : the drawing function is called at 31 fps, but the canvas rendering on the screen is laggy (visually, I would say that it is updated at 10fps max, plus some intervals of 0.5 seconds where it is not updated at all)
  • on iOS Chrome : the performance is awful, since I get 2.9 fps

On a desktop mac, I get a steady performance : 55 fps with firefox and 45 fps with chrome

So, I have two questions

  • How would force the canvas to be refreshed faster on iOs safari (in order to have a real 30 fps rendering, or may be a little lower) ?
  • How would you optimize the performance ? Did I miss a possible optimization ?

Please refer to the code below : it is a single html file that reproduces my problems.

I know I could use a webworker, but since I am using emscripten this would not be optimal (each webworker starts with a fresh memory, and I need to keep record of the state).

See code here (it is a single html file, the js is self contained). Please move the mouse inside the canvas in order to see the calculated fps.

<canvas width=800 height=600 id="canvas"> </canvas>

<script>


//Disable scroll : usefull for tablets where touch events
//will scroll the page
function DisableScroll()
{
  window.addEventListener("touchmove", function(event) {
    if (!event.target.classList.contains('scrollable')) {
      // no more scrolling
      event.preventDefault();
    }
  }, false);
}

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
          };
})();


window.countFPS = (function () 
{
  var nbSamples = 20; //number of samples before giving a fps
  var counter = 0;
  var fps = 0;
  var timeStart = new Date().getTime();
  return function()
  {
    counter++;
    if (counter == nbSamples)
    {
      var timeEnd = new Date().getTime();
      var delaySeconds = (timeEnd - timeStart) / 1000;
      fps = 1 / delaySeconds * nbSamples;

      counter = 0;
      timeStart = timeEnd;
    }
    return fps.toFixed(2);
  }
}());


function getMousePos(canvas, evt) {
  var rect = canvas.getBoundingClientRect();
  return {
    x: evt.clientX - rect.left,
    y: evt.clientY - rect.top
  };
}
function getTouchPos(canvas, evt) {
  var rect = canvas.getBoundingClientRect();
  return {
    x: evt.targetTouches[0].clientX - rect.left,
    y: evt.targetTouches[0].clientY - rect.top
  };
}


DisableScroll();

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext("2d");
var canvasData = "empty";

function myDraw(pos)
{
  canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  var binaryData = canvasData.data;

  var idx = 0;
  for (y = 0; y < canvas.height; y++)
  {
    for (x = 0; x < canvas.width; x++)
    {
      //Red
      binaryData[idx ++] = x % 255;
      //Green : add a little animation on the green channel
      //var dist = Math.sqrt( (pos.x - x) * (pos.x - x) + (pos.y - y) * (pos.y - y));
      var dist = Math.abs(pos.x - x) + Math.abs(pos.y - y);
      var g = 255 - dist;
      if ( g < 0 )
        g = 0;
      binaryData[idx++] = g;
      //Blue
      binaryData[idx ++] = y % 255;
      //Alpha
      binaryData[idx ++] =  255;
    }
  }

  ctx.putImageData(canvasData, 0, 0);
}



var OnLoad = function()
{
  myDraw({x:0, y:0});
}


//
// Mouse & touch callbacks
//
function CanvasMouseMove(pos)
{
  myDraw(pos);
  var elem = document.getElementById("fps");
  elem.value = window.countFPS();

}
canvas.addEventListener("touchmove", function(e){ CanvasMouseMove( getTouchPos(canvas, e)); } , false);
canvas.addEventListener("mousemove", function(e){ CanvasMouseMove( getMousePos(canvas, e) ); });


</script>

<body onload=OnLoad()>
<br/>
FPS<input type=text id="fps" />&nbsp;&nbsp;&nbsp;
</body>
Was it helpful?

Solution

Rq :
- avoid leaking global and declare x,y as vars in myDraw.
The suggestions :
- cache canvas.width and canvas.height to avoid DOM access,
- cache pos.x and pos.y
- trade (% 255) for (& 0xFF)
- cache Math.abs
- just create ONE imageData that you keep on modifying (relieves the g.c.).
- draw on requestAnimationFrame (otherwise you might have to wait for a frame to draw).
- cache the bounding rect of the canvas (and its top/left values).

jsbin is here :

http://jsbin.com/saruzoqo/4/

you can switch old/new with 2 buttons.

looks like

var staticCanvasData = ctx.getImageData(0, 0, canvas.width, canvas.height);

function myDraw2(pos) {
    canvasData = staticCanvasData;
    var binaryData = canvasData.data;
    var cw = canvas.width,
        ch = canvas.height;
    var posX = pos.x,
        posY = pos.y;
    var idx = 0;
    var abs = Math.abs;
    for (var y = 0; y < ch; y++) {
        var yDiff = abs(posY - y) ;
        for (var x = 0; x < cw; x++) {
            //Red
            binaryData[idx++] = x & 0xFF;
            //Green : add a little animation on the green channel
            //var dist = Math.sqrt( (pos.x - x) * (pos.x - x) + (pos.y - y) * (pos.y - y));
            var dist = abs(posX - x) + yDiff;
            var g = 255 - dist;
     //       if (g < 0) g = 0; // useless array is clamped
            binaryData[idx++] = g;
            //Blue
            binaryData[idx++] = y & 0xFF;
            //Alpha
            binaryData[idx++] = 255;
        }
    }
    ctx.putImageData(canvasData, 0, 0);
}

The results are quite good, FF takes half time (10 vs 20ms) time, Chrome 15 ms less (116 (!) to 100), and safari takes 7 instead of 20 !! (mac OS)

i did not investigate a lot, but it seem the fact alone not to create/copy a imageData on each redraw accounts for more than 60% of the gains.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top