Skip to content

Instantly share code, notes, and snippets.

@james2doyle
Last active October 25, 2024 14:12
Show Gist options
  • Save james2doyle/5694700 to your computer and use it in GitHub Desktop.
Save james2doyle/5694700 to your computer and use it in GitHub Desktop.
a native scrollTo function in javascript that uses requestAnimationFrame and easing for animation
// easing functions http://goo.gl/5HLl8
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) {
return c/2*t*t + b
}
t--;
return -c/2 * (t*(t-2) - 1) + b;
};
Math.easeInCubic = function(t, b, c, d) {
var tc = (t/=d)*t*t;
return b+c*(tc);
};
Math.inOutQuintic = function(t, b, c, d) {
var ts = (t/=d)*t,
tc = ts*t;
return b+c*(6*tc*ts + -15*ts*ts + 10*tc);
};
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
var requestAnimFrame = (function(){
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); };
})();
function scrollTo(to, callback, duration) {
// because it's so fucking difficult to detect the scrolling element, just move them all
function move(amount) {
document.documentElement.scrollTop = amount;
document.body.parentNode.scrollTop = amount;
document.body.scrollTop = amount;
}
function position() {
return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop;
}
var start = position(),
change = to - start,
currentTime = 0,
increment = 20;
duration = (typeof(duration) === 'undefined') ? 500 : duration;
var animateScroll = function() {
// increment the time
currentTime += increment;
// find the value with the quadratic in-out easing function
var val = Math.easeInOutQuad(currentTime, start, change, duration);
// move the document.body
move(val);
// do the animation unless its over
if (currentTime < duration) {
requestAnimFrame(animateScroll);
} else {
if (callback && typeof(callback) === 'function') {
// the animation is done so lets callback
callback();
}
}
};
animateScroll();
}
@ostrgard
Copy link

"because it's so ****ing difficult to detect the scrolling element, just move them all".

True! Thanks for this Gist 👍

@yckart
Copy link

yckart commented Apr 9, 2016

"because it's so fucking difficult to detect the scrolling element, just move them all"

False! Thanks for this Gist 👍


document.scrollingElement

...for legacies:

do {
  if (element.clientHeight < element.scrollHeight || element.clientWidth < element.scrollWidth) {
    return element
  }
} while (element = element.parentNode)

@japalo
Copy link

japalo commented Jun 13, 2017

I quickly translated this for Angular 2 and/or Angular 4:

import { Injectable, ElementRef } from '@angular/core';

@Injectable()
export class ScrollToService {

    constructor() {}

    // easing functions http://goo.gl/5HLl8
    easeInOutQuad = function (t, b, c, d) {
        t /= d/2;
        if (t < 1) {
            return c/2*t*t + b
        }
        t--;
        return -c/2 * (t*(t-2) - 1) + b;
    };

    easeInCubic = function(t, b, c, d) {
        var tc = (t/=d)*t*t;
        return b+c*(tc);
    };

    inOutQuintic = function(t, b, c, d) {
        var ts = (t/=d)*t,
        tc = ts*t;
        return b+c*(6*tc*ts + -15*ts*ts + 10*tc);
    };

    scrollTo(to: number, callback: any, duration: number) {
        function move(amount) {
            document.documentElement.scrollTop = amount;
            (<Element>document.body.parentNode).scrollTop = amount;
            document.body.scrollTop = amount;
        }
        function position() {
            return document.documentElement.scrollTop || (<Element>document.body.parentNode).scrollTop || document.body.scrollTop;
        }
        let animationFrameReqFunc = window.requestAnimationFrame || window.webkitRequestAnimationFrame || (<any>window).mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); };
        let start = position(),
        change = to - start,
        currentTime = 0,
        increment = 20;
        duration = (typeof(duration) === 'undefined') ? 500 : duration;
        let animateScroll = () => {
            // increment the time
            currentTime += increment;
            // find the value with the quadratic in-out easing function
            let val = this.easeInCubic(currentTime, start, change, duration);
            // move the document.body
            move(val);
            // do the animation unless its over
            if (currentTime < duration) {
                animationFrameReqFunc(animateScroll);
            } else {
                if (callback && typeof(callback) === 'function') {
                    // the animation is done so lets callback
                    callback();
                }
            }
        };
        animateScroll();
    }
}

@tranghaviet
Copy link

I found an other solution much simpler:

/**
 *  Scroll to element, for now it scroll to Top.
 * @param duration default 1000ms
 */
export const scrollToElement = (duration = 1000) => {
  let cosParameter = window.scrollY / 2
  let scrollCount = 0
  let oldTimestamp = performance.now()

  function step (newTimestamp) {
    scrollCount += Math.PI / (duration / (newTimestamp - oldTimestamp))
    if (scrollCount >= Math.PI) window.scrollTo(0, 0)
    if (window.scrollY === 0) return
    window.scrollTo(0, Math.round(cosParameter + cosParameter * Math.cos(scrollCount)))
    oldTimestamp = newTimestamp
    window.requestAnimationFrame(step)
  }

  window.requestAnimationFrame(step)
}

@dminkovsky
Copy link

dminkovsky commented Mar 14, 2019

shouldn't time be calculated from the timestamp passed to the raf callback? As in the example https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame?

@marinoguerieri
Copy link

Thank you man, you saved my ass after I run out of options for pulling out cross browser compatible animation. I lost like 5 hours in frustration, and your solution works best.

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