javascript - 带有动画轮子的计时器和屏幕锁定时出现的错误

标签 javascript html css svg timer

我正在创建一个带有轮子动画的间隔计时器。计时器应该这样工作:如果有人只在工作中输入秒数 - 秒数会倒计时,无限次。如果有人在工作和休息时输入秒数,则秒数会无限倒计时。如果您输入所有字段,计时器的工作方式将与顶部相同。

我有几个问题。为什么这个定时器在手机屏幕锁定时无法正常工作,有时秒倒数不好,或者倒数为负数。我怎样才能制作这些最小的圆圈,以便每次从输入的重复数字的开始到结束倒计时时,这条线都会减少。这段代码完全正确吗?请帮助我,我不知道该怎么做。

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>

最佳答案

修改

您的代码有许多不同的 setInterval 实例。就我个人而言,我认为这使得工作有点困难。客观上,这可能会导致一些与执行时序相关的问题。因此,我冒昧地稍微修改了您的代码,以便它只使用 setTimeout 的一个实例。操作整个计时器的事情。当计数器过早结束时(例如,通过再次单击“开始”),您的代码还很难将计时器重置为其初始状态。


解决方案

解决方案的工作原理如下:

  • 如果未指定重复值,则计数器无限重复
  • 按照您的示例可运行代码片段,我已将工作时间休息时间的第一个刻度设置为延迟1秒
  • 该解决方案仅使用一个 setTimeout实例,可用于清除之前的计数器
  • 当计数器提前结束时,代码会将计时器重置为其初始状态
  • 代码使用值 -1检测中断值重复值何时未插入或为0。此-1然后使用值执行您想要的操作,即不运行中断时间计数器并无限期地重复。这个-1值还决定了圆圈视觉效果的初始状态。
  • countDown设置超时的函数的工作原理如下:
    1. 它始终检查是否 workValue是否为 0。如果没有,请设置一个新的超时,调用 countDown 1 秒后函数更新为 workValue (即当前 workValue - 1 )。它还更新了文本和视觉效果。
    2. 何时 workValue为 0,则需要运行中断时间计数器,但仅当其值不为 0。逻辑工作方式类似于 workValue (每 1 秒设置一次超时,更新的 breakValue [即当前 breakValue - 1 ])
    3. 何时 breakValue为 0,它将检查 repeatValue 。如果repeatValue-1 (意味着无限期地重复),将计数器重置为其初始状态,并再次使用初始值调用新的超时。
    4. 但是,如果 repeatValue不是-1并且确实被指定(并且大于 0),将计数器重置为其几乎初始状态,区别在于重复值现在更新为 repeatValue - 1 。这将持续到 repeatValue是 0。
    5. 何时 repeatValue为 0,更新视觉效果并将文本设置为 - 。另外,停止计时器。

这是可行的解决方案。请尝试运行它;-)

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>

根据要求,以下解决方案即使在移动设备的后台运行(例如,当手机锁定时),仍然可以使计时器正常工作。而不是使用 setTimeout ,您可以使用setInterval为此,您必须对代码进行大量修改。

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>

关于javascript - 带有动画轮子的计时器和屏幕锁定时出现的错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61500333/

相关文章:

javascript - 从服务 Javascript 接收变量 - React Hooks

javascript - 避免拆分第一个字符串数组时的第一个空格

javascript - 将 html 表单发送到 Django 项目中的电子邮件地址的最佳方式?

javascript - jQuery fadeIn div 显示

javascript - 如何有效地将 css 值导入 javascript 文件?

c# - WebKit.NET C# : CSS Gradient (Radial)

jquery - 如何在带有动画翻转的图像顶部使用jquery在悬停时淡入div

javascript - 如何设置 Google map 缩放级别以在中心显示所有标记和用户?

javascript - Google Maps Javascript API V3 - 如何删除自定义控件?

html - 如何在 css 中制作滑雪板(或挤压矩形)形状的 div?