I'm making a game using javascript + canvas.
I use the code below to ensure
var imgLoaded = 0;
var imgToLoad = multiImgs;
var onImgLoad = function()
{
imgLoaded++;
if(imgLoaded == imgToLoad)
{
ctx.drawImage()
}
}
for(var i = 0; i < multiImgs; i++)
{
images[i] = new Image();
images[i].onload = onImgLoad();
images[i].src = 'images/'+i+'.png';
}
This code at times works fine, esp. when the images are cached. However, when loading for the first time, at times, it gives INDEX_SIZE_ERR: DOM Exception 1
which I found is due to height & width of image not being available as suggested by Quickredfox in this answer... but then here drawImage is called only when all the images are loaded?
The error primarily occurs in mobile devices
You have to provide a reference to the load handler. Otherwise, the function executes immediately. Use:
images[i].onload = onImgLoad;
Related
hello i have a sketch which basically pulls from a giant database of over 8,000 images to select an image. sometimes, an image will fail to load and its not feasible for me to go through all 8000 images and find which ones are causing my sketch to freeze on "Loading..."
i just want a way to reset the sketch if the image selected fails to load and just select the next image or a random image or placeholder image in case one fails to load, instead of it just hanging on a "Loading..." screen.
seems like a really simple thing but i can't find any simple solution. i tried making a script to check if "p5_loading" div exists after a Timeout period, but then i realized since the sketch wont load if its broken then my Timeout will never run out to be able to check if it is broken.
thanks.
The bad news is that preload() and loadImage() have some serious shortcomings in this regard. The way preload() works is that when you have it declared, p5.js wraps all of the loadXXX() functions with a function that increments a counter before actually calling the underlying loadXXX() function. Each loadXXX() function then decrements the preload counter, but only if the resource is loaded successfully. As a result, unless one resorts to accessing p5.js internals, there is no way to re-try or recover from an error during preload.
Additionally loadImage() does not have good error handling logic. The issue is that loadImage() first performs an fetch() request for the resource, and if that does not raise an error it creates a Image object and uses its src property to actually load the image data. The problem with that is that it doesn't check the response status code and the Image object does not generate detailed error information. I would go so far as to call this a defect in p5.js worth of filing a GitHub issue. As a consequence, any robust loader that uses loadImage() has to treat all load failures equally (as opposed to an intelligent solution that would treat 5xx errors and timeouts as retry-able, but treat 4xx errors as not worth retrying).
So, given the bad news, what could we do? Roll our own loading screen and use the somewhat suboptimal error handling from loadImage():
const availableImages = [1, 2, 3, 4, 5];
const loading = {};
const loaded = [];
const size = 50;
const maxRetries = 3;
let isLoading = true;
function robustLoad(success, failure) {
// select a random image
let ix = floor(random(0, availableImages.length));
// remove that image from the list of available images
let id = availableImages.splice(ix, 1)[0];
console.log(`attempting to load image id ${id}`);
function tryLoadImage(retryCount) {
loading[id] = loadImage(
`https://robustp5jspreload.kumupaul.repl.co/get-image?id=${id}`,
() => {
// success
loaded.push(loading[id]);
delete loading[id];
success();
},
err => {
console.warn(`error loading image ${id}`);
console.log(err);
if (retryCount < maxRetries) {
console.log(`retrying image ${id}`);
tryLoadImage(retryCount + 1);
} else {
// throw in the towel on this id
delete loading[id];
if (availableImages.length > 0) {
robustLoad(success, failure);
} else {
failure();
}
}
}
);
}
tryLoadImage(0);
}
function setup() {
createCanvas(windowWidth, windowHeight);
background(200);
textSize(48);
textAlign(LEFT, TOP);
text('Loading...', 10, 10);
// attempt to load two random images
let status = [false, false];
for (let n = 0; n < 2; n++) {
let currentN = n;
robustLoad(
() => {
status[currentN] = true;
if (status.every(v => v)) {
// We're done loading.
isLoading = false;
drawBg();
loop();
}
},
() => {
console.warn(`unable to load image ${currentN}`);
}
);
}
}
function drawBg() {
let i = 0;
let j = 0;
for (let y = 0; y < height; y += size) {
for (let x = 0; x < width; x += size) {
image(loaded[i++ % loaded.length], x, y, size, size);
}
i = ++j;
}
}
function draw() {
if (!isLoading) {
circle(mouseX, mouseY, 30);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
Note: This example is not meant to be something you would just use as is. It is just an example of one possible implementation.
The source I'm loading image from is a proof of concept I made on Replit which returns 502 66% of the time. As a result it's theoretically that it will completely fail some percentage of the time.
I'm trying to preload images from an array synchronously with a callback when all are done. I can find lots of solutions on here which get me near to where I want to be but none of them work synchronously and allow the usage I require.
The code I have below works great but all are loaded async. I have another script which does the job sequentially but I can't seem to fire a callback once all the items in the array are done. This code below allows for user defined callback outside of the function which is what I need as I'd be calling this function on different arrays.
What I'm attempting to do is preload one array of images, check a timestamp comparison previously stored in a variable to see whether the network is slow or not, then (if the network is slow) preload a second array of images using the same preload function when the first array is done, then fire my image slideshow function once the second array is done. This is to avoid starting my image slideshow before enough images are initially loaded - to avoid blank slides, if that makes sense.
Code which works but only asynchronously:
function preload(arr){
var newimages=[], loadedImages = 0;
var postaction=function(){}
var arr=(typeof arr!="object")? [arr] : arr;
function imageloadpost(){
loadedImages++;
if (loadedImages==arr.length){
postaction(newimages); //call postaction and pass in newimages array as parameter
}
}
for (var i=0; i<arr.length; i++){
newimages[i]=new Image();
newimages[i].src=arr[i];
newimages[i].onload=function(){
imageloadpost();
}
newimages[i].onerror=function(){
imageloadpost();
}
}
return { //return blank object with done() method
done:function(f){
postaction=f || postaction; //remember user defined callback functions to be called when images load
}
}
}
Sample result I want would be:
Image 1...Loaded, then...
Image 2...Loaded, then...
Image 3...Loaded, then...
Callback: all are loaded - do something else
UPDATE:
This is the synchronous code that waits for the first image to load before firing the function again for the second and third etc etc. This is the behaviour I want but can't seem to get a callback to work when all items in the array are loaded.
function preload(arrayOfImages, index) {
index = index || 0;
if (arrayOfImages && arrayOfImages.length && arrayOfImages.length > index) {
var img = new Image();
img.onload = function() {
preload(arrayOfImages, index + 1);
};
img.onerror = function() {
preload(arrayOfImages, index + 1);
};
img.src = arrayOfImages[index];
}
$j('#slide-' + index + '').addClass('loaded');
}
I'm new to javascript, and I've been stuck on this simple problem for days. I have a series of thousands of images (collectively a "dataset") that when viewed rapidly one after the other gives the appearance of video. I want to loop through all of my images.
I've created a function like so:
function updateImage(dataset, record_id) {
image_url = '/image?dataset='+dataset+'&record-id='+record_id;
if ($('#mpeg-image').length > 0) {
$('#mpeg-image')[0].src = image_url;
} else {
$("#image-thumbnail").html('<img id="mpeg-image", src="'+image_url+'"> </img>');
}
}
Calling the function once, e.g., updateImage('dataset_28_18-08-11',444); results in the image content updating in the browser. However, I want to show the images in a loop, so I created a function like this:
function playVideo() {
for (i = 0; i < 1800; i++) {
updateImage(dataset,i);
}
}
This function uses the same updateImage() function that worked above, but when I run playVideo(); Chrome doesn't update the image until the very end. The src tag changes, but the actual content does not. How can I alter my loop so that each image is loaded and shown in sequence? I don't want to use something like "Sprite" that merges all images into one, since that's too data intensive on my backend. I just want to load and show my images one after the other.
Browsers won't re-render page elements until there's a break in the execution of your code. Your entire image loop will run before the browser ever gets a chance to re-paint the images.
It's not because the loop "runs too fast" or because the images haven't loaded (though that can definitely pose problems). To see an example of redrawing issue, try changing your animation into an infinite loop, where it continually plays. The page will freeze ("pinwheel") forever and the page elements will never be updated, even long after all of your images have loaded. You need to give the browser an opportunity to redraw the page whenever you've made changes to page elements.
In web animation, this is often done via window.requestAnimationFrame() (see MDN docs). The method takes a callback function which is executed after the browser has redrawn the page. The browser will execute your callback once it's ready for page changes.
A simplistic example of this with you code would go something like this.
var imageNo = 0;
function step() {
updateImage(dataset, imageNo);
imageNo++;
if (imageNo < 1800) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
You can achieve a similar effect using setTimeout() or setInterval(), but those methods aren't optimized for animation.
The reason why you only see the last image is because of two things.
1. as #andriusain said, it takes the browser some indeterminate amount of time to download each image.
2. The loop runs so fast that the last iteration is what takes affect.
With that said, it would be best to either sprite the image (which it sounds like you can't do), or you can preload the images so that the browser caches them, then set the urls. You can do something like this.
// using es6
// I'd recommend adding <img src="mpeg-image" /> to your html page
// just to simplify the code. Also I recommend saving the image
// element in a local variable instead of using jQuery in each
// iteration loop which will definitely give a slower performance.
const mpegImgElement = $('#mpeg-image')[0];
const FPS = 30 / 1000; // 30 Frames Per Second
function playVideo() {
preLoadImages()
.then(urls => {
playUrls(urls);
})
.catch(error => {
console.log('error loading images', error);
})
}
function playUrls(urls) {
const index = 0;
const intervalId = setInterval(() => {
// set the url
mpegImgElement.src = urls[index];
// all done. stop the interval
if (++index === urls.length) {
clearInterval(intervalId);
}
}, FPS);
}
function preLoadImages() {
const promises = [];
for (i = 0; i < 1800; i++) {
const imageUrl = '/image?dataset='+dataset+'&record-id='+record_id;
const promise = loadImage(imageUrl);
promises.push(promise);
}
return Promise.all(promises);
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener('load', () => {
resolve(url);
});
image.addEventListener('error', error => {
reject(error);
});
image.src = url;
});
}
This happens because the browser is still loading the images... I think your best bet is to programmatically load them first and once they are loaded then play the video... to do that maybe something like this will help:
var loadImages = function(images, callback) {
var loadTotal = 0;
for (var i = 0; i < images.length; i++) {
var image = new Image();
image.addEventListener('load', function() {
loadTotal++;
if (loadTotal === images.length) {
callback();
}
});
image.src = images[i];
}
}
var playVideo = function() {
// do stuff...
}
var images = ['image1.jpg', 'image2.jpg'];
loadImages(images, playVideo);
This idea is terrible.
JS is executed in single thread, while calling data from src is asynchronous method. Calling one by one and wait then definitely block the thread.
In this case you need promise to prepare all images first.
I make a workable example using google logo.
paths = ["https://www.google.com.hk/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png",
"https://www.google.com.hk/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png",
"https://www.google.com.hk/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"]
var imgCount = paths.length;
function loadImage(url) {
return new Promise((resolve, reject) => {
let img = new Image();
img.addEventListener('load', e => resolve(img));
img.src = url;
});
}
promises = []
for (var i = 0; i < paths.length; i++){
path = paths[i];
promises.push(loadImage(path));
}
Promise.all(promises).then(imgs => {
imgs.forEach(e => document.getElementById('image-holder').appendChild(e));});
//What I done here is display then all, but you can make it like, replace one by one, or declare your own display function.
<div id="image-holder"></div>
When an image object is created, can know when is fully loaded using the "complete" property, or the "onload" method, then, this image has processed ( resizing for example ) using some time, that can be some seconds in big files.
How to know when browser finish to process an image after loading it?
EDIT: In examples can see a lag between "complete" message and the appearance of the image, I want avoid this.
Example ussing "onload" method:
var BIGimage;
putBIGimage();
function putBIGimage(){
BIGimage=document.createElement("IMG");
BIGimage.height=200;
BIGimage.src="http://orig09.deviantart.net/5e53/f/2013/347/f/d/i_don_t_want_to_ever_leave_the_lake_district_by_martinkantauskas-d6xrdch.jpg";
BIGimage.onload=function(){waitBIGimage();};
}
function waitBIGimage(){
console.log("Complete.");
document.body.appendChild(BIGimage);
}
Example using "complete" property:
var BIGimage;
putBIGimage();
function putBIGimage(){
BIGimage=document.createElement("IMG");
BIGimage.height=200;
BIGimage.src="http://orig09.deviantart.net/5e53/f/2013/347/f/d/i_don_t_want_to_ever_leave_the_lake_district_by_martinkantauskas-d6xrdch.jpg";
waitBIGimage();
}
function waitBIGimage(){
if (!BIGimage.complete){
console.log("Loading...");
setTimeout(function(){
waitBIGimage();
},16);
} else {
console.log("Complete.");
document.body.appendChild(BIGimage);
}
}
EDIT: Thanks the #Kaiido's response I made this sollution for wait the images process.
var imagesList=["https://omastewitkowski.files.wordpress.com/2013/07/howard-prairie-lake-oregon-omaste-witkowski-owfotografik-com-2-2.jpg",
"http://orig03.deviantart.net/7b8d/f/2015/289/0/f/0ffd635880709fb39c2b69f782de9663-d9d9w6l.jpg",
"http://www.livandiz.com/dpr/Crater%20Lake%20Pano%2016799x5507.JPG"];
var BIGimages=loadImages(imagesList);
onLoadImages(BIGimages,showImages);
function loadImages(listImages){
var image;
var list=[];
for (var i=0;i<listImages.length;i++){
image=document.createElement("IMG");
image.height=200;
image.src=listImages[i]+"?"+Math.random();
list.push(image);
}
return list;
}
function showImages(){
loading.style.display="none";
for (var i=0; i<BIGimages.length;i++){
document.body.appendChild(BIGimages[i]);
}
};
function onLoadImages(images,callBack,n) {
if (images==undefined) return null;
if (callBack==undefined) callBack=function(){};
else if (typeof callBack!="function") return null;
var list=[];
if (!Array.isArray(images)){
if (typeof images =="string") images=document.getElementById(images);
if (!images || images.tagName!="IMG") return null;
list.push(images);
} else list=images;
if (n==undefined || n<0 || n>=list.length) n=0;
for (var i=n; i<list.length; i++){
if (!list[i].complete){
setTimeout(function(){onLoadImages(images,callBack,i);},16);
return false;
}
var ctx = document.createElement('canvas').getContext('2d');
ctx.drawImage(list[i], 0, 0);
}
callBack();
return true;
}
<DIV id="loading">Loading some big images...</DIV>
The HTMLImageElement interface has a decode() method, which does allow us to wait until the image is ready to be drawn, just like you want.
var BIGimage;
putBIGimage();
function putBIGimage() {
BIGimage = document.createElement("IMG");
BIGimage.height = 200;
BIGimage.src = "https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg?r=" + Math.random();
BIGimage.onload = e => console.log('load event', performance.now());
BIGimage.decode().then(waitBIGimage);
BIGimage.onerror = console.error;
}
function waitBIGimage() {
// uses the synchronous testing method to check it works fine
var start = performance.now();
// only to see if it worked fine
var ctx = document.createElement('canvas').getContext('2d');
ctx.drawImage(BIGimage, 0, 0);
ctx.drawImage(ctx.canvas, 0, 0);
var end = performance.now();
console.log(`it took ${end - start}ms to draw`)
// do your stuff
document.body.appendChild(BIGimage);
}
Another way, is to use the fact that the CanvasContext2D drawImage method can be synchronous. Some browsers may try to delay the actual painting for the next painting frame, but by drawing the canvas onto itself we can force most of the current UAs to render our image synchronously.
So you can use it as a waiting method in your waitBIGimage method.
var BIGimage;
putBIGimage();
function putBIGimage() {
BIGimage = document.createElement("IMG");
BIGimage.height = 200;
BIGimage.src = "https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg?r=" + Math.random();
BIGimage.onload = waitBIGimage;
BIGimage.onerror = console.error;
}
function waitBIGimage() {
// only for demo
// we've got to also log the time since even the console.log method will be blocked during processing
var start = performance.now();
console.log('waiting', start);
// this is all needed
var ctx = document.createElement('canvas').getContext('2d');
ctx.drawImage(BIGimage, 0, 0);
// on some system the drawing of the image may be delayed to the next painting frame
// we can try force the synchronous rendering by drawing the canvas over itself
ctx.drawImage(ctx.canvas, 0, 0);
// demo only
var end = performance.now();
console.log("Complete.", end);
console.log(`it took ${end - start}ms`)
// do your stuff
document.body.appendChild(BIGimage);
}
On my Firefox it takes about 1 second to process the image, while on my Chrome, it's a bit faster.
But one big issue with method is that it is synchronous, and thus will block your scripts during all the time the Image is processed.
Yet another way, is to use the createImageBitmap() method, which should allocate the image bitmap in the GPU, ready to be painted.
There is no reliable way to know - your browser can continue to execute arbitrary javascript or perform built in functions (i.e., resizing of the browser window) which can directly or indirectly affect the image and cause it to be either redrawn, or appear to not finish drawing for a while.
You can hook in to particular events in the lifetime of an image, but, strictly speaking, "finish" only happens when the browser window is closed.
I'm trying to draw a few images on a canvas, to make sure the images are loaded when I draw them, I preload them using an array.
var num = 0,
length = 3,
images = {
img1: '/img/img1.png',
img2: '/img/img2.png',
img3: '/img/img3.png'
}
for(var i in images)
{
var tmp = new Image();
tmp.src = images[i];
tmp.onload = function()
{
num ++;
if(num == length)
{
startDoingStuff();
}
}
}
After preloading the startDoingStuff is fired where I use the image again
<snip>
this.img = new Image();
img.src = images[id].src;
</snip>
This works in Firefox and Opera but not in Chrome. Chrome probably thinks the images aren't loaded because if I put yet another onload it will work. Which is stupid because the images were already loaded :/
Only thing I found to solve this is by putting the loaded images in another array (or overwriting the original array) thus keeping reference.
Why doesn't Chrome not realize he already has the images in cache and use them when asked for?
Your code to preload the images are not very correct. Try next function:
function preloadImages(images,callback){
var img,imgs,finished=0,onLoadError;
if(images instanceof Array)imgs=images;
else{
imgs=[];
for(var p in images)if(images.hasOwnProperty(p))imgs.push(images[p]);
}
onLoadError=function(ev){
//ev=ev||window.event;
// ev.type can be used to check
// when image is loaded succ (ev.type==="load")
// or fail (ev.type==="error")
finished++;
if(finished===imgs.length)callback();
};
for(var i=0;i<imgs.length;i++){
img=new Image();
img.onload=img.onerror=onLoadError;
img.src=imgs[i]; // NOTE: .src must be assigned after .onload and .onerror!
}
}
You can use this function like in next examples:
preloadImages({ img1: '/img/img1.png',
img2: '/img/img2.png',
img3: '/img/img3.png' },
startDoingStuff);
// or
preloadImages([ '/img/img1.png',
'/img/img2.png',
'/img/img3.png' ],
startDoingStuff);