Timer with animated wheels and errors with the screen locked

…衆ロ難τιáo~ 提交于 2020-05-13 19:27:26

问题


I'm creating an interval timer with wheel animation. The timer is supposed to work this way: If somebody only types the seconds in the work - the seconds count down, infinite times. If someone types the seconds in work and break, the seconds count down infinitely. If you enter all the fields, the timer will work the same way as at the top.

I have a few questions. Why does this timer not work properly when the phone screen is locked, sometimes the seconds count down badly, or count down to minus numbers. How can I make these smallest circle so that this line decreases every time the countdown starts from the beginning to the end of the entered repetition numbers. Is this code at all correct?Please help me, I have no idea how to do this anymore.

var circle1 = document.querySelectorAll('circle')[0];
var circle2 = document.querySelectorAll('circle')[1];
var circle3 = document.querySelectorAll('circle')[2];
var circumference1 = circle1.getTotalLength();
var circumference2 = circle2.getTotalLength();
var circumference3 = circle3.getTotalLength();
circle1.style.strokeDasharray = circumference1;
circle2.style.strokeDasharray = circumference2;
circle3.style.strokeDasharray = circumference3;

function setProgress1(percent) {
  var offset = circumference1 - percent / 100 * circumference1;
  circle1.style.strokeDashoffset = offset;
}
function setProgress2(percent) {
  var offset = circumference2 - percent / 100 * circumference2;
  circle2.style.strokeDashoffset = offset;
}
function setProgress3(percent) {
  var offset = circumference3 - percent / 100 * circumference3;
  circle3.style.strokeDashoffset = offset;
}

document.getElementById('btn').addEventListener('click',function(){
  var workValue = Math.ceil(document.getElementById('work-seconds').value),
      breakValue = Math.ceil(document.getElementById('break-seconds').value),
      repeatValue = Math.ceil(document.getElementById('repeat').value),
      showSec = document.querySelector('text');
  clearInterval(countDownI);
  if(repeatValue > 0){
    breakValue > 0 ? setTimeout(end,((workValue + breakValue + 2) * repeatValue) * 1000) : setTimeout(end,((workValue + breakValue + 1) * repeatValue) * 1000);
  }
  if(breakValue > 0){
    var countDownI = setInterval(countDown,(workValue + breakValue + 2) * 1000);
  } else {
    var countDownI = setInterval(countDown,(workValue + breakValue + 1) * 1000);
  }
  function end(){
    clearInterval(countDownI);
    showSec.innerHTML = '–';
  }
  countDown();
  function countDown(){
    var workSec = new Date().getTime() + workValue * 1000;
    countWorkSec();
    var workI = setInterval(countWorkSec,1000);
    function countWorkSec(){
      var countSec = Math.ceil((workSec - new Date().getTime()) / 1000);
      countSec < 10 ? showSec.textContent = "0" + countSec : showSec.textContent = countSec;
      showSec.style.fontSize = 'calc(74px /' + showSec.textContent.length + ')';
      setProgress1(countSec * 100 / workValue);
      if(countSec <= 0){
        clearInterval(workI);
        setTimeout(function(){
          setProgress1(100);
        },1000);
      };
    };
    if(breakValue > 0){
      setTimeout(function(){
        var breakSec = new Date().getTime() + breakValue * 1000;
        countBreakSec();
        var breakI = setInterval(countBreakSec,1000);
        function countBreakSec(){
          var countSec = Math.ceil((breakSec - new Date().getTime()) / 1000);
          countSec < 10 ? showSec.textContent = "0" + countSec : showSec.textContent = countSec;
          showSec.style.fontSize = 'calc(74px /' + showSec.textContent.length + ')';
          setProgress2(countSec * 100 / breakValue);
          if(countSec <= 0){
            clearInterval(breakI);
            setTimeout(function(){
              setProgress2(100);
            },1000);
          };
        };
      },(workValue + 1) * 1000);
    };
  };
});
svg circle {
  fill: none;
  stroke: #f90;
  stroke-width: 21;
  stroke-linecap: round;
  transition: 1s;
  transform: rotate(-90deg);
  transform-origin: center center;
}
<svg width="233" height="233">
  <circle cx="50%" cy="50% " r="calc(50% - 10.5px)"/>
  <circle cx="50%" cy="50% " r="calc(50% - 34.5px)"/>
  <circle cx="50%" cy="50% " r="calc(50% - 58.5px)"/>
  <text x="50%" y="50%" text-anchor="middle" alignment-baseline="middle"></text>
</svg>

<p>Work</p>
<input type="number" id="work-seconds" placeholder="seconds" min="0">
<br>
<p>Break</p>
<input type="number" id="break-seconds" placeholder="break" min="0">
<br>
<p>Repeat</p>
<input type="number" id="repeat" placeholder="repeat" min="0">
<br>
<br>
<button id="btn">START</button>

回答1:


Modification

Your code has a lot of different instances of setInterval. Personally, I think that this makes it a little hard to work on. Objectively, this can cause some issues related to the execution timing. So, I've taken the liberty to modify your code a little so that it only uses one instance of setTimeout to operate the whole timer thing. Your code also struggles with resetting the timer to its initial state when the counter is ended prematurely (e.g. by clicking start again).


Solution

The solution works as follows:

  • If the repeat value is not specified, the counter repeats indefinitely
  • Following your example runnable snippet, I've made it so that the first tick of the work time and break time have a delay of 1s
  • The solution only uses one setTimeout instance, which can be used to clear the previous counter
  • The code resets the timer to its initial state when the counter is ended prematurely
  • The code uses the value -1 to detect when the break value and repeat value are not inserted or are 0. This -1 value is then used to do what you wanted, i.e. not running the break time counter and repeating indefinitely. This -1 value also determines the initial state of the circles' visuals.
  • The countDown function which sets the timeout works like so:
    1. It always checks if workValue is 0 or not. If not, set a new timeout calling the countDown function after 1s with the updated workValue (i.e. current workValue - 1). It also updates the text and the visual.
    2. When workValue is 0, it's time to run the break time counter, but only if its value is not 0. The logic works like workValue (setting a timeout every 1s with the updated breakValue [i.e. current breakValue - 1])
    3. When breakValue is 0, it will check for repeatValue. If repeatValue is -1 (meaning repeating indefinitely), reset the counter to its initial state and call a new timeout with the initial value again.
    4. However, if the repeatValue is not -1 and is indeed specified (and larger than 0), reset the counter to its almost-initial state, the difference being that the repeat value is now updated to repeatValue - 1. This will continue until repeatValue is 0.
    5. When repeatValue is 0, update the visuals and set the text to -. Also, stop the timer.

Here's the working solution. Do try running it ;-)

window.onload = function() {
  var circle1 = document.querySelectorAll('circle')[0];
  var circle2 = document.querySelectorAll('circle')[1];
  var circle3 = document.querySelectorAll('circle')[2];

  var circumference1 = circle1.getTotalLength();
  var circumference2 = circle2.getTotalLength();
  var circumference3 = circle3.getTotalLength();
  circle1.style.strokeDasharray = circumference1;
  circle2.style.strokeDasharray = circumference2;
  circle3.style.strokeDasharray = circumference3;

  function setProgress1(percent) {
    var offset = circumference1 - percent / 100 * circumference1;
    circle1.style.strokeDashoffset = offset;
  }
  function setProgress2(percent) {
    var offset = circumference2 - percent / 100 * circumference2;
    circle2.style.strokeDashoffset = offset;
  }
  function setProgress3(percent) {
    var offset = circumference3 - percent / 100 * circumference3;
    circle3.style.strokeDashoffset = offset;
  }

  var timeout
  document.getElementById('btn').addEventListener('click', function() {
    var initialWorkValue = Math.ceil(document.getElementById('work-seconds').value),
        initialBreakValue = Math.ceil(document.getElementById('break-seconds').value) || -1,
        initialRepeatValue = Math.ceil(document.getElementById('repeat').value) || -1,
        showSec = document.querySelector('text'),
        workTime = initialWorkValue * 1000,
        breakTime = initialBreakValue * 1000,
        workAndBreakTime = workTime + breakTime,
        totalTime = initialRepeatValue * workAndBreakTime,
        initialBreakProgress = initialBreakValue === -1 ? 0 : 100,
        initialRepeatProgress = initialRepeatValue === -1 ? 0 : 100

    // Reset timer
    clearTimeout(timeout)
    setProgress1(100)
    setProgress2(initialBreakProgress)
    setProgress3(initialRepeatProgress)

    countDown(initialRepeatValue, initialWorkValue, initialBreakValue)

    function countDown(repeatValue, workValue, breakValue) {
      if (workValue >= 0) {
        setProgress1(workValue * 100 / initialWorkValue)
        showSec.textContent = workValue
        timeout = setTimeout(countDown, 1000, repeatValue, workValue -= 1, breakValue)
      } else if (breakValue >= 0) {
        setProgress2(breakValue * 100 / initialBreakValue)
        showSec.textContent = breakValue
        timeout = setTimeout(countDown, 1000, repeatValue, workValue, breakValue -= 1)
      } else if (repeatValue === -1) {
        setProgress1(100)
        setProgress2(initialBreakProgress)
        countDown(repeatValue, initialWorkValue, initialBreakValue)
      } else if ((repeatValue - 1) > 0) {
        setProgress1(100)
        setProgress2(initialBreakProgress)
        setProgress3((repeatValue - 1) * 100 / initialRepeatValue)
        countDown(repeatValue -= 1, initialWorkValue, initialBreakValue)
      } else {
        clearTimeout(null)
        setProgress3(0)
        showSec.innerHTML = '&ndash;'
      }

      showSec.style.fontSize = `calc(30px / ${showSec.textContent.length})`
    }
  })
}
svg circle {
  fill: none;
  stroke: #f90;
  stroke-width: 21;
  stroke-linecap: round;
  transition: 1s;
  transform: rotate(-90deg);
  transform-origin: center center;
}
<svg width="233" height="233">
  <circle cx="50%" cy="50% " r="calc(50% - 10.5px)"/>
  <circle cx="50%" cy="50% " r="calc(50% - 34.5px)"/>
  <circle cx="50%" cy="50% " r="calc(50% - 58.5px)"/>
  <text x="50%" y="50%" text-anchor="middle" alignment-baseline="middle"></text>
</svg>

<p>Work</p>
<input type="number" id="work-seconds" placeholder="seconds" min="0">
<br>
<p>Break</p>
<input type="number" id="break-seconds" placeholder="break" min="0">
<br>
<p>Repeat</p>
<input type="number" id="repeat" placeholder="repeat" min="0">
<br>
<br>
<button id="btn">START</button>

As requested, here's the solution that still causes the timer to work properly even when it is run in the background in a mobile device (e.g. when the phone is locked). Instead of using setTimeout, you can use setInterval for this and you'd have to modify the code by quite a lot.

window.onload = function() {
  const button = document.querySelector('#btn')
  const circle1 = document.querySelectorAll('circle')[0]
  const circle2 = document.querySelectorAll('circle')[1]
  const circle3 = document.querySelectorAll('circle')[2]
  const showSec = document.querySelector('text')

  const circumference1 = circle1.getTotalLength()
  const circumference2 = circle2.getTotalLength()
  const circumference3 = circle3.getTotalLength()
  circle1.style.strokeDasharray = circumference1
  circle2.style.strokeDasharray = circumference2
  circle3.style.strokeDasharray = circumference3

  function setProgress1(percent) {
    let offset = circumference1 - percent / 100 * circumference1
    circle1.style.strokeDashoffset = offset
  }
  function setProgress2(percent) {
    let offset = circumference2 - percent / 100 * circumference2
    circle2.style.strokeDashoffset = offset
  }
  function setProgress3(percent) {
    let offset = circumference3 - percent / 100 * circumference3
    circle3.style.strokeDashoffset = offset
  }

  let interval
  btn.addEventListener('click', function() {
    let counterValues = new (function() {
      this.workTimeStartDelay = 1000
      this.workTime = Math.ceil(document.getElementById('work-seconds').value) * 1000
      this.breakTimeStartDelay = 1000
      this.breakTime = Math.ceil(document.getElementById('break-seconds').value) * 1000 || -1 // Checking for 0
      this.repeatValue = Math.ceil(document.getElementById('repeat').value) || -1 // Checking for 0
      this.startTime = Date.now()
    })()
    
    // Clearing interval and restart the timer
    clearInterval(interval)
    setProgress1(100)
    setProgress2(counterValues.breakTime === -1 ? 0 : 100)
    setProgress3(counterValues.repeatValue === -1 ? 0 : 100)
    showSec.textContent = 'Go'
    
    interval = setInterval(countDown, 1000, {...counterValues})
    showSec.style.fontSize = `calc(30px / ${showSec.textContent.length})`
    
    // Counting down that works even when mobile phone is locked
    function countDown(values) {
      let nowTime = Date.now()
      let elapsedTimeSinceStart = nowTime - values.startTime
      let repetitionTime = values.workTimeStartDelay + values.workTime
      if (values.breakTime !== -1) repetitionTime += values.breakTimeStartDelay + values.breakTime
      let currRepElapsedTime = elapsedTimeSinceStart % repetitionTime
      
      // Timer should or should have stopped
      // Don't continue after this if when true
      let totalTime = values.repeatValue === -1 ? -1 : values.repeatValue * repetitionTime
      if (totalTime !== -1 && totalTime <= elapsedTimeSinceStart) {
        setProgress1(0)
        setProgress2(0)
        setProgress3(0)
        showSec.innerHTML = '&ndash;'
        clearInterval(interval)
        return
      }
      
      let counterState = 0 // 0 = still on workTimeStartDelay, 1 = workTime, 2 = breakTimeStartDelay, 3 = breakTime
      // Determine which counter the timer is counting down
      let counterKeys = Object.keys(values).splice(0, 4)
      for (let key of counterKeys) {
        if (values[key] !== -1 && currRepElapsedTime >= values[key]) {
          currRepElapsedTime -= values[key]
          counterState += 1
        } else break
      }
      
      // Apply different logic for different state the counter is at
      if (counterState === 0) {
        setProgress1(0)
        setProgress2(0)
        showSec.textContent = 0
      } else if (counterState === 1) {
        let currentTimeText = Math.floor(values.workTime / 1000) - Math.floor((elapsedTimeSinceStart % repetitionTime - values.workTimeStartDelay) / 1000)
        setProgress1(currentTimeText / Math.floor(values.workTime / 1000) * 100)
        setProgress2(counterValues.breakTime === -1 ? 0 : 100)
        showSec.textContent = currentTimeText
      } else if (counterState === 2) { 
        setProgress1(0)
        setProgress2(100)
        showSec.textContent = 0
      } else if (counterState === 3) {
        let currentTimeText = Math.floor(values.workTime / 1000) - Math.floor((elapsedTimeSinceStart % repetitionTime - values.workTimeStartDelay - values.workTime - values.breakTimeStartDelay) / 1000)
        setProgress1(0)
        setProgress2(currentTimeText / Math.floor(values.breakTime / 1000) * 100)
        showSec.textContent = currentTimeText
      }
      
      if (values.repeatValue !== -1) {
        let repeatedBy = Math.floor(elapsedTimeSinceStart / repetitionTime)
        console.log(repeatedBy)
        let repeatPercent = 100 - (repeatedBy / values.repeatValue) * 100
        setProgress3(repeatPercent)
      } else {
        setProgress3(0)
      }
      showSec.style.fontSize = `calc(30px / ${showSec.textContent.length})`
    }
  })
}
svg circle {
  fill: none;
  stroke: #f90;
  stroke-width: 21;
  stroke-linecap: round;
  transition: 1s;
  transform: rotate(-90deg);
  transform-origin: center center;
}
<svg width="233" height="233">
  <circle cx="50%" cy="50% " r="calc(50% - 10.5px)"/>
  <circle cx="50%" cy="50% " r="calc(50% - 34.5px)"/>
  <circle cx="50%" cy="50% " r="calc(50% - 58.5px)"/>
  <text x="50%" y="50%" text-anchor="middle" alignment-baseline="middle"></text>
</svg>

<p>Work</p>
<input type="number" id="work-seconds" placeholder="seconds" min="0">
<br>
<p>Break</p>
<input type="number" id="break-seconds" placeholder="break" min="0">
<br>
<p>Repeat</p>
<input type="number" id="repeat" placeholder="repeat" min="0">
<br>
<br>
<button id="btn">START</button>
<div class="visibilityChanged"></div>


来源:https://stackoverflow.com/questions/61500333/timer-with-animated-wheels-and-errors-with-the-screen-locked

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!