Search code examples
javascriptalgorithmfrontendgame-physicsgame-development

How to make all the bars move smoothly at the same time even though they are moving at different speeds?


I have 5 progress bars that are all moving at the same time all at different speeds. Their speeds are dynamic and need to be modifiable programmatically which is why this is implemented with JS and not using animations by CSS. Here's a jsfiddle: https://jsfiddle.net/ezg97/ey319g48/7/
The problem is this: The progress bars move choppy, I want them to all move smoothly, I'm not sure how to accomplish this? This is for a game that I'm building.

If they increment at a very small number like 1, they appear to almost be moving smoothly; however, in reality they will be able to move at any rate.

The code is also viewable here:

// array to maintain progress bars
var pbArr = [{
    pid: 'bar1', // parent container id
    speed: 31, // max speed value
    accl: 10, // speed at which it will accelerate
    curr: 0, // current speed
    dist: 0 // total distance/displacement 
}, {
    pid: 'bar2',
    speed: 40,
        accl: 1,
    curr: 0,
    dist: 0
}, {
    pid: 'bar3',
    speed: 21,
        accl: 20,
    curr: 0,
    dist: 0
}, {
    pid: 'bar4',
    speed: 35,
        accl: 6,
    curr: 0,
    dist: 0
}, {
    pid: 'bar5',
    speed: 25,
    accl: 16,
    curr: 0,
    dist: 0
}];

var loopCnt = 1; // loop count to maintain width
var pb_timeout; // progress bar timeout function

// create progress bar funtion

var createPB = function () {

    var is_all_pb_complete = true; // flag to check whether all progress bar are completed executed

    for (var i = 0; i < pbArr.length; i++) {
        var childDiv = document.querySelector('#' + pbArr[i].pid + ' div'); // child div
        
        // When initially starting, set current speed to acceleration speed
        if (pbArr[i].curr === 0) {
            pbArr[i].curr = pbArr[i].accl;
        } else {
            // if the current speed + the acceleration is less than top speed, add more acceleration
            if ((pbArr[i].curr + pbArr[i].accl) < pbArr[i].speed) {
                pbArr[i].curr += pbArr[i].accl;
          } 
          // if the current speed + the acceleration is greater than acceleration, then make current speed equal to top speed
          else {
                pbArr[i].curr = pbArr[i].speed;
          }
        }
        // removed output: console.log(pbArr[i]);
        // add the current speed to the distance traveled already
        pbArr[i].dist += pbArr[i].curr;
        var newWidth = pbArr[i].dist; // new width
        
        //if your new distance traveled is less than 100, add it to the progress bar
        if (newWidth <= 100) {
            is_all_pb_complete = false;
            childDiv.style.width = newWidth + '%';
        } else {
            // if your new distance traveled is greater than or equal to 100, set the prgoress bar to 100%
            childDiv.style.width = '100%';
        }

    }

    if (is_all_pb_complete) { // if true, then clear timeout
        clearTimeout(pb_timeout);
        return;
    }

    loopCnt++; // increment loop count

    // recall function
    pb_timeout = setTimeout(function () {
        createPB();
    }, 1000);
}

// call function to initiate progress bars
createPB();
.bar{
    width:200px;
    height:20px;
    background-color:black;
    position:relative;
}
.child{
    position:abosoute;
    top:0px;
    left:0px;
    background-color:red;
    height:20px;
}
.clr{
    width:100%;
    height:2px;    
}
<div class="bar" id="bar1"><div class="child"></div></div>
<div class="clr"></div>
<div class="bar" id="bar2"><div class="child"></div></div>
<div class="clr"></div>
<div class="bar" id="bar3"><div class="child"></div></div>
<div class="clr"></div>
<div class="bar" id="bar4"><div class="child"></div></div>
<div class="clr"></div>
<div class="bar" id="bar5"><div class="child"></div></div>


Solution

  • I saw this question when it initially came through, and didn't have time for it. Another answer recently revived it.

    The problem, of course, is that you're repainting once a second. That cannot possibly lead to smooth animation. We could try to fix this using some fixed sub-second increment, and rerun on a fixed schedule. That would be smoother than this, but can still be choppy as the browser might get to your requests at different rates depending on other load. The next trick would be to call for the next step using setTimeout/setInterval and use the time differential between now and when we started to determine where we are in the animation, making changes appropriately. That was state of the art ten years ago, but browser vendors realized it would be easier if they supplied an API to make this easier. That is requestAnimationFrame. We supply a callback to this function, and it calls us with the current elapsed time. We use that elapsed time to determine the current state of our animation and update the DOM. AFAICT, this is still the state of the art for those who cannot for whatever reasons use CSS animations.

    The only trouble is that your code is not using a continuous technique to calculate positions. Instead, you are using the acceleration to update the current velocity and using the current velocity to update the position. We would like a continuous function that will accept not just times such as 1s, 2s, 3s,... but also 0.18641s and 2.5184s. To get there, we need to use some simple calculus.

    You might remember that the function for velocity at a given time is the derivative of the function for position at a given time. And the function for acceleration is the derivative of velocity. With some initial values, we can go the other way with antiderivatives. And if this sounds complex, given a fixed acceleration, as in this problem, it's really not. The only complexity comes from the maximum velocity values. Without that, given an acceleration of a, an initial velocity of 0 and an initial position of 0, we can calculate velocity, v, and position, p, like this:

    v = a * t
    p = (a/2) * t^2 
    

    just by the simplest of antiderivatives.

    Knowing that we have a maximum velocity, say m, we will have to adjust these. These are now more complex, but not absolutely horrible:

    v = a * t                           when t < m/a
    v = m                               when t >= m/a
    
    p = (a/2) * t^2                     when t < m/a
    p = m ( t - m/a) + (a/2 * (m/a)^2)  when t >= m/a
    

    And that last can be simplified to

    p = (a/2) * t^2                     when t < m/a
    p = m ( t - m/a) + (m^2 / 2a)       when t >= m/a
    

    We can now write this code with immutable data structures (always my preference) that are transformed into new ones on each step by applying this function from the time, the initial acceleration, and the maximum velocity, to get a new the dist property. It could look like this:

    const processPbs = (pbs) => (time) => {
      const t = time / 1000
      const newPbs = pbs .map (setPosition (t))
      displayPbs (newPbs)
      if (newPbs .some (({dist}) => dist < 100)) {
        window .requestAnimationFrame (processPbs (newPbs))
      }
    }
    
    const setPosition = (t) => (pb) => ({
      ... pb,
      dist: Math .min (100, t < pb .speed / pb .accl
        ? (pb .accl /2) * t ** 2
        : pb .speed * (t - pb .speed / pb .accl) + ((pb .speed) ** 2 / (2 * pb.accl)))
    })
    
    const displayPbs = (pbs) => pbs .forEach (({pid, dist}) =>
      document.querySelector('#' + pid + ' div') .style .width = dist + '%'
    )
    
    var pbArr = [{pid: 'bar1', speed: 31, accl: 10, dist: 0}, {pid: 'bar2', speed: 40, accl: 1,  dist: 0}, {pid: 'bar3', speed: 21, accl: 20, dist: 0}, {pid: 'bar4', speed: 35, accl: 6,  dist: 0}, {pid: 'bar5', speed: 25, accl: 16, dist: 0}]
    
    window .requestAnimationFrame (processPbs (pbArr))
    .bar {width: 200px; height: 20px; background-color: black; position: relative;} .child {position: abosoute; top: 0px; left: 0px; background-color: red; height:20px;} .clr {width:100%; height:2px;}
    <div class="bar" id="bar1"><div class="child"></div></div><div class="clr"></div> <div class="bar" id="bar2"><div class="child"></div></div><div class="clr"></div> <div class="bar" id="bar3"><div class="child"></div></div><div class="clr"></div> <div class="bar" id="bar4"><div class="child"></div></div><div class="clr"></div> <div class="bar" id="bar5"><div class="child"></div></div>

    Our main function is processPbs, which accepts a list of progress bars and returns a function which takes a time, uses that to create a new version of the progress bars, using the formula above to set the dist property, then displays the new bars, and if there are any that haven't yet hit the magic 100 threshold, requests an additional animation frame to repeat the process.

    It uses two helpers. setPosition does the actual calculation using that formula, and displayPbs updates the DOM. A strong suggestion would be that you always isolate DOM changes into separate functions from your logic.

    There are things we might want to change here. We should probably cache the DOM nodes in our objects rather than recalculating them on every iteration. I leave that as an exercise for the reader. We also might decide we don't want all these temporary objects, and choose to mutate the values in a static list of progress bars. I will do that only under duress, though, as I prefer immutable data. But if there were performance problems, this wouldn't be too hard to accomplish.