0

I am working on a dynamic canvas animation that will load and animate multiple images.

Seems I've run into a race condition, and I don't know how to resolve it.

This object constructor loads the images, creates objects for them, and sets width and height variables as 1/2 of the natural image size.

window.imgWidth;
window.imgHeight;

//array to hold character objects
window.character = [];
window.characterPosition;

function Character(name, x, y){
    //define the image object within the Character
    this.imageObject = new Image();
    this.imageObject.src = 'data:text/javascript;base64,'+name;

    window.character.push(this);
    window.characterPosition = window.character.indexOf(this);

    //set natural width and natural height once the image is loaded
    //conditional used by Chrome
    if (this.imageObject.addEventListener){
        this.imageObject.addEventListener('load', function(){
            window.imgWidth = this.naturalWidth/2;
            window.imgHeight = this.naturalHeight/2;

            //set natural width and natural height to object
            window.character[characterPosition]['imageObject']['w'] = window.character[characterPosition]['imageObject']['w0'] = window.imgWidth;
            window.character[characterPosition]['imageObject']['h'] = window.character[characterPosition]['imageObject']['h0'] = window.imgHeight;

            //set initial x and y position
            window.character[characterPosition]['imageObject']['x1'] = x;
            window.character[characterPosition]['imageObject']['y1'] = y;

            //set loaded property for the object once loading is done
            window.character[characterPosition]['imageObject']['loaded'] = true;

            console.log(characterPosition);
            console.log(window.character[characterPosition]['imageObject']);

            function imageLoaded(element, index, array){
                return element['imageObject']['loaded'] == true;
            }

            //test whether every object in array has the image loaded
            if(character.every(imageLoaded)){
                $('button#play').show();
            };
        });
    }
} //end object constructor

Inside document ready, I am using this object constructor to create two objects.

var sun0 = new Character('iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAW0lEQVR42mL8//8/AzpgZGTcC6KBcs5wMRwK/0MVMsLEmLAoEmXAApiwiKUhaRJCltgLsQVsWwIQ/wTx0fBeRigD7B6Y24i1mj4Kn4KI7Uie2Y7FI8+B2AMgwABjRynfWgpcxQAAAABJRU5ErkJggg==', 12, 12);
var sun1 = new Character('R0lGODlhCgAKANUCAEKtP0StQf8AAG2/a97w3qbYpd/x3mu/aajZp/b79vT69MnnyK7crXTDcqraqcfmxtLr0VG0T0ivRpbRlF24Wr7jveHy4Pv9+53UnPn8+cjnx4LIgNfu1v///37HfKfZpq/crmG6XgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAZIQIGAUDgMEASh4BEANAGAxRAaaHoYAAPCCZUoOIDPAdCAQhIRgJGiAG0uE+igAMB0MhYoAFmtJEJcBgILVU8BGkpEAwMOggJBADs=',0,0);

The problem is that since this.imageObject.addEventListener('load', function(){ is asynchronous, the console.log messages that run each time the object constructor is run, both display information from the second object. I want the first set to display information from the first object and the second set to display information from the second object.

//First set, run by sun0
  console.log(characterPosition); //1
  console.log(window.character[characterPosition]['imageObject']); //info for sun1
//Second set, run by sun1
  console.log(characterPosition); //1
  console.log(window.character[characterPosition]['imageObject']); //info for sun1

JS Fiddle demonstrating this

I was able to work around it by setting a timer like so:

var sun0 = new Character('iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAW0lEQVR42mL8//8/AzpgZGTcC6KBcs5wMRwK/0MVMsLEmLAoEmXAApiwiKUhaRJCltgLsQVsWwIQ/wTx0fBeRigD7B6Y24i1mj4Kn4KI7Uie2Y7FI8+B2AMgwABjRynfWgpcxQAAAABJRU5ErkJggg==', 12, 12);
window.setTimeout(slowLoad, 2000);
function slowLoad(){
    var sun1 = new Character('R0lGODlhCgAKANUCAEKtP0StQf8AAG2/a97w3qbYpd/x3mu/aajZp/b79vT69MnnyK7crXTDcqraqcfmxtLr0VG0T0ivRpbRlF24Wr7jveHy4Pv9+53UnPn8+cjnx4LIgNfu1v///37HfKfZpq/crmG6XgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAZIQIGAUDgMEASh4BEANAGAxRAaaHoYAAPCCZUoOIDPAdCAQhIRgJGiAG0uE+igAMB0MhYoAFmtJEJcBgILVU8BGkpEAwMOggJBADs=',0,0);
}

This worked as expected.

//First set, run by sun0
      console.log(characterPosition); //0
      console.log(window.character[characterPosition]['imageObject']); //info for sun0
//Second set, run by sun1
      console.log(characterPosition); //1
      console.log(window.character[characterPosition]['imageObject']); //info for sun1

JS Fiddle for this approach

But obviously, I don't want to introduce a manual delay in order to get the function for sun1 to run only after sun0 completes. How can I get var sun1 to run immediately after sun0 finishes its function (including the asynchronous part)? Keep in mind that I will be doing this for several (about 15) other images as well.

Mike Eng
  • 1,593
  • 4
  • 34
  • 53
  • Don't use global variables in the callback. Use local variables so they'll be captured in the closure. – Barmar Dec 24 '15 at 05:05

1 Answers1

0

There's probably a better way, but I hacked around this with a function that has a bunch of conditional statements in it to see if an object is defined before I run the function that creates that object.

function loadImages(){
    if (!character[0]){
        var sun0 = new Character('sun0', sunParent.x, sunParent.y);
    } else if (!character[1]){
        var sun1 = new Character('sun1', sunParent.x, sunParent.y);
    } else if (!character[2]){
        var sun2 = new Character('sun2', sunParent.x, sunParent.y);
    } else if (!character[3]){
        var bed = new Character('bed', 50, 500);
    } else if (!character[4]){
        var bed = new Character('plant1', 480, 490);
    } else if(character.every(imageLoaded)){
        $('button#play').show();
    }

    //test whether every object in array has the image loaded
    function imageLoaded(element, index, array){
        return element['imageObject']['loaded'] == true;
    }
}

I call loadImages(); initially on document ready and also call it within the Character construction at the end inside the asynchronous part that loads the image.

view full file on github.

Mike Eng
  • 1,593
  • 4
  • 34
  • 53