You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
535 lines
14 KiB
535 lines
14 KiB
// 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 : 150, // [Hz] |
|
animationTime : 400, // [px] |
|
stepSize : 120, // [px] |
|
|
|
// Pulse (less tweakable) |
|
// ratio of "tail" to "acceleration" |
|
pulseAlgorithm : false, |
|
pulseScale : 8, |
|
pulseNormalize : 1, |
|
|
|
// Acceleration |
|
accelerationDelta : 10, // 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); |
|
} |
|
|
|
})(); |