Ben Aston
Ben Aston

Reputation: 55729

Scrolling with an easing function

I have written a function that when invoked, will scroll the page downwards in a smooth fashion.

The problem is that the amount scrolled is not precise. It can be off by one pixel or so depending on the distance scrolled and the duration of the scroll.

Is there a better way to do this such that the scroll can move the exact number of desired pixels?

const EASING = BezierEasing(0, .1, .1, 1); // Example values.
const DURATION = 1000; // ms.

document.addEventListener('DOMContentLoaded', 
  () =>  {
    document.querySelector('#foo')
      .addEventListener('click', onClick, false);
});

function onClick(e) {
  scroll(400); // px.
  e.preventDefault();
  return false;
}

function scroll(distance) {
  var start, travelled, html;

  start = performance.now();
  travelled = 0;
  html = document.querySelector('html');

  (function move(now) {
    var fractionDone, dy, toMove;

    fractionDone = (now-start) / DURATION;

    if((1 - fractionDone) <= 0) {
      return; // Done!
    }

    if(window.scrollY + window.innerHeight 
      === html.offsetHeight) {
      return; // At bottom of page.
    }

    dy = ((EASING.get(fractionDone)) * distance);
    toMove = Math.floor((dy - travelled)); // `scrollBy` only accepts integers.

    if(toMove > 0) {
      window.scrollBy(0, toMove);
      travelled += toMove;
    }

    requestAnimationFrame(move);
  }(start));
}
<!DOCTYPE html>
<html>
  <head>
    <script src="https://rawgit.com/gre/bezier-easing/master/build.js"></script>
  </head>
  <body>
    <a href id="foo">Click Me!</a>
    <script>
      /* print some numbers to
           the DOM to help visualize 
           the scrolling */
      var body = document.querySelector('body');
      for(var x = 0; x < 50; x++) {  
        var div = document.createElement("div"); 
        var text = document.createTextNode(x);
        div.appendChild(text);
        body.appendChild(div);
      }
    </script>
  </body> 
</html>

Upvotes: 1

Views: 373

Answers (2)

Ben Aston
Ben Aston

Reputation: 55729

I decided to modify the done logic to move any remaining distance:

// ...
if((1 - fractionDone) <= 0) {
  window.scrollBy(0, (distance - travelled)); // Scroll any remaining distance.
  return; // Done!
}
// ...

Which, I think, solves the issue.

Upvotes: 0

Rooster
Rooster

Reputation: 10077

could you do something like this to account for the last iteration?

basically if toMove gets rounded down to 0, but distance hasnt been travelled yet, force it to do scroll one more?

if(toMove > 0 || (toMove == 0 && travelled != distance) {
  window.scrollBy(0, (toMove ? toMove : 1));
  travelled += toMove;
}

Upvotes: 1

Related Questions