cabralpinto
cabralpinto

Reputation: 2088

Timer function not working correctly when stopped and started multiple times

First of all, you can find an example of my code in JS Fiddle and also below the question.

I'm working on a personal training webapp and basically you can hit play and then you get five minutes to do a series of tasks in a random order. The program creates the sessionTasks array in which are put in a random order tasks for the tasks array in order to fit the five minute limit. Right now the tasks array is just one I created with four tasks and the respective times just for testing.

The problem I ran into is the following: When you click the task so you can move forward to the next task, the next time you play seconds will move faster. The way I found to replicate is:

  1. Click play.
  2. Power through the tasks by clicking the task text very quickly.
  3. Click play again.

Now the seconds should be moving quicker. If not, repeat what you just did. It is irregular but it usually does it in the second try.

I cannot for the life of me understand why it behaves like this. I thought that maybe it was creating more Timers that all used #taskTimer to run but that didn't make sense to me. Is it something wrong with the Timer function? What is wrong in my code?

mainMenu();


var totalSessionTasks, taskIterator, selectedTimeInSecs = 300;

var taskTimer = new Timer("#taskTimer", nextTask);

var globalTimer = new Timer("#globalTimer", function() {

});

var tasks = [
  ["First task", 0, 30],
  ["Second task", 0, 15],
  ["Third task", 0, 10],
  ["Fourth task", 3, 0]
];

var sessionTasks = [

]




function setUpSession() {

  sessionTasks = []

  if (tasks.length != 0) {

    var sessionTasksSeconds = 0; //the seconds of the session being filled
    var sessionTasksSecondsToFill = selectedTimeInSecs; //seconds left in the session to fill
    var newTaskSeconds = 0; //seconds of the next task being added to the session
    var sessionFull = false;

    console.log('Session Empty');

    while (sessionFull === false) {

      var areThereAnyTaskThatFitInTheSession =
        tasks.some(function(item) {
          return ((item[1] * 60 + item[2]) <= sessionTasksSecondsToFill) && (item != sessionTasks[sessionTasks.length - 1]);
        });
      console.log(areThereAnyTaskThatFitInTheSession);

      if (areThereAnyTaskThatFitInTheSession) {
        do {
          var randTaskNum = Math.floor(Math.random() * tasks.length);
        } while (((tasks[randTaskNum][1] * 60 + tasks[randTaskNum][2]) > sessionTasksSecondsToFill) || (tasks[randTaskNum] == sessionTasks[sessionTasks.length - 1]))

        sessionTasks.push(tasks[randTaskNum]);
        newTaskSeconds = (tasks[randTaskNum][1]) * 60 + tasks[randTaskNum][2];
        sessionTasksSecondsToFill -= newTaskSeconds;
        sessionTasksSeconds += newTaskSeconds;

        console.log(tasks[randTaskNum][0] + ": " + newTaskSeconds + "s");
        console.log(sessionTasksSeconds)

      } else if (sessionTasks.length == 0) {
        note("All your tasks are too big for a game of " + selectedTimeInSecs / 60 + " minutes!");
        break;
      } else {
        console.log('Session full');
        sessionFull = true;
        taskIterator = -1;
        totalSessionTasks = sessionTasks.length;
        console.log(totalSessionTasks);

        globalTimer.set(0, sessionTasksSeconds);
        nextTask();
        globalTimer.run();
        taskTimer.run();
      }


    }

  } else {
    note("You don't have have any tasks in your playlists!");
  }

}


function nextTask() {

  if (taskIterator + 1 < totalSessionTasks) {
    taskIterator++;
    $("#taskText").text(sessionTasks[taskIterator][0]);
    globalTimer.subtract(0, taskTimer.getTotalTimeInSeconds())
    taskTimer.set(sessionTasks[taskIterator][1], sessionTasks[taskIterator][2]);
    $("#taskCounter").text(taskIterator + 1 + " of " + totalSessionTasks + " tasks");
  } else {
    mainMenu();
    taskTimer.stop();
    globalTimer.stop();
    note("Thanks for playing!");
  }


}

//timer object function
function Timer(element, callback) {

  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, interval = 1000,
    self = this,
    timeLeftToNextSecond = 1000;
  this.running = false;

  this.set = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds = inputMinutes * 60 + inputSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.add = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds += inputMinutes * 60 + inputSeconds;
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.subtract = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds -= inputMinutes * 60 + inputSeconds;
    if (finalTimeInSeconds <= 0) {
      callback()
    }
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;
    this.print();
  }

  this.reset = function() {

    this.set(0, 0);
  }

  this.print = function() {

    displayMinutes = (minutes.toString().length == 1) ? "0" + minutes : minutes; //ternary operator: adds a zero to the beggining 
    displaySeconds = (seconds.toString().length == 1) ? "0" + seconds : seconds; //of the number if it has only one caracter.

    $(element).text(displayMinutes + ":" + displaySeconds);
  }

  this.run = function() {

    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        interval = 1000;
      }
      ac = setInterval(_f, interval);


    }
  }

  this.stop = function() {

    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
      stopped = new Date;
      interval = 1000 - (stopped - secondStarted);
      clearInterval(ac);
    }
  }

  this.getTotalTimeInSeconds = function() {


    return finalTimeInSeconds;
  }

  this.reset();

}

function note(string) {
  alert(string);
}

function mainMenu() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //BUTTONS
    "<div id='playButton' class='mainButton'><div class='buttonText mainButtonText'>PLAY</div></div>"
  );
  //BINDS
  $("#playButton").bind("click", function(){
  	playMain();
    setUpSession();
  });

}

function playMain() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //TASK TEXT
    "<p class='text' id='taskText'>Lorem ipsum dolor sit amet.</p>",
    //TIMERS
    "<div id='taskTimerWrap'><p class='text timer' id='taskTimer'>00:00</p><p class='text' id='taskTimerText'>Task Time</p></div>",
    "<div id='globalTimerWrap'><p class='text timer' id='globalTimer'>00:00</p><p class='text' id='globalTimerText'>Global Time</p></div>",
    //TASK COUNTER
    "<div class='text' id='taskCounter'>0/0 tasks completed</div>"
  );
  //BINDS
  $("#taskText").bind("click", nextTask);
}
#taskText {
  text-align: center;
  display: table;
  vertical-align: middle;
  height: auto;
  width: 100%;
  top: 50px;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: auto;
  font-size: 65px;
  cursor: pointer;
}

#taskTimerWrap {
  text-align: center;
  top: 0;
  right: 0;
  left: 170px;
  margin: 5px;
  position: absolute;
  -webkit-transition: all 0.5s ease;
}

.timer {
  font-size: 64px;
  margin: 0;
  line-height: 0.88;
}

#taskTimerText {
  font-size: 34.4px;
  margin: 0;
  line-height: 0.65;
}

#globalTimerWrap {
  text-align: center;
  top: 0;
  left: 0;
  right: 170px;
  margin: 5px;
  position: absolute;
}

#globalTimerText {
  font-size: 28.5px;
  margin: 0;
  line-height: 0.78;
  transform: scale(1, 1.2);
}

#taskCounter {
  text-align: center;
  bottom: 0;
  right: 0;
  left: 0;
  width: auto;
  position: absolute;
  font-size: 30px;
  color: #98D8D9;
  -webkit-transition: all 0.5s ease;
}

#taskCounter:hover {
  color: #F1F2F0
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Upvotes: 14

Views: 697

Answers (3)

Dan Sin
Dan Sin

Reputation: 491

Here is an update in jsfiddle

The problem is on the variable interval of Timer

This line creates unpredictable interval under Timer.stop():

interval = 1000 - (stopped - secondStarted);

If you would like to shorten the time interval, you can add a play_count property:

    ...

    //timer object function
    function Timer(element, callback) {

  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, interval = 1000,
    self = this,
    timeLeftToNextSecond = 1000;

  this.running = false;
 play_count = 0;  // play count property

...

  this.run = function() {

    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        interval = Math.max(1000 - play_count * 100, 500);  // ** <-- shorten time interval
      }
      ac = setInterval(_f, interval);


    }
  }

  this.stop = function() {

    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
//      stopped = new Date;
//      interval = 1000 - (stopped - secondStarted);
            play_count++;
      clearInterval(ac);
    }
  }

  this.getTotalTimeInSeconds = function() {


    return finalTimeInSeconds;
  }

  this.reset();

}
...

Upvotes: 1

As I see, you are trying to set the value for variable interval from inside the interval callback function.

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        //interval = 1000; REMOVE THIS LINE
      }
      interval = 1000; //ADD IT HERE
      ac = setInterval(_f, interval);

Actually when setInterval is executed, the value for interval is not 1000. It's not updated by _f yet, because that function will run after the setInterval() is executed. And once setInterval() is called with the existing value of interval, changing it later has no impact on the created interval, because the setInterval() function has already set the delay time for the interval created. And the value for interval changes from it's initial value 1000 because of this line :

 interval = 1000 - (stopped - secondStarted); //I'm not sure what you are trying to do with this, possibly removing this line will also fix your problem.)

Complete Working Demo:

And here is the JS Fiddle.

mainMenu();


var totalSessionTasks, taskIterator, selectedTimeInSecs = 300;

var taskTimer = new Timer("#taskTimer", nextTask);

var globalTimer = new Timer("#globalTimer", function() {

});

var tasks = [
  ["First task", 0, 30],
  ["Second task", 0, 15],
  ["Third task", 0, 10],
  ["Fourth task", 3, 0]
];

var sessionTasks = [

]




function setUpSession() {

  sessionTasks = []

  if (tasks.length != 0) {

    var sessionTasksSeconds = 0; //the seconds of the session being filled
    var sessionTasksSecondsToFill = selectedTimeInSecs; //seconds left in the session to fill
    var newTaskSeconds = 0; //seconds of the next task being added to the session
    var sessionFull = false;

    console.log('Session Empty');

    while (sessionFull === false) {

      var areThereAnyTaskThatFitInTheSession =
        tasks.some(function(item) {
          return ((item[1] * 60 + item[2]) <= sessionTasksSecondsToFill) && (item != sessionTasks[sessionTasks.length - 1]);
        });
      console.log(areThereAnyTaskThatFitInTheSession);

      if (areThereAnyTaskThatFitInTheSession) {
        do {
          var randTaskNum = Math.floor(Math.random() * tasks.length);
        } while (((tasks[randTaskNum][1] * 60 + tasks[randTaskNum][2]) > sessionTasksSecondsToFill) || (tasks[randTaskNum] == sessionTasks[sessionTasks.length - 1]))

        sessionTasks.push(tasks[randTaskNum]);
        newTaskSeconds = (tasks[randTaskNum][1]) * 60 + tasks[randTaskNum][2];
        sessionTasksSecondsToFill -= newTaskSeconds;
        sessionTasksSeconds += newTaskSeconds;

        console.log(tasks[randTaskNum][0] + ": " + newTaskSeconds + "s");
        console.log(sessionTasksSeconds)

      } else if (sessionTasks.length == 0) {
        note("All your tasks are too big for a game of " + selectedTimeInSecs / 60 + " minutes!");
        break;
      } else {
        console.log('Session full');
        sessionFull = true;
        taskIterator = -1;
        totalSessionTasks = sessionTasks.length;
        console.log(totalSessionTasks);

        globalTimer.set(0, sessionTasksSeconds);
        nextTask();
        globalTimer.run();
        taskTimer.run();
      }


    }

  } else {
    note("You don't have have any tasks in your playlists!");
  }

}


function nextTask() {

  if (taskIterator + 1 < totalSessionTasks) {
    taskIterator++;
    $("#taskText").text(sessionTasks[taskIterator][0]);
    globalTimer.subtract(0, taskTimer.getTotalTimeInSeconds())
    taskTimer.set(sessionTasks[taskIterator][1], sessionTasks[taskIterator][2]);
    $("#taskCounter").text(taskIterator + 1 + " of " + totalSessionTasks + " tasks");
  } else {
    mainMenu();
    taskTimer.stop();
    globalTimer.stop();
    note("Thanks for playing!");
  }


}

//timer object function
function Timer(element, callback) {

  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, interval = 1000,
    self = this,
    timeLeftToNextSecond = 1000;
  this.running = false;

  this.set = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds = inputMinutes * 60 + inputSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.add = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds += inputMinutes * 60 + inputSeconds;
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.subtract = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds -= inputMinutes * 60 + inputSeconds;
    if (finalTimeInSeconds <= 0) {
      callback()
    }
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;
    this.print();
  }

  this.reset = function() {

    this.set(0, 0);
  }

  this.print = function() {

    displayMinutes = (minutes.toString().length == 1) ? "0" + minutes : minutes; //ternary operator: adds a zero to the beggining 
    displaySeconds = (seconds.toString().length == 1) ? "0" + seconds : seconds; //of the number if it has only one caracter.

    $(element).text(displayMinutes + ":" + displaySeconds);
  }

  this.run = function() {

    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        //interval = 1000; REMOVE THIS LINE
      }
      interval = 1000; //ADD IT HERE
      ac = setInterval(_f, interval);


    }
  }

  this.stop = function() {

    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
      stopped = new Date;
      interval = 1000 - (stopped - secondStarted);
      clearInterval(ac);
    }
  }

  this.getTotalTimeInSeconds = function() {


    return finalTimeInSeconds;
  }

  this.reset();

}

function note(string) {
  alert(string);
}

function mainMenu() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //BUTTONS
    "<div id='playButton' class='mainButton'><div class='buttonText mainButtonText'>PLAY</div></div>"
  );
  //BINDS
  $("#playButton").bind("click", function(){
  	playMain();
    setUpSession();
  });

}

function playMain() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //TASK TEXT
    "<p class='text' id='taskText'>Lorem ipsum dolor sit amet.</p>",
    //TIMERS
    "<div id='taskTimerWrap'><p class='text timer' id='taskTimer'>00:00</p><p class='text' id='taskTimerText'>Task Time</p></div>",
    "<div id='globalTimerWrap'><p class='text timer' id='globalTimer'>00:00</p><p class='text' id='globalTimerText'>Global Time</p></div>",
    //TASK COUNTER
    "<div class='text' id='taskCounter'>0/0 tasks completed</div>"
  );
  //BINDS
  $("#taskText").bind("click", nextTask);
}
#taskText {
  text-align: center;
  display: table;
  vertical-align: middle;
  height: auto;
  width: 100%;
  top: 50px;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: auto;
  font-size: 65px;
  cursor: pointer;
}

#taskTimerWrap {
  text-align: center;
  top: 0;
  right: 0;
  left: 170px;
  margin: 5px;
  position: absolute;
  -webkit-transition: all 0.5s ease;
}

.timer {
  font-size: 64px;
  margin: 0;
  line-height: 0.88;
}

#taskTimerText {
  font-size: 34.4px;
  margin: 0;
  line-height: 0.65;
}

#globalTimerWrap {
  text-align: center;
  top: 0;
  left: 0;
  right: 170px;
  margin: 5px;
  position: absolute;
}

#globalTimerText {
  font-size: 28.5px;
  margin: 0;
  line-height: 0.78;
  transform: scale(1, 1.2);
}

#taskCounter {
  text-align: center;
  bottom: 0;
  right: 0;
  left: 0;
  width: auto;
  position: absolute;
  font-size: 30px;
  color: #98D8D9;
  -webkit-transition: all 0.5s ease;
}

#taskCounter:hover {
  color: #F1F2F0
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Upvotes: 1

Johan Karlsson
Johan Karlsson

Reputation: 6476

In Timer.stop() you change the interval that's used for the next run. Changing the interval variable in _f() isn't going to change the interval used by setInterval().

In this case you will have to use setTimeout() instead:

function Timer(element, callback) {    
  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, timeout = 1000,
    self = this,
    timeLeftToNextSecond = 1000;
  this.running = false;

  /* ... */

  this.run = function() {
    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);        
        ac = setTimeout(_f, 1000);
      }
      ac = setTimeout(_f, timeout);
    }
  }

  this.stop = function() {    
    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
      stopped = new Date;
      timeout = 1000 - (stopped - secondStarted);
      clearTimeout(ac);
    }
  }

  /* ... */    
}

Upvotes: 2

Related Questions