-1

I’m trying to fluidify a circle progress bar in a timer. The progress bar represents the progression in the duration of the timer. I made an svg <circle> circle element for the progress bar. Every 10th of second, I change the css attribute stroke-dashoffset of the circle. This works fine but if we choose less than 5 minutes for the time, the movements of the progress bar are not fluid. What can I do to make that fluid ?

class Timer {
    constructor(circle, text) {
        this.circle = circle
        this.text = text
        this.text.innerHTML
    }

    async start(hours, minutes, seconds) {
        this.circle.style["stroke-dasharray"] = parseInt(Math.PI * this.circle.getBoundingClientRect().width)
        this.circle.style["stroke-dashoffset"] = parseInt(Math.PI * this.circle.getBoundingClientRect().width)
        await this.countdown()
        this.circle.classList = "progress"
        var remaining, interval, duration, end;
        duration = parseInt(hours) * 3600000 + parseInt(minutes) * 60000 + (parseInt(seconds) + 1) * 1000
        end = Date.now() + duration
        interval = setInterval(async () => {
            remaining = end - Date.now()
            this.circle.style["stroke-dashoffset"] = remaining * parseInt(Math.PI * this.circle.getBoundingClientRect().width) / duration;
            if (remaining < 0) {
                this.circle.style["stroke-dashoffset"] = 0
                clearInterval(interval)
                window.location.href = "./"
                return true
            }  else {
                this.text.innerHTML = `${("0" + parseInt(remaining / 3600000)).slice(-2)}:${("0" + parseInt(remaining % 3600000 / 60000)).slice(-2)}:${("0" + parseInt(remaining % 3600000 % 60000 / 1000)).slice(-2)}`
            }
        }, 100)
    }

    countdown() {
        var duration;
        duration = 2
        this.text.innerHTML = 3
        return new Promise(resolve => { 
                setInterval(async () => {
                if (duration <= 0) {
                    resolve(true)
                }  else {
                    this.text.innerHTML = duration
                    duration -= 1
                }
            }, 1000)
        })
    }
}

const timer = new Timer(document.getElementById("progress"), document.getElementById("text"))
const params = new URLSearchParams(window.location.search)
timer.start(0, 0, 10)
:root {
    --pi: 3.141592653589793
}

circle.progress {
    display: block;
    position: absolute;
    fill: none;
    stroke: url(#circle.progress.color);
    stroke-width: 4.5vmin;
    stroke-linecap: round;
    transform-origin: center;
    transform: rotate(-90deg);
}

circle.progress.animation {
    animation: circle 3s linear forwards;
}

.progress-container {
    left: 50vw;
    top: 50vh;
    width: 90vmin;
    height: 90vmin;
    margin-top: -45vmin;
    margin-left: -45vmin;
    position: absolute;
    padding: none;
}

.outer {
    margin: none;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    box-shadow: 6px 6px 10px -1px rgba(0, 0, 0, 0.15), -6px -6px 10px -1px rgba(255, 255, 255, 0.7);
    padding: 2.5%;
}

.inner {
    margin: 2.5%;
    width: 95%;
    height: 95%;
    border-radius: 50%;
    box-shadow: inset 4px 4px 6px -1px rgba(0, 0, 0, 0.15), inset -4px -4px 6px -1px rgba(255, 255, 255, 0.7);
}

svg {
    display: block;
    position: absolute;
    left: 50vw;
    top: 50vh;
    width: 90.5vmin;
    height: 90.5vmin;
    margin-top: -45.25vmin;
    margin-left: -45.25vmin;
}

svg text {
    font-size: 10vmin;
    font-family: 'Roboto', sans-serif;
    
}

@keyframes circle {
    0% {
        stroke-dashoffset: 0;
    }
    100% {
        stroke-dashoffset: calc(45.5vmin * var(--pi) * 2);
    }
}
<link href="https://cdn.jsdelivr.net/npm/@materializecss/materialize@1.2.1/dist/css/materialize.min.css" rel="stylesheet"/>
<div class="progress-container">
    <div class="outer center-align">
        <div class="inner"></div>
    </div>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="center" version="1.1">
    <defs>
        <linearGradient id="circle.progress.color">
        <stop offset="0%" stop-color="BlueViolet" />
        <stop offset="100%" stop-color="MediumVioletRed" />
        </linearGradient>
    </defs>
    <circle id="progress" class="progress animation" cy="45.25vmin" cx="45.25vmin" r="42.75vmin" />
    <text id="text" text-anchor="middle" x="50%" y="50%">Temps restant</text>
</svg>
<script src="https://cdn.jsdelivr.net/npm/@materializecss/materialize@1.2.1/dist/js/materialize.min.js"></script>

The code here runs the timer for 10 seconds. Normally, you would have to choose the time in another page. To have the time input, go to that page (the page is in french).

1 Answers1

0

I'm not able to explain the issue, so here I have an alternative solution. The function setInterval has issues with the timing (maybe that is actually you issue...). You cannot expect it to be precise. Instead of controlling the progress using setInterval you can use a keyframe animation with the Web Animations API. This is a better alternative to the JavaScript animation where you update an attribute/style, and easier to work with then SVG SMIL animations.

So, I rely on the animation doing its job and then update the time displayed by asking for the currenttime on the animation object.

const progress = document.getElementById('progress');
const text = document.getElementById('text');

document.forms.form01.addEventListener('click', e => {
  if(e.target.value){
    var progressKeyframes = new KeyframeEffect(
        progress, 
        [
          { strokeDasharray: '0 100' }, 
          { strokeDasharray: '100 100' }
        ], 
        { duration: parseInt(e.target.value), fill: 'forwards' }
      );

    var a1 = new Animation(progressKeyframes, document.timeline);
    a1.play();
    
    let timer = setInterval(function(){
      if(a1.playState == 'running'){
        text.textContent = Math.floor(a1.currentTime/1000);
      }else if(a1.playState == 'finished'){
        text.textContent = Math.round(e.target.value/1000);
        clearInterval(timer);
      }
    }, 100);
  }
});
<form name="form01">
  <input type="button" value="2000" />
  <input type="button" value="5000" />
  <input type="button" value="10000" />
</form>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="300">
  <defs>
    <linearGradient id="circle.progress.color">
      <stop offset="0%" stop-color="BlueViolet" />
      <stop offset="100%" stop-color="MediumVioletRed" />
    </linearGradient>
    <filter id="shadow">
      <feDropShadow dx=".4" dy=".4" stdDeviation="1" flood-color="gray"/>
    </filter>
  </defs>
  <circle r="40" stroke="white" stroke-width="8" fill="none" transform="translate(50 50)" filter="url(#shadow)"/>
  <circle id="progress" r="40" stroke="url(#circle.progress.color)" stroke-width="8" fill="none" pathLength="100" stroke-dasharray="0 100" transform="translate(50 50) rotate(-90)" stroke-linecap="round"/>
  <text id="text" dominant-baseline="middle" text-anchor="middle" x="50" y="50">0</text>
</svg>
chrwahl
  • 8,675
  • 2
  • 20
  • 30