-
Star
(336)
You must be signed in to star a gist -
Fork
(54)
You must be signed in to fork a gist
-
-
Save joshbeckman/6764939 to your computer and use it in GitHub Desktop.
document.getElementsByTagName('button')[0].onclick = function () { | |
scrollTo(document.body, 0, 1250); | |
} | |
function scrollTo(element, to, duration) { | |
var start = element.scrollTop, | |
change = to - start, | |
currentTime = 0, | |
increment = 20; | |
var animateScroll = function(){ | |
currentTime += increment; | |
var val = Math.easeInOutQuad(currentTime, start, change, duration); | |
element.scrollTop = val; | |
if(currentTime < duration) { | |
setTimeout(animateScroll, increment); | |
} | |
}; | |
animateScroll(); | |
} | |
//t = current time | |
//b = start value | |
//c = change in value | |
//d = duration | |
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; | |
}; |
Here's a typescript version if anyone is interested:
type EaseInOutQuadOptions = {
currentTime: number;
start: number;
change: number;
duration: number;
};
const easeInOutQuad = ({
currentTime,
start,
change,
duration,
}: EaseInOutQuadOptions) => {
let newCurrentTime = currentTime;
newCurrentTime /= duration / 2;
if (newCurrentTime < 1) {
return (change / 2) * newCurrentTime * newCurrentTime + start;
}
newCurrentTime -= 1;
return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};
type SmoothScrollOptions = {
duration: number;
element: HTMLElement;
to: number;
};
export default function smoothScroll({
duration,
element,
to,
}: SmoothScrollOptions) {
const start = element.scrollTop;
const change = to - start;
const startDate = new Date().getTime();
const animateScroll = () => {
const currentDate = new Date().getTime();
const currentTime = currentDate - startDate;
element.scrollTop = easeInOutQuad({
currentTime,
start,
change,
duration,
});
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
element.scrollTop = to;
}
};
animateScroll();
}
function scrollTo(element, durationTop, durationLeft) {
var startTop = element.scrollTop,
startLeft = element.scrollLeft,
changeTop = durationTop - startTop,
changeLeft = durationLeft - startLeft,//window.scrollX - startLeft,
currentTimeTop = 0,
currentTimeLeft = 0,
increment = 20;
var animateScrollTop = function(){
currentTimeTop += increment;
var val = Math.easeInOutQuad(currentTimeTop, startTop, changeTop, durationTop);
element.scrollTop = val;
if(currentTimeTop < durationTop) {
setTimeout(animateScrollTop, increment);
}
};
var animateScrollLeft = function(){
currentTimeLeft += increment;
var val = Math.easeInOutQuad(currentTimeLeft, startLeft, changeLeft, durationLeft);
element.scrollLeft = val;
if(currentTimeLeft < durationLeft) {
setTimeout(animateScrollLeft, increment);
}
};
animateScrollTop();
animateScrollLeft();
}
//t = current time
//b = start value
//c = change in value
//d = duration
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2tt + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
};
@andjosh Thank you!! Very useful!!
Thank you for this!!! Spent all day looking for a solution.
Thanks!!!
دعای خیر جهانیان پشت سرت :)))
Works Great. Thanks!!
Just a heads up that supplying 0
duration will make Math.easeInOutQuad() return the value -Infinity
and supplying 1
duration will make it return large negative numbers. If you need to supply 0
duration, an easy fix would be to add an extra if statement at the start of this function returning the original supplied duration.
Math.easeInOutQuad = function (t, b, c, d) {
if (d <= 0)
return c;
...
}
Of course, the better way may be to just set scrollTop instead of using this function altogether.
Nice resource, is very useful 🥇
Here's a typescript version if anyone is interested:
type EaseInOutQuadOptions = { currentTime: number; start: number; change: number; duration: number; }; const easeInOutQuad = ({ currentTime, start, change, duration, }: EaseInOutQuadOptions) => { let newCurrentTime = currentTime; newCurrentTime /= duration / 2; if (newCurrentTime < 1) { return (change / 2) * newCurrentTime * newCurrentTime + start; } newCurrentTime -= 1; return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; }; type SmoothScrollOptions = { duration: number; element: HTMLElement; to: number; }; export default function smoothScroll({ duration, element, to, }: SmoothScrollOptions) { const start = element.scrollTop; const change = to - start; const startDate = new Date().getTime(); const animateScroll = () => { const currentDate = new Date().getTime(); const currentTime = currentDate - startDate; element.scrollTop = easeInOutQuad({ currentTime, start, change, duration, }); if (currentTime < duration) { requestAnimationFrame(animateScroll); } else { element.scrollTop = to; } }; animateScroll(); }
Small update... and extend about direction / type
interface EaseInOutQuadOptions {
currentTime: number;
start: number;
change: number;
duration: number;
}
const easeInOutQuad = (currentTime, start, change, duration): EaseInOutQuadOptions => {
let newCurrentTime = currentTime;
newCurrentTime /= duration / 2;
if (newCurrentTime < 1) {
return (change / 2) * newCurrentTime * newCurrentTime + start;
}
newCurrentTime -= 1;
return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};
interface SmoothScrollOptions {
duration: number;
element: HTMLElement;
to: number;
type: 'scrollTop' | 'scrollLeft';
}
const smoothScroll = (duration, element, to, type = 'scrollTop'): SmoothScrollOptions => {
const start = element[type];
const change = to - start;
const startDate = new Date().getTime();
const animateScroll = () => {
const currentDate = new Date().getTime();
const currentTime = currentDate - startDate;
element[type] = easeInOutQuad(currentTime, start, change, duration);
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
element[type] = to;
}
};
animateScroll();
return null;
};
export { smoothScroll };
@KarloZKvasin I think your update is not properly typed. easeInOutQuad
is set to return an EaseInOutQuadOptions
. Same goes for smoothScroll
, set to return a SmoothScrollOptions
.
You should dump these interfaces and type each function argument instead.
Updated:
const easeInOutQuad = (
currentTime: number,
start: number,
change: number,
duration: number,
): number => {
let newCurrentTime = currentTime;
newCurrentTime /= duration / 2;
if (newCurrentTime < 1) {
return (change / 2) * newCurrentTime * newCurrentTime + start;
}
newCurrentTime -= 1;
return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};
const smoothScroll = (
duration: number,
element: HTMLElement,
to: number,
property: 'scrollTop' | 'scrollLeft',
): void => {
const start = element[property];
const change = to - start;
const startDate = new Date().getTime();
const animateScroll = () => {
const currentDate = new Date().getTime();
const currentTime = currentDate - startDate;
element[property] = easeInOutQuad(currentTime, start, change, duration);
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
element[property] = to;
}
};
animateScroll();
};
export { smoothScroll };
how do work with scroll-snap-type: x mandatory;
const scrollTo = function(to, duration) { const element = document.scrollingElement || document.documentElement, start = element.scrollTop, change = to - start, startDate = +new Date(), // t = current time // b = start value // c = change in value // d = duration 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; }, animateScroll = function() { const currentDate = +new Date(); const currentTime = currentDate - startDate; element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration)); if(currentTime < duration) { requestAnimationFrame(animateScroll); } else { element.scrollTop = to; } }; animateScroll(); };
Here's the code a bit modernized. Now it's a lot smoother and works in Safari as well. If you don't work with babel then replace
const
withvar
.
Thanks. it is a lot more soomother
I also changed it to use performance.now()
:
var scrollTo = function(to, duration) {
var element = document.scrollingElement || document.documentElement,
start = element.scrollTop,
change = to - start,
startTs = performance.now(),
// t = current time
// b = start value
// c = change in value
// d = duration
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;
},
animateScroll = function(ts) {
var currentTime = ts - startTs;
element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration));
if(currentTime < duration) {
requestAnimationFrame(animateScroll);
}
else {
element.scrollTop = to;
}
};
requestAnimationFrame(animateScroll);
};
For the utility if you just want to paste this around, here is minimized version of the requestAnimationFrame
version:
var scrollTo=function(l,t){var c=document.scrollingElement||document.documentElement,m=c.scrollTop,a=l-m,s=performance.now(),i=function(o){var n,e,r=o-s;c.scrollTop=parseInt((n=r,e=m,o=a,(n/=t/2)<1?o/2*n*n+e:-o/2*(--n*(n-2)-1)+e)),r<t?requestAnimationFrame(i):c.scrollTop=l};requestAnimationFrame(i)};
Then use it like the others
scrollTo(150, 1000);
How would you go about making a linear function, no easing, just scroll down evenly in a set duration? Thank you!
@Alesvetina you would change the easing function for easeInOutQuad = function (t) { ...
to simply this:
function (t) { return t; }
Here's An updated version that satisfies much more strict ESLint parameters, plus new ES syntax:
export const animateScrollTo = (to, duration) => {
const element = document.scrollingElement || document.documentElement;
const start = element.scrollTop;
const change = to - start;
const startDate = +new Date();
// t = current time
// b = start value
// c = change in value
// d = duration
const easeInOutQuad = (t, b, c, d) => {
let t2 = t;
t2 /= d / 2;
if (t2 < 1) return (c / 2) * t2 * t2 + b;
t2 -= 1;
return (-c / 2) * (t2 * (t2 - 2) - 1) + b;
};
const animateScroll = () => {
const currentDate = +new Date();
const currentTime = currentDate - startDate;
element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration), 10);
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
element.scrollTop = to;
}
};
animateScroll();
};
Hi, an alternative version with top and left props (typescript).
const scrollTo = ({element, top, left, duration}: {
element: HTMLElement,
top: number,
left: number,
duration: number,
}) => {
const startTop = element.scrollTop;
const startLeft = element.scrollLeft;
const changeTop = top - startTop;
const changeLeft = left - startLeft;
const startDate = new Date().getTime();
const animateScroll = function(){
const currentDate = new Date().getTime();
const currentTime = currentDate - startDate;
element.scrollTop = easeInOutQuad(currentTime, startTop, changeTop, duration);
element.scrollLeft = easeInOutQuad(currentTime, startLeft, changeLeft, duration);
if(currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
element.scrollTop = top;
element.scrollLeft = left;
}
};
animateScroll();
}
Has anyone added more easing functions?