[Vuejs]-How to fix setTimeout/requestAnimationFrame accuracy

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.

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>

Leave a comment