Is there a way to access fonts icons from html5 canvas? - javascript

I am new to html5 canvas.
I need to display fonticons (fontawesome) as images.
Is this possible? Thanks

It is indeed possible, though it is a bit cumbersome.
Since Canvas will draw with a fallback font if the actual font it not yet ready, and since fonts are lazy loaded you will need to hold of rendering until the font is ready. This should be possible using something like Google/Typekit's Web Font Loader (https://github.com/typekit/webfontloader)
Once the font is ready, you can draw it in canvas as any other string, something like
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '48px FontAwesome';
ctx.fillText(String.fromCharCode(61449), 10, 50);
The biggest challenge is that you have to remap all the symbols in Font Awesome, the their JavaScript char representations.
Edit:
This can actually be done using the name, by calcualting CSS rules
getFAChar = function (name) {
var elm = document.createElement('i');
elm.className = 'fa fa-' + name;
elm.style.display = 'none';
document.body.appendChild(elm);
var content = window.getComputedStyle(
elm, ':before'
).getPropertyValue('content')
document.body.removeChild(elm);
return content;
};
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '48px FontAwesome';
ctx.fillText(getFAChar('bed'), 10, 50)
Edit:
To improve performance, FA icons should be cached, especially if the Canvas is redrawn often (adding and removing a lot of DOM elements is not a good idea when trying to reach 60 fps)
var FontAwesome = (function () {
var me = {},
FACache = {};
function find (name) {
var elm = document.createElement('i');
elm.className = 'fa fa-' + name;
elm.style.display = 'none';
document.body.appendChild(elm);
var content = window.getComputedStyle(
elm, ':before'
).getPropertyValue('content')
document.body.removeChild(elm);
return content;
};
me.get = function (name) {
if (!!FACache[name]) return FACache[name];
var c = find(name);
FACache[name] = c;
return c;
};
return me;
}());
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '48px FontAwesome';
ctx.fillText(FontAwesome.get('bed'), 10, 50);
Edit
Complete example using deferred render, auto css injection and mapping of css chars, though only tested in Chrome (Uses font loading API and Promises without polyfill)
var FontAwesome = function () {
return new Promise(function (done, failed) {
var me = {},
FACache = {};
function find (name) {
var elm = document.createElement('i');
elm.className = 'fa fa-' + name;
elm.style.display = 'none';
document.body.appendChild(elm);
var content = window.getComputedStyle(
elm, ':before'
).getPropertyValue('content')
document.body.removeChild(elm);
return content;
};
me.get = function (name) {
if (!!FACache[name]) return FACache[name];
var c = find(name)[1];
FACache[name] = c;
return c;
};
(function() {
var l = document.createElement('link'); l.rel = 'stylesheet';
l.onload = function () {
document.fonts.load('10px FontAwesome')
.then(function (e) { done(me); })
.catch(failed);
}
l.href = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css';
var h = document.getElementsByTagName('head')[0]; h.parentNode.insertBefore(l, h);
}());
});
};
FontAwesome()
.then(function (fa) {
// All set, and ready to render!
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '48px FontAwesome';
ctx.fillText(fa.get('bed'), 10, 50);
});
<canvas id="canvas"></canvas>

Related

How to use shortened LUTs with javascript

based on this question
How to use LUT with JavaScript?
I tried to use this code
http://jsfiddle.net/gxu080ve/1/
var img = new Image,
canvas = document.getElementById("myCanvas"),
ctx = canvas.getContext("2d"),
src = "http://i.imgur.com/0vcCTK9.jpg?1"; // insert image url here
img.crossOrigin = "Anonymous";
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage( img, 0, 0 );
// alternateImage(img, canvas, ctx);
localStorage.setItem( "savedImageData", canvas.toDataURL("image/png") );
loadNew();
}
img.src = src;
// make sure the load event fires for cached images too
if ( img.complete || img.complete === undefined ) {
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
img.src = src;
}
var imgLut = new Image,
canvasLut = document.getElementById("myCanvasLut"),
ctxLut = canvasLut.getContext("2d"),
srcLut = "https://gm1.ggpht.com/nX2ZUYrMwPSXu5zGeeoMKyZP_R4nE205ivAdc3_yaccMEy8QYInfY_ynUB-NmrjvPKn0i1k7bdtHyk3Ul99ndvjvoCASud8FdIdq1fRrqDOCGK01rXgZZQ34ATKvtrkoysUCBmTUG70ZW_-TQxHExbu8gjhH-haIg0EuiWgJSxkL45jE1B4xWaOQNQXgJtmb7i1bSVcRgdmJq0XbttjsZnZn3YTW_LYw_3F-WyEEryTRritkZm6CW6NgaoVUfGH6XIaHp5Igs_dzm01lci9XwvoUwS0KA85w3npkjseL0zZX6u-pYWbSXOzkTLDJDMKPpOPt1VH6UUwARlD1YH1dQb0qdq4FrN8_beghJc00UaO9WHgyLyQ-NXMXFt5zXpeKuWtGwWtB0bzDhEvUXUhoDeOwaTbHlEjv3NgrqfzzpLBfLMM9J2BZLZodaEFA6WiroIsq70Qh6g_yMCVg02oi3s-L_2SSW2duayIIcfljyOxmC5AHbjzS2i-4RnKlVzK5Ge39wmiXX_4wtL0nb5XeDPwGbqqJsCPaeIYFN7z43HW6bA--5E3pUo3mjxLPTMSa8T-omZIw7w=w1896-h835-l75";
function loadNew(){
imgLut.crossOrigin = "Anonymous2";
imgLut.onload = function() {
canvasLut.width = imgLut.width;
canvasLut.height = imgLut.height;
ctxLut.drawImage( imgLut, 0, 0 );
filterImage(img, imgLut, canvasLut, ctxLut);
localStorage.setItem( "savedImageDataLut", canvasLut.toDataURL("image/png") );
}
imgLut.src = srcLutbyte;
// make sure the load event fires for cached images too
if ( imgLut.complete || imgLut.complete === undefined ) {
imgLut.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
imgLut.src = srcLutbyte;
}
}
function alternateImage(img, canvastoapply, ctxToApply){
//var c=document.getElementById("myCanvas");
//var img=document.getElementById("scream");
ctxToApply.drawImage(img,0,0);
var imgData=ctxToApply.getImageData(0,0,canvastoapply.width,canvastoapply.height);
// invert colors
for (var i=0;i<imgData.data.length;i+=4){
imgData.data[i]=255-imgData.data[i];
imgData.data[i+1]=255-imgData.data[i+1];
imgData.data[i+2]=255-imgData.data[i+2];
imgData.data[i+3]=255;
}
ctxToApply.putImageData(imgData,0,0);
};
function filterImage(img, filter, canvastoapply, ctxToApply){
//var c=document.getElementById("myCanvas");
//var img=document.getElementById("scream");
// ctxToApply.drawImage(img,0,0);
var lutWidth = canvasLut.width;
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var filterData=ctxToApply.getImageData(0,0,canvastoapply.width,canvastoapply.height);
// invert colors
for (var i=0;i<imgData.data.length;i+=4){
var r=Math.floor(imgData.data[i]/4);
var g=Math.floor(imgData.data[i+1]/4);
var b=Math.floor(imgData.data[i+2]/4);
var a=255;
var lutX = (b % 8) * 64 + r;
var lutY = Math.floor(b / 8) * 64 + g;
var lutIndex = (lutY * lutWidth + lutX)*4;
var Rr = filterData.data[lutIndex];
var Gg = filterData.data[lutIndex+1];
var Bb = filterData.data[lutIndex+2];
imgData.data[i] = filterData.data[lutIndex];
imgData.data[i+1] = filterData.data[lutIndex+1];
imgData.data[i+2] = filterData.data[lutIndex+2];
imgData.data[i+3] = 255;
}
ctx.putImageData(imgData,0,0);
for the attached LUT and it does not work properly because it only has 5 colums instead of 8. Of course I read the comment from How to use LUT with JavaScript? which says "This code only applies for 64x64x64 3DLUT images. The parameters vary if your LUT has other dimensions; the / 4, * 64, % 8, / 8 must be changed for other dimensions, but in this question's scope the LUT is 64x64x64."
i tried to do so and only ended in a mess.
What would I please have to change to make it work?

HTML canvas measureText().width is too big

I try to find out the width of a text in a html canvas.
I'm using the measureText method but it gives me a value the is more than double as expected. I generate the Text by using the toSting method. If I just hard code the return from toSting into measureText it works fine...
console.log("--->width "+this.ctx.measureText("-100").width);
returns 22
console.log(labels[i]);
returns "-100"
console.log("->width "+this.ctx.measureText(labels[i]).width);
returns 48
Any ideas why?
thanks for help!
Here is the whole code: https://jsfiddle.net/falkinator/Lze1rnm5/4/
function createGraph(){
this.canvas = document.getElementById("graph");
this.ctx = this.canvas.getContext("2d");
this.options={};
this.datasets=[];
this.options.fontSize=11;
this.ctx.font=this.options.fontSize+"px Arial";
this.ctx.textAlign="left";
this.ctx.textBaseline="middle";
this.datasetLength=500;
this.dataset=function(options){
/*this.strokeColor=options.strokeColor;
this.signalName=options.signalName;
this.signalMin=options.signalMin;
this.signalMax=options.signalMax;
this.signalUnit=options.signalUnit;*/
this.data=[];
this.min;
this.max;
};
this.buildYLabels = function(scaleMin, scaleMax){
var labels = [];
var maxLabelWidth=0;
console.log("--->width "+this.ctx.measureText("-100").width);
for(i=10;i>=0;i--){
labels.push((scaleMin+((scaleMax-scaleMin)/10)*i).toString());
console.log((scaleMin+((scaleMax-scaleMin)/10)*i).toString());
console.log("->width "+this.ctx.measureText(labels[i]).width);
if(maxLabelWidth<this.ctx.measureText(labels[i]).width){
maxLabelWidth=this.ctx.measureText(labels[i]).width;
}
}
return {labels: labels,
maxLabelWidth: maxLabelWidth};
};
this.buildXLabels = function(x){
};
this.draw = function (){
var _this=this;
each(this.datasets,function(dataset, index){
//plot data
if(index>0)return;
//draw scale
var canvasHeight = _this.canvas.height;
console.log("canvas height="+canvasHeight);
var yLabels = _this.buildYLabels(-100, 500);
var currX = _this.options.fontSize/2;
var scaleHeight = canvasHeight-_this.options.fontSize*1.5;
var maxLabelWidth=yLabels.maxLabelWidth;
console.log(yLabels.maxLabelWidth);
each(yLabels.labels,function(label, index){
_this.ctx.fillText(label,0,currX);
console.log(label);
currX+=(scaleHeight/10);
});
_this.ctx.beginPath();
_this.ctx.moveTo(maxLabelWidth,0);
_this.ctx.lineTo(maxLabelWidth,canvasHeight);
_this.ctx.stroke();
});
};
this.addSignal = function(){
var dataset = new this.dataset({});
this.datasets.push(dataset);
};
this.pushData = function(data){
var _this=this;
if(data.length!=this.datasets.length){
console.error("the number of pushed data is diffrent to the number of datasets!");
return;
}
each(data,function(data, index){
_this.datasets[index].data.push(data);
if(_this.datasets[index].data.length>_this.datasetLength){
_this.datasets[index].data.shift();
}
});
};
this.calculateScaling = function(dataset){
var range = dataset.max - dataset.min;
var decStep = Math.pow(10,Math.floor(Math.log10(range)));
var scaleMin = roundTo(dataset.min/*+(decStep*10)/2*/, decStep);
var scaleMax = roundTo(dataset.max/*+(decStep*10)/2*/, decStep);
var scaleStep = (scaleMax - scaleMin)/10;
};
var minx=-34, maxx=424;
var range = maxx - minx;
var decStep = Math.pow(10, Math.floor(Math.log10(range)));
var scaleMin = roundTo(minx-(decStep/2), decStep);
var scaleMax = roundTo(maxx+(decStep/2), decStep);
var scaleStep = (scaleMax - scaleMin)/10;
console.log(this.buildYLabels(scaleMin,scaleMax));
console.log("range="+range);
console.log("log="+Math.floor(Math.log10(range)));
console.log("scaleStep="+scaleStep);
console.log("decStep="+decStep);
console.log("scaleMin="+scaleMin);
console.log("scaleMax="+scaleMax);
}
graph = new createGraph();
graph.addSignal();
graph.addSignal();
graph.addSignal();
graph.pushData([1,2,3]);
graph.pushData([1,2,3]);
graph.draw();
function each(array, callback){
console.log(callback);
for(i in array){
callback(array[i], i);
}
}
function roundTo(num, to){
return Math.round(num/to)*to;
}
<canvas id="graph"></canvas>
In your code that generates the labels, the loop index i is going from 10 to 0. Each time though the loop you are pushing a new label to the array (i.e. labels[0], labels[1], ...) but you are attempting to measure the labels using the loop index i (i.e. labels[10], labels[9], ...). Thus, the first few measurments are of the text "undefined".
Change...
console.log("->width "+this.ctx.measureText(labels[i]).width);
if(maxLabelWidth<this.ctx.measureText(labels[i]).width){
maxLabelWidth=this.ctx.measureText(labels[i]).width;
to...
console.log("->width "+this.ctx.measureText(labels[labels.length-1]).width);
if(maxLabelWidth<this.ctx.measureText(labels[labels.length-1]).width){
maxLabelWidth=this.ctx.measureText(labels[labels.length-1]).width;

Using PreloadJS to load images and adding them to CreateJS stage

I'm trying to create a Tri Peaks Solitaire game in Javascript, using CreateJS to help interact with the HTML canvas. I'm not entirely new to programming, as I've taken college courses on Java and C, but I am new to both Javascript and HTML.
I created a large amount of the card game using a Card class and Deck class, with the image adding, event listening, and game logic done in one Javascript file, but this resulted in a lot of messy, somewhat buggy code that I felt needed cleaning up.
I'm trying to implement a separate class for the card table, which will be the canvas, or stage, and it will draw the 4 rows of playable cards, the stock pile, and the waste pile. I'm currently just trying to get the stock pile to show up on the canvas, but it's not happening.
My question is, why is my stock pile not showing up? Also, as you can see, the card images are being loaded when the cards are created. Should I be doing that differently?
Card class:
var Card = function(number) {
this.isFaceUp = false;
//number for image file path
this.number = number;
this.value = Math.ceil( (this.number)/4 );
//back of the card is default image
this.initialize("images/" + 0 + ".png");
console.log("card created")
this.height = this.getBounds().height;
this.width = this.getBounds().width;
//load the card's front image
this.frontImage = new Image();
this.frontImage.src = ( this.getImagePath() );
};
Card.prototype = new createjs.Bitmap("images/" + this.number + ".png");
Card.prototype.getImagePath = function() {
return "images/" + this.number + ".png";
};
Card.prototype.getValue = function () {
return this.value;
}
Card.prototype.flip = function() {
// this.image = new Image();
// this.image.src = ( this.getImagePath() );
this.image = this.frontImage;
this.isFaceUp = true;
};
Card.prototype.getHeight = function() {
return this.height;
};
Card.prototype.getWidth = function() {
return this.width;
};
CardDeck class:
function CardDeck() {
//create empty array
this.deck = [];
//fill empty array
for(i=1; i<=52; i++) {
//fill deck with cards, with i being the number of the card
this.deck[i-1] = new Card(i);
}
//shuffle deck
this.shuffle();
}
CardDeck.prototype.getCard = function() {
if(this.deck.length < 1) alert("No cards in the deck");
return this.deck.pop();
};
CardDeck.prototype.getSize = function() {
return this.deck.length;
};
CardDeck.prototype.shuffle = function() {
for (i=0; i<this.deck.length; i++) {
var randomIndex = Math.floor( Math.random()*this.deck.length );
var temp = this.deck[i];
this.deck[i] = this.deck[randomIndex];
this.deck[randomIndex] = temp;
}
};
CardDeck.prototype.getRemainingCards = function() {
return this.deck.splice(0);
}
CardDeck.prototype.listCards = function() {
for(i=0; i<this.deck.length; i++) {
console.log(this.deck[i].number);
}
};
CardTable class:
var CardTable = function(canvas) {
this.initialize(canvas);
this.firstRow = [];
this.secondRow = [];
this.thirdRow = [];
this.fourthRow = [];
this.stockPile = [];
this.wastePile = [];
//startX is card width
this.startX = ( new Card(0) ).width*3;
//startY is half the card height
this.startY = ( new Card(0) ).height/2;
};
CardTable.prototype = new createjs.Stage();
CardTable.prototype.createStockPile = function(cards) {
for(i=0; i<23; i++) {
cards[i].x = 10;
cards[i].y = 50;
this.stockPile[i] = cards[i];
}
};
CardTable.prototype.addToWastePile = function(card) {
card.x = startX + card.width;
card.y = startY;
this.wastePile.push(card);
this.addChild(card);
this.update();
};
CardTable.prototype.drawStockPile = function() {
for(i=0; i<this.stockPile.length; i++) {
console.log("this.stockPile[i]");
this.addChild(this.stockPile[i]);
}
this.update();
};
HTML file:
<html>
<head>
<title>Tri-Peaks Solitaire</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://code.createjs.com/easeljs-0.8.1.min.js"></script>
<script src="card.js"></script>
<script src="carddeck.js"></script>
<script src="cardtable.js"></script>
<script>
function init(){
var canvas = document.getElementById("canvas");
var table = new CardTable("canvas");
deck = new CardDeck();
table.createStockPile(deck.getRemainingCards());
table.drawStockPile();
table.update();
}
</script>
</head>
<body onload="init()">
<h1>Tri-Peaks Solitaire</h1>
<canvas style='border: 5px solid green; background: url("images/background.jpg");'
id="canvas" width="1000" height="450">
</canvas>
</body>
</html>
PRELOADJS UPDATE:
var canvas = document.getElementById("canvas");
var stage = new createjs.Stage(canvas);
var gameDeck = new CardDeck();
var numbers = [];
var preloader;
var progressText = new createjs.Text("", "20px Arial","#FF0000");
progressText.x = 50;
progressText.y = 20;
stage.addChild(progressText);
stage.update();
setupManifest();
startPreload();
function setupManifest() {
for(i=0; i<=52; i++) {
console.log("manifest");
manifest.push( {src:"images/" + i + ".png",
id: i} );
numbers.push(i);
}
}
function startPreload() {
preload = new createjs.LoadQueue(true);
preload.on("fileload", handleFileLoad);
preload.on("progress", handleFileProgress);
preload.on("complete", loadComplete);
preload.on("error", loadError);
preload.loadManifest(manifest);
}
function handleFileLoad(event) {
console.log("A file has loaded of type: " + event.item.type);
//console.log(numbers.pop());
var cardNumber = numbers.pop();
var newCard = new Card(cardNumber);
//faces.push( new Image() );
//faces[ faces.length - 1 ].src = "images/" + cardNumber + ".png";
gameDeck.insertCard(newCard);
}
function loadError(evt) {
console.log("error",evt.text);
}
function handleFileProgress(event) {
progressText.text = (preload.progress*100|0) + " % Loaded";
stage.update();
}
function loadComplete(event) {
console.log("Finished Loading Assets");
}
You are only updating the stage immediately after creating the cards. The cards are loading bitmap images, which is a concurrent operation that takes time. As such, the only time you render the stage is before the images load.
I would suggest using PreloadJS to preload your card images. This will also give you a lot more control over when / how they load, and let you show a progress bar or distractor to the user as they load.
I would suggest waiting for all the images to load and then calling stage.update(). Alternatively use the createjs.Ticker.addEventListener("tick", handleTick); event handler to update the stage for you.
Reference: http://www.createjs.com/docs/easeljs/classes/Ticker.html

EaselJs: Objects, Classes, and Making A Square Move

Although I have used Javascript extensively in the past, I have never used classes and objects in my programs. This is also the first for me using the HTML5 canvas element with an extra Javascript library. The library I'm using is EaselJS.
Short and sweet, I'm trying to make a square move with keyboard input, using object-oriented programming. I've already looked over sample game files, but I've never been able to properly get one to work.
The following is my classes script:
/*global createjs*/
// Shorthand createjs.Shape Variable
var Shape = createjs.Shape;
// Main Square Class
function square(name) {
this.name = name;
this.vX = 0;
this.vY = 0;
this.canMove = false;
this.vX = this.vY = 0;
this.canMove = false;
this.body = new Shape();
this.body.graphics.beginFill("#ff0000").drawRect(0, 0, 100, 100);
}
And below is my main script:
/*global createjs, document, window, square, alert*/
// Canvas and Stage Variables
var c = document.getElementById("c");
var stage = new createjs.Stage("c");
// Shorthand Create.js Variables
var Ticker = createjs.Ticker;
// Important Keycodes
var keycode_w = 87;
var keycode_a = 65;
var keycode_s = 83;
var keycode_d = 68;
var keycode_left = 37;
var keycode_right = 39;
var keycode_up = 38;
var keycode_down = 40;
var keycode_space = 32;
// Handle Key Down
window.onkeydown = handleKeyDown;
var lfHeld = false;
var rtHeld = false;
// Create Protagonist
var protagonist = new square("Mr. Blue");
// Set Up Ticker
Ticker.setFPS(60);
Ticker.addEventListener("tick", stage);
if (!Ticker.hasEventListener("tick")) {
Ticker.addEventListener("tick", tick);
}
// Init Function, Prepare Protagonist Placement
function init() {
protagonist.x = c.width / 2;
protagonist.y = c.height / 2;
stage.addChild(protagonist);
}
// Ticker Test
function tick() {
if (lfHeld) {
alert("test");
}
}
// Handle Key Down Function
function handleKeyDown(event) {
switch(event.keyCode) {
case keycode_a:
case keycode_left: lfHeld = true; return false;
case keycode_d:
case keycode_right: rtHeld = true; return false;
}
}
This is the error I get in the Developer Tools of Chrome:
Uncaught TypeError: undefined is not a function
easeljs-0.7.0.min.js:13
In case you're wondering, the order of my script tags is the EaselJS CDN, followed by my class, followed by the main script file.
I would really like closure on this question. Thank you in advance.
I figured it out. I was adding the entire protagonist instance to the stage. I've fixed by adding the protagonist.body to the stage.

Changes to Javascript created element doesn't reflect to DOM

I have a class, that is supposed to display grey overlay over page and display text and loading gif. Code looks like this:
function LoadingIcon(imgSrc) {
var elem_loader = document.createElement('div'),
elem_messageSpan = document.createElement('span'),
loaderVisible = false;
elem_loader.style.position = 'absolute';
elem_loader.style.left = '0';
elem_loader.style.top = '0';
elem_loader.style.width = '100%';
elem_loader.style.height = '100%';
elem_loader.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
elem_loader.style.textAlign = 'center';
elem_loader.appendChild(elem_messageSpan);
elem_loader.innerHTML += '<br><img src="' + imgSrc + '">';
elem_messageSpan.style.backgroundColor = '#f00';
this.start = function () {
document.body.appendChild(elem_loader);
loaderVisible = true;
};
this.stop = function() {
if (loaderVisible) {
document.body.removeChild(elem_loader);
loaderVisible = false;
}
};
this.setText = function(text) {
elem_messageSpan.innerHTML = text;
};
this.getElems = function() {
return [elem_loader, elem_messageSpan];
};
}
Problem is, when I use setText method, it sets innerHTML of elem_messageSpan, but it doesn't reflect to the span, that was appended to elem_loader. You can use getElems method to find out what both elements contains.
Where is the problem? Because I don't see single reason why this shouldn't work.
EDIT:
I call it like this:
var xml = new CwgParser('cwg.geog.cz/cwg.xml'),
loader = new LoadingIcon('./ajax-loader.gif');
xml.ondataparse = function() {
loader.stop();
document.getElementById('cover').style.display = 'block';
};
loader.setText('Loading CWG list...');
loader.start();
xml.loadXML();
xml.loadXML() is function that usually takes 3 - 8 seconds to execute (based on download speed of client), thats why I'm displaying loading icon.

Categories

Resources