├── .gitignore ├── .gitattributes ├── gulpfile.js ├── bower.json ├── package.json ├── LICENSE.md ├── demos └── default.html ├── readme.md ├── dist └── scrollSnap.min.js └── src └── scripts └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/**/* binary -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let name = require('./package.json').moduleName 4 | let gulp = require('gulp') 5 | let tasks = require('@electerious/basictasks')(gulp, name) 6 | 7 | const scripts = tasks.scripts({ 8 | from : './src/scripts/main.js', 9 | to : './dist' 10 | }) 11 | 12 | const watch = function() { 13 | gulp.watch('./src/scripts/**/*.js', [ 'scripts' ]) 14 | } 15 | 16 | gulp.task('scripts', scripts) 17 | gulp.task('default', [ 'scripts' ]) 18 | gulp.task('watch', [ 'default' ], watch) -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollSnap", 3 | "authors": [ 4 | "Tobias Reich " 5 | ], 6 | "description": "Section-based scrolling for your site", 7 | "main": "dist/scrollSnap.min.js", 8 | "keywords": [ 9 | "parallax", 10 | "scroll", 11 | "scrolling", 12 | "section", 13 | "snap" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/electerious/scrollSnap", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/electerious/scrollSnap.git" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollsnap", 3 | "moduleName": "scrollSnap", 4 | "version": "1.3.3", 5 | "authors": [ 6 | "Tobias Reich " 7 | ], 8 | "description": "Section-based scrolling for your site", 9 | "main": "dist/scrollSnap.min.js", 10 | "keywords": [ 11 | "parallax", 12 | "scroll", 13 | "scrolling", 14 | "section", 15 | "snap" 16 | ], 17 | "scripts": { 18 | "start": "gulp watch", 19 | "compile": "gulp" 20 | }, 21 | "license": "MIT", 22 | "homepage": "https://github.com/electerious/scrollSnap", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/electerious/scrollSnap.git" 26 | }, 27 | "devDependencies": { 28 | "gulp": "^3.9.1", 29 | "@electerious/basictasks": "^1.1.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tobias Reich (http://electerious.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /demos/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scrollSnap Demo 6 | 7 | 8 | 9 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 | 48 | 61 | 62 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [Deprecated] scrollSnap 2 | 3 | Scroll, stop, snap. 4 | 5 | scrollSnap provides a hassle-free alternative to scroll-hijacking, allowing a section-based navigation without harming the user-experience. It's written in Vanilla JS and has zero dependencies. 6 | 7 | Tested with the latest versions of [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/new/), [Apple Safari](https://www.apple.com/safari/), [Google Chrome](https://www.google.com/chrome/browser/), [Microsoft Internet Explorer](http://windows.microsoft.com/en-us/internet-explorer/download-ie) (10+) and [Opera](http://www.opera.com/). 8 | 9 | ## Demos 10 | 11 | | Name | Description | Link | 12 | |:-----------|:------------|:------------| 13 | | Basic | Snaps to the nearest section | [Try it on CodePen](http://codepen.io/electerious/pen/gpxbZp) | 14 | 15 | ## Features 16 | 17 | - Works in all modern browsers 18 | - Zero dependencies 19 | - CommonJS and AMD support 20 | - Performance-optimized 21 | - Fluid animations 22 | 23 | ## Performance 24 | 25 | scrollSnap has been developed with performance in mind. It uses modern technologies to get the most of your browser: 26 | 27 | - Works without additional libraries to keep your site slim 28 | - Written in ES2015 and transformed to ES5 using [Babel](https://babeljs.io) 29 | - `requestAnimationFrame` for fluid animations 30 | - Size-calculations will be performed at start and after a defined scroll-delay. All data gets cached to avoid unnecessary recalculations. 31 | 32 | ## Installation 33 | 34 | We recommend to install scrollSnap using [Bower](http://bower.io/) or [npm](https://npmjs.com). 35 | 36 | bower install scrollSnap 37 | npm install scrollsnap 38 | 39 | ## Requirements 40 | 41 | scrollSnap dependents on the following browser APIs: 42 | 43 | - [requestAnimationFrame](http://caniuse.com/#feat=requestanimationframe) 44 | 45 | Some of these APIs are capable of being polyfilled in older browser. Check the linked resources above to determine if you must polyfill to achieve your desired level of browser support. 46 | 47 | ## Include 48 | 49 | Simply include the JS-file at the end of your `body`. 50 | 51 | ```html 52 | 53 | ``` 54 | 55 | ## Options 56 | 57 | List of options you can pass to the `scrollSnap.init`-function: 58 | 59 | ```js 60 | scrollSnap.init({ 61 | 62 | // NodeList of snap-elements (required) 63 | // scrollSnap always snaps to the nearest element 64 | elements: document.querySelectorAll('section'), 65 | 66 | // Integer - Set a minimum window-size (required) 67 | // scrollSnap will be deactivated when the window is smaller than the given dimensions 68 | minWidth: 600, 69 | minHeight: 400, 70 | 71 | // Boolean - Deactivate scrollSnap on mobile devices (optional) 72 | detectMobile: true, 73 | 74 | // Boolean - Keyboard-navigation (optional) 75 | keyboard: true, 76 | 77 | // Integer - Snap-animation-speed (optional) 78 | // Higher = slower 79 | duration: 20, 80 | 81 | // Function - Set a custom timing-function for the snap-animation (optional) 82 | timing: scrollSnap._timing 83 | 84 | }) 85 | ``` 86 | -------------------------------------------------------------------------------- /dist/scrollSnap.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.scrollSnap=e()}}(function(){return function e(t,n,o){function r(u,l){if(!n[u]){if(!t[u]){var d="function"==typeof require&&require;if(!l&&d)return d(u,!0);if(i)return i(u,!0);var a=new Error("Cannot find module '"+u+"'");throw a.code="MODULE_NOT_FOUND",a}var s=n[u]={exports:{}};t[u][0].call(s.exports,function(e){var n=t[u][1][e];return r(n?n:e)},s,s.exports,e,t,n,o)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u=e.minWidth&&d.height>=e.minHeight,u=d.widthe&&(e=0),e>t-1&&(e=t-1),e},h=function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];return null==e.elements?(console.error("Elements missing: opts.elements"),!1):null==e.minWidth||e.minWidth<0?(console.error("Property missing or not a number: opts.minWidth"),!1):null==e.minHeight||e.minHeight<0?(console.error("Property missing or not a number: opts.minHeight"),!1):(e.detectMobile!==!1&&(e.detectMobile=!0),(null==e.duration||e.duration<0)&&(e.duration=20),null==e.timing&&(e.timing=m),e.keyboard!==!1&&(e.keyboard=!0),!0)},g=function(){var e=document.body.getBoundingClientRect(),t={width:window.innerWidth,height:window.innerHeight};return{top:-1*e.top,maxTop:e.height-t.height,bottom:-1*e.top+t.height,width:t.width,height:t.height}},p=function(e,t,n){if(null==e)return!1;var o={index:n,active:!1,top:e.offsetTop,bottom:e.offsetTop+e.offsetHeight,height:e.offsetHeight,dom:e};return o.visiblePercentage=b(o,t).vP,o},b=function(e,t){var n=0,o=0,r=0,i=0;return n=t.top>e.top?t.top:e.top,o=t.bottom>e.bottom?e.bottom:t.bottom,r=o-n,i=100/e.height*r,0>r&&(r=0),0>i&&(i=0),{vH:r,vP:i}},w=function(e,t){function n(){var e=s-m(d,0,c,f);document.body.scrollTop=e,document.documentElement.scrollTop=e,d>=f||document.body.scrollTop===t.maxTop&&0!==d?r=!1:(d++,requestAnimationFrame(n))}for(var o=e.dom,i=0;i0?1:-1,d=g(),a=[];for(var u=0;un.visiblePercentage)&&(n=c)}o=n.index+t,o=v(o,a.length),a[o].visiblePercentage*=i;for(var u=0;un.visiblePercentage&&(n=c)}return w(n,d)},H=function(){r=!0;for(var e=null,t=0;t=n.top&&(e=n)}return w(e,d)}},{}]},{},[1])(1)}); -------------------------------------------------------------------------------- /src/scripts/main.js: -------------------------------------------------------------------------------- 1 | let on = null 2 | let animating = false 3 | 4 | let scrollTimer = null 5 | let resizeTimer = null 6 | 7 | let computedOpts = null 8 | let computedWindow = null 9 | let computedElements = null 10 | 11 | export const init = function(opts = {}) { 12 | 13 | // Check if opts includes all required properties 14 | if (valid(opts)===false) return false 15 | 16 | // Disable on mobile devices 17 | if (opts.detectMobile===true && isMobile()===true) return false 18 | 19 | // Save computed options 20 | computedOpts = opts 21 | 22 | // Listen to window-size changes 23 | window.addEventListener('resize', onResize) 24 | 25 | // Start the internal init function 26 | return _init(computedOpts) 27 | 28 | } 29 | 30 | const _init = function(opts) { 31 | 32 | // Get size of window 33 | computedWindow = getWindowMetrics() 34 | 35 | // Reset computed elements 36 | computedElements = [] 37 | 38 | // Update the metrics of each element 39 | for (let i = 0; i < opts.elements.length; ++i) { 40 | 41 | let element = opts.elements[i] 42 | let elementMetrics = getElementMetrics(element, computedWindow, i) 43 | 44 | // Save metrics of element 45 | computedElements.push(elementMetrics) 46 | 47 | } 48 | 49 | let isBig = computedWindow.width >= opts.minWidth && computedWindow.height >= opts.minHeight 50 | let isSmall = computedWindow.width < opts.minWidth || computedWindow.height < opts.minHeight 51 | 52 | if (isBig===true && (on===false || on===null)) return start(opts) 53 | else if (isSmall===true && (on===true || on===null)) return stop(opts) 54 | 55 | } 56 | 57 | const isMobile = function() { 58 | 59 | return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera) 60 | 61 | } 62 | 63 | const timing = function(t, b, c, d) { 64 | 65 | // t = Current frame 66 | // b = Start-value 67 | // c = End-value 68 | // d = Duration 69 | 70 | t /= d 71 | return -c * t*(t-2) + b 72 | 73 | } 74 | 75 | const normalizePosition = function(newPos, maxPos) { 76 | 77 | if (newPos<0) newPos = 0 78 | if (newPos>maxPos-1) newPos = maxPos - 1 79 | 80 | return newPos 81 | 82 | } 83 | 84 | const valid = function(opts = {}) { 85 | 86 | // Check required properties 87 | 88 | if (opts.elements==null) { 89 | console.error('Elements missing: opts.elements') 90 | return false 91 | } 92 | 93 | if (opts.minWidth==null || opts.minWidth<0) { 94 | console.error('Property missing or not a number: opts.minWidth') 95 | return false 96 | } 97 | 98 | if (opts.minHeight==null || opts.minHeight<0) { 99 | console.error('Property missing or not a number: opts.minHeight') 100 | return false 101 | } 102 | 103 | // Set optional properties 104 | 105 | if (opts.detectMobile!==false) opts.detectMobile = true 106 | if (opts.duration==null || opts.duration<0) opts.duration = 20 107 | if (opts.timing==null) opts.timing = timing 108 | if (opts.keyboard!==false) opts.keyboard = true 109 | 110 | return true 111 | 112 | } 113 | 114 | const getWindowMetrics = function() { 115 | 116 | let boundingClientRect = document.body.getBoundingClientRect() 117 | let windowSize = { width: window.innerWidth, height: window.innerHeight } 118 | 119 | return { 120 | top : boundingClientRect.top * -1, 121 | maxTop : boundingClientRect.height - windowSize.height, 122 | bottom : boundingClientRect.top * -1 + windowSize.height, 123 | width : windowSize.width, 124 | height : windowSize.height 125 | } 126 | 127 | } 128 | 129 | const getElementMetrics = function(elem, windowMetrics, index) { 130 | 131 | if (elem==null) return false 132 | 133 | let obj = { 134 | index, 135 | active : false, 136 | top : elem.offsetTop, 137 | bottom : elem.offsetTop + elem.offsetHeight, 138 | height : elem.offsetHeight, 139 | dom : elem 140 | } 141 | 142 | obj.visiblePercentage = getElementVisiblePercentage(obj, windowMetrics).vP 143 | 144 | return obj 145 | 146 | } 147 | 148 | const getElementVisiblePercentage = function(elementMetrics, windowMetrics) { 149 | 150 | let sP = 0 151 | let eP = 0 152 | let vH = 0 153 | let vP = 0 154 | 155 | // Calculate start-point (sP) 156 | sP = (windowMetrics.top > elementMetrics.top ? windowMetrics.top : elementMetrics.top) 157 | 158 | // Calculate end-point (eP) 159 | eP = (windowMetrics.bottom > elementMetrics.bottom ? elementMetrics.bottom : windowMetrics.bottom) 160 | 161 | // Calculate visible height in pixels (vH) 162 | vH = eP - sP 163 | 164 | // Convert vH from pixels to a percentage value 165 | // 100 = element completely visible 166 | // 0 = element not visible at all 167 | vP = (100 / elementMetrics.height) * vH 168 | 169 | // Normalize output 170 | if (vH<0) vH = 0 171 | if (vP<0) vP = 0 172 | 173 | // Return the visible height in percent 174 | return { vH, vP } 175 | 176 | } 177 | 178 | const setElementVisible = function(elementMetrics, windowMetrics) { 179 | 180 | let elem = elementMetrics.dom 181 | 182 | // Remove all active-states 183 | for (let i = 0; i < computedElements.length; ++i) { 184 | 185 | let elementMetrics = computedElements[i] 186 | 187 | elementMetrics.dom.classList.remove('active') 188 | elementMetrics.active = false 189 | 190 | } 191 | 192 | // Add active-state to the element 193 | elem.classList.add('active') 194 | elementMetrics.active = true 195 | 196 | let currentFrame = 0 197 | let startScrollTop = -document.body.getBoundingClientRect().top 198 | let difference = startScrollTop - elementMetrics.top 199 | let duration = computedOpts.duration 200 | let timing = computedOpts.timing 201 | 202 | function animation() { 203 | 204 | let newScrollTop = startScrollTop - timing(currentFrame, 0, difference, duration) 205 | 206 | // Scroll to element 207 | document.body.scrollTop = newScrollTop // Safari, Chrome 208 | document.documentElement.scrollTop = newScrollTop // Firefox 209 | 210 | // Stop the animation when ... 211 | // ... all frames have been shown 212 | // ... scrollTop reached its maximum after the first frame 213 | if ((currentFrame>=duration) || 214 | (document.body.scrollTop===windowMetrics.maxTop && currentFrame!==0)) { 215 | 216 | // Animation finished 217 | animating = false 218 | 219 | } else { 220 | 221 | // Continue with next frame 222 | currentFrame++ 223 | 224 | // Continue animation 225 | requestAnimationFrame(animation) 226 | 227 | } 228 | 229 | } 230 | 231 | // Start the animation 232 | animation() 233 | 234 | return true 235 | 236 | } 237 | 238 | const start = function(opts) { 239 | 240 | on = true 241 | 242 | window.addEventListener('wheel', onScroll) 243 | if (opts.keyboard===true) document.body.addEventListener('keydown', onKeydown) 244 | 245 | for (let i = 0; i < computedElements.length; ++i) { computedElements[i].dom.classList.remove('active') } 246 | 247 | return scrollToNearest() 248 | 249 | } 250 | 251 | const stop = function(opts) { 252 | 253 | on = false 254 | 255 | window.removeEventListener('wheel', onScroll) 256 | if (opts.keyboard===true) document.body.removeEventListener('keydown', onKeydown) 257 | 258 | for (let i = 0; i < computedElements.length; ++i) { computedElements[i].dom.classList.add('active') } 259 | 260 | return true 261 | 262 | } 263 | 264 | const onKeydown = function(e) { 265 | 266 | let key = e.keyCode 267 | let newPos = 0 268 | 269 | if (key!==38 && key!==40) return true 270 | if (animating===true) return false 271 | 272 | animating = true 273 | 274 | // Get current position 275 | for (let i = 0; i < computedElements.length; ++i) { if (computedElements[i].active===true) newPos = i } 276 | 277 | // 38 = Up 278 | // 40 = Down 279 | if (key===38) newPos += -1 280 | else if (key===40) newPos += 1 281 | 282 | // Check if next element exists 283 | newPos = normalizePosition(newPos, computedElements.length) 284 | 285 | // Show the new element 286 | setElementVisible(computedElements[newPos], computedWindow) 287 | 288 | e.preventDefault() 289 | return false 290 | 291 | } 292 | 293 | const onResize = function() { 294 | 295 | // Reset timeout 296 | clearTimeout(resizeTimer) 297 | 298 | // Set new timeout 299 | resizeTimer = setTimeout(() => init(computedOpts), 200) 300 | 301 | return true 302 | 303 | } 304 | 305 | const onScroll = function(e) { 306 | 307 | if (animating===true) return false 308 | 309 | // Reset timeout 310 | clearTimeout(scrollTimer) 311 | 312 | // Set new timeout 313 | scrollTimer = setTimeout(() => scrollTo(e), 200) 314 | 315 | return true 316 | 317 | } 318 | 319 | const scrollTo = function(e) { 320 | 321 | animating = true 322 | 323 | let direction = 0 324 | let topElement = {} 325 | let nextElementNum = null 326 | let nextElement = {} 327 | let gravitation = 9.807 328 | 329 | // Get the direction from the event 330 | if (e.type==='wheel') direction = e.deltaY 331 | 332 | // Normalize direction 333 | if (direction>0) direction = 1 334 | else direction = -1 335 | 336 | // Update window metrics 337 | computedWindow = getWindowMetrics() 338 | 339 | // Reset computed elements 340 | computedElements = [] 341 | 342 | // Update the metrics of each element 343 | for (let i = 0; i < computedOpts.elements.length; ++i) { 344 | 345 | let element = computedOpts.elements[i] 346 | let elementMetrics = getElementMetrics(element, computedWindow, i) 347 | 348 | // Save metrics of element 349 | computedElements.push(elementMetrics) 350 | 351 | // Get the element which is most visible and save it 352 | if (topElement.visiblePercentage==null || elementMetrics.visiblePercentage>topElement.visiblePercentage) topElement = elementMetrics 353 | 354 | } 355 | 356 | // Use the velocity to calculate the next element 357 | nextElementNum = topElement.index + direction 358 | 359 | // Check if next element exists 360 | nextElementNum = normalizePosition(nextElementNum, computedElements.length) 361 | 362 | // Add velocity to next element 363 | computedElements[nextElementNum].visiblePercentage *= gravitation 364 | 365 | // Re-check if there is a new most visible element 366 | for (let i = 0; i < computedElements.length; ++i) { 367 | 368 | let elementMetrics = computedElements[i] 369 | 370 | if (elementMetrics.visiblePercentage>topElement.visiblePercentage) topElement = elementMetrics 371 | 372 | } 373 | 374 | return setElementVisible(topElement, computedWindow) 375 | 376 | } 377 | 378 | const scrollToNearest = function() { 379 | 380 | animating = true 381 | 382 | let nextElementMetrics = null 383 | 384 | for (let i = 0; i < computedOpts.elements.length; ++i) { 385 | 386 | let elementMetrics = computedElements[i] 387 | 388 | if (computedWindow.top>=elementMetrics.top) nextElementMetrics = elementMetrics 389 | 390 | } 391 | 392 | return setElementVisible(nextElementMetrics, computedWindow) 393 | 394 | } --------------------------------------------------------------------------------