0

I'm working on a small canvas animation that requires me to step through a large sprite sheet png so I'm getting a lot of mileage out of drawImage(). I've never had trouble in the past using it, but today I'm running into an odd blocking delay after firing drawImage.

My understanding is that drawImage is synchronous, but when I run this code drawImage fired! comes about 700ms before the image actually appears. It's worth noting it's 700ms in Chrome and 1100ms in Firefox.

window.addEventListener('load', e => {
    console.log("page loaded");

    let canvas = document.getElementById('pcb');
    let context = canvas.getContext("2d");

    let img = new Image();

    img.onload = function() {
        context.drawImage(
            img,
            800, 0,
            800, 800,
            0, 0,
            800, 800
        );

        console.log("drawImage fired!");
    };

    img.src = "/i/sprite-comp.png";
});

enter image description here

In the larger context this code runs in a requestAnimationFrame loop and I only experience this delay during the first execution of drawImage.

I think this is related to the large size of my sprite sheet (28000 × 3200) @ 600kb though the onload event seems to be firing correctly.

edit: Here's a printout of the time (ms) between rAF frames. I get this result consistently unless I remove the drawImage function.

enter image description here

maxmclau
  • 79
  • 10
  • Can we see the larger context? – Ry- Jul 01 '19 at 20:50
  • The image should be loaded already after the onload event fires. I've checked by logging the naturalWidth and complete properties of the image object. Both return proper values directly after the onload event. – maxmclau Jul 01 '19 at 21:09
  • I'm not sure what part of the larger script I could add here. What's show in that gif demonstrates the same issue I'm having with the full script and is just the code posted. – maxmclau Jul 01 '19 at 21:11

1 Answers1

2

That's because the load event only is a network event. It only tells that the browser has fetched the media, parsed the metadata, and has recognized it is a valid media file it can decode.
However, the rendering part may still not have been made when this event fires, and that's why you have a first rendering that takes so much time. (Though it used to be an FF only behavior..)

Because yes drawImage() is synchronous, It will thus make that decoding + rendering a synchrounous operation too. It's so true, that you can even use drawImage as a way to tell when an image really is ready..

Note that there is now a decode() method on the HTMLImageElement interface that will tell us exactly about this, in a non-blocking means, so it's better to use it when available, and to anyway perform warming rounds of all your functions off-screen before running an extensive graphic app.


But since your source image is a sprite-sheet, you might actually be more interested in the createImageBitmap() method, which will generate an ImageBitmap from your source image, optionally cut off. These ImageBitmaps are already decoded and can be drawn to the canvas with no delay. It should be your preferred way since it will also avoid that you draw the whole sprite-sheet every time. And for browsers that don't support this method, you can monkey patch it by returning an HTMLCanvasElement with the part of the image drawn on it:

if (typeof window.createImageBitmap !== "function") {
  window.createImageBitmap = monkeyPatch;
}

var img = new Image();
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/b/be/SpriteSheet.png";
img.onload = function() {
  makeSprites()
    .then(draw);
};


function makeSprites() {
  var coords = [],
    x, y;
  for (y = 0; y < 3; y++) {
    for (x = 0; x < 4; x++) {
      coords.push([x * 132, y * 97, 132, 97]);
    }
  }
  return Promise.all(coords.map(function(opts) {
      return createImageBitmap.apply(window, [img].concat(opts));
    })
  );
}

function draw(sprites) {
  var delay = 96;
  var current = 0,
    lastTime = performance.now(),
    ctx = document.getElementById('canvas').getContext('2d');
  anim();

  function anim(t) {
    requestAnimationFrame(anim);
    if (t - lastTime < delay) return;
    lastTime = t;
    current = (current + 1) % sprites.length;
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
    ctx.drawImage(sprites[current], 0, 0);
  }

}

function monkeyPatch(source, sx, sy, sw, sh) {
  return Promise.resolve()
    .then(drawImage);

  function drawImage() {
    var canvas = document.createElement('canvas');
    canvas.width = sw || source.naturalWidth || source.videoWidth || source.width;
    canvas.height = sh || source.naturalHeight || source.videoHeight || source.height;
    canvas.getContext('2d').drawImage(source,
      sx || 0, sy || 0, canvas.width, canvas.height,
      0, 0, canvas.width, canvas.height
    );
    return canvas;
  }
}
<canvas id="canvas" width="132" height="97"></canvas>
Kaiido
  • 123,334
  • 13
  • 219
  • 285