0

I have a data set which contains an array of layers for an HTML5 canvas. I’m using fabric.js to manage all elements of my canvas. The layers are supposed to load in order, forming different scenes. I’m looping through the data set via a forEach loop. (EDIT - after further research, I see that both for and forEach loops are synchronous).

The problem is, once the code gets to the point of loading an image, asynchronous behavior kicks in and the process jumps to the next iteration of my loop to start processing the next layer. As you can imagine, this leads to layers appearing out of order on the canvas and ultimately distorted scenes.

I’ve been working on this and researching it for weeks and haven’t had much luck.

Is there any way to prevent a forEach loop (or a for loop for that matter) to not start the next iteration before the current one finishes?

Here is my code for the forEach loop:

$.ajax({
    url: url,
    method: "GET"
}).done(function (data) {
    data.forEach(function (data) {
        switch (data.layer_type) {
            case "texture":
                {
                    addTexture(data)
                    break;
                }
            case "color":
                {
                    addColor(data)
                    break;
                }
            case "room":
                {
                    addBaseImg(data);
                    break;
                }
            case "decor":
                {
                    addBaseImg(data);
                    break;
                }
            case "floor":
                {
                    addBaseImg(data);
                    break;
                }
            default:
                {
                    addOtherObjects(data);
                }
        }
    });
});

Here is an example of a case that loads an image:

The initial function triggers a click event:

function addTexture(data) {
    $('img[data-obj-id="' + data.object_id + '"]').trigger("click");
}

that leads to this function:

$(document).on("click", ".img-patt", function () {

    var imgURL = $(this).data("src");

    var _this = this;

    //this is where the process skips to the next iteration, before the image is loaded and added to the canvas

    fabric.Image.fromURL(imgURL, function (img) {

        //some code to handle the pattern removed from here just to shorten up the snippet

        var rect = new fabric.Rect({
            name: "texture",
            visible: true,
            selectable: false,
            data: {
                type: $(_this).data("type"),
                objid: $(_this).data("obj-id")
            },
            top: 0,
            left: 0,
            width: fabCanvas.width,
            height: fabCanvas.height,
            fill: pattern
        });

        fabCanvas.add(rect);
        fabCanvas.renderAll();
    });

});
  • 1
    No. You cannot make a loop wait. But you can mimic a loop with delay using recursion – Rajesh Jun 04 '17 at 05:29
  • I recently [answered](https://stackoverflow.com/a/43535024/3783478) a similar [question](https://stackoverflow.com/questions/43534797/keep-for-loop-from-executing-in-javascript/43535024#43535024). You can try to embed it – Rajesh Jun 04 '17 at 05:36
  • Check out this https://stackoverflow.com/questions/16149431/make-function-wait-until-element-exists Do tell if it helps! Cheers! – Anirudh shetty Jun 04 '17 at 05:39
  • *"I chose forEach because it processes synchronously as opposed to a for loop, which processes asynchronously"* – this is a wrong understanding. Both `for` and `Array.prototype.forEach` are synchronous – Mulan Jun 04 '17 at 06:36

2 Answers2

0

If you want to handle it manually instead of relying on some kind of Promise implementation ( bluebird for example ) you can use this logic.

Overall looks more complicated, but apart the fact that should work, when you see and undertand this, adding your application knowledge you will be able to do something smaller, better.

The concept is not adding the images on canvas but create an ordered array of objects already loaded, then when you load them all, add that array of object on the canvas.

Your onclick becomes:

function addToCanvas(object) {
  canvas.add(object);
}

$(document).on("click", ".img-patt", function () {

   var imgURL = $(this).data("src");

   addSomething(imgUrl, null, null, addToCanvas)

});

Your add something is:

function addSomething(imgUrl, index, objectStack, callback) {

        //this is where the process skips to the next iteration, before the image is loaded and added to the canvas

        fabric.Image.fromURL(imgURL, function (img) {

            //some code to handle the pattern removed from here just to shorten up the snippet

            var rect = new fabric.Rect({
                name: "texture",
                visible: true,
                selectable: false,
                data: {
                    type: $(_this).data("type"),
                    objid: $(_this).data("obj-id")
                },
                top: 0,
                left: 0,
                width: fabCanvas.width,
                height: fabCanvas.height,
                fill: pattern
            });

            objectStack && objectStack[index] = rect;
            callback(rect)
          });
}

You main loop becomes: (for loop and forEach are both sincronous)

$.ajax({
    url: url,
    method: "GET"
}).done(function (data) {
    var objectStack = new Array(data.length);
    var loaded = data.length;
    var onFinish = function() {
      loaded--
      if (loaded === 0) {
        objectStack.forEach(function(object) {
          canvas.add(object);
        });
      }
    }
    data.forEach(function (data, index) {
        var src = $('img[data-obj-id="' + data.object_id + '"]').data('src');
        switch (data.layer_type) {
            case "texture":
                {
                    addSomething(src, index, objectStack, onFinish)
                    break;
                }
            case "color":
                {
                   ...
                }
            case "room":
                {
                    ...
                }
            case "decor":
                {
                   ...
                }
            case "floor":
                {
                    ...
                }
            default:
                {
                    ...
                }
        }
    });
});
AndreaBogazzi
  • 14,323
  • 3
  • 38
  • 63
  • I implemented this methodology and it worked perfectly! Thanks so much for this. I was able to use this same methodology for another portion of my app that was having similar issues (elements loading out of order due to images loading fromURL) and it fixed that issue as well. – Vincent Visconti Jun 04 '17 at 20:15
-2

You may want to use a trick and you should get what you want.

Before starting the for set a variable that force the wait.

Now I show you in your case

var wait = false;
$.ajax({
    url: url,
    method: "GET"
}).done(function (data) {
    wait = true;
    data.forEach(function (data) {
        switch (data.layer_type) {
            case "texture":
                {
                    addTexture(data)
                    break;
                }
            ...
            default:
                {
                    addOtherObjects(data);
                }
        }
    });
    wait=false;
});

And at the click event you have to do so

$(document).on("click", ".img-patt", function () {
    if (wait){
    // do samething
    }else{

    }
});

In addition if you put a timer and a function becomes more reliable

function DoSamething(){
    if (wait){
        // do samething
    }else{
        window.setInterval(DoSamething, 1000);
    }
}

Your click event will be so

$(document).on("click", ".img-patt", function () {
     window.setInterval(DoSamething, 1000);
});