Tom Davies
Tom Davies

Reputation: 161

How do I draw multiple images on a canvas using a single function?

I am trying to draw multiple images onto an HTML Canvas element.

assetData is an array of objects, each containing the data required to write an image to the canvas, fetched from a database. Crucially, assetData could be of any length: It could contain one image, or it could contain 100.

The code below does not work, as gfxCtx.drawImage() draws the final image specified in the array multiple times, rather than each individual image.

for(var x = 0; x < assetData.length; x++) {
  var layer = eval(assetData[x])
  var gfx = new Image
  gfx.src = "images/" + layer.src
  gfx.onload = function() {
    gfxCtx.drawImage(gfx, layer.x, layer.y, layer.w, layer.h)
    compile()
  }
}

compile() handles drawing the images onto the canvas

If there were a fixed number of images to be drawn, I would assign each to a separate variable. In this instance, however, I do not necessarily know how many images are to be drawn, and I would like to keep the code fairly concise.

Is there a clean, straightforward solution to this problem?

Thanks!

Upvotes: 2

Views: 568

Answers (3)

traktor
traktor

Reputation: 19376

This is a FAQ about the effects of asynchronous code. When an image onload handler is called back later, it sees the value of layer and gfx set in the last loop iteration. This results in the last image being drawn multiple times into the last layer's canvas area. (I would be surprised if errors were not sometimes generated on the console as well.)

The simplest solution is to declare the layer and gfx variables using let. This makes them block scoped and creates a separate binding for each loop iteration: the callback sees the variable values for the iteration in which the call back function object was created.

For extended discussion of the topic please refer to JavaScript closure inside loops – simple practical example

Upvotes: 1

Dacre Denny
Dacre Denny

Reputation: 30390

To resolve this issue, consider using the forEach() method on your assetData array, rather than a regular for loop as you currently are.

This has to advantage of creating a unique "closure" for each loop iteration, which in turn allows you to correctly draw each unique image into the canvas as required:

assetData.forEach(function(assetItem) {

  // This function has a unique closure which
  // allows you to draw the current assetItem
  // to the canvas to achieve the desired result
  var layer = eval(assetItem);
  var gfx = new Image();
  gfx.src = "images/" + layer.src;
  gfx.onload = function() {

    gfxCtx.drawImage(gfx, layer.x, layer.y, layer.w, layer.h);
    compile();
  }

});

Upvotes: 2

Neil VanLandingham
Neil VanLandingham

Reputation: 1086

Try referencing the onload event target instead of gfx because the value of gfx may have changed by the time the image loads.

for(var x = 0; x < assetData.length; x++) {
    var layer = eval(assetData[x])
    var gfx = new Image
    gfx.src = "images/" + layer.src
    gfx.onload = function(e) {
       gfxCtx.drawImage(e.target, layer.x, layer.y, layer.w, layer.h)
   compile()
  }
}

Upvotes: 0

Related Questions