Include Image Data in JSON FabricJS - javascript

I'm trying to use FabricJS canvas, and I'd like to export canvas as JSON.
I've tried loading image using both new fabric.Image and fabric.Image.fromURL both of them work great.
Now I want to get JSON from canvas. But I want 2 kinds of JSON. One where image's src would be link to a image which I used initially. Another would simply add base64 data right on JSON. So I tried to use canvas.toJSON() and canvas.toDatalessJSON(), but to my surprise, It simply gives same result with link, and none of them contain image data.
How do I export to JSON which INCLUDES image data right on JSON? (I already got with the link)
I've put together a small demo of what I have till now. Notice when you click on export and see on console, both objects have source link and none of them actually have base64 data.
The reason I want base64 is because I want it instant when I re-use somewhere else.
I tried searching over the internet, according to docs, toJSON should contain, but looks like it's only for shapes, and not image. Or did I miss something?
Thanks in advance!

Extend toObject of fabric.Image
fabric.Image.prototype.toObject = (function(toObject) {
return function() {
return fabric.util.object.extend(toObject.call(this), {
src: this.toDataURL()
});
};
})(fabric.Image.prototype.toObject);
And for src use object.toDataURL()
DEMO
const useFabricImage = () => {
const c = document.getElementById("designer");
const canvas = new fabric.Canvas(c, {width: 500, height: 500})
const url = "https://i.imgur.com/KxijB.jpg";
const img = new Image();
img.src = url;
const fabricImage = new fabric.Image(img, {});
canvas.add(fabricImage);
return canvas;
}
const useFromURL = () => {
const c = document.getElementById("designer");
const canvas = new fabric.Canvas(c, {width: 500, height: 500})
const url = "https://i.imgur.com/KxijB.jpg";
fabric.Image.fromURL(url, (img) => {
canvas.add(img);
},{
crossOrigin:'annonymous'
});
return canvas;
}
fabric.Image.prototype.toDatalessObject = fabric.Image.prototype.toObject;
fabric.Image.prototype.toObject = (function(toObject) {
return function() {
return fabric.util.object.extend(toObject.call(this), {
src: this.toDataURL()
});
};
})(fabric.Image.prototype.toObject);
const canvas = useFromURL();
const button = document.getElementById("export");
button.addEventListener("click", () => {
console.log(canvas.toJSON());
console.log(canvas.toDatalessJSON());
})
#designer {
border: 1px solid aqua;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.3/fabric.min.js"></script>
<canvas id="designer" height="500" width="500"></canvas>
<button id="export">Export</button>

You need to extend fabric.Image.protype.toObject():
fabric.Image.prototype.toObject = (function (toObject) {
return function () {
var image = this;
var getData = function () {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
context = canvas.getContext('2d');
context.drawImage(image.getElement(), 0, 0);
return canvas.toDataURL('image/png').replace(/^data:image\/png;base64,/, '');
};
return fabric.util.object.extend(toObject.call(this), {
dataURL: getData(),
});
};
})(fabric.Image.prototype.toObject);
After that dataURL property will be automatically added to your object.

Related

How to trigger double click using Vanilla JavaScript [duplicate]

Is the function I wrote below enough to preload images in most, if not all, browsers commonly used today?
function preloadImage(url)
{
var img=new Image();
img.src=url;
}
I have an array of image URLs that I loop over and call the preloadImage function for each URL.
Yes. This should work on all major browsers.
Try this I think this is better.
var images = [];
function preload() {
for (var i = 0; i < arguments.length; i++) {
images[i] = new Image();
images[i].src = preload.arguments[i];
}
}
//-- usage --//
preload(
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg",
"http://domain.tld/gallery/image-003.jpg"
)
Source: http://perishablepress.com/3-ways-preload-images-css-javascript-ajax/
You can move this code to index.html for preload images from any url
<link rel="preload" href="https://via.placeholder.com/160" as="image">
In my case it was useful to add a callback to your function for onload event:
function preloadImage(url, callback)
{
var img=new Image();
img.src=url;
img.onload = callback;
}
And then wrap it for case of an array of URLs to images to be preloaded with callback on all is done:
https://jsfiddle.net/4r0Luoy7/
function preloadImages(urls, allImagesLoadedCallback){
var loadedCounter = 0;
var toBeLoadedNumber = urls.length;
urls.forEach(function(url){
preloadImage(url, function(){
loadedCounter++;
console.log('Number of loaded images: ' + loadedCounter);
if(loadedCounter == toBeLoadedNumber){
allImagesLoadedCallback();
}
});
});
function preloadImage(url, anImageLoadedCallback){
var img = new Image();
img.onload = anImageLoadedCallback;
img.src = url;
}
}
// Let's call it:
preloadImages([
'//upload.wikimedia.org/wikipedia/commons/d/da/Internet2.jpg',
'//www.csee.umbc.edu/wp-content/uploads/2011/08/www.jpg'
], function(){
console.log('All images were loaded');
});
const preloadImage = src =>
new Promise((resolve, reject) => {
const image = new Image()
image.onload = resolve
image.onerror = reject
image.src = src
})
// Preload an image
await preloadImage('https://picsum.photos/100/100')
// Preload a bunch of images in parallel
await Promise.all(images.map(x => preloadImage(x.src)))
CSS2 Alternative: http://www.thecssninja.com/css/even-better-image-preloading-with-css2
body:after {
content: url(img01.jpg) url(img02.jpg) url(img03.jpg);
display: none;
}
CSS3 Alternative: https://perishablepress.com/preload-images-css3/
(H/T Linh Dam)
.preload-images {
display: none;
width: 0;
height: 0;
background: url(img01.jpg),
url(img02.jpg),
url(img03.jpg);
}
NOTE: Images in a container with display:none might not preload.
Perhaps visibility:hidden will work better but I have not tested this. Thanks Marco Del Valle for pointing this out
I recommend you use a try/catch to prevent some possible issues:
OOP:
var preloadImage = function (url) {
try {
var _img = new Image();
_img.src = url;
} catch (e) { }
}
Standard:
function preloadImage (url) {
try {
var _img = new Image();
_img.src = url;
} catch (e) { }
}
Also, while I love DOM, old stupid browsers may have problems with you using DOM, so avoid it altogether IMHO contrary to freedev's contribution. Image() has better support in old trash browsers.
Working solution as of 2020
Most answers on this post no longer work - (atleast on Firefox)
Here's my solution:
var cache = document.createElement("CACHE");
cache.style = "position:absolute;z-index:-1000;opacity:0;";
document.body.appendChild(cache);
function preloadImage(url) {
var img = new Image();
img.src = url;
img.style = "position:absolute";
cache.appendChild(img);
}
Usage:
preloadImage("example.com/yourimage.png");
Obviously <cache> is not a "defined" element, so you could use a <div> if you wanted to.
Use this in your CSS, instead of applying the style attribute:
cache {
position: absolute;
z-index: -1000;
opacity: 0;
}
cache image {
position: absolute;
}
If you have tested this, please leave a comment.
Notes:
Do NOT apply display: none; to cache - this will not load the
image.
Don't resize the image element, as this will also affect the
quality of the loaded image when you come to use it.
Setting position: absolute to the image is necessary, as the image elements will eventually make it's way outside of the viewport - causing them to not load, and affect performance.
UPDATE
While above solution works, here's a small update I made to structure it nicely:
(This also now accepts multiple images in one function)
var cache = document.createElement("CACHE");
document.body.appendChild(cache);
function preloadImage() {
for (var i=0; i<arguments.length; i++) {
var img = new Image();
img.src = arguments[i];
var parent = arguments[i].split("/")[1]; // Set to index of folder name
if ($(`cache #${parent}`).length == 0) {
var ele = document.createElement("DIV");
ele.id = parent;
cache.appendChild(ele);
}
$(`cache #${parent}`)[0].appendChild(img);
console.log(parent);
}
}
preloadImage(
"assets/office/58.png",
"assets/leftbutton/124.png",
"assets/leftbutton/125.png",
"assets/leftbutton/130.png",
"assets/leftbutton/122.png",
"assets/leftbutton/124.png"
);
Preview:
Notes:
Try not to keep too many images preloaded at the same time (this can cause major performance issues) - I got around this by hiding images, which I knew wasn't going to be visible during certain events. Then, of course, show them again when I needed it.
This approach is a little more elaborate. Here you store all preloaded images in a container, may be a div. And after you could show the images or move it within the DOM to the correct position.
function preloadImg(containerId, imgUrl, imageId) {
var i = document.createElement('img'); // or new Image()
i.id = imageId;
i.onload = function() {
var container = document.getElementById(containerId);
container.appendChild(this);
};
i.src = imgUrl;
}
Try it here, I have also added few comments
Solution for ECMAScript 2017 compliant browsers
Note: this will also work if you are using a transpiler like Babel.
'use strict';
function imageLoaded(src, alt = '') {
return new Promise(function(resolve) {
const image = document.createElement('img');
image.setAttribute('alt', alt);
image.setAttribute('src', src);
image.addEventListener('load', function() {
resolve(image);
});
});
}
async function runExample() {
console.log("Fetching my cat's image...");
const myCat = await imageLoaded('https://placekitten.com/500');
console.log("My cat's image is ready! Now is the time to load my dog's image...");
const myDog = await imageLoaded('https://placedog.net/500');
console.log('Whoa! This is now the time to enable my galery.');
document.body.appendChild(myCat);
document.body.appendChild(myDog);
}
runExample();
You could also have waited for all images to load.
async function runExample() {
const [myCat, myDog] = [
await imageLoaded('https://placekitten.com/500'),
await imageLoaded('https://placedog.net/500')
];
document.body.appendChild(myCat);
document.body.appendChild(myDog);
}
Or use Promise.all to load them in parallel.
async function runExample() {
const [myCat, myDog] = await Promise.all([
imageLoaded('https://placekitten.com/500'),
imageLoaded('https://placedog.net/500')
]);
document.body.appendChild(myCat);
document.body.appendChild(myDog);
}
More about Promises.
More about "Async" functions.
More about the destructuring assignment.
More about ECMAScript 2015.
More about ECMAScript 2017.
The HTML Living Standard now supports 'preload'
According to the W3C HTML spec you can now preload using JavaScript like so:
var link = document.createElement("link");
link.rel = "preload";
link.as = "image";
link.href = "https://example.com/image.png";
document.head.appendChild(link);
Here is my approach:
var preloadImages = function (srcs, imgs, callback) {
var img;
var remaining = srcs.length;
for (var i = 0; i < srcs.length; i++) {
img = new Image;
img.onload = function () {
--remaining;
if (remaining <= 0) {
callback();
}
};
img.src = srcs[i];
imgs.push(img);
}
};
Yes this will work, however browsers will limit(between 4-8) the actual calls and thus not cache/preload all desired images.
A better way to do this is to call onload before using the image like so:
function (imageUrls, index) {
var img = new Image();
img.onload = function () {
console.log('isCached: ' + isCached(imageUrls[index]));
*DoSomething..*
img.src = imageUrls[index]
}
function isCached(imgUrl) {
var img = new Image();
img.src = imgUrl;
return img.complete || (img .width + img .height) > 0;
}
I can confirm that the approach in the question is sufficient to trigger the images to be downloaded and cached (unless you have forbidden the browser from doing so via your response headers) in, at least:
Chrome 74
Safari 12
Firefox 66
Edge 17
To test this, I made a small webapp with several endpoints that each sleep for 10 seconds before serving a picture of a kitten. Then I added two webpages, one of which contained a <script> tag in which each of the kittens is preloaded using the preloadImage function from the question, and the other of which includes all the kittens on the page using <img> tags.
In all the browsers above, I found that if I visited the preloader page first, waited a while, and then went to the page with the <img> tags, my kittens rendered instantly. This demonstrates that the preloader successfully loaded the kittens into the cache in all browsers tested.
You can see or try out the application I used to test this at https://github.com/ExplodingCabbage/preloadImage-test.
Note in particular that this technique works in the browsers above even if the number of images being looped over exceeds the number of parallel requests that the browser is willing to make at a time, contrary to what Robin's answer suggests. The rate at which your images preload will of course be limited by how many parallel requests the browser is willing to send, but it will eventually request each image URL you call preloadImage() on.
The browser will work best using the link tag in the head.
export function preloadImages (imageSources: string[]): void {
imageSources
.forEach(i => {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'preload');
linkEl.setAttribute('href', i);
linkEl.setAttribute('as', 'image');
document.head.appendChild(linkEl);
});
}
This is what I did, using promises:
const listOfimages = [
{
title: "something",
img: "https://www.somewhere.com/assets/images/someimage.jpeg"
},
{
title: "something else",
img: "https://www.somewhere.com/assets/images/someotherimage.jpeg"
}
];
const preload = async () => {
await Promise.all(
listOfimages.map(
(a) =>
new Promise((res) => {
const preloadImage = new Image();
preloadImage.onload = res;
preloadImage.src = a.img;
})
)
);
}
This is the original answer but a with a more modern ES syntax:
let preloadedImages = [];
export function preloadImages(urls) {
preloadedImages = urls.map(url => {
let img = new Image();
img.src = url;
img.onload = () => console.log(`image url [${url}] has been loaded successfully`);
return img;
});
}
For anyone interested, here's some alternatives to code provided by OP.
preloadImage()
Function now returns
function preloadImage = function(url){
const img = new Image();
img.src = url;
return img
}
v1: Preload by passing images as arguments to preloadImages()
Returns array of Image typed objects returned by function. Useful to check status of preload.
jsFiddle
function preloadImage(url){
const img = new Image();
img.src = url;
return img
}
function preloadImages() {
const images = []
for (var i = 0; i < arguments.length; i++) {
images[i] = preloadImage(arguments[i])
}
return images
}
//-- usage --//
const images = preloadImages(
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg",
"http://domain.tld/gallery/image-003.jpg"
)
v2: Preload by passing images as an array to preloadImages()
Not type safe
Overwrites provided array with an Image type object. Returns array of Image typed objects returned by function. Useful to check status of preload.
jsFiddle
function preloadImage(url){
const img = new Image();
img.src = url;
return img
}
function preloadImages(images) {
for (var i = 0; i < images.length; i++) {
images[i] = preloadImage(images[i])
}
return images
}
//-- usage --//
let arr = [
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg",
"http://domain.tld/gallery/image-003.jpg"
]
const images = preloadImages(arr)
console.dir(images)
v3: Preload by passing either array(s) and/or argument(s to preloadImages()
Type safe. Returns array of Image typed objects returned by function. Useful to check status of preload.
jsFiddle
function preloadImage(url){
const img = new Image();
img.src = url;
return img
}
function preloadImages() {
const images = []
let c = 0
for (var i = 0; i < arguments.length; i++) {
if (Array.isArray(arguments[i])) {
for(var arr = 0; arr < arguments[i].length; arr++) {
if(typeof arguments[i][arr] == 'string') {
images[c] = preloadImage(arguments[i][arr])
c++
}
}
}
else if(typeof arguments[i] == 'string') {
images[c] = preloadImage(arguments[i])
c++
}
}
return images
}
//-- usage --//
var arr = [
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg"
]
const images = preloadImages(
arr,
"http://domain.tld/gallery/image-003.jpg",
"http://domain.tld/gallery/image-004.jpg",
[
"http://domain.tld/gallery/image-005.jpg",
"http://domain.tld/gallery/image-006.jpg"
]
)
console.dir(images)
Inspiration derived from: http://perishablepress.com/3-ways-preload-images-css-javascript-ajax/

Async image preloading by Promise.all not working on non-Chrome browsers

const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
img.src = url;
img.onload = () => res(img);
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
});
};
You do see a <div> containing all these images in Elements and the paths are correct:
<div style="display: none"> // I try removing <display: none> but not working, either
<img src="img/329b774421235a3b27d7142b1707ea01.jpg">
<img src="img/33df62feaa1871d7ff4c2b933aa82992.jpg">
<img src="img/5681a01b46e89618d96ff523dc81a1fb.jpg">
<img src="img/183ad681c899a84c82e288ac8ad30604.jpg">
<img src="img/1d9c8fdb875c31cbfa1f83e11a7038af.jpg">
<img src="img/71b1abb6a445059bf43463ab80e75506.jpg">
<img src="img/40734ae2e8255713e76814eba786f018.jpg">
<img src="img/8c40e3bdea0a863b76d888ad9952cf74.jpg">
<img src="img/8aa56b4e08ef9a40c92e6e0609991280.jpg">
</div>
Also in the Network you can see all the requests have been fetched properly (screenshot from Edge 18, where preloading is not working):
The async preloading only works in Chrome (and Opera), any other browsers (ff, edge, ie) lead to a flickering like not preloaded at all (but there's no extra http request when being displayed, which means those images were fetched and cached).
I check the browser support of Promise, Promise.all and async await and no problem.
However the traditional synchronous preloading works perfectly across browsers:
urls.forEach(url => {
const img = new Image();
img.src = url;
toolDiv.appendChild(img);
});
document.body.appendChild(toolDiv);
These images were processed by Webpack file-loader with Babel (core-js & runtime) but I think that's not the issue.
Need help thx!!
Update: I just borrow my roommate's iPhone 8 and no flickering.
I will make it clearer here. These images display as background images switching by css classes and triggered by mousewheel, e.g.:
/*.css */
.img1::before,
.img1::after {
background-image: url(../img/img1.jpg); /* Would be ../img/dasdfsafadasdasda.jpg after file-loader */
}
.img2::before,
.img2::after {
background-image: url(../img/img2.jpg);
}
<!-- .html -->
<aside class="img1" id="aside"></aside>
// .js
something.onwheel = () => {
document.getElementById('aside').className = 'img2';
};
Switch images by class rename.
But I still think this class switch is not the issue, either.
Final update: #Kaiido's method works. This issue is caused by the partial availability of fetched images. A following decode process is required for the full availability. whatwg
What you are facing here is that the load event only tells us that the resource has been fetched and that the browser is able to handle the media.
A big step still remains: Image decoding.
Indeed, even if all the resource has been fetched from the server and that the browser could parse from the file's headers that it will be able to decode it, and other data like the dimension of the media, some browser will wait until it's really required before trying to actually decode the image data (the "pixels" if you will).
This process still takes time, and it is not before you actually attach all these <img> elements to the document that these browsers will start doing that operation, hence the flicker.
Now, why Chrome doesn't face this issue? Because as demonstrated in this very related Q/A they do actually decode the images before firing the load event. There are pros and cons to this strategy, and specs are currently only asking for the no-decode behavior.
Now, there are ways around that issue, as demonstrated in the previously linked Q/A:
In supporting browsers, you can further wait for the HTMLImageElement.decode() promise, which will force the browser to entirely decode the image, and thus will be ready to paint it directly after the promise resolved:
img.onload = (evt) => {
img.decode().then(() => res(img));
};
// using big images so the latency is more visible
imgs = `https://upload.wikimedia.org/wikipedia/commons/d/dc/Spotted_hyena_%28Crocuta_crocuta%29.jpg
https://upload.wikimedia.org/wikipedia/commons/3/37/Mud_Cow_Racing_-_Pacu_Jawi_-_West_Sumatra%2C_Indonesia.jpg
https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg`.split(/\s+/g);
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
// we disable cache for demo
img.src = url + '?r=' + Math.random();
// further wait for the decoding
img.onload = (evt) => {
console.log('loaded data of a single image');
img.decode().then(() => res(img));
};
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
toolDiv.style.display = "";
console.log("all done");
});
};
d.onclick = () => Array.from(x = document.querySelectorAll('div')).forEach(x => x.parentNode.removeChild(x));
c.onclick = () => {
preloadImg(...imgs);
};
preloadImg(...imgs);
img {
width: 100vw
}
<button id="c">click</button><button id="d">click</button>
And if you need to support older browsers, you could monkeypatch it quite easily using an HTMLCanvasElement:
if( !HTMLImageElement.prototype.decode ) {
const canvas = document.createElement( 'canvas' );
canvas.width = canvas.height = 1; // low memory footprint
const ctx = canvas.getContext('2d');
HTMLImageElement.prototype.decode = function() {
return new Promise( ( resolve, reject ) => {
setTimeout( () => { // truly async
try {
ctx.drawImage(this,0,0);
resolve()
}
catch( err ) {
reject( err );
}
}, 0 );
} );
};
}
Accept Kaiido's answer if it works.
Used suggestion by Kaiido. Still flickers on Chrome. Keeping this here so it doesn't clutter OP's main post.
imgs=`https://i.imgur.com/OTQMjbE.jpg
https://i.imgur.com/gUpn5Jf.jpg
https://i.imgur.com/sIXWJWD.jpg
https://i.imgur.com/qhzfDD6.jpg`.split(/\s+/g);
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
img.src = url;
// using decode
img.onload = (evt) => {
img.decode().then(() => res(img));
};
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
});
};
preloadImg(...imgs);
let i = 1;
document.body.onwheel = () => {
document.getElementById('aside').className = 'img' + (i++%4+1)
};
/*.css */
.img1::before,
.img1::after,.img1 {
background-image: url(https://i.imgur.com/sIXWJWD.jpg); /* Would be ../img/dasdfsafadasdasda.jpg after file-loader */
}
.img2::before,
.img2::after,.img2 {
background-image: url(https://i.imgur.com/qhzfDD6.jpg);
}
.img3::before,
.img3::after,.img3 {
background-image: url(https://i.imgur.com/OTQMjbE.jpg);
}
.img4::before,
.img4::after,.img4 {
background-image: url(https://i.imgur.com/gUpn5Jf.jpg);
}
body {
height:5000px
}
#aside {
width: 500px;
height: 500px;
}
<div><aside class="img1" id="aside"></aside></div>

Retrieve preloaded image from cached array

I'm using multiple image preloading with JavaScript, and I'm assigning some style attributes to each of them, which is possible since these are HTML Images elements. I use the Promise technique, that I found here on StackOverflow:
function preloadImages(srcs) {
function loadImage(imgToLoad) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(imgToLoad.src);
};
img.className = 'testimg'; //Test to see if I can retrieve img
img.src = imgToLoad.src;
img.style.top = imgToLoad.top;
});
}
var promises = [];
for (var i = 0; i < srcs.length; i++) {
promises.push(loadImage(srcs[i]));
}
return Promise.all(promises);
}
An example of the value of "srcs":
srcs[0].top = '15%';
srcs[0].src = 'img/myImage1.jpg';
srcs[1].top = '24%';
srcs[1].src = 'img/myImage2.jpg';
The preloading part works perfectly, and I'm able to get all of the preloaded images at the end of the process, seeing that the style attributes have been applied, as following:
preloadImages(imgToLoad).then(function(imgs) {
// all images are loaded now and in the array imgs
console.log(imgs); // This works
console.log(document.getElementsByClassName('testimg')); //This doesn't
}, function(errImg) {
// at least one image failed to load
console.log('error in loading images.');
});
However, later in my code, I would like to take some of the elements from the array of preloaded images, so I can append them without having to precise the style attributes.
By that, I mean something more than only .append('aSrcThatHasBeenPreloaded'); . I want to keep the class and style attributes I defined during the preload.
Is that even possible? And if so, how should I proceed?
I tried document.getElementsByClassName('myPreloadedClass') but it returns an empty result, which means the preloaded images are not part of the DOM (which makes perfect sense).
Thank you for your help.
You're going to have to make sure you can somehow retrieve the img elements you've preloaded. There are many ways to do this but three that come to mind straight away.
1. Added loaded image in the DOM
In your example you're trying to retrieve the images with a query selector. This fails because the img elements aren't added to the DOM. You could attach them to a DOM element that is hidden from the user.
var
images = [
{
src: '//placehold.it/550x250?text=A',
top: 0,
class:'img-a' // unique class name to retrieve item using a query selector.
},
{
src: '//placehold.it/550x250?text=B',
top: 0,
class:'img-b'
}],
imageCache = document.getElementById('image-cache');
function preloadImages(srcs) {
function loadImage(imgToLoad) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
// The image has been loaded, add it to the cache element.
imageCache.appendChild(img);
resolve(img);
};
img.onerror = img.onabort = function() {
reject(imgToLoad.src);
};
img.className = 'testimg ' + imgToLoad.class; //Test to see if I can retrieve img
img.src = imgToLoad.src;
img.style.top = imgToLoad.top;
});
}
var promises = [];
for (var i = 0; i < srcs.length; i++) {
promises.push(loadImage(srcs[i]));
}
return Promise.all(promises);
}
// Load all the images.
preloadImages(images)
.then(result => {
// Add img B from the cache to the proof div.
document.getElementById('proof').appendChild(imageCache.querySelector('.img-b'));
});
.hidden {
display: none;
}
<div id="image-cache" class="hidden"></div>
<div id="proof"></div>
2. Added loaded image to a document fragment
If you don't want to add the img to the DOM you could add them to a document fragment. This way a visitor of the website won't be able to easily access them and you can still use a query selector to retrieve the image.
var
images = [
{
src: '//placehold.it/550x250?text=A',
top: 0,
class:'img-a' // unique class name to retrieve item using a query selector.
},
{
src: '//placehold.it/550x250?text=B',
top: 0,
class:'img-b'
}],
// Create a document fragment to keep the images in.
imageCache = document.createDocumentFragment();;
function preloadImages(srcs) {
function loadImage(imgToLoad) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
// The image has been loaded, add it to the cache element.
imageCache.appendChild(img);
resolve(img);
};
img.onerror = img.onabort = function() {
reject(imgToLoad.src);
};
img.className = 'testimg ' + imgToLoad.class; //Test to see if I can retrieve img
img.src = imgToLoad.src;
img.style.top = imgToLoad.top;
});
}
var promises = [];
for (var i = 0; i < srcs.length; i++) {
promises.push(loadImage(srcs[i]));
}
return Promise.all(promises);
}
// Load all the images.
preloadImages(images)
.then(result => {
// Add img B from the cache to the proof div.
document.getElementById('proof').appendChild(imageCache.querySelector('.img-b'));
// And try to add img B from the cache to the proof2 div. The image will only be visible once in the site.
document.getElementById('proof2').appendChild(imageCache.querySelector('.img-b'));
});
.hidden {
display: none;
}
<div id="proof"></div>
<div id="proof2" style="margin-top:1em"></div>
The downside
This second solution is very similar to the first one. You can still use a query selector to access the images. However both solutions have the same downside, once you place an image from the cache anywhere else on the site you can no longer retrieve the image from the cache. This is because an element can be in the DOM only once and appending it to another element will remove it from its previous parent element.
3. A reusable bit of code
I would recommend creating either a closure or an ES6 class where you can put all the functionality for preloading the images and retrieving them.
The closure from the snippet below returns two methods:
preloadImages which takes an array of images to preload.
getImage which takes a valid CSS selector which is used to retrieve the needed img element from the document fragment. Before returning the element it will clone it so you can add the same preloaded image multiple times to the DOM.
Cloning an element will also clone its ID, something to be mindful of. I think it is not much of an issue here since the img is created in code so you have full control over it.
I used a document fragment to store the preloaded images because I think it is a more elegant solution than adding the images to the DOM. It makes it a lot more difficult to get the img element without using the getImage method and thus prevents that the preloaded img element is somehow removed from the image cache.
var
images = [
{
src: '//placehold.it/550x250?text=A',
top: 0,
class:'img-a' // unique class name to retrieve item using a query selector.
},
{
src: '//placehold.it/550x250?text=B',
top: 0,
class:'img-b'
}];
var imagesPreloader = (function() {
var imagesCache = document.createDocumentFragment();
function loadImage(imgToLoad) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
imagesCache.appendChild(img)
resolve();
};
img.onerror = img.onabort = function() {
reject(imgToLoad.src);
};
img.className = 'testimg ' + imgToLoad.class ; //Test to see if I can retrieve img
img.src = imgToLoad.src;
img.style.top = imgToLoad.top;
});
}
function preloadImages(srcs) {
var promises = [];
for (var i = 0; i < srcs.length; i++) {
promises.push(loadImage(srcs[i]));
}
return Promise.all(promises);
}
function getImage(imageSelector) {
const
// Find the image in the fragment
imgElement = imagesCache.querySelector(imageSelector);
// When the selector didn't yield an image, return null.
if (imgElement === null) {
return null;
}
// Clone the image element and return it.
const
resultElement = imgElement.cloneNode();
return resultElement;
}
return {
getImage,
preloadImages
};
})();
imagesPreloader.preloadImages(images)
.then(result => {
console.log('Image with class "img-a": ', imagesPreloader.getImage('.img-a'));
document.querySelector('#proof').appendChild(imagesPreloader.getImage('.img-b'));
document.querySelector('#proof2').appendChild(imagesPreloader.getImage('.img-b'));
});
<div id="proof"></div>
<div id="proof2" style="margin-top:1em"></div>

Canvas/JS storing vector/bitmap objects

In my canvas project I have two types of sprite object, those that are drawn with vector graphics and those that have a bitmap image drawn to the canvas.
draw function(context){
...
context.lineTo(some place)
context.lineTo(some other place)
etc.
}
The other type of sprite is a bitmap image, at the minute they're placeholder assets so are small enough to not need an onload event handler, but the real assets will be larger, so will need one.
bitmap loadImage function(){
this.image = new Image();
//Currently my images are too small to warrant an onload eventHandler as they're placeholders
this.image.src = "some path";
}
Currently i'm storing all of my sprites in the same container and execute the drawing with a simple loop. This works for now, as the bitmap sprites don't have an onload function.
for each sprite {
sprite.draw(context);
}
Once i've replaced the placeholder assets with spritesheets, they're going to need a function assigned to the onload event - this brakes my design.
Can anyone shed any light on how I could achieve storing all the sprites in the same container and calling draw through iterating through that collection?
Note: With the onload event handler added the bitmaps draw, however (obviously) i'm getting an error when draw is called on the bitmap sprite before the image is loaded.
I made this loader which will allow you to add all the image URLs to it and then call load() to initiate the load.
You can use this kind of loader to support progress callback so you can display the current progress to the user.
If you need cross-origin images you can add support for this by adding a flag to tell the loaded to set crossOrigin type for you for the images. The following example sets this for all images but it can be extended to support this for individual images.
Live demo here
Loader:
/// callback - function to call when finished (mandatory)
/// progress - function to call for each image loaded (optional)
/// progress contains an argument with an "event" containing properties
/// img (the image loaded), url, current (integer) and total (integer)
function imageLoader(callback, progress, error) {
if (typeof callback !== 'function') throw 'Need a function for callback!';
var lst = [],
crossOrigin = false;
this.crossOrigin = function (state) {
if (typeof state !== 'bool') return crossOrigin;
crossOrigin = state;
return this;
}
this.add = function (url) {
lst.push(url);
return this;
}
this.load = function () {
if (lst.length > 0) {
startLoading();
}
return this;
}
function startLoading() {
var i = 0,
url,
count = lst.length,
images = [];
for (; url = lst[i]; i++) {
var img = document.createElement('img');
images.push(img);
img.onload = function () {
_handler(url, this)
};
img.onerror = function (e) {
_handlerError(url, e)
};
if (crossOrigin === true) img.crossOrigin = 'anonymous';
img.src = url;
}
function _handler(url, img) {
count--;
if (typeof progress === 'function') progress({
current: lst.length - count,
total: lst.length,
url: url,
img: img
});
if (count === 0) callback({
images: images,
urls: lst
});
}
function _handlerError(url, e) {
if (typeof error === 'function') error({
url: url,
error: e
});
console.warn('WARNING: Could not load image:', url);
_handler();
}
}
return this;
}
Usage:
var loader = new imageLoader(done, progress);
/// methods can be chained:
loader.add(url1)
.add(url2)
.add(url3)
.load();
(see demo for full example)
The handlers then can do:
function done(e) {
for (i = 0; i < e.images.length; i++) {
/// draw the image
ctx.drawImage(e.images[i], i * 20, i * 20, 40, 40);
}
}
function progress(e) {
///progress bar
status.style.width = ((e.current / e.total) * 100).toFixed(0) + '%';
/// current loaded image
ctx.drawImage(e.img, 0, 340, 60, 60);
}
http://jsbin.com/IMoFOz/1/edit
Note that because it's asynchronous, you need some sort of 'state machine' or indicator that the assets are done loading. Unlike a desktop game where the loading 'blocks' the rest of the execution, you can still do stuff while the images load in the background. Therefore you can probably call a function like start that will draw your images.
Like so:
function start() {
context.drawImage(image1, 69, 50);
context.drawImage(image2, 150, 150);
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var image1 = new Image();
var image2 = new Image();
var Image = {
count: 0,
Load: function(asset, url) {
asset.src = url;
asset.onload = function() {
Image.count--;
if (Image.count == 0)
{
console.log("Done loading everything.");
start();
}
}
Image.count++;
}
};
Image.Load(image1, 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg');
Image.Load(image2, 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg');

Preloading images with JavaScript

Is the function I wrote below enough to preload images in most, if not all, browsers commonly used today?
function preloadImage(url)
{
var img=new Image();
img.src=url;
}
I have an array of image URLs that I loop over and call the preloadImage function for each URL.
Yes. This should work on all major browsers.
Try this I think this is better.
var images = [];
function preload() {
for (var i = 0; i < arguments.length; i++) {
images[i] = new Image();
images[i].src = preload.arguments[i];
}
}
//-- usage --//
preload(
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg",
"http://domain.tld/gallery/image-003.jpg"
)
Source: http://perishablepress.com/3-ways-preload-images-css-javascript-ajax/
You can move this code to index.html for preload images from any url
<link rel="preload" href="https://via.placeholder.com/160" as="image">
In my case it was useful to add a callback to your function for onload event:
function preloadImage(url, callback)
{
var img=new Image();
img.src=url;
img.onload = callback;
}
And then wrap it for case of an array of URLs to images to be preloaded with callback on all is done:
https://jsfiddle.net/4r0Luoy7/
function preloadImages(urls, allImagesLoadedCallback){
var loadedCounter = 0;
var toBeLoadedNumber = urls.length;
urls.forEach(function(url){
preloadImage(url, function(){
loadedCounter++;
console.log('Number of loaded images: ' + loadedCounter);
if(loadedCounter == toBeLoadedNumber){
allImagesLoadedCallback();
}
});
});
function preloadImage(url, anImageLoadedCallback){
var img = new Image();
img.onload = anImageLoadedCallback;
img.src = url;
}
}
// Let's call it:
preloadImages([
'//upload.wikimedia.org/wikipedia/commons/d/da/Internet2.jpg',
'//www.csee.umbc.edu/wp-content/uploads/2011/08/www.jpg'
], function(){
console.log('All images were loaded');
});
const preloadImage = src =>
new Promise((resolve, reject) => {
const image = new Image()
image.onload = resolve
image.onerror = reject
image.src = src
})
// Preload an image
await preloadImage('https://picsum.photos/100/100')
// Preload a bunch of images in parallel
await Promise.all(images.map(x => preloadImage(x.src)))
CSS2 Alternative: http://www.thecssninja.com/css/even-better-image-preloading-with-css2
body:after {
content: url(img01.jpg) url(img02.jpg) url(img03.jpg);
display: none;
}
CSS3 Alternative: https://perishablepress.com/preload-images-css3/
(H/T Linh Dam)
.preload-images {
display: none;
width: 0;
height: 0;
background: url(img01.jpg),
url(img02.jpg),
url(img03.jpg);
}
NOTE: Images in a container with display:none might not preload.
Perhaps visibility:hidden will work better but I have not tested this. Thanks Marco Del Valle for pointing this out
I recommend you use a try/catch to prevent some possible issues:
OOP:
var preloadImage = function (url) {
try {
var _img = new Image();
_img.src = url;
} catch (e) { }
}
Standard:
function preloadImage (url) {
try {
var _img = new Image();
_img.src = url;
} catch (e) { }
}
Also, while I love DOM, old stupid browsers may have problems with you using DOM, so avoid it altogether IMHO contrary to freedev's contribution. Image() has better support in old trash browsers.
Working solution as of 2020
Most answers on this post no longer work - (atleast on Firefox)
Here's my solution:
var cache = document.createElement("CACHE");
cache.style = "position:absolute;z-index:-1000;opacity:0;";
document.body.appendChild(cache);
function preloadImage(url) {
var img = new Image();
img.src = url;
img.style = "position:absolute";
cache.appendChild(img);
}
Usage:
preloadImage("example.com/yourimage.png");
Obviously <cache> is not a "defined" element, so you could use a <div> if you wanted to.
Use this in your CSS, instead of applying the style attribute:
cache {
position: absolute;
z-index: -1000;
opacity: 0;
}
cache image {
position: absolute;
}
If you have tested this, please leave a comment.
Notes:
Do NOT apply display: none; to cache - this will not load the
image.
Don't resize the image element, as this will also affect the
quality of the loaded image when you come to use it.
Setting position: absolute to the image is necessary, as the image elements will eventually make it's way outside of the viewport - causing them to not load, and affect performance.
UPDATE
While above solution works, here's a small update I made to structure it nicely:
(This also now accepts multiple images in one function)
var cache = document.createElement("CACHE");
document.body.appendChild(cache);
function preloadImage() {
for (var i=0; i<arguments.length; i++) {
var img = new Image();
img.src = arguments[i];
var parent = arguments[i].split("/")[1]; // Set to index of folder name
if ($(`cache #${parent}`).length == 0) {
var ele = document.createElement("DIV");
ele.id = parent;
cache.appendChild(ele);
}
$(`cache #${parent}`)[0].appendChild(img);
console.log(parent);
}
}
preloadImage(
"assets/office/58.png",
"assets/leftbutton/124.png",
"assets/leftbutton/125.png",
"assets/leftbutton/130.png",
"assets/leftbutton/122.png",
"assets/leftbutton/124.png"
);
Preview:
Notes:
Try not to keep too many images preloaded at the same time (this can cause major performance issues) - I got around this by hiding images, which I knew wasn't going to be visible during certain events. Then, of course, show them again when I needed it.
This approach is a little more elaborate. Here you store all preloaded images in a container, may be a div. And after you could show the images or move it within the DOM to the correct position.
function preloadImg(containerId, imgUrl, imageId) {
var i = document.createElement('img'); // or new Image()
i.id = imageId;
i.onload = function() {
var container = document.getElementById(containerId);
container.appendChild(this);
};
i.src = imgUrl;
}
Try it here, I have also added few comments
Solution for ECMAScript 2017 compliant browsers
Note: this will also work if you are using a transpiler like Babel.
'use strict';
function imageLoaded(src, alt = '') {
return new Promise(function(resolve) {
const image = document.createElement('img');
image.setAttribute('alt', alt);
image.setAttribute('src', src);
image.addEventListener('load', function() {
resolve(image);
});
});
}
async function runExample() {
console.log("Fetching my cat's image...");
const myCat = await imageLoaded('https://placekitten.com/500');
console.log("My cat's image is ready! Now is the time to load my dog's image...");
const myDog = await imageLoaded('https://placedog.net/500');
console.log('Whoa! This is now the time to enable my galery.');
document.body.appendChild(myCat);
document.body.appendChild(myDog);
}
runExample();
You could also have waited for all images to load.
async function runExample() {
const [myCat, myDog] = [
await imageLoaded('https://placekitten.com/500'),
await imageLoaded('https://placedog.net/500')
];
document.body.appendChild(myCat);
document.body.appendChild(myDog);
}
Or use Promise.all to load them in parallel.
async function runExample() {
const [myCat, myDog] = await Promise.all([
imageLoaded('https://placekitten.com/500'),
imageLoaded('https://placedog.net/500')
]);
document.body.appendChild(myCat);
document.body.appendChild(myDog);
}
More about Promises.
More about "Async" functions.
More about the destructuring assignment.
More about ECMAScript 2015.
More about ECMAScript 2017.
The HTML Living Standard now supports 'preload'
According to the W3C HTML spec you can now preload using JavaScript like so:
var link = document.createElement("link");
link.rel = "preload";
link.as = "image";
link.href = "https://example.com/image.png";
document.head.appendChild(link);
Here is my approach:
var preloadImages = function (srcs, imgs, callback) {
var img;
var remaining = srcs.length;
for (var i = 0; i < srcs.length; i++) {
img = new Image;
img.onload = function () {
--remaining;
if (remaining <= 0) {
callback();
}
};
img.src = srcs[i];
imgs.push(img);
}
};
Yes this will work, however browsers will limit(between 4-8) the actual calls and thus not cache/preload all desired images.
A better way to do this is to call onload before using the image like so:
function (imageUrls, index) {
var img = new Image();
img.onload = function () {
console.log('isCached: ' + isCached(imageUrls[index]));
*DoSomething..*
img.src = imageUrls[index]
}
function isCached(imgUrl) {
var img = new Image();
img.src = imgUrl;
return img.complete || (img .width + img .height) > 0;
}
I can confirm that the approach in the question is sufficient to trigger the images to be downloaded and cached (unless you have forbidden the browser from doing so via your response headers) in, at least:
Chrome 74
Safari 12
Firefox 66
Edge 17
To test this, I made a small webapp with several endpoints that each sleep for 10 seconds before serving a picture of a kitten. Then I added two webpages, one of which contained a <script> tag in which each of the kittens is preloaded using the preloadImage function from the question, and the other of which includes all the kittens on the page using <img> tags.
In all the browsers above, I found that if I visited the preloader page first, waited a while, and then went to the page with the <img> tags, my kittens rendered instantly. This demonstrates that the preloader successfully loaded the kittens into the cache in all browsers tested.
You can see or try out the application I used to test this at https://github.com/ExplodingCabbage/preloadImage-test.
Note in particular that this technique works in the browsers above even if the number of images being looped over exceeds the number of parallel requests that the browser is willing to make at a time, contrary to what Robin's answer suggests. The rate at which your images preload will of course be limited by how many parallel requests the browser is willing to send, but it will eventually request each image URL you call preloadImage() on.
The browser will work best using the link tag in the head.
export function preloadImages (imageSources: string[]): void {
imageSources
.forEach(i => {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'preload');
linkEl.setAttribute('href', i);
linkEl.setAttribute('as', 'image');
document.head.appendChild(linkEl);
});
}
This is what I did, using promises:
const listOfimages = [
{
title: "something",
img: "https://www.somewhere.com/assets/images/someimage.jpeg"
},
{
title: "something else",
img: "https://www.somewhere.com/assets/images/someotherimage.jpeg"
}
];
const preload = async () => {
await Promise.all(
listOfimages.map(
(a) =>
new Promise((res) => {
const preloadImage = new Image();
preloadImage.onload = res;
preloadImage.src = a.img;
})
)
);
}
This is the original answer but a with a more modern ES syntax:
let preloadedImages = [];
export function preloadImages(urls) {
preloadedImages = urls.map(url => {
let img = new Image();
img.src = url;
img.onload = () => console.log(`image url [${url}] has been loaded successfully`);
return img;
});
}
For anyone interested, here's some alternatives to code provided by OP.
preloadImage()
Function now returns
function preloadImage = function(url){
const img = new Image();
img.src = url;
return img
}
v1: Preload by passing images as arguments to preloadImages()
Returns array of Image typed objects returned by function. Useful to check status of preload.
jsFiddle
function preloadImage(url){
const img = new Image();
img.src = url;
return img
}
function preloadImages() {
const images = []
for (var i = 0; i < arguments.length; i++) {
images[i] = preloadImage(arguments[i])
}
return images
}
//-- usage --//
const images = preloadImages(
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg",
"http://domain.tld/gallery/image-003.jpg"
)
v2: Preload by passing images as an array to preloadImages()
Not type safe
Overwrites provided array with an Image type object. Returns array of Image typed objects returned by function. Useful to check status of preload.
jsFiddle
function preloadImage(url){
const img = new Image();
img.src = url;
return img
}
function preloadImages(images) {
for (var i = 0; i < images.length; i++) {
images[i] = preloadImage(images[i])
}
return images
}
//-- usage --//
let arr = [
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg",
"http://domain.tld/gallery/image-003.jpg"
]
const images = preloadImages(arr)
console.dir(images)
v3: Preload by passing either array(s) and/or argument(s to preloadImages()
Type safe. Returns array of Image typed objects returned by function. Useful to check status of preload.
jsFiddle
function preloadImage(url){
const img = new Image();
img.src = url;
return img
}
function preloadImages() {
const images = []
let c = 0
for (var i = 0; i < arguments.length; i++) {
if (Array.isArray(arguments[i])) {
for(var arr = 0; arr < arguments[i].length; arr++) {
if(typeof arguments[i][arr] == 'string') {
images[c] = preloadImage(arguments[i][arr])
c++
}
}
}
else if(typeof arguments[i] == 'string') {
images[c] = preloadImage(arguments[i])
c++
}
}
return images
}
//-- usage --//
var arr = [
"http://domain.tld/gallery/image-001.jpg",
"http://domain.tld/gallery/image-002.jpg"
]
const images = preloadImages(
arr,
"http://domain.tld/gallery/image-003.jpg",
"http://domain.tld/gallery/image-004.jpg",
[
"http://domain.tld/gallery/image-005.jpg",
"http://domain.tld/gallery/image-006.jpg"
]
)
console.dir(images)
Inspiration derived from: http://perishablepress.com/3-ways-preload-images-css-javascript-ajax/

Categories

Resources