Smooth animation for JS/HTML circular progress bar

Viewed 45

I am trying to smooth the animation for a circular progress bar i have made using HTML canvas arc and javascript.

The progress bar is drawn using data pulled from an XML sheet (semic.cgx) which is updated dynamically by a PLC.

My idea is to check the XML sheet every 100ms for an update and between this add 1/10th of an increment every 10ms to the variable that controls the progress. Turning 1 increment into 10.

Ive not yet been able to achieve my desired affect. The below code has issues once the upper limit of progress bar has been reached.

I know there must be other ways of doing this as i have seen a lot of examples online of smooth animations but most contain no information on how the code is actually working and my knowledge is basic. I would appreciate any help.

var req

function reloadData() {
    
    url = 'semic.cgx'
    
    try
    {
        req = new XMLHttpRequest();
    }
    catch (e)
    {
        alert('No AJAX Support');
        return;
    }
    
   req.onreadystatechange = myFunction;
   req.open('GET', url, true);
   req.send(null);
}




function myFunction() {
    

if (req.readyState == 4)
{
    if (req.status == 200)
    {
        
        var x = req.responseXML;
        var v1 = x.getElementsByTagName('text')[0].childNodes[1].innerHTML;

        var angle = (0.75 +((v1 / 100)* 1.5));
        

        
        var c = document.getElementById("myCanvas");
        var ctx = c.getContext("2d");
        
         setInterval(function () {
            
            if (angle >= 2.25) {
        ctx.clearRect(0,0,500,500);
        }
            
            
            if (angle < 2.25) {
            angle = angle + 0.0015;
            
            ctx.globalCompositeOperation = "source-over";
        ctx.rotate(0.5*2*Math.PI);
        ctx.lineWidth = 15;
        ctx.imageSmoothingEnabled = true;
        ctx.imageSmoothingQuality = "high";
        ctx.beginPath();
        ctx.arc(250, 250, 200, (0.75 * Math.PI), (angle * Math.PI));
        ctx.strokeStyle = "#DE2700";
        ctx.stroke();   
            }
    
        
            console.log(angle);
        }, 10);
    
        
        timeoutID = setTimeout('reloadData()', 100);    
    }

}
}
1 Answers

I would start by clearly separating your arc drawing function from the other app logic.

Here's your drawing code refactored in to a single function that takes:

  • A value between 0 and 1 to start the arc,
  • A value between 0 and 1 to end the arc,
  • The arc angle that corresponds to 0,
  • The arc angle that corresponds to 1

const ctx = document.getElementById("myCanvas").getContext("2d");
ctx.globalCompositeOperation = "source-over";
ctx.lineWidth = 15;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";

function draw(p0, p1, start = 0.75 * Math.PI, end = 2.25 * Math.PI) {
  // Map from 0-1 to start-end
  const range = end - start;
  
  const fromAngle = range * p0 + start;
  const toAngle = range * p1 + start;
  
  ctx.beginPath();
  ctx.arc(250, 250, 200, fromAngle, toAngle);
  ctx.strokeStyle = "#DE2700";
  ctx.stroke();
}

draw(0, 1);
canvas { 
  transform-origin: top left;
  transform: scale3d(0.3, 0.3, 1);
}
<canvas id="myCanvas" width="500" height="500"></canvas>

With that sorted, you can focus on animation. The animate function below takes three arguments:

  • A value between 0 and 1 to start the animation,
  • A value between 0 and 1 to end the animation,
  • The duration of the animation in milliseconds

// Setup
const ctx = document.getElementById("myCanvas").getContext("2d");
ctx.globalCompositeOperation = "source-over";
ctx.lineWidth = 15;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";

// Animate from 0 to 100% in 1s
animate(0, 1, 1000);

function draw(p0, p1, start = 0.75 * Math.PI, end = 2.25 * Math.PI) {
  const range = end - start;
  const fromAngle = range * p0 + start;
  const toAngle = range * p1 + start;
  
  ctx.beginPath();
  ctx.arc(250, 250, 200, fromAngle, toAngle);
  ctx.strokeStyle = "#DE2700";
  ctx.stroke();
}

function animate(from, to, duration) {
  const range = to - from;
  let start = null;
  
  const next = (ts) => {
    if (!start) {
      start = ts;
    }
    
    // Progress between from and to as value from 0 to 1
    const dt = Math.min(ts - start, duration);
    const p = dt / duration;
    
    draw(from, from + p * range);
    
    if (dt < duration) requestAnimationFrame(next);
  }

  requestAnimationFrame(next);
}
canvas { 
  transform-origin: top left;
  transform: scale3d(0.3, 0.3, 1);
}
<canvas id="myCanvas" width="500" height="500"></canvas>

Now, the hardest part that remains is linking the drawing and animation to your external value updates. It's hard for me to mock your xml workflow, but this fake implementation should give you an idea:

  • Request the latest loading value from the xml document,
  • Tell the animation function to animate from the previously loaded value to the new one in 100ms
  • After 100ms, if we're not at 100% yet, schedule a new call

// Setup
const ctx = document.getElementById("myCanvas").getContext("2d");
ctx.globalCompositeOperation = "source-over";
ctx.lineWidth = 15;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";

// Fake a +- 50s load time
let p = 0;
const update = () => {
  const newP = Math.min(p + Math.random() / 50, 1);
  animate(p, newP, 100);
  
  p = newP;
  
  if (p < 1) setTimeout(update, 100);
}

update();

// Drawing + animation functions
function draw(p0, p1, start = 0.75 * Math.PI, end = 2.25 * Math.PI) {
  const range = end - start;
  const fromAngle = range * p0 + start;
  const toAngle = range * p1 + start;
  
  ctx.beginPath();
  ctx.arc(250, 250, 200, fromAngle, toAngle);
  ctx.strokeStyle = "#DE2700";
  ctx.stroke();
}

function animate(from, to, duration) {
  const range = to - from;
  let start = null;
  
  const next = (ts) => {
    if (!start) {
      start = ts;
    }
    
    // Progress between from and to as value from 0 to 1
    const dt = Math.min(ts - start, duration);
    const p = dt / duration;
    
    draw(from, from + p * range);
    
    if (dt < duration) requestAnimationFrame(next);
  }

  requestAnimationFrame(next);
}
canvas { 
  transform-origin: top left;
  transform: scale3d(0.3, 0.3, 1);
}
<canvas id="myCanvas" width="500" height="500"></canvas>

Related