0

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://codepen.io/sayes2x/embed/GYdLqL?default-tabs=js%2Cresult&height=600&host=https%3A%2F%2Fcodepen.io&referrer=https%3A%2F%2Fmedium.com%2Fmedia%2Fb90251c55fe9ac7717ae8451081f6366%3FpostId%3D255f3f5cf50c&slug-hash=GYdLqL

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);
   }
}
nandi95
  • 3
  • 1
  • 1
    You're using an offset to the start time, called `elapsed`, which progresses no matter what the mouse does. So every time you un-pause the progress bar jumps back to where it is supposed to be, as if you hadn't paused at all. Or is that not the jumping you're talking about? – KIKO Software Jun 19 '19 at 21:42
  • One possible issue is that you're nesting `requestAnimationFrame` in another function that calls `requestAnimationFrame`. Shouldn't `this.progressId = requestAnimationFrame(this.progressBar);` just be `this.progressId = this.progressBar;`? – Andrew Jun 19 '19 at 21:44
  • I think setTimeout and requestAnimationFrame are incompatible – Mister Jojo Jun 19 '19 at 21:45
  • @KIKOSoftware Ah yes I can see that now. I'll try to implement a fix for that soon, but how would that explain the pausing behavior near to the end of the progress when the jump is minimal or non-existent – nandi95 Jun 19 '19 at 23:08

2 Answers2

0

I think simply using SetInterval is enough :

const progressBar = {
  MsgBox : document.querySelector('#Message'),
  Info   : document.querySelector('#Message h1'),
  barr   : document.querySelector('#Message progress'),
  interV : 0,
  DTime  : 0,
  D_Max  : 0,

  Init() {
    this.MsgBox.onmouseover=_=> {   // pause
      clearInterval( this.interV )
    }
    this.MsgBox.onmouseout=_=>{     // restart
      this._run()
    }
  },
  Start(t,txt)
  {
    this.DTime = this.D_Max = t * 1000
    this.barr.value = 0
    this.barr.max = this.D_Max
    this.Info.textContent = txt
    this._run()
  },
  _run()
  {
    let D_End = new Date(Date.now() + this.DTime )

    this.interV = setInterval(_=>{
      this.DTime = D_End - (new Date(Date.now()))

      if (this.DTime > 0) { this.barr.value = this.D_Max - this.DTime }
      else                { clearInterval( this.interV ); console.clear(); console.log( "finish" ) }      
    }, 100);
  }
}


progressBar.Init()

progressBar.Start(10, 'Hello!') // 10 seconds
#Message {
  box-sizing: border-box;
  display: block;
  float: right;
  width: 200px;
  height: 80px;
  background-color: darkslategrey;
  padding: 0 1em;
  color:#e4a8b4;
  cursor: pointer;
  margin-right:1.5em;
}
#Message h1 { margin: .3em 0 0 0}
#Message progress { height: .1em; margin: 0; width:100%; background-color:black; }
#Message progress::-moz-progress-bar,
#Message progress::-webkit-progress-value { background-color:greenyellow; }
<div id="Message">
  <progress value="50" max="100" ></progress>
  <h1> </h1>
</div>
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
  • 2
    `requestAnimationFrame` has multiple properties that make it superior to `setInterval` when it comes to graphic rendering. I don't think it's a good idea to forego it simply because the existing iteration doesn't work for OP – Andrew Jun 19 '19 at 23:54
0

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.

Kaiido
  • 123,334
  • 13
  • 219
  • 285