I have implemented a countup which animates up to the data-target. How can I adjust the counter so that all counters stop at the same time?
Or is there a better way to implement this?
function animationEffect(){
const counters = document.querySelectorAll('.counter');
const speed = 2000;
counters.forEach((counter) => {
const updateCount = () => {
const target = + counter.getAttribute('data-target');
const count = + counter.innerText;
const inc = target / speed;
if(count < target) {
counter.innerText = Math.ceil(count + inc);
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
}
updateCount();
});
}
<div class="counter" data-target="299" onmouseover="animationEffect()">0</div>
<div class="counter" data-target="1299" onmouseover="animationEffect()">0</div>
<div class="counter" data-target="99" onmouseover="animationEffect()">0</div>
Updated Answer
Looks like I misunderstood what you mean by at the same time, but using setTimeout like this is still a really bad practice. Here is my take on it:
const counters = Array.from(document.querySelectorAll(".counter"));
const counterValues = [];
const speed = 500;
const updateCount = (counter, target, count, index) => {
const inc = target / speed;
counterValues[index] = count + inc;
if (count < target) {
counter.innerText = Math.floor(counterValues[index]);
} else {
counter.innerText = target;
}
};
counters.forEach((counter, index) => {
counterValues.push(0)
const interval = setInterval(() => {
const target = +counter.getAttribute("data-target");
const count = counterValues[index];
if (target !== count) {
updateCount(counter, target, count, index)
} else {
clearInterval(interval);
}
}, 1)
});
<div class="counter" data-target="32">0</div>
<div class="counter" data-target="3000">0</div>
<div class="counter" data-target="10">0</div>
Please check my old answer for why setInterval is better for this problem.
Other than that here is what is going on in this snippet:
I defined an array called counterValues which will hold the count values in a float format. In your example when you store the ceiled number to be used in your calculation later again, you are not doing a correct calculation.
If I remember correct one of your counters must be increased by 0.145, while you are incrementing it by 1 every time. By the way flooring is the right method for this as it won't reach to the target until it really reaches to it. If the target is 10, but your counter is at 9.5 it will be written as 10 in your code although it is not there yet.
updateCount is almost the same function. It now uses floor. It updates the amount of counter by using the previous floating value and then while writing to the DOM, it uses the floored value.
For each counter, it adds an interval which will update the counter and cancel itself whenever the counter reaches to the target value.
I used a shared state and indexed calculation for simplicity.
Old Answer
If you paste this code to the top of your code and run, when you log window.activeTimers you will see that there are hundreds of timers defined. It is because every time updateCount is called you are setting a new timer to updateCount. Although your animationEffect function is like the main function of your program, if you invoke it every time I mouseover your counters, it will set new timers which means faster update on your counters every time. To sum up, you don't have control at all at the moment.
For periodic calls you should use setInterval. It takes a function and a delay parameter (there are also optional args. You can check the docs). It repeatedly calls a function or executes a code snippet, with a fixed time delay between each call (From Mozilla docs). It also returns an interval ID so that you can cancel it later (meaning we can control it).
So in your case, just to stop in a desired moment first thing you should do is to get rid of onmouseover calls to the animationEffect and add a button to stop execution of updateCounters.
<div class="counter" data-target="299">0</div>
<div class="counter" data-target="1299">0</div>
<div class="counter" data-target="99">0</div>
<button id="stop">Stop</button>
After assigning variables counters and speed we can define an array to hold the intervals' IDs to cancel them later.
const counters = document.querySelectorAll(".counter");
const speed = 2000;
const cancelButton = document.getElementById('stop')
const countIntervals = [];
cancelButton.addEventListener('click', () => {
countIntervals.forEach(interval => clearInterval(interval))
})
As you can see I defined an event listener to our button. When you click on it it will iterate the interval IDs in countIntervals and clear those intervals. For simplicity, I didn't implement features like pausing and resetting and tried not to change your code much. You can experiment later.
Now first thing you should do is to comment or delete setTimeout line in your if statement. Then we will push the returned interval ID to our array countIntervals:
counters.forEach((counter) => {
const updateCount = () => {
const target = +counter.getAttribute("data-target");
const count = +counter.innerText;
const inc = target / speed;
if (count < target) {
counter.innerText = Math.ceil(count + inc);
// setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
};
countIntervals.push(setInterval(updateCount, 10))
});
Now your counters will stop once you hit the Stop button. I ignored the speed feature, but If you understand setInterval you can implement it easily.
I don't know if it helps, but i got it working when i change this line:
const inc = target / speed;
to this:
const inc = 1/(speed / target);
AND get rid of the Math.ceil() because this const speed = 2000; which is actually a step creates floating values. Maybe try smaller speed value
EDIT
A little more tweaking and we have a smooth answer without floating values >:D
function animationEffect(){
if(animationEffect.called){return null}
animationEffect.called=true
//these 2 lines prevent this function from being called multiple times to prevent overlapping
//the code works without these lines though so remove them if u don't want them
const counters = document.querySelectorAll('.counter');
const incrementConstant = 2; //the higher the number the faster the count rate, the lower the number the slower the count rate
const speed = 2000;
counters.forEach((counter) => {
const updateCount = () => {
const target = + counter.getAttribute('data-target');
counter.storedValue=counter.storedValue||counter.innerText-0; //saving a custom value in the element(the floating value)
const count = + counter.storedValue; //accessing custom value(and rounding it)
const inc = incrementConstant/(speed / target); //the math thanks to #Tidus
if(count < target) {
counter.storedValue=count+inc
counter.innerText = Math.round(counter.storedValue);
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
}
updateCount();
});
}
<div class="counter" data-target="299" onmouseover="animationEffect()">0</div>
<div class="counter" data-target="1299" onmouseover="animationEffect()">0</div>
<div class="counter" data-target="99" onmouseover="animationEffect()">0</div>
Related
I am trying to create a div where the number starts from 1 and automatically increments by one - like a stopwatch. But when I run the code, it crashes or does not work.
<html>
<body>
<div></div>
<script>
var div = document.querySelector('div');
var i = 1;
while(i>0){
div.innerText=i;
i++;
}
</script>
</body>
</html>
You never let the JavaScript engine rest. As soon as it has incremented the variable, the loop starts over, and it never exits. This blocks everything else.
You need to pause between loops to let the browser do other things.
You can use the requestAnimationFrame function to do this.
const div = document.querySelector('div');
let i = 1;
const incrementCounter = () => {
div.innerText = i;
i++;
requestAnimationFrame(incrementCounter);
}
incrementCounter();
<div></div>
Since you mentioned the term "stopwatch" maybe you prefer something like this:
This increments i by 1 every second using setInterval()
The first parameter is our function, that gets executed on that interval.
The second parameter 1000 is the interval. In this case 1000 ms.
You can adjust it as needed.
You can stop the timer with:
clearInterval(incrementCounter);
const div = document.querySelector('div');
let i = 1;
const incrementCounter = setInterval(() => {
div.innerText = i;
i++;
},1000);
<div></div>
You can find more information here:
https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval
This question already has answers here:
javascript interval
(3 answers)
Closed 4 years ago.
$(function() {
let testOne = 'test one.';
let testTwo = 'test two';
let messageBox = $('messagebox');
let a = ['test:', testOne,'test2:', testTwo];
let i = 1
setInterval(cool, 1000)
function cool() {
messageBox.text(a[1])
}
});
Hi there,
I am new to JS. I am looking to have testOne and testTwo (going to add a few more) display in timers across my screen. I was given help to get this far.
I am trying to, for example, have a word and its English definition appear on the screen in a time loop, rotating the words in a loop. (kind of like a live screen-saver)
What am I missing?
Thank you for your time, help, and effort.
You've got a good start.
As others have mentioned, unless you're using a custom HTML element (i.e. <messagebox>), use # at the beginning of your selector to indicate that "messagebox" is an ID. See jQuery's ID Selector.
$('#messagebox')
Alternatively, use a class and the class selector.
$('.messagebox')
The index of the array element to display is currently hard-coded to 1. We want to increment it upon each iteration so the text will change. But we only want to count up to the number of array elements and then go back to the first one and start over.
Below, I'm using JavaScript's increment and remainder operators to increment i while limiting it to the number of elements in a. Note that the "postfix" method "returns the value before incrementing", so i starts at zero.
a[i++ % a.length]
Working example:
$(function() {
let $messageBox = $('#messagebox');
let testOne = 'test one.';
let testTwo = 'test two.';
let a = ['test:', testOne, 'test2:', testTwo];
let i = 0;
function cool() {
$messageBox.text(a[i++ % a.length])
}
setInterval(cool, 1000);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="messagebox"></div>
EDIT
I don't like letting i count up indefinitely. The math might get wonky after about 9 quadrillion loop iterations, which is how high JavaScript can safely count.
Safe in this context refers to the ability to represent integers exactly and to correctly compare them. For example, Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 will evaluate to true, which is mathematically incorrect. -- developer.mozilla.org
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MAX_SAFE_INTEGER + 1);
console.log(Number.MAX_SAFE_INTEGER + 2);
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);
So, here's what happens after about three million centuries:
$(function() {
let $messageBox = $('#messagebox');
let testOne = 'test one.';
let testTwo = 'test two.';
let a = ['test:', testOne, 'test2:', testTwo];
let i = 9007199254740990;
function cool() {
console.log(i);
$messageBox.text(a[i++ % a.length])
}
setInterval(cool, 1000);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="messagebox"></div>
That's not good enough.
We need this thing running well past the end of time.
Let's keep it safe:
$(function() {
let $messageBox = $('#messagebox');
let testOne = 'test one.';
let testTwo = 'test two.';
let a = ['test:', testOne, 'test2:', testTwo];
let i = 0;
function cycleText() {
console.log(i);
$messageBox.text(a[i]);
i = ++i % a.length;
setTimeout(cycleText, 1000);
}
cycleText();
});
body {
font-size: 2em;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="messagebox"></div>
You can easily swap out the messages in your array and update an html element using your code. Instead of passing a hardcoded index in, just increment a number until it reaches the length of the array (n < array.length) and reset it to 0.
I personally would recommend making your messagebox element a div or something out of the box just for readability sake (so nobody comes in and gets confused where messagebox is coming from). However, if you have a specific use case for a custom html element, make sure you're doing it correctly.
https://jsfiddle.net/mswilson4040/oxbn8t14/2/
<messagebox>Initial Value...</messagebox> // custom HTML element called messagebox
$(function() {
let testOne = 'test one.';
let testTwo = 'test two';
let interval = -1;
let messageBox = $('messagebox');
let a = ['test:', testOne,'test2:', testTwo];
// let i = 1 <-- this isn't doing anything
setInterval(cool, 1000)
function cool() {
interval = interval < a.length ? interval += 1 : 0;
messageBox.text(a[interval])
}
});
I'm trying to have the numbers 1-6 quickly flash on the screen when a button is clicked, then stop and display the random generated number. If I put clearInterval into the function it just displays the random Number and doesn't display the flashes up numbers before hand.
HTML
<div id='dice'>
<div id='number'>0</div>
</div>
<div id='button'>
<button onclick='roll()'>Roll Dice</button>
</div>
JAVASCRIPT
let rollButton = document.querySelector('button');
let diceNumber = document.getElementById ('number');
function roll(){
diceSides = [1,2,3,4,5,6];
var i = 0;
let shuffleDice = setTimeout(function(){
diceNumber.innerHTML = diceSides[i++];
if(diceNumber == diceSides.length){
i = 0;
}
}, 500);
let random = Math.floor(Math.random() * 6);
diceNumber.innerHTML = random;
}
Maybe this works for you
hint: you can use i as a counter variable OR use your approach (define an array with numbers and use index to find them and put them in number tag)
HTML:
<div id='dice'>
<div id='number'>0</div>
</div>
<div id='button'>
<button onclick='roll()'>Roll Dice</button>
</div>
JS:
let rollButton = document.querySelector('button');
let diceNumber = document.getElementById ('number');
function roll(){
diceSides = [1,2,3,4,5,6];
var i = 6;
let shuffleDice = setInterval(function(){
diceNumber.innerHTML = i;
if(i == 0){
clearInterval(shuffleDice);
let random = Math.floor(Math.random() * 6);
diceNumber.innerHTML = random;
}
i--;
}, 1000);
}
CodePen
clearInterval
setInterval
setTimeout
SetTimeout only executes once. Also your variable is changed by the interval!
After its over, you have to clear the interval with clearInterval.
let rollButton = document.querySelector('button');
let diceNumber = document.getElementById ('number');
function roll(){
diceSides = [1,2,3,4,5,6];
var i = 0;
var shuffleDice = setInterval(function(){
diceNumber.innerHTML = diceSides[i++];
//use i
if(i == diceSides.length){
i = 0;
//clear
clearInterval(shuffleDice);
// moved because the interval will change it
let random = Math.floor(Math.random() * 6);
diceNumber.innerHTML = String(random);
}
}, 500);
}
<div id = 'dice'>
<div id = 'number'>0</div>
</div>
<div id = 'button'>
<button onclick = 'roll()'>Roll Dice</button>
</div>
I suggest something like this:
var diceNumber = document.getElementById ('number');
const diceSides = [1,2,3,4,5,6];
function roll(){
let arr = [...diceSides,diceSides[Math.floor(Math.random() * diceSides.length)]];
cycle(diceNumber,arr,200);
}
function cycle(element,arr,delay) {
element.innerText=arr.shift();
if (arr.length > 0)
setTimeout(cycle,delay,element,arr,delay);
}
Given that you know precisely the list you want to iterate through, and thus the fixed number of iterations you want to execute, this seems cleaner and more concise than setInterval, which you would have to explicitly stop when you reach the end of the list. I like setInterval for things that will run an indeterminate period of time (for instance, until stopped by a user action).
What this says is: "Take this list of numbers (the last one being the random one). Show the first one and remove it from the list. Then wait a while and do it again, until you're out of numbers."
A couple other fine points here:
innerText rather than innerHTML, since you're just setting string content.
You want to use your random number as a key against your array of die faces, not directly - used directly, you get [0-5], not [1-6].
Use the length of your array, rather than hard-coding '6' - always avoid magic numbers when you can. By referring everything back to your constant array, changing the values on the faces of the die (or the number of them) becomes trivial; just change the array and everything else will still work.
Normally, there are stack-size concerns with recursive code. In this case, that wouldn't be a problem because of the small size of the array. But beyond that, the fact that the recursion is going through setTimeout means that each one is a seprate entry on the queue, and the prior isn't waiting for the next to complete before it can exit.
This question already has answers here:
Is there a better way of writing v = (v == 0 ? 1 : 0); [closed]
(31 answers)
Closed 5 years ago.
I want a variable's value regularly changing between 0 and 1. If I have a variable whose value is 0 (counter = 0), how can I increase it by 1 (counter = 1) after a few seconds, then decrease it back to 0 ( counter = 0) after another few seconds? An endless loop basically is what I want.
I'm assuming this will require setTimeout or setInterval, but I've absolutely no idea how I'd go about this. I'm very unfamiliar with the syntax; I'm very much a newbie. Does anyone have any pointers?
Thanks!
You can create an endless, timed loop by having a function that calls itself at the end via setTimeout. See below.
var count = 0;
function flip() {
count = Number(!count);
console.log(count);
setTimeout(flip, 1000);
}
flip();
A more generic approach:
// possible addition: allow user to stop the timer
const rotate = (time, vals) => {
// TODO: handle incorrect vals (non-empty array) or time (positive number)
let idx = 0;
setInterval(() => idx = (idx + 1) % vals.length, time);
return {
get val() {return vals[idx];}
}
}
const toggle = rotate(1000, [0, 1])
toggle.val //=> depends on when you call it, changes every 1000 ms
// but always either 0 or 1.
One advantage of this is that it doesn't keep the value in global scope where someone else can mess with it, but encapsulates it in a closure.
It's more generic because you can easily change the time between updates and you can choose whatever you want for values (for example, rotate(5000, ['foo', 'bar', 'baz').)
var counter = 0;
var changeCounter = function () {
counter = counter === 0 ? 1 : 0;
console.log('counter', counter);
setTimeout(changeCounter, 1000);
}
changeCounter();
This sounds like homework but try this:
var value = 0;
setInterval(
function() {
value = value===0 ? 1 : 0;
console.log('value =', value);
},
1000
);
setInterval will call the function over and over again without needing to call setTimeout over and over again.
setInterval is what you want, as documented in W3C, you should pass a function and a time interval in milliseconds on how often to execute the code.
var counter = 0;
setInterval(function(){
counter = 1 - counter;
//Do what you want with the result...
//alert(counter);
}, 1000);
https://codepen.io/paulodiogo/pen/xPPOKa?editors=1010
Not using global variable...
https://codepen.io/paulodiogo/pen/KyyMXZ
I am trying to mix the initial string and randomly compare the string's elements with the right elements on the right indexes, and if true push them into a set, to reconstruct the initial string. Doing this I met the problem that while loop does nothing just crushng the browser. Help me out with this.
function checker() {
var text = document.getElementById("inp").value;
var a = [];
var i = 0;
while (a.length < text.length) {
var int = setInterval((function() {
var rnd = Math.floor(Math.random() * text.length);
if (text[rnd] === text[i]) {
a.push(text[rnd]);
clearInterval(int);
i++;
}
}), 100)
}
}
P.S. I need the setInterval() function because I need the process to happen in exactly the same periods of time.
So, 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".
In my answer its doing exactly what you want - creating exactly the same string by randomly mixing the initial, and also using setInterval. You didn't write where you want the result, so you have it written in the console and also in another input field with id output_string.
HTML:
<input id="input_string" value="some_text" />
<input id="output_string" value="" readonly="readonly" />
JavaScript:
function checker() {
var text = document.getElementById("input_string").value;
var result = '';
// split your input string to array
text = text.split('');
var int = setInterval((function() {
var rnd = Math.floor(Math.random() * text.length);
// add random character from input string (array) to the result
result += text[rnd];
// remove used element from the input array
text.splice(rnd, 1);
// if all characters were used
if (text.length === 0) {
clearInterval(int);
console.log(result);
document.getElementById("output_string").value = result;
}
}), 100);
}
checker();
DEMO
Honestly, I have no idea what you are trying to do here, but you seem to have lost track of how your code is operating exactly.
All your while loop does, is creating the interval, which is ran asynchronous from the loop itself.
In other words, the only way your while condition equates to false, is after multiple 100ms intervals have elapsed. 100 miliseconds is an eternity when comparing it to the speed of 1 loop iteration. We're looking at 1000s of iterations before your first setInterval even triggers, not something a browser can keep up with, let alone wait several of these intervals before you change a.length.
Try more like this:
function checker() {
var text = document.getElementById("inp").value;
var a = [];
var i = 0;
// start to do a check every 100ms.
var interv = setInterval(function() {
var rnd = Math.floor(Math.random() * text.length);
if (text[rnd] === text[i]) {
a.push(text[rnd]);
i++;
}
// at the end of each call, see if a is long enough yet
if(a.length > text.length){
clearInterval(interv); // if so, stop this interval from running
alert(a); // and do whatever you need to in the UI.
}
}, 100);
}
}