I am trying to build an accurate countdown timer that shows mins and seconds left. I have tried 2 approaches. Approach 1 uses the setTimer function and calculates the drift. For that approach, some values get skipped and some values get repeated. Approach 2 yields all of the necessary values, but the values are not getting printed to the screen at even intervals (tested in repl.it). How can I make a timer that is both accurate and prints all of the values ?
Approach1:
function countTime(duration) {
var expected = 1;
var secsLeft;
var startT = new Date().getTime();
var oneSecond = 1000;
var expected = startT + oneSecond;
window.setTimeout(step, oneSecond);
function step() {
var nowT = new Date().getTime();
var drift = nowT - expected;
if (drift > oneSecond) {
console.log("drift is over 1 second long!");
}
console.log('drift is ' + drift);
var msDelta = nowT - startT;
var secsLeft = duration - Math.floor(msDelta / 1000);
console.log("secsLeft" + secsLeft);
if (secsLeft === 0) {
++count;
console.log("cleared");
} else {
expected += oneSecond;
setTimeout(step, Math.max(0, oneSecond - drift));
}
}
}
countTime(60);
Approach2:
function countTime(duration) {
var expected = 1;
var secsLeft;
var inter;
var startT = new Date().getTime();
inter = setInterval(function() {
//change in seconds
var sChange = Math.floor((new Date().getTime() - startT) / 1000);
if (sChange === expected) {
expected++;
secsLeft = duration - sChange;
console.log("seconds Left" + secsLeft);
}
if (secsLeft === 0) {
window.clearInterval(inter);
console.log("cleared");
}
}, 100);
}
countTime(60);
Consider using requestAnimationFrame.
function countTime(duration) {
requestAnimationFrame(function(starttime) {
var last = null;
function frame(delta) {
var timeleft = Math.floor(duration - (delta - starttime)/1000),
minutes = Math.floor(timeleft/60),
seconds = timeleft%60;
if( timeleft > 0) {
if( last != timeleft) {
console.log("Time left: "+minutes+"m "+seconds+"s");
last = timeleft;
}
requestAnimationFrame(frame);
}
}
frame(starttime);
});
}
countTime(60);
This will be precise to within the framerate of the browser itself :)
function(){
date = get the date
curSeconds = compute the number of seconds to be displayed
if(oldSeconds!=curSeconds) then refresh display and do oldSeconds = curSeconds;
}
Call this function quite often, the more you call it, the more accurate your timer will be. I advise you to use requestAnimationFrame() instead of setTimout() it will be called 60 times per second (period 16ms) since it is the refresh rate of most displays, it is the maximum visible "accuracy". Also it won't be called when page is not visible.
Simple, clean, no drift over long periods of time.
It also handle not being called for a while.
Related
if(localStorage.getItem("total_seconds")){
var total_seconds = localStorage.getItem("total_seconds");
}
else {
var total_seconds = 10*10;
}
var minutes = parseInt(total_seconds/60);
var seconds = parseInt(total_seconds%60);
function countDownTimer(){
if(seconds < 10){
seconds= "0"+ seconds ;
}
if(minutes < 10){
minutes= "0"+ minutes ;
}
document.getElementById("timer").innerHTML = "Result after: "+minutes+" minutes "+seconds+" seconds";
if(total_seconds <= 0){
setTimeout("document.quiz.submit()" ,1);
document.getElementById("timer").innerHTML = "";
localStorage.removeItem("total_seconds");
} else {
total_seconds = total_seconds -1 ;
minutes = parseInt(total_seconds/60);
seconds = parseInt(total_seconds%60);
localStorage.setItem("total_seconds",total_seconds)
setTimeout("countDownTimer()" ,1000);
}
}setTimeout("countDownTimer()" ,1000);
Above is the code i am using for coutdown using local storage but it doesn't count downs and resume where the user has left if site/browser is closed any tips on how to make it so the coutdown could even work if the user isn't at site or have closed his browser.
Your code only runs while the client browser is open. If your script should look & behave in the way your current code does you will need a server. However, we can make use of the built-in Date object in JavaScript in order to detect how long has passed since the creation of the timer:
const DEFAULT_TIMER_DURATION = 10; // In seconds.
// Here, we get the status of the currently-active timer:
let totalSeconds = parseInt(localStorage.getItem("total_seconds"));
let startDate = parseInt(localStorage.getItem("start_date"));
if(!(totalSeconds && startDate)) {
// If there isn't an active timer, set one up:
localStorage.setItem("total_seconds", DEFAULT_TIMER_DURATION);
localStorage.setItem("start_date", new Date().getTime());
totalSeconds = DEFAULT_TIMER_DURATION;
startDate = new Date().getTime();
console.log("Reset timer:", DEFAULT_TIMER_DURATION);
}
// This function updates the timer and displays the countdown:
function displayElapsedSecond(timerData) {
if(timerData.secondsLeft <= 0) {
console.log("Time is up!");
localStorage.removeItem("total_seconds");
localStorage.removeItem("start_date");
return;
}
if(timerData.initialDelay) {
setTimeout(() => {
displayElapsedSecond({secondsLeft: timerData.secondsLeft});
}, initialDelay);
return;
}
timerData.secondsLeft--;
console.log("Seconds left:", timerData.secondsLeft);
setTimeout(() => {
displayElapsedSecond({secondsLeft: timerData.secondsLeft});
}, 1000);
}
// this block of code calculates how long has passed and continues counting down from wherever the user left of:
const now = new Date().getTime();
const secondsPassed = (now - startDate) / 1000;
let secondsLeft = totalSeconds - secondsPassed;
const initialDelay = Math.ceil(secondsLeft) - secondsLeft;
secondsLeft = Math.ceil(secondsLeft);
displayElapsedSecond({initialDelay, secondsLeft});
// Obviously some improvements can be made for time complexity, but the code nicely illustrates the way this timer works
After some research, I understand the most accurate way to create a timer in Javascript is to compare the elapsed time from a starting point using date.now() -as this answer suggests.
var startTime = Date.now();
var interval = setInterval(function() {
var elapsedTime = Date.now() - startTime;
var time = (elapsedTime / 1000).toFixed(3);
console.log(time);
}, 10);
This provides the accuracy I am looking for, but I am looking to make this timer reset itself after a certain value (let's say var total = 12000). This value is determined by the length of a video playlist (I am building a 'live' playhead for a video program) and is a variable.
I'm stuck how to go about this, as the counter is counting elapsed time from a certain point in time, it doesn't quite fit the logic of what I am looking for. Any suggestions would be appreciated.
You could use a .setTimeout() at the end of your code to restart your .setInterval(), after the desired time has passed
function runInterval (loopAfter) {
var startTime = Date.now();
var interval = setInterval(function() {
var elapsedTime = Date.now() - startTime;
var time = (elapsedTime / 1000).toFixed(3);
console.log(time);
}, 10);
setTimeout(() => {
clearInterval(interval);
runInterval(loopAfter);
}, loopAfter);
}
runInterval(5000);
Try this:
var startTime = Date.now();
let total = 0;
var interval = setInterval(function() {
if (total === 12000) {
total = 0;
startTime = Date.now();
} else {
total++;
var elapsedTime = Date.now() - startTime;
var time = (elapsedTime / 1000).toFixed(3);
}
console.log(time);
}, 10);
I am working on a stopwatch and for now it works, but I want that I can calculate the average of time people get. E.g. let's say I have 5 times in an array which looks like the following: scores = ["00:32:192", "00:30:126", "00:31:542", "00:25:236", "00:36:320"];. You may think now: What the hell is it? Well the times are in: minutes:seconds:milliseconds. The array is printed to the screen by using a for loop.
jQuery
var int,
ms=0,
s=0,
m=0;
$('#swatch').text("00:00:00");
function swatch(){
var startTime = new Date().getTime();
int = setInterval(function(){
var time = new Date().getTime();
var dif = time-startTime;
ms= dif%1000;
s = Math.floor(dif/1000)%60;
m = Math.floor(dif/1000/60)%60;
if(ms < 10) {
ms = '0'+ms;
}
if(s < 10) {
s = '0'+s;
}
if(m < 10) {
m = '0'+m;
}
$('#swatch').text(m+':'+s+':'+ ms);
},1);
}
var scores= [];
$(document).on('keydown', function(e){
var result = $('#swatch').text();
var i = parseInt(scores.length);
if(e.keyCode == 32 && !int){
swatch();
} else if (e.keyCode == 32){
clearInterval(int);
int=0;
scores.push(result);
$('#score ol').append('<li>' + scores[i] + '</li>');
if(scores.length >= 5) {
$('#ao5').html("ao5: 00:27:43");
$('#ao5').slideDown(500);
}
if (scores.length >= 12) {
$('#ao12').html("ao12: 00:27:43");
$('#ao12').slideDown(500);
}
}
});
In my code above this, you see this:
if(scores.length >= 5) {
$('#ao5').html("ao5: 00:27:43");
$('#ao5').slideDown(500);
}
if (scores.length >= 12) {
$('#ao12').html("ao12: 00:27:43");
$('#ao12').slideDown(500);
}
I want if the array has 5 different time values (as in the example above where I showed you the array format) it outputs the average on the screen. As you see I just filled it in for myself to picture it, but I want a function that calculates it. I am building this in jQuery because the timer worked better in here than in JS.
If some of you guys could give me an example and re-write my code with the function in it, that'd be great. I am really struggling with this for days to figure out how I can calculate an average of 5 and/or 12.
Thank you.
Note that the code I provide below doesn't rely on JQuery or any library directly. You feed it an array of 'time-strings', and it gives you back an average. You can use whatever library you choose to get that array of strings.
First, you need a utility function which breaks a time-string into it's component pieces:
var str_to_time = function(time_str) {
var pieces =time_str.split(':');
return {
minutes: parseInt(pieces[0], 10),
seconds: parseInt(pieces[1], 10),
milliseconds: parseInt(pieces[2], 10)
};
};
Now a function to convert an array of time-strings to an array of times:
var str_array_to_time_array = function(str_array) {
return str_array.map(str_to_time);
};
Lastly, a way to average all these values together:
var average_time = function(time_array) {
var minutes = 0;
var seconds = 0;
var milliseconds = 0;
for (var i = 0; i < time_array.length; i++) {
minutes += time_array[i].minutes;
seconds += time_array[i].seconds;
milliseconds += time_array[i].milliseconds;
}
minutes /= time_array.length;
seconds /= time_array.length;
milliseconds /= time_array.length;
// Minutes and seconds may be fractional. Carry the fractions down.
seconds += (minutes - Math.floor(minutes)) * 60;
minutes = Math.floor(minutes);
milliseconds += (seconds - Math.floor(seconds)) * 1000;
seconds = Math.floor(seconds);
milliseconds = Math.round(milliseconds);
// if milliseconds is >= 1000, add a second.
seconds += Math.floor(milliseconds / 1000);
milliseconds %= 1000;
// If seconds >= 60, add a minute.
minutes += Math.floor(seconds / 60);
seconds %= 60;
return {
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds
};
};
Now you can call something like the following to get an average:
average_time(str_array_to_time_array(['33:23:354', '34:00:32']))
// Object {minutes: 33, seconds: 41, milliseconds: 693}
I'm trying to create a stopwatch with miliseconds which is as accurate as possible.
Of course the browser/server/cpu or whatever will need more than 1 ms to execute the function and display the digit on the watch. So I thought i want to reset the ms to 0 each time a second goes up.
jQuery code looks like this.
(function($) {
$.fn.stopwatch = function() {
// The Element where the HTML code is added
var clock = $(this);
var timestamprunningms;
var timestampstartms;
var milliseconds = 0;
// Predefinition of the timestamprunningseconds variable
var timestamprunningseconds;
var display = clock.find("#display");
var time = clock.find("#time");
// Predefinition of the seconds variable
// Value 0 because it is used to define
// The timestampstartseconds variable in the click-event
var seconds = 0;
// Predefinition for the timestampstartseconds variable
var timestampstartseconds;
// Predefinition for the timer variable
var timer;
// Time Variables
var h = clock.find("#h");
var m = clock.find("#m");
var s = clock.find("#s");
var ms = clock.find("#ms");
// Button Variables
var resetlog = clock.find("#resetlog")
var showms = clock.find("#showms")
var hidems = clock.find("#hidems")
var start = clock.find("#start");
var pause = clock.find("#pause");
var reset = clock.find("#reset");
var log = clock.find("#log");
ms.hide();
resetlog.click(function (){
time.html("");
});
// Hides the pause and hidems Button
pause.hide();
hidems.hide();
// Triggered by clicking the start button
start.click(function() {
// Hides the start and shows the pause button
start.hide(),
pause.show(),
// Defines the value of timestampstartseconds or saves it
// if there is a value in seconds
timestampstartseconds = Math.round(new Date().getTime() / 1000) - seconds;
timestampstartms = new Date().getTime() - milliseconds;
timer = setInterval(do_time, 20);
});
// Triggered by clicking the pause button
pause.click(function() {
// Resets the interval in which the do_time function occurs
clearInterval(timer),
// Hides the pause and shows the start button
pause.hide(),
start.show(),
timer = 0;
});
// Triggered by clicking the reset button
reset.click(function() {
// Resets the interval in which the do_time function occurs
clearInterval(timer),
// Resets the value of the display
h.html("00"),
m.html("00"),
s.html("00"),
ms.html("000")
// Hides the pause and shows the start button
pause.hide(),
start.show();
seconds = 0;
});
log.click(function() {
time.append("<li>" + display.text() + "</li>");
});
// The function for calculating the seconds/minutes/hours
showms.click(function() {
showms.hide();
hidems.show();
ms.show();
});
hidems.click(function() {
hidems.hide();
showms.show();
ms.hide();
});
function do_time() {
// Timestamp that refreshes everytime the function is executed
timestamprunningseconds = Math.round(new Date().getTime() / 1000);
timestamprunningms = new Date().getTime();
// The actual seconds that are going to be displayed
milliseconds = timestamprunningms - timestampstartms;
seconds = timestamprunningseconds - timestampstartseconds;
// Value of the display
var hour = parseFloat(h.text());
var minute = parseFloat(m.text());
if (milliseconds > 999) {
milliseconds = 0;
timestampstartms = new Date().getTime();
}
// Reset seconds display and add a minute every time 60 seconds pass
if (seconds > 59) {
seconds = 0;
timestampstartseconds = Math.round(new Date().getTime() / 1000);
minute++;
}
// Reset minute display and add an hour every time 60 minutes pass
if (minute > 59) {
minute = 0;
hour++;
}
// Display value
h.html("0".substring(hour >= 10) + hour);
m.html("0".substring(minute >= 10) + minute);
s.html("0".substring(seconds >= 10) + seconds.toString());
ms.html(":" + "0".substring(milliseconds >= 100) +"0".substring(milliseconds >= 10) + milliseconds.toString());
};
};
})(jQuery);
As I already said, my goal is to reset the millisecond timer every time a second goes up. (the seconds are accurate, the milliseconds aren't).
would that be something like this?:
while (seconds++) {
milliseconds = 0;
timestampstartms = new Date().getTime();
}
I'm really new to javascript/jQuery and programming in general so it would be very nice if you could help me with this problem and maybe give a little feedback so I can improve.
Ok I found a solution: i just added a variable which is like a timestamp of the current second. It has a default value of 0 and goes 1 up if the seconds that is used in the display is greater than the second timestamp.
looks a bit like this: var secondnow = 0; (on top of the jQuery function)
and this is how it's used
if (seconds > secondnow) {
milliseconds = 0;
secondnow++;
timestampstartms = new Date().getTime();
console.log(secondnow);
}
(in the function do_time)
I have a countdown like this one:
var countdown = {
startInterval: function() {
var count = 600
var countorig = 600;
var currentId = setInterval(function() {
var min = (count - (count % 60)) / 60;
var sec = count % 60;
if (sec < 10) {
$('#timer').html(min + ':0' + sec);
} else {
$('#timer').html(min + ':' + sec);
}
$('#time').val(countorig - count);
if (count == 0) {
$('#form').submit();
}--count;
}, 1000);
countdown.intervalId = currentId;
}
};
It works. But if I load the page, the countdown starts but it stutter it is not "round" like a clock is.
JSFiddle.
setInterval isn’t exact. You should use Dates instead, to get an accurate time, and then choose an interval of less than one second to get a smoother clock. Here’s a demo!
var countdown = {
startInterval: function() {
var count = 600;
var start = new Date(); // The current date!
var currentId = setInterval(function() {
var difference = Math.max(0, count - (new Date() - start) / 1000 | 0);
var min = difference / 60 | 0;
var sec = difference % 60;
$('#timer').text(min + ':' + (sec < 10 ? '0' : '') + sec);
$('#time').val(difference);
if(count === 0) {
$('#form').submit();
}
}, 200);
countdown.intervalId = currentId;
}
};
It's never a good idea to assume your timers are exact. Instead, use delta timing.
var startTime = new Date().getTime();
setInterval(function() {
var elapsed = new Date().getTime()-startTime;
console.log("Been running for "+Math.floor(elapsed/1000)+" seconds");
},25);
That is because setInterval is not meant to be a high resolution timer. It will NOT hit every 1000 milliseconds on the dot. You might have swings as much as 20 to 30 milliseconds in either direction, resulting in a clock that is off.
Using Date.now(), this is a quick example of a countdown function ( x is milliseconds )
function countdown(x){
var o = {future: Date.now()+x, last:-1, i:null}; // object so we can pass by-ref if req.
o.i = setInterval( function() { // use o.i so we can clear interval
var remain = o.future - Date.now(),
secs = Math.floor( remain / 1000 ),
mins = 0;
if( remain < 0 ){ // finished, do whatever
return clearInterval(o.i); // then clear & exit
}
if( secs === o.last ) return; // skip this iteration if it's not been a second
o.last = secs; // else update last time
// do whatever you want for this new second
if( secs > 59 ) mins = Math.floor( secs / 60 ), secs = secs % 60;
console.log(
(mins < 10 ? '0'+mins : mins) + ':' +
(secs < 10 ? '0'+secs : secs) + ' remain.'
);
}, 100);
}
If you know it wont be used in IE, consider adding o as an argument to the callback function in the interval and also as the last argument to setInterval (so it is passed to the callback as first arg), which means closure is independent => callback can be defined anywhere.