How do I draw images in layers on canvas?

Viewed 111

I have a canvas where I use drawImage to draw a bunch of images to the canvas.

How I want the result to be:

I want the first image i draw to be on layer 1, the next image on layer 2 and so on

What really happens:

The images get placed on random layers.

const images = [
    'https://attefallsverket.picarioxpo.com/1_series_base.jpg?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_housebase.png?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_roof_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_windows.pfs?1=1&p.c=71343a&p.tn=&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_door_01.pfs?1=1&p.c=&p.tn=rainsystem_grey.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_01.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_corners.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_tin_windows.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_tin_roof.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_roof_metal_orange.png?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_rain_system.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1_series_terrace.png?1=1&width=2000',
];

let c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

for(let i=0; i<images.length; i++) {
    let img = new Image();
    img.crossOrigin = '';
    img.src = images[i]
    img.onload = () => {
        ctx.drawImage(img, 0, 0, c.width, c.height);
    }
}
<canvas id="myCanvas" width="280" height="157.5" style="border:1px solid #d3d3d3;">
    Your browser does not support the HTML5 canvas tag.
</canvas>

4 Answers

You would need to ensure that the first image has loaded before launching the load of the next. So make an asynchronous loop:

const images = [
    'https://attefallsverket.picarioxpo.com/1_series_base.jpg?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_housebase.png?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_roof_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_windows.pfs?1=1&p.c=71343a&p.tn=&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_door_01.pfs?1=1&p.c=&p.tn=rainsystem_grey.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_01.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_corners.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_tin_windows.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_tin_roof.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_roof_metal_orange.png?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_rain_system.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1_series_terrace.png?1=1&width=2000',
];

let c = document.getElementById("myCanvas");
let ctx = c.getContext("2d");

(function loop(i) {
    if (i >= images.length) return; // all done
    let img = new Image();
    img.crossOrigin = '';
    img.onload = () => {
        ctx.drawImage(img, 0, 0, c.width, c.height);
        loop(i+1); // continue with next...
    }
    img.src = images[i];
})(0); // start loop with first image
<canvas id="myCanvas" width="280" height="157.5"</canvas>

Here's what you really want to do:

addEventListener('load', ()=>{ // page and script load
const images = [
    'https://attefallsverket.picarioxpo.com/1_series_base.jpg?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_housebase.png?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_roof_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_windows.pfs?1=1&p.c=71343a&p.tn=&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_door_01.pfs?1=1&p.c=&p.tn=rainsystem_grey.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_01.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_facade_corners.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_tin_windows.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_tin_roof.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_roof_metal_orange.png?1=1&width=2000',
    'https://attefallsverket.picarioxpo.com/1kp_rain_system.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
    'https://attefallsverket.picarioxpo.com/1_series_terrace.png?1=1&width=2000'
];
const canvas = document.getElementById('myCanvas'), ctx = canvas.getContext('2d'), promises = [];
let w = canvas.width, h = canvas.height, p;
for(let m of images){
  p = new Promise(r=>{
    const im = new Image;
    im.onload = ()=>{
      r(im);
    }
    im.src = m;
  });
  promises.push(p);
}
Promise.all(promises).then(imgs=>{
  for(let im of imgs){
    ctx.drawImage(im, 0, 0, w, h);
  }
});
}); // end page load
<canvas id='myCanvas' width='280' height='157.5'></canvas>

The problem is that you you can't really control how long it will take the browser to download each image. So the first image that fires the onload event might not be the first image in the array - likewise the second picture might be 10th in the array and so on.

To workaround I'd recommend going through your images array one by one and start loading a new image as soon as the last image finished loading.

Here's an example:

const images = [
  'https://attefallsverket.picarioxpo.com/1_series_base.jpg?1=1&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_housebase.png?1=1&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_facade_roof_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_windows.pfs?1=1&p.c=71343a&p.tn=&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_door_01.pfs?1=1&p.c=&p.tn=rainsystem_grey.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_facade_01.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_facade_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_facade_corners.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_tin_windows.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_tin_roof.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_roof_metal_orange.png?1=1&width=2000',
  'https://attefallsverket.picarioxpo.com/1kp_rain_system.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000',
  'https://attefallsverket.picarioxpo.com/1_series_terrace.png?1=1&width=2000',
];
let imagesLoaded = 0;
let c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");


function loadImage() {
  let img = new Image();
  img.crossOrigin = '';

  img.onload = () => {
    ctx.drawImage(img, 0, 0, c.width, c.height);
    if (imagesLoaded + 1 < images.length) {
      imagesLoaded++;
      loadImage(imagesLoaded);
    }
  }
  img.src = images[imagesLoaded];
}

loadImage(0)
<canvas id="myCanvas" width="280" height="157.5" style="border:1px solid #d3d3d3;">
        Your browser does not support the HTML5 canvas tag.
    </canvas>

Existing answers solve the problem, but they're serialized and don't fire off requests simultaneously. If you want to optimize for getting something on the screen and don't care how long it takes to reach the complete image, this is fine, but if your goal is to get the entire image drawn as quickly as possible and/or not show a partially-completed image, one-by-one network requests are suboptimal.

Instead of this:

request image 0
wait for request 0 over the wire or file IO
draw image 0
request image 1
wait for request 1 over the wire or file IO
draw image 1
...
request image n
wait for request n over the wire or file IO
draw image n

It might make sense to:

request/load all images at once
wait for all images to be received
draw all images in order

The idea is to exploit parallelism and only wait for one image (the slowest) to arrive, overlapping all other requests within the slowest load time, rather than incurring the cost of loading all n images one at a time.

A good way to do this is with promises. You can promisify the onload and onerror callbacks to resolve and reject respectively, then use Promise.all to wait for all images to arrive, at which point you can apply a traditional, synchronous loop to draw the layers in order.

const images = ['https://attefallsverket.picarioxpo.com/1_series_base.jpg?1=1&width=2000','https://attefallsverket.picarioxpo.com/1kp_housebase.png?1=1&width=2000','https://attefallsverket.picarioxpo.com/1kp_facade_roof_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_windows.pfs?1=1&p.c=71343a&p.tn=&width=2000','https://attefallsverket.picarioxpo.com/1kp_door_01.pfs?1=1&p.c=&p.tn=rainsystem_grey.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_facade_01.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_facade_panels.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_facade_corners.pfs?1=1&p.c=&p.tn=wooden_summer_green.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_tin_windows.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_tin_roof.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000','https://attefallsverket.picarioxpo.com/1kp_roof_metal_orange.png?1=1&width=2000','https://attefallsverket.picarioxpo.com/1kp_rain_system.pfs?1=1&p.c=&p.tn=rainsystem_white.jpg&width=2000','https://attefallsverket.picarioxpo.com/1_series_terrace.png?1=1&width=2000',];

const c = document.getElementById("myCanvas");
const ctx = c.getContext("2d");

Promise.all(images.map(url =>
  new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = "";
    img.onerror = e => reject(`${url} failed to load`);
    img.onload = function () { 
      resolve(this);
    };
    img.src = url;
  })))
  .then(images =>
    images.forEach(e =>
      ctx.drawImage(e, 0, 0, c.width, c.height)
    )
  )
  .catch(err => console.error(err))
;
<canvas id="myCanvas" width="280" height="157.5" style="border:1px solid #d3d3d3;">
    Your browser does not support the HTML5 canvas tag.
</canvas>

If your goal is to get something on screen as quickly as possible, you could combine the two approaches, doing one fast, serial request for the background, then doing the rest in parallel, or even in batches. But this feels like overkill for this case; I mention the technique for completeness.

Related