stop watch

Time

Last time I talked about when to progress the problem difficulty (I use the average answer-time over the last 20 problems). Today I’m going to talk about how I keep track of time and how I animate the timer.


The visual timer element is made from three nested, overlapping <div> elements: one to display the remaining time, one to show a visual representation of how much time is remaining, and one for the background that contains the other two.

<div id="countdownBarBack">
<div id="countdownBarFront">
<div id="countdownTimer"></div>
</div>
</div>

My original implementation used a setInterval() method that ran every 10 ms, calling the same timeDown() function each time.

function timeDown() {
  if (width <= 0) {
    clearInterval(timer);
    countdownTimer.innerHTML = "0.00";
    damagePlayer();
    newProblem();
  } else {
    width -= 0.34;
    countdownBarFront.style.width = width + "px";
    let timeLeft = (width / 34);
    countdownTimer.innerHTML = timeLeft.toFixed(2);
  }
}

let width - 340;
let timer = setInterval(timeDown, 10);

The width of my green timer bar was 340px and the length of the timer was 10 seconds. That meant — theoretically — that the function should run 1,000 times in 10 seconds, so I needed to decrease the width of the timer bar 1/1000 of its width each iteration of the function: 340 ÷ 1000 = 0.34.

The time left was derived by dividing the width of the timer bar by 34; if the width was 340, the time left would be displayed as 10. When displaying the time, I used the number.toFixed() method to get rid of all but the last two decimal places.

Once width dropped below 0, the timer interval was cleared, the player was damaged, and a new problem was generated. If the player answered a question correctly the interval was cleared and the average was taken from the countdownTimer.innerHTML.

I thought this was okay. This was the method I used in version 2 of my game. But in version 3, things had to work differently. For one thing, there isn’t always a timer bar. The player can choose to have no timer or 3 levels of timer. This meant that I had to use another method to keep track of time in those cases.

Date Object

The most popular method of tracking time in JavaScript is the Date object. Creating a new Date in JavaScript returns the number of milliseconds since January 1st, 1970. By creating a second Date object, I can subtract the two values to find the time elapsed between the two in milliseconds.

let timeStart = new Date();
let timeEnd = new Date();
let timeElapsed = timeEnd - timeStart

To make things cleaner, I decided to create a Class to keep all of my time related functions and variables together.

class Timer {
  constructor(timerValue) {
    this.width = 340;
    this.decrement = (timerValue > 2) ? 
.68:(.17 * timerValue); this.time = null; this.totalTime = null; this.timeLeft = null; this.timeIndex = timerValue; } get answerTime() { return (this.totalTime - this.timeLeft) / 1000; } timeDown() { if (this.width < 1) { this.width = 340; this.totalTime += this.decrement * (1000 / 34); damagePlayer(); } else { this.width -= this.decrement; countdownBarFront.style.width = this.width + "px"; let timeLeft = (this.width / (this.decrement * 100)); countdownTimer.innerHTML = timeLeft.toFixed(2); } } startTimer() { this.timeLeft = new Date(); }
stopTimer() { this.totalTime = new Date(); this.time += (this.totalTime - this.timeLeft); }
stopTime() { if (this.timeIndex) { clearInterval(this.time); } this.stopTimer(); } }

width remains the same as before, only now it’s fixed to the Class. The decrement is how many pixels to reduce the timer bar by for each iteration. Because there are three different timers (5, 10, and 20 seconds) the timer needs to decrement different for each one. this.time, this.totalTime, and this.timeLeft are initialized to null in anticipation of holding a value later.

The timer is started in a similar manner as before.

timer = new Timer(timerValue);
if (timerValue > 0) {
timer.time = setInterval(function() {
timer.timeDown();
}, 10);
}
timer.startTimer();

By using the Class, I only have to instantiate one variable that has all of the variables it needs attached to it already. If the timerValue is above 0, the function that animates the timer bar runs every 10 ms. Finally, a new Date is created by running the method timer.startTimer().

The Problem

This is a very simple system to set up and it’s reasonably accurate. The problem that soon appeared was a discrepancy between the time calculated by this method, and the elapsed time displayed in my timer bar. It turns out that setInterval() isn’t very accurate when calculating times less than 100 ms. The time calculated using setInterval() is almost 3 seconds slower than the time calculated using two Date objects over a 10 second period.

The solution was to separate the decrement of the timer bar from the setInterval() cycles. To achieve this I added a getter method and slightly changed the code of my timeDown() method.

this.decrement = (timerValue > 2) ? .068 : (.017 * timerValue);

timeDown
() {
if (this.width <= 0) {
this.stopTimer();
this.startTimer();
this.width = 340;
damagePlayer();
} else {
this.width = 340 - (this.checkTimer * this.decrement);
countdownBarFront.style.width = this.width + "px";
this.timeLeft = this.width / (this.decrement * 1000);
countdownTimer.innerHTML = this.timeLeft.toFixed(2);
}
}

get checkTimer() {
let checkTime = new Date();
return checkTime - this.timeStart;
}

I modified my original decrement variable to reflect how much the timer bar should decrement per millisecond instead of every 10 ms, then I could multiply this value (this.decrement) by the elapsed time (this.checkTimer) to get an exact count of how many pixels to remove from my timer bar every iteration of timeDown().


  • How do you handle keeping track of time in your projects?
  • Are there better ways of keeping track of time?

If you have any comments, criticisms, or suggestions, please let me know by leaving a comment.


The latest demo of the game is hosted on GitHub and is available here. Feel free to check out the repository as well.