I'm creating a canvas with mutiple images, but they need to be added in a certain order, for example sky first, the clouds, moutains, hills, grass, road, then car.
When I add the images they load at different times so can't get the order right. My plan was to use a async function so that they wait for each image to finish. But it's not working. Code below.
function addImageProcess(src){
return new Promise((resolve, reject) => {
let img = new Image()
img.onload = () => resolve(img)
img.onerror = reject
img.src = src
})
}
addImageProcess('sky.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
addImageProcess('clouds.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
addImageProcess('moutains.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
addImageProcess('hills.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
addImageProcess('grass.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
addImageProcess('road.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
addImageProcess('car.png').then(img => { canvas2.getContext('2d').drawImage(img, 0, 0) });
Have been pulling my hair out for hours trying to get this to work, hopefully someone can help!
Try using Promise.all:
Promise.all(
[
'sky.png', 'clouds.png', 'mountains.png', 'hills.png', 'grass.png', 'road.png', 'car.png'
].map(
addImageProcess
)
).then(
(allImages)=>allImages.map(
img=>canvas2.getContext('2d').drawImage(img, 0, 0)
)
)
Promise.all can be used to wait for all promises to finish before moving on to the next phase.
If you want to do something different with each image you can do something like this:
Promise.all(
[
'sky.png', 'clouds.png', 'mountains.png', 'hills.png', 'grass.png', 'road.png', 'car.png'
].map(
addImageProcess
)
).then(
([sky, clouds, mountains, hills, grass, road, car])=>{
const context = canvas2.getContext('2d');
context.drawImage(clounds, 5, 2);
context.drawImage(hills, 20, 50);
}
)
Related
I'm loading one or multiple pictures from local storage and putting it to a canvas on top of another picture.
In the code below I first collect all the loading images in promises that only resolve after the image has loaded. Second after all the promises have resolved I send the created image data.
var Promises = [];
for (const selectedOverlayImage of selectedOverlayImages.values()) {
Promises.push(new Promise((resolve, reject) => {
const overlayImg = new Image();
overlayImg.onerror = () => { console.log(`Image ${selectedImage.value} loading error`); reject(); };
overlayImg.onload = () => {
canvasContext.drawImage(overlayImg, 0, 0, canvas.width, canvas.height);
resolve();
};
overlayImg.src = `overlayImages/${selectedOverlayImage.value}.png`;
console.log(selectedOverlayImage.value);
}));
}
console.log("1");
var ret = Promise.all(Promises).then(() => {
console.log("IN");
let imageData = canvas.toDataURL('image/png');
fetch("http://localhost:8000/picture-editing.php", { //Call php to create picture in database
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
body: `imageData=${imageData}`
});
alert('Images successfully loaded!'); //This creates a gain of time before the refresh, allowing the new picture to be present more often after refresh
window.location.reload();
}).catch((error) => {console.log(error);});
console.log("2");
console.log(ret);
setTimeout(() => {
console.log('the queue is now empty');
console.log(ret);
});
});
On Chrome I am able to create one image consisting of multiple pictures.
Console logs when successfully creating picture with wine as overlay-image, we can see the Promise.all function gets entered
On firefox I am not able to create one image of multiple pictures.
We can see the code never enters Promise.all while Promise.all never resolves
Thank you for your help!
Reproduce the error
Set the following in index.php.
<h1 class="pageTitle">Create Picture</h1>
<form method="get">
<input name="modeOfPicture" type="submit" value="webcam"/>
<input name="modeOfPicture" type="submit" value="upload"/>
</form>
<div class="wrapperPictureEditing">
<div id="getPicture" class="block1PictureEditing"></div>
<canvas id="takePictureCanvas" style="display:none;" width="320" height="240"></canvas>
<form id="takePictureButton" class="block2PictureEditing">
<p>Choose Overlay Image:</p>
<label><input type="checkbox" name="overlay" value="wine"> Wine</label><br>
<button type="submit">Take photo</button><br>
</form>
<script>
function displaySelectedPicture(input) {
var selectedPictureDisplay = document.getElementById('selectedPictureDisplay');
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
selectedPictureDisplay.src = e.target.result;
};
reader.readAsDataURL(input.files[0]);
} else { selectedPictureDisplay.removeAttribute('src') }
}
function takePicture(camera) {
const takePictureButton = document.getElementById('takePictureButton');
takePictureButton.addEventListener("submit", () => {
let canvas = document.getElementById('takePictureCanvas');
let canvasContext = canvas.getContext('2d');
selectedOverlayImages = document.querySelectorAll('input[name="overlay"]:checked');
console.log(selectedOverlayImages);
if (camera) {
const stream = document.querySelector('video');
canvasContext.drawImage(stream, 0, 0, canvas.width, canvas.height);
} else {
const selectedImage = document.getElementById('selectedPictureDisplay');
canvasContext.drawImage(selectedImage, 0, 0, canvas.width, canvas.height);
}
var Promises = [];
for (const selectedOverlayImage of selectedOverlayImages.values()) {
Promises.push(new Promise((resolve, reject) => {
const overlayImg = new Image();
overlayImg.onerror = () => { console.log(`Image ${selectedImage.value} loading error`); reject(); };
overlayImg.onload = () => {
canvasContext.drawImage(overlayImg, 0, 0, canvas.width, canvas.height);
resolve();
};
overlayImg.src = `image.png`;
console.log(selectedOverlayImage.value);
}));
}
console.log("1");
var ret = Promise.all(Promises).then(() => {
console.log("IN");
let imageData = canvas.toDataURL('image/png');
alert('Images successfully loaded!'); //This creates a gain of time before the refresh, allowing the new picture to be present more often after refresh
window.location.reload();
}).catch((error) => {console.log(error);});
console.log("2");
console.log(ret);
setTimeout(() => {
console.log('the queue is now empty');
console.log(ret);
});
});
}
function setupVideoStream(stream) {
const video = document.querySelector('video');
video.srcObject = stream;
video.play();
}
function errorMsg(msg, error) {
const errorElement = document.getElementById('error');
errorElement.innerHTML += `${msg}`;
if (typeof error !== 'undefined') {
console.error(error);
}
}
function streamError(error) {
if (error.name === 'NotAllowedError') {
errorMsg('Permissions have not been granted to use your camera' +
', you need to allow the page access to your camera in ' +
'order to take pictures. Instead you can upload an image.');
} else if (error.name === "NotFoundError") {
errorMsg('No camera has been found on this device. ' +
'Instead you can upload an image.');
} else {
errorMsg(`getUserMedia error: ${error.name}`, error);
}
}
async function init() {
var getPictureHTML = document.getElementById('getPicture');
var modeOfPicture = (new URLSearchParams(document.location.search)).get('modeOfPicture');
if (modeOfPicture === null || modeOfPicture === "webcam") {
try {
const stream = await navigator.mediaDevices.getUserMedia({video: true});
getPictureHTML.insertAdjacentHTML('afterbegin',
"<video width='320' height='240' autoplay></video><br>");
setupVideoStream(stream);
takePicture(true);
} catch (error) {
getPictureHTML.insertAdjacentHTML('afterbegin',
"<p id='error' class='error'></p>" +
"<label><input type='file' name='selectedPicture' accept='image/*' " +
"onchange='displaySelectedPicture(this);'><br>Upload image<br></label>" +
"<br><img id='selectedPictureDisplay' width='320' height='240'/>");
streamError(error);
takePicture(false);
}
} else {
getPictureHTML.insertAdjacentHTML('afterbegin',
"<label><input type='file' name='selectedPicture' accept='image/*' " +
"onchange='displaySelectedPicture(this);'><br>Upload image<br></label>" +
"<br><img id='selectedPictureDisplay' width='320' height='240'/>");
takePicture(false);
}
}
init();
</script>
In same directory put an image named image.png, such as this one image example
Then run the server with this command in same directory:
php -S localhost:8000
Visit localhost:8000 in Chrome and Firefox, see the console, on success "IN" should appear after taking a picture with wine overlay image.
I have such code: (I put here the whole code in order not to miss anything)
let updateCoverLoaded;
function coverLoaded(cover){
clearInterval(updateCoverLoaded);
updateCoverLoaded = setInterval(() => coverLoaded(cover), 25);
const coverSelector = $(`[aria-label="${cover}"]`).find("image")[0];
return new Promise(resolve => {
if(typeof coverSelector !== "undefined"){
const coverSelectorSource = coverSelector.getAttribute("xlink:href");
if(getCoverWidth(coverSelectorSource) !== 0) {
clearInterval(updateCoverLoaded);
setTimeout(() => {
player.SetVar(`${cover}`, true);
setTimeout(() => player.SetVar(`${cover}`, false), 100);
resolve();
}, 100);
}
}
});
function getCoverWidth(src) {
const image = new Image();
image.src = src;
return image.naturalWidth;
}
}
// Add SFX to cover entrance and exit
async function feedbackCover(){
await coverLoaded('coverB');
console.log('after resoving promise as expected!');
}
Although code reaches the player.SetVar(${cover}, true); line of code and I'm sure that resolve() is executed the promise doesn't resolve and I can't see console.log('after resoving promise as expected!'); Why? it works in some other situations!
Note: The code actually waits for an element (an image) to load and then resolves the promise.
There are two possible problems, the first is that if the image isn't loaded yet it the first Promise doesn't resolve and all subsequent Promises are discarded, the second is that if coverLoaded is called again, the first promise will never resolve.
This should work as expected:
function coverLoaded(cover) {
return new Promise((resolve) => {
const updateCoverLoaded = setInterval(() => {
const coverSelector = $(`[aria-label="${cover}"]`).find("image")[0];
if (typeof coverSelector !== "undefined") {
const coverSelectorSource = coverSelector.getAttribute("xlink:href");
if (getCoverWidth(coverSelectorSource) !== 0) {
clearInterval(updateCoverLoaded);
setTimeout(() => {
player.SetVar(`${cover}`, true);
setTimeout(() => player.SetVar(`${cover}`, false), 100);
resolve();
}, 100);
}
}
}, 25);
});
function getCoverWidth(src) {
const image = new Image();
image.src = src;
return image.naturalWidth;
}
}
// Add SFX to cover entrance and exit
async function feedbackCover() {
await coverLoaded("coverB");
console.log("after resoving promise as expected!");
}
I'm trying to create a photo capture web app on a nodeJS server, and i'm using the javascript code below.
const btn = document.querySelector('#btn');
btn.addEventListener('click', (event) => {
navigator.mediaDevices.getUserMedia({video: true})
.then(gotMedia)
.catch(error => console.error('getUserMedia() error:', error));
event.preventDefault()
})
And the gotMedia function is this:
function gotMedia(mediaStream) {
const mediaStreamTrack = mediaStream.getVideoTracks()[0];
const imageCapture = new ImageCapture(mediaStreamTrack);
console.log(imageCapture);
const canvas = document.querySelector('canvas');
// ...
imageCapture.grabFrame()
.then(imageBitmap => {
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
canvas.getContext('2d').drawImage(imageBitmap, 0, 0);
})
.catch(error => console.error('grabFrame() error:', error));
}
Everything works fine, the image capturing is ok, but an error occurs, when i snap photos rapidly one after another, that says:
grabFrame() error: DOMException: The associated Track is in an invalid state.
This usually happens when i capture too many photos (like clicking rapidly for about more than 20 seconds), but it also has happened on the first five snapshots. Does anyone know what's going on and what should i change, in order to fix this? Thank you for your time.
According to the spec such error could be result of not acceptable state. In chromium sources I have found this method.
I've overcame error using code like this:
const promise = getPrevImagePromise();
if (!promise && !(imageCapture.track.readyState != 'live' || !imageCapture.track.enabled || imageCapture.track.muted)) {
const imagePromise = imageCapture.grabFrame()
.then((image) => {
// do work with image
})
.then(() => {
deletePrevImagePromise()
})
.catch((error) => {
//
});
setPrevImagePromise(imagePromise);
}
I ran into the same issue and was unable to get the bleeding-edge grabFrame function to work reliably.
Instead, you can draw from an intermediate video source. In your function, that would look something like this (untested):
function gotMedia(mediaStream) {
const mediaStreamTrack = mediaStream.getVideoTracks()[0];
const canvas = document.querySelector('canvas');
const video = document.createElement('video');
video.autoplay = true;
video.srcObject = mediaStream;
video.onplay = () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
};
}
Of course, if you're going to be capturing a lot of frames, it'd be best to create a single video alongside the stream and to capture from that each time.
My two cents:
let preview = document.getElementById('preview')
let recording = document.getElementById('recording')
let photo = document.getElementById('photo')
let photoButton = document.getElementById('photoButton')
let imageHolder = document.getElementById('image-holder')
async function startWebcam() {
return navigator.mediaDevices.getUserMedia({
video: true
})
};
photoButton.addEventListener('click', async() => {
let webcamStream = await startWebcam()
preview.srcObject = webcamStream
const track = webcamStream.getVideoTracks()[0]
const imageCapture = new ImageCapture(track)
const bitmap = await imageCapture.grabFrame()
const canvas = document.getElementById('canvas')
canvas.width = bitmap.width
canvas.height = bitmap.height
const context = canvas.getContext('2d')
context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height)
let image = canvas.toDataURL()
imageHolder.innerHTML += `<img src="${image}"></img>`
track.stop()
})
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Desktoprecord</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma#0.9.0/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<script src="node_modules/axios/dist/axios.min.js"></script>
<script src="app4.js" defer></script>
</head>
<div class="container">
<video id="preview" width="25%" height="auto" autoplay muted poster="https://placekitten.com/g/150"></video>
<canvas id="canvas"></canvas>
<img src="http://placekitten.com/g/320/261" id="photo" alt="photo">
<div id="image-holder">
To be replaced with pretty pictures
</div>
<video src="" id="preview"></video>
</jdiv>
<div class="buttons are-large">
<button id="photoButton" class="button is-success"><i class="fas fa-camera"></i></button>
</div>
</div>
Each time you are going to take a photo you start the camera and after you got the pic you have to stop the track
I want to grabFrame() in a loop (reused without stop()), thanks to fomasha,
my code works as follow
`
async function aSleep(ms) {
return new Promise(R => setTimeout(R, ms));
}
var prevP=null;
function getPrevImagePromise() {return prevP;}
function setPrevImagePromise(p) {prevP=p;}
function deletePrevImagePromise() {prevP=null;}
async function aRun() {
imageCapture = new ImageCapture(gImageTrack);
while(true) {
const promise = getPrevImagePromise();
if (!promise && !(imageCapture.track.readyState != 'live' || !imageCapture.track.enabled || imageCapture.track.muted)) {
const imagePromise = imageCapture.grabFrame()
.then((image) => {
context.drawImage(image, 0, 0, 640, 480);
})
.then(() => {
deletePrevImagePromise()
})
.catch((error) => {
//
});
setPrevImagePromise(imagePromise);
}
await aSleep(500);
}
}
`
refactoring
`
function zCamera() {
var T=this;
T.aInit=async function() {
T.mStm=await navigator.mediaDevices.getUserMedia({ video: { width: 4096, height: 3200, facingMode: 'environment' } });
T.mTrk=T.mStm.getTracks()[0];
T.mCap=new ImageCapture(T.mTrk);
}
T.getSetting=function() {
return T.mTrk.getSettings();
}
T.aGrabFrame=async function() {
var P=new Promise((R)=>{
T.mCap.grabFrame().then((vImg) => {
R(vImg);
}).catch((error) => {
R(null);
});
});
return P;
}
}
var gContext;
var oCamera;
async function aInitCamera()
{
var canvas=document.getElementById('canvas');
gContext=canvas.getContext('2d');
oCamera=new zCamera();
await oCamera.aInit();
}
async function aRun() {
while(true) {
var wImg=await oCamera.aGrabFrame();
if (wImg!=null)
gContext.drawImage(wImg, 0, 0, 640, 480);
await aSleep(100);
}
}
async function jMain() {
await aInitCamera();
aRun();
}
`
I made a loading manager that loads a few object classes. It loops the load function until everything is loaded. The items are promises that initializes classes (that take a long time to finish).
In chrome this works fine, the loading gif is animating and the textContent changes when loading different items.
In firefox however, the gif isn't animating and I get this popup saying 'the website is slowing down the browser'. (the textContent does update though).
Am I doing this all wrong? Is there a better way?
Thanks in advance.
const loadManager = {
loading: false,
loadingItem: -1, //2
load(){
if (!this.loading){
//pick next item
this.loadingItem++;
//DONE!
if(this.loadingItem >= this.objects.length){
this.showPlayButton();
return;
}
document.getElementById('loadingItem').textContent = this.objects[this.loadingItem].label;
this.loading = true;
//a timeout to give the DOM time to update the text in the 'loadingItem' element.
setTimeout( ()=>{
//start loading new object
this.objects[this.loadingItem].load().then( this.loading = false );
}, 200);
}
setTimeout(function(){this.load()}.bind(this), 2000);
},
showPlayButton(){
if ( vehicles[0].object ){
document.getElementById('loadingDiv').style.display = 'none';
document.getElementById('playButton').style.display = 'block';
} else {
console.log('assets not ready yet, trying again in 200ms');
setTimeout(this.showPlayButton.bind(this), 200);
}
},
objects:[
{
label: 'Earthlike planet',
load(){
return new Promise( resolve =>{
let earthColors = [];
earthColors = earthColors.concat( new THREE.Color('darkgreen').toArray() );
earthColors = earthColors.concat( new THREE.Color('green').toArray() );
earthColors = earthColors.concat( new THREE.Color('darkgrey').toArray() );
earthColors = earthColors.concat( new THREE.Color('silver').toArray() );
earthColors = earthColors.concat( new THREE.Color('gold').toArray() );
earthColors = earthColors.concat( new THREE.Color('darkcyan').toArray() );
celestialObjects.push(new Planet({
position: new THREE.Vector3(120000, 80000, 120000),
radius: 80000,
gravity: 12,
atmosphere: true,
colors: earthColors,
vegetation: true,
boulderTexture: new THREE.TextureLoader().load('./resources/boulderGrey.jpg'),
rocks: './resources/earthRocks.json',
grassTexture: './resources/grass.png'
}));
resolve();
});
}
},
{
label: 'Orange planet',
load(){
return new Promise( resolve =>{
let orangeColors = [];
orangeColors = orangeColors.concat( new THREE.Color('darkorange').toArray() );
orangeColors = orangeColors.concat( new THREE.Color('orange').toArray() );
orangeColors = orangeColors.concat( new THREE.Color('darkred').toArray() );
orangeColors = orangeColors.concat( new THREE.Color('silver').toArray() );
orangeColors = orangeColors.concat( new THREE.Color('gold').toArray() );
orangeColors = orangeColors.concat( new THREE.Color('darkcyan').toArray() );
celestialObjects.push(new Planet({
position: new THREE.Vector3(- 240000, -200000, 150000),
radius: 40000,
gravity: 12,
atmosphere: true,
colors: orangeColors,
vegetation: false,
boulderTexture: new THREE.TextureLoader().load('./resources/boulderRed.jpg'),
rocks: './resources/redRocks.json',
grassTexture: './resources/grassRed.png'
}));
resolve();
});
}
},
{
label: 'Asteroids',
load(){
return new Promise( resolve =>{
for (let i = 0; i < 8; i ++){
celestialObjects.push( new Asteroid({
position: new THREE.Vector3(
random(-1, 1),
random(-1, 1),
random(-1, 1)
).setLength( random( 80000, 300000)),
radius: random(3500, 8000),
gravity: 0,
atmosphere: false,
boulderTexture: new THREE.TextureLoader().load('./resources/boulderGrey.jpg'),
}))
}
resolve();
});
}
}
]
}
I would do something using Promise.all and avoid the setTimeout and the recursive function unless you want to load them synchronously which would defeat the purpose using the Promises in the first place.
Using promises i would do something like this. Although this would load everything asynchronously.
load(){
if (!this.loading){
//DONE!
const psArr = []
for(const obj in this.objects){
const ps = obj.load()
psArr.push(ps)
}
Promise.all(psArr).then(() => this.showPlayButton())
}
If you really want to keep the Promises but also want to keep the code synchronous you can do something like this.
load(){
if (!this.loading){
//DONE!
const psArr = [Promise.resolve()]
let i = 0
for(const obj in this.objects){
const previous = psArr[i]
// first array element is a promise object that is already resolved
previous.then(() => {
//you can add your label changes inside this block.
const ps = obj.load()
psArr.push(ps)
})
// the obj.load() will now only run everytime the previous promise is resolved.
i++
}
// promise.all still works for this case
Promise.all(psArr).then(() => this.showPlayButton())
}
this would also be way easier to write using async/await
I am trying to convert my promise based code to RxJs but have a hard time to get my head around Rx especially RxJs.
I have a an array with paths.
var paths = ["imagePath1","imagePath2"];
And I like to load images in Javascript
var img = new Image();
img.src = imagePath;
image.onload // <- when this callback fires I'll add them to the images array
and when all Images are loaded I like to execute a method on.
I know there is
Rx.Observable.fromArray(imagepathes)
there is also something like
Rx.Observable.fromCallback(...)
and there is something like flatMapLatest(...)
And Rx.Observable.interval or timebased scheduler
Based on my research I would assume that these would be the ingredients to solve it but I cannot get the composition to work.
So how do I load images from a array paths and when all images are loaded I execute a method based on an interval?
Thanks for any help.
At first you need a function that will create a Observable or Promise for separate image:
function loadImage(imagePath){
return Rx.Observable.create(function(observer){
var img = new Image();
img.src = imagePath;
img.onload = function(){
observer.onNext(img);
observer.onCompleted();
}
img.onError = function(err){
observer.onError(err);
}
});
}
Than you can use it to load all images
Rx.Observable
.fromArray(imagepathes)
.concatMap(loadImage) // or flatMap to get images in load order
.toArray()
.subscribe(function(images){
// do something with loaded images
})
I think you don't have to create an Observable yourself for this.
import { from, fromEvent } from 'rxjs';
import { mergeMap, map, scan, filter } from 'rxjs/operators';
const paths = ["imagePath1","imagePath2"];
from(paths).pipe(
mergeMap((path) => {
const img = new Image();
img.src = path;
return fromEvent(img, 'load').pipe(
map((e) => e.target)
);
}),
scan((acc, curr) => [...acc, curr], []),
filter((images) => images.length === paths.length)
).subscribe((images) => {
// do what you want with images
});
function loadImage(url){
var img = new Image;
img.src = url;
var o = new Rx.Subject();
img.onload = function(){ o.onNext(img); o.onCompleted(); };
img.onerror = function(e){ o.onError(e); }; // no fromEvent for err handling
return o;
}
var imageUrls = ['url1', 'url2', 'url3'];
var joined = Rx.Observable.merge(imageUrls.map(loadImage));
// consume one by one:
joined.subscribe(function(item){
// wait for item
});
joined.toArray().subscribe(function(arr){
// access results array in arr
});
Or in short:
var imageUrls = ['url1', 'url2', 'url3'];
fromArray(imageUrls).map(url => {
var img = new Image;
img.src = url;
return fromEvent(img, "load");
}).toArray().subscribe(function(arr){
// access results here
});
I don't think you can do that easily with observables, as there's nothing there to indicate a finish (unless you have an initial size). Look at the other answers for the Rx version.
However, you can use an array of Promises:
/**
* Loads an image and returns a promise
* #param {string} url - URL of image to load
* #return {Promise<Image>} - Promise for an image once finished loading.
*/
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.src = imagePath;
image.onload = function() { resolve(img); };
image.onerror = reject;
});
}
And with that, you can easily do something like this:
var imageUrls = ['url1', 'url2', 'url3'];
Promise.all(imageUrls.map(loadImageAsync))
.then(function(arrayOfImageElements) {
// All done!
});
The other RX based solutions here did not really work for me. Bogdan Savluk’s version did not work at all. Benjamin Gruenbaum’s version waits until an image is loaded before starting to load the next image so it gets really slow (correct me if I am wrong)
Here is my solution which just compares the total amount of images with the number of already loaded images and if they are equal, the onNext() method of the returned Observable gets called with the array of images as an argument:
var imagesLoaded = function (sources) {
return Rx.Observable.create(function (observer) {
var numImages = sources.length
var loaded = 0
var images = []
function onComplete (img) {
images.push(img)
console.log('loaded: ', img)
loaded += 1
if (loaded === numImages) {
observer.onNext(images)
observer.onCompleted()
}
}
sources.forEach(function (src) {
var img = new Image()
img.onload = function () {
onComplete(img)
}
console.log('add src: ' + src)
img.src = src
if (img.complete) {
img.onload = null
onComplete(img)
}
})
})
}
Usage:
console.time('load images'); // start measuring execution time
imagesLoaded(sources)
// use flatMap to get the individual images
// .flatMap(function (x) {
// return Rx.Observable.from(x)
// })
.subscribe(function (x) {
console.timeEnd('load images'); // see how fast this was
console.log(x)
})
Here is the Angular / Typescript version to load an Image with RxJS:
import { Observable, Observer } from "rxjs";
public loadImage(imagePath: string): Observable<HTMLImageElement> {
return Observable.create((observer: Observer<HTMLImageElement>) => {
var img = new Image();
img.src = imagePath;
img.onload = () => {
observer.next(img);
observer.complete();
};
img.onerror = err => {
observer.error(err);
};
});
}
Here a really better implementation that cancels loading of the image in case you unsubscribe from the Observable https://stackblitz.com/edit/rxjs-loadimage?file=index.ts
import { Observable, Subscriber } from "rxjs";
/**
* RxJS Observable of loading image that is cancelable
*/
function loadImage(
url: string,
crossOrigin?: string
): Observable<HTMLImageElement> {
return new Observable(function subscriber(subscriber) {
let img = new Image();
img.onload = function onload() {
subscriber.next(img);
subscriber.complete();
};
img.onerror = function onerror(err: Event | string) {
subscriber.error(err);
};
// data-urls appear to be buggy with crossOrigin
// https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
// see https://code.google.com/p/chromium/issues/detail?id=315152
// https://bugzilla.mozilla.org/show_bug.cgi?id=935069
// crossOrigin null is the same as not set.
if (
url.indexOf("data") !== 0 &&
crossOrigin !== undefined &&
crossOrigin !== null
) {
img.crossOrigin = crossOrigin;
}
// IE10 / IE11-Fix: SVG contents from data: URI
// will only be available if the IMG is present
// in the DOM (and visible)
if (url.substring(0, 14) === "data:image/svg") {
// TODO: Implement this :)
// img.onload = null;
// fabric.util.loadImageInDom(img, onLoadCallback);
}
img.src = url;
return function unsubscribe() {
img.onload = img.onerror = undefined;
if (!img.complete) {
img.src = "";
}
img = undefined;
};
});
}
// Example
const cacheBurst = new Date().getTime();
const imgUrl = `https://i.pinimg.com/originals/36/0c/62/360c628d043b2461d011d0b7f9b4d880.jpg?nocache=${cacheBurst}`;
const s = loadImage(imgUrl).subscribe(
img => {
console.log("Img", img);
},
err => {
console.log("Err", err);
}
);
setTimeout(() => {
// uncomment to check how canceling works
// s.unsubscribe();
}, 100);