问题
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
setTimeoutinstance, 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
-1to detect when the break value and repeat value are not inserted or are 0. This-1value is then used to do what you wanted, i.e. not running the break time counter and repeating indefinitely. This-1value also determines the initial state of the circles' visuals. - The
countDownfunction which sets the timeout works like so:- It always checks if
workValueis 0 or not. If not, set a new timeout calling thecountDownfunction after 1s with the updatedworkValue(i.e. currentworkValue - 1). It also updates the text and the visual. - When
workValueis 0, it's time to run the break time counter, but only if its value is not 0. The logic works likeworkValue(setting a timeout every 1s with the updatedbreakValue[i.e. currentbreakValue - 1]) - When
breakValueis 0, it will check forrepeatValue. IfrepeatValueis-1(meaning repeating indefinitely), reset the counter to its initial state and call a new timeout with the initial value again. - However, if the
repeatValueis not-1and 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 torepeatValue - 1. This will continue untilrepeatValueis 0. - When
repeatValueis 0, update the visuals and set the text to-. Also, stop the timer.
- It always checks if
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 = '–'
}
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 = '–'
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