-2

So I created a stopwatch in JavaScript and HTML for a project, it's just a generic stopwatch, and for the start_timer() function I used the setTimeout() function to determine the speed. But when I run the files in my browser they appear to be counting too slowly as the seconds are out of sync. Video here to illustrate how slow the clock is: https://drive.google.com/file/d/1mqB4w5ZX7YqW8C57HhgWtIC63eyfKaGe/view?usp=sharing

I feel like this is a really dumb question but help much appreciated ;) I was wondering if I need to use a web worker or something (this is my first full-scale javascript project so I'm new to some things) Code below

var isTiming = false; //as mentioned in the Design, this is to determine whether the stopwatch IS counting or IS NOT
var speed = 10;  //10 milliseconds count allowing for hundredths of a millisecond for better accuracy
var binaryTrue = 0; //checks to see if the stopwatch is paused
function start_stopwatch() { //function to start the counting
  if (isTiming == true) {
    var stopwatch = document.getElementById("TimeTable").innerHTML; //set the stopwatch to the digital clock in the HTML


    //use of an array to display elapsed time in the format hh:mm:ss:lll
    //each chronological place value is at a different spot in the array from left to right (numeric values)
    var arr = stopwatch.split(":");
    /*splits the array by separating each value (seconds, minutes, etc.) everytime there is a ":"
                                        when a button is pressed */
    var hr = arr[0]; //first position :hr: at the first space of the array
    var min = arr[1]; //seccond position :mm at the second space of the array
    var sec = arr[2]; //third position :ss at the third space of the array
    var millisec = arr[3]; //fourth position :lll at the fourth space of the array


    //Counting up in normal digital clock format, executes every 10 milliseconds which allows for hundredths of a second
    //returns next value after 10 milliseconds are up
    millisec++;
    if (millisec < 10) {
      millisec = "0" + millisec;
    }
    if (millisec / 100 === 1) {
      millisec = 0;
      millisec = "0" + millisec;
      if (sec / 59 === 1) {
        sec = 0;
        sec = "0" + sec; //added in after the place value displayed as "0" and not "00"
        if (min / 59 === 1) {
          hr++;
          min = 0;
          min = "0" + min; //added in after the place value displayed as "0" and not "00"
          sec = 0;
          sec = "0" + sec; //added in after the place value displayed as "0" and not "00"
        } else {
          min++;
          if (min < 10) {
            min = "0" + min;
          }
        }
      } else {
        sec++;
        if (sec < 10) {
          if (sec < 1) {
            sec = "0" + "0" + sec;
          } //added in after it was displayed as "0" not "00"//
          else {
            sec = "0" + sec;
          }
        } else {
          if (sec < 1) {
            sec = "0" + sec;
          }
        }
      }
    }


    //update the HTML digital time
    document.getElementById("TimeTable").innerHTML = hr + ":" + min + ":" + sec + ":" + millisec; //modify the array to the updated time
    setTimeout(start_stopwatch, (speed /* * ((6/7)/1.072)*/ )); //THIS TENDS TO BE OUT OF SYNC AS IT IS SLOWER THAN A NORMAL CLOCK. SO * ((6/7)/1.072) TO IMPROVE SPEED.
    document.getElementById("control").innerHTML = "Pause";
  }
}


//function to allow the user to change the speed of counting as desired, and as many times as they like
//
function change_speed() {
  speedInput = window.prompt("Enter speed, where '1' = 1 second or '2' counts twice as fast, or '0.5' counts slower.");

  //Input validation/verification as the user should only input an actual number, whether a float or an integer.
  //isNaN is a useful function here where NaN means "Not a Number"
  if (isNaN(speedInput)) { //if the value that the user puts in to change the speed into isn't actually (only) a number
    speedInput = window.prompt("Enter speed AS A NUMBER where '1' = 1 second or '2' counts twice as fast, or '0.5' counts slower. Numbers only.");
  }

  speedLength = speedInput.length; //defensive design in the form of input validation - minimum length
  speedInputHolder = speedInput;
  speedInput = 1 / speedInputHolder; //converts speed requested by the USER into counting length (e.g. 2x speed = 0.5 second length, which is 2x faster, so accessible user interface)

  if (speedInput === null) { // "Cancel" button pressed
    speed = speed;
  } else {
    if (speedLength > 0) {
      speedInputInMilliseconds = (speedInput * 10); //not * 1000 as tenth-seconds are included too
      speed = (speedInputInMilliseconds /** ((60/7)/1.072)*/ ); //need to make sure the 4.275 method still syncs*/
    }
  }
  if (speedInputHolder == 1) {
    var indicateSpeed = document.getElementById("speedIndicator").innerHTML = "Counting speed:  " + "(" + (1 / speedInput) + "x speed" + ")";
    var indicateInterval = document.getElementById("FractionalIndicator").innerHTML = "Counting frequency: " + speedInput + " second."
  } else {
    var indicateSpeed = document.getElementById("speedIndicator").innerHTML = "Counting speed:  " + "(" + (1 / speedInput) + "x speed" + ")";
    var indicateInterval = document.getElementById("FractionalIndicator").innerHTML = "Counting frequency: " + speedInput + " seconds."
  }
}

function change_state() { //function to change from start to pause or pause to start
  if (isTiming == false) {
    isTiming = true;
    binaryTrue = 1;
    start_stopwatch();
    var started = document.getElementById("control").innerHTML = "Pause";
    console.log("Timer started.");
  } else {
    stop_stopwatch();
    console.log("Timer stopped.");
  }
}

function reset() {
  if (binaryTrue == 1) {
    var reset = document.getElementById("TimeTable").innerHTML = "00" + ":" + "00" + ":" + "00" + ":" + "00";
    if (isTiming == false) {
      var started = document.getElementById("control").innerHTML = "Start";
    }
    console.log("Timer reset.");
  } else {
    if (binaryTrue == 0) {
      var reset = document.getElementById("TimeTable").innerHTML = "00" + ":" + "00" + ":" + "00" + ":" + "00";
      if (isTiming == false) {
        var started = document.getElementById("control").innerHTML = "Start/Pause";
        console.log("Timer reset.");
      }
    }
  }
}

//Pausing the stop watch, so STOPPING it from continuing counting
//Set the isTiming variable as FALSE which controls the start_timer function from continuing
function stop_stopwatch() {
  isTiming = false;
  document.getElementById("control").innerHTML = "Resume";
  console.log("Timer stopped.");
}


//the same as stop_stopwatch but also RESETS the stopwatch

/* if isTiming is already FALSE and so the timer is paused
change button label into "Start/Pause"
otherwise make isTiming to be FALSE and reset the timer to 00:00:00:00
and reset the label to "start"
(leave the label as "Start/Pause" if the stopwatch isn't used at least once yet so that the user can still see the original instructions")*/
function stop_and_reset_stopwatch() {
  if (binaryTrue == 0) { //if the timer hasn't been used at least once
    if (isTiming == false) {
      document.getElementById("control").innerHTML = "Start/Pause"; //the user won't have knowledge of the instructions yet
    }
  } else {
    isTiming = false;
    document.getElementById("TimeTable").innerHTML = "00" + ":" + "00" + ":" + "00" + ":" + "00";
    if (binaryTrue == 0) { //if the stopwatch HASN'T been used yet at least once, meaning that binaryTrue is still 0
      document.getElementById("control").innerHTML = "Start/Pause";
    } else { //if the stopwatch HAS been used at least once, meaning that binaryTrue is 1
      document.getElementById("control").innerHTML = "Start";
    }
  }
  console.log("Timer stopped and reset.");
}
<link href="https://fonts.googleapis.com/css2?family=Dosis:wght@600&display=swap" rel="stylesheet">
<div class="Stopwatch">
  <div class="SpeedIndicator">
    <div class="IntervalIndicator">
      <div class="CountTable">
        <p><span id="TimeTable" style="color: #00ffbf; font-size: 200px;">00:00:00:00</span></p>
        <div class="Controllers">
          <button id="control" onclick="change_state();">Start/Pause </button>
          <button id="reset" onclick="reset();">Reset </button>
          <button id="stop" onclick="stop_and_reset_stopwatch();">Stop and Reset</button>
          <button id="speed" onclick="change_speed();">Change speed</button>
          <script type="text/javascript" src="timer.js"></script>
          <p><br><span id="Label" style="color: #00ffbf; font-size: 100px; font-weight: bold; font-family: Arial, Helvetica, sans-serif">hr:min:sec:hundredth</span></br>
          </p>
          <heading id="speedIndicator" style="color: #00aba3; font-size: 20px; font-family: Arial">Stopwatch speed: 1 second (default) </heading>
          <br>
          <heading id="FractionalIndicator" style="color: #00aba3; font-size: 10px; font-family: Arial"></heading>
          </br>
FZs
  • 16,581
  • 13
  • 41
  • 50
Met3oR
  • 3
  • 1
  • 3
  • 3
    Quite frankly, I won't even try to follow all that code, but… `setTimeout` and `setInterval` aren't guaranteeing any specific timing at all, no. You cannot expect your code to execute exactly on the millisecond, since the browser may or may not have a bunch of other things to do and will only execute your code on a best-effort basis. You need to code with that expectation in mind. – deceze Oct 07 '20 at 12:21
  • Also the reliable minimal timeout is about [10ms](https://stackoverflow.com/questions/9647215/what-is-minimum-millisecond-value-of-settimeout). Everything lower can not be trusted. – Lain Oct 07 '20 at 12:22

1 Answers1

4

setTimeout guarantees that a function will be called no sooner than the time specified. It is a very poor mechanism for measuring time.

In general, if you need something updated frequently, you should use requestAnimationFrame instead.

Whether you switch to it or not, don't try to count seconds.

Create a date object to store the time that you start, and then every time you perform an update: compare the current time to the start time.

For example:

document.querySelector("button").addEventListener("click", () => {

  const start = moment();

  requestAnimationFrame(function update() {
    const now = moment();
    const duration = moment.duration(now.diff(start));
    document.querySelector('output').value = duration.asSeconds();
    requestAnimationFrame(update);
  });

});
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.0/moment.min.js"></script>

<output>
<button>Start</button>
Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335