├── .eslintignore ├── .gitignore ├── CHANGELOG.md ├── .prettierrc.json ├── .npmignore ├── tsconfig.json ├── vite.config.ts ├── .github └── workflows │ ├── pullRequest.yml │ └── releaseCreated.yml ├── .eslintrc.json ├── LICENSE ├── package.json ├── README.md └── src └── index.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | .idea 3 | .DS_Store 4 | .vscode 5 | /dist 6 | 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.7] - 2018-7-24 2 | 3 | ### Added 4 | 5 | - Gyro support 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80 7 | 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .husky 3 | .vscode 4 | src 5 | .eslintrc.json 6 | prettierrc.json 7 | tsconfig.json 8 | vite.config.ts 9 | .github 10 | .eslintignore 11 | .prettierrc.json 12 | CHANGELOG.md -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "outDir": "dist/types", 5 | "module": "esnext", 6 | "target": "es5", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | "lib": ["es2018", "dom"], 11 | "moduleResolution": "node" 12 | }, 13 | "include": ["src/index.ts"], 14 | "exclude": ["src/lib/**/tests"], 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import dts from 'vite-plugin-dts'; 4 | // https://vitejs.dev/guide/build.html#library-mode 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: resolve(__dirname, 'src/index.ts'), 9 | name: 'ParallaxContent', 10 | fileName: 'parallaxContent', 11 | }, 12 | }, 13 | plugins: [dts()], 14 | }); 15 | -------------------------------------------------------------------------------- /.github/workflows/pullRequest.yml: -------------------------------------------------------------------------------- 1 | name: pull-request 2 | on: 3 | pull_request: 4 | branches: 5 | - 'master' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code 11 | uses: actions/checkout@v4 12 | - name: Set up node 13 | uses: actions/setup-node@v3 14 | - name: Install dependencies 15 | run: npm install 16 | - name: Run linter 17 | run: npm run lint 18 | - name: Run build 19 | run: npm run build -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "modules": true, 8 | "experimentalObjectRestSpread": true 9 | } 10 | }, 11 | "plugins": ["@typescript-eslint", "prettier"], 12 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 13 | "rules": { 14 | "prettier/prettier": 1, 15 | "no-unused-vars": 0, 16 | "@typescript-eslint/no-unused-vars": ["error"], 17 | "@typescript-eslint/no-this-alias": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/releaseCreated.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build-and-publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out code 10 | uses: actions/checkout@v4 11 | - name: Set up node 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: '16.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Run liter 19 | run: npm run lint 20 | - name: Deploy 21 | run: | 22 | npm run build 23 | npm publish --access public 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Illia Liemiehov 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parallax_content", 3 | "version": "2.0.1", 4 | "description": "VanillaJS parallax content plugin", 5 | "types": "./dist/index.d.ts", 6 | "main": "./dist/parallaxContent.umd.js", 7 | "module": "./dist/parallaxContent.mjs", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/parallaxContent.mjs", 12 | "require": "./dist/parallaxContent.umd.js" 13 | } 14 | }, 15 | "devDependencies": { 16 | "@lemehovskiy/scroller": "^0.2.7", 17 | "@types/jquery": "^3.5.29", 18 | "@types/node": "^20.10.4", 19 | "@typescript-eslint/eslint-plugin": "^6.13.2", 20 | "@typescript-eslint/parser": "^6.13.2", 21 | "eslint": "^8.55.0", 22 | "eslint-plugin-prettier": "^5.0.1", 23 | "husky": "^8.0.3", 24 | "lint-staged": "^15.2.0", 25 | "prettier": "^3.1.0", 26 | "typescript": "^5.3.3", 27 | "vite": "^5.0.6", 28 | "vite-plugin-dts": "^3.6.4" 29 | }, 30 | "peerDependencies": { 31 | "gsap": "^3.12.4" 32 | }, 33 | "scripts": { 34 | "build": "tsc && vite build", 35 | "lint": "npm run lint:eslint && npm run lint:prettier", 36 | "lint:eslint": "eslint src/ --ext .ts --max-warnings=0", 37 | "lint:prettier": "prettier 'src/**/*.ts' --check", 38 | "fix": "npm run fix:prettier && npm run fix:eslint", 39 | "fix:eslint": "eslint src/ --ext .ts --fix --max-warnings=0", 40 | "fix:prettier": "prettier 'src/**/*.ts' --write", 41 | "prepare": "husky install" 42 | }, 43 | "keywords": [ 44 | "parallax", 45 | "gsap", 46 | "greensock", 47 | "parallax scroll", 48 | "plugin", 49 | "animation" 50 | ], 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/lemehovskiy/parallax-content" 54 | }, 55 | "author": "lemehovskiy", 56 | "license": "ISC" 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## parallax-content 2 | 3 | Create captivating parallax contents effortlessly with the ParallaxContent plugin. This lightweight Vanilla JavaScript plugin, powered by GSAP animation, supports Scroll and Gyroscope events. 4 | 5 | Features: 6 | 7 | * Vanilla JavaScript and GSAP powered 8 | * Scroll, Gyroscope events 9 | * Customizable shift and animation duration 10 | 11 | ### Demo 12 | 13 | [Basic demo](https://codesandbox.io/p/devbox/parallax-content-basic-demo-pk4crm) 14 | 15 | ### Package Managers 16 | 17 | ```sh 18 | # NPM 19 | npm install parallax_content 20 | ``` 21 | 22 | ### Installation 23 | 24 | #### Include js 25 | 26 | ```html 27 | 28 | 29 | ``` 30 | 31 | #### Set HTML 32 | 33 | ```html 34 |
Parallax title
35 | ``` 36 | 37 | #### Call the plugin 38 | 39 | ```html 40 | 48 | ``` 49 | 50 | #### In result 51 | 52 | ```html 53 | 54 | 55 | My website 56 | 57 | 58 |
Parallax title
59 | 60 | 61 | 62 | //optional for jQuery initialize 63 | 64 | 65 | 70 | 78 | 79 | 80 | ``` 81 | 82 | ### Data Attribute Settings 83 | 84 | In parallaxContent you can add settings using the data-parallax-content attribute. You still need to call 85 | new ParallaxContent(selector) 86 | to initialize parallaxContent on the element. 87 | 88 | Example: 89 | 90 | ```html 91 |
92 | ``` 93 | 94 | ### Settings 95 | 96 | | Option | Type | Default | 97 | | --------------- | ----- | ------------------ | 98 | | events | [SCROLL, GYRO] | [SCROLL] | 99 | | shift | int | 10 | 100 | | duration | int | 1.5 | 101 | | gyroSensitivity | int | 30 | 102 | 103 | ### Browser support 104 | 105 | * Chrome 106 | * Firefox 107 | 108 | ### Dependencies 109 | 110 | * GSAP animation library (Version 3.10.4) 111 | 112 | ## Contributing 113 | 114 | If you'd like to get involved, please consider opening an issue or submitting a pull request. Your input is highly valued, and I'm enthusiastic about collaborating to enhance this tool. 115 | 116 | ## License 117 | 118 | parallax-content is released under the MIT License. See the [LICENSE](LICENSE) file for comprehensive details regarding the terms and conditions of use. 119 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface JQuery { 3 | parallaxContent(arg: Partial): void; 4 | } 5 | } 6 | 7 | declare global { 8 | interface Window { 9 | $: JQuery; 10 | } 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | declare let gsap: any; 15 | 16 | import Scroller from '@lemehovskiy/scroller'; 17 | 18 | enum EventTypes { 19 | Scroll = 'SCROLL', 20 | Gyro = 'GYRO', 21 | } 22 | 23 | enum DeviceOrientationTypes { 24 | Landscape = 'LANDSCAPE', 25 | Portrait = 'PORTRAIT', 26 | } 27 | 28 | type OptionsType = { 29 | events: Array; 30 | shift: number; 31 | duration: number; 32 | gyroSensitivity: number; 33 | }; 34 | 35 | type AnimationParamsType = { 36 | x?: number; 37 | y?: number; 38 | }; 39 | 40 | export default class ParallaxContent { 41 | element: HTMLElement; 42 | settings: OptionsType; 43 | elementSize: [number, number]; 44 | shift: number; 45 | doubleShift: number; 46 | deviceOrientation: DeviceOrientationTypes | undefined; 47 | animationLength: number; 48 | isVisible: boolean; 49 | positionX: number; 50 | positionY: number; 51 | 52 | constructor(element: HTMLElement, options?: Partial) { 53 | this.settings = { 54 | events: [EventTypes.Scroll], 55 | shift: 30, 56 | duration: 1.5, 57 | gyroSensitivity: 30, 58 | ...options, 59 | }; 60 | 61 | this.element = element; 62 | const dataOptions = JSON.parse( 63 | this.element.getAttribute('data-parallax-content') 64 | ); 65 | this.settings = { ...this.settings, ...dataOptions }; 66 | this.elementSize = [0, 0]; 67 | this.deviceOrientation = undefined; 68 | this.animationLength = -this.settings.shift + this.settings.shift * 2; 69 | this.isVisible = false; 70 | 71 | this.init(); 72 | } 73 | 74 | private init() { 75 | if (typeof gsap === 'undefined') { 76 | console.warn( 77 | 'Gsap library is required... https://gsap.com/docs/v3/Installation/' 78 | ); 79 | return; 80 | } 81 | 82 | const observer = new IntersectionObserver( 83 | (e) => { 84 | this.isVisible = e[0].isIntersecting; 85 | }, 86 | { root: null, rootMargin: '0px' } 87 | ); 88 | observer.observe(this.element); 89 | 90 | new Set(this.settings.events).forEach((event) => { 91 | if (event === EventTypes.Scroll) { 92 | this.subscribeScrollEvent(); 93 | } else if (event === EventTypes.Gyro) { 94 | this.subscribeGyroEvent(); 95 | } 96 | }); 97 | } 98 | 99 | private updateOrientation() { 100 | if (window.innerWidth > window.innerHeight) { 101 | this.deviceOrientation = DeviceOrientationTypes.Landscape; 102 | } else { 103 | this.deviceOrientation = DeviceOrientationTypes.Portrait; 104 | } 105 | } 106 | 107 | private subscribeGyroEvent() { 108 | this.updateOrientation(); 109 | 110 | let lastGamma = 0, 111 | lastBeta = 0, 112 | rangeGamma = this.settings.gyroSensitivity / 2, 113 | rangeBeta = this.settings.gyroSensitivity / 2; 114 | 115 | window.addEventListener('resize', () => { 116 | this.updateOrientation(); 117 | }); 118 | 119 | window.addEventListener( 120 | 'deviceorientation', 121 | (e) => { 122 | if (!this.isVisible) return; 123 | const roundedGamma = Math.round(e.gamma || 0), 124 | roundedBeta = Math.round(e.beta || 0); 125 | 126 | if (roundedBeta > lastBeta && rangeBeta > 0) { 127 | rangeBeta--; 128 | } else if ( 129 | roundedBeta < lastBeta && 130 | rangeBeta < this.settings.gyroSensitivity 131 | ) { 132 | rangeBeta++; 133 | } 134 | 135 | if (roundedGamma > lastGamma && rangeGamma > 0) { 136 | rangeGamma--; 137 | } else if ( 138 | roundedGamma < lastGamma && 139 | rangeGamma < this.settings.gyroSensitivity 140 | ) { 141 | rangeGamma++; 142 | } 143 | 144 | lastGamma = roundedGamma; 145 | lastBeta = roundedBeta; 146 | 147 | const gammaProgress = rangeGamma / this.settings.gyroSensitivity; 148 | const betaProgress = rangeBeta / this.settings.gyroSensitivity; 149 | 150 | if (this.deviceOrientation === DeviceOrientationTypes.Landscape) { 151 | this.animate(gammaProgress, betaProgress); 152 | } else { 153 | this.animate(betaProgress, gammaProgress); 154 | } 155 | }, 156 | true 157 | ); 158 | } 159 | 160 | private animate(progressY: number, progressX?: number, isSet = false) { 161 | const y = this.animationLength * progressY; 162 | const x = this.animationLength * progressX; 163 | const params: AnimationParamsType = { y }; 164 | 165 | if (progressX !== undefined) { 166 | params.x = x; 167 | } 168 | 169 | if (isSet) { 170 | gsap.killTweensOf(this.element); 171 | gsap.set(this.element, params); 172 | } else { 173 | gsap.to(this.element, this.settings.duration, params); 174 | } 175 | } 176 | 177 | private subscribeScrollEvent() { 178 | const scroller = new Scroller(this.element, { 179 | autoAdjustScrollOffset: true, 180 | }); 181 | 182 | scroller.progressChanged.on((progress) => { 183 | if (this.isVisible) { 184 | this.animate(progress); 185 | } else { 186 | this.animate(progress, undefined, true); 187 | } 188 | }); 189 | } 190 | } 191 | 192 | if (window.$ !== undefined) { 193 | $.fn.parallaxContent = function ( 194 | ...params: [Partial] | Array 195 | ) { 196 | const opt = params[0]; 197 | const args = Array.prototype.slice.call(params, 1); 198 | const length = this.length; 199 | let ret = undefined; 200 | for (let i = 0; i < length; i++) { 201 | if (typeof opt === 'object' || typeof opt === 'undefined') { 202 | this[i].parallaxContent = new ParallaxContent(this[i], opt); 203 | } else { 204 | // eslint-disable-next-line prefer-spread 205 | ret = this[i].parallaxContent[opt].apply(this[i].parallaxContent, args); 206 | } 207 | if (typeof ret !== 'undefined') return ret; 208 | } 209 | return this; 210 | }; 211 | } 212 | --------------------------------------------------------------------------------