3

So I've made a simple custom progress bar for an audio object within my website. The progress bar works fine, but I've noticed it is very choppy. I also noticed that on websites such as Facebook and YouTube, their progress bar transitions seem to be exceptionally smooth (watch any video and you'll see what I mean).

I thought a workaround to this might be to use some crafty JavaScript and CSS, but in the end it just seemed very tacky, CPU heavy for no reason and looked essentially exactly the same as before. (This is what I came up with):

setInterval(function(){
    var rect = elapsedContainer.getBoundingClientRect();
    var percentage = audio.currentTime / audio.duration;
    elapsed.style.width = (percentage * rect.width) + "px";
}, 33); // 30fps
.elapsed-container{
    width: 100%;
    height: 10px;
    background: grey;
}
.elapsed{
    left: 0;
    height: 100%;
    background: red;
    transition: width 33ms linear;
}

JsFiddle

All help is appreciated, cheers.

GROVER.
  • 4,071
  • 2
  • 19
  • 66

4 Answers4

3

You can try using window.requestAnimationFrame() instead of setTimeout(). The requestAnimationFrame callback allows the computer to try to get as close to 60fps as possible, but can alter the framerate for load, making it more performant than setTimeout(), which has to always match the specified framerate and can then end up skipping frames and then 'flicker' (see here for more info).

I also removed the CSS transition, so you are not mixing animations (since the requestAnimationFrame already animates at 60fps, the CSS transition is somewhat irrelevant)

// Change setTimeout to requestFrameAnimation
function progress_animation() {
  var rect = container.getBoundingClientRect();
  var percentage = audio.currentTime / audio.duration;
  elapsed.style.width = (percentage * rect.width) + "px";

  window.requestAnimationFrame(progress_animation);
};

// Only run animation when relevant
document.getElementById("play").onclick = function(){
  window.requestAnimationFrame(progress_animation);

  audio.play();
}

https://jsfiddle.net/4ymch2jg/

This appears smoother for me at least

Oliver
  • 2,930
  • 1
  • 20
  • 24
2

Idea is to use requestAnimationFrame instead of simple setInterval. You may read more about it here. Main thins is that this method optimizes browsers render possibilities and resources usage. Thats why it looks much more smooth.

Here is the code example which you then can extend as you need.

let duration = 20000;
let startTime = Date.now();
const elapsedContainer2 = document.querySelector('#con-2');
const elapsed2 = document.querySelector('#el-2');

function animate() {
  var rect = elapsedContainer2.getBoundingClientRect();
    let percentage = (Date.now()-startTime) / duration;
    elapsed2.style.width = (percentage * rect.width) + "px";
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Here is the link to the codepen where you can see the difference between both approaches. By the way, the more loaded your machine is the more difference is noticeable.

Note, that I've also changed the css animation from left 33ms in your case to the width 0.16s. In the code we change the width of the div, not the left property, so it should be mentioned in the CSS animation. 0.16s is close to 60fps which is exactly what requestAnimationFrame is trying to achieve.

.elapsed{
    left: 0;
    height: 100%;
    background: red;
    transition: width 0.16s linear;
}
Artem Arkhipov
  • 7,025
  • 5
  • 30
  • 52
0

You can add artificial smooth using an moving average on 10 percent.

https://en.m.wikipedia.org/wiki/Moving_average

It mean you calcul the average speed on the last 10 percent of your slide bar and show an estimation using speed = distance / time

Vincent Bénet
  • 1,212
  • 6
  • 20
  • 1
    It would be great if you create a working example and attach it – Artem Arkhipov Sep 03 '19 at 09:00
  • A working example would be great. Only problem is I wouldn’t know distance considering the user always has the option of pausing the song at any given moment. – GROVER. Sep 03 '19 at 09:04
0

You have to wait for the css transition to end before you set new width. So you need to use transitionend event. Here is one example:

var percentDone = 0;
var elapsed =  document.getElementsByClassName("elapsed")[0];
setProgress = function() {
    percentDone++;
    if( percentDone<=100 ) {
        elapsed.style.width = percentDone + "%";
    };
};
elapsed.addEventListener('transitionend', setProgress);
setProgress();
.elapsed-container{
    width: 100%;
    height: 10px;
    background: grey;
}
.elapsed{
    left: 0;
    width: 0;
    height: 100%;
    background: red;
    transition: width 33ms linear;
}
<div class="elapsed-container">
    <div class="elapsed"></div>
</div>

You can see it working on JSFiddle

skobaljic
  • 9,379
  • 1
  • 25
  • 51
  • This is very smooth! Do you know what support is like on the transitioned event? – GROVER. Sep 03 '19 at 09:09
  • It is [well supported](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/transitionend_event#Browser_compatibility), also on [caniuse](https://caniuse.com/#feat=css-transitions), see below: **Support listed is for transition properties as well as the transitionend event.**. – skobaljic Sep 05 '19 at 08:38