├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── funding.yml └── settings.yml ├── .gitignore ├── .release-it.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── build.js ├── utils │ ├── index.js │ ├── log.js │ └── write.js └── webpack.config.karma.js ├── circle.yml ├── dist ├── vue-motion.cjs.js ├── vue-motion.es.js ├── vue-motion.js └── vue-motion.min.js ├── docs ├── .nojekyll ├── README.md ├── dist │ └── src │ │ ├── App.js │ │ └── App.js.map ├── index.html ├── src │ ├── App.vue │ ├── Gallery.vue │ ├── PhotosContainer.vue │ ├── Playground.vue │ ├── Tab.vue │ ├── Tabs.vue │ ├── VueLogo.vue │ └── VueSvg.vue └── static │ ├── cat1.jpg │ ├── cat2.jpg │ ├── cat3.jpg │ ├── cat4.jpg │ └── cat5.jpg ├── package.json ├── src ├── Motion.js ├── index.js ├── presets.js ├── stepper.js └── utils.js ├── test ├── .eslintrc ├── helpers │ ├── index.js │ ├── utils.js │ └── wait-for-update.js ├── index.js ├── karma.conf.js └── specs │ └── Motion.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions" 9 | ] 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "transform-vue-jsx", 16 | "transform-object-rest-spread" 17 | ], 18 | "env": { 19 | "test": { 20 | "plugins": [ 21 | "istanbul" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | docs/dist/**/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'posva', 4 | // add your custom rules here 5 | rules: { 6 | // allow async-await 7 | 'generator-star-spacing': 0, 8 | }, 9 | globals: { 10 | requestAnimationFrame: true, 11 | performance: true, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited! 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/{{ githubAccount }}/{{ name }}). 6 | 7 | ## Pull Requests 8 | 9 | Here are some guidelines to make the process smoother: 10 | 11 | - **Add a test** - New features and bugfixes need tests. If you find it difficult to test, please tell us in the pull request and we will try to help you! 12 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 13 | - **Run `npm test` locally** - This will allow you to go faster 14 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 15 | - **Send coherent history** - Make sure your commits message means something 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | ## Creating issues 19 | 20 | ### Bug reports 21 | 22 | Always try to provide as much information as possible. If you are reporting a bug, try to provide a repro on jsfiddle.net (or anything else) or a stacktrace at the very least. This will help us check the problem quicker. 23 | 24 | ### Feature requests 25 | 26 | Lay out the reasoning behind it and propose an API for it. Ideally, you should have a practical example to prove the utility of the feature you're requesting. 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | **What kind of change does this PR introduce?** (check at least one) 10 | 11 | - [ ] Bugfix 12 | - [ ] Feature 13 | - [ ] Code style update 14 | - [ ] Refactor 15 | - [ ] Build-related changes 16 | - [ ] Other, please describe: 17 | 18 | **Does this PR introduce a breaking change?** (check one) 19 | 20 | - [ ] Yes 21 | - [ ] No 22 | 23 | If yes, please describe the impact and migration path for existing applications: 24 | 25 | **The PR fulfills these requirements:** 26 | 27 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number) 28 | - [ ] All tests are passing 29 | - [ ] New/updated tests are included 30 | 31 | If adding a **new feature**, the PR's description includes: 32 | - [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it) 33 | 34 | **Other information:** 35 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: posva 2 | patreon: posva 3 | custom: https://www.paypal.me/posva 4 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | - name: bug 3 | color: ee0701 4 | - name: contribution welcome 5 | color: 0e8a16 6 | - name: discussion 7 | color: 4935ad 8 | - name: docs 9 | color: 8be281 10 | - name: enhancement 11 | color: a2eeef 12 | - name: good first issue 13 | color: 7057ff 14 | - name: help wanted 15 | color: 008672 16 | - name: question 17 | color: d876e3 18 | - name: wontfix 19 | color: ffffff 20 | - name: WIP 21 | color: ffffff 22 | - name: need repro 23 | color: c9581c 24 | - name: feature request 25 | color: fbca04 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/coverage 5 | yarn-error.log 6 | reports 7 | test/dist 8 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "yarn build", 3 | "github": { 4 | "release": true, 5 | "releaseName": "Release %s", 6 | "tokenRef": "GITHUB_TOKEN" 7 | }, 8 | "npm": { 9 | "publish": true 10 | }, 11 | "src": { 12 | "commitMessage": "🔖 %s", 13 | "tagName": "v%s" 14 | }, 15 | "changelogCommand": "git log --pretty=format:'* %s (%h)' [REV_RANGE]" 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/posva/vue-motion). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Keep the same style** - eslint will automatically be ran before committing 11 | 12 | - **Tip** to pass lint tests easier use the `npm run lint:fix` command 13 | 14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 15 | 16 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create feature branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure your commits message means something 25 | 26 | 27 | ## Running Tests 28 | 29 | Launch visual tests and watch the components at the same time 30 | 31 | ``` bash 32 | $ npm run dev 33 | ``` 34 | 35 | 36 | **Happy coding**! 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Eduardo San Martin Morote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueMotion 2 | 3 | [![Build Status](https://img.shields.io/circleci/project/posva/vue-motion.svg)](https://circleci.com/gh/posva/vue-motion) [![codecov](https://codecov.io/gh/posva/vue-motion/branch/master/graph/badge.svg)](https://codecov.io/gh/posva/vue-motion) [![npm](https://img.shields.io/npm/v/vue-motion.svg)](https://www.npmjs.com/package/vue-motion) [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) ![size](http://img.badgesize.io/posva/vue-motion/master/dist/vue-motion.min.js.svg?compression=gzip&nocache) 4 | 5 | 6 | > Easy and natural state transitions 7 | 8 | ## Documentation 9 | 10 | Check out the [docs](https://posva.net/vue-motion/#/home) and 11 | the [Demo](https://posva.net/vue-motion) 12 | 13 | ```bash 14 | npm install --save vue-motion 15 | ``` 16 | 17 | ## Development 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | ## License 24 | 25 | Thanks to @chenglou and all who contributed to [react-motion](https://github.com/chenglou/react-motion), from which, this project was inspired. 26 | 27 | [MIT](http://opensource.org/licenses/MIT) 28 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp') 2 | const rollup = require('rollup').rollup 3 | const vue = require('rollup-plugin-vue') 4 | const jsx = require('rollup-plugin-jsx') 5 | const buble = require('rollup-plugin-buble') 6 | const replace = require('rollup-plugin-replace') 7 | const cjs = require('rollup-plugin-commonjs') 8 | const node = require('rollup-plugin-node-resolve') 9 | const uglify = require('uglify-js') 10 | 11 | // Make sure dist dir exists 12 | mkdirp('dist') 13 | 14 | const { 15 | logError, 16 | write, 17 | banner, 18 | name, 19 | moduleName, 20 | version, 21 | } = require('./utils') 22 | 23 | function rollupBundle ({ env }) { 24 | return rollup({ 25 | entry: 'src/index.js', 26 | plugins: [ 27 | node(), 28 | cjs(), 29 | vue({ compileTemplate: true, css: false }), 30 | jsx({ factory: 'h' }), 31 | replace(Object.assign({ 32 | __VERSION__: version, 33 | }, env)), 34 | buble({ 35 | objectAssign: 'Object.assign', 36 | }), 37 | ], 38 | }) 39 | } 40 | 41 | const bundleOptions = { 42 | banner, 43 | exports: 'named', 44 | format: 'umd', 45 | moduleName, 46 | } 47 | 48 | function createBundle ({ name, env, format }) { 49 | return rollupBundle({ 50 | env, 51 | }).then(function (bundle) { 52 | const options = Object.assign({}, bundleOptions) 53 | if (format) options.format = format 54 | const code = bundle.generate(options).code 55 | if (/min$/.test(name)) { 56 | const minified = uglify.minify(code, { 57 | output: { 58 | preamble: banner, 59 | ascii_only: true, // eslint-disable-line camelcase 60 | }, 61 | }).code 62 | return write(`dist/${name}.js`, minified) 63 | } else { 64 | return write(`dist/${name}.js`, code) 65 | } 66 | }).catch(logError) 67 | } 68 | 69 | // Browser bundle (can be used with script) 70 | createBundle({ 71 | name: `${name}`, 72 | env: { 73 | 'process.env.NODE_ENV': '"development"', 74 | }, 75 | }) 76 | 77 | // Commonjs bundle (preserves process.env.NODE_ENV) so 78 | // the user can replace it in dev and prod mode 79 | createBundle({ 80 | name: `${name}.common`, 81 | env: {}, 82 | format: 'cjs', 83 | }) 84 | 85 | // uses export and import syntax. Should be used with modern bundlers 86 | // like rollup and webpack 2 87 | createBundle({ 88 | name: `${name}.esm`, 89 | env: {}, 90 | format: 'es', 91 | }) 92 | 93 | // Minified version for browser 94 | createBundle({ 95 | name: `${name}.min`, 96 | env: { 97 | 'process.env.NODE_ENV': '"production"', 98 | }, 99 | }) 100 | -------------------------------------------------------------------------------- /build/utils/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | red, 3 | logError, 4 | } = require('./log') 5 | 6 | const uppercamelcase = require('uppercamelcase') 7 | 8 | exports.write = require('./write') 9 | 10 | const { 11 | author, 12 | name, 13 | version, 14 | } = require('../../package.json') 15 | 16 | const authorName = author.replace(/\s+<.*/, '') 17 | const minExt = process.env.NODE_ENV === 'production' ? '.min' : '' 18 | 19 | exports.author = authorName 20 | exports.version = version 21 | exports.name = name 22 | exports.moduleName = uppercamelcase(name) 23 | exports.filename = name + minExt 24 | exports.banner = `/*! 25 | * ${name} v${version} 26 | * (c) ${new Date().getFullYear()} ${authorName} 27 | * Released under the MIT License. 28 | */ 29 | ` 30 | 31 | // log.js 32 | exports.red = red 33 | exports.logError = logError 34 | -------------------------------------------------------------------------------- /build/utils/log.js: -------------------------------------------------------------------------------- 1 | function logError (e) { 2 | console.log(e) 3 | } 4 | 5 | function blue (str) { 6 | return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m` 7 | } 8 | 9 | function green (str) { 10 | return `\x1b[1m\x1b[32m${str}\x1b[39m\x1b[22m` 11 | } 12 | 13 | function red (str) { 14 | return `\x1b[1m\x1b[31m${str}\x1b[39m\x1b[22m` 15 | } 16 | 17 | function yellow (str) { 18 | return `\x1b[1m\x1b[33m${str}\x1b[39m\x1b[22m` 19 | } 20 | 21 | module.exports = { 22 | blue, 23 | green, 24 | red, 25 | yellow, 26 | logError, 27 | } 28 | -------------------------------------------------------------------------------- /build/utils/write.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const { blue } = require('./log.js') 4 | 5 | function write (dest, code) { 6 | return new Promise(function (resolve, reject) { 7 | fs.writeFile(dest, code, function (err) { 8 | if (err) return reject(err) 9 | console.log(blue(dest) + ' ' + getSize(code)) 10 | resolve(code) 11 | }) 12 | }) 13 | } 14 | 15 | function getSize (code) { 16 | return (code.length / 1024).toFixed(2) + 'kb' 17 | } 18 | 19 | module.exports = write 20 | -------------------------------------------------------------------------------- /build/webpack.config.karma.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | resolve: { 6 | extensions: ['.js', '.vue', '.jsx'], 7 | alias: { 8 | src: resolve(__dirname, '../src'), 9 | }, 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /.jsx?$/, 15 | use: 'babel-loader', 16 | include: [ 17 | resolve(__dirname, '../src'), 18 | resolve(__dirname, '../test'), 19 | ], 20 | }, 21 | { 22 | test: /\.vue$/, 23 | loader: 'vue-loader', 24 | options: { 25 | loaders: { 26 | js: 'babel-loader', 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | devtool: '#inline-source-map', 33 | } 34 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/node:8-browsers 8 | 9 | working_directory: ~/repo 10 | 11 | steps: 12 | - checkout 13 | 14 | # Download and cache dependencies 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "yarn.lock" }} 18 | # fallback to using the latest cache if no exact match is found 19 | - v1-dependencies- 20 | 21 | - run: yarn install 22 | 23 | - save_cache: 24 | paths: 25 | - node_modules 26 | key: v1-dependencies-{{ checksum "yarn.lock" }} 27 | 28 | # run tests! 29 | - run: npm test 30 | 31 | - run: 32 | name: Send code coverage 33 | command: bash <(curl -s https://codecov.io/bash) 34 | -------------------------------------------------------------------------------- /dist/vue-motion.cjs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-motion v0.2.3 3 | * (c) 2018 Eduardo San Martin Morote 4 | * @license MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | Object.defineProperty(exports, '__esModule', { value: true }); 10 | 11 | /* @flow */ 12 | 13 | // stepper is used a lot. Saves allocation to return the same array wrapper. 14 | // This is fine and danger-free against mutations because the callsite 15 | // immediately destructures it and gets the numbers inside without passing the 16 | // array reference around. 17 | var reusedTuple = [0, 0]; 18 | function stepper ( 19 | secondPerFrame, 20 | x, 21 | v, 22 | destX, 23 | k, 24 | b, 25 | precision 26 | ) { 27 | // Spring stiffness, in kg / s^2 28 | 29 | // for animations, destX is really spring length (spring at rest). initial 30 | // position is considered as the stretched/compressed position of a spring 31 | var Fspring = -k * (x - destX); 32 | 33 | // Damping, in kg / s 34 | var Fdamper = -b * v; 35 | 36 | // usually we put mass here, but for animation purposes, specifying mass is a 37 | // bit redundant. you could simply adjust k and b accordingly 38 | // let a = (Fspring + Fdamper) / mass 39 | var a = Fspring + Fdamper; 40 | 41 | var newV = v + a * secondPerFrame; 42 | var newX = x + newV * secondPerFrame; 43 | 44 | if (Math.abs(newV) < precision && Math.abs(newX - destX) < precision) { 45 | reusedTuple[0] = destX; 46 | reusedTuple[1] = 0; 47 | return reusedTuple 48 | } 49 | 50 | reusedTuple[0] = newX; 51 | reusedTuple[1] = newV; 52 | return reusedTuple 53 | } 54 | 55 | /* @flow */ 56 | var presets = { 57 | noWobble: { stiffness: 170, damping: 26, precision: 0.01 }, // the default, if nothing provided 58 | gentle: { stiffness: 120, damping: 14, precision: 0.01 }, 59 | wobbly: { stiffness: 180, damping: 12, precision: 0.01 }, 60 | stiff: { stiffness: 210, damping: 20, precision: 0.01 }, 61 | }; 62 | 63 | var raf = typeof window !== 'undefined' 64 | ? window.requestAnimationFrame.bind(window) 65 | : function (_) {}; 66 | var now = typeof performance !== 'undefined' 67 | ? performance.now.bind(performance) 68 | : Date.now.bind(Date); 69 | var isArray = Array.isArray.bind(Array); 70 | var isObject = function (value) { return value !== null && typeof value === 'object'; }; 71 | 72 | var msPerFrame = 1000 / 60; 73 | 74 | var Motion = { 75 | data: function data () { 76 | return { 77 | currentValues: null, 78 | currentVelocities: null, 79 | } 80 | }, 81 | 82 | props: { 83 | value: Number, 84 | values: [Object, Array], 85 | tag: { 86 | type: String, 87 | default: 'span', 88 | }, 89 | spring: { 90 | type: [Object, String], 91 | default: 'noWobble', 92 | }, 93 | }, 94 | 95 | computed: { 96 | springConfig: function springConfig () { 97 | return typeof this.spring === 'string' ? presets[this.spring] : this.spring 98 | }, 99 | realValues: function realValues () { 100 | return this.value != null ? { value: this.value } : this.values 101 | }, 102 | }, 103 | 104 | render: function render (h) { 105 | return h(this.tag, [this.$scopedSlots.default(this.currentValues)]) 106 | }, 107 | 108 | watch: { 109 | realValues: function realValues (current, old) { 110 | if (old !== current && !this.wasAnimating) { 111 | this.prevTime = now(); 112 | this.accumulatedTime = 0; 113 | this.animate(); 114 | } 115 | }, 116 | }, 117 | 118 | created: function created () { 119 | var current = this.defineInitialValues(this.realValues, null); 120 | 121 | this.currentValues = current.values; 122 | this.currentVelocities = current.velocities; 123 | }, 124 | 125 | mounted: function mounted () { 126 | this.prevTime = now(); 127 | this.accumulatedTime = 0; 128 | 129 | var ideal = this.defineInitialValues(this.currentValues, this.currentVelocities); 130 | 131 | this.idealValues = ideal.values; 132 | this.idealVelocities = ideal.velocities; 133 | 134 | this.animate(); 135 | }, 136 | 137 | methods: { 138 | defineInitialValues: function defineInitialValues (values, velocities) { 139 | var newValues = {}; 140 | var newVelocities = {}; 141 | 142 | this.defineValues(values, velocities, newValues, newVelocities); 143 | 144 | return { values: newValues, velocities: newVelocities } 145 | }, 146 | 147 | defineValues: function defineValues (values, velocities, newValues, newVelocities) { 148 | var this$1 = this; 149 | 150 | for (var key in values) { 151 | // istanbul ignore if 152 | if (!Object.prototype.hasOwnProperty.call(values, key)) { continue } 153 | 154 | if (isArray(values[key]) || isObject(values[key])) { 155 | newValues[key] = {}; 156 | newVelocities[key] = {}; 157 | 158 | this$1.defineValues( 159 | values[key], 160 | velocities && velocities[key], 161 | newValues[key], 162 | newVelocities[key] 163 | ); 164 | 165 | continue 166 | } 167 | 168 | newValues[key] = values[key]; 169 | newVelocities[key] = velocities ? velocities[key] : 0; 170 | } 171 | }, 172 | 173 | animate: function animate () { 174 | var this$1 = this; 175 | 176 | this.animationId = raf(function () { 177 | if (shouldStopAnimation(this$1.currentValues, this$1.realValues, this$1.currentVelocities)) { 178 | if (this$1.wasAnimating) { this$1.$emit('motion-end'); } 179 | 180 | // reset everything for next animation 181 | this$1.animationId = null; 182 | this$1.wasAnimating = false; 183 | return 184 | } 185 | 186 | if (!this$1.wasAnimating) { this$1.$emit('motion-start'); } 187 | this$1.wasAnimating = true; 188 | 189 | // get time from last frame 190 | var currentTime = now(); 191 | var timeDelta = currentTime - this$1.prevTime; 192 | this$1.prevTime = currentTime; 193 | this$1.accumulatedTime += timeDelta; 194 | 195 | // more than 10 frames? prolly switched browser tab. Restart 196 | if (this$1.accumulatedTime > msPerFrame * 10) { 197 | this$1.accumulatedTime = 0; 198 | } 199 | 200 | if (this$1.accumulatedTime === 0) { 201 | // no need to cancel animationID here; shouldn't have any in flight 202 | this$1.animationID = null; 203 | this$1.$emit('motion-restart'); 204 | this$1.animate(); 205 | return 206 | } 207 | 208 | var currentFrameCompletion = 209 | (this$1.accumulatedTime - Math.floor(this$1.accumulatedTime / msPerFrame) * msPerFrame) / 210 | msPerFrame; 211 | var framesToCatchUp = Math.floor(this$1.accumulatedTime / msPerFrame); 212 | var springConfig = this$1.springConfig; 213 | 214 | this$1.animateValues({ 215 | framesToCatchUp: framesToCatchUp, 216 | currentFrameCompletion: currentFrameCompletion, 217 | springConfig: springConfig, 218 | realValues: this$1.realValues, 219 | currentValues: this$1.currentValues, 220 | currentVelocities: this$1.currentVelocities, 221 | idealValues: this$1.idealValues, 222 | idealVelocities: this$1.idealVelocities, 223 | }); 224 | 225 | // out of the update loop 226 | this$1.animationID = null; 227 | // the amount we're looped over above 228 | this$1.accumulatedTime -= framesToCatchUp * msPerFrame; 229 | 230 | // keep going! 231 | this$1.animate(); 232 | }); 233 | }, 234 | 235 | animateValues: function animateValues (ref) { 236 | var this$1 = this; 237 | var framesToCatchUp = ref.framesToCatchUp; 238 | var currentFrameCompletion = ref.currentFrameCompletion; 239 | var springConfig = ref.springConfig; 240 | var realValues = ref.realValues; 241 | var currentValues = ref.currentValues; 242 | var currentVelocities = ref.currentVelocities; 243 | var idealValues = ref.idealValues; 244 | var idealVelocities = ref.idealVelocities; 245 | 246 | for (var key in realValues) { 247 | // istanbul ignore if 248 | if (!Object.prototype.hasOwnProperty.call(realValues, key)) { continue } 249 | 250 | if (isArray(realValues[key]) || isObject(realValues[key])) { 251 | // the value may have been added 252 | if (!idealValues[key]) { 253 | var ideal = this$1.defineInitialValues(this$1.realValues[key], null); 254 | var current = this$1.defineInitialValues(this$1.realValues[key], null); 255 | this$1.$set(idealValues, key, ideal.values); 256 | this$1.$set(idealVelocities, key, ideal.velocities); 257 | this$1.$set(currentValues, key, current.values); 258 | this$1.$set(currentVelocities, key, current.velocities); 259 | } 260 | 261 | this$1.animateValues({ 262 | framesToCatchUp: framesToCatchUp, 263 | currentFrameCompletion: currentFrameCompletion, 264 | springConfig: springConfig, 265 | realValues: realValues[key], 266 | currentValues: currentValues[key], 267 | currentVelocities: currentVelocities[key], 268 | idealValues: idealValues[key], 269 | idealVelocities: idealVelocities[key], 270 | }); 271 | 272 | // nothing to animate 273 | continue 274 | } 275 | 276 | var newIdealValue = idealValues[key]; 277 | var newIdealVelocity = idealVelocities[key]; 278 | var value = realValues[key]; 279 | 280 | // iterate as if the animation took place 281 | for (var i = 0; i < framesToCatchUp; i++) { 282 | var assign; 283 | (assign = stepper( 284 | msPerFrame / 1000, 285 | newIdealValue, 286 | newIdealVelocity, 287 | value, 288 | springConfig.stiffness, 289 | springConfig.damping, 290 | springConfig.precision 291 | ), newIdealValue = assign[0], newIdealVelocity = assign[1]); 292 | } 293 | 294 | var ref$1 = stepper( 295 | msPerFrame / 1000, 296 | newIdealValue, 297 | newIdealVelocity, 298 | value, 299 | springConfig.stiffness, 300 | springConfig.damping, 301 | springConfig.precision 302 | ); 303 | var nextIdealValue = ref$1[0]; 304 | var nextIdealVelocity = ref$1[1]; 305 | 306 | currentValues[key] = 307 | newIdealValue + (nextIdealValue - newIdealValue) * currentFrameCompletion; 308 | currentVelocities[key] = 309 | newIdealVelocity + (nextIdealVelocity - newIdealVelocity) * currentFrameCompletion; 310 | idealValues[key] = newIdealValue; 311 | idealVelocities[key] = newIdealVelocity; 312 | } 313 | }, 314 | }, 315 | }; 316 | 317 | function shouldStopAnimation (currentValues, values, currentVelocities) { 318 | for (var key in values) { 319 | // istanbul ignore if 320 | if (!Object.prototype.hasOwnProperty.call(values, key)) { continue } 321 | 322 | if (isArray(values[key]) || isObject(values[key])) { 323 | if (!shouldStopAnimation(currentValues[key], values[key], currentVelocities[key])) { 324 | return false 325 | } 326 | // skip the other checks 327 | continue 328 | } 329 | 330 | if (currentVelocities[key] !== 0) { return false } 331 | 332 | // stepper will have already taken care of rounding precision errors, so 333 | // won't have such thing as 0.9999 !=== 1 334 | if (currentValues[key] !== values[key]) { return false } 335 | } 336 | 337 | return true 338 | } 339 | 340 | function plugin (Vue) { 341 | Vue.component('Motion', Motion); 342 | } 343 | 344 | // Install by default if using the script tag 345 | if (typeof window !== 'undefined' && window.Vue) { 346 | window.Vue.use(plugin); 347 | } 348 | 349 | // Allow doing VueMotion.presets.custom = ... 350 | plugin.presets = presets; 351 | 352 | var version = '0.2.3'; 353 | 354 | exports['default'] = plugin; 355 | exports.Motion = Motion; 356 | exports.version = version; 357 | exports.presets = presets; 358 | -------------------------------------------------------------------------------- /dist/vue-motion.es.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-motion v0.2.3 3 | * (c) 2018 Eduardo San Martin Morote 4 | * @license MIT 5 | */ 6 | 7 | /* @flow */ 8 | 9 | // stepper is used a lot. Saves allocation to return the same array wrapper. 10 | // This is fine and danger-free against mutations because the callsite 11 | // immediately destructures it and gets the numbers inside without passing the 12 | // array reference around. 13 | var reusedTuple = [0, 0]; 14 | function stepper ( 15 | secondPerFrame, 16 | x, 17 | v, 18 | destX, 19 | k, 20 | b, 21 | precision 22 | ) { 23 | // Spring stiffness, in kg / s^2 24 | 25 | // for animations, destX is really spring length (spring at rest). initial 26 | // position is considered as the stretched/compressed position of a spring 27 | var Fspring = -k * (x - destX); 28 | 29 | // Damping, in kg / s 30 | var Fdamper = -b * v; 31 | 32 | // usually we put mass here, but for animation purposes, specifying mass is a 33 | // bit redundant. you could simply adjust k and b accordingly 34 | // let a = (Fspring + Fdamper) / mass 35 | var a = Fspring + Fdamper; 36 | 37 | var newV = v + a * secondPerFrame; 38 | var newX = x + newV * secondPerFrame; 39 | 40 | if (Math.abs(newV) < precision && Math.abs(newX - destX) < precision) { 41 | reusedTuple[0] = destX; 42 | reusedTuple[1] = 0; 43 | return reusedTuple 44 | } 45 | 46 | reusedTuple[0] = newX; 47 | reusedTuple[1] = newV; 48 | return reusedTuple 49 | } 50 | 51 | /* @flow */ 52 | var presets = { 53 | noWobble: { stiffness: 170, damping: 26, precision: 0.01 }, // the default, if nothing provided 54 | gentle: { stiffness: 120, damping: 14, precision: 0.01 }, 55 | wobbly: { stiffness: 180, damping: 12, precision: 0.01 }, 56 | stiff: { stiffness: 210, damping: 20, precision: 0.01 }, 57 | }; 58 | 59 | var raf = typeof window !== 'undefined' 60 | ? window.requestAnimationFrame.bind(window) 61 | : function (_) {}; 62 | var now = typeof performance !== 'undefined' 63 | ? performance.now.bind(performance) 64 | : Date.now.bind(Date); 65 | var isArray = Array.isArray.bind(Array); 66 | var isObject = function (value) { return value !== null && typeof value === 'object'; }; 67 | 68 | var msPerFrame = 1000 / 60; 69 | 70 | var Motion = { 71 | data: function data () { 72 | return { 73 | currentValues: null, 74 | currentVelocities: null, 75 | } 76 | }, 77 | 78 | props: { 79 | value: Number, 80 | values: [Object, Array], 81 | tag: { 82 | type: String, 83 | default: 'span', 84 | }, 85 | spring: { 86 | type: [Object, String], 87 | default: 'noWobble', 88 | }, 89 | }, 90 | 91 | computed: { 92 | springConfig: function springConfig () { 93 | return typeof this.spring === 'string' ? presets[this.spring] : this.spring 94 | }, 95 | realValues: function realValues () { 96 | return this.value != null ? { value: this.value } : this.values 97 | }, 98 | }, 99 | 100 | render: function render (h) { 101 | return h(this.tag, [this.$scopedSlots.default(this.currentValues)]) 102 | }, 103 | 104 | watch: { 105 | realValues: function realValues (current, old) { 106 | if (old !== current && !this.wasAnimating) { 107 | this.prevTime = now(); 108 | this.accumulatedTime = 0; 109 | this.animate(); 110 | } 111 | }, 112 | }, 113 | 114 | created: function created () { 115 | var current = this.defineInitialValues(this.realValues, null); 116 | 117 | this.currentValues = current.values; 118 | this.currentVelocities = current.velocities; 119 | }, 120 | 121 | mounted: function mounted () { 122 | this.prevTime = now(); 123 | this.accumulatedTime = 0; 124 | 125 | var ideal = this.defineInitialValues(this.currentValues, this.currentVelocities); 126 | 127 | this.idealValues = ideal.values; 128 | this.idealVelocities = ideal.velocities; 129 | 130 | this.animate(); 131 | }, 132 | 133 | methods: { 134 | defineInitialValues: function defineInitialValues (values, velocities) { 135 | var newValues = {}; 136 | var newVelocities = {}; 137 | 138 | this.defineValues(values, velocities, newValues, newVelocities); 139 | 140 | return { values: newValues, velocities: newVelocities } 141 | }, 142 | 143 | defineValues: function defineValues (values, velocities, newValues, newVelocities) { 144 | var this$1 = this; 145 | 146 | for (var key in values) { 147 | // istanbul ignore if 148 | if (!Object.prototype.hasOwnProperty.call(values, key)) { continue } 149 | 150 | if (isArray(values[key]) || isObject(values[key])) { 151 | newValues[key] = {}; 152 | newVelocities[key] = {}; 153 | 154 | this$1.defineValues( 155 | values[key], 156 | velocities && velocities[key], 157 | newValues[key], 158 | newVelocities[key] 159 | ); 160 | 161 | continue 162 | } 163 | 164 | newValues[key] = values[key]; 165 | newVelocities[key] = velocities ? velocities[key] : 0; 166 | } 167 | }, 168 | 169 | animate: function animate () { 170 | var this$1 = this; 171 | 172 | this.animationId = raf(function () { 173 | if (shouldStopAnimation(this$1.currentValues, this$1.realValues, this$1.currentVelocities)) { 174 | if (this$1.wasAnimating) { this$1.$emit('motion-end'); } 175 | 176 | // reset everything for next animation 177 | this$1.animationId = null; 178 | this$1.wasAnimating = false; 179 | return 180 | } 181 | 182 | if (!this$1.wasAnimating) { this$1.$emit('motion-start'); } 183 | this$1.wasAnimating = true; 184 | 185 | // get time from last frame 186 | var currentTime = now(); 187 | var timeDelta = currentTime - this$1.prevTime; 188 | this$1.prevTime = currentTime; 189 | this$1.accumulatedTime += timeDelta; 190 | 191 | // more than 10 frames? prolly switched browser tab. Restart 192 | if (this$1.accumulatedTime > msPerFrame * 10) { 193 | this$1.accumulatedTime = 0; 194 | } 195 | 196 | if (this$1.accumulatedTime === 0) { 197 | // no need to cancel animationID here; shouldn't have any in flight 198 | this$1.animationID = null; 199 | this$1.$emit('motion-restart'); 200 | this$1.animate(); 201 | return 202 | } 203 | 204 | var currentFrameCompletion = 205 | (this$1.accumulatedTime - Math.floor(this$1.accumulatedTime / msPerFrame) * msPerFrame) / 206 | msPerFrame; 207 | var framesToCatchUp = Math.floor(this$1.accumulatedTime / msPerFrame); 208 | var springConfig = this$1.springConfig; 209 | 210 | this$1.animateValues({ 211 | framesToCatchUp: framesToCatchUp, 212 | currentFrameCompletion: currentFrameCompletion, 213 | springConfig: springConfig, 214 | realValues: this$1.realValues, 215 | currentValues: this$1.currentValues, 216 | currentVelocities: this$1.currentVelocities, 217 | idealValues: this$1.idealValues, 218 | idealVelocities: this$1.idealVelocities, 219 | }); 220 | 221 | // out of the update loop 222 | this$1.animationID = null; 223 | // the amount we're looped over above 224 | this$1.accumulatedTime -= framesToCatchUp * msPerFrame; 225 | 226 | // keep going! 227 | this$1.animate(); 228 | }); 229 | }, 230 | 231 | animateValues: function animateValues (ref) { 232 | var this$1 = this; 233 | var framesToCatchUp = ref.framesToCatchUp; 234 | var currentFrameCompletion = ref.currentFrameCompletion; 235 | var springConfig = ref.springConfig; 236 | var realValues = ref.realValues; 237 | var currentValues = ref.currentValues; 238 | var currentVelocities = ref.currentVelocities; 239 | var idealValues = ref.idealValues; 240 | var idealVelocities = ref.idealVelocities; 241 | 242 | for (var key in realValues) { 243 | // istanbul ignore if 244 | if (!Object.prototype.hasOwnProperty.call(realValues, key)) { continue } 245 | 246 | if (isArray(realValues[key]) || isObject(realValues[key])) { 247 | // the value may have been added 248 | if (!idealValues[key]) { 249 | var ideal = this$1.defineInitialValues(this$1.realValues[key], null); 250 | var current = this$1.defineInitialValues(this$1.realValues[key], null); 251 | this$1.$set(idealValues, key, ideal.values); 252 | this$1.$set(idealVelocities, key, ideal.velocities); 253 | this$1.$set(currentValues, key, current.values); 254 | this$1.$set(currentVelocities, key, current.velocities); 255 | } 256 | 257 | this$1.animateValues({ 258 | framesToCatchUp: framesToCatchUp, 259 | currentFrameCompletion: currentFrameCompletion, 260 | springConfig: springConfig, 261 | realValues: realValues[key], 262 | currentValues: currentValues[key], 263 | currentVelocities: currentVelocities[key], 264 | idealValues: idealValues[key], 265 | idealVelocities: idealVelocities[key], 266 | }); 267 | 268 | // nothing to animate 269 | continue 270 | } 271 | 272 | var newIdealValue = idealValues[key]; 273 | var newIdealVelocity = idealVelocities[key]; 274 | var value = realValues[key]; 275 | 276 | // iterate as if the animation took place 277 | for (var i = 0; i < framesToCatchUp; i++) { 278 | var assign; 279 | (assign = stepper( 280 | msPerFrame / 1000, 281 | newIdealValue, 282 | newIdealVelocity, 283 | value, 284 | springConfig.stiffness, 285 | springConfig.damping, 286 | springConfig.precision 287 | ), newIdealValue = assign[0], newIdealVelocity = assign[1]); 288 | } 289 | 290 | var ref$1 = stepper( 291 | msPerFrame / 1000, 292 | newIdealValue, 293 | newIdealVelocity, 294 | value, 295 | springConfig.stiffness, 296 | springConfig.damping, 297 | springConfig.precision 298 | ); 299 | var nextIdealValue = ref$1[0]; 300 | var nextIdealVelocity = ref$1[1]; 301 | 302 | currentValues[key] = 303 | newIdealValue + (nextIdealValue - newIdealValue) * currentFrameCompletion; 304 | currentVelocities[key] = 305 | newIdealVelocity + (nextIdealVelocity - newIdealVelocity) * currentFrameCompletion; 306 | idealValues[key] = newIdealValue; 307 | idealVelocities[key] = newIdealVelocity; 308 | } 309 | }, 310 | }, 311 | }; 312 | 313 | function shouldStopAnimation (currentValues, values, currentVelocities) { 314 | for (var key in values) { 315 | // istanbul ignore if 316 | if (!Object.prototype.hasOwnProperty.call(values, key)) { continue } 317 | 318 | if (isArray(values[key]) || isObject(values[key])) { 319 | if (!shouldStopAnimation(currentValues[key], values[key], currentVelocities[key])) { 320 | return false 321 | } 322 | // skip the other checks 323 | continue 324 | } 325 | 326 | if (currentVelocities[key] !== 0) { return false } 327 | 328 | // stepper will have already taken care of rounding precision errors, so 329 | // won't have such thing as 0.9999 !=== 1 330 | if (currentValues[key] !== values[key]) { return false } 331 | } 332 | 333 | return true 334 | } 335 | 336 | function plugin (Vue) { 337 | Vue.component('Motion', Motion); 338 | } 339 | 340 | // Install by default if using the script tag 341 | if (typeof window !== 'undefined' && window.Vue) { 342 | window.Vue.use(plugin); 343 | } 344 | 345 | // Allow doing VueMotion.presets.custom = ... 346 | plugin.presets = presets; 347 | 348 | var version = '0.2.3'; 349 | 350 | export { Motion, version, presets }; 351 | export default plugin; 352 | -------------------------------------------------------------------------------- /dist/vue-motion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-motion v0.2.3 3 | * (c) 2018 Eduardo San Martin Morote 4 | * @license MIT 5 | */ 6 | 7 | (function (global, factory) { 8 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 9 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 10 | (factory((global.VueMotion = {}))); 11 | }(this, (function (exports) { 'use strict'; 12 | 13 | /* @flow */ 14 | 15 | // stepper is used a lot. Saves allocation to return the same array wrapper. 16 | // This is fine and danger-free against mutations because the callsite 17 | // immediately destructures it and gets the numbers inside without passing the 18 | // array reference around. 19 | var reusedTuple = [0, 0]; 20 | function stepper ( 21 | secondPerFrame, 22 | x, 23 | v, 24 | destX, 25 | k, 26 | b, 27 | precision 28 | ) { 29 | // Spring stiffness, in kg / s^2 30 | 31 | // for animations, destX is really spring length (spring at rest). initial 32 | // position is considered as the stretched/compressed position of a spring 33 | var Fspring = -k * (x - destX); 34 | 35 | // Damping, in kg / s 36 | var Fdamper = -b * v; 37 | 38 | // usually we put mass here, but for animation purposes, specifying mass is a 39 | // bit redundant. you could simply adjust k and b accordingly 40 | // let a = (Fspring + Fdamper) / mass 41 | var a = Fspring + Fdamper; 42 | 43 | var newV = v + a * secondPerFrame; 44 | var newX = x + newV * secondPerFrame; 45 | 46 | if (Math.abs(newV) < precision && Math.abs(newX - destX) < precision) { 47 | reusedTuple[0] = destX; 48 | reusedTuple[1] = 0; 49 | return reusedTuple 50 | } 51 | 52 | reusedTuple[0] = newX; 53 | reusedTuple[1] = newV; 54 | return reusedTuple 55 | } 56 | 57 | /* @flow */ 58 | var presets = { 59 | noWobble: { stiffness: 170, damping: 26, precision: 0.01 }, // the default, if nothing provided 60 | gentle: { stiffness: 120, damping: 14, precision: 0.01 }, 61 | wobbly: { stiffness: 180, damping: 12, precision: 0.01 }, 62 | stiff: { stiffness: 210, damping: 20, precision: 0.01 }, 63 | }; 64 | 65 | var raf = typeof window !== 'undefined' 66 | ? window.requestAnimationFrame.bind(window) 67 | : function (_) {}; 68 | var now = typeof performance !== 'undefined' 69 | ? performance.now.bind(performance) 70 | : Date.now.bind(Date); 71 | var isArray = Array.isArray.bind(Array); 72 | var isObject = function (value) { return value !== null && typeof value === 'object'; }; 73 | 74 | var msPerFrame = 1000 / 60; 75 | 76 | var Motion = { 77 | data: function data () { 78 | return { 79 | currentValues: null, 80 | currentVelocities: null, 81 | } 82 | }, 83 | 84 | props: { 85 | value: Number, 86 | values: [Object, Array], 87 | tag: { 88 | type: String, 89 | default: 'span', 90 | }, 91 | spring: { 92 | type: [Object, String], 93 | default: 'noWobble', 94 | }, 95 | }, 96 | 97 | computed: { 98 | springConfig: function springConfig () { 99 | return typeof this.spring === 'string' ? presets[this.spring] : this.spring 100 | }, 101 | realValues: function realValues () { 102 | return this.value != null ? { value: this.value } : this.values 103 | }, 104 | }, 105 | 106 | render: function render (h) { 107 | return h(this.tag, [this.$scopedSlots.default(this.currentValues)]) 108 | }, 109 | 110 | watch: { 111 | realValues: function realValues (current, old) { 112 | if (old !== current && !this.wasAnimating) { 113 | this.prevTime = now(); 114 | this.accumulatedTime = 0; 115 | this.animate(); 116 | } 117 | }, 118 | }, 119 | 120 | created: function created () { 121 | var current = this.defineInitialValues(this.realValues, null); 122 | 123 | this.currentValues = current.values; 124 | this.currentVelocities = current.velocities; 125 | }, 126 | 127 | mounted: function mounted () { 128 | this.prevTime = now(); 129 | this.accumulatedTime = 0; 130 | 131 | var ideal = this.defineInitialValues(this.currentValues, this.currentVelocities); 132 | 133 | this.idealValues = ideal.values; 134 | this.idealVelocities = ideal.velocities; 135 | 136 | this.animate(); 137 | }, 138 | 139 | methods: { 140 | defineInitialValues: function defineInitialValues (values, velocities) { 141 | var newValues = {}; 142 | var newVelocities = {}; 143 | 144 | this.defineValues(values, velocities, newValues, newVelocities); 145 | 146 | return { values: newValues, velocities: newVelocities } 147 | }, 148 | 149 | defineValues: function defineValues (values, velocities, newValues, newVelocities) { 150 | var this$1 = this; 151 | 152 | for (var key in values) { 153 | // istanbul ignore if 154 | if (!Object.prototype.hasOwnProperty.call(values, key)) { continue } 155 | 156 | if (isArray(values[key]) || isObject(values[key])) { 157 | newValues[key] = {}; 158 | newVelocities[key] = {}; 159 | 160 | this$1.defineValues( 161 | values[key], 162 | velocities && velocities[key], 163 | newValues[key], 164 | newVelocities[key] 165 | ); 166 | 167 | continue 168 | } 169 | 170 | newValues[key] = values[key]; 171 | newVelocities[key] = velocities ? velocities[key] : 0; 172 | } 173 | }, 174 | 175 | animate: function animate () { 176 | var this$1 = this; 177 | 178 | this.animationId = raf(function () { 179 | if (shouldStopAnimation(this$1.currentValues, this$1.realValues, this$1.currentVelocities)) { 180 | if (this$1.wasAnimating) { this$1.$emit('motion-end'); } 181 | 182 | // reset everything for next animation 183 | this$1.animationId = null; 184 | this$1.wasAnimating = false; 185 | return 186 | } 187 | 188 | if (!this$1.wasAnimating) { this$1.$emit('motion-start'); } 189 | this$1.wasAnimating = true; 190 | 191 | // get time from last frame 192 | var currentTime = now(); 193 | var timeDelta = currentTime - this$1.prevTime; 194 | this$1.prevTime = currentTime; 195 | this$1.accumulatedTime += timeDelta; 196 | 197 | // more than 10 frames? prolly switched browser tab. Restart 198 | if (this$1.accumulatedTime > msPerFrame * 10) { 199 | this$1.accumulatedTime = 0; 200 | } 201 | 202 | if (this$1.accumulatedTime === 0) { 203 | // no need to cancel animationID here; shouldn't have any in flight 204 | this$1.animationID = null; 205 | this$1.$emit('motion-restart'); 206 | this$1.animate(); 207 | return 208 | } 209 | 210 | var currentFrameCompletion = 211 | (this$1.accumulatedTime - Math.floor(this$1.accumulatedTime / msPerFrame) * msPerFrame) / 212 | msPerFrame; 213 | var framesToCatchUp = Math.floor(this$1.accumulatedTime / msPerFrame); 214 | var springConfig = this$1.springConfig; 215 | 216 | this$1.animateValues({ 217 | framesToCatchUp: framesToCatchUp, 218 | currentFrameCompletion: currentFrameCompletion, 219 | springConfig: springConfig, 220 | realValues: this$1.realValues, 221 | currentValues: this$1.currentValues, 222 | currentVelocities: this$1.currentVelocities, 223 | idealValues: this$1.idealValues, 224 | idealVelocities: this$1.idealVelocities, 225 | }); 226 | 227 | // out of the update loop 228 | this$1.animationID = null; 229 | // the amount we're looped over above 230 | this$1.accumulatedTime -= framesToCatchUp * msPerFrame; 231 | 232 | // keep going! 233 | this$1.animate(); 234 | }); 235 | }, 236 | 237 | animateValues: function animateValues (ref) { 238 | var this$1 = this; 239 | var framesToCatchUp = ref.framesToCatchUp; 240 | var currentFrameCompletion = ref.currentFrameCompletion; 241 | var springConfig = ref.springConfig; 242 | var realValues = ref.realValues; 243 | var currentValues = ref.currentValues; 244 | var currentVelocities = ref.currentVelocities; 245 | var idealValues = ref.idealValues; 246 | var idealVelocities = ref.idealVelocities; 247 | 248 | for (var key in realValues) { 249 | // istanbul ignore if 250 | if (!Object.prototype.hasOwnProperty.call(realValues, key)) { continue } 251 | 252 | if (isArray(realValues[key]) || isObject(realValues[key])) { 253 | // the value may have been added 254 | if (!idealValues[key]) { 255 | var ideal = this$1.defineInitialValues(this$1.realValues[key], null); 256 | var current = this$1.defineInitialValues(this$1.realValues[key], null); 257 | this$1.$set(idealValues, key, ideal.values); 258 | this$1.$set(idealVelocities, key, ideal.velocities); 259 | this$1.$set(currentValues, key, current.values); 260 | this$1.$set(currentVelocities, key, current.velocities); 261 | } 262 | 263 | this$1.animateValues({ 264 | framesToCatchUp: framesToCatchUp, 265 | currentFrameCompletion: currentFrameCompletion, 266 | springConfig: springConfig, 267 | realValues: realValues[key], 268 | currentValues: currentValues[key], 269 | currentVelocities: currentVelocities[key], 270 | idealValues: idealValues[key], 271 | idealVelocities: idealVelocities[key], 272 | }); 273 | 274 | // nothing to animate 275 | continue 276 | } 277 | 278 | var newIdealValue = idealValues[key]; 279 | var newIdealVelocity = idealVelocities[key]; 280 | var value = realValues[key]; 281 | 282 | // iterate as if the animation took place 283 | for (var i = 0; i < framesToCatchUp; i++) { 284 | var assign; 285 | (assign = stepper( 286 | msPerFrame / 1000, 287 | newIdealValue, 288 | newIdealVelocity, 289 | value, 290 | springConfig.stiffness, 291 | springConfig.damping, 292 | springConfig.precision 293 | ), newIdealValue = assign[0], newIdealVelocity = assign[1]); 294 | } 295 | 296 | var ref$1 = stepper( 297 | msPerFrame / 1000, 298 | newIdealValue, 299 | newIdealVelocity, 300 | value, 301 | springConfig.stiffness, 302 | springConfig.damping, 303 | springConfig.precision 304 | ); 305 | var nextIdealValue = ref$1[0]; 306 | var nextIdealVelocity = ref$1[1]; 307 | 308 | currentValues[key] = 309 | newIdealValue + (nextIdealValue - newIdealValue) * currentFrameCompletion; 310 | currentVelocities[key] = 311 | newIdealVelocity + (nextIdealVelocity - newIdealVelocity) * currentFrameCompletion; 312 | idealValues[key] = newIdealValue; 313 | idealVelocities[key] = newIdealVelocity; 314 | } 315 | }, 316 | }, 317 | }; 318 | 319 | function shouldStopAnimation (currentValues, values, currentVelocities) { 320 | for (var key in values) { 321 | // istanbul ignore if 322 | if (!Object.prototype.hasOwnProperty.call(values, key)) { continue } 323 | 324 | if (isArray(values[key]) || isObject(values[key])) { 325 | if (!shouldStopAnimation(currentValues[key], values[key], currentVelocities[key])) { 326 | return false 327 | } 328 | // skip the other checks 329 | continue 330 | } 331 | 332 | if (currentVelocities[key] !== 0) { return false } 333 | 334 | // stepper will have already taken care of rounding precision errors, so 335 | // won't have such thing as 0.9999 !=== 1 336 | if (currentValues[key] !== values[key]) { return false } 337 | } 338 | 339 | return true 340 | } 341 | 342 | function plugin (Vue) { 343 | Vue.component('Motion', Motion); 344 | } 345 | 346 | // Install by default if using the script tag 347 | if (typeof window !== 'undefined' && window.Vue) { 348 | window.Vue.use(plugin); 349 | } 350 | 351 | // Allow doing VueMotion.presets.custom = ... 352 | plugin.presets = presets; 353 | 354 | var version = '0.2.3'; 355 | 356 | exports['default'] = plugin; 357 | exports.Motion = Motion; 358 | exports.version = version; 359 | exports.presets = presets; 360 | 361 | Object.defineProperty(exports, '__esModule', { value: true }); 362 | 363 | }))); 364 | -------------------------------------------------------------------------------- /dist/vue-motion.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-motion v0.2.3 3 | * (c) 2018 Eduardo San Martin Morote 4 | * @license MIT 5 | */ 6 | 7 | !function(e,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(e.VueMotion={})}(this,function(e){"use strict";var i=[0,0];function t(e,t,n,a,r,s,u){var l=n+(-r*(t-a)+-s*n)*e,o=t+l*e;return Math.abs(l)10*l&&(e.accumulatedTime=0),0===e.accumulatedTime)return e.animationID=null,e.$emit("motion-restart"),void e.animate();var n=(e.accumulatedTime-Math.floor(e.accumulatedTime/l)*l)/l,a=Math.floor(e.accumulatedTime/l),o=e.springConfig;e.animateValues({framesToCatchUp:a,currentFrameCompletion:n,springConfig:o,realValues:e.realValues,currentValues:e.currentValues,currentVelocities:e.currentVelocities,idealValues:e.idealValues,idealVelocities:e.idealVelocities}),e.animationID=null,e.accumulatedTime-=a*l,e.animate()})},animateValues:function(e){var i=this,n=e.framesToCatchUp,a=e.currentFrameCompletion,r=e.springConfig,o=e.realValues,c=e.currentValues,f=e.currentVelocities,d=e.idealValues,m=e.idealVelocities;for(var p in o)if(Object.prototype.hasOwnProperty.call(o,p))if(s(o[p])||u(o[p])){if(!d[p]){var V=i.defineInitialValues(i.realValues[p],null),h=i.defineInitialValues(i.realValues[p],null);i.$set(d,p,V.values),i.$set(m,p,V.velocities),i.$set(c,p,h.values),i.$set(f,p,h.velocities)}i.animateValues({framesToCatchUp:n,currentFrameCompletion:a,springConfig:r,realValues:o[p],currentValues:c[p],currentVelocities:f[p],idealValues:d[p],idealVelocities:m[p]})}else{for(var v=d[p],g=m[p],w=o[p],y=0;y Natural animations in Vue 4 | 5 | Vue Motion uses springs to generate smooth and natural transition between 6 | numerical values. This allows you to create smooth animations that feels natural 7 | and that automatically adapt to its target value. These kind of _animations_ can 8 | help with **fluid** interfaces. 9 | 10 | ## Why do I need this? 11 | 12 | You may think you got covered by using Vue `transition` with CSS transitions, there's one big problem about CSS transition, and more specifically about using easing functions: interrupting a transition in the middle to play a different one makes the animation look floppy and unnatural. This is is because every easing requires you to define **how long a transition** takes. **Vue Motion doesn't**, let the transition takes the time it needs but control how it should behave! 13 | 14 | ## Installation 15 | 16 | You can install Vue Motion as any other plugin: 17 | 18 | ### Bundlers 19 | 20 | ```js 21 | import Vue from 'vue' 22 | import VueMotion from 'vue-motion' 23 | 24 | Vue.use(VueMotion) 25 | ``` 26 | 27 | ### Browsers 28 | 29 | ```html 30 | 31 | 32 | ``` 33 | 34 | This will give you global access to all components. 35 | 36 | ### Local import 37 | 38 | If you don't want to globally install the components, you can import them locally, 39 | and even give them different names: 40 | 41 | ```js 42 | import { Motion } from 'vue-motion' 43 | 44 | export default { 45 | components: { MyMotion: Motion }, 46 | } 47 | ``` 48 | 49 | ## Components 50 | 51 | ### Motion 52 | 53 | `Motion` is the main component that allows you to transition a single value or a 54 | group of values. You simply give it a value and it will give you access to the 55 | transitioning value in a scoped slot. 56 | 57 | 58 | #### Examples 59 | 60 | ##### Single Value 61 | 62 | When transitioning a single value, pass it down with the `value` prop. You then 63 | get access to the transitioning value in the scope with that same key: `value`. 64 | 65 | ```html 66 | 67 |
68 |
69 | ``` 70 | 71 | Then just set the value as you would normally do. `Motion` will take care of the 72 | rest 🙂: 73 | 74 | ```js 75 | // in the component 76 | this.offset = 200 77 | ``` 78 | 79 | ##### Multiple values 80 | 81 | When transitioning a group of values, pass down an object or array (will be 82 | converted to an object) to the `values` prop (with an _s_). You'll get access to 83 | the transitioning values in the scope with the original keys. 84 | 85 | ```html 86 | 87 |
88 |
89 | ``` 90 | 91 | You can nest objects and arrays: 92 | 93 | ```js 94 | // Given an array of sizes: 95 | data () { 96 | return { 97 | sizes: [ 98 | { width: 100, height: 200 }, 99 | { width: 200, height: 70 }, 100 | { width: 120, height: 170 }, 101 | ] 102 | } 103 | } 104 | ``` 105 | 106 | Same usage 😉 107 | 108 | ```html 109 | 110 |
111 |
112 | ``` 113 | 114 | 115 | #### Props 116 | 117 | |Name|Type|Required|Default|Comments| 118 | |----|----|--------|-------|--------| 119 | |`value`|`Number`|only when `values` is not provided||Actual value to transition after| 120 | |`values`|`Object` or `Array`|only when `value` is not provided||It contains multiple values to transition after| 121 | |`tag`|`String`|No|`span`|Allows you to define the container element's tag| 122 | |`spring`|`Object` or `String`|No|`noWobble`|Defines how the transition behaves. Default to a non-wobbly spring that is used in the examples. Check the _Playground_ in the Demo | 123 | 124 | When setting the `spring` on a motion, you can use any of the predefined [springs](#springs). 125 | 126 | #### Events 127 | 128 | - `motion-start`: Emitted when a new transition starts (basically when the value 129 | changes and there was no transition occurring) 130 | - `motion-end`: Emitted when a transition finishes 131 | - `motion-restart`: Emitted when a transition restart. This may happen when the 132 | animations takes too long to complete (slow frame) or when the user switches 133 | to another tab and comes back after a while. 134 | 135 | ## Springs 136 | 137 | Internally, Vue Motion uses springs to transition values. A spring is defined by 138 | its `stiffness` and its `damping`. Additionally, it's also takes a `precision` 139 | value, used for calculations. These are the predefined springs: 140 | 141 | |Name|Stiffness|Damping| 142 | |----|---------|-------| 143 | |noWobble|170|26| 144 | |gentle|120|14| 145 | |wobbly|180|12| 146 | |stiff|210 |20| 147 | 148 | The easiest way to find the kind of animation you want, is to play around with 149 | values. Use the _Playground_ in the Demo for that. 150 | 151 |

152 | All springs have a `0.01` precision which is enough for animations to look good. 153 |

154 | 155 | ## Version 156 | 157 | You can accesse the current version of the package with 158 | 159 | ```js 160 | import { version } from 'vue-motion' 161 | ``` 162 | -------------------------------------------------------------------------------- /docs/dist/src/App.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Landing=e():t.Landing=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="/",e(e.s=44)}([function(t,e){t.exports=function(t,e,n,r){var i,o=t=t||{},s=typeof t.default;"object"!==s&&"function"!==s||(i=t,o=t.default);var a="function"==typeof o?o.options:o;if(e&&(a.render=e.render,a.staticRenderFns=e.staticRenderFns),n&&(a._scopeId=n),r){var u=a.computed||(a.computed={});Object.keys(r).forEach(function(t){var e=r[t];u[t]=function(){return e}})}return{esModule:i,exports:o,options:a}}},function(t,e){t.exports=function(){var t=[];return t.toString=function(){for(var t=[],e=0;en.parts.length&&(r.parts.length=n.parts.length)}else{for(var o=[],i=0;i1e3/60*10&&(t.accumulatedTime=0),0===t.accumulatedTime)return t.animationID=null,t.$emit("motion-restart"),void t.animate();var r=(t.accumulatedTime-Math.floor(t.accumulatedTime/(1e3/60))*(1e3/60))/(1e3/60),o=Math.floor(t.accumulatedTime/(1e3/60)),s=t.springConfig;t.animateValues({framesToCatchUp:o,currentFrameCompletion:r,springConfig:s,realValues:t.realValues,currentValues:t.currentValues,currentVelocities:t.currentVelocities,idealValues:t.idealValues,idealVelocities:t.idealVelocities}),t.animationID=null,t.accumulatedTime-=o*(1e3/60),t.animate()})},animateValues:function(t){var e=t.framesToCatchUp,n=t.currentFrameCompletion,r=t.springConfig,i=t.realValues,s=t.currentValues,u=t.currentVelocities,c=t.idealValues,d=t.idealVelocities;for(var f in i)if(Object.prototype.hasOwnProperty.call(i,f))if((0,l.isArray)(i[f])||(0,l.isObject)(i[f]))this.animateValues({framesToCatchUp:e,currentFrameCompletion:n,springConfig:r,realValues:i[f],currentValues:s[f],currentVelocities:u[f],idealValues:c[f],idealVelocities:d[f]});else{for(var p=c[f],h=d[f],m=i[f],v=0;v=this.photos.length&&(this.current=0)},previous:function(){--this.current<0&&(this.current=this.photos.length-1)},leftSpace:function(t){for(var e=Array(this.photos.length),n=0,r=this.current;r0){n=0;for(var i=this.current-1;i>=0;--i)n-=t["w"+i],e[i]=n}return e}},components:{Motion:i.a,PhotosContainer:s.a}}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n.n(r);e.default={props:["sizes","current"],computed:{left:function(){for(var t=this,e=0,n=0;n10?"hidden":"initial"}}},computed:{spring:function(){return{stiffness:180,damping:12,precision:.01}},values:function(){return{x:this.x,rotated:this.rotated}}},components:{Motion:i.a,VueSvg:s.a}}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default={props:{width:Number,height:Number,x:Number,rotated:Number},computed:{style:function(){return{width:this.width,height:this.height,transform:"perspective(200px) translateX("+this.x+"px) rotateY("+this.rotated+"deg)"}}}}},function(t,e,n){e=t.exports=n(1)(),e.push([t.i,".tabs{display:flex;justify-content:center;margin:1rem 0}",""])},function(t,e,n){e=t.exports=n(1)(),e.push([t.i,".logo[data-v-262cfbc0]{border:2px solid #d3d3d3;background-color:#fff;border-radius:1rem;padding:1.2rem 0;max-width:100%;width:480px;margin:auto}.logo svg[data-v-262cfbc0]{display:block;margin:auto}",""])},function(t,e,n){e=t.exports=n(1)(),e.push([t.i,".demo[data-v-46356e1c]{width:100px;height:100px;background-color:crimson}.demo-container[data-v-46356e1c]{width:300px;background-color:#d3d3d3}",""])},function(t,e,n){e=t.exports=n(1)(),e.push([t.i,".demo[data-v-63623a41]{display:flex;align-items:center;height:600px}.demo-inner[data-v-63623a41]{width:100%}.container[data-v-63623a41]{position:relative;overflow:hidden;margin:auto;max-width:100%}.photos[data-v-63623a41]{position:absolute;white-space:nowrap}.controls[data-v-63623a41]{display:flex;max-width:500px}.controls button[data-v-63623a41]{flex:1}.controls input[data-v-63623a41]{flex:3}",""])},function(t,e,n){e=t.exports=n(1)(),e.push([t.i,"@import url(https://fonts.googleapis.com/css?family=Lato);",""]),e.push([t.i,"[data-v-9e48fde0]{box-sizing:border-box;font-family:Lato,helvetica neue,sans-serif}.landing[data-v-9e48fde0]{background-color:#f8f8ff;padding:20px;overflow-x:hidden}.landing__title[data-v-9e48fde0]{font-size:48px;margin:1rem 0;text-align:center}.landing__subtitle[data-v-9e48fde0]{font-size:1.1rem;text-align:center;color:gray}.landing__main[data-v-9e48fde0]{position:relative;width:100%;height:656px}.landing__content[data-v-9e48fde0]{position:absolute;background-color:#fff;left:0;right:0;width:100%;border:1px solid #e5e5e5;padding:1rem;border-radius:.5rem;display:inline-block}.landing__docs-link[data-v-9e48fde0]{text-align:center;margin-bottom:.7rem}",""])},function(t,e,n){e=t.exports=n(1)(),e.push([t.i,".tabs__tab{border:1px solid #555;color:#333;padding:.5rem;margin:0 .3rem;border-radius:.3rem;background-color:#ddd;user-select:none;font-size:1rem}.tabs__tab:not(.tabs__tab--active):hover{background-color:#eee;cursor:pointer}.tabs__tab--active{color:#eee;background-color:#35495e}",""])},function(t,e,n){n(40);var r=n(0)(n(9),n(33),"data-v-63623a41",null);t.exports=r.exports},function(t,e,n){var r=n(0)(n(10),n(34),null,null);t.exports=r.exports},function(t,e,n){n(39);var r=n(0)(n(11),n(32),"data-v-46356e1c",null);t.exports=r.exports},function(t,e,n){n(42);var r=n(0)(n(12),n(36),null,null);t.exports=r.exports},function(t,e,n){n(37);var r=n(0)(n(13),n(29),null,null);t.exports=r.exports},function(t,e,n){n(38);var r=n(0)(n(14),n(30),"data-v-262cfbc0",null);t.exports=r.exports},function(t,e,n){var r=n(0)(n(15),n(31),null,null);t.exports=r.exports},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"tabs"},[t._t("default")],2)},staticRenderFns:[]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("Motion",{attrs:{tag:"div",values:t.values,spring:t.spring},on:{"motion-end":t.end},scopedSlots:t._u([["default",function(e){return[n("div",{staticClass:"logo",style:t.style(e.x)},[n("VueSvg",{attrs:{width:256,height:221,x:e.x,rotated:e.rotated},nativeOn:{touchstart:function(e){t.animate(e)}}})],1)]}]])})},staticRenderFns:[]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("svg",{style:t.style,attrs:{viewBox:"0 0 256 221",xmlns:"http://www.w3.org/2000/svg",preserveAspectRatio:"xMinYMin meet"}},[n("path",{attrs:{d:"M0 0l128 220.8L256 0h-51.2L128 132.48 50.56 0H0z",fill:"#41B883"}}),t._v(" "),n("path",{attrs:{d:"M50.56 0L128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56z",fill:"#35495E"}})])},staticRenderFns:[]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("Motion",{attrs:{value:t.n,spring:t.config},on:{"motion-start":t.start,"motion-end":t.end,"motion-restart":t.restart},scopedSlots:t._u([["default",function(e){return[n("span",[t._v("Value is")]),t._v(" "),n("pre",[t._v(t._s(e))]),t._v(" "),n("div",{staticClass:"demo-container"},[n("div",{staticClass:"demo",style:{transform:"translate3d("+e.value+"px, 0, 0)"}})])]}]])}),t._v(" "),n("input",{directives:[{name:"model",rawName:"v-model.number",value:t.n,expression:"n",modifiers:{number:!0}}],attrs:{step:"10",type:"number"},domProps:{value:t.n},on:{input:function(e){e.target.composing||(t.n=t._n(e.target.value))},blur:function(e){t.$forceUpdate()}}}),t._v(" "),n("br"),t._v(" "),n("button",{on:{click:t.toggle}},[t._v("Toggle")]),t._v(" "),n("br"),t._v(" "),n("label",[t._v("\n Stiffness\n "),n("input",{directives:[{name:"model",rawName:"v-model.number",value:t.config.stiffness,expression:"config.stiffness",modifiers:{number:!0}}],attrs:{step:"10",type:"number"},domProps:{value:t.config.stiffness},on:{input:function(e){e.target.composing||(t.config.stiffness=t._n(e.target.value))},blur:function(e){t.$forceUpdate()}}})]),t._v(" "),n("br"),t._v(" "),n("label",[t._v("\n Damping\n "),n("input",{directives:[{name:"model",rawName:"v-model.number",value:t.config.damping,expression:"config.damping",modifiers:{number:!0}}],attrs:{step:"1",type:"number"},domProps:{value:t.config.damping},on:{input:function(e){e.target.composing||(t.config.damping=t._n(e.target.value))},blur:function(e){t.$forceUpdate()}}})]),t._v(" "),n("br"),t._v(" "),n("label",[t._v("\n Precision\n "),n("input",{directives:[{name:"model",rawName:"v-model.number",value:t.config.precision,expression:"config.precision",modifiers:{number:!0}}],attrs:{step:"0.01",type:"number"},domProps:{value:t.config.precision},on:{input:function(e){e.target.composing||(t.config.precision=t._n(e.target.value))},blur:function(e){t.$forceUpdate()}}})]),t._v(" "),n("br"),t._v(" "),t._l(t.presets,function(e,r){return n("button",{on:{click:function(n){t.setSpring(e)}}},[t._v(t._s(r))])})],2)},staticRenderFns:[]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("div",{staticClass:"controls"},[n("button",{on:{click:t.previous}},[t._v("Previous")]),t._v(" "),n("input",{directives:[{name:"model",rawName:"v-model",value:t.current,expression:"current"}],attrs:{type:"range",min:"0",max:t.photos.length-1},domProps:{value:t.current},on:{__r:function(e){t.current=e.target.value}}}),t._v(" "),n("button",{on:{click:t.next}},[t._v("Next")])]),t._v(" "),n("div",{staticClass:"demo"},[n("Motion",{staticClass:"demo-inner",attrs:{values:t.sizesNormalized,tag:"div"},scopedSlots:t._u([["default",function(e){return[n("PhotosContainer",{staticClass:"container",style:{width:e.layout.width+"px",height:e.layout.height+"px"},attrs:{sizes:t.sizes,current:t.current},scopedSlots:t._u([["default",function(r){return[n("div",{staticClass:"photos",style:{left:r.left+"px"}},t._l(t.photos,function(r,i){return n("img",{staticClass:"photo",style:{width:e.pictures[i].width+"px",height:e.pictures[i].height+"px"},attrs:{src:r.src},on:{touchstart:t.next}})}))]}]])})]}]])})],1)])},staticRenderFns:[]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement;return(t._self._c||e)("Motion",{attrs:{tag:"div",value:t.left},scopedSlots:t._u([["default",function(e){return[t._t("default",null,{left:e.value})]}]])})},staticRenderFns:[]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"landing"},[n("h1",{staticClass:"landing__title"},[t._v("Vue Motion")]),t._v(" "),n("h2",{staticClass:"landing__subtitle"},[t._v("Natural animations for Vue")]),t._v(" "),n("VueLogo"),t._v(" "),n("section",[n("Tabs",{model:{value:t.currentTab,callback:function(e){t.currentTab=e},expression:"currentTab"}},[n("Tab",{attrs:{index:0}},[t._v("Playground")]),t._v(" "),n("Tab",{attrs:{index:1}},[t._v("Gallery Example")])],1),t._v(" "),t._m(0),t._v(" "),n("Motion",{staticClass:"landing__main",attrs:{tag:"div",values:t.tabsPositions},scopedSlots:t._u([["default",function(e){return[n("Playground",{ref:"first",staticClass:"landing__content",style:{transform:"translateX("+e.first+"px)"}}),t._v(" "),n("Gallery",{ref:"second",staticClass:"landing__content",style:{transform:"translateX("+e.second+"px)"}})]}]])})],1)],1)},staticRenderFns:[function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"landing__docs-link"},[n("a",{attrs:{href:"#/home"}},[t._v("Documentation")])])}]}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"tabs__tab",class:t.classes,attrs:{role:"button",tabindex:"0"},on:{click:t.select,keyup:function(e){if(!("button"in e)&&t._k(e.keyCode,"enter",13)&&t._k(e.keyCode,"space",32))return null;t.select(e)}}},[t._t("default")],2)},staticRenderFns:[]}},function(t,e,n){var r=n(16);"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n(2)("bab2b4d2",r,!0)},function(t,e,n){var r=n(17);"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n(2)("1d85cfb3",r,!0)},function(t,e,n){var r=n(18);"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n(2)("29bcf9ae",r,!0)},function(t,e,n){var r=n(19);"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n(2)("0b6f84f8",r,!0)},function(t,e,n){var r=n(20);"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n(2)("cddfbd04",r,!0)},function(t,e,n){var r=n(21);"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n(2)("d3770410",r,!0)},function(t,e){t.exports=function(t,e){for(var n=[],r={},i=0;i 2 | 3 | 4 | 5 | 6 | 7 | VueMotion 8 | 9 | 10 | 11 |
12 | 13 | 14 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/src/App.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 74 | 75 | 124 | -------------------------------------------------------------------------------- /docs/src/Gallery.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 141 | 142 | 177 | -------------------------------------------------------------------------------- /docs/src/PhotosContainer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /docs/src/Playground.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 102 | 103 | 115 | -------------------------------------------------------------------------------- /docs/src/Tab.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /docs/src/Tabs.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /docs/src/VueLogo.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 82 | 83 | 98 | -------------------------------------------------------------------------------- /docs/src/VueSvg.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | -------------------------------------------------------------------------------- /docs/static/cat1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posva/vue-motion/a0c574a56012990fa1e9f0c0ba4529b813d43f20/docs/static/cat1.jpg -------------------------------------------------------------------------------- /docs/static/cat2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posva/vue-motion/a0c574a56012990fa1e9f0c0ba4529b813d43f20/docs/static/cat2.jpg -------------------------------------------------------------------------------- /docs/static/cat3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posva/vue-motion/a0c574a56012990fa1e9f0c0ba4529b813d43f20/docs/static/cat3.jpg -------------------------------------------------------------------------------- /docs/static/cat4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posva/vue-motion/a0c574a56012990fa1e9f0c0ba4529b813d43f20/docs/static/cat4.jpg -------------------------------------------------------------------------------- /docs/static/cat5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posva/vue-motion/a0c574a56012990fa1e9f0c0ba4529b813d43f20/docs/static/cat5.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-motion", 3 | "version": "0.2.3", 4 | "description": "Easy and natural state transitions", 5 | "author": { 6 | "name": "Eduardo San Martin Morote", 7 | "email": "posva13@gmail.com" 8 | }, 9 | "main": "dist/vue-motion.cjs.js", 10 | "browser": "dist/vue-motion.es.js", 11 | "unpkg": "dist/vue-motion.js", 12 | "files": [ 13 | "dist", 14 | "src" 15 | ], 16 | "scripts": { 17 | "build": "rollit", 18 | "predocs": "cd docs && rimraf dist", 19 | "docs": "cd docs && vue build src/App.vue --lib Landing --prod", 20 | "docs:dev": "npm-run-parallel docs:build docs:serve", 21 | "docs:build": "cd docs && vue build src/App.vue", 22 | "docs:serve": "docute docs", 23 | "docs:release": "yon docs && git add docs/dist && git commit -m '📦 Bundle docs Demo'", 24 | "lint": "eslint --ext js --ext jsx --ext vue src test/**/*.spec.js test/*.js test/helpers docs/src build", 25 | "lint:fix": "yon run lint -- --fix", 26 | "lint:staged": "lint-staged", 27 | "pretest": "yon run lint", 28 | "test": "cross-env BABEL_ENV=test karma start test/karma.conf.js --single-run", 29 | "dev": "cross-env BABEL_ENV=test karma start test/karma.conf.js" 30 | }, 31 | "lint-staged": { 32 | "*.{vue,jsx,js}": [ 33 | "eslint --fix", 34 | "git add" 35 | ] 36 | }, 37 | "pre-commit": "lint:staged", 38 | "devDependencies": { 39 | "babel-core": "^6.25.0", 40 | "babel-helper-vue-jsx-merge-props": "^2.0.2", 41 | "babel-loader": "^7.1.1", 42 | "babel-plugin-istanbul": "^4.1.4", 43 | "babel-plugin-syntax-jsx": "^6.18.0", 44 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 45 | "babel-plugin-transform-runtime": "^6.23.0", 46 | "babel-plugin-transform-vue-jsx": "^3.5.0", 47 | "babel-preset-env": "^1.6.0", 48 | "buble": "^0.15.2", 49 | "chai": "^4.1.0", 50 | "chai-dom": "^1.5.0", 51 | "cross-env": "^5.0.1", 52 | "eslint": "^4.3.0", 53 | "eslint-config-posva": "^1.0.0", 54 | "inject-loader": "^3.0.1", 55 | "karma": "^1.7.0", 56 | "karma-chai-dom": "^1.1.0", 57 | "karma-chrome-launcher": "^2.2.0", 58 | "karma-coverage": "^1.1.1", 59 | "karma-mocha": "^1.3.0", 60 | "karma-sinon-chai": "^1.3.1", 61 | "karma-sourcemap-loader": "^0.3.7", 62 | "karma-spec-reporter": "^0.0.31", 63 | "karma-webpack": "^2.0.4", 64 | "lint-staged": "^4.0.2", 65 | "mkdirp": "^0.5.1", 66 | "mocha": "^3.4.2", 67 | "npm-run-parallel": "^0.5.0", 68 | "pre-commit": "^1.2.2", 69 | "rimraf": "^2.6.1", 70 | "rollup": "^0.45.2", 71 | "rollup-plugin-buble": "^0.15.0", 72 | "rollup-plugin-commonjs": "^8.0.2", 73 | "rollup-plugin-jsx": "^1.0.3", 74 | "rollup-plugin-node-resolve": "^3.0.0", 75 | "rollup-plugin-replace": "^1.1.1", 76 | "rollup-plugin-vue": "^2.4.1", 77 | "sinon": "^2.4.1", 78 | "sinon-chai": "^2.12.0", 79 | "uglify-js": "^3.0.26", 80 | "uppercamelcase": "^3.0.0", 81 | "vue": "^2.4.2", 82 | "vue-loader": "^13.0.2", 83 | "vue-template-compiler": "^2.4.2", 84 | "webpack": "^3.4.1", 85 | "yarn-or-npm": "^2.0.4" 86 | }, 87 | "peerDependencies": { 88 | "vue": "^2.1.10" 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/posva/vue-motion.git" 93 | }, 94 | "bugs": { 95 | "url": "https://github.com/posva/vue-motion/issues" 96 | }, 97 | "homepage": "https://github.com/posva/vue-motion#readme", 98 | "license": "MIT", 99 | "module": "dist/vue-motion.es.js" 100 | } 101 | -------------------------------------------------------------------------------- /src/Motion.js: -------------------------------------------------------------------------------- 1 | import stepper from './stepper' 2 | import presets from './presets' 3 | import { raf, now, isArray, isObject } from './utils' 4 | 5 | const msPerFrame = 1000 / 60 6 | 7 | export default { 8 | data () { 9 | return { 10 | currentValues: null, 11 | currentVelocities: null, 12 | } 13 | }, 14 | 15 | props: { 16 | value: Number, 17 | values: [Object, Array], 18 | tag: { 19 | type: String, 20 | default: 'span', 21 | }, 22 | spring: { 23 | type: [Object, String], 24 | default: 'noWobble', 25 | }, 26 | }, 27 | 28 | computed: { 29 | springConfig () { 30 | return typeof this.spring === 'string' ? presets[this.spring] : this.spring 31 | }, 32 | realValues () { 33 | return this.value != null ? { value: this.value } : this.values 34 | }, 35 | }, 36 | 37 | render (h) { 38 | return h(this.tag, [this.$scopedSlots.default(this.currentValues)]) 39 | }, 40 | 41 | watch: { 42 | realValues (current, old) { 43 | if (old !== current && !this.wasAnimating) { 44 | this.prevTime = now() 45 | this.accumulatedTime = 0 46 | this.animate() 47 | } 48 | }, 49 | }, 50 | 51 | created () { 52 | const current = this.defineInitialValues(this.realValues, null) 53 | 54 | this.currentValues = current.values 55 | this.currentVelocities = current.velocities 56 | }, 57 | 58 | mounted () { 59 | this.prevTime = now() 60 | this.accumulatedTime = 0 61 | 62 | const ideal = this.defineInitialValues(this.currentValues, this.currentVelocities) 63 | 64 | this.idealValues = ideal.values 65 | this.idealVelocities = ideal.velocities 66 | 67 | this.animate() 68 | }, 69 | 70 | methods: { 71 | defineInitialValues (values, velocities) { 72 | const newValues = {} 73 | const newVelocities = {} 74 | 75 | this.defineValues(values, velocities, newValues, newVelocities) 76 | 77 | return { values: newValues, velocities: newVelocities } 78 | }, 79 | 80 | defineValues (values, velocities, newValues, newVelocities) { 81 | for (const key in values) { 82 | // istanbul ignore if 83 | if (!Object.prototype.hasOwnProperty.call(values, key)) continue 84 | 85 | if (isArray(values[key]) || isObject(values[key])) { 86 | newValues[key] = {} 87 | newVelocities[key] = {} 88 | 89 | this.defineValues( 90 | values[key], 91 | velocities && velocities[key], 92 | newValues[key], 93 | newVelocities[key] 94 | ) 95 | 96 | continue 97 | } 98 | 99 | newValues[key] = values[key] 100 | newVelocities[key] = velocities ? velocities[key] : 0 101 | } 102 | }, 103 | 104 | animate () { 105 | this.animationId = raf(() => { 106 | if (shouldStopAnimation(this.currentValues, this.realValues, this.currentVelocities)) { 107 | if (this.wasAnimating) this.$emit('motion-end') 108 | 109 | // reset everything for next animation 110 | this.animationId = null 111 | this.wasAnimating = false 112 | return 113 | } 114 | 115 | if (!this.wasAnimating) this.$emit('motion-start') 116 | this.wasAnimating = true 117 | 118 | // get time from last frame 119 | const currentTime = now() 120 | const timeDelta = currentTime - this.prevTime 121 | this.prevTime = currentTime 122 | this.accumulatedTime += timeDelta 123 | 124 | // more than 10 frames? prolly switched browser tab. Restart 125 | if (this.accumulatedTime > msPerFrame * 10) { 126 | this.accumulatedTime = 0 127 | } 128 | 129 | if (this.accumulatedTime === 0) { 130 | // no need to cancel animationID here; shouldn't have any in flight 131 | this.animationID = null 132 | this.$emit('motion-restart') 133 | this.animate() 134 | return 135 | } 136 | 137 | const currentFrameCompletion = 138 | (this.accumulatedTime - Math.floor(this.accumulatedTime / msPerFrame) * msPerFrame) / 139 | msPerFrame 140 | const framesToCatchUp = Math.floor(this.accumulatedTime / msPerFrame) 141 | const springConfig = this.springConfig 142 | 143 | this.animateValues({ 144 | framesToCatchUp, 145 | currentFrameCompletion, 146 | springConfig, 147 | realValues: this.realValues, 148 | currentValues: this.currentValues, 149 | currentVelocities: this.currentVelocities, 150 | idealValues: this.idealValues, 151 | idealVelocities: this.idealVelocities, 152 | }) 153 | 154 | // out of the update loop 155 | this.animationID = null 156 | // the amount we're looped over above 157 | this.accumulatedTime -= framesToCatchUp * msPerFrame 158 | 159 | // keep going! 160 | this.animate() 161 | }) 162 | }, 163 | 164 | animateValues ({ 165 | framesToCatchUp, 166 | currentFrameCompletion, 167 | springConfig, 168 | realValues, 169 | currentValues, 170 | currentVelocities, 171 | idealValues, 172 | idealVelocities, 173 | }) { 174 | for (const key in realValues) { 175 | // istanbul ignore if 176 | if (!Object.prototype.hasOwnProperty.call(realValues, key)) continue 177 | 178 | if (isArray(realValues[key]) || isObject(realValues[key])) { 179 | // the value may have been added 180 | if (!idealValues[key]) { 181 | const ideal = this.defineInitialValues(this.realValues[key], null) 182 | const current = this.defineInitialValues(this.realValues[key], null) 183 | this.$set(idealValues, key, ideal.values) 184 | this.$set(idealVelocities, key, ideal.velocities) 185 | this.$set(currentValues, key, current.values) 186 | this.$set(currentVelocities, key, current.velocities) 187 | } 188 | 189 | this.animateValues({ 190 | framesToCatchUp, 191 | currentFrameCompletion, 192 | springConfig, 193 | realValues: realValues[key], 194 | currentValues: currentValues[key], 195 | currentVelocities: currentVelocities[key], 196 | idealValues: idealValues[key], 197 | idealVelocities: idealVelocities[key], 198 | }) 199 | 200 | // nothing to animate 201 | continue 202 | } 203 | 204 | let newIdealValue = idealValues[key] 205 | let newIdealVelocity = idealVelocities[key] 206 | const value = realValues[key] 207 | 208 | // iterate as if the animation took place 209 | for (let i = 0; i < framesToCatchUp; i++) { 210 | [newIdealValue, newIdealVelocity] = stepper( 211 | msPerFrame / 1000, 212 | newIdealValue, 213 | newIdealVelocity, 214 | value, 215 | springConfig.stiffness, 216 | springConfig.damping, 217 | springConfig.precision 218 | ) 219 | } 220 | 221 | const [nextIdealValue, nextIdealVelocity] = stepper( 222 | msPerFrame / 1000, 223 | newIdealValue, 224 | newIdealVelocity, 225 | value, 226 | springConfig.stiffness, 227 | springConfig.damping, 228 | springConfig.precision 229 | ) 230 | 231 | currentValues[key] = 232 | newIdealValue + (nextIdealValue - newIdealValue) * currentFrameCompletion 233 | currentVelocities[key] = 234 | newIdealVelocity + (nextIdealVelocity - newIdealVelocity) * currentFrameCompletion 235 | idealValues[key] = newIdealValue 236 | idealVelocities[key] = newIdealVelocity 237 | } 238 | }, 239 | }, 240 | } 241 | 242 | function shouldStopAnimation (currentValues, values, currentVelocities) { 243 | for (const key in values) { 244 | // istanbul ignore if 245 | if (!Object.prototype.hasOwnProperty.call(values, key)) continue 246 | 247 | if (isArray(values[key]) || isObject(values[key])) { 248 | if (!shouldStopAnimation(currentValues[key], values[key], currentVelocities[key])) { 249 | return false 250 | } 251 | // skip the other checks 252 | continue 253 | } 254 | 255 | if (currentVelocities[key] !== 0) return false 256 | 257 | // stepper will have already taken care of rounding precision errors, so 258 | // won't have such thing as 0.9999 !=== 1 259 | if (currentValues[key] !== values[key]) return false 260 | } 261 | 262 | return true 263 | } 264 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Motion from './Motion' 2 | import presets from './presets' 3 | 4 | function plugin (Vue) { 5 | Vue.component('Motion', Motion) 6 | } 7 | 8 | // Install by default if using the script tag 9 | if (typeof window !== 'undefined' && window.Vue) { 10 | window.Vue.use(plugin) 11 | } 12 | 13 | // Allow doing VueMotion.presets.custom = ... 14 | plugin.presets = presets 15 | 16 | export default plugin 17 | const version = '__VERSION__' 18 | // Export all components too 19 | export { Motion, version, presets } 20 | -------------------------------------------------------------------------------- /src/presets.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export default { 3 | noWobble: { stiffness: 170, damping: 26, precision: 0.01 }, // the default, if nothing provided 4 | gentle: { stiffness: 120, damping: 14, precision: 0.01 }, 5 | wobbly: { stiffness: 180, damping: 12, precision: 0.01 }, 6 | stiff: { stiffness: 210, damping: 20, precision: 0.01 }, 7 | } 8 | -------------------------------------------------------------------------------- /src/stepper.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // stepper is used a lot. Saves allocation to return the same array wrapper. 4 | // This is fine and danger-free against mutations because the callsite 5 | // immediately destructures it and gets the numbers inside without passing the 6 | // array reference around. 7 | const reusedTuple = [0, 0] 8 | export default function stepper ( 9 | secondPerFrame, 10 | x, 11 | v, 12 | destX, 13 | k, 14 | b, 15 | precision 16 | ) { 17 | // Spring stiffness, in kg / s^2 18 | 19 | // for animations, destX is really spring length (spring at rest). initial 20 | // position is considered as the stretched/compressed position of a spring 21 | const Fspring = -k * (x - destX) 22 | 23 | // Damping, in kg / s 24 | const Fdamper = -b * v 25 | 26 | // usually we put mass here, but for animation purposes, specifying mass is a 27 | // bit redundant. you could simply adjust k and b accordingly 28 | // let a = (Fspring + Fdamper) / mass 29 | const a = Fspring + Fdamper 30 | 31 | const newV = v + a * secondPerFrame 32 | const newX = x + newV * secondPerFrame 33 | 34 | if (Math.abs(newV) < precision && Math.abs(newX - destX) < precision) { 35 | reusedTuple[0] = destX 36 | reusedTuple[1] = 0 37 | return reusedTuple 38 | } 39 | 40 | reusedTuple[0] = newX 41 | reusedTuple[1] = newV 42 | return reusedTuple 43 | } 44 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const raf = typeof window !== 'undefined' 2 | ? window.requestAnimationFrame.bind(window) 3 | : _ => {} 4 | export const now = typeof performance !== 'undefined' 5 | ? performance.now.bind(performance) 6 | : Date.now.bind(Date) 7 | export const isArray = Array.isArray.bind(Array) 8 | export const isObject = value => value !== null && typeof value === 'object' 9 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import { createVM, Vue } from './utils' 2 | import { nextTick, delay } from './wait-for-update' 3 | 4 | export { 5 | createVM, 6 | Vue, 7 | nextTick, 8 | delay, 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | export function createVM (context, template, opts = {}) { 6 | const el = document.createElement('div') 7 | document.getElementById('tests').appendChild(el) 8 | const render = typeof template === 'string' 9 | ? { template: `
${template}
` } 10 | : { render: template } 11 | return new Vue({ 12 | el, 13 | name: 'Test', 14 | ...render, 15 | ...opts, 16 | }) 17 | } 18 | 19 | const emptyNodes = document.querySelectorAll('nonexistant') 20 | Vue.prototype.$$ = function $$ (selector) { 21 | const els = document.querySelectorAll(selector) 22 | const vmEls = this.$el.querySelectorAll(selector) 23 | const fn = vmEls.length 24 | ? el => vmEls.find(el) 25 | : el => this.$el === el 26 | const found = Array.from(els).filter(fn) 27 | return found.length 28 | ? found 29 | : emptyNodes 30 | } 31 | 32 | Vue.prototype.$ = function $ (selector) { 33 | const els = document.querySelectorAll(selector) 34 | const vmEl = this.$el.querySelector(selector) 35 | const fn = vmEl 36 | ? el => el === vmEl 37 | : el => el === this.$el 38 | // Allow should chaining for tests 39 | return Array.from(els).find(fn) || emptyNodes 40 | } 41 | 42 | export { Vue } 43 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue' 2 | 3 | // Testing helper 4 | // nextTick().then(() => { 5 | // 6 | // Automatically waits for nextTick 7 | // }).then(() => { 8 | // return a promise or value to skip the wait 9 | // }) 10 | function nextTick () { 11 | const jobs = [] 12 | let done 13 | 14 | const chainer = { 15 | then (cb) { 16 | jobs.push(cb) 17 | return chainer 18 | }, 19 | } 20 | 21 | function shift (...args) { 22 | const job = jobs.shift() 23 | let result 24 | try { 25 | result = job(...args) 26 | } catch (e) { 27 | jobs.length = 0 28 | done(e) 29 | } 30 | 31 | // wait for nextTick 32 | if (result !== undefined) { 33 | if (result.then) { 34 | result.then(shift) 35 | } else { 36 | shift(result) 37 | } 38 | } else if (jobs.length) { 39 | requestAnimationFrame(() => Vue.nextTick(shift)) 40 | } 41 | } 42 | 43 | // First time 44 | Vue.nextTick(() => { 45 | done = jobs[jobs.length - 1] 46 | if (done.toString().slice(0, 14) !== 'function (err)') { 47 | throw new Error('waitForUpdate chain is missing .then(done)') 48 | } 49 | shift() 50 | }) 51 | 52 | return chainer 53 | } 54 | 55 | function delay (time) { 56 | return new Promise(resolve => setTimeout(resolve, time)) 57 | } 58 | 59 | export { 60 | nextTick, 61 | delay, 62 | } 63 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // require all src files for coverage. 2 | // you can also change this to match only the subset of files that 3 | // you want coverage for. 4 | const srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) 5 | srcContext.keys().forEach(srcContext) 6 | 7 | // Use a div to insert elements 8 | before(function () { 9 | const el = document.createElement('DIV') 10 | el.id = 'tests' 11 | document.body.appendChild(el) 12 | }) 13 | 14 | // Remove every test html scenario 15 | afterEach(function () { 16 | const el = document.getElementById('tests') 17 | for (let i = 0; i < el.children.length; ++i) { 18 | el.removeChild(el.children[i]) 19 | } 20 | }) 21 | 22 | const specsContext = require.context('./specs', true) 23 | specsContext.keys().forEach(specsContext) 24 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../build/webpack.config.karma.js') 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | // to run in additional browsers: 6 | // 1. install corresponding karma launcher 7 | // http://karma-runner.github.io/0.13/config/browsers.html 8 | // 2. add it to the `browsers` array below. 9 | browsers: ['Chrome'], 10 | frameworks: ['mocha', 'chai-dom', 'sinon-chai'], 11 | reporters: ['spec', 'coverage'], 12 | files: ['./index.js'], 13 | preprocessors: { 14 | './index.js': ['webpack', 'sourcemap'], 15 | }, 16 | webpack: webpackConfig, 17 | webpackMiddleware: { 18 | noInfo: true, 19 | }, 20 | coverageReporter: { 21 | dir: './coverage', 22 | reporters: [ 23 | { type: 'lcov', subdir: '.' }, 24 | { type: 'text-summary' }, 25 | ], 26 | }, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /test/specs/Motion.spec.js: -------------------------------------------------------------------------------- 1 | import MotionInjector from 'inject-loader!src/Motion' 2 | import presets from 'src/presets' 3 | import { isArray, isObject } from 'src/utils' 4 | import { createVM, nextTick } from '../helpers' 5 | 6 | const msPerFrame = 1000 / 60 7 | 8 | let Motion 9 | 10 | describe('Motion', function () { 11 | beforeEach(function () { 12 | let now = 0 13 | const queue = [] 14 | this.raf = sinon.spy(cb => { 15 | queue.push(cb) 16 | }) 17 | this.step = function step (n = 1) { 18 | for (let i = 0; i < n; ++i) { 19 | if (!queue.length) return 20 | queue.shift()() 21 | } 22 | } 23 | this.stepUntil = function step (fn, maxCount = 5000) { 24 | let count = 0 25 | while (queue.length && !fn() && count++ < maxCount) { 26 | queue.shift()() 27 | } 28 | if (count >= maxCount) throw new Error('Too many calls') 29 | } 30 | this.timeSlowdown = 1 31 | this.now = sinon.spy(() => (now += this.timeSlowdown * msPerFrame)) // eslint-disable-line no-return-assign 32 | Motion = MotionInjector({ 33 | './utils': { 34 | raf: this.raf, 35 | now: this.now, 36 | isArray, 37 | isObject, 38 | }, 39 | }).default 40 | }) 41 | 42 | it('works with perfect time', function (done) { 43 | const vm = createVM( 44 | this, 45 | ` 46 | 47 | 50 | 51 | `, 52 | { 53 | data: { 54 | n: 0, 55 | config: { 56 | stiffness: 170, 57 | damping: 26, 58 | precision: 0.01, 59 | }, 60 | }, 61 | components: { Motion }, 62 | } 63 | ) 64 | vm.$('pre').should.have.text('0') 65 | vm.n = 10 66 | nextTick() 67 | .then(() => { 68 | this.step() 69 | }) 70 | .then(() => { 71 | vm.$('pre').should.have.text('0.4722222222222221') 72 | this.step() 73 | }) 74 | .then(() => { 75 | vm.$('pre').should.have.text('1.1897376543209877') 76 | this.stepUntil(() => vm.$('pre').text === '10') 77 | }) 78 | .then(done) 79 | }) 80 | 81 | it('works with imperfect time', function (done) { 82 | this.timeSlowdown = 11 83 | const vm = createVM( 84 | this, 85 | ` 86 | 87 | 90 | 91 | `, 92 | { 93 | data: { 94 | n: 0, 95 | config: { 96 | stiffness: 170, 97 | damping: 26, 98 | precision: 0.01, 99 | }, 100 | }, 101 | components: { Motion }, 102 | } 103 | ) 104 | vm.$('pre').should.have.text('0') 105 | vm.n = 10 106 | nextTick() 107 | .then(() => { 108 | this.step() 109 | }) 110 | .then(() => { 111 | vm.$('pre').should.have.text('0') 112 | this.timeSlowdown = 0.01 113 | this.step() 114 | }) 115 | .then(() => { 116 | vm.$('pre').should.have.text('0.0047222222222211485') 117 | this.step() 118 | }) 119 | .then(() => { 120 | vm.$('pre').should.have.text('0.009444444444442297') 121 | }) 122 | .then(done) 123 | }) 124 | 125 | it('accepts a string as the spring', function (done) { 126 | const vm = createVM( 127 | this, 128 | ` 129 | 130 | 133 | 134 | `, 135 | { 136 | data: { 137 | n: 0, 138 | spring: 'noWobble', 139 | }, 140 | components: { Motion }, 141 | } 142 | ) 143 | vm.$refs.motion.springConfig.should.eql(presets.noWobble) 144 | vm.spring = 'gentle' 145 | nextTick() 146 | .then(() => { 147 | vm.$refs.motion.springConfig.should.eql(presets.gentle) 148 | }) 149 | .then(done) 150 | }) 151 | 152 | it('can define custom presets for springs', function (done) { 153 | presets.custom = { 154 | stiffness: 10, 155 | damping: 20, 156 | precision: 0.03, 157 | } 158 | 159 | const vm = createVM( 160 | this, 161 | ` 162 | 163 | 166 | 167 | `, 168 | { 169 | data: { 170 | n: 0, 171 | spring: 'noWobble', 172 | }, 173 | components: { Motion }, 174 | } 175 | ) 176 | vm.$refs.motion.springConfig.should.eql(presets.noWobble) 177 | vm.spring = 'custom' 178 | nextTick() 179 | .then(() => { 180 | vm.$refs.motion.springConfig.should.eql(presets.custom) 181 | 182 | delete presets.custom 183 | }) 184 | .then(done) 185 | }) 186 | 187 | it('uses noWobble by default as the spring', function () { 188 | const vm = createVM( 189 | this, 190 | ` 191 | 192 | 195 | 196 | `, 197 | { 198 | data: { n: 0 }, 199 | components: { Motion }, 200 | } 201 | ) 202 | vm.$refs.motion.springConfig.should.eql(presets.noWobble) 203 | }) 204 | 205 | it('supports array syntax', function (done) { 206 | const vm = createVM( 207 | this, 208 | ` 209 | 210 | 214 | 215 | `, 216 | { 217 | data: { 218 | values: [0, -10], 219 | config: { 220 | stiffness: 170, 221 | damping: 26, 222 | precision: 0.01, 223 | }, 224 | }, 225 | components: { Motion }, 226 | } 227 | ) 228 | vm.$('.a').should.have.text('0') 229 | vm.values[0] = 10 230 | nextTick() 231 | .then(() => { 232 | this.step() 233 | vm.values[1] = 0 234 | }) 235 | .then(() => { 236 | vm.$('.a').should.have.text('0.4722222222222222') 237 | this.step() 238 | }) 239 | .then(() => { 240 | vm.$('.a').should.have.text('1.1897376543209877') 241 | vm.$('.b').should.have.text('-9.527777777777779') 242 | this.stepUntil(() => vm.$('.a').text === '10') 243 | }) 244 | .then(done) 245 | }) 246 | 247 | it('supports object syntax', function (done) { 248 | const vm = createVM( 249 | this, 250 | ` 251 | 252 | 256 | 257 | `, 258 | { 259 | data: { 260 | values: { 261 | a: 0, 262 | b: -10, 263 | }, 264 | config: { 265 | stiffness: 170, 266 | damping: 26, 267 | precision: 0.01, 268 | }, 269 | }, 270 | components: { Motion }, 271 | } 272 | ) 273 | vm.$('.a').should.have.text('0') 274 | vm.values.a = 10 275 | nextTick() 276 | .then(() => { 277 | this.step() 278 | vm.values.b = 0 279 | }) 280 | .then(() => { 281 | vm.$('.a').should.have.text('0.4722222222222222') 282 | this.step() 283 | }) 284 | .then(() => { 285 | vm.$('.a').should.have.text('1.1897376543209877') 286 | vm.$('.b').should.have.text('-9.527777777777779') 287 | this.stepUntil(() => vm.$('.a').text === '10') 288 | }) 289 | .then(done) 290 | }) 291 | 292 | it('supports nested arrays', function (done) { 293 | const vm = createVM( 294 | this, 295 | ` 296 | 297 | 303 | 304 | `, 305 | { 306 | data: { 307 | values: [[0, -10], [-10, 0]], 308 | config: { 309 | stiffness: 170, 310 | damping: 26, 311 | precision: 0.01, 312 | }, 313 | }, 314 | components: { Motion }, 315 | } 316 | ) 317 | vm.$('.v00').should.have.text('0') 318 | vm.values[0][0] = 10 319 | nextTick() 320 | .then(() => { 321 | this.step() 322 | vm.values[1][0] = 0 323 | }) 324 | .then(() => { 325 | vm.$('.v00').should.have.text('0.4722222222222222') 326 | this.step() 327 | }) 328 | .then(() => { 329 | vm.$('.v00').should.have.text('1.1897376543209877') 330 | vm.$('.v10').should.have.text('-9.527777777777779') 331 | this.stepUntil(() => vm.$('.v00').text === '10') 332 | }) 333 | .then(done) 334 | }) 335 | 336 | it.skip('supports pushing new elements to arrays', function (done) { 337 | const vm = createVM( 338 | this, 339 | ` 340 | 341 |
342 | {{ v }} 343 |
344 |
345 | `, 346 | { 347 | data: { 348 | values: [10], 349 | }, 350 | components: { Motion }, 351 | } 352 | ) 353 | // vm.$('span').should.have.text('10 ') 354 | vm.values = [10, 20] 355 | nextTick() 356 | .then(() => { 357 | this.step() 358 | }) 359 | .then(() => { 360 | vm.$('.container').should.have.text('10 20 ') 361 | vm.values[1] = 0 362 | // vm.$('.container').should.have.text('10 20') 363 | // this.stepUntil(() => vm.$('.container').text === '10 20 ') 364 | }) 365 | .then(done) 366 | }) 367 | 368 | it('supports nested objects', function (done) { 369 | const vm = createVM( 370 | this, 371 | ` 372 | 373 | 379 | 380 | `, 381 | { 382 | data: { 383 | values: { 384 | a: { a: 0, b: -10 }, 385 | b: { a: -10, b: 0 }, 386 | }, 387 | config: { 388 | stiffness: 170, 389 | damping: 26, 390 | precision: 0.01, 391 | }, 392 | }, 393 | components: { Motion }, 394 | } 395 | ) 396 | vm.$('.vaa').should.have.text('0') 397 | vm.values.a.a = 10 398 | nextTick() 399 | .then(() => { 400 | this.step() 401 | vm.values.b.a = 0 402 | }) 403 | .then(() => { 404 | vm.$('.vaa').should.have.text('0.4722222222222222') 405 | this.step() 406 | }) 407 | .then(() => { 408 | vm.$('.vaa').should.have.text('1.1897376543209877') 409 | vm.$('.vba').should.have.text('-9.527777777777779') 410 | this.stepUntil(() => vm.$('.vaa').text === '10') 411 | }) 412 | .then(done) 413 | }) 414 | 415 | it('supports nested objects in arrays', function (done) { 416 | const vm = createVM( 417 | this, 418 | ` 419 | 420 | 426 | 427 | `, 428 | { 429 | data: { 430 | values: [{ a: 0, b: -10 }, { a: -10, b: 0 }], 431 | config: { 432 | stiffness: 170, 433 | damping: 26, 434 | precision: 0.01, 435 | }, 436 | }, 437 | components: { Motion }, 438 | } 439 | ) 440 | vm.$('.v0a').should.have.text('0') 441 | vm.values[0].a = 10 442 | nextTick() 443 | .then(() => { 444 | this.step() 445 | vm.values[1].a = 0 446 | }) 447 | .then(() => { 448 | vm.$('.v0a').should.have.text('0.4722222222222222') 449 | this.step() 450 | }) 451 | .then(() => { 452 | vm.$('.v0a').should.have.text('1.1897376543209877') 453 | vm.$('.v1a').should.have.text('-9.527777777777779') 454 | this.stepUntil(() => vm.$('.v0a').text === '10') 455 | }) 456 | .then(done) 457 | }) 458 | 459 | it('supports nested arrays in objects', function (done) { 460 | const vm = createVM( 461 | this, 462 | ` 463 | 464 | 470 | 471 | `, 472 | { 473 | data: { 474 | values: { 475 | a: [0, -10], 476 | b: [-10, 0], 477 | }, 478 | config: { 479 | stiffness: 170, 480 | damping: 26, 481 | precision: 0.01, 482 | }, 483 | }, 484 | components: { Motion }, 485 | } 486 | ) 487 | vm.$('.va0').should.have.text('0') 488 | vm.values.a[0] = 10 489 | nextTick() 490 | .then(() => { 491 | this.step() 492 | vm.values.b[0] = 0 493 | }) 494 | .then(() => { 495 | vm.$('.va0').should.have.text('0.4722222222222222') 496 | this.step() 497 | }) 498 | .then(() => { 499 | vm.$('.va0').should.have.text('1.1897376543209877') 500 | vm.$('.vb0').should.have.text('-9.527777777777779') 501 | this.stepUntil(() => vm.$('.va0').text === '10') 502 | }) 503 | .then(done) 504 | }) 505 | }) 506 | --------------------------------------------------------------------------------