├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── bower.json ├── dist ├── parallax-vanilla.css ├── parallax-vanilla.js └── src │ └── ts │ ├── constants.d.ts │ ├── init.d.ts │ ├── initBlock.d.ts │ ├── initContainer.d.ts │ ├── parallax-vanilla.d.ts │ ├── resize.d.ts │ ├── translate.d.ts │ └── types.d.ts ├── index.html ├── index.js ├── package.json ├── src ├── less │ └── parallax-vanilla.less └── ts │ ├── constants.ts │ ├── init.ts │ ├── initBlock.ts │ ├── initContainer.ts │ ├── parallax-vanilla.ts │ ├── resize.ts │ ├── translate.ts │ └── types.ts ├── test ├── css │ ├── bootstrap.min.css │ └── demo.css ├── media │ ├── Sunrise - 7127.mp4 │ ├── animal.mp4 │ ├── butterfly.jpg │ ├── dolphin.mp4 │ ├── earth.mov │ ├── favicon.png │ ├── forest.mp4 │ ├── ladybug.jpg │ ├── leaves.jpg │ ├── owl.jpg │ ├── peach.jpg │ ├── plums.jpg │ ├── raindrops.mp4 │ ├── reflection.jpg │ ├── sunset.jpg │ ├── tomatoes.jpg │ ├── water.mp4 │ └── zebras.jpg └── parallax-vanilla.spec.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "env": { 10 | "es6": true, 11 | "node": false 12 | }, 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "prettier/prettier": "error", 18 | "@typescript-eslint/explicit-module-boundary-types": "off" 19 | }, 20 | "plugins": ["@typescript-eslint", "prettier"] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _loc 3 | npm-debug.log 4 | .DS_Store 5 | config.cson 6 | yarn-error.log 7 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | package.json 4 | gruntfile.js 5 | _loc 6 | bower.json 7 | .git 8 | .gitignore 9 | .DS_Store 10 | npm-debug.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "max-len": "off", 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Engervall IT 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parallax-vanilla.js 2 | 3 | Seamless and lightweight parallax scrolling library implemented in pure JavaScript utilizing Hardware acceleration for extra performance. 4 | 5 | ## [Demo](https://erikengervall.github.io/parallax-vanilla/) 6 | 7 | ## Main features 8 | 9 | ### Super lightweight without dependencies 10 | 11 | A few kilobytes of pure JavaScript. 12 | 13 | ### Viewport-only animations 14 | 15 | Parallax elements are only animated within the current viewport, saving a lot of resources. 16 | 17 | ### Dynamic sizing 18 | 19 | Image-elements are dynamically sized and adjusted relative to the pv-speed. 20 | 21 | ### Performance is key 22 | 23 | Vanilla Parallax maximizes your parallax effects with hardware acceleration and zero external libraries. 24 | 25 | ### Media type independence 26 | 27 | The parallax effect applies not only on images but on videos as well. Videos' audio will play if the videos are clicked and remain within the viewport. 28 | 29 | ## Browser support 30 | 31 | Tested browsers: 32 | 33 | | Chrome | Safari | Firefox | 34 | | ------ | ------ | ------- | 35 | | 60+ | 10+ | 44+ | 36 | 37 | ## Installation 38 | 39 | ### [bower](https://github.com/erikengervall/parallax-vanilla) 40 | 41 | ```sh 42 | bower i --save parallax-vanilla 43 | ``` 44 | 45 | ### [npm](https://www.npmjs.com/package/parallax-vanilla) 46 | 47 | ```sh 48 | npm i --save parallax-vanilla 49 | ``` 50 | 51 | ### Include 52 | 53 | - Include `parallax-vanilla.css` in `` 54 | - Include `parallax-vanilla.js` just before `` 55 | 56 | ```html 57 | 58 | 59 | ``` 60 | 61 | ## Usage 62 | 63 | ### Simple usage 64 | 65 | **1**. Wrap a `pv-block` with a `pv-container`. 66 | 67 | ```html 68 |
69 |
70 |
71 | ``` 72 | 73 | **2**. Attach a mediapath to `pv-block` 74 | 75 | ```html 76 |
77 |
78 |
79 | ``` 80 | 81 | **3**. Initialize library. 82 | 83 | ```html 84 |
85 |
>
86 | 87 | 90 | ``` 91 | 92 | ### JavaScript initialization options 93 | 94 | Optional global settings can be configured upon initialization. 95 | 96 | ```javascript 97 | pv.init({ 98 | container: { 99 | class: String, 100 | height: String || Number, 101 | }, 102 | block: { 103 | class: String, 104 | speed: Number || Float, 105 | mediapath: String, 106 | mediatype: String, 107 | mute: Boolean, 108 | }, 109 | }) 110 | ``` 111 | 112 | #### JavaScript Settings 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 |
NameTypeDefaultDescriptionExample values
settingsObject{container, block}Settings object. These settings will be applied to each container and block. Can be individually overwritten by data attributes.{container: {...}, block: {...}
settings.containerObject{class, height}The container object.{...}
settings.container.classStringpv-containerThe class of the container element. Remember to update the CSS classes if you wish to change this.pv-container
settings.container.heightString || Number250pxThe container's height in either pixels or viewport heights. If the string lacks a suffix, or a number is entered, it will default to pixels.250px, 50vh, 250
settings.blockObject{class, speed, mediatype, mediapath}The block object.{...}
settings.block.classStringpv-blockThe class of the block element. Remember to update the CSS classes if you wish to change this.pv-block
settings.block.speedNumber || Float-Math.PIThe speed and direction at which the parallax animated. Negative values will animate the `block` upwards when scrolling downwards on the page.1, 1.5, -1, -1.5
settings.block.mediatypeStringimageThe block's media type. Blocks with mediapaths containing a video extension will automatically be considered videos.image, video or none
settings.block.mediapathStringundefinedThe block's media path.../path/to/file.ext
settings.block.muteBooleanfalseDefines whether or not all videos should be muted.true or false
211 | 212 | ### Data attributes: Customize individual elements 213 | 214 | Data attributes allow fine control over each individual element and will overwrite the global JavaScript settings. 215 | 216 | ```html 217 |
218 |
219 |
220 | ``` 221 | 222 | This code will produce a container with class `pv-container` with height `100vh` containing a block with class `pv-block` with a parallax speed of `3.14` displaying the media `epic_montage.mp4` of type `video` with `pv-mute=false`. 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 |
Data attributes for container
pv-height
Data attributes for block
pv-speed
pv-mediatype
pv-mediapath
pv-mute
253 | 254 | The descriptions and the default values are the same as the corresponding properties of the JavaScript settings object. 255 | 256 | ### CSS 257 | 258 | The CSS in `parallax-vanilla.css` is required in order for parallax-vanilla to function properly. 259 | 260 | ```css 261 | .pv-container { 262 | ...; 263 | } 264 | .pv-container .pv-block { 265 | ...; 266 | } 267 | .pv-container .pv-block video { 268 | ...; 269 | } 270 | .audio-icon { 271 | ...; 272 | } 273 | ``` 274 | 275 | # LICENSE 276 | 277 | MIT 278 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parallax-vanilla", 3 | "description": "Simple parallax in pure JavaScript designed for performance and dynamic sizing.", 4 | "main": ["dist/vanilla-parallax.js", "dist/main.css"], 5 | "authors": ["Erik Engervall"], 6 | "license": "MIT", 7 | "keywords": ["vanilla", "javascript", "parallax", "animation", "library"], 8 | "homepage": "https://github.com/erikengervall/parallax-vanilla", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "src", 13 | "package.json", 14 | "gruntfile.js", 15 | "_loc", 16 | "bower.json", 17 | ".git", 18 | ".gitignore", 19 | ".DS_Store", 20 | ".npmignore", 21 | "npm-debug.log" 22 | ], 23 | "version": "1.2.1" 24 | } 25 | -------------------------------------------------------------------------------- /dist/parallax-vanilla.css: -------------------------------------------------------------------------------- 1 | .pv-container{overflow:hidden}.pv-container .pv-block{will-change:transform;background-repeat:no-repeat;background-position:50%;background-size:cover}.pv-container .pv-block video{position:absolute;top:0;left:0;height:100%;width:100%;object-fit:cover}.pv-container .audio-icon{height:30px;width:30px;position:absolute;top:10px;left:10px}.pv-container .audio-icon span{display:block;width:8px;height:8px;background:#fff;margin:11px 0 0 2px}.pv-container .audio-icon span:after{content:"";position:absolute;width:0;height:0;border-color:transparent #fff transparent transparent;border-style:solid;border-width:10px 14px 10px 15px;left:-13px;top:5px}.pv-container .audio-icon span:before{transform:rotate(45deg);border-radius:0 50px 0 0;content:"";position:absolute;width:13px;height:13px;border-color:#fff;border-style:double;border-width:7px 7px 0 0;left:18px;top:9px;transition:all .2s ease-out}.pv-container .audio-icon:hover span:before{transform:scale(.8) translate(-3px) rotate(42deg)}.pv-container .audio-icon.mute span:before{transform:scale(.5) translate(-15px) rotate(36deg);opacity:0} -------------------------------------------------------------------------------- /dist/parallax-vanilla.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function o(n){if(e[n])return e[n].exports;var r=e[n]={i:n,l:!1,exports:{}};return t[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.m=t,o.c=e,o.d=function(t,e,n){o.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},o.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(e,"a",e),e},o.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},o.p="",o(o.s="./src/ts/parallax-vanilla.ts")}({"./src/less/parallax-vanilla.less": 2 | /*!****************************************!*\ 3 | !*** ./src/less/parallax-vanilla.less ***! 4 | \****************************************/ 5 | /*! no static exports found */function(t,e,o){},"./src/ts/constants.ts": 6 | /*!*****************************!*\ 7 | !*** ./src/ts/constants.ts ***! 8 | \*****************************/ 9 | /*! exports provided: VIDEO_EXTENSIONS, MEDIA_TYPES, ELEMENT_DATA_KEYS, defaultSettings */function(t,e,o){"use strict";o.r(e),o.d(e,"VIDEO_EXTENSIONS",(function(){return n})),o.d(e,"MEDIA_TYPES",(function(){return r})),o.d(e,"ELEMENT_DATA_KEYS",(function(){return i})),o.d(e,"defaultSettings",(function(){return s}));const n=["3g2","3gp","asf","avi","flv","h264","m4v","mov","mp4","mpg","mpeg","rm","srt","swf","vow","vob","wmv"],r={image:"image",video:"video",none:"none"},i={MEDIAPATH:"pv-mediapath",MEDIATYPE:"pv-mediatype",MUTE:"pv-mute",HEIGHT:"pv-height",SPEED:"pv-speed"},s={container:{class:"pv-container",height:"250px"},block:{class:"pv-block",speed:-Math.PI,mediatype:r.image,mediapath:null,mute:!1}}},"./src/ts/init.ts": 10 | /*!************************!*\ 11 | !*** ./src/ts/init.ts ***! 12 | \************************/ 13 | /*! exports provided: init */function(t,e,o){"use strict";o.r(e),o.d(e,"init",(function(){return s}));var n=o(/*! ./constants */"./src/ts/constants.ts"),r=o(/*! ./initContainer */"./src/ts/initContainer.ts"),i=o(/*! ./initBlock */"./src/ts/initBlock.ts");const s=t=>{const{pv:e}=window;e.containerArr=[],e.settings=c(t,n.defaultSettings);const o=document.getElementsByClassName(e.settings.container.class);for(let t=0;t({container:Object.assign(Object.assign({},e.container),t.container),block:Object.assign(Object.assign({},e.block),t.block)}),a=t=>{return t.getBoundingClientRect().top+(window.pageYOffset||document.documentElement.scrollTop)}},"./src/ts/initBlock.ts": 14 | /*!*****************************!*\ 15 | !*** ./src/ts/initBlock.ts ***! 16 | \*****************************/ 17 | /*! exports provided: setBlockSpeed, setBlockMediaProps, setBlockMute, setBlockVisual, setBlockAttributes */function(t,e,o){"use strict";o.r(e),o.d(e,"setBlockSpeed",(function(){return r})),o.d(e,"setBlockMediaProps",(function(){return i})),o.d(e,"setBlockMute",(function(){return s})),o.d(e,"setBlockVisual",(function(){return a})),o.d(e,"setBlockAttributes",(function(){return l}));var n=o(/*! ./constants */"./src/ts/constants.ts");const r=(t,e)=>{const o=t.getAttribute(n.ELEMENT_DATA_KEYS.SPEED);if(!o)return e.block.speed;const r=parseInt(o,10);if(isNaN(r))throw console.error("Invalid type for attribute speed for block: "+t),new Error("Invalid type for attribute speed for block");return 0===r?e.block.speed:r},i=(t,e)=>{let o=t.getAttribute(n.ELEMENT_DATA_KEYS.MEDIATYPE);const r=t.getAttribute(n.ELEMENT_DATA_KEYS.MEDIAPATH);if(o===n.MEDIA_TYPES.none)return{mediatype:o,mediapath:r};if(o||(o=e.block.mediatype),r&&d(o,r)&&(o=n.MEDIA_TYPES.video),!r&&o!==n.MEDIA_TYPES.none)throw console.error("Media path not defined for block: "+t),new Error("Media path not defined");return{mediatype:o,mediapath:r}},s=(t,e)=>{const o=t.getAttribute(n.ELEMENT_DATA_KEYS.MUTE);return null!=o?"true"===o:e.block.mute},c=(t,e)=>{const{pv:o}=window;o.unmutedBlock&&o.unmutedBlock.videoEl!==t&&(o.unmutedBlock.videoEl&&(o.unmutedBlock.videoEl.muted=!0),o.unmutedBlock.audioButton&&o.unmutedBlock.audioButton.classList.add("mute")),o.unmutedBlock=e,t.muted=!t.muted,e.muted=t.muted,e.audioButton&&e.audioButton.classList.toggle("mute")},a=t=>{const{mediatype:e}=t;if(e!==n.MEDIA_TYPES.image){if(e!==n.MEDIA_TYPES.video)throw console.error("Failed to set media for block:",t),new Error("Failed to set media");(t=>{const{mediapath:e}=t,o=document.createElement("video");if(o.src=e,o.autoplay=!0,o.loop=!0,o.defaultMuted=!0,o.muted=!0,t.muted=!0,t.videoEl=o,t.blockEl.appendChild(o),void 0===window.orientation&&!t.mute){o.addEventListener("click",(function(){c(o,t)}));const e=document.createElement("a");e.href="#",e.className+="audio-icon mute",e.appendChild(document.createElement("span")),e.addEventListener("click",(function(e){e.preventDefault(),c(o,t)})),t.audioButton=e,t.blockEl.insertAdjacentElement("afterend",e)}})(t)}else(t=>{const{mediapath:e}=t;t.blockEl.style.backgroundImage="url('"+e+"')",window.getComputedStyle(t.blockEl).getPropertyValue("background-image")})(t)},l=(t,e)=>{const{pv:o}=window;u();let n=0,r=0,i=0;t.offset0?(n=-Math.abs(t.offset),i=t.height+t.offset):i=r+t.height):(r=(t.height+o.windowProps.windowHeight)/Math.abs(e.speed),i=r+t.height,e.speed>0?(n=-r,i=t.height+o.windowProps.windowHeight/Math.abs(e.speed)):i=r+t.height),Math.abs(n)>=Math.abs(i)&&(i=Math.abs(n)+1),e.blockEl.style.setProperty("padding-bottom",i+"px"),e.blockEl.style.setProperty("margin-top",n+"px")},d=(t,e)=>t===n.MEDIA_TYPES.video||-1!==n.VIDEO_EXTENSIONS.indexOf((t=>{const e=t.substr(t.lastIndexOf(".")+1,t.length).toLowerCase();if(0===e.length)throw console.error("Invalid extension for media with media path: "+t),new Error("Invalid extension for media");return e})(e)),u=()=>{const{pv:t}=window;t.windowProps={scrollTop:window.scrollY||document.documentElement.scrollTop,windowHeight:window.innerHeight,windowMidHeight:window.innerHeight/2}}},"./src/ts/initContainer.ts": 18 | /*!*********************************!*\ 19 | !*** ./src/ts/initContainer.ts ***! 20 | \*********************************/ 21 | /*! exports provided: setContainerHeight */function(t,e,o){"use strict";o.r(e),o.d(e,"setContainerHeight",(function(){return r}));var n=o(/*! ./constants */"./src/ts/constants.ts");const r=(t,e)=>{const o=t.getAttribute(n.ELEMENT_DATA_KEYS.HEIGHT);if(!o)return e.container.height;if(!isNaN(Number(o)))return o+"px";const r=o.substr(o.length-2,o.length);if("px"===r||"vh"===r)return o;throw new Error('Invalid height suffix, expected "px" or "vh" but got: '+r)}},"./src/ts/parallax-vanilla.ts": 22 | /*!************************************!*\ 23 | !*** ./src/ts/parallax-vanilla.ts ***! 24 | \************************************/ 25 | /*! no exports provided */function(t,e,o){"use strict";o.r(e);o(/*! ../less/parallax-vanilla.less */"./src/less/parallax-vanilla.less");var n=o(/*! ./constants */"./src/ts/constants.ts"),r=o(/*! ./init */"./src/ts/init.ts"),i=o(/*! ./resize */"./src/ts/resize.ts"),s=o(/*! ./translate */"./src/ts/translate.ts");(t=>{void 0===t.pv?(t.pv=(()=>{const e={init:r.init,containerArr:[],mostReContainerInViewport:-1,prevScrollTop:-1,settings:n.defaultSettings,windowProps:{scrollTop:-1,windowHeight:-1,windowMidHeight:-1}};t.pv=e,void 0===t.orientation&&(t.onresize=()=>Object(i.resize)()),t.raf=t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||function(e){t.setTimeout(e,1e3/60)};const o=()=>{Object(s.translate)(),t.raf(o)};return t.raf(o),e})(),console.log("%c parallax-vanilla defined.","color: green")):console.log("%c parallax-vanilla already defined.","color: red")})(window)},"./src/ts/resize.ts": 26 | /*!**************************!*\ 27 | !*** ./src/ts/resize.ts ***! 28 | \**************************/ 29 | /*! exports provided: resize */function(t,e,o){"use strict";o.r(e),o.d(e,"resize",(function(){return i}));var n=o(/*! ./constants */"./src/ts/constants.ts"),r=o(/*! ./initBlock */"./src/ts/initBlock.ts");const i=()=>{const{pv:t}=window;t.containerArr.forEach(t=>{t.height=t.containerEl.clientHeight,t.blocks.forEach(e=>{e.mediatype!==n.MEDIA_TYPES.none&&Object(r.setBlockAttributes)(t,e)})})}},"./src/ts/translate.ts": 30 | /*!*****************************!*\ 31 | !*** ./src/ts/translate.ts ***! 32 | \*****************************/ 33 | /*! exports provided: translate */function(t,e,o){"use strict";o.r(e),o.d(e,"translate",(function(){return n}));const n=()=>{const{pv:t}=window;t.windowProps.scrollTop=window.scrollY||document.documentElement.scrollTop,t.windowProps.scrollTop!==t.prevScrollTop&&(t.prevScrollTop=t.windowProps.scrollTop,t.containerArr.forEach((e,o)=>{let n=0;if(i(e.offset,e.height))o>t.mostReContainerInViewport&&(t.mostReContainerInViewport=o),n=e.offset{e.videoEl&&(e.videoEl.play(),t.unmutedBlock!==e||e.muted||(e.videoEl.muted=e.muted,t.unmutedBlock.audioButton&&(e.muted?t.unmutedBlock.audioButton.classList.add("mute"):t.unmutedBlock.audioButton.classList.remove("mute")))),r(e.blockEl,"translate3d(0,"+Math.round(n/e.speed)+"px, 0)")});else{e.hasVideoBlock&&e.blocks.forEach(e=>{e.videoEl&&(e.videoEl.pause(),t.unmutedBlock===e&&(e.videoEl.muted=!0))});const n=t.containerArr[o+1];if(n&&!i(n.offset,n.height)&&t.mostReContainerInViewport{t.style.webkitTransform=e,t.style.MozTransform=e,t.style.msTransform=e,t.style.OTransform=e,t.style.transform=e},i=(t,e)=>{const{pv:o}=window;return o.windowProps.scrollTop+o.windowProps.windowHeight-t>0&&o.windowProps.scrollTopt.offset+t.height>e.offset+e.height}}); -------------------------------------------------------------------------------- /dist/src/ts/constants.d.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from './types'; 2 | export declare const VIDEO_EXTENSIONS: string[]; 3 | export declare const MEDIA_TYPES: { 4 | image: 'image'; 5 | video: 'video'; 6 | none: 'none'; 7 | }; 8 | export declare const ELEMENT_DATA_KEYS: { 9 | MEDIAPATH: string; 10 | MEDIATYPE: string; 11 | MUTE: string; 12 | HEIGHT: string; 13 | SPEED: string; 14 | }; 15 | export declare const defaultSettings: Settings; 16 | -------------------------------------------------------------------------------- /dist/src/ts/init.d.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from './types'; 2 | export declare const init: (userSettings: Partial) => void; 3 | -------------------------------------------------------------------------------- /dist/src/ts/initBlock.d.ts: -------------------------------------------------------------------------------- 1 | import { Block, Container, Settings } from './types'; 2 | export declare const setBlockSpeed: (blockEl: HTMLElement, settings: Settings) => number; 3 | export declare const setBlockMediaProps: (blockEl: HTMLElement, settings: Settings) => { 4 | mediatype: "image" | "video" | "none"; 5 | mediapath: string | null; 6 | }; 7 | export declare const setBlockMute: (blockEl: HTMLElement, settings: Settings) => boolean; 8 | export declare const setBlockVisual: (block: Block) => void; 9 | export declare const setBlockAttributes: (container: Container, block: Block) => void; 10 | -------------------------------------------------------------------------------- /dist/src/ts/initContainer.d.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from './types'; 2 | export declare const setContainerHeight: (containerEl: HTMLElement, settings: Settings) => string; 3 | -------------------------------------------------------------------------------- /dist/src/ts/parallax-vanilla.d.ts: -------------------------------------------------------------------------------- 1 | import '../less/parallax-vanilla.less'; 2 | -------------------------------------------------------------------------------- /dist/src/ts/resize.d.ts: -------------------------------------------------------------------------------- 1 | export declare const resize: () => void; 2 | -------------------------------------------------------------------------------- /dist/src/ts/translate.d.ts: -------------------------------------------------------------------------------- 1 | export declare const translate: () => void; 2 | -------------------------------------------------------------------------------- /dist/src/ts/types.d.ts: -------------------------------------------------------------------------------- 1 | import { MEDIA_TYPES } from './constants'; 2 | export interface Settings { 3 | container: { 4 | class: string; 5 | height: string; 6 | }; 7 | block: { 8 | class: string; 9 | speed: number; 10 | mediatype: keyof typeof MEDIA_TYPES; 11 | mediapath: null; 12 | mute: boolean; 13 | }; 14 | } 15 | export interface Block { 16 | blockEl: HTMLElement; 17 | speed: number; 18 | mediatype: string | null; 19 | mediapath: string | null; 20 | mute: boolean; 21 | muted: boolean; 22 | videoEl?: HTMLVideoElement; 23 | audioButton?: HTMLAnchorElement; 24 | } 25 | export interface Container { 26 | containerEl: HTMLElement; 27 | offset: number; 28 | height: number; 29 | blocks: Block[]; 30 | hasVideoBlock?: boolean; 31 | } 32 | export declare type PV = any; 33 | export interface Window { 34 | raf: typeof window.requestAnimationFrame; 35 | pv?: PV; 36 | orientation: typeof window.orientation; 37 | requestAnimationFrame: typeof window.requestAnimationFrame; 38 | webkitRequestAnimationFrame: typeof window.requestAnimationFrame; 39 | mozRequestAnimationFrame: typeof window.requestAnimationFrame; 40 | setTimeout: typeof window.setTimeout; 41 | onresize: () => void; 42 | } 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | parallax-vanilla.js 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |

Super lightweight without dependencies

24 |

A few kilobytes of pure JavaScript.

25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |

Dynamic sizing

34 |

Image-elements are dynamically sized and adjusted relative to the pv-speed.

35 |
36 |
37 |
38 | 39 |
40 |
41 |

Media type independence

42 |

The parallax effect applies not only on images but on videos as well.

43 |
44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 | 54 |
55 |
56 |

Performance is key

57 |

Maximize your parallax effects with hardware acceleration and 58 | zero external libraries.

59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | 112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | 145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | 162 |
163 |
164 |

Viewport-only animations

165 |

Parallax elements are only animated within the current viewport, saving enormous resources.

166 |
167 |
168 | 169 |
170 |
171 |
172 | 173 | 174 | 175 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const path = require('path') 4 | 5 | app.use('/dist', express.static(__dirname + '/dist')) 6 | app.use('/test', express.static(__dirname + '/test')) 7 | 8 | app.set('view engine', 'html') 9 | 10 | app.get('/', (req, res) => { 11 | res.sendFile(path.join(__dirname + '/index.html')) 12 | }) 13 | 14 | const PORT = process.env.PORT || 3000 15 | app.listen(PORT, () => { 16 | console.log(`Express server running on http://localhost:${PORT}`) 17 | }) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parallax-vanilla", 3 | "version": "1.2.3", 4 | "description": "Simple parallax in pure JavaScript designed for performance and dynamic sizing.", 5 | "main": "dist/parallax-vanilla.js", 6 | "files": [ 7 | "dist/**/*" 8 | ], 9 | "author": "Erik Engervall erik.engervall@gmail.com https://github.com/erikengervall", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/erikengervall/parallax-vanilla" 14 | }, 15 | "keywords": [ 16 | "vanilla", 17 | "javascript", 18 | "parallax", 19 | "animation", 20 | "library" 21 | ], 22 | "scripts": { 23 | "server": "nodemon index.js", 24 | "build": "rimraf ./dist && webpack", 25 | "build:watch": "yarn build --watch", 26 | "dev": "concurrently 'yarn server' 'yarn build:watch'", 27 | "test": "jest ./test/parallax-vanilla.spec.js" 28 | }, 29 | "dependencies": {}, 30 | "devDependencies": { 31 | "@typescript-eslint/eslint-plugin": "^4.29.3", 32 | "@typescript-eslint/parser": "^4.29.3", 33 | "concurrently": "^4.1.2", 34 | "css-loader": "^3.2.0", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^6.1.0", 37 | "eslint-plugin-prettier": "^3.1.0", 38 | "express": "^4.16.2", 39 | "jest": "^24.9.0", 40 | "less": "^3.10.3", 41 | "less-loader": "^5.0.0", 42 | "mini-css-extract-plugin": "^0.8.0", 43 | "nodemon": "^1.12.1", 44 | "optimize-css-assets-webpack-plugin": "^5.0.3", 45 | "prettier-eslint": "^9.0.0", 46 | "rimraf": "^3.0.0", 47 | "style-loader": "^1.0.0", 48 | "terser-webpack-plugin": "^1.4.2", 49 | "ts-loader": "^6.0.4", 50 | "typescript": "^3.6.2", 51 | "webpack": "^4.39.3", 52 | "webpack-cli": "^3.3.7" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/less/parallax-vanilla.less: -------------------------------------------------------------------------------- 1 | .pv-container { 2 | overflow: hidden; 3 | 4 | .pv-block { 5 | will-change: transform; 6 | background-repeat: no-repeat; 7 | background-position: center center; 8 | background-size: cover; 9 | video { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | height: 100%; 14 | width: 100%; 15 | object-fit: cover; 16 | } 17 | } 18 | 19 | .audio-icon { 20 | height: 30px; 21 | width: 30px; 22 | position: absolute; 23 | top: 10px; 24 | left: 10px; 25 | 26 | span { 27 | display: block; 28 | width: 8px; 29 | height: 8px; 30 | background: #fff; 31 | margin: 11px 0 0 2px; 32 | 33 | &:after { 34 | content: ''; 35 | position: absolute; 36 | width: 0; 37 | height: 0; 38 | border-style: solid; 39 | border-color: transparent #fff transparent transparent; 40 | border-width: 10px 14px 10px 15px; 41 | left: -13px; 42 | top: 5px; 43 | } 44 | 45 | &:before { 46 | transform: rotate(45deg); 47 | border-radius: 0 50px 0 0; 48 | content: ''; 49 | position: absolute; 50 | width: 13px; 51 | height: 13px; 52 | border-style: double; 53 | border-color: #fff; 54 | border-width: 7px 7px 0 0; 55 | left: 18px; 56 | top: 9px; 57 | transition: all 0.2s ease-out; 58 | } 59 | } 60 | 61 | &:hover { 62 | span:before { 63 | transform: scale(0.8) translate(-3px, 0) rotate(42deg); 64 | } 65 | } 66 | 67 | &.mute { 68 | span:before { 69 | transform: scale(0.5) translate(-15px, 0) rotate(36deg); 70 | opacity: 0; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ts/constants.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from './types' 2 | 3 | export const VIDEO_EXTENSIONS = [ 4 | '3g2', 5 | '3gp', 6 | 'asf', 7 | 'avi', 8 | 'flv', 9 | 'h264', 10 | 'm4v', 11 | 'mov', 12 | 'mp4', 13 | 'mpg', 14 | 'mpeg', 15 | 'rm', 16 | 'srt', 17 | 'swf', 18 | 'vow', 19 | 'vob', 20 | 'wmv', 21 | ] 22 | 23 | export const MEDIA_TYPES: { 24 | image: 'image' 25 | video: 'video' 26 | none: 'none' 27 | } = { 28 | image: 'image', 29 | video: 'video', 30 | none: 'none', 31 | } 32 | 33 | export const ELEMENT_DATA_KEYS = { 34 | MEDIAPATH: 'pv-mediapath', 35 | MEDIATYPE: 'pv-mediatype', 36 | MUTE: 'pv-mute', 37 | HEIGHT: 'pv-height', 38 | SPEED: 'pv-speed', 39 | } 40 | 41 | export const defaultSettings: Settings = { 42 | container: { 43 | class: 'pv-container', 44 | height: '250px', 45 | }, 46 | block: { 47 | class: 'pv-block', 48 | speed: -Math.PI, 49 | mediatype: MEDIA_TYPES.image, 50 | mediapath: null, 51 | mute: false, 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /src/ts/init.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings, MEDIA_TYPES } from './constants' 2 | import { setContainerHeight } from './initContainer' 3 | import { 4 | setBlockSpeed, 5 | setBlockMediaProps, 6 | setBlockMute, 7 | setBlockVisual, 8 | setBlockAttributes, 9 | } from './initBlock' 10 | import { Block, Container, Settings, Window } from './types' 11 | 12 | export const init = (userSettings: Partial) => { 13 | const { pv } = (window as unknown) as Window 14 | pv.containerArr = [] 15 | pv.settings = mergeSettings(userSettings, defaultSettings) 16 | 17 | const containerElements = document.getElementsByClassName(pv.settings.container.class) 18 | 19 | for (let i = 0; i < containerElements.length; i++) { 20 | const containerEl = containerElements[i] as HTMLElement 21 | const offset = calculateOffsetTop(containerEl) 22 | containerEl.style.height = setContainerHeight(containerEl, pv.settings) 23 | const height = containerEl.clientHeight 24 | const blocks: Block[] = [] 25 | 26 | const container: Container = { 27 | containerEl, 28 | offset, 29 | height, 30 | blocks, 31 | } 32 | 33 | const blockElements = containerElements[i].getElementsByClassName(pv.settings.block.class) 34 | 35 | for (let j = 0; j < blockElements.length; j++) { 36 | const blockEl = blockElements[j] as HTMLElement 37 | const speed = setBlockSpeed(blockEl, pv.settings) 38 | const { mediatype, mediapath } = setBlockMediaProps(blockEl, pv.settings) 39 | 40 | const block: Block = { 41 | blockEl, 42 | speed, 43 | mediatype, 44 | mediapath, 45 | mute: setBlockMute(blockEl, pv.settings), 46 | muted: false, 47 | } 48 | 49 | if (block.mediatype !== MEDIA_TYPES.none) { 50 | if (block.mediatype === MEDIA_TYPES.video) { 51 | container.hasVideoBlock = true 52 | } 53 | 54 | setBlockVisual(block) 55 | setBlockAttributes(container, block) 56 | } 57 | 58 | container.blocks.push(block) 59 | } 60 | 61 | pv.containerArr.push(container) 62 | } 63 | } 64 | 65 | const mergeSettings = ( 66 | userSettings: Partial = {}, 67 | defaultSettings: Settings 68 | ): Settings => { 69 | return { 70 | container: { 71 | ...defaultSettings.container, 72 | ...userSettings.container, 73 | }, 74 | block: { 75 | ...defaultSettings.block, 76 | ...userSettings.block, 77 | }, 78 | } 79 | } 80 | 81 | // Calculates the top offset from an element to the window's || document's top, Link: https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/ 82 | const calculateOffsetTop = (el: HTMLElement) => { 83 | const rectTop = el.getBoundingClientRect().top 84 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop 85 | 86 | return rectTop + scrollTop 87 | } 88 | -------------------------------------------------------------------------------- /src/ts/initBlock.ts: -------------------------------------------------------------------------------- 1 | import { VIDEO_EXTENSIONS, ELEMENT_DATA_KEYS, MEDIA_TYPES } from './constants' 2 | import { Block, Container, Settings, Window } from './types' 3 | 4 | export const setBlockSpeed = (blockEl: Block['blockEl'], settings: Settings) => { 5 | const attrSpeed = blockEl.getAttribute(ELEMENT_DATA_KEYS.SPEED) 6 | 7 | // No data attribute defined 8 | if (!attrSpeed) { 9 | return settings.block.speed 10 | } 11 | 12 | const attrSpeedAsNumber = parseInt(attrSpeed, 10) 13 | if (isNaN(attrSpeedAsNumber)) { 14 | console.error('Invalid type for attribute speed for block: ' + blockEl) 15 | throw new Error('Invalid type for attribute speed for block') 16 | } 17 | 18 | return attrSpeedAsNumber === 0 ? settings.block.speed : attrSpeedAsNumber 19 | } 20 | 21 | export const setBlockMediaProps = (blockEl: Block['blockEl'], settings: Settings) => { 22 | let mediatype = blockEl.getAttribute(ELEMENT_DATA_KEYS.MEDIATYPE) as 23 | | keyof typeof MEDIA_TYPES 24 | | null 25 | const mediapath = blockEl.getAttribute(ELEMENT_DATA_KEYS.MEDIAPATH) 26 | 27 | if (mediatype === MEDIA_TYPES.none) { 28 | return { 29 | mediatype, 30 | mediapath, 31 | } 32 | } 33 | 34 | // No data attribute defined 35 | if (!mediatype) { 36 | mediatype = settings.block.mediatype 37 | } 38 | 39 | // Media type set to video 40 | if (mediapath && isVideo(mediatype, mediapath)) { 41 | mediatype = MEDIA_TYPES.video 42 | } 43 | 44 | // No data attribute defined 45 | if (!mediapath && mediatype !== MEDIA_TYPES.none) { 46 | console.error('Media path not defined for block: ' + blockEl) 47 | throw new Error('Media path not defined') 48 | } 49 | 50 | return { 51 | mediatype, 52 | mediapath, 53 | } 54 | } 55 | 56 | export const setBlockMute = (blockEl: Block['blockEl'], settings: Settings) => { 57 | const attrMute = blockEl.getAttribute(ELEMENT_DATA_KEYS.MUTE) 58 | 59 | if (attrMute !== undefined && attrMute !== null) { 60 | return attrMute === 'true' 61 | } 62 | 63 | return settings.block.mute 64 | } 65 | 66 | const setBlockImage = (block: Block) => { 67 | const { mediapath } = block 68 | 69 | block.blockEl.style.backgroundImage = "url('" + mediapath + "')" 70 | 71 | // Check if the background image wasn't set 72 | const backgroundImageFromDOM = window 73 | .getComputedStyle(block.blockEl) 74 | .getPropertyValue('background-image') 75 | 76 | return backgroundImageFromDOM !== 'none' 77 | } 78 | 79 | const videoElClicked = (videoEl: HTMLVideoElement, block: Block) => { 80 | const { pv } = (window as unknown) as Window 81 | 82 | if (pv.unmutedBlock && pv.unmutedBlock.videoEl !== videoEl) { 83 | if (pv.unmutedBlock.videoEl) { 84 | pv.unmutedBlock.videoEl.muted = true 85 | } 86 | 87 | if (pv.unmutedBlock.audioButton) { 88 | pv.unmutedBlock.audioButton.classList.add('mute') 89 | } 90 | } 91 | 92 | pv.unmutedBlock = block 93 | videoEl.muted = !videoEl.muted 94 | block.muted = videoEl.muted 95 | 96 | if (block.audioButton) { 97 | block.audioButton.classList.toggle('mute') 98 | } 99 | } 100 | 101 | const setBlockVideo = (block: Block) => { 102 | const { mediapath } = block 103 | 104 | const videoEl = document.createElement('video') 105 | videoEl.src = mediapath as string 106 | videoEl.autoplay = true 107 | videoEl.loop = true 108 | videoEl.defaultMuted = true 109 | videoEl.muted = true 110 | block.muted = true 111 | block.videoEl = videoEl 112 | block.blockEl.appendChild(videoEl) 113 | 114 | if (typeof window.orientation === 'undefined') { 115 | if (!block.mute) { 116 | videoEl.addEventListener('click', function() { 117 | videoElClicked(videoEl, block) 118 | }) 119 | const audioButton = document.createElement('a') 120 | audioButton.href = '#' 121 | audioButton.className += 'audio-icon mute' 122 | audioButton.appendChild(document.createElement('span')) 123 | audioButton.addEventListener('click', function(e) { 124 | e.preventDefault() 125 | videoElClicked(videoEl, block) 126 | }) 127 | block.audioButton = audioButton 128 | block.blockEl.insertAdjacentElement('afterend', audioButton) 129 | } 130 | } 131 | 132 | return true 133 | } 134 | 135 | export const setBlockVisual = (block: Block) => { 136 | const { mediatype } = block 137 | 138 | if (mediatype === MEDIA_TYPES.image) { 139 | setBlockImage(block) 140 | return 141 | } 142 | if (mediatype === MEDIA_TYPES.video) { 143 | setBlockVideo(block) 144 | return 145 | } 146 | 147 | console.error('Failed to set media for block:', block) 148 | throw new Error('Failed to set media') 149 | } 150 | 151 | export const setBlockAttributes = (container: Container, block: Block) => { 152 | const { pv } = (window as unknown) as Window 153 | 154 | updateWindowProps() 155 | // calculates the negative top property 156 | // negative scroll distance 157 | // plus container height / factor, because whenever we pass the element we'll always scroll the window faster then the animation (if factor < 1 it'll be increased to all is good) 158 | let marginTop = 0 159 | let scrollDist = 0 160 | let paddingBottom = 0 161 | 162 | // if the pv-block offset is less than the windowheight, then the scrolldist will have to be recalculated 163 | if (container.offset < pv.windowProps.windowHeight) { 164 | scrollDist = (container.height + container.offset) / Math.abs(block.speed) 165 | 166 | if (block.speed > 0) { 167 | marginTop = -Math.abs(container.offset) 168 | paddingBottom = container.height + container.offset 169 | } else { 170 | paddingBottom = scrollDist + container.height 171 | } 172 | } else { 173 | // the pv-block is below the initial windowheight 174 | scrollDist = (container.height + pv.windowProps.windowHeight) / Math.abs(block.speed) 175 | paddingBottom = scrollDist + container.height 176 | 177 | if (block.speed > 0) { 178 | marginTop = -scrollDist 179 | paddingBottom = container.height + pv.windowProps.windowHeight / Math.abs(block.speed) 180 | } else { 181 | paddingBottom = scrollDist + container.height 182 | } 183 | } 184 | 185 | if (Math.abs(marginTop) >= Math.abs(paddingBottom)) paddingBottom = Math.abs(marginTop) + 1 186 | 187 | block.blockEl.style.setProperty('padding-bottom', paddingBottom + 'px') 188 | block.blockEl.style.setProperty('margin-top', marginTop + 'px') 189 | } 190 | 191 | // Returns the extension of a media path 192 | const getExtension = (attrMediapath: string) => { 193 | const extension = attrMediapath 194 | .substr(attrMediapath.lastIndexOf('.') + 1, attrMediapath.length) 195 | .toLowerCase() 196 | 197 | if (extension.length === 0) { 198 | console.error('Invalid extension for media with media path: ' + attrMediapath) 199 | throw new Error('Invalid extension for media') 200 | } 201 | 202 | return extension 203 | } 204 | 205 | // returns `true` if media is a video 206 | const isVideo = (attrMediatype: keyof typeof MEDIA_TYPES, attrMediapath: string) => 207 | attrMediatype === MEDIA_TYPES.video || 208 | VIDEO_EXTENSIONS.indexOf(getExtension(attrMediapath)) !== -1 209 | 210 | const updateWindowProps = () => { 211 | const { pv } = (window as unknown) as Window 212 | 213 | pv.windowProps = { 214 | scrollTop: window.scrollY || document.documentElement.scrollTop, 215 | windowHeight: window.innerHeight, 216 | windowMidHeight: window.innerHeight / 2, 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/ts/initContainer.ts: -------------------------------------------------------------------------------- 1 | import { ELEMENT_DATA_KEYS } from './constants' 2 | import { Container, Settings } from './types' 3 | 4 | export const setContainerHeight = (containerEl: Container['containerEl'], settings: Settings) => { 5 | const attrHeight = containerEl.getAttribute(ELEMENT_DATA_KEYS.HEIGHT) 6 | 7 | // No data attribute 8 | if (!attrHeight) return settings.container.height 9 | 10 | // String only consists of integers, add px 11 | if (!isNaN(Number(attrHeight))) return attrHeight + 'px' 12 | 13 | // String has more than integers, assume suffix is either px or vh 14 | const suffix = attrHeight.substr(attrHeight.length - 2, attrHeight.length) 15 | if (suffix === 'px' || suffix === 'vh') return attrHeight 16 | 17 | throw new Error('Invalid height suffix, expected "px" or "vh" but got: ' + suffix) 18 | } 19 | -------------------------------------------------------------------------------- /src/ts/parallax-vanilla.ts: -------------------------------------------------------------------------------- 1 | import '../less/parallax-vanilla.less' 2 | import { defaultSettings } from './constants' 3 | import { init } from './init' 4 | import { PV, Window } from './types' 5 | import { resize } from './resize' 6 | import { translate } from './translate' 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 9 | ;(window => { 10 | const defineParallaxVanilla = () => { 11 | const pv: PV = { 12 | init, 13 | containerArr: [], 14 | mostReContainerInViewport: -1, 15 | prevScrollTop: -1, 16 | settings: defaultSettings, 17 | windowProps: { 18 | scrollTop: -1, 19 | windowHeight: -1, 20 | windowMidHeight: -1, 21 | }, 22 | } 23 | 24 | window.pv = pv // exposes init function to user 25 | 26 | if (typeof window.orientation === 'undefined') { 27 | window.onresize = () => resize() 28 | } 29 | 30 | // Request animation frame, also binds function to window 31 | window.raf = (() => { 32 | return ( 33 | window.requestAnimationFrame || 34 | window.webkitRequestAnimationFrame || 35 | window.mozRequestAnimationFrame || 36 | function(callback) { 37 | window.setTimeout(callback, 1000 / 60) // 60 FPS 38 | } 39 | ) 40 | })() 41 | 42 | // Main loop for updating variables and performing translates 43 | const mainLoop = () => { 44 | translate() 45 | window.raf(mainLoop) 46 | } 47 | 48 | // Initialize main loop 49 | window.raf(mainLoop) 50 | 51 | return pv 52 | } 53 | 54 | // Define pv 55 | if (typeof window.pv === 'undefined') { 56 | window.pv = defineParallaxVanilla() 57 | console.log('%c parallax-vanilla defined.', 'color: green') 58 | } else { 59 | console.log('%c parallax-vanilla already defined.', 'color: red') 60 | } 61 | })((window as unknown) as Window) 62 | -------------------------------------------------------------------------------- /src/ts/resize.ts: -------------------------------------------------------------------------------- 1 | import { MEDIA_TYPES } from './constants' 2 | import { setBlockAttributes } from './initBlock' 3 | import { Block, Container, Window } from './types' 4 | 5 | export const resize = () => { 6 | const { pv } = (window as unknown) as Window 7 | 8 | pv.containerArr.forEach((container: Container) => { 9 | container.height = container.containerEl.clientHeight 10 | 11 | container.blocks.forEach((block: Block) => { 12 | if (block.mediatype !== MEDIA_TYPES.none) { 13 | setBlockAttributes(container, block) 14 | } 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/ts/translate.ts: -------------------------------------------------------------------------------- 1 | import { Block, Container, Window } from './types' 2 | 3 | export const translate = () => { 4 | const { pv } = (window as unknown) as Window 5 | 6 | // Update selected attributes in windowProps on window raf event 7 | pv.windowProps.scrollTop = window.scrollY || document.documentElement.scrollTop 8 | if (pv.windowProps.scrollTop === pv.prevScrollTop) { 9 | // No scrolling has occured 10 | return 11 | } else { 12 | pv.prevScrollTop = pv.windowProps.scrollTop 13 | } 14 | 15 | // translate the parallax blocks, creating the parallax effect 16 | pv.containerArr.forEach((container: Container, index: number) => { 17 | let calc = 0 18 | 19 | // check if parallax block is in viewport 20 | if (isInViewport(container.offset, container.height)) { 21 | if (index > pv.mostReContainerInViewport) pv.mostReContainerInViewport = index 22 | // if any parallax is within the first windowheight, transform from 0 (pv.scrollTop) 23 | if (container.offset < pv.windowProps.windowHeight) { 24 | calc = pv.windowProps.scrollTop 25 | 26 | // if the parallax is further down on the page 27 | // calculate windowheight - parallax offset + scrollTop to start from 0 whereever it appears 28 | } else { 29 | calc = pv.windowProps.windowHeight - container.offset + pv.windowProps.scrollTop 30 | } 31 | 32 | container.blocks.forEach((block: Block) => { 33 | if (block.videoEl) { 34 | block.videoEl.play() 35 | 36 | if (pv.unmutedBlock === block && !block.muted) { 37 | block.videoEl.muted = block.muted 38 | 39 | if (pv.unmutedBlock.audioButton) { 40 | block.muted 41 | ? pv.unmutedBlock.audioButton.classList.add('mute') 42 | : pv.unmutedBlock.audioButton.classList.remove('mute') 43 | } 44 | } 45 | } 46 | 47 | transform(block.blockEl, 'translate3d(0,' + Math.round(calc / block.speed) + 'px, 0)') 48 | }) 49 | } else { 50 | // check if container has at least one video block 51 | if (container.hasVideoBlock) { 52 | // pause blocks with playing videos 53 | container.blocks.forEach((block: Block) => { 54 | if (block.videoEl) { 55 | block.videoEl.pause() 56 | if (pv.unmutedBlock === block) { 57 | block.videoEl.muted = true 58 | } 59 | } 60 | }) 61 | } 62 | const nextContainer = pv.containerArr[index + 1] 63 | // check if next container is in view - else break 64 | if ( 65 | nextContainer && 66 | !isInViewport(nextContainer.offset, nextContainer.height) && 67 | pv.mostReContainerInViewport < index && 68 | !nextContainerIsSmaller(container, nextContainer) 69 | ) { 70 | return 71 | } else { 72 | if (nextContainer && isInViewport(nextContainer.offset, nextContainer.height)) { 73 | pv.mostReContainerInViewport = index + 1 74 | } 75 | } 76 | } 77 | }) 78 | } 79 | 80 | // Transform prefixes for CSS 81 | const transform = (element: HTMLElement, style: string) => { 82 | element.style.webkitTransform = style 83 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 84 | // @ts-ignore 85 | element.style.MozTransform = style 86 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 87 | // @ts-ignore 88 | element.style.msTransform = style 89 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 90 | // @ts-ignore 91 | element.style.OTransform = style 92 | element.style.transform = style 93 | } 94 | 95 | // Check if the container is in view 96 | const isInViewport = (offset: number, height: number) => { 97 | const { pv } = (window as unknown) as Window 98 | 99 | return ( 100 | pv.windowProps.scrollTop + pv.windowProps.windowHeight - offset > 0 && 101 | pv.windowProps.scrollTop < offset + height 102 | ) 103 | } 104 | 105 | const nextContainerIsSmaller = (container: Container, nextContainer: Container) => 106 | container.offset + container.height > nextContainer.offset + nextContainer.height 107 | -------------------------------------------------------------------------------- /src/ts/types.ts: -------------------------------------------------------------------------------- 1 | import { MEDIA_TYPES } from './constants' 2 | import { init } from './init' 3 | 4 | export interface Settings { 5 | container: { 6 | class: string 7 | height: string 8 | } 9 | block: { 10 | class: string 11 | speed: number 12 | mediatype: keyof typeof MEDIA_TYPES 13 | mediapath: null 14 | mute: boolean 15 | } 16 | } 17 | 18 | export interface Block { 19 | blockEl: HTMLElement 20 | speed: number 21 | mediatype: string | null 22 | mediapath: string | null 23 | mute: boolean 24 | muted: boolean 25 | videoEl?: HTMLVideoElement 26 | audioButton?: HTMLAnchorElement 27 | } 28 | 29 | export interface Container { 30 | containerEl: HTMLElement 31 | offset: number 32 | height: number 33 | blocks: Block[] 34 | hasVideoBlock?: boolean 35 | } 36 | 37 | export interface PV { 38 | containerArr: Container[] 39 | settings: Settings 40 | prevScrollTop: number 41 | mostReContainerInViewport: number 42 | unmutedBlock?: Block 43 | windowProps: { 44 | scrollTop: number 45 | windowHeight: number 46 | windowMidHeight: number 47 | } 48 | init: typeof init 49 | } 50 | 51 | export interface Window { 52 | raf: typeof window.requestAnimationFrame 53 | pv: PV 54 | orientation: typeof window.orientation 55 | requestAnimationFrame: typeof window.requestAnimationFrame 56 | webkitRequestAnimationFrame: typeof window.requestAnimationFrame 57 | mozRequestAnimationFrame: typeof window.requestAnimationFrame 58 | setTimeout: typeof window.setTimeout 59 | onresize: () => void 60 | } 61 | -------------------------------------------------------------------------------- /test/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=d5fc771961d5957ea617f1a16b9ba8a0) 9 | * Config saved to config.json and https://gist.github.com/d5fc771961d5957ea617f1a16b9ba8a0 10 | *//*! 11 | * Bootstrap v3.3.6 (http://getbootstrap.com) 12 | * Copyright 2011-2015 Twitter, Inc. 13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed} -------------------------------------------------------------------------------- /test/css/demo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic body styling 3 | */ 4 | body { 5 | background-color: #f4f4f4; 6 | } 7 | 8 | /** 9 | * Remove padding from all bootstrap columns 10 | */ 11 | [class*='col'], 12 | [class*='container'] { 13 | padding: 0; 14 | } 15 | 16 | /** 17 | * Allow for absolute
 comments
18 |  */
19 | .para-container {
20 |   position: relative;
21 | }
22 | 
23 | /**
24 |  * Comments
25 |  */
26 | pre {
27 |   margin: 0;
28 |   display: inline-block;
29 |   position: absolute;
30 |   top: 0;
31 |   left: 0;
32 |   border-top-left-radius: 0;
33 |   border-top-right-radius: 0;
34 |   border-bottom-left-radius: 0;
35 |   font-size: 12px;
36 |   z-index: 1;
37 |   padding: 5px;
38 |   background-color: rgba(245, 245, 245, 0.5);
39 |   white-space: pre-wrap;
40 | }
41 | 
42 | /**
43 |  * Various filler sized
44 |  */
45 | .filler {
46 |   position: relative;
47 | }
48 | 
49 | .filler > div {
50 |   position: absolute;
51 |   top: 50%;
52 |   left: 50%;
53 |   -webkit-transform: translate(-50%, -50%);
54 |   -moz-transform: translate(-50%, -50%);
55 |   -ms-transform: translate(-50%, -50%);
56 |   -o-transform: translate(-50%, -50%);
57 |   width: 90%;
58 |   text-align: center;
59 | }
60 | 
61 | .pv-container {
62 |   position: relative; /* In order for tooltips to position properly */
63 | }
64 | 
65 | .text-wrapper {
66 |   position: absolute;
67 |   top: 50%;
68 |   left: 50%;
69 |   transform: translate3d(-50%, -50%, 0);
70 |   width: 100%;
71 |   text-align: center;
72 |   color: white;
73 |   z-index: 9;
74 |   text-shadow: 1px 1px 1px black;
75 | }
76 | 


--------------------------------------------------------------------------------
/test/media/Sunrise - 7127.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/Sunrise - 7127.mp4


--------------------------------------------------------------------------------
/test/media/animal.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/animal.mp4


--------------------------------------------------------------------------------
/test/media/butterfly.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/butterfly.jpg


--------------------------------------------------------------------------------
/test/media/dolphin.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/dolphin.mp4


--------------------------------------------------------------------------------
/test/media/earth.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/earth.mov


--------------------------------------------------------------------------------
/test/media/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/favicon.png


--------------------------------------------------------------------------------
/test/media/forest.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/forest.mp4


--------------------------------------------------------------------------------
/test/media/ladybug.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/ladybug.jpg


--------------------------------------------------------------------------------
/test/media/leaves.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/leaves.jpg


--------------------------------------------------------------------------------
/test/media/owl.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/owl.jpg


--------------------------------------------------------------------------------
/test/media/peach.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/peach.jpg


--------------------------------------------------------------------------------
/test/media/plums.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/plums.jpg


--------------------------------------------------------------------------------
/test/media/raindrops.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/raindrops.mp4


--------------------------------------------------------------------------------
/test/media/reflection.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/reflection.jpg


--------------------------------------------------------------------------------
/test/media/sunset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/sunset.jpg


--------------------------------------------------------------------------------
/test/media/tomatoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/tomatoes.jpg


--------------------------------------------------------------------------------
/test/media/water.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/water.mp4


--------------------------------------------------------------------------------
/test/media/zebras.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/zebras.jpg


--------------------------------------------------------------------------------
/test/parallax-vanilla.spec.js:
--------------------------------------------------------------------------------
 1 | const pv = require('../src/js/parallax-vanilla')
 2 | 
 3 | const mockInit = jest.fn()
 4 | jest.mock('../src/js/init', () => mockInit)
 5 | 
 6 | describe('Test cases for parallax-vanilla.js', () => {
 7 |   it('should initialize variables', () => {
 8 |     expect(mockInit).not.toHaveBeenCalled() // should not fire without user input
 9 |     expect(window.raf).toEqual(expect.any(Function))
10 |   })
11 | })
12 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "outDir": "./dist/",
 4 |     "module": "es6",
 5 |     "target": "es2017",
 6 |     "allowJs": true,
 7 |     "baseUrl": "./src",
 8 |     // "paths": { "src": ["./*"] },
 9 |     "esModuleInterop": true,
10 |     "lib": ["es2015", "es2016", "es2017", "dom"],
11 |     "moduleResolution": "node",
12 |     "noImplicitAny": true,
13 |     "sourceMap": true,
14 |     "strict": true,
15 |     "noUnusedParameters": true,
16 |     "noUnusedLocals": true,
17 |     "resolveJsonModule": true,
18 |     "declaration": true
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
 3 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
 4 | const TerserPlugin = require('terser-webpack-plugin')
 5 | 
 6 | module.exports = {
 7 |   mode: 'development',
 8 |   entry: './src/ts/parallax-vanilla.ts',
 9 |   module: {
10 |     rules: [
11 |       {
12 |         test: /\.tsx?$/,
13 |         use: 'ts-loader',
14 |         exclude: /node_modules/,
15 |       },
16 |       {
17 |         test: /\.less$/,
18 |         use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
19 |       },
20 |     ],
21 |   },
22 |   resolve: {
23 |     extensions: ['.ts'],
24 |   },
25 |   output: {
26 |     filename: 'parallax-vanilla.js',
27 |     path: path.resolve(__dirname, 'dist/'),
28 |   },
29 |   plugins: [
30 |     new MiniCssExtractPlugin({
31 |       filename: 'parallax-vanilla.css',
32 |       chunkFilename: 'parallax-vanilla.css',
33 |     }),
34 |     new OptimizeCssAssetsPlugin({
35 |       assetNameRegExp: /\.css$/g,
36 |       cssProcessorPluginOptions: {
37 |         preset: ['default', { discardComments: { removeAll: true } }],
38 |       },
39 |       canPrint: true,
40 |     }),
41 |   ],
42 |   devtool: 'source-map',
43 |   optimization: {
44 |     minimize: true,
45 |     minimizer: [new TerserPlugin()],
46 |   },
47 | }
48 | 


--------------------------------------------------------------------------------