Creating multiple canvas patterns fails

Viewed 235

I am trying to create an array of patterns to use for my Chart.js doughnut chart so I do this:

var surfacePatterns
var surfaceNames = ['Unknown', 'Paved', 'Unpaved', 'Concrete', 'Cobblestone', 'Metal', 'Wood', 'Ground', 'Sand', 'Grass']

$(document).ready(() => {    
    surfacePatterns = []
    console.log(surfacePatterns)
    for (i = 0; i < surfaceNames.length; i++) {
        var temp = new Image()
        temp.onload = () => surfacePatterns.push($("#canvas1")[0].getContext('2d').createPattern(temp, 'repeat'))
        temp.src = '../img/surfaces/'+ surfaceNames[i] + '.jpg'
    }
});

function chart(){
   console.log(surfacePatterns)
}

The chart() function is called by a button press. I deliberately wait for a few seconds so it can load the images and create the patterns but when I call the function it gives me this:

console

Out of the nine images, only 8 and 9 got converted into a pattern and the rest is null. I have no idea what's going on because I get no errors and all images are found. If I change a String in surfaceNames I, as expected, get an error that an image couldn't be found and if I use patterns 8 and 9 they work perfectly fine.

Another weird thing: If I change

temp.onload = () => surfacePatterns.push($("#canvas1")[0].getContext('2d').createPattern(temp, 'repeat'))

to

temp.onload = () => surfacePatterns[i] = canvas1[0].getContext('2d').createPattern(temp, 'repeat')

The entire array is entirely empty (=/= null). Also, the first log after initializing the array as an empty array gives me an empty array with a length of 9 and I have no idea why since nothing else has happened yet.

2 Answers

You need to wait for all of the patterns to load before you can log the array. In fact, the array should be the result of awaiting all the patterns, rather than a variable in the outer scope that you populate asynchronously. See Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference

As for how to solve this, you can use Promise.all() after converting each callback into a pending promise:

const surfaceNames = ['stackoverflow.com', 'codegolf.stackexchange.com', 'superuser.com', 'codereview.stackexchange.com'];
const context = document.querySelector('#canvas1').getContext('2d');
const promises = surfaceNames.map(surfaceName =>
  new Promise((resolve, reject) => {
    const image = new Image();

    image.addEventListener('load', () => {
      resolve(context.createPattern(image, 'repeat'));
    });
    image.addEventListener('error', () => {
      reject(new Error(`${surfaceName} failed to load ${image.src}`));
    });
    image.src = `https://${surfaceName}/favicon.ico`;
  }).catch(error => {
    // populating array with null entry and logging error
    // instead of ignoring all the other pending promises
    console.log(error.message);
    return null;
  })
);

Promise.all(promises).then(surfacePatterns => {
  console.log(surfacePatterns.map(pattern => pattern.constructor.name));
});
<canvas id="canvas1"></canvas>

As you can see, each of these successfully resolves with a CanvasPattern.

When calling an async function several times in this case inside a "for", you have to wait for all the calls. If you don't wait they you lost some async calls and you got an empty array.

If you can put a snipped I'll try to fix that problem.

window.surfacePatterns = [];
window.c = 0;
//window.surfaceNames = ['Unknown', 'Paved', 'Unpaved', 'Concrete', 'Cobblestone', 'Metal', 'Wood', 'Ground', 'Sand', 'Grass'];
window.surfaceNames = ["https://www.worldatlas.com/r/w1200-h630-c1200x630/upload/37/99/85/northern-pygmy-owl.jpg",
  "https://www.friendsofwehr.org/wp-content/uploads/2013/06/Great-horned_Owl_RWD_at_CRC1transparent.jpg"
];

$(document).ready(() => {
  console.log('Step 1: ', window.surfacePatterns);
  for (i = 0; i < window.surfaceNames.length; i++) {
    var temp = new Image();
    temp.onload = function() {
      var my = $(".canvas1")[0].getContext('2d').createPattern(temp, 'repeat');
      console.log('Step inside async: ', my);
      window.surfacePatterns.push(my);
      window.c++;
      if (window.surfaceNames.length === c) {
        console.log('Complete async calls: ', window.surfacePatterns);
      }
    }
    //temp.src = '../img/surfaces/'+ window.surfaceNames[i] + '.jpg';
    temp.src = "https://www.friendsofwehr.org/wp-content/uploads/2013/06/Great-horned_Owl_RWD_at_CRC1transparent.jpg"

  }
  console.log('Outside For: ', window.surfacePatterns);
});

function chart() {
  console.log(window.surfacePatterns);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas class="canvas1"></canvas>
<canvas class="canvas1"></canvas>

Related