Skip to content

Instantly share code, notes, and snippets.

@dezinezync
Last active January 28, 2021 09:17
Show Gist options
  • Star 62 You must be signed in to star a gist
  • Fork 23 You must be signed in to fork a gist
  • Save dezinezync/5487119 to your computer and use it in GitHub Desktop.
Save dezinezync/5487119 to your computer and use it in GitHub Desktop.
Vanilla Javascript Scrolling function for easing based scrolling
function scrollTo(Y, duration, easingFunction, callback) {
var start = Date.now(),
elem = document.documentElement.scrollTop?document.documentElement:document.body,
from = elem.scrollTop;
if(from === Y) {
callback();
return; /* Prevent scrolling to the Y point if already there */
}
function min(a,b) {
return a<b?a:b;
}
function scroll(timestamp) {
var currentTime = Date.now(),
time = min(1, ((currentTime - start) / duration)),
easedT = easingFunction(time);
elem.scrollTop = (easedT * (Y - from)) + from;
if(time < 1) requestAnimationFrame(scroll);
else
if(callback) callback();
}
requestAnimationFrame(scroll)
}
/* bits and bytes of the scrollTo function inspired by the works of Benjamin DeCock */
/*
* Easing Functions - inspired from http://gizma.com/easing/
* only considering the t value for the range [0, 1] => [0, 1]
*/
var easing = {
// no easing, no acceleration
linear: function (t) { return t },
// accelerating from zero velocity
easeInQuad: function (t) { return t*t },
// decelerating to zero velocity
easeOutQuad: function (t) { return t*(2-t) },
// acceleration until halfway, then deceleration
easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
// accelerating from zero velocity
easeInCubic: function (t) { return t*t*t },
// decelerating to zero velocity
easeOutCubic: function (t) { return (--t)*t*t+1 },
// acceleration until halfway, then deceleration
easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
// accelerating from zero velocity
easeInQuart: function (t) { return t*t*t*t },
// decelerating to zero velocity
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
// accelerating from zero velocity
easeInQuint: function (t) { return t*t*t*t*t },
// decelerating to zero velocity
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
}
/* Add the following to you main js file
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}()); */
@Polaris72
Copy link

First: this script is really good, works nice with little payload, well done!
But: When using addEventListener, like grayghostvisuals does, the browser performs the "normal" anchor-jump for a split second and then the nice scrolling takes place, via the script as it is supposed to be.

Any ideas how to prevent the first browser-scroll? I tried "onclick" on the container, instead of addEventListener, didn't work at all.

p.s.
firebug reported an "easedT = easingFunction(time); is not a function"-error on loading a page, so i added:

if (easingFunction)
{
    var currentTime = Date.now(),
    time = min(1, ((currentTime - start) / duration)),
    easedT = easingFunction(time);

    elem.scrollTop = (easedT * (Y - from)) + from;

    if(time < 1) requestAnimationFrame(scroll);
    else if(callback) callback();
}

to function "scroll" to prevent this, which does not affect my primary issue.

@deanwagman
Copy link

There seems to be a bug to where the document will not scroll if scrollTop is 0 in some browsers. And some browsers will always report 0 for scrollTop. Therefore, I've found a patch for line 4.

elem = (document.documentElement.scrollTop++ !== 0) ? document.documentElement : document.body,

This attempts to increment the position to see if changing it does anything. If it does, then use it, else use document.body.

Whew…

@deanwagman
Copy link

Turns out that wasn't a good fix either. Firefox didn't like the incrementer.

Instead you should set the scrollTop to itself plus one, then check it.

Like so:

                // Used to see which object we should scroll on
        document.documentElement.scrollTop = document.documentElement.scrollTop + 1;

        var start = Date.now(),
            elem = (document.documentElement.scrollTop !== 0) ? document.documentElement : document.body,
            from = elem.scrollTop,
...

Hope that helps.

@marcobiedermann
Copy link

Why not use the build it function Math.min on line 19 instead of creating your own function at line 12?

time = Math.min(1, ((currentTime - start) / duration)),

@pawelgrzybek
Copy link

This gist was very helpful for me. Because we ended up a bit confused about element detection I put all the things together.

function scrollIt(destination, duration = 200, easing = 'linear', callback) {
  // define timing functions
  let easings = {
    // no easing, no acceleration
    linear(t) {
      return t;
    },
    // accelerating from zero velocity
    easeInQuad(t) {
      return t * t;
    },
    // decelerating to zero velocity
    easeOutQuad(t) {
      return t * (2 - t);
    },
    // acceleration until halfway, then deceleration
    easeInOutQuad(t) {
      return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    },
    // accelerating from zero velocity
    easeInCubic(t) {
      return t * t * t;
    },
    // decelerating to zero velocity
    easeOutCubic(t) {
      return (--t) * t * t + 1;
    },
    // acceleration until halfway, then deceleration
    easeInOutCubic(t) {
      return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
    },
    // accelerating from zero velocity
    easeInQuart(t) {
      return t * t * t * t;
    },
    // decelerating to zero velocity
    easeOutQuart(t) {
      return 1 - (--t) * t * t * t;
    },
    // acceleration until halfway, then deceleration
    easeInOutQuart(t) {
      return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t;
    },
    // accelerating from zero velocity
    easeInQuint(t) {
      return t * t * t * t * t;
    },
    // decelerating to zero velocity
    easeOutQuint(t) {
      return 1 + (--t) * t * t * t * t;
    },
    // acceleration until halfway, then deceleration
    easeInOutQuint(t) {
      return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
    }
  };

  function checkElement() {
    // returns document.documentElement for chrome and safari
    // document.body for rest of the world
    document.documentElement.scrollTop += 1;
    let elm = (document.documentElement.scrollTop !== 0) ? document.documentElement : document.body;
    document.documentElement.scrollTop -= 1;
    return elm;
  }

  let element = checkElement();
  let start = element.scrollTop;
  let startTime = Date.now();

  function scroll() {
    let now = Date.now();
    let time = Math.min(1, ((now - startTime) / duration));
    let timeFunction = easings[easing](time);
    element.scrollTop = (timeFunction * (destination - start)) + start;

    if (element.scrollTop === destination) {
      callback;
      return;
    }
    requestAnimationFrame(scroll);
  }
  scroll();
}

And this is how to use it:

scrollIt(1000, 300, 'easeInQuad', console.log('done'));

or simple version with default arguments if you don't care about duration, easing and callback...

scrollIt(1000);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment