For demonstration of the issue please see here:
https://gyazo.com/06e423d07afecfa2fbdb06a6da77f66a
I'm getting a jumping behavior on un-pausing the notification. This is also influenced by how long the mouse stays on the notification and how close the progress is to the end.
I've tried so many things, I'm not sure anymore if the problem is truly with setTimeout.
It is like as if since the calculation of this.timerFinishesAt to the first iteration of requestAnimationFrame the progress jumps due to waiting on cpu time? But then again, why would it be influenced by the hover time and progress.
How do I mitigate the jumping behavior?
I read/tried to implement the fix from the following resources amongst looking at other stackoverflow questions:
https://gist.github.com/tanepiper/4215634
How to create an accurate timer in javascript?
What is the reason JavaScript setTimeout is so inaccurate?
https://www.sitepoint.com/creating-accurate-timers-in-javascript/
https://github.com/Falc/Tock.js/tree/master
https://github.com/philipyoungg/timer
https://github.com/Aaronik/accurate_timer
https://github.com/husa/timer.js
timerStart(){
// new future date = future date + elapsed time since pausing
this.timerFinishesAt = new Date( this.timerFinishesAt.getTime() + (Date.now() - this.timerPausedAt.getTime()) );
// set new timeout
this.timerId = window.setTimeout(this.toggleVisibility, (this.timerFinishesAt.getTime() - Date.now()));
// animation start
this.progressId = requestAnimationFrame(this.progressBar);
},
timerPause(){
// stop notification from closing
window.clearTimeout(this.timerId);
// set to null so animation won't stay in a loop
this.timerId = null;
// stop loader animation from progressing
cancelAnimationFrame(this.progressId);
this.progressId = null;
this.timerPausedAt = new Date();
},
progressBar(){
if (this.progress < 100) {
let elapsed = Date.now() - this.timerStarted.getTime();
let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
this.progress = Math.ceil((elapsed / wholeTime) * 100);
if (this.timerId) {
this.progressId = requestAnimationFrame(this.progressBar);
}
} else {
this.progressId = cancelAnimationFrame(this.progressId);
}
}
When you do calculate the current progress of your timer, you are not taking the pause time into consideration. Hence the jumps: This part of your code is only aware of the startTime and currentTime, it won't be affected by the pauses.
To circumvent it, you can either accumulate all this pause times in the startTimer function
class Timer {
constructor() {
this.progress = 0;
this.totalPauseDuration = 0;
const d = this.timerFinishesAt = new Date(Date.now() + 10000);
this.timerStarted = new Date();
this.timerPausedAt = new Date();
}
timerStart() {
const pauseDuration = (Date.now() - this.timerPausedAt.getTime())
this.totalPauseDuration += pauseDuration;
// new future date = future date + elapsed time since pausing
this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration);
// set new timeout
this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now()));
// animation start
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
timerPause() {
// stop notification from closing
window.clearTimeout(this.timerId);
// set to null so animation won't stay in a loop
this.timerId = null;
// stop loader animation from progressing
cancelAnimationFrame(this.progressId);
this.progressId = null;
this.timerPausedAt = new Date();
}
progressBar() {
if (this.progress < 100) {
let elapsed = (Date.now() - this.timerStarted.getTime()) - this.totalPauseDuration;
let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
this.progress = Math.ceil((elapsed / wholeTime) * 100);
log.textContent = this.progress;
if (this.timerId) {
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
} else {
this.progressId = cancelAnimationFrame(this.progressId);
}
}
toggleVisibility() {
console.log("done");
}
};
const timer = new Timer();
btn.onclick = e => {
if (timer.timerId) timer.timerPause();
else timer.timerStart();
};
<pre id="log"></pre>
<button id="btn">toggle</button>
or update the startTime, which seems to be more reliable:
class Timer {
constructor() {
this.progress = 0;
const d = this.timerFinishesAt = new Date(Date.now() + 10000);
this.timerStarted = new Date();
this.timerPausedAt = new Date();
}
timerStart() {
const pauseDuration = (Date.now() - this.timerPausedAt.getTime())
// update timerStarted
this.timerStarted = new Date(this.timerStarted.getTime() + pauseDuration);
// new future date = future date + elapsed time since pausing
this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration);
// set new timeout
this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now()));
// animation start
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
timerPause() {
// stop notification from closing
window.clearTimeout(this.timerId);
// set to null so animation won't stay in a loop
this.timerId = null;
// stop loader animation from progressing
cancelAnimationFrame(this.progressId);
this.progressId = null;
this.timerPausedAt = new Date();
}
progressBar() {
if (this.progress < 100) {
let elapsed = Date.now() - this.timerStarted.getTime();
let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
this.progress = Math.ceil((elapsed / wholeTime) * 100);
log.textContent = this.progress;
if (this.timerId) {
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
} else {
this.progressId = cancelAnimationFrame(this.progressId);
}
}
toggleVisibility() {
console.log("done");
}
};
const timer = new Timer();
btn.onclick = e => {
if (timer.timerId) timer.timerPause();
else timer.timerStart();
};
<pre id="log"></pre>
<button id="btn">toggle</button>
As to the final gap, not seeing how this code is linked with your UI, it's hard to tell what happens.