Wait for image loading to complete in JavaScript - javascript

I'm loading images with JavaScript. Something like this:
images[0]=new Image();
images[0].onload=function(){loaded++;console.log(loaded)};
images[0].src="assets/img/image.png";
When I look at the log, I see that all the images are loaded nicely, since the value of the "loaded" variable increases with each loaded image.
However I would like to stop any further action to be executed until this amount reaches it's maximum, so right after setting up the images, I place a while cycle.
while(loaded<11){
document.getElementById("test").innerHTML="Loading "+loaded+"/11";
console.log(loaded);
}
//Some code here which should only run after everything has been loaded
//In other words: when the statement in the while cycle becomes false
However my browser simply crashes, since the while seems to be stuck in an infinite loop. When I check the log, I see that "0" was written 1000 times, and after that, the numbers from 1 to 11 (which implies that the images in fact gets loaded, but the while does not care about it, and crashes faster than it could happen).
I believe that the method I'm trying to use here is not the right approach to solve this problem.
How can I put everything on hold until every asset which is needed for the site is loaded?

Using promises and async functions, there is a nice way to wait until all the images are loaded (no callbacks, no loaded image counting):
async function loadImages(imageUrlArray) {
const promiseArray = []; // create an array for promises
const imageArray = []; // array for the images
for (let imageUrl of imageUrlArray) {
promiseArray.push(new Promise(resolve => {
const img = new Image();
// if you don't need to do anything when the image loads,
// then you can just write img.onload = resolve;
img.onload = function() {
// do stuff with the image if necessary
// resolve the promise, indicating that the image has been loaded
resolve();
};
img.src = imageUrl;
imageArray.push(img);
}));
}
await Promise.all(promiseArray); // wait for all the images to be loaded
console.log("all images loaded");
return imageArray;
}
Or you can wait for a single image to load:
async function loadImage(imageUrl) {
let img;
const imageLoadPromise = new Promise(resolve => {
img = new Image();
img.onload = resolve;
img.src = imageUrl;
});
await imageLoadPromise;
console.log("image loaded");
return img;
}
You can use it like this (using promise chaining):
loadImages(myImages).then(images => {
// the loaded images are in the images array
})
Or inside an async function:
const images = await loadImages(myImages);

Personally I hate using while()... I think the easiest way to do it, is using event listeners.
var img = new Image;
img.addEventListener("load", function () {
//Img loaded
});
img.src= e.target.result;

Javascript is single threaded. This means that if you add an event listener that listener will not run until the current execution has completed. Hence if you start a loop that relies on a event to end it it will never happen as the event will never fire because the current execution is preventing it from running. Also the events are place on the call stack asynchronously, thus if your execution is slower than the rate of event firing (placing a call on the call stack) you also risk the a page crash. This is a common mistake when using setInterval when the interval is set to less time than the code takes to execute. NEVER USE setInterval.
Just remember Javascript can not do two things at once.
The best way to deal with resource monitored loading is to use setTimeout.
var allLoaded = false;
var imgCount = 0; // this counts the loaded images
// list of images to load
const imageURLS =["a.jpg","b.jpg","c.jpg","d.jpg","e.jpg"];
// array of images
var images = [];
const onImageLoad = function(){ imgCount += 1; } // onload event
// loads an image an puts it on the image array
const loadImage = function(url){
images.push(new Image());
images[images.length-1].src = url
images[images.length-1].onload = onImageLoad;
}
const waitForLoaded = function(){
if(imgCount === images.length){
allLoaded = true; // flag that the image have loaded
}else{
// display the progress here
...
setTimeout(waitForLoaded,100); // try again in 100ms
}
}
// create the images and set the URLS
imageURLS.forEach(loadImage);
setTimeout(waitForLoaded,100); // monitor the image loading

Related

Can image load event be synchronous, or will it always be asynchronous?

Let's say I have the following code:
let img = document.createElement("img");
img.addEventListener("load", function() {
alert("Loaded!");
});
img.src = external_string; // Can "load" fire here?
The question is - if the image in external_string is already in browser cache, or maybe is base64 encoded, or whatever - can it so happen that the load event is called immediately at the same line as src is being assigned?
Note: I actually want it to be asynchronous because I want to set up a few more things before the load event gets fired. But if there is a chance that it might get fired synchronously then I need to put in extra work to postpone that.
I could not find this subtlety explained in any document, but practical tests always call the event asynchronously.
You can use the decode() method to wait for images to be ready. Even if the image loads immediately, queuing the promise micro-task will always ensure code runs after the promise resolves
Promise handlers .then/.catch/.finally are always asynchronous.
Even when a Promise is immediately resolved, the code on the lines below .then/.catch/.finally will still execute before these handlers.
// Base64 data URI requires no loading time
const red_dot = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
const img = document.createElement("img");
img.src = red_dot;
img.decode().then(() => {
console.log("img ready, this will log second");
document.body.append(img);
});
console.log("img loading, this will log first");
Keep in mind that you cannot call decode() before setting src.
The load event will also always trigger and a listener can be added at any time however since it relies on the event loop, control over the order of operations is non-deterministic. That being said, any procedural code you add after setting the src will always execute before the event handler fires.
// Base64 data URI requires no loading time
const red_dot = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
const img = document.createElement("img");
img.addEventListener("load", () => {
console.log("img ready, this will log second");
document.body.append(img);
});
img.src = red_dot;
console.log("img loading, this will log first");

Using Callback in image loading (Sequential execution)

I am playing with image loading and stuck on sequentially executing the code
function processImage(callback) {
console.log("Start");
callback('http://lorempixel.com/400/200/');
console.log("Finish");
}
function loadImage(url) {
console.log('Load Method')
var img = new Image();
img.src = url;
img.onload = function (event) {
console.log('Image Loaded')
}
}
processImage(loadImage);
For the above code, I want the output as Start -> Load Method -> Image Loaded -> Finish
Instead, I am getting Start -> Load Method -> Finish -> Image Loaded
I am having trouble using callback method I guess.
Any help appreciated.
img.onload is setting an event listener - so when the event occurs, it'll call the function. So, unless your image loads in about a millionth of second (the amount of time I guess it takes JS to go to the next line), the console.log("Finish") will happen first.
When the image is fully loaded, the function which is set to .onload is called. As the image loads after JavaScript has processed the next line, this one will happen first. If you really want to indicate the start and finish, put your finish console.log after the image loaded console.log.
img.onload = function (event) {
console.log('Image Loaded')
console.log('Finish)
}

javascript : calculating and returning values after Image loaded

I already checked multiple responses to asynchronous javascript behaviours and experimented with callbacks, experimenting with promises next, but parallell I want to send SoS request here :/
Because often first answers are ("What do you want to do?")
I have to find out all the non-empty or transparent pixels in an Image.(kinda like a mask) and give them back an array of binary arithmetical numbers.
What my program does in short:
it gets Image URL and creates new Image() from it.
it creates a Canvas and adds it to DOM
in image.onLoad() it draws the image on the canvas
after the Image is on canvas, it scans every pixel for its color and gives back a data array.
My problem is to force the calculation to WAIT until the image is loaded. I tried to do with something like this:
return getDataArray(image.onLoad()= function(){
// ...init things...
}));
Still, it goes into the getDataArray function, before the image.onLoad happens.
I'm gonna take a break and walk outside because I'm out of productive ideas.
Here is the original function:
getImageScan: function() {
this.myImage.src = imageScanner.imgUrl;
var is = this;
return is.getArrayFromCanvas(this.myImage.onload = function () {
is.imageHeight = is.myImage.height;
is.imageWidth = is.myImage.width;
is.appendCanvasToBody();
is.initGraphicalContent();
is.drawImageOnCanvas();
console.log("image is loaded");
})
},
getArrayFromCanvas: function () {
console.log("request for calculations");
var booleanJson = this.getJsonFromCanvas()
return this.getBinaryArithmeticFromBooleans(booleanJson);
}
and this is the result
request for calculations
[]
the image is loaded
here is the entire *.js if you want more information (it's my private project in slacktime, so no copyright issues):
https://github.com/Vilkaz/gridToImage/blob/master/web/resources/js/imageScanner.js
You try to pass something to getArrayFromCanvas although it has no parameters. I don't understand why you do this, but I guess you want something like this:
getImageScan: function(callback) {
this.myImage.src = imageScanner.imgUrl;
var is = this;
this.myImage.onload = function () {
is.imageHeight = is.myImage.height;
is.imageWidth = is.myImage.width;
is.appendCanvasToBody();
is.initGraphicalContent();
is.drawImageOnCanvas();
console.log("image is loaded");
callback(is.getArrayFromCanvas());
}
}
One difference between the asynchronous behavior above and your original code is that getImageScan returns immediately and that a callback is called later to "return" the result.

How to execute the following ES6 lines in sequence?

I'm using the following function to load high-res version of images once they have loaded:
store.loadFullPano = (pano) => {
$('#panos-wrapper').addClass('blurred')
setTimeout(function () {
const image = new Image()
image.src = pano.url
image.onload = () => {
pano.url = pano.urlHigh
$('#panos-wrapper').removeClass('blurred')
}
}, 1000)
}
As you can see I'm blurring the low-res images with CSS. It works...except the low-res image shows for a few seconds after the hig-res one has been replaced and the blur has been removed.
I think that's because the two lines inside image.onload are being executed at the same time (or the last one first).
Is there a way to execute the first one and only then the last one?
EDIT:
I used a promise:
var p = new Promise(function (resolve, reject) {
pano.url = pano.urlHigh
console.log('1')
resolve(pano.url)
})
p.then(function () {
$('#panos-wrapper').removeClass('blurred')
console.log('2')
})
But the low-res is still showing for a few seconds. What's a possible solution for this?
From the Vue docs:
you can use Vue.nextTick(callback) immediately after the data is changed. The callback will be called after the DOM has been updated
and
There is also the vm.$nextTick() instance method, which is especially handy inside components, because it doesn’t need global Vue and its callback’s this context will be automatically bound to the current Vue instance
Depending on how it is implemented, however, I'm not sure that "the DOM has been updated" will be enough though, since loading an image may need more time. Maybe by then though, if you can obtain the updated/created hi-res image object though, maybe you can wait for its onload...
Update: It looks like you will need to utilize the API to obtain the image element and to use its onload listener since images do not load synchronously. Apparently you can get this with the $el property.
So I'd try updating to something like this:
image.onload = () => {
pano.url = pano.urlHigh
pano.$el.onload = function () {
$('#panos-wrapper').removeClass('blurred')
};
}
v-el might also be handy.
Update 2:
If $el doesn't find your image element, I'd try this:
<img v-bind:src="pano.url" v-el:image>
and then:
image.onload = () => {
pano.url = pano.urlHigh
pano.$els.image.onload = function () {
$('#panos-wrapper').removeClass('blurred')
};
}
The browser will be executing these lines in sequence. Add a breakpoint above the first line then just use the step-by-step debugger to follow the flow.
image.onload = () => {
debugger;
pano.url = pano.urlHigh
$('#panos-wrapper').removeClass('blurred')
}
I suspect that they seem out of order because pano.url = pano.urlHigh probably doesn't have an immediate effect.
It's a synchronous statement and JavaScript is single threaded, which means that even if you've got some other code that responds to changing the url of pano, it can't execute until it's finished executing all the synchronous code.
pano.url = pano.urlHigh
// there's no time for any other code to happen elsewhere here.
$('#panos-wrapper').removeClass('blurred')
The jQuery call is guaranteed to be the next thing that happens after you set the url property.
If you're using a template engine that reacts to changes in the properties of pano, then the best way to run the jQuery call after the changes have been made, is to delay it til the next frame.
pano.url = pano.urlHigh
requestAnimationFrame(function() {
$('#panos-wrapper').removeClass('blurred')
});

data:image/jpeg;base64 how to get its width and height in pixels

How do you get the width and height in pixels image which its src is in data:image/jpeg;base64 ?
Didn't succeed using setting img.src to it and then calling width().
var img = jQuery("<img src="+oFREvent.target.result+">");
img.width() // = 0
This will work :
var img = new Image();
img.onload = function() {
alert(this.width);
}
img.src = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAA.......";
FIDDLE
I made sure the base64 was valid by converting an image here, and the onload function should come before setting the source of the image.
You have to wait for the image to be loaded (or complete, if cached). This will result in an asynchronous callback operation:
// pure JS:
var img = new Image();
img.src = myDataString;
if (img.complete) { // was cached
alert('img-width: '+img.width);
}
else { // wait for decoding
img.onload = function() {
alert('img-width: '+img.width);
}
}
Note: I once had the same problem with a project using jQuery. jQuery doesn't provide an access to the image directly enough just after you have created it. It seems, it's not possible to be done, but in pure JS. But you could try a timeout-loop and wait for the img-width to have a value (and catch any loading/decoding errors).
[Edit, see also comments] Why this works (why there is no race-condition):
JS is single-threaded, meaning there's only one part of code executed at a given time. Any events will be queued until the current scope is exited and will only fire then. Thanks to late-binding, any listeners will be evaluated only then (after the current scope has been executed). So the handler will be present and listening as soon as the onload-Event is fired, regardless, if the listener was setup before or after setting the src-attribute. In contrast to this, the complete-flag is set as soon as the src-attribute is set.

Categories