├── .babelrc ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── ---feature-request.md └── stale.yml ├── .gitignore ├── .npmignore ├── README.md ├── demos ├── air-hockey.vue ├── gifs │ ├── air-hockey.gif │ ├── basic.gif │ ├── config.gif │ ├── innet text.gif │ ├── superheros.gif │ └── svh.gif └── superheros.vue ├── docs ├── .vuepress │ ├── Chart.vue │ ├── config.js │ ├── enhanceApp.js │ └── public │ │ └── leaps.svg ├── README.md ├── getting-started.md ├── leaps.md ├── parallax.md └── reveal.md ├── package.json ├── scripts ├── build.js ├── config.js ├── docs-deploy.sh └── watcher.js ├── src ├── components │ ├── Leaps.js │ ├── Parallax.js │ ├── Reveal.js │ └── Timeline.js └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-object-rest-spread"] 3 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 0.0.x] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 0.0.x] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F407 Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Label to use when marking as stale 6 | staleLabel: false 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *. 4 | *.log 5 | demo -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | scripts 3 | src 4 | .gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | leaps logo 4 | 5 |

6 | 7 |

8 | Average time to resolve an issue 9 | Percentage of issues still open 10 | npm 11 | npm 12 |

13 | 14 | 15 |

16 | Inner Text Animation 17 | Basic Animation 18 | SVG Animation 19 |

20 | 21 |

22 | Inner Text Animation 23 |

24 | 25 |

26 | Inner Text Animation 27 |

28 | 29 | # Leaps 30 | 31 | Leaps is a set of simple, physics-based Vue.js animation components. It covers the most of your UI related animation needs where CSS just isn't enough anymore. 32 | 33 | ## Shipped with 34 | 35 | * [Leaps](https://baianat.github.io/leaps/leaps.html) 36 | * The primary animation component, which is a spring-physics based. Its main role is to move property from one value to another, with more natural animation and easing. 37 | * [Parallax](https://baianat.github.io/leaps/parallax.html) 38 | * Used move property from one value to another, based on the scrolled distance. 39 | * [Reveal](https://baianat.github.io/leaps/Reveal.html) 40 | * Used to apply CSS animation class to an element, when it enters the view-port. 41 | 42 | ## Why Physics 43 | 44 | Traditional animation methods are based on duration time and ease function, while the animation goes from the start state to the end state, event if using Bézier easing can be very limiting. Due to having only two handles, you can't produce some complex physics effects. If you go beyond three sequences CSS become complex with delays and you end up having to do a lot of recalculation if you adjust the timing. 45 | 46 | Hard-coded durations are opposed to continuous, fluid interactivity. If your animation is interrupted mid-way, you'd get a weird completion animation if you hard-coded the time. 47 | Instead of hard-coded duration, we will use a [physical model](https://en.wikipedia.org/wiki/Damping_ratio) to determine our animation duration and easing, based on the element dumping, and mass. Instead of guessing the animation parameters that fits best with your animation, there is an interactive configuration section in the documentation that will guide you. 48 | 49 |

50 | Config Animation 51 |

-------------------------------------------------------------------------------- /demos/air-hockey.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 55 | 56 | 73 | 74 | -------------------------------------------------------------------------------- /demos/gifs/air-hockey.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/leaps/94769e57956a4d751ab0715ac3a4ed457c772aee/demos/gifs/air-hockey.gif -------------------------------------------------------------------------------- /demos/gifs/basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/leaps/94769e57956a4d751ab0715ac3a4ed457c772aee/demos/gifs/basic.gif -------------------------------------------------------------------------------- /demos/gifs/config.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/leaps/94769e57956a4d751ab0715ac3a4ed457c772aee/demos/gifs/config.gif -------------------------------------------------------------------------------- /demos/gifs/innet text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/leaps/94769e57956a4d751ab0715ac3a4ed457c772aee/demos/gifs/innet text.gif -------------------------------------------------------------------------------- /demos/gifs/superheros.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/leaps/94769e57956a4d751ab0715ac3a4ed457c772aee/demos/gifs/superheros.gif -------------------------------------------------------------------------------- /demos/gifs/svh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/leaps/94769e57956a4d751ab0715ac3a4ed457c772aee/demos/gifs/svh.gif -------------------------------------------------------------------------------- /demos/superheros.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 72 | 73 | -------------------------------------------------------------------------------- /docs/.vuepress/Chart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Leaps', 3 | description: 'Vue.js physics based animation library', 4 | base: '/leaps/', 5 | serviceWorker: true, 6 | head: [ 7 | ['meta', { charset: 'utf-8' }], 8 | ['meta', { name: "theme-color", content: "#41b883" }], 9 | ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }], 10 | ['meta', { property: 'og:image', content: '/leaps/leaps.svg' }], 11 | ], 12 | themeConfig: { 13 | repo: 'baianat/leaps', 14 | docsRepo: 'baianat/leaps', 15 | docsDir: 'docs', 16 | docsBranch: 'master', 17 | editLinks: true, 18 | locales: { 19 | '/': { 20 | label: 'English', 21 | selectText: 'Languages', 22 | editLinkText: 'Help us improve this page!', 23 | nav: [ 24 | { text: 'Leaps', link: '/leaps' }, 25 | { text: 'Parallax', link: '/parallax' }, 26 | { text: 'Reveal', link: '/reveal' } 27 | ], 28 | sidebar: [ 29 | 'getting-started', 30 | 'leaps', 31 | 'parallax', 32 | 'reveal' 33 | ] 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import { Leaps, Parallax, Reveal, install } from '../../dist/leaps.esm.js'; 2 | import Chart from './Chart.vue'; 3 | 4 | export default ({ Vue }) => { 5 | Vue.use(install); 6 | Vue.component('Leaps', Leaps); 7 | Vue.component('Parallax', Parallax); 8 | Vue.component('Reveal', Reveal); 9 | Vue.component('Chart', Chart); 10 | }; 11 | -------------------------------------------------------------------------------- /docs/.vuepress/public/leaps.svg: -------------------------------------------------------------------------------- 1 | leaps -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /leaps.svg 4 | actionText: Getting Started → 5 | actionLink: /getting-started 6 | --- 7 | 8 | 24 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Leaps 4 | 5 | Leaps is a set of simple, physics-based Vue.js animation components. It covers the most of your UI related animation needs where CSS just isn't enough anymore. 6 | 7 | Leaps does not have to resort to using hard-coded easing curves and duration. Set up a stiffness and damping for your UI element, and let the magic of physics take care of the rest This isn't meant to solve each and every problem, but rather give you tools that are flexible enough to confidently cast ideas into moving interfaces. 8 | 9 | ## Available Components 10 | 11 | * [Leaps](/leaps.html) 12 | * [Parallax](/parallax.html) 13 | * [Reveal](/reveal.html) 14 | 15 | ## Installation 16 | 17 | First step is to install it using `yarn` or `npm`: 18 | 19 | ```bash 20 | npm install leaps 21 | 22 | # or use yarn 23 | yarn add leaps 24 | ``` -------------------------------------------------------------------------------- /docs/leaps.md: -------------------------------------------------------------------------------- 1 | # Leaps 2 | 3 | `Lepas` is the primary animation component, which is a spring-physics based. Its main role is to move property from one value to another, with more natural animation and easing. 4 | 5 | ## Using 6 | 7 | `Leaps` is a render-less component, which accepts `from` and `to` props, and provide you with the current state using `leaps` [scoped-slot](https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots). 8 | 9 | ### basic example 10 | 11 | 12 |
17 |
18 | 19 | ```vue 20 | 21 |
26 |
27 | ``` 28 | 29 | ### animate attributes 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | ```vue 39 | 40 | 43 | 45 | 46 | 47 | ``` 48 | 49 | ### inner text 50 | 51 | 52 | {{ Math.round(leaps.text) }} 53 | 54 | 55 | ```vue 56 | 57 | {{ Math.round(leaps.text) }} 58 | 59 | ``` 60 | 61 | ## Configuration 62 | 63 | The animation duration and easing is based on physics parameters (stiffness, damping and mass), changing one of those parms or all can give you a different animation behavior. in this demo section there's a great "Spring Parameters Chooser" for you to have a feel of what spring is appropriate, rather than guessing a duration in the dark. 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
78 | {{ Math.round(leaps.x * 100) / 100 }} 79 |
80 |
81 | 82 | 83 | 84 | ## Props 85 | 86 | |Prop |Default |Description| 87 | |------------|-----|-----------| 88 | |`from` |{} |start values| 89 | |`to` |1 |end values| 90 | |`stiffness` |170 |the force required to cause element deflection| 91 | |`damping` |26 |element damping force| 92 | |`mass` |1 |element mass| 93 | |`velocity` |0 |element starting velocity| 94 | |`precision` |0.01|animation damped precision| 95 | |`direction` |'forwards'|set animation direction to be forward, reverse, or alternate| 96 | 97 | -------------------------------------------------------------------------------- /docs/parallax.md: -------------------------------------------------------------------------------- 1 | # Parallax 2 | 3 | `Parallax` component used move property from one value to another, based on the scrolled distance. 4 | 5 | ## Using 6 | 7 | `Parallax` is a render-less component, which accepts `from` and `to` props, and provide you with the current state using `parallax` [scoped-slot](https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots). 8 | 9 | ```vue 10 | 11 |
16 |
17 | ``` 18 | 19 | The element will have the starting value, when its top enters the view-port, the value will accelerate when view-port scrolls, and will reach its ending value when the element leaves the view-port. 20 | 21 | 22 |
{{ parallax.x }}
27 |
28 | 29 | ```vue 30 | 31 |
36 |
37 | ``` 38 | 39 | You can set ratio of view-port, that the element should travel to reach its ending value, using `viewportRatio` prop. 40 | 41 | 42 |
{{ parallax.x }}
47 |
48 | 49 | ```vue 50 | 51 |
{{ parallax.x }}
56 |
57 | ``` 58 | 59 | In the previous example, the element reached its ending value, when the element bottom arrives to the half of the view-port. 60 | If you want to reach the ending value when element top edge reached the half of the view-port, you can set `useElHeight` prop to be false. 61 | 62 | 63 |
{{ parallax.x }}
68 |
69 | 70 | ```vue 71 | 72 |
77 |
78 | ``` 79 | 80 | ## Props 81 | 82 | |Prop |Default|Description| 83 | |---------------|-------|-----------| 84 | |`from` |{} |start values| 85 | |`to` |{} |end values| 86 | |`viewportRatio`|'1' |ratio of the view-port, where the element should reach the end value| 87 | |`useElHeight` |true |flag to indicated, whether using element's height in calculations or not| 88 | -------------------------------------------------------------------------------- /docs/reveal.md: -------------------------------------------------------------------------------- 1 | # Reveal 2 | 3 | `Reveal` component used to apply CSS animation class to the element when it enters the view-port. 4 | You can create your desired animation, or use [animista](http://animista.net/), (animate.css)[https://daneden.github.io/animate.css/], or any similar library. 5 | 6 | ## Install Plug-in 7 | 8 | Reveal component requires to install it as a global plugin. 9 | 10 | ```js 11 | import Vue from 'vue'; 12 | import { Reveal, install as RevealPlugin } from '../dist/leaps'; 13 | 14 | Vue.use(RevealPlugin, { options }); 15 | Vue.component('Reveal', Reveal); 16 | ``` 17 | 18 | ## Example 19 | 20 | 21 |
22 |
23 | 24 | ```vue 25 | 26 |
27 |
28 | ``` 29 | 30 | 31 | ## Component Props 32 | 33 | |Prop |Default|Description| 34 | |--------------|-------|-----------| 35 | |`duration` |'1s' |Length of time that an animation takes to complete one cycle.| 36 | |`delay` |'0s' |When an animation starts. accepts negative values | 37 | |`iteration` |1 |The number of times an animation cycle should be played before stopping.| 38 | |`animateClass`|'animated'|Main class name that triggers animation| 39 | |`name` |'' |The animation class name| 40 | |`tag` |'span' |Element tag name| 41 | |`visible` |false |Set if element starts visible or hidden| 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaps", 3 | "version": "0.0.9", 4 | "description": "Declarative Vue.js animations library", 5 | "module": "dist/leaps.esm.js", 6 | "unpkg": "dist/leaps.js", 7 | "main": "dist/leaps.js", 8 | "scripts": { 9 | "lint": "eslint ./src --fix", 10 | "build": "NODE_ENV=production node scripts/build", 11 | "demo": "webpack-dev-server --hot --inline --config ./demo/webpack.config.js", 12 | "dev": "node scripts/watcher", 13 | "docs:dev": "vuepress dev docs", 14 | "docs:build": "vuepress build docs", 15 | "docs:deploy": "bash scripts/docs-deploy.sh", 16 | "test": "jest --config jest.config.json" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.1.2", 20 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 21 | "browser-sync": "^2.26.3", 22 | "chalk": "^2.4.2", 23 | "chart.js": "^2.7.3", 24 | "filesize": "^4.0.0", 25 | "fs": "^0.0.1-security", 26 | "gzip-size": "^5.0.0", 27 | "mkdirp": "^0.5.1", 28 | "path": "^0.12.7", 29 | "rollup": "^1.1.0", 30 | "rollup-plugin-babel": "^4.3.0", 31 | "rollup-plugin-commonjs": "^9.2.2", 32 | "rollup-plugin-node-resolve": "^4.0.0", 33 | "rollup-plugin-replace": "^2.1.0", 34 | "rollup-plugin-vue": "^4.6.1", 35 | "style-loader": "^0.23.1", 36 | "util": "^0.11.1", 37 | "vue": "2.5.22", 38 | "vuepress": "^1.0.0-alpha.32" 39 | }, 40 | "license": "MIT", 41 | "keywords": [], 42 | "maintainers": [ 43 | { 44 | "name": "Abdelrahman3D", 45 | "email": "abdelrahman3d@gmail.com" 46 | } 47 | ], 48 | "optionalDependencies": {}, 49 | "dependencies": { 50 | "lodash.merge": "^4.6.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const filesize = require('filesize'); 3 | const gzipSize = require('gzip-size'); 4 | const path = require('path'); 5 | const chalk = require('chalk'); 6 | const mkdirpNode = require('mkdirp'); 7 | const { rollup } = require('rollup'); 8 | const { promisify } = require('util'); 9 | const { builds, paths } = require('./config'); 10 | 11 | const mkdirp = promisify(mkdirpNode); 12 | 13 | build('umd'); 14 | build('es'); 15 | 16 | async function build (format) { 17 | await mkdirp(paths.dist); 18 | console.log(chalk.cyan(`Generating ${format} build...`)); 19 | 20 | const bundle = await rollup(builds.input); 21 | const { output } = await bundle.generate({ 22 | format, 23 | ...builds.output 24 | }); 25 | const code = output[0].code; 26 | let extensions = format === 'es' ? '.esm' : ''; 27 | const outputPath = path.join(paths.dist, `leaps${extensions}.js`); 28 | fs.writeFile(outputPath, code, (err) => { 29 | if (err) { 30 | throw err; 31 | } 32 | let stats = getStats({ code, path: outputPath }); 33 | console.log(`${chalk.green(`Output File: leaps ${format}`).padEnd(35, ' ')} ${stats}`); 34 | }); 35 | } 36 | 37 | function getStats ({ path, code }) { 38 | const { size } = fs.statSync(path); 39 | const gzipped = gzipSize.sync(code); 40 | 41 | return `Size: ${filesize(size)} | Gzip: ${filesize(gzipped)}`; 42 | } 43 | 44 | module.exports = { 45 | build 46 | }; -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const resolve = require('rollup-plugin-node-resolve'); 3 | const babel = require('rollup-plugin-babel'); 4 | const replace = require('rollup-plugin-replace'); 5 | const vue = require('rollup-plugin-vue'); 6 | const commonjs = require('rollup-plugin-commonjs'); 7 | const version = process.env.VERSION || require('../package.json').version; 8 | 9 | const paths = { 10 | src: path.join(__dirname, '../src'), 11 | dist: path.join(__dirname, '../dist') 12 | } 13 | const builds = { 14 | input: { 15 | input: path.join(paths.src, 'index.js'), 16 | plugins: [ 17 | replace({ __VERSION__: version }), 18 | vue(), 19 | babel(), 20 | resolve(), 21 | commonjs() 22 | ] 23 | }, 24 | output: { 25 | name: 'leaps', 26 | exports: 'named', 27 | banner: 28 | `/** 29 | * Leaps ${version} 30 | * (c) ${new Date().getFullYear()} 31 | * @license MIT 32 | */`, 33 | outputFolder: path.join(__dirname, '../dist'), 34 | } 35 | }; 36 | 37 | const uglifyOptions = { 38 | toplevel: true, 39 | compress: true, 40 | mangle: true 41 | } 42 | 43 | module.exports = { 44 | paths, 45 | builds, 46 | uglifyOptions 47 | }; -------------------------------------------------------------------------------- /scripts/docs-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | npm run docs:build 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | git init 13 | git add -A 14 | git commit -m 'deploy' 15 | 16 | git push -f git@github.com:baianat/leaps.git master:gh-pages 17 | cd - -------------------------------------------------------------------------------- /scripts/watcher.js: -------------------------------------------------------------------------------- 1 | const { paths } = require('./config'); 2 | const { build } = require('./build'); 3 | const bs = require('browser-sync').create(); 4 | 5 | bs.init({ 6 | open: false, 7 | ui: false, 8 | files: [ 9 | paths.dist, { 10 | match: paths.src, 11 | fn (event, file) { 12 | build('umd'); 13 | } 14 | } 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/Leaps.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Leaps', 3 | props: { 4 | from: { 5 | default() { 6 | return {}; 7 | }, 8 | type: Object, 9 | }, 10 | to: { 11 | default() { 12 | return {}; 13 | }, 14 | type: Object, 15 | }, 16 | // spring stiffness, in kg / s^2 17 | stiffness: { 18 | default: 170, 19 | type: Number, 20 | }, 21 | // damping constant, in kg / s 22 | damping: { 23 | default: 26, 24 | type: Number, 25 | }, 26 | // spring mass 27 | mass: { 28 | default: 1, 29 | type: Number, 30 | }, 31 | // initial velocity 32 | velocity: { 33 | default: 0, 34 | type: Number, 35 | }, 36 | // precision 37 | precision: { 38 | default: 0.1, 39 | type: Number, 40 | }, 41 | // animation direction, forward, reverse, or alternate 42 | direction: { 43 | default: 'forward', 44 | type: String, 45 | }, 46 | }, 47 | data() { 48 | return { 49 | looping: '', 50 | frameRate: 1 / 60, // how many frame per ms 51 | start: {}, 52 | end: {}, 53 | leaps: {}, 54 | AnimationRequestID: 0, 55 | velocities: {}, 56 | isReverse: (() => this.direction === 'reverse')(), 57 | isAlternate: (() => this.direction === 'alternate')(), 58 | }; 59 | }, 60 | computed: { 61 | isLeapEnd() { 62 | return Object.keys(this.velocities).every(key => { 63 | return this.velocities[key] === 0; 64 | }); 65 | }, 66 | }, 67 | watch: { 68 | to() { 69 | window.requestAnimationFrame(this.leap); 70 | }, 71 | from() { 72 | this.setup(); 73 | }, 74 | }, 75 | methods: { 76 | setup() { 77 | Object.keys(this.to).forEach(key => { 78 | if (!this.from[key]) { 79 | this.$set(this.from, key, 0); 80 | } 81 | this.$set(this.velocities, key, this.velocity); 82 | this.$set(this.leaps, key, this.isReverse ? this.to[key] : this.from[key]); 83 | }); 84 | }, 85 | leap() { 86 | const end = this.isReverse ? this.from : this.to; 87 | 88 | Object.keys(this.to).forEach(key => { 89 | const springForce = -this.stiffness * (this.leaps[key] - end[key]); 90 | const damperForce = -this.damping * this.velocities[key]; 91 | const acceleration = (springForce + damperForce) / this.mass; 92 | 93 | this.velocities[key] += acceleration * this.frameRate; 94 | this.leaps[key] += this.velocities[key] * this.frameRate; 95 | 96 | if (this.isDumped(this.velocities[key], this.leaps[key] - end[key])) { 97 | this.velocities[key] = 0; 98 | this.leaps[key] = Number(end[key]); 99 | } 100 | }); 101 | if (!this.isLeapEnd) { 102 | window.cancelAnimationFrame(this.AnimationRequestID); 103 | this.AnimationRequestID = window.requestAnimationFrame(this.leap); 104 | } 105 | }, 106 | isDumped(velocity, distance) { 107 | return Math.abs(velocity) < this.precision && Math.abs(distance) < this.precision; 108 | }, 109 | }, 110 | created() { 111 | this.setup(); 112 | }, 113 | mounted() { 114 | window.requestAnimationFrame(this.leap); 115 | }, 116 | render() { 117 | return this.$scopedSlots.default({ 118 | leaps: this.leaps, 119 | }); 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /src/components/Parallax.js: -------------------------------------------------------------------------------- 1 | let PARALLAX_OBSERVERS_FLAG; 2 | let PARALLAX_ELEMENTS = []; 3 | let SCROLLED; 4 | 5 | // scroll optimization https://developer.mozilla.org/en-US/docs/Web/Events/scroll 6 | function scrollHandler () { 7 | SCROLLED = window.scrollY; 8 | let ticking = false; 9 | if (!ticking) { 10 | window.requestAnimationFrame(() => { 11 | PARALLAX_ELEMENTS.forEach(el => el.update()); 12 | ticking = false; 13 | }); 14 | ticking = true; 15 | } 16 | } 17 | function resizeHandler () { 18 | let ticking = false; 19 | if (!ticking) { 20 | window.requestAnimationFrame(() => { 21 | PARALLAX_ELEMENTS.forEach(el => { 22 | el.updateConfig(); 23 | el.update(); 24 | }); 25 | ticking = false; 26 | }); 27 | ticking = true; 28 | } 29 | } 30 | 31 | function initObservers () { 32 | PARALLAX_OBSERVERS_FLAG = true; 33 | if (SCROLLED === undefined) { 34 | SCROLLED = window.scrollY; 35 | } 36 | window.addEventListener('scroll', scrollHandler, { passive: true }); 37 | window.addEventListener('resize', resizeHandler, { passive: true }); 38 | } 39 | 40 | function destroyObservers () { 41 | PARALLAX_OBSERVERS_FLAG = false; 42 | window.removeEventListener('scroll', scrollHandler, { passive: true }); 43 | window.removeEventListener('resize', resizeHandler, { passive: true }); 44 | } 45 | 46 | export default { 47 | name: 'LeapsParallax', 48 | props: { 49 | from: { 50 | default: {}, 51 | type: Object 52 | }, 53 | to: { 54 | default: {}, 55 | type: Object 56 | }, 57 | viewportRatio: { 58 | default: 1, 59 | type: Number 60 | }, 61 | useElHeight: { 62 | default: true, 63 | type: Boolean 64 | } 65 | }, 66 | data () { 67 | return { 68 | scrolled: 0, 69 | viewportHeight: 0, 70 | viewportWidth: 0, 71 | moved: 0, 72 | elRect: null, 73 | unitPerScroll: {}, 74 | parallax: {} 75 | } 76 | }, 77 | methods: { 78 | updateConfig () { 79 | const elRect = this.$el.getBoundingClientRect(); 80 | this.elRect = { 81 | top: elRect.top + SCROLLED - (this.parallax.translateY || 0), 82 | bottom: elRect.bottom + SCROLLED - (this.parallax.translateY || 0), 83 | height: elRect.height, 84 | width: elRect.width 85 | } 86 | this.viewportHeight = window.innerHeight; 87 | this.viewportWidth = window.innerWidth; 88 | this.denominator = 89 | this.viewportRatio * this.viewportHeight + 90 | (this.to.translateY || 0) + 91 | (this.useElHeight ? this.elRect.height: 0); 92 | Object.keys(this.to).forEach(key => { 93 | this.unitPerScroll[key] = this.valuePerScroll(key); 94 | }); 95 | this.parallax = Object.assign({}, this.from); 96 | }, 97 | valuePerScroll (key) { 98 | const from = this.from[key] || 0; 99 | const to = this.to[key]; 100 | return (to - from) / this.denominator; 101 | }, 102 | inViewport () { 103 | return SCROLLED <= this.elRect.bottom && 104 | SCROLLED >= this.elRect.top - this.viewportHeight; 105 | }, 106 | getValue (key) { 107 | const from = this.from[key] || 0; 108 | const to = this.to[key]; 109 | const uPS = this.unitPerScroll[key]; 110 | const upperBound = Math.max(from, to); 111 | const lowerBound = Math.min(from, to); 112 | return Math.max(Math.min(from + uPS * this.moved, upperBound), lowerBound); 113 | }, 114 | observe () { 115 | if (!PARALLAX_OBSERVERS_FLAG) { 116 | initObservers(); 117 | } 118 | PARALLAX_ELEMENTS.push(this); 119 | }, 120 | unobserve () { 121 | PARALLAX_ELEMENTS.splice(PARALLAX_ELEMENTS.indexOf(this), 1); 122 | if (!PARALLAX_ELEMENTS.length) { 123 | destroyObservers(); 124 | } 125 | }, 126 | update () { 127 | if (this.inViewport()) { 128 | this.moved = SCROLLED - this.elRect.top + this.viewportHeight; 129 | Object.keys(this.to).forEach(key => { 130 | this.parallax[key] = this.getValue(key) 131 | }); 132 | } 133 | } 134 | }, 135 | mounted () { 136 | this.observe(); 137 | this.updateConfig(); 138 | this.update(); 139 | }, 140 | render () { 141 | return this.$scopedSlots.default({ 142 | parallax: this.parallax 143 | }); 144 | }, 145 | destroyed () { 146 | this.unobserve(); 147 | }, 148 | } 149 | -------------------------------------------------------------------------------- /src/components/Reveal.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | 3 | let ANIMATION_OBSERVER; 4 | 5 | export default { 6 | name: 'LeapsReveal', 7 | functional: true, 8 | props: { 9 | duration: { 10 | type: String, 11 | default: '1s' 12 | }, 13 | delay: { 14 | type: String, 15 | default: '0' 16 | }, 17 | iteration: { 18 | type: Number, 19 | default: 1 20 | }, 21 | name: { 22 | type: String, 23 | default: '' 24 | }, 25 | animateClass: { 26 | type: String, 27 | default: 'animated' 28 | }, 29 | isVisible: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | tag: { 34 | type: String, 35 | default: null 36 | } 37 | }, 38 | render (h, ctx) { 39 | const data = merge( 40 | ctx.data, 41 | { 42 | style: { 43 | visibility: ctx.props.visible ? 'visible' : 'hidden' 44 | }, 45 | attrs: { 46 | 'aria-hidden': ctx.props.visible ? false : true 47 | }, 48 | directives: [ 49 | { name: 'leaps-observer', value: ctx.props } 50 | ] 51 | }); 52 | const children = ctx.slots().default; 53 | if (!children && process.env.NODE_ENV !== 'production') { 54 | console.warn('Your component does not have any elements'); 55 | return; 56 | } 57 | if (children.length === 1 && !ctx.props.tag) { 58 | const el = children[0]; 59 | const tag = el.tag || ctx.props.tag || 'span'; 60 | const elData = merge(el.data, data); 61 | return h(tag, elData, el.children || el.text) 62 | } 63 | return h(ctx.props.tag || 'span', data, children); 64 | } 65 | }; 66 | 67 | export function install (Vue) { 68 | const directive = { 69 | bind (el, { value }) { 70 | el.__leapsProps = value; 71 | observe(el); 72 | }, 73 | destroyed (el) { 74 | unobserve(el); 75 | } 76 | }; 77 | Vue.directive('leaps-observer', directive); 78 | }; 79 | 80 | function startAnimating (el) { 81 | const { name, animateClass, delay, iteration, duration } = el.__leapsProps; 82 | el.style.visibility = ''; 83 | el.style.animationDelay = delay; 84 | el.style.animationDuration = duration; 85 | el.style.animationIterationCount = iteration; 86 | el.classList.add(name, animateClass); 87 | el.setAttribute('aria-hidden', false); 88 | 89 | const onEnd = () => { 90 | el.classList.remove(name, animateClass); 91 | el.removeAttribute('style'); 92 | unobserve(el); 93 | el.removeEventListener('animationend', onEnd); 94 | }; 95 | 96 | el.addEventListener('animationend', onEnd); 97 | } 98 | 99 | function unobserve (el) { 100 | ANIMATION_OBSERVER.unobserve(el); 101 | } 102 | 103 | function observe (el) { 104 | if (!ANIMATION_OBSERVER) { 105 | initObserver(); 106 | } 107 | ANIMATION_OBSERVER.observe(el); 108 | } 109 | 110 | function initObserver () { 111 | ANIMATION_OBSERVER = new IntersectionObserver((entries) => { 112 | entries.forEach(entry => { 113 | if(entry.isIntersecting) { 114 | startAnimating(entry.target); 115 | } 116 | }); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /src/components/Timeline.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'LeapsTimeline', 3 | provide () { 4 | return { 5 | $timeline: this 6 | } 7 | }, 8 | props: { 9 | from: { 10 | default() { return {} }, 11 | type: Object 12 | }, 13 | keyframes: { 14 | default: null, 15 | type: Array 16 | }, 17 | // animation direction, forward, reverse, or alternate 18 | direction: { 19 | default: 'loop', 20 | type: String 21 | } 22 | }, 23 | data () { 24 | return { 25 | currentFrame: 0, 26 | frames: {} 27 | } 28 | }, 29 | computed: { 30 | isLastFrame () { 31 | return this.currentFrame === this.keyframes.length - 1; 32 | } 33 | }, 34 | methods: { 35 | setup () { 36 | this.frames = Object.assign({}, ...this.keyframes) 37 | Object.keys(this.frames).forEach(key => { 38 | this.frames[key] = 0; 39 | }); 40 | this.updateFrame(); 41 | }, 42 | nextFrame () { 43 | if (this.currentFrame < this.keyframes.length) { 44 | this.currentFrame++; 45 | } 46 | this.updateFrame(); 47 | }, 48 | updateFrame () { 49 | Object.assign(this.frames, this.keyframes[this.currentFrame]); 50 | } 51 | }, 52 | created() { 53 | this.setup(); 54 | this.$on('next', this.nextFrame); 55 | }, 56 | render () { 57 | return this.$scopedSlots.default({ 58 | frames: this.frames 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Leaps from './components/leaps'; 2 | import Parallax from './components/Parallax.js'; 3 | import Reveal, { install } from './components/Reveal.js'; 4 | import Timeline from './components/Timeline.js'; 5 | 6 | 7 | export { Leaps, Reveal, Parallax, Timeline, install }; 8 | 9 | export default Leaps; 10 | --------------------------------------------------------------------------------