smooth-scroll.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. // SmoothScroll for websites v1.2.1
  2. // Licensed under the terms of the MIT license.
  3. // https://gist.github.com/galambalazs/6477177/
  4. // People involved
  5. // - Balazs Galambosi (maintainer)
  6. // - Michael Herf (Pulse Algorithm)
  7. (function(){
  8. // Scroll Variables (tweakable)
  9. var defaultOptions = {
  10. // Scrolling Core
  11. frameRate : 150, // [Hz]
  12. animationTime : 400, // [px]
  13. stepSize : 120, // [px]
  14. // Pulse (less tweakable)
  15. // ratio of "tail" to "acceleration"
  16. pulseAlgorithm : true,
  17. pulseScale : 8,
  18. pulseNormalize : 1,
  19. // Acceleration
  20. accelerationDelta : 20, // 20
  21. accelerationMax : 1, // 1
  22. // Keyboard Settings
  23. keyboardSupport : true, // option
  24. arrowScroll : 50, // [px]
  25. // Other
  26. touchpadSupport : true,
  27. fixedBackground : true,
  28. excluded : ""
  29. };
  30. var options = defaultOptions;
  31. // Other Variables
  32. var isExcluded = false;
  33. var isFrame = false;
  34. var direction = { x: 0, y: 0 };
  35. var initDone = false;
  36. var root = document.documentElement;
  37. var activeElement;
  38. var observer;
  39. var deltaBuffer = [ 120, 120, 120 ];
  40. var key = { left: 37, up: 38, right: 39, down: 40, spacebar: 32,
  41. pageup: 33, pagedown: 34, end: 35, home: 36 };
  42. /***********************************************
  43. * SETTINGS
  44. ***********************************************/
  45. var options = defaultOptions;
  46. /***********************************************
  47. * INITIALIZE
  48. ***********************************************/
  49. /**
  50. * Tests if smooth scrolling is allowed. Shuts down everything if not.
  51. */
  52. function initTest() {
  53. var disableKeyboard = false;
  54. // disable keyboard support if anything above requested it
  55. if (disableKeyboard) {
  56. removeEvent("keydown", keydown);
  57. }
  58. if (options.keyboardSupport && !disableKeyboard) {
  59. addEvent("keydown", keydown);
  60. }
  61. }
  62. /**
  63. * Sets up scrolls array, determines if frames are involved.
  64. */
  65. function init() {
  66. if (!document.body) return;
  67. var body = document.body;
  68. var html = document.documentElement;
  69. var windowHeight = window.innerHeight;
  70. var scrollHeight = body.scrollHeight;
  71. // check compat mode for root element
  72. root = (document.compatMode.indexOf('CSS') >= 0) ? html : body;
  73. activeElement = body;
  74. initTest();
  75. initDone = true;
  76. // Checks if this script is running in a frame
  77. if (top != self) {
  78. isFrame = true;
  79. }
  80. /**
  81. * This fixes a bug where the areas left and right to
  82. * the content does not trigger the onmousewheel event
  83. * on some pages. e.g.: html, body { height: 100% }
  84. */
  85. else if (scrollHeight > windowHeight &&
  86. (body.offsetHeight <= windowHeight ||
  87. html.offsetHeight <= windowHeight)) {
  88. // DOMChange (throttle): fix height
  89. var pending = false;
  90. var refresh = function () {
  91. if (!pending && html.scrollHeight != document.height) {
  92. pending = true; // add a new pending action
  93. setTimeout(function () {
  94. html.style.height = document.height + 'px';
  95. pending = false;
  96. }, 500); // act rarely to stay fast
  97. }
  98. };
  99. html.style.height = 'auto';
  100. setTimeout(refresh, 10);
  101. // clearfix
  102. if (root.offsetHeight <= windowHeight) {
  103. var underlay = document.createElement("div");
  104. underlay.style.clear = "both";
  105. body.appendChild(underlay);
  106. }
  107. }
  108. // disable fixed background
  109. if (!options.fixedBackground && !isExcluded) {
  110. body.style.backgroundAttachment = "scroll";
  111. html.style.backgroundAttachment = "scroll";
  112. }
  113. }
  114. /************************************************
  115. * SCROLLING
  116. ************************************************/
  117. var que = [];
  118. var pending = false;
  119. var lastScroll = +new Date;
  120. /**
  121. * Pushes scroll actions to the scrolling queue.
  122. */
  123. function scrollArray(elem, left, top, delay) {
  124. delay || (delay = 1000);
  125. directionCheck(left, top);
  126. if (options.accelerationMax != 1) {
  127. var now = +new Date;
  128. var elapsed = now - lastScroll;
  129. if (elapsed < options.accelerationDelta) {
  130. var factor = (1 + (30 / elapsed)) / 2;
  131. if (factor > 1) {
  132. factor = Math.min(factor, options.accelerationMax);
  133. left *= factor;
  134. top *= factor;
  135. }
  136. }
  137. lastScroll = +new Date;
  138. }
  139. // push a scroll command
  140. que.push({
  141. x: left,
  142. y: top,
  143. lastX: (left < 0) ? 0.99 : -0.99,
  144. lastY: (top < 0) ? 0.99 : -0.99,
  145. start: +new Date
  146. });
  147. // don't act if there's a pending queue
  148. if (pending) {
  149. return;
  150. }
  151. var scrollWindow = (elem === document.body);
  152. var step = function (time) {
  153. var now = +new Date;
  154. var scrollX = 0;
  155. var scrollY = 0;
  156. for (var i = 0; i < que.length; i++) {
  157. var item = que[i];
  158. var elapsed = now - item.start;
  159. var finished = (elapsed >= options.animationTime);
  160. // scroll position: [0, 1]
  161. var position = (finished) ? 1 : elapsed / options.animationTime;
  162. // easing [optional]
  163. if (options.pulseAlgorithm) {
  164. position = pulse(position);
  165. }
  166. // only need the difference
  167. var x = (item.x * position - item.lastX) >> 0;
  168. var y = (item.y * position - item.lastY) >> 0;
  169. // add this to the total scrolling
  170. scrollX += x;
  171. scrollY += y;
  172. // update last values
  173. item.lastX += x;
  174. item.lastY += y;
  175. // delete and step back if it's over
  176. if (finished) {
  177. que.splice(i, 1); i--;
  178. }
  179. }
  180. // scroll left and top
  181. if (scrollWindow) {
  182. window.scrollBy(scrollX, scrollY);
  183. }
  184. else {
  185. if (scrollX) elem.scrollLeft += scrollX;
  186. if (scrollY) elem.scrollTop += scrollY;
  187. }
  188. // clean up if there's nothing left to do
  189. if (!left && !top) {
  190. que = [];
  191. }
  192. if (que.length) {
  193. requestFrame(step, elem, (delay / options.frameRate + 1));
  194. } else {
  195. pending = false;
  196. }
  197. };
  198. // start a new queue of actions
  199. requestFrame(step, elem, 0);
  200. pending = true;
  201. }
  202. /***********************************************
  203. * EVENTS
  204. ***********************************************/
  205. /**
  206. * Mouse wheel handler.
  207. * @param {Object} event
  208. */
  209. function wheel(event) {
  210. if (!initDone) {
  211. init();
  212. }
  213. var target = event.target;
  214. var overflowing = overflowingAncestor(target);
  215. // use default if there's no overflowing
  216. // element or default action is prevented
  217. if (!overflowing || event.defaultPrevented ||
  218. isNodeName(activeElement, "embed") ||
  219. (isNodeName(target, "embed") && /\.pdf/i.test(target.src))) {
  220. return true;
  221. }
  222. var deltaX = event.wheelDeltaX || 0;
  223. var deltaY = event.wheelDeltaY || 0;
  224. // use wheelDelta if deltaX/Y is not available
  225. if (!deltaX && !deltaY) {
  226. deltaY = event.wheelDelta || 0;
  227. }
  228. // check if it's a touchpad scroll that should be ignored
  229. if (!options.touchpadSupport && isTouchpad(deltaY)) {
  230. return true;
  231. }
  232. // scale by step size
  233. // delta is 120 most of the time
  234. // synaptics seems to send 1 sometimes
  235. if (Math.abs(deltaX) > 1.2) {
  236. deltaX *= options.stepSize / 120;
  237. }
  238. if (Math.abs(deltaY) > 1.2) {
  239. deltaY *= options.stepSize / 120;
  240. }
  241. scrollArray(overflowing, -deltaX, -deltaY);
  242. event.preventDefault();
  243. }
  244. /**
  245. * Keydown event handler.
  246. * @param {Object} event
  247. */
  248. function keydown(event) {
  249. var target = event.target;
  250. var modifier = event.ctrlKey || event.altKey || event.metaKey ||
  251. (event.shiftKey && event.keyCode !== key.spacebar);
  252. // do nothing if user is editing text
  253. // or using a modifier key (except shift)
  254. // or in a dropdown
  255. if ( /input|textarea|select|embed/i.test(target.nodeName) ||
  256. target.isContentEditable ||
  257. event.defaultPrevented ||
  258. modifier ) {
  259. return true;
  260. }
  261. // spacebar should trigger button press
  262. if (isNodeName(target, "button") &&
  263. event.keyCode === key.spacebar) {
  264. return true;
  265. }
  266. var shift, x = 0, y = 0;
  267. var elem = overflowingAncestor(activeElement);
  268. var clientHeight = elem.clientHeight;
  269. if (elem == document.body) {
  270. clientHeight = window.innerHeight;
  271. }
  272. switch (event.keyCode) {
  273. case key.up:
  274. y = -options.arrowScroll;
  275. break;
  276. case key.down:
  277. y = options.arrowScroll;
  278. break;
  279. case key.spacebar: // (+ shift)
  280. shift = event.shiftKey ? 1 : -1;
  281. y = -shift * clientHeight * 0.9;
  282. break;
  283. case key.pageup:
  284. y = -clientHeight * 0.9;
  285. break;
  286. case key.pagedown:
  287. y = clientHeight * 0.9;
  288. break;
  289. case key.home:
  290. y = -elem.scrollTop;
  291. break;
  292. case key.end:
  293. var damt = elem.scrollHeight - elem.scrollTop - clientHeight;
  294. y = (damt > 0) ? damt+10 : 0;
  295. break;
  296. case key.left:
  297. x = -options.arrowScroll;
  298. break;
  299. case key.right:
  300. x = options.arrowScroll;
  301. break;
  302. default:
  303. return true; // a key we don't care about
  304. }
  305. scrollArray(elem, x, y);
  306. event.preventDefault();
  307. }
  308. /**
  309. * Mousedown event only for updating activeElement
  310. */
  311. function mousedown(event) {
  312. activeElement = event.target;
  313. }
  314. /***********************************************
  315. * OVERFLOW
  316. ***********************************************/
  317. var cache = {}; // cleared out every once in while
  318. setInterval(function () { cache = {}; }, 10 * 1000);
  319. var uniqueID = (function () {
  320. var i = 0;
  321. return function (el) {
  322. return el.uniqueID || (el.uniqueID = i++);
  323. };
  324. })();
  325. function setCache(elems, overflowing) {
  326. for (var i = elems.length; i--;)
  327. cache[uniqueID(elems[i])] = overflowing;
  328. return overflowing;
  329. }
  330. function overflowingAncestor(el) {
  331. var elems = [];
  332. var rootScrollHeight = root.scrollHeight;
  333. do {
  334. var cached = cache[uniqueID(el)];
  335. if (cached) {
  336. return setCache(elems, cached);
  337. }
  338. elems.push(el);
  339. if (rootScrollHeight === el.scrollHeight) {
  340. if (!isFrame || root.clientHeight + 10 < rootScrollHeight) {
  341. return setCache(elems, document.body); // scrolling root in WebKit
  342. }
  343. } else if (el.clientHeight + 10 < el.scrollHeight) {
  344. overflow = getComputedStyle(el, "").getPropertyValue("overflow-y");
  345. if (overflow === "scroll" || overflow === "auto") {
  346. return setCache(elems, el);
  347. }
  348. }
  349. } while (el = el.parentNode);
  350. }
  351. /***********************************************
  352. * HELPERS
  353. ***********************************************/
  354. function addEvent(type, fn, bubble) {
  355. window.addEventListener(type, fn, (bubble||false));
  356. }
  357. function removeEvent(type, fn, bubble) {
  358. window.removeEventListener(type, fn, (bubble||false));
  359. }
  360. function isNodeName(el, tag) {
  361. return (el.nodeName||"").toLowerCase() === tag.toLowerCase();
  362. }
  363. function directionCheck(x, y) {
  364. x = (x > 0) ? 1 : -1;
  365. y = (y > 0) ? 1 : -1;
  366. if (direction.x !== x || direction.y !== y) {
  367. direction.x = x;
  368. direction.y = y;
  369. que = [];
  370. lastScroll = 0;
  371. }
  372. }
  373. var deltaBufferTimer;
  374. function isTouchpad(deltaY) {
  375. if (!deltaY) return;
  376. deltaY = Math.abs(deltaY)
  377. deltaBuffer.push(deltaY);
  378. deltaBuffer.shift();
  379. clearTimeout(deltaBufferTimer);
  380. var allDivisable = (isDivisible(deltaBuffer[0], 120) &&
  381. isDivisible(deltaBuffer[1], 120) &&
  382. isDivisible(deltaBuffer[2], 120));
  383. return !allDivisable;
  384. }
  385. function isDivisible(n, divisor) {
  386. return (Math.floor(n / divisor) == n / divisor);
  387. }
  388. var requestFrame = (function () {
  389. return window.requestAnimationFrame ||
  390. window.webkitRequestAnimationFrame ||
  391. function (callback, element, delay) {
  392. window.setTimeout(callback, delay || (1000/60));
  393. };
  394. })();
  395. /***********************************************
  396. * PULSE
  397. ***********************************************/
  398. /**
  399. * Viscous fluid with a pulse for part and decay for the rest.
  400. * - Applies a fixed force over an interval (a damped acceleration), and
  401. * - Lets the exponential bleed away the velocity over a longer interval
  402. * - Michael Herf, http://stereopsis.com/stopping/
  403. */
  404. function pulse_(x) {
  405. var val, start, expx;
  406. // test
  407. x = x * options.pulseScale;
  408. if (x < 1) { // acceleartion
  409. val = x - (1 - Math.exp(-x));
  410. } else { // tail
  411. // the previous animation ended here:
  412. start = Math.exp(-1);
  413. // simple viscous drag
  414. x -= 1;
  415. expx = 1 - Math.exp(-x);
  416. val = start + (expx * (1 - start));
  417. }
  418. return val * options.pulseNormalize;
  419. }
  420. function pulse(x) {
  421. if (x >= 1) return 1;
  422. if (x <= 0) return 0;
  423. if (options.pulseNormalize == 1) {
  424. options.pulseNormalize /= pulse_(1);
  425. }
  426. return pulse_(x);
  427. }
  428. var isChrome = /chrome/i.test(window.navigator.userAgent);
  429. var wheelEvent = null;
  430. if ("onwheel" in document.createElement("div"))
  431. wheelEvent = "wheel";
  432. else if ("onmousewheel" in document.createElement("div"))
  433. wheelEvent = "mousewheel";
  434. if (wheelEvent && isChrome) {
  435. addEvent(wheelEvent, wheel);
  436. addEvent("mousedown", mousedown);
  437. addEvent("load", init);
  438. }
  439. })();