// smoothscroll for websites v1.2.1 // licensed under the terms of the mit license. // people involved // - balazs galambosi (maintainer) // - michael herf (pulse algorithm) (function(){ // scroll variables (tweakable) var defaultoptions = { // scrolling core framerate : 300, // [hz] animationtime : 1200, // [px] stepsize : 120, // [px] // pulse (less tweakable) // ratio of "tail" to "acceleration" pulsealgorithm : true, pulsescale : 8, pulsenormalize : 1, // acceleration accelerationdelta : 20, // 20 accelerationmax : 1, // 1 // keyboard settings keyboardsupport : true, // option arrowscroll : 50, // [px] // other touchpadsupport : true, fixedbackground : true, excluded : "" }; var options = defaultoptions; // other variables var isexcluded = false; var isframe = false; var direction = { x: 0, y: 0 }; var initdone = false; var root = document.documentelement; var activeelement; var observer; var deltabuffer = [ 120, 120, 120 ]; var key = { left: 37, up: 38, right: 39, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 }; /*********************************************** * settings ***********************************************/ var options = defaultoptions; /*********************************************** * initialize ***********************************************/ /** * tests if smooth scrolling is allowed. shuts down everything if not. */ function inittest() { var disablekeyboard = false; // disable keyboard support if anything above requested it if (disablekeyboard) { removeevent("keydown", keydown); } if (options.keyboardsupport && !disablekeyboard) { addevent("keydown", keydown); } } /** * sets up scrolls array, determines if frames are involved. */ function init() { if (!document.body) return; var body = document.body; var html = document.documentelement; var windowheight = window.innerheight; var scrollheight = body.scrollheight; // check compat mode for root element root = (document.compatmode.indexof('css') >= 0) ? html : body; activeelement = body; inittest(); initdone = true; // checks if this script is running in a frame if (top != self) { isframe = true; } /** * this fixes a bug where the areas left and right to * the content does not trigger the onmousewheel event * on some pages. e.g.: html, body { height: 100% } */ else if (scrollheight > windowheight && (body.offsetheight <= windowheight || html.offsetheight <= windowheight)) { // domchange (throttle): fix height var pending = false; var refresh = function () { if (!pending && html.scrollheight != document.height) { pending = true; // add a new pending action settimeout(function () { html.style.height = document.height + 'px'; pending = false; }, 500); // act rarely to stay fast } }; html.style.height = 'auto'; settimeout(refresh, 10); // clearfix if (root.offsetheight <= windowheight) { var underlay = document.createelement("div"); underlay.style.clear = "both"; body.appendchild(underlay); } } // disable fixed background if (!options.fixedbackground && !isexcluded) { body.style.backgroundattachment = "scroll"; html.style.backgroundattachment = "scroll"; } } /************************************************ * scrolling ************************************************/ var que = []; var pending = false; var lastscroll = +new date; /** * pushes scroll actions to the scrolling queue. */ function scrollarray(elem, left, top, delay) { delay || (delay = 1000); directioncheck(left, top); if (options.accelerationmax != 1) { var now = +new date; var elapsed = now - lastscroll; if (elapsed < options.accelerationdelta) { var factor = (1 + (30 / elapsed)) / 2; if (factor > 1) { factor = math.min(factor, options.accelerationmax); left *= factor; top *= factor; } } lastscroll = +new date; } // push a scroll command que.push({ x: left, y: top, lastx: (left < 0) ? 0.99 : -0.99, lasty: (top < 0) ? 0.99 : -0.99, start: +new date }); // don't act if there's a pending queue if (pending) { return; } var scrollwindow = (elem === document.body); var step = function (time) { var now = +new date; var scrollx = 0; var scrolly = 0; for (var i = 0; i < que.length; i++) { var item = que[i]; var elapsed = now - item.start; var finished = (elapsed >= options.animationtime); // scroll position: [0, 1] var position = (finished) ? 1 : elapsed / options.animationtime; // easing [optional] if (options.pulsealgorithm) { position = pulse(position); } // only need the difference var x = (item.x * position - item.lastx) >> 0; var y = (item.y * position - item.lasty) >> 0; // add this to the total scrolling scrollx += x; scrolly += y; // update last values item.lastx += x; item.lasty += y; // delete and step back if it's over if (finished) { que.splice(i, 1); i--; } } // scroll left and top if (scrollwindow) { window.scrollby(scrollx, scrolly); } else { if (scrollx) elem.scrollleft += scrollx; if (scrolly) elem.scrolltop += scrolly; } // clean up if there's nothing left to do if (!left && !top) { que = []; } if (que.length) { requestframe(step, elem, (delay / options.framerate + 1)); } else { pending = false; } }; // start a new queue of actions requestframe(step, elem, 0); pending = true; } /*********************************************** * events ***********************************************/ /** * mouse wheel handler. * @param {object} event */ function wheel(event) { if (!initdone) { init(); } var target = event.target; var overflowing = overflowingancestor(target); // use default if there's no overflowing // element or default action is prevented if (!overflowing || event.defaultprevented || isnodename(activeelement, "embed") || (isnodename(target, "embed") && /\.pdf/i.test(target.src))) { return true; } var deltax = event.wheeldeltax || 0; var deltay = event.wheeldeltay || 0; // use wheeldelta if deltax/y is not available if (!deltax && !deltay) { deltay = event.wheeldelta || 0; } // check if it's a touchpad scroll that should be ignored if (!options.touchpadsupport && istouchpad(deltay)) { return true; } // scale by step size // delta is 120 most of the time // synaptics seems to send 1 sometimes if (math.abs(deltax) > 1.2) { deltax *= options.stepsize / 120; } if (math.abs(deltay) > 1.2) { deltay *= options.stepsize / 120; } scrollarray(overflowing, -deltax, -deltay); event.preventdefault(); } /** * keydown event handler. * @param {object} event */ function keydown(event) { var target = event.target; var modifier = event.ctrlkey || event.altkey || event.metakey || (event.shiftkey && event.keycode !== key.spacebar); // do nothing if user is editing text // or using a modifier key (except shift) // or in a dropdown if ( /input|textarea|select|embed/i.test(target.nodename) || target.iscontenteditable || event.defaultprevented || modifier ) { return true; } // spacebar should trigger button press if (isnodename(target, "button") && event.keycode === key.spacebar) { return true; } var shift, x = 0, y = 0; var elem = overflowingancestor(activeelement); var clientheight = elem.clientheight; if (elem == document.body) { clientheight = window.innerheight; } switch (event.keycode) { case key.up: y = -options.arrowscroll; break; case key.down: y = options.arrowscroll; break; case key.spacebar: // (+ shift) shift = event.shiftkey ? 1 : -1; y = -shift * clientheight * 0.9; break; case key.pageup: y = -clientheight * 0.9; break; case key.pagedown: y = clientheight * 0.9; break; case key.home: y = -elem.scrolltop; break; case key.end: var damt = elem.scrollheight - elem.scrolltop - clientheight; y = (damt > 0) ? damt+10 : 0; break; case key.left: x = -options.arrowscroll; break; case key.right: x = options.arrowscroll; break; default: return true; // a key we don't care about } scrollarray(elem, x, y); event.preventdefault(); } /** * mousedown event only for updating activeelement */ function mousedown(event) { activeelement = event.target; } /*********************************************** * overflow ***********************************************/ var cache = {}; // cleared out every once in while setinterval(function () { cache = {}; }, 10 * 1000); var uniqueid = (function () { var i = 0; return function (el) { return el.uniqueid || (el.uniqueid = i++); }; })(); function setcache(elems, overflowing) { for (var i = elems.length; i--;) cache[uniqueid(elems[i])] = overflowing; return overflowing; } function overflowingancestor(el) { var elems = []; var rootscrollheight = root.scrollheight; do { var cached = cache[uniqueid(el)]; if (cached) { return setcache(elems, cached); } elems.push(el); if (rootscrollheight === el.scrollheight) { if (!isframe || root.clientheight + 10 < rootscrollheight) { return setcache(elems, document.body); // scrolling root in webkit } } else if (el.clientheight + 10 < el.scrollheight) { overflow = getcomputedstyle(el, "").getpropertyvalue("overflow-y"); if (overflow === "scroll" || overflow === "auto") { return setcache(elems, el); } } } while (el = el.parentnode); } /*********************************************** * helpers ***********************************************/ function addevent(type, fn, bubble) { window.addeventlistener(type, fn, (bubble||false)); } function removeevent(type, fn, bubble) { window.removeeventlistener(type, fn, (bubble||false)); } function isnodename(el, tag) { return (el.nodename||"").tolowercase() === tag.tolowercase(); } function directioncheck(x, y) { x = (x > 0) ? 1 : -1; y = (y > 0) ? 1 : -1; if (direction.x !== x || direction.y !== y) { direction.x = x; direction.y = y; que = []; lastscroll = 0; } } var deltabuffertimer; function istouchpad(deltay) { if (!deltay) return; deltay = math.abs(deltay) deltabuffer.push(deltay); deltabuffer.shift(); cleartimeout(deltabuffertimer); var alldivisable = (isdivisible(deltabuffer[0], 120) && isdivisible(deltabuffer[1], 120) && isdivisible(deltabuffer[2], 120)); return !alldivisable; } function isdivisible(n, divisor) { return (math.floor(n / divisor) == n / divisor); } var requestframe = (function () { return window.requestanimationframe || window.webkitrequestanimationframe || function (callback, element, delay) { window.settimeout(callback, delay || (1000/60)); }; })(); /*********************************************** * pulse ***********************************************/ /** * viscous fluid with a pulse for part and decay for the rest. * - applies a fixed force over an interval (a damped acceleration), and * - lets the exponential bleed away the velocity over a longer interval * - michael herf, http://stereopsis.com/stopping/ */ function pulse_(x) { var val, start, expx; // test x = x * options.pulsescale; if (x < 1) { // acceleartion val = x - (1 - math.exp(-x)); } else { // tail // the previous animation ended here: start = math.exp(-1); // simple viscous drag x -= 1; expx = 1 - math.exp(-x); val = start + (expx * (1 - start)); } return val * options.pulsenormalize; } function pulse(x) { if (x >= 1) return 1; if (x <= 0) return 0; if (options.pulsenormalize == 1) { options.pulsenormalize /= pulse_(1); } return pulse_(x); } var ischrome = /chrome/i.test(window.navigator.useragent); var wheelevent = null; if ("onwheel" in document.createelement("div")) wheelevent = "wheel"; else if ("onmousewheel" in document.createelement("div")) wheelevent = "mousewheel"; if (wheelevent && ischrome) { addevent(wheelevent, wheel); addevent("mousedown", mousedown); addevent("load", init); } })();