I'm trying to create a non-JQuery image slideshow, where the images fade in/out based on a timer. After a ton of research today, I believe that JavaScript's Promise functionality can do this, but it's not quite right...
My problem is that the fade in and fade out functions appear to be happening at the same time, so the image just flickers. I'm trying to adapt some of the many examples of using then() to chain the functions, but obviously something is wrong.
My logic was to:
Show the initial image on page load (via a CSS rule)
Fade out
Changing the background image URL
Fade in
My online example is here, and the JavaScript is here:
// Background images to show
var arr_Images = [
'https://cdn.pixabay.com/photo/2015/08/24/12/53/banner-904884_960_720.jpg',
'https://cdn.pixabay.com/photo/2016/11/19/22/52/coding-1841550_960_720.jpg',
'https://cdn.pixabay.com/photo/2014/05/27/23/32/matrix-356024_960_720.jpg'
];
var cycleImage = document.getElementById("cycleImage");
// Preload images
for(x = 0; x < arr_Images.length; x++)
{
var img = new Image();
img.src = arr_Images[x];
}
function fadeOutPromise(element, nextBgImgUrl) {
console.log("Fading out");
return new Promise(function(resolve) {
var op = 1; // initial opacity
var timer1 = setInterval(function () {
if (op <= 0.1){
clearInterval(timer1);
element.style.display = 'none';
cycleImage.style.backgroundImage = "url('" + nextBgImgUrl + "')";
}
element.style.opacity = op;
element.style.filter = 'alpha(opacity=' + op * 100 + ")";
op -= op * 0.1;
}, 100);
resolve("Faded In");
});
}
function fadeInPromise(element) {
console.log("Fading in");
return new Promise(function(resolve) {
var op = 0.1; // initial opacity
element.style.display = 'block';
var timer2 = setInterval(function () {
if (op >= 1){
clearInterval(timer2);
}
element.style.opacity = op;
element.style.filter = 'alpha(opacity=' + op * 100 + ")";
op += op * 0.1;
}, 10);
resolve("Faded Out");
});
}
function slideShow() {
// Loop background image using array
var i = 0;
var delay = 3000;
// Loop
let imageLoopTimer = setTimeout(function tick() {
fadeOutPromise(cycleImage, arr_Images[i]).then(
fadeInPromise(cycleImage).then(function() {
i++;
if ( i >= arr_Images.length) { i = 0; }
imageLoopTimer = setTimeout(tick, delay);
})
);
}, delay);
}
slideShow();
Can somebody first explain what's wrong, then provide a solution?
You are almost there. The problem is how you resolve your promises.
What you intent to do in fade functions is:
- Create a promise
- Execute the logic with intervals
- Resolve promise
But you resolve the promise immediately, whereas you should resolve your promise once fade operation is done:
function fadeInPromise(element) {
console.log("Fading in");
return new Promise(function(resolve) {
var op = 0.1; // initial opacity
element.style.display = 'block';
var timer2 = setInterval(function () {
if (op >= 1){
clearInterval(timer2);
resolve(): // Fade is done here
}
element.style.opacity = op;
element.style.filter = 'alpha(opacity=' + op * 100 + ")";
op += op * 0.1;
}, 10);
});
}
And same thing in fadeout
Another tip for flickering
You should not change opacity that often if you want smooth effects. In css, there is already 'transition' property. So you should instead do something like this with transition:
- Show image with opacity 1
- Set opactiy to 0
- After ms, change url and set opacity to 1
setInterval() is running as a background task therefore resolve() will be fired right after the initialization of timer2.
Its pretty similar to running an async operation, but not awaiting
it.
Or with promises it would be like calling a function that returns a Promise, but wanting to access the data without a .then() callback
You need to resolve your promise inside the if (op >= 1) part for it to be actually resolved after the fadeIn/Out happened.
Check this snippet for an updated version of yours. The Promises are resolved inside the setInterval().
Here is the relevant JavaScript code of it as a direct snippet:
function fadeOutPromise(element, nextBgImgUrl) {
console.log("Fading out"+i);
return new Promise(function(resolve, reject) {
element.style.opacity = 0;
setTimeout(() => {
console.log("Fading out done"+i);
element.style.backgroundImage = "url('" + nextBgImgUrl + "')";
resolve();
}, 1000);
});
}
function fadeInPromise(element) {
console.log("Fading in"+i);
return new Promise(function(resolve, reject) {
element.style.opacity = 1;
setTimeout(resolve, 1000);
});
}
var i = 0;
var delay = 3000;
function slideShow() {
// Loop background image using array
// Loop
let imageLoopTimer = setTimeout(() => tick(), delay);
}
function tick() {
fadeOutPromise(cycleImage, arr_Images[i]).then(() =>
fadeInPromise(cycleImage).then(function() {
i++;
if ( i >= arr_Images.length) { i = 0; }
imageLoopTimer = setTimeout(() => tick(), delay);
})
);
}
Edit (with transition opacity)
I have edited the snippet and now it works with transition on opacity.
Related
function falling()
{
isFalling = true;
while (isFalling == true)
{
if (y < 120) {
y++;
}
else if (y == 120) {
isFalling = false;
}
}
}
I have tried adding setTimeout(function() around the entire loop, around the if statement, around the y++. I don't know what i'm doing wrong. Any time I add any of these the page becomes unresponsive once the falling function is called. I'm well aware that this is probably a duplicate question but the duplicate questions failed ti help.
{ }, 100)
You would do it like this:
function falling(y) {
if (y < 120) {
setTimeout(falling.bind(this, y + 1), 100); // specify your delay.
}
}
falling(0);
The question has indeed been answered several times, and the answers here are really not that different from this.
Note that I removed the apparently global variable isFalling. If you do need that variable in other code, then you can keep that variable updated as follows:
function falling(y) {
isFalling = y < 120;
if (isFalling) {
setTimeout(falling.bind(this, y + 1), 100); // specify your delay.
}
}
I would use window.setInterval() instead, as you want it to repeat until a certain number
working plunkr:
https://plnkr.co/edit/TbpplnIShiaJR7sHDUjP?p=preview
function falling()
{
var isFalling = true;
var y=0;
myInterval = window.setInterval(function() {
y++;
console.log(y);
if (y== 120) {
clearInterval(myInterval);
}
}, 100)
}
falling();
If the intent of calling falling() is to temporarily set the value of a global variable to true (as suggested by your current code), then this should do the trick.
var isFalling = false;
function falling(fallDuration) {
isFalling = true;
console.log("now falling");
setTimeout(function(){
isFalling = false;
console.log("landed");
}, fallDuration || 1000);
}
falling();
I imagine though that you might actually want to fall and then continue doing something else. In that case you probably want to look at a callback or into promises. You might do a simple:
var isFalling = false;
function falling(fallDuration, andThen) {
isFalling = true;
console.log("now falling");
setTimeout(function() {
isFalling = false;
console.log("landed");
andThen();
}, fallDuration);
}
falling(1000, function(){ console.log("now standing"); });
I want to make a delay inside my for loop, but it won't really work.
I've already tried my ways that are on stackoverflow, but just none of them work for what I want.
This is what I've got right now:
var iframeTimeout;
var _length = $scope.iframes.src.length;
for (var i = 0; i < _length; i++) {
// create a closure to preserve the value of "i"
(function (i) {
$scope.iframeVideo = false;
$scope.iframes.current = $scope.iframes.src[i];
$timeout(function () {
if ((i + 1) == $scope.iframes.src.length) {
$interval.cancel(iframeInterval);
/*Change to the right animation class*/
$rootScope.classess = {
pageClass: 'nextSlide'
}
currentId++;
/*More information about resetLoop at the function itself*/
resetLoop();
} else {
i++;
$scope.iframes.current = $scope.iframes.src[i];
}
}, $scope.iframes.durationValue[i]);
}(i));
}
alert("done");
This is what I want:
First of all I got an object that holds src, duration and durationValue.
I want to play both video's that I have in my object.
I check how many video's I've got
I make iframeVideo visible (ngHide)
I insert the right <iframe> tag into my div container
It starts the $timeout with the right duration value
If that's done, do the same if there is another video. When it was the last video it should fire some code.
I hope it's all clear.
I've also tried this:
var iframeInterval;
var i = 0;
$scope.iframeVideo = false;
$scope.iframes.current = $scope.iframes.src[i];
iframeInterval = $interval(function () {
if ((i + 1) == $scope.iframes.src.length) {
$interval.cancel(iframeInterval);
/*Change to the right animation class*/
$rootScope.classess = {
pageClass: 'nextSlide'
}
currentId++;
/*More information about resetLoop at the function itself*/
resetLoop();
} else {
i++;
$scope.iframes.current = $scope.iframes.src[i];
}
}, $scope.iframes.durationValue[i])
Each $timeout returns a different promise. To properly cancel them, you need to save everyone of them.
This example schedules several subsequent actions starting at time zero.
var vm = $scope;
vm.playList = []
vm.playList.push({name:"video1", duration:1200});
vm.playList.push({name:"video2", duration:1300});
vm.playList.push({name:"video3", duration:1400});
vm.playList.push({name:"video4", duration:1500});
vm.watchingList=[];
var timeoutPromiseList = [];
vm.isPlaying = false;
vm.start = function() {
console.log("start");
//ignore if already playing
if (vm.isPlaying) return;
//otherwise
vm.isPlaying = true;
var time = 0;
for (var i = 0; i < vm.playList.length; i++) {
//IIFE closure
(function (i,time) {
console.log(time);
var item = vm.playList[i];
var p = $timeout(function(){playItem(item)}, time);
//push each promise to list
timeoutPromiseList.push(p);
})(i,time);
time += vm.playList[i].duration;
}
console.log(time);
var lastPromise = $timeout(function(){vm.stop()}, time);
//push last promise
timeoutPromiseList.push(lastPromise);
};
Then to stop, cancel all of the $timeout promises.
vm.stop = function() {
console.log("stop");
for (i=0; i<timeoutPromiseList.length; i++) {
$timeout.cancel(timeoutPromiseList[i]);
}
timeoutPromiseList = [];
vm.isPlaying = false;
};
The DEMO on PLNKR.
$timeout returns promise. You can built a recursive chain of promises like this, so every next video will play after a small amount of time.
I have a progress bar that I update in a loop of many iterations.
https://jsfiddle.net/k29qy0do/32/
(open the console before you click the start button)
var progressbar = {};
$(function () {
progressbar = {
/** initial progress */
progress: 0,
/** maximum width of progressbar */
progress_max: 0,
/** The inner element of the progressbar (filled box). */
$progress_bar: $('#progressbar'),
/** Set the progressbar */
set: function (num) {
if (this.progress_max && num) {
this.progress = num / this.progress_max * 100;
console.log('percent: ' + this.progress + '% - ' + num + '/' + this.progress_max);
this.$progress_bar.width(String(this.progress) + '%');
}
},
fn_wrap: function (num) {
setTimeout(function() {
this.set(num);
}, 0);
}
};
});
$('#start_button').on('click', function () {
var iterations = 1000000000;
progressbar.progress_max = iterations;
var loop = function () {
for (var i = 1; i <= iterations; i++) {
if (iterations % i === 100) {
progressbar.set(i); //only updates the progressbar in the last iteration
//progressbar.fn_wrap(i); //even worse, since no output to the console is produced
}
}
}
//setTimeout(loop, 0);
loop();
});
The console is updated iteratively as expected.
However, the progressbar is not updating.
The problem is that the browser window seems to 'hang' until the loop finishes.
Only the console is updated, not the progressbar.
I have tried to add the setTimeout, as suggested below, in several places.
But that just makes things worse, because I then do not even get the console to output the progress while executing the loop.
Okay, I found a solution in the answer to this question:
Javascript: How to update a progress bar in a 'for' loop
var i = 0;
(function loop() {
i++;
if (iterations % i === 100) {
progressbar.set(i); //updates the progressbar, even in loop
}
if (i < iterations) {
setTimeout(loop, 0);
}
})();
My solution:
https://jsfiddle.net/ccvs4rer/3/
Lets break this down to steps
Step 1: Clean up HTML
Assuming the purpose of your question is to understand how to work the progress bar and not the styles or the labels (loading, please be patient, etc.). Lets just have the progress bar and the start button.
<div id='progressbar-outer' style="">
<div id='progressbar' style=""></div>
</div>
<button id="start_button">Start</button>
Step 2: The Styles
Lets make the progress bar visible to the user
#progressbar-outer {
height:2em;
border:5px solid #000;
width:15em;
}
#progressbar {
width:0%;
background-color:#F00;
height:100%;
}
Step 3: Using setTimeout where it belongs
In your code, you have used setTimeout to set the value of your progress bar. However, the for loop is still active.
for (var i = 1; i <= iterations; i++) {
if (iterations % i === 100) {
progressbar.set(i); //only updates the progressbar in the last iteration
//progressbar.fn_wrap(i); //even worse, since no output to the console is produced
//setTimeout(function() {
// progressbar.set(i);
//}, 0);
}
}
The use of setTimeout does not affect the rest of the code. Hence, the UI was held hostage till the loop ended. Try the following code.
$('#start_button').on('click', function () {
var iterations = 100;
progressbar.progress_max = iterations;
var loop = function (value) {
progressbar.set(value);
if (value < iterations) setTimeout(function () {
loop(value + 1)
}, 30);
else $('#progressbar').css('background-color', '#0F0');
}
loop(1);
});
Preview
Try this fiddle: https://jsfiddle.net/Ljc3b6rn/4/
What you really want is an Asynchronous loop to allow the browser to update the DOM in between iterations.
JSFiddle: http://jsfiddle.net/u5b6gr1w/
function delayedLoop(collection, delay, callback, context) {
context = context || null;
var i = 0,
nextInteration = function() {
if (i === collection.length) {
return;
}
callback.call(context, collection[i], i);
i++;
setTimeout(nextInteration, delay);
};
nextInteration();
}
Some HTML:
<div class="progress-bar"><div style="width: 0"></div></div>
A splash of CSS:
.progress-bar {
border: 1px solid black;
background-color: #f0f0f0;
}
.progress-bar div {
background-color: red;
height: 1.25em;
}
And some JavaScript to wire things together:
var progressBar = document.querySelector(".progress-bar div"),
items = [1,2,3,4,5,6,7,8,9,10];
delayedLoop(items, 500, function(item, index) {
var width = (item / items.length * 100) + "%";
progressBar.style.width = width;
progressBar.innerHTML = width;
});
My guess would be that all your progress updates are running in the same call stack. While JavaScript code is running, the DOM cannot update. Maybe this question will help you come up with a work-around.
What do you wnat to do? Why do you need it? You should only use a progressbar when you have to wait for something to finish. But we don't know what you do on your page.
If you want to display the progress of an ajax upload:
$.ajax({
...
xhr: function() {
var xhr = $.ajaxSettings.xhr();
$(xhr.upload).bind("progress", function(event) {
var e = event.originalEvent;
var percent = 0;
if (e.lengthComputable)
percent = Math.ceil(e.loaded/e.total*100);
$("#progress").width(percent+"%");
});
return xhr;
}
...
});
For images, you need an ajax call:
$.ajax({
method: "GET",
url: "http://example.com/path/image.jpg",
xhr: function() {/* see the code above*/ }
...
});
For getting the content of an uploaded file:
var reader = new FileReader();
reader.readAsText(uploadedFile);
$(reader).bind("progress", function(e) {
var percent = 0;
if (e.lengthComputable)
percent = Math.ceil(e.loaded/e.total*100);
$("#progress").css("width", percent+"%");
});
For large around of process, like math or appending a lot of divs that will take 10+ secons:
Main.js:
var worker = new Worker("Worker.js");
$(worker).bind("message", function(data) {
$("#progress").width((data*100)+"%");
});
Worker.js:
var total = 43483,
finished = 0,
doStuff = function() {
++finished;
return 1+1;
};
setInterval(function()
{
self.postMessage(finished/total);
}, 100);
for (var i = 0; i < total; ++i)
setTimeout(doStuff, i*10);
Because it's nice, and you want to tell the user there's a progress when there isn't, just animate the div:
$("#progress").animate({width: "100%"}, 3000);
You can use promises to wait until the width is set before continuing the loop.
Updating the progress bar for 1000000000 iterations will be slow if you go 1 by 1, so you might find it useful to decrease the update frequency.
Instead of a for loop, I used a recursive function that loops when the promise has been fulfilled.
set: function (num) {
var deferred = $.Deferred();
if (this.progress_max && num) {
this.progress = num / this.progress_max * 100;
var self = this;
self.$progress_bar.animate({"width": String(this.progress) + '%'}, "fast", function() {
deferred.resolve();
});
return deferred;
}
}
$('#start_button').on('click', function () {
var iterations = 1000000000;
var i = 0;
progressbar.progress_max = iterations;
var loop = function(){
i+=100000000;
if(i <= iterations){
progressbar.set(i).then(function(){
loop();
}); ;
}
};
loop();
});
https://jsfiddle.net/k29qy0do/34/
You have to use window.requestAnimationFrame, otherwise the browser will block until your loop is finished. The callback passed to requestAnimationFrame will get a timestamp as a parameter which you might be able to use for calculations of the progress.
This are my 2 takes on the question:
Using a web worker. The webworker blob code comes from here
Web worker code:
<script type="text/ww">
function loop(e) {
var data = JSON.parse(e.data);
var i = parseInt(data.i, 10);
var iterations = parseInt(data.iterations, 10);
while (iterations % ++i !== 100 && i <= iterations);
if(i <= iterations) {
self.postMessage(JSON.stringify({ i: i, iterations: iterations }));
}
}
self.onmessage = function(e) {
loop(e);
};
</script>
The code:
var ww = document.querySelector('script[type="text/ww"]'),
code = ww.textContent,
blob = new Blob([code], {type: 'text/javascript'}),
blobUrl = URL.createObjectURL(blob),
worker = new Worker(blobUrl);
worker.onmessage = function(e) {
var data = JSON.parse(e.data);
var i = parseInt(data.i, 10);
var iterations = parseInt(data.iterations, 10);
progressbar.set(i);
worker.postMessage(JSON.stringify({ i: i, iterations: iterations }));
}
$('#start_button').on('click', function () {
var iterations = 1000000000;
progressbar.progress_max = iterations;
worker.postMessage(JSON.stringify({ i: 0, iterations: iterations }));
});
The other idea hangs the UI thread, but changes the width visually, as I use requestAnimationFrame to break the counting, change width of the progressbar, and then continue the count.
function loopFrame(i, iterations) {
requestAnimationFrame(function() {
if (iterations % i === 100) {
progressbar.set(i);
}
if(i < iterations) {
loopFrame(i + 1, iterations);
}
});
}
$('#start_button').on('click', function () {
var iterations = 1000000000;
console.log(iterations);
progressbar.progress_max = iterations;
loopFrame(0, iterations);
});
Maybe this will be usefull.
var service = new Object();
//function with interrupt for show progress of operations
service.progressWhile = new Object();
service.progressWhile.dTime = 50; //step ms between callback display function
service.progressWhile.i = 0; //index
service.progressWhile.timer = 0; //start time for cycle
//#parametr arr - array for actions
//#parametr actionCallback - The function for processing array's elements
//#parametr progressCallback - function to display the array index
function progressWhile(arr, actionCallback, progressCallback) {
try {
var d = new Date();
service.progressWhile.timer = d.getTime();
log(service.progressWhile.i);
if (service.progressWhile.i >= arr.length) {
service.progressWhile.i = 0;
return;
}
while (service.progressWhile.i < arr.length) {
actionCallback(arr[service.progressWhile.i++]);
d = new Date();
if (d.getTime() - service.progressWhile.timer > service.progressWhile.dTime) {
break;
}
}
if (progressCallback != undefined)
progressCallback(service.progressWhile.i);
} catch (er) {
log(er);
return;
}
setTimeout(function () {
progressWhile(arr, actionCallback, progressCallback);
}, 0);
}
Here's updated fiddle
I used animate to make it a progress bar like look and feel.
Hope this will help you.
var progressbar = {};
$(function() {
progressbar = {
/** initial progress */
progress : 0,
/** maximum width of progressbar */
progress_max : 0,
/** The inner element of the progressbar (filled box). */
$progress_bar : $('#progressbar'),
/** Method to set the progressbar.*/
set : function(num) {
if (this.progress_max && num) {
this.progress = num / this.progress_max * 100;
console.log('percent: ' + this.progress + '% - ' + num + '/' + this.progress_max);
$('#progressbar').animate({
width : String(this.progress) + '%',
}, 500, function() {
// Animation complete.
});
}
},
fn_wrap : function(num) {
setTimeout(function() {
this.set(num);
}, 0);
}
};
});
$('#start_button').on('click', function() {
$('#progressbar').css('width', '0%');
var iterations = 1000000000;
progressbar.progress_max = iterations;
var loop = function() {
for (var i = 1; i <= iterations; i++) {
if (iterations % i === 100) {
progressbar.set(i);
//only updates the progressbar in the last iteration
}
}
}
loop();
});
Fiddler
[1]: https://jsfiddle.net/k29qy0do/21/
Is it possible that we could stop setInterval means next interval will call only when first event completed.
I want to load next image only when previous image loaded.This event should work only after mouse over.But if next image is large in size then next interval is calling in my case..i want to stop interval until last image not loaded.
This is my code:
jQuery('.product').on('mouseover', function(){
timer = setInterval(function() {
if (counter === product_images.length) {
counter = 0;
}
// selector.addClass('hidden');
loading_image.removeClass('hidden');
selector.attr('src', 'localhost/product/' + product_images[counter]);
var loadImage = new Image();
loadImage.src = selector.attr('src');
loadImage.onload = function() {console.log('after load');
loading_image.addClass('hidden');
// selector.removeClass('hidden');
selector.show();
};
counter = counter + 1;
}, 1000);
How we can stop setInterval or prevent it to go next interval even task of last interval is not completed.Not for clearInterval,It should be continue(setInterval) if first task completed.
Please help me.
try the below code. this may work,
var flag = 1;
jQuery('.product').on('mouseover', function(){
if(typeof timer != "undefined")
clearInterval(timer);
timer = setInterval(function() {
if(flag){
if (counter === product_images.length) {
counter = 0;
}
loading_image.removeClass('hidden');
selector.attr('src', 'localhost/product/' + product_images[counter]);
flag = 0;
var loadImage = new Image();
loadImage.src = selector.attr('src');
loadImage.onload = function() {
console.log('after load');
loading_image.addClass('hidden');
selector.show();
flag = 1;
};
counter = counter + 1;
}
}, 1000);
NOTE: I have not tested this code.
I would like to add a small dice-rolling effect to my Javascript code. I think a good way is to use the setInterval() method. My idea was the following code (just for testing):
function roleDice() {
var i = Math.floor((Math.random() * 25) + 5);
var j = i;
var test = setInterval(function() {
i--;
document.getElementById("dice").src = "./images/dice/dice" + Math.floor((Math.random() * 6) + 1) + ".png";
if (i < 1) {
clearInterval(test);
}
}, 50);
}
Now I would like to wait for the setInterval until it is done. So I added a setTimeout.
setTimeout(function(){alert("test")}, (j + 1) * 50);
This code works quite okay.
But in my main code the roleDice() function returns a value. Now I don’t know how I could handle that... I can’t return from the setTimeout(). If I add a return to the end of the function, the return will rise too fast. Does anyone have an idea, of how I could fix that?
Edit
Hmm, okay I understand what the callback does and I think I know how it works but I have still the problem. I think it’s more of an "interface" problem...
Here is my code:
function startAnimation(playername, callback) {
var i = Math.floor((Math.random() * 25) + 5);
var int = setInterval(function() {
i--;
var number = Math.floor((Math.random() * 6) + 1);
document.getElementById("dice").src = "./images/dice/dice" + number + ".png";
if(i < 1) {
clearInterval(int);
number = Math.floor((Math.random() * 6) + 1);
addText(playername + " rolled " + number);
document.getElementById("dice").src = "./images/dice/dice" + number + ".png";
callback(number);
}
}, 50);
}
function rnd(playername) {
var callback = function(value){
return value; // I knew thats pointless...
};
startAnimation(playername, callback);
}
The function rnd() should wait and return the value… I’m a little bit confused. At the moment I have no clue how to going on... The code wait for the var callback... but how I could combine it with the return? I would like to run the animation and return after that the last number with rnd() to another function.
You stumbled into the pitfall most people hit at some point when they get in touch with asynchronous programming.
You cannot "wait" for an timeout/interval to finish - trying to do so would not work or block the whole page/browser. Any code that should run after the delay needs to be called from the callback you passed to setInterval when it's "done".
function rollDice(callback) {
var i = Math.floor((Math.random() * 25) + 5);
var j = i;
var test = setInterval(function() {
i--;
var value = Math.floor((Math.random() * 6) + 1);
document.getElementById("dice").src = "./images/dice/dice" + value + ".png";
if(i < 1) {
clearInterval(test);
callback(value);
}
}, 50);
}
You then use it like this:
rollDice(function(value) {
// code that should run when the dice has been rolled
});
You can now use Promises and async/await
Like callbacks, you can use Promises to pass a function that is called when the program is done running. If you use reject you can also handle errors with Promises.
function rollDice() {
return new Promise((resolve, reject) => {
const dice = document.getElementById('dice');
let numberOfRollsLeft = Math.floor(Math.random() * 25 + 5);
const intervalId = setInterval(() => {
const diceValue = Math.floor(Math.random() * 6 + 1);
// Display the dice's face for the new value
dice.src = `./images/dice/dice${diceValue}.png`;
// If we're done, stop rolling and return the dice's value
if (--numberOfRollsLeft < 1) {
clearInterval(intervalId);
resolve(diceValue);
}
}, 50);
});
}
Then, you can use the .then() method to run a callback when the promise resolves with your diceValue.
rollDice().then((diceValue) => {
// display the dice's value to the user via the DOM
})
Or, if you're in an async function, you can use the await keyword.
async function takeTurn() {
// ...
const diceValue = await rollDice()
// ...
}
Orginally your code was all sequential. Here is a basic dice game where two players roll one and they see who has a bigger number. [If a tie, second person wins!]
function roleDice() {
return Math.floor(Math.random() * 6) + 1;
}
function game(){
var player1 = roleDice(),
player2 = roleDice(),
p1Win = player1 > player2;
alert( "Player " + (p1Win ? "1":"2") + " wins!" );
}
game();
The code above is really simple since it just flows. When you put in a asynchronous method like that rolling the die, you need to break up things into chunks to do processing.
function roleDice(callback) {
var i = Math.floor((Math.random() * 25) + 5);
var j = i;
var test = setInterval(function(){
i--;
var die = Math.floor((Math.random() * 6) + 1);
document.getElementById("dice").src = "./images/dice/dice" + die + ".png";
if(i < 1) {
clearInterval(test);
callback(die); //Return the die value back to a function to process it
}
}, 50);
}
function game(){
var gameInfo = { //defaults
"p1" : null,
"p2" : null
},
playerRolls = function (playerNumber) { //Start off the rolling
var callbackFnc = function(value){ //Create a callback that will
playerFinishes(playerNumber, value);
};
roleDice( callbackFnc );
},
playerFinishes = function (playerNumber, value) { //called via the callback that role dice fires
gameInfo["p" + playerNumber] = value;
if (gameInfo.p1 !== null && gameInfo.p2 !== null ) { //checks to see if both rolls were completed, if so finish game
giveResult();
}
},
giveResult = function(){ //called when both rolls are done
var p1Win = gameInfo.p1 > gameInfo.p2;
alert( "Player " + (p1Win ? "1":"2") + " wins!" );
};
playerRolls("1"); //start player 1
playerRolls("2"); //start player 2
}
game();
The above code could be better in more of an OOP type of way, but it works.
There are a few issues for the above solutions to work. Running the program doesn't (at least not in my preferred browser) show any images, so these has to be loaded before running the game.
Also, by experience I find the best way to initiate the callback method in cases like preloading N images or having N players throw a dice is to let each timeout function do a countdown to zero and at that point execute the callback. This works like a charm and does not rely on how many items needing to be processed.
<html><head><script>
var game = function(images){
var nbPlayers = 2, winnerValue = -1, winnerPlayer = -1;
var rollDice = function(player,callbackFinish){
var playerDice = document.getElementById("dice"+player);
var facesToShow = Math.floor((Math.random() * 25) + 5);
var intervalID = setInterval(function(){
var face = Math.floor(Math.random() * 6);
playerDice.src = images[face].src;
if (--facesToShow<=0) {
clearInterval(intervalID);
if (face>winnerValue){winnerValue=face;winnerPlayer=player}
if (--nbPlayers<=0) finish();
}
}, 50);
}
var finish = function(){
alert("Player "+winnerPlayer+" wins!");
}
setTimeout(function(){rollDice(0)},10);
setTimeout(function(){rollDice(1)},10);
}
var preloadImages = function(images,callback){
var preloads = [], imagesToLoad = images.length;
for (var i=0;i<images.length;++i){
var img=new Image();
preloads.push(img);
img.onload=function(){if(--imagesToLoad<=0)callback(preloads)}
img.src = images[i];
}
}
preloadImages(["dice1.png","dice2.png","dice3.png","dice4.png","dice5.png","dice6.png"],game);
</script></head><body>
<img src="" id="dice0" /><img src="" id="dice1" /></body></html>
To achieve that goal, using vanilla setInterval function is simply impossible. However, there is better alternative to it: setIntervalAsync.
setIntervalAsync offers the same functionality as setInterval, but it guarantees that the function will never executed more than once in a given interval.
npm i set-interval-async
Example:
setIntervalAsync(
() => {
console.log('Hello')
return doSomeWork().then(
() => console.log('Bye')
)
},
1000
)
Example with Promises & setIntervals.. this is how I created a 'flat' chain of functions that wait until the other is completed...
The below snippet uses a library called RobotJS (here it returns a color at a specific pixel on screen) to wait for a button to change color with setInterval, after which it resolves and allows the code in the main loop to continue running.
So we have a mainChain async function, in which we run functions that we declare below. This makes it clean to scale, by just putting all your await someFunctions(); one after each other:
async function mainChain() {
await waitForColorChange('91baf1', 1400, 923)
console.log('this is run after the above finishes')
}
async function waitForColorChange(colorBeforeChange, pixelColorX, pixelColorY) {
return new Promise((resolve, reject) => {
let myInterval = setInterval(() => {
let colorOfNextBtn = robot.getPixelColor(pixelColorX, pixelColorY)
if (colorOfNextBtn == colorBeforeChange) {
console.log('waiting for color change')
} else {
console.log('color has changed')
clearInterval(myInterval);
resolve();
}
}, 1000)
})
}
//Start the main function
mainChain()