├── demos ├── demo1-chat-heads │ ├── 0.jpg │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── index.jsx │ ├── index.html │ └── Demo.jsx ├── demo4-photo-gallery │ ├── 0.jpg │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── index.jsx │ ├── index.html │ └── Demo.jsx ├── demo8-draggable-list │ ├── cursor.png │ ├── index.jsx │ ├── index.html │ └── Demo.jsx ├── demo7-water-ripples │ ├── index.jsx │ ├── index.html │ └── Demo.jsx ├── demo0-simple-transition │ ├── index.jsx │ ├── index.html │ └── Demo.jsx ├── demo2-draggable-balls │ ├── index.jsx │ ├── index.html │ └── Demo.jsx ├── demo3-todomvc-list-transition │ ├── index.jsx │ ├── index.html │ ├── Demo.jsx │ └── index.css ├── demo5-spring-parameters-chooser │ ├── index.jsx │ ├── index.html │ └── Demo.jsx └── README.md ├── test ├── index.js ├── integration │ ├── README.md │ ├── package.json │ ├── bower.json │ └── bower.html ├── stripStyle-test.js ├── createMockRaf.js ├── mergeDiff-test.js ├── StaggeredMotion-test.js ├── Motion-test.js └── TransitionMotion-test.js ├── .travis.yml ├── .eslintignore ├── .gitignore ├── .npmignore ├── src ├── presets.js ├── reorderKeys.js ├── spring.js ├── mapToZero.js ├── react-motion.js ├── stripStyle.js ├── shouldStopAnimation.js ├── stepper.js ├── Types.js ├── mergeDiff.js ├── Motion.js ├── StaggeredMotion.js └── TransitionMotion.js ├── .babelrc ├── .size-snapshot.json ├── server.js ├── AUTHORS ├── .flowconfig ├── bower.json ├── LICENSE ├── webpack.config.js ├── karma.conf.js ├── rollup.config.js ├── .eslintrc ├── package.json ├── HISTORY.md └── README.md /demos/demo1-chat-heads/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo1-chat-heads/0.jpg -------------------------------------------------------------------------------- /demos/demo1-chat-heads/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo1-chat-heads/1.jpg -------------------------------------------------------------------------------- /demos/demo1-chat-heads/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo1-chat-heads/2.jpg -------------------------------------------------------------------------------- /demos/demo1-chat-heads/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo1-chat-heads/3.jpg -------------------------------------------------------------------------------- /demos/demo1-chat-heads/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo1-chat-heads/4.jpg -------------------------------------------------------------------------------- /demos/demo1-chat-heads/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo1-chat-heads/5.jpg -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo4-photo-gallery/0.jpg -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo4-photo-gallery/1.jpg -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo4-photo-gallery/2.jpg -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo4-photo-gallery/3.jpg -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo4-photo-gallery/4.jpg -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo4-photo-gallery/5.jpg -------------------------------------------------------------------------------- /demos/demo8-draggable-list/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenglou/react-motion/HEAD/demos/demo8-draggable-list/cursor.png -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const testsContext = require.context('./', true, /-test\.js$/); 2 | 3 | testsContext.keys().forEach(testsContext); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 10 5 | 6 | script: 7 | - npm run -s lint 8 | - npm run -s flow_check 9 | - npm run -s test:travis 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | lib 4 | node_modules 5 | **/*/all.js 6 | webpack.*.js 7 | server.js 8 | karma.*.js 9 | test/integration 10 | demos/demo6/babel.js 11 | demos/*/Demo.jsx 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | 4 | pids 5 | *.pid 6 | *.seed 7 | 8 | coverage 9 | node_modules 10 | bower_components 11 | 12 | .DS_Store 13 | 14 | /demos/**/all.* 15 | /build/ 16 | /lib/ 17 | -------------------------------------------------------------------------------- /demos/demo1-chat-heads/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo7-water-ripples/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo8-draggable-list/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo0-simple-transition/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo2-draggable-balls/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo3-todomvc-list-transition/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /demos/demo5-spring-parameters-chooser/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo from './Demo'; 4 | 5 | ReactDOM.render(, document.querySelector('#content')); 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.* 2 | 3 | /bower_components/ 4 | 5 | /coverage/ 6 | /demos/ 7 | /test/ 8 | 9 | /karma.conf.js 10 | /webpack.* 11 | /server.js 12 | 13 | # sublime 14 | /*.sublime-project 15 | /*.sublime-workspace 16 | 17 | /rollup.config.js 18 | -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- 1 | ## Demos folder 2 | 3 | **Note**: since this is the master branch, the demos might be a bit ahead of the current stable API. [This commit](https://github.com/chenglou/react-motion/tree/0627243316c564f6c2f480bf615b82135f649a0a/demos) contains the stable demos. 4 | -------------------------------------------------------------------------------- /src/presets.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export default { 3 | noWobble: { stiffness: 170, damping: 26 }, // the default, if nothing provided 4 | gentle: { stiffness: 120, damping: 14 }, 5 | wobbly: { stiffness: 180, damping: 12 }, 6 | stiff: { stiffness: 210, damping: 20 }, 7 | }; 8 | -------------------------------------------------------------------------------- /demos/demo3-todomvc-list-transition/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RedoMVC 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | Simple folder for testing whether the release worked or not. 2 | 3 | Please run this: 4 | ``` 5 | rm -rf bower_components 6 | rm -rf node_modules 7 | bower install 8 | npm install 9 | node -e 'console.log(require("react-motion"))' 10 | ``` 11 | 12 | Check that the output of that looks normal. 13 | 14 | For Bower, please also open up bower.html 15 | -------------------------------------------------------------------------------- /src/reorderKeys.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | let hasWarned = false; 4 | export default function reorderKeys() { 5 | if (process.env.NODE_ENV === 'development') { 6 | if (!hasWarned) { 7 | hasWarned = true; 8 | console.error( 9 | "`reorderKeys` has been removed, since it is no longer needed for TransitionMotion's new styles array API.", 10 | ); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/spring.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import presets from './presets'; 3 | import type { OpaqueConfig, SpringHelperConfig } from './Types'; 4 | 5 | const defaultConfig = { 6 | ...presets.noWobble, 7 | precision: 0.01, 8 | }; 9 | 10 | export default function spring( 11 | val: number, 12 | config?: SpringHelperConfig, 13 | ): OpaqueConfig { 14 | return { ...defaultConfig, ...config, val }; 15 | } 16 | -------------------------------------------------------------------------------- /src/mapToZero.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { PlainStyle, Style } from './Types'; 3 | 4 | // currently used to initiate the velocity style object to 0 5 | export default function mapToZero(obj: Style | PlainStyle): PlainStyle { 6 | let ret = {}; 7 | for (const key in obj) { 8 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 9 | ret[key] = 0; 10 | } 11 | } 12 | return ret; 13 | } 14 | -------------------------------------------------------------------------------- /test/integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Simple folder for testing whether the release worked or not.", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "react-motion": "^0.5.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/stripStyle-test.js: -------------------------------------------------------------------------------- 1 | import stripStyle from '../src/stripStyle'; 2 | import spring from '../src/spring'; 3 | 4 | describe('stripStyle', () => { 5 | it('should return spring object into value', () => { 6 | expect(stripStyle({a: spring(1, [1, 2])})).toEqual({a: 1}); 7 | }); 8 | 9 | it('should ignore non-configured values', () => { 10 | expect(stripStyle({a: 10, b: 0})).toEqual({a: 10, b: 0}); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { "modules": "commonjs", "loose": true }], 4 | "@babel/flow", 5 | "@babel/react" 6 | ], 7 | "plugins": [ 8 | ["@babel/proposal-class-properties", { "loose": true }], 9 | ["transform-react-remove-prop-types", { "mode": "unsafe-wrap" }] 10 | ], 11 | "env": { 12 | "test": { 13 | "plugins": [ 14 | "@babel/transform-modules-commonjs" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/integration/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration", 3 | "version": "0.4.1", 4 | "homepage": "https://github.com/chenglou/react-motion", 5 | "authors": [ 6 | "Cheng Lou " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "react-motion": "https://unpkg.com/react-motion/bower.zip" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/react-motion.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export { default as Motion } from './Motion'; 3 | export { default as StaggeredMotion } from './StaggeredMotion'; 4 | export { default as TransitionMotion } from './TransitionMotion'; 5 | export { default as spring } from './spring'; 6 | export { default as presets } from './presets'; 7 | export { default as stripStyle } from './stripStyle'; 8 | 9 | // deprecated, dummy warning function 10 | export { default as reorderKeys } from './reorderKeys'; 11 | -------------------------------------------------------------------------------- /src/stripStyle.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // turn {x: {val: 1, stiffness: 1, damping: 2}, y: 2} generated by 3 | // `{x: spring(1, {stiffness: 1, damping: 2}), y: 2}` into {x: 1, y: 2} 4 | 5 | import type { Style, PlainStyle } from './Types'; 6 | 7 | export default function stripStyle(style: Style): PlainStyle { 8 | let ret = {}; 9 | for (const key in style) { 10 | if (!Object.prototype.hasOwnProperty.call(style, key)) { 11 | continue; 12 | } 13 | ret[key] = typeof style[key] === 'number' ? style[key] : style[key].val; 14 | } 15 | return ret; 16 | } 17 | -------------------------------------------------------------------------------- /test/integration/bower.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "build/react-motion.js": { 3 | "bundled": 77059, 4 | "minified": 22596, 5 | "gzipped": 6616 6 | }, 7 | "build/react-motion.min.js": { 8 | "bundled": 52481, 9 | "minified": 15210, 10 | "gzipped": 4446 11 | }, 12 | "lib/react-motion.esm.js": { 13 | "bundled": 43290, 14 | "minified": 14594, 15 | "gzipped": 3583, 16 | "treeshaked": { 17 | "rollup": { 18 | "code": 6381, 19 | "import_statements": 196 20 | }, 21 | "webpack": { 22 | "code": 7588 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | var webpack = require('webpack'); 6 | var WebpackDevServer = require('webpack-dev-server'); 7 | var config = require('./webpack.config'); 8 | var port = process.env.PORT || 3000; 9 | 10 | new WebpackDevServer(webpack(config), { 11 | publicPath: config.output.publicPath, 12 | hot: true, 13 | stats: { 14 | chunkModules: false, 15 | colors: true, 16 | } 17 | }).listen(port, '0.0.0.0', function (err) { 18 | if (err) { 19 | console.log(err); 20 | } 21 | 22 | console.log('Listening at 0.0.0.0:' + port); 23 | }); 24 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Adrian le Bas 2 | Amadeus Junqueira 3 | Benjamin San Souci 4 | Bishop Zareh 5 | Brenton Simpson 6 | Cesar Andreu 7 | Cheng Lou 8 | Dan Abramov 9 | Daniel Dunderfelt 10 | Dustan Kasten 11 | Frederick Fogerty 12 | Gaëtan Renaudeau 13 | Google, Inc. 14 | Henry Zhu 15 | Ivan Starkov 16 | Jeroen van Aert 17 | Jesper Petersson 18 | Jevgeni Geimanen 19 | Joe Lencioni 20 | John Amiah Ford 21 | Jon Lebensold 22 | Justin Morris 23 | Kyle Mathews 24 | Ludovico Fischer 25 | Michael J Hoffman 26 | Mirko Mariani 27 | Neil Kistner 28 | Nik Butenko 29 | Nikhil Baradwaj 30 | Olivier Tassinari 31 | Paolo Moretti 32 | Raymond Zhou 33 | Robert Haritonov 34 | Sorin Iclanzan 35 | Stefan Dombrowski 36 | Stephen J. Collings 37 | Sundeep Malladi 38 | Sunil Pai 39 | Travis Arnold 40 | Wilfred Denton 41 | -------------------------------------------------------------------------------- /demos/demo0-simple-transition/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Toggle 7 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | How Many Demos Do You Need 7 | 8 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/fbjs/lib/PromiseMap.js 3 | .*/node_modules/fbjs/lib/fetchWithRetries.js 4 | .*/node_modules/fbjs/lib/Deferred.js.flow 5 | .*/node_modules/fbjs/lib/equalsSet.js.flow 6 | .*/node_modules/fbjs/lib/shallowEqual.js.flow 7 | .*/node_modules/fbjs/lib/someSet.js.flow 8 | .*/node_modules/fbjs/lib/everySet.js.flow 9 | .*/node_modules/fbjs/lib/UnicodeBidi.js.flow 10 | .*/node_modules/fbjs/lib/UnicodeBidiService.js.flow 11 | .*/node_modules/kefir/kefir.js.flow 12 | .*/node_modules/kefir/dist/kefir.js.flow 13 | .*/test/.* 14 | .*/node_modules/@webassemblyjs/.* 15 | .*/node_modules/eslint-plugin-jsx-a11y/.* 16 | .*/node_modules/rollup-plugin-size-snapshot/.* 17 | .*/node_modules/babel-plugin-transform-react-remove-prop-types 18 | 19 | [include] 20 | 21 | [libs] 22 | 23 | [options] 24 | -------------------------------------------------------------------------------- /demos/demo7-water-ripples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ripples 7 | 8 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-motion", 3 | "version": "0.5.1", 4 | "homepage": "https://github.com/chenglou/react-motion", 5 | "authors": [ 6 | "chenglou" 7 | ], 8 | "description": "A spring that solves your animation problems.", 9 | "main": [ 10 | "build/react-motion.js", 11 | "build/react-motion.map" 12 | ], 13 | "dependencies": { 14 | "react": "^0.14.9 || ^15.3.0" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "component", 19 | "react-component", 20 | "transitiongroup", 21 | "spring", 22 | "tween", 23 | "motion", 24 | "animation", 25 | "transition", 26 | "ui" 27 | ], 28 | "license": "MIT", 29 | "ignore": [ 30 | "**/.*", 31 | "node_modules", 32 | "test", 33 | "demo*", 34 | "server.js", 35 | "src", 36 | "webpack.config.js", 37 | "webpack.prod.config.js", 38 | "karma.conf.js", 39 | "package.json" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/createMockRaf.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | type Callback = (now: number) => void; 4 | 5 | export default function (): Object { 6 | let allCallbacks = []; 7 | let prevTime = 0; 8 | let id = 0; 9 | 10 | const now = () => prevTime; 11 | 12 | const raf = (cb: Callback) => { 13 | id++; 14 | allCallbacks.push({id, cb}); 15 | return id; 16 | }; 17 | 18 | raf.cancel = id2 => { 19 | allCallbacks = allCallbacks.filter(item => item.id !== id2); 20 | }; 21 | 22 | const defaultTimeInterval = 1000 / 60; 23 | const singleStep = ms => { 24 | const allCallbacksBefore = allCallbacks; 25 | allCallbacks = []; 26 | 27 | prevTime += ms; 28 | allCallbacksBefore.forEach(({cb}) => cb(prevTime)); 29 | }; 30 | 31 | const step = (howMany = 1, ms = defaultTimeInterval) => { 32 | for (let i = 0; i < howMany; i++) { 33 | singleStep(ms); 34 | } 35 | }; 36 | 37 | return {now, raf, step}; 38 | } 39 | -------------------------------------------------------------------------------- /src/shouldStopAnimation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { PlainStyle, Style, Velocity } from './Types'; 3 | 4 | // usage assumption: currentStyle values have already been rendered but it says 5 | // nothing of whether currentStyle is stale (see unreadPropStyle) 6 | export default function shouldStopAnimation( 7 | currentStyle: PlainStyle, 8 | style: Style, 9 | currentVelocity: Velocity, 10 | ): boolean { 11 | for (let key in style) { 12 | if (!Object.prototype.hasOwnProperty.call(style, key)) { 13 | continue; 14 | } 15 | 16 | if (currentVelocity[key] !== 0) { 17 | return false; 18 | } 19 | 20 | const styleValue = 21 | typeof style[key] === 'number' ? style[key] : style[key].val; 22 | // stepper will have already taken care of rounding precision errors, so 23 | // won't have such thing as 0.9999 !=== 1 24 | if (currentStyle[key] !== styleValue) { 25 | return false; 26 | } 27 | } 28 | 29 | return true; 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 React Motion authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /demos/demo2-draggable-balls/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Grid of Balls 7 | 8 | 43 | 44 | 45 | 46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demos/demo1-chat-heads/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Heads 7 | 8 | 54 | 55 | 56 | 57 |
58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /demos/demo0-simple-transition/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Motion, spring} from '../../src/react-motion'; 3 | 4 | export default class Demo extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {open: false}; 8 | }; 9 | 10 | handleMouseDown = () => { 11 | this.setState({open: !this.state.open}); 12 | }; 13 | 14 | handleTouchStart = (e) => { 15 | e.preventDefault(); 16 | this.handleMouseDown(); 17 | }; 18 | 19 | render() { 20 | return ( 21 |
22 | 27 | 28 | 29 | {({x}) => 30 | // children is a callback which should accept the current value of 31 | // `style` 32 |
33 |
37 |
38 | } 39 | 40 |
41 | ); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /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 | let reusedTuple: [number, number] = [0, 0]; 8 | export default function stepper( 9 | secondPerFrame: number, 10 | x: number, 11 | v: number, 12 | destX: number, 13 | k: number, 14 | b: number, 15 | precision: number, 16 | ): [number, number] { 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 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | mode: "development", 5 | devtool: 6 | process.env.NODE_ENV === 'development' ? 'eval-source-map' : 'source-map', 7 | entry: { 8 | 'demo0-simple-transition': './demos/demo0-simple-transition/index.jsx', 9 | 'demo1-chat-heads': './demos/demo1-chat-heads/index.jsx', 10 | 'demo2-draggable-balls': './demos/demo2-draggable-balls/index.jsx', 11 | 'demo3-todomvc-list-transition': './demos/demo3-todomvc-list-transition/index.jsx', 12 | 'demo4-photo-gallery': './demos/demo4-photo-gallery/index.jsx', 13 | 'demo5-spring-parameters-chooser': './demos/demo5-spring-parameters-chooser/index.jsx', 14 | 'demo7-water-ripples': './demos/demo7-water-ripples/index.jsx', 15 | 'demo8-draggable-list': './demos/demo8-draggable-list/index.jsx', 16 | }, 17 | output: { 18 | filename: '[name]/all.js', 19 | publicPath: '/demos/', 20 | path: __dirname + '/demos/', 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/, 26 | exclude: /build|lib|bower_components|node_modules/, 27 | loader: 'babel-loader' 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: ['style-loader', 'css-loader'] 32 | }, 33 | { 34 | test: /\.jsx?$/, 35 | loader: 'eslint-loader', 36 | exclude: /build|lib|bower_components|node_modules/ 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | extensions: ['.js', '.jsx'] 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /demos/demo1-chat-heads/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StaggeredMotion, spring, presets} from '../../src/react-motion'; 3 | import range from 'lodash.range'; 4 | 5 | export default class Demo extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {x: 250, y: 300}; 9 | }; 10 | 11 | componentDidMount() { 12 | window.addEventListener('mousemove', this.handleMouseMove); 13 | window.addEventListener('touchmove', this.handleTouchMove); 14 | }; 15 | 16 | handleMouseMove = ({pageX: x, pageY: y}) => { 17 | this.setState({x, y}); 18 | }; 19 | 20 | handleTouchMove = ({touches}) => { 21 | this.handleMouseMove(touches[0]); 22 | }; 23 | 24 | getStyles = (prevStyles) => { 25 | // `prevStyles` is the interpolated value of the last tick 26 | const endValue = prevStyles.map((_, i) => { 27 | return i === 0 28 | ? this.state 29 | : { 30 | x: spring(prevStyles[i - 1].x, presets.gentle), 31 | y: spring(prevStyles[i - 1].y, presets.gentle), 32 | }; 33 | }); 34 | return endValue; 35 | }; 36 | 37 | render() { 38 | return ( 39 | ({x: 0, y: 0}))} 41 | styles={this.getStyles}> 42 | {balls => 43 |
44 | {balls.map(({x, y}, i) => 45 |
53 | )} 54 |
55 | } 56 | 57 | ); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /demos/demo8-draggable-list/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Framer cards 7 | 8 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /demos/demo7-water-ripples/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TransitionMotion, spring} from '../../src/react-motion'; 3 | 4 | const leavingSpringConfig = {stiffness: 60, damping: 15}; 5 | 6 | export default class Demo extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {mouse: [], now: 't' + 0}; 10 | }; 11 | 12 | handleMouseMove = ({pageX, pageY}) => { 13 | // Make sure the state is queued and not batched. 14 | this.setState(() => { 15 | return { 16 | mouse: [pageX - 25, pageY - 25], 17 | now: 't' + Date.now(), 18 | }; 19 | }); 20 | }; 21 | 22 | handleTouchMove = (e) => { 23 | e.preventDefault(); 24 | this.handleMouseMove(e.touches[0]); 25 | }; 26 | 27 | willLeave = (styleCell) => { 28 | return { 29 | ...styleCell.style, 30 | opacity: spring(0, leavingSpringConfig), 31 | scale: spring(2, leavingSpringConfig), 32 | }; 33 | }; 34 | 35 | render() { 36 | const {mouse: [mouseX, mouseY], now} = this.state; 37 | const styles = mouseX == null ? [] : [{ 38 | key: now, 39 | style: { 40 | opacity: spring(1), 41 | scale: spring(0), 42 | x: spring(mouseX), 43 | y: spring(mouseY), 44 | } 45 | }]; 46 | return ( 47 | 48 | {circles => 49 |
53 | {circles.map(({key, style: {opacity, scale, x, y}}) => 54 |
63 | )} 64 |
65 | } 66 | 67 | ); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var withCoverage = process.argv.indexOf('coverage') !== -1 || process.env.COVERAGE; 6 | 7 | var webpackConfig = { 8 | mode: 'development', 9 | module: { 10 | rules: withCoverage ? 11 | [ 12 | { 13 | test: /\.js$/, 14 | loader: 'babel-loader', 15 | include: [path.resolve('./test')] 16 | }, 17 | { 18 | test: /\.js$/, 19 | loader: 'isparta-loader', 20 | include: [path.resolve('./src')] 21 | }, 22 | ] : 23 | [ 24 | { 25 | test: /\.js$/, 26 | loader: 'babel-loader', 27 | include: [path.resolve('./src'), path.resolve('./test')], 28 | }, 29 | ], 30 | }, 31 | stats: { 32 | colors: true, 33 | } 34 | }; 35 | 36 | module.exports = function (config) { 37 | config.set({ 38 | basePath: '', 39 | frameworks: ['jasmine'], 40 | files: [ 41 | './node_modules/@babel/polyfill/browser.js', 42 | 'test/index.js', 43 | ], 44 | webpack: webpackConfig, 45 | webpackMiddleware: { 46 | stats: { 47 | chunkModules: false, 48 | colors: true, 49 | }, 50 | }, 51 | exclude: [], 52 | preprocessors: { 53 | 'test/index.js': ['webpack'], 54 | }, 55 | reporters: ['jasmine-diff', 'progress'], 56 | jasmineDiffReporter: { 57 | pretty: true, 58 | color: { 59 | expectedBg: '', 60 | expectedFg: 'red', 61 | actualBg: '', 62 | actualFg: 'green', 63 | defaultBg: '', 64 | defaultFg: 'grey' 65 | } 66 | }, 67 | coverageReporter: { 68 | dir: './coverage/', 69 | subdir: '.', 70 | reporters: [ 71 | {type: 'html'}, 72 | {type: 'lcovonly'}, 73 | {type: 'text', file: 'text.txt'}, 74 | {type: 'text-summary', file: 'text-summary.txt'}, 75 | ], 76 | }, 77 | captureTimeout: 90000, 78 | browserNoActivityTimeout: 60000, 79 | port: 9876, 80 | colors: true, 81 | logLevel: config.LOG_INFO, 82 | autoWatch: false, 83 | browsers: ['ChromeHeadless'], 84 | singleRun: true, 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import babel from 'rollup-plugin-babel'; 4 | import replace from 'rollup-plugin-replace'; 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 6 | import { uglify } from 'rollup-plugin-uglify'; 7 | import pkg from './package.json'; 8 | 9 | const input = './src/react-motion.js'; 10 | const name = 'ReactMotion'; 11 | const globals = { 12 | react: 'React', 13 | }; 14 | 15 | // treat as external "module/path" modules and reserved rollup paths 16 | const external = id => 17 | !id.startsWith('\0') && !id.startsWith('.') && !id.startsWith('/'); 18 | 19 | const getBabelOptions = () => ({ 20 | babelrc: false, 21 | exclude: '**/node_modules/**', 22 | runtimeHelpers: true, 23 | plugins: [ 24 | ['@babel/proposal-class-properties', { loose: true }], 25 | ['transform-react-remove-prop-types', { mode: 'unsafe-wrap' }], 26 | ['@babel/transform-runtime', { useESModules: true }], 27 | ], 28 | presets: [ 29 | ['@babel/env', { modules: false, loose: true }], 30 | '@babel/flow', 31 | '@babel/react', 32 | ], 33 | }); 34 | 35 | const commonjsOptions = { 36 | include: '**/node_modules/**', 37 | }; 38 | 39 | export default [ 40 | { 41 | input, 42 | output: { file: 'build/react-motion.js', format: 'umd', name, globals }, 43 | external: Object.keys(globals), 44 | plugins: [ 45 | nodeResolve(), 46 | babel(getBabelOptions()), 47 | commonjs(commonjsOptions), 48 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 49 | sizeSnapshot(), 50 | ], 51 | }, 52 | 53 | { 54 | input, 55 | output: { file: 'build/react-motion.min.js', format: 'umd', name, globals }, 56 | external: Object.keys(globals), 57 | plugins: [ 58 | nodeResolve(), 59 | babel(getBabelOptions()), 60 | commonjs(commonjsOptions), 61 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 62 | sizeSnapshot(), 63 | uglify(), 64 | ], 65 | }, 66 | 67 | { 68 | input, 69 | output: { file: pkg.module, format: 'esm' }, 70 | external, 71 | plugins: [babel(getBabelOptions()), sizeSnapshot()], 72 | }, 73 | ]; 74 | -------------------------------------------------------------------------------- /demos/demo4-photo-gallery/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Motion, spring} from '../../src/react-motion'; 3 | 4 | const springSettings = {stiffness: 170, damping: 26}; 5 | const NEXT = 'show-next'; 6 | 7 | export default class Demo extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | photos: [[500, 350], [800, 600], [800, 400], [700, 500], [200, 650], [600, 600]], 12 | currPhoto: 0, 13 | }; 14 | }; 15 | 16 | handleChange = ({target: {value}}) => { 17 | this.setState({currPhoto: value}); 18 | }; 19 | 20 | clickHandler = (btn) => { 21 | let photoIndex = btn === NEXT ? this.state.currPhoto+1 : this.state.currPhoto-1; 22 | 23 | photoIndex = photoIndex >= 0 ? photoIndex : this.state.photos.length - 1; 24 | photoIndex = photoIndex >= this.state.photos.length ? 0 : photoIndex; 25 | 26 | this.setState({ 27 | currPhoto: photoIndex 28 | }) 29 | }; 30 | 31 | render() { 32 | const {photos, currPhoto} = this.state; 33 | const [currWidth, currHeight] = photos[currPhoto]; 34 | 35 | const widths = photos.map(([origW, origH]) => currHeight / origH * origW); 36 | 37 | const leftStartCoords = widths 38 | .slice(0, currPhoto) 39 | .reduce((sum, width) => sum - width, 0); 40 | 41 | let configs = []; 42 | photos.reduce((prevLeft, [origW, origH], i) => { 43 | configs.push({ 44 | left: spring(prevLeft, springSettings), 45 | height: spring(currHeight, springSettings), 46 | width: spring(widths[i], springSettings), 47 | }); 48 | return prevLeft + widths[i]; 49 | }, leftStartCoords); 50 | 51 | return ( 52 |
53 |
Scroll Me
54 | 55 | 61 | 62 |
63 | 64 | {container => 65 |
66 | {configs.map((style, i) => 67 | 68 | {style => 69 | 70 | } 71 | 72 | )} 73 |
74 | } 75 |
76 |
77 |
78 | ); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb", "prettier"], 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "jasmine": true 8 | }, 9 | "rules": { 10 | "eqeqeq": [2, "allow-null"], 11 | "id-length": 0, 12 | "no-console": 0, // use it for warnings 13 | "no-nested-ternary": 0, 14 | "prefer-const": 0, 15 | "no-multiple-empty-lines": 0, 16 | "no-useless-escape": 0, 17 | "no-else-return": 0, 18 | "comma-dangle": 0, 19 | "lines-between-class-members": 0, 20 | "indent": 0, 21 | "operator-linebreak": 0, 22 | "object-curly-newline": 0, 23 | "function-paren-newline": 0, 24 | "prefer-destructuring": 0, 25 | "camelcase": ["error", {"allow": ["^UNSAFE_"]}], 26 | 27 | "react/jsx-wrap-multilines": 0, 28 | "react/jsx-one-expression-per-line": 0, 29 | "react/no-unused-state": 0, 30 | "react/require-default-props": 0, 31 | "react/destructuring-assignment": 0, 32 | "react/no-did-mount-set-state": 2, 33 | "react/no-multi-comp": 0, 34 | "react/jsx-boolean-value": [2, "always"], 35 | "react/sort-comp": [ 36 | 2, { 37 | "order": [ 38 | "displayName", 39 | "propTypes", 40 | "contextTypes", 41 | "childContextTypes", 42 | "mixins", 43 | "statics", 44 | "defaultProps", 45 | "getDefaultProps", 46 | "getInitialState", 47 | "getChildContext", 48 | "componentWillMount", 49 | "UNSAFE_componentWillMount", 50 | "componentDidMount", 51 | "componentWillReceiveProps", 52 | "UNSAFE_componentWillReceiveProps", 53 | "shouldComponentUpdate", 54 | "componentWillUpdate", 55 | "UNSAFE_componentWillUpdate", 56 | "componentDidUpdate", 57 | "componentWillUnmount", 58 | "/^on.+$/", 59 | "/^get.+$/", 60 | "/^render.+$/", 61 | "/^.+$/", // All other methods go here 62 | "render" 63 | ] 64 | } 65 | ], 66 | 67 | "max-len": 0, 68 | "no-mixed-operators": 0, 69 | "no-continue": 0, 70 | "no-restricted-syntax": 0, 71 | "no-plusplus": 0, 72 | "no-confusing-arrow": 0, 73 | "arrow-parens": 0, 74 | "arrow-body-style": 0, 75 | "react/jsx-indent": 0, 76 | "react/jsx-indent-props": 0, 77 | "react/jsx-closing-bracket-location": 0, 78 | "react/prefer-es6-class": 0, 79 | "react/jsx-filename-extension": 0, 80 | "react/prefer-stateless-function": 0, 81 | "object-curly-spacing": 0, 82 | "import/imports-first": 0, 83 | "import/no-unresolved": 0, 84 | "import/no-extraneous-dependencies": 0, 85 | "import/order": 0, 86 | "import/no-webpack-loader-syntax": 0, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Babel 5.x doesn't support type parameters, so we make this alias here out of 4 | // Babel's sight. 5 | /* eslint-disable spaced-comment, no-undef */ 6 | /*:: 7 | import type {Element} from 'react'; 8 | export type ReactElement = Element<*>; 9 | */ 10 | 11 | // === basic reused types === 12 | // type of the second parameter of `spring(val, config)` all fields are optional 13 | export type SpringHelperConfig = { 14 | stiffness?: number, 15 | damping?: number, 16 | precision?: number, 17 | }; 18 | // the object returned by `spring(value, yourConfig)`. For internal usage only! 19 | export type OpaqueConfig = { 20 | val: number, 21 | stiffness: number, 22 | damping: number, 23 | precision: number, 24 | }; 25 | // your typical style object given in props. Maps to a number or a spring config 26 | export type Style = { [key: string]: number | OpaqueConfig }; 27 | // the interpolating style object, with the same keys as the above Style object, 28 | // with the values mapped to numbers, naturally 29 | export type PlainStyle = { [key: string]: number }; 30 | // internal velocity object. Similar to PlainStyle, but whose numbers represent 31 | // speed. Might be exposed one day. 32 | export type Velocity = { [key: string]: number }; 33 | 34 | // === Motion === 35 | export type MotionProps = { 36 | defaultStyle?: PlainStyle, 37 | style: Style, 38 | children: (interpolatedStyle: PlainStyle) => ReactElement, 39 | onRest?: () => void, 40 | }; 41 | 42 | // === StaggeredMotion === 43 | export type StaggeredProps = { 44 | defaultStyles?: Array, 45 | styles: (previousInterpolatedStyles: ?Array) => Array 115 | 116 | 117 | 118 |
119 |
Default: {stiffness: 170, damping: 26}
120 |

Drag a circle to see the differences in animation behavior

121 |
122 |
123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /test/mergeDiff-test.js: -------------------------------------------------------------------------------- 1 | import mergeDiff from '../src/mergeDiff'; 2 | 3 | const id = (_, s) => s; 4 | const n = () => null; 5 | 6 | // helper to make the tests more concise 7 | function test(prevRaw, nextRaw, expectedRaw, customOnRemove) { 8 | // we elaborately construct prev/nextKeyStyleValMap + randomized style value to 9 | // check that the style object of the latter correctly merged into the final 10 | // output 11 | let prev = []; 12 | let prevKeyStyleValMap = {}; 13 | prevRaw.forEach(num => { 14 | const styleVal = Math.random(); 15 | // key needs to be a string; cast it 16 | prev.push({key: String(num), style: {a: styleVal}}); 17 | prevKeyStyleValMap[num] = styleVal; 18 | }); 19 | let next = []; 20 | let nextKeyStyleValMap = {}; 21 | nextRaw.forEach(num => { 22 | const styleVal = Math.random(); 23 | next.push({key: String(num), style: {a: styleVal}}); 24 | nextKeyStyleValMap[num] = styleVal; 25 | }); 26 | 27 | const expected = expectedRaw.map(num => { 28 | return { 29 | key: String(num), 30 | style: {a: Object.prototype.hasOwnProperty.call(nextKeyStyleValMap, num) ? nextKeyStyleValMap[num] : prevKeyStyleValMap[num]}, 31 | }; 32 | }); 33 | 34 | expect(mergeDiff(prev, next, n)).toEqual(next); 35 | // some tests pass in a `customOnRemove` to check edge cases; interpret 36 | // `expected`/`expectedRaw` as the output of mergeDiff using `customOnRemove` 37 | // instead of the default `id` function 38 | expect(mergeDiff(prev, next, customOnRemove || id)).toEqual(expected); 39 | } 40 | 41 | describe('mergeDiff', () => { 42 | it('should work with various merge orders', () => { 43 | // most of these tests are significant. Don't casually remove some. Those 44 | // marked as "meh" are the ones whose order can differ. We've chosen a 45 | // deterministic default in our mergeDiff implementation 46 | test([4], [], [4]); 47 | test([], [3], [3]); 48 | test([3], [3], [3]); 49 | test([], [], []); 50 | test([2, 4, 5, 6], [2, 3, 4, 5], [2, 3, 4, 5, 6]); 51 | test([2, 4, 5, 6, 7], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6, 7]); 52 | test([1, 2, 3], [2, 3, 4], [1, 2, 3, 4]); 53 | test([2, 3, 4], [1, 2, 3], [1, 2, 3, 4]); 54 | test([4], [1, 2, 3], [4, 1, 2, 3]); // meh 55 | test([1, 2, 3], [4], [1, 2, 3, 4]); // meh 56 | test([4, 2], [1, 2, 3], [4, 1, 2, 3]); // meh 57 | test([2, 4], [1, 2, 3], [1, 2, 4, 3]); // meh 58 | test([1, 5, 10], [3, 5, 7, 10], [1, 3, 5, 7, 10]); // meh 59 | test([4, 5, 10], [3, 5, 7, 10], [4, 3, 5, 7, 10]); // meh 60 | test([4], [3], [4, 3]); // meh 61 | test([1, 5], [5, 3], [1, 5, 3]); 62 | test([5, 6], [3, 5], [3, 5, 6]); 63 | test([1, 2, 3], [3, 2, 1], [3, 2, 1]); 64 | test([3, 2, 1], [1, 2, 3], [1, 2, 3]); 65 | test([1, 2, 3], [2, 1, 3], [2, 1, 3]); 66 | test([1, 2, 3], [1, 3, 2], [1, 3, 2]); 67 | test([1, 2, 3], [1, 2, 3], [1, 2, 3]); 68 | }); 69 | 70 | it('should work with some more typical onRemove callbacks', () => { 71 | test([1, 2, 3], [1, 9], [1, 2, 9], (index, s) => index === 1 ? s : null); 72 | test([1, 2, 3, 4], [5, 4, 2], [1, 5, 4, 2], (index, s) => index === 0 ? s : null); 73 | }); 74 | 75 | it('should not call cb more than once per disappearing key', () => { 76 | let count = 0; 77 | test([1], [], [], () => { 78 | count++; 79 | return null; 80 | }); 81 | expect(count).toBe(1); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /demos/demo8-draggable-list/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Motion, spring} from '../../src/react-motion'; 3 | import range from 'lodash.range'; 4 | 5 | function reinsert(arr, from, to) { 6 | const _arr = arr.slice(0); 7 | const val = _arr[from]; 8 | _arr.splice(from, 1); 9 | _arr.splice(to, 0, val); 10 | return _arr; 11 | } 12 | 13 | function clamp(n, min, max) { 14 | return Math.max(Math.min(n, max), min); 15 | } 16 | 17 | const springConfig = {stiffness: 300, damping: 50}; 18 | const itemsCount = 4; 19 | 20 | export default class Demo extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | topDeltaY: 0, 25 | mouseY: 0, 26 | isPressed: false, 27 | originalPosOfLastPressed: 0, 28 | order: range(itemsCount), 29 | }; 30 | }; 31 | 32 | componentDidMount() { 33 | window.addEventListener('touchmove', this.handleTouchMove); 34 | window.addEventListener('touchend', this.handleMouseUp); 35 | window.addEventListener('mousemove', this.handleMouseMove); 36 | window.addEventListener('mouseup', this.handleMouseUp); 37 | }; 38 | 39 | handleTouchStart = (key, pressLocation, e) => { 40 | this.handleMouseDown(key, pressLocation, e.touches[0]); 41 | }; 42 | 43 | handleTouchMove = (e) => { 44 | e.preventDefault(); 45 | this.handleMouseMove(e.touches[0]); 46 | }; 47 | 48 | handleMouseDown = (pos, pressY, {pageY}) => { 49 | this.setState({ 50 | topDeltaY: pageY - pressY, 51 | mouseY: pressY, 52 | isPressed: true, 53 | originalPosOfLastPressed: pos, 54 | }); 55 | }; 56 | 57 | handleMouseMove = ({pageY}) => { 58 | const {isPressed, topDeltaY, order, originalPosOfLastPressed} = this.state; 59 | 60 | if (isPressed) { 61 | const mouseY = pageY - topDeltaY; 62 | const currentRow = clamp(Math.round(mouseY / 100), 0, itemsCount - 1); 63 | let newOrder = order; 64 | 65 | if (currentRow !== order.indexOf(originalPosOfLastPressed)){ 66 | newOrder = reinsert(order, order.indexOf(originalPosOfLastPressed), currentRow); 67 | } 68 | 69 | this.setState({mouseY: mouseY, order: newOrder}); 70 | } 71 | }; 72 | 73 | handleMouseUp = () => { 74 | this.setState({isPressed: false, topDeltaY: 0}); 75 | }; 76 | 77 | render() { 78 | const {mouseY, isPressed, originalPosOfLastPressed, order} = this.state; 79 | 80 | return ( 81 |
82 | {range(itemsCount).map(i => { 83 | const style = originalPosOfLastPressed === i && isPressed 84 | ? { 85 | scale: spring(1.1, springConfig), 86 | shadow: spring(16, springConfig), 87 | y: mouseY, 88 | } 89 | : { 90 | scale: spring(1, springConfig), 91 | shadow: spring(1, springConfig), 92 | y: spring(order.indexOf(i) * 100, springConfig), 93 | }; 94 | return ( 95 | 96 | {({scale, shadow, y}) => 97 |
107 | {order.indexOf(i) + 1} 108 |
109 | } 110 |
111 | ); 112 | })} 113 |
114 | ); 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-motion", 3 | "version": "0.5.2", 4 | "description": "A spring that solves your animation problems.", 5 | "main": "lib/react-motion.js", 6 | "module": "lib/react-motion.esm.js", 7 | "peerDependencies": { 8 | "react": "^0.14.9 || ^15.3.0 || ^16.0.0" 9 | }, 10 | "devDependencies": { 11 | "@babel/cli": "^7.0.0-rc.1", 12 | "@babel/core": "^7.7.2", 13 | "@babel/plugin-proposal-class-properties": "^7.7.0", 14 | "@babel/plugin-transform-modules-commonjs": "^7.7.0", 15 | "@babel/plugin-transform-runtime": "^7.6.2", 16 | "@babel/polyfill": "^7.7.0", 17 | "@babel/preset-env": "^7.7.1", 18 | "@babel/preset-flow": "^7.0.0", 19 | "@babel/preset-react": "^7.7.0", 20 | "babel-eslint": "^10.0.3", 21 | "babel-loader": "^8.0.6", 22 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 23 | "codemirror": "^5.5.0", 24 | "cross-env": "^5.2.0", 25 | "css-loader": "^0.28.11", 26 | "eslint": "^5.16.0", 27 | "eslint-config-airbnb": "^17.0.0", 28 | "eslint-config-prettier": "^2.9.0", 29 | "eslint-loader": "^2.0.0", 30 | "eslint-plugin-flowtype": "^2.49.3", 31 | "eslint-plugin-import": "^2.13.0", 32 | "eslint-plugin-jsx-a11y": "^6.1.0", 33 | "eslint-plugin-react": "^7.10.0", 34 | "flow-bin": "^0.53.1", 35 | "flow-copy-source": "^1.1.0", 36 | "husky": "^0.14.3", 37 | "inject-loader": "^4.0.1", 38 | "isparta-loader": "^2.0.0", 39 | "jasmine-core": "^3.1.0", 40 | "karma": "^2.0.4", 41 | "karma-chrome-launcher": "^2.2.0", 42 | "karma-coverage": "^1.1.2", 43 | "karma-jasmine": "^1.1.2", 44 | "karma-jasmine-diff-reporter": "^1.2.0", 45 | "karma-webpack": "^3.0.0", 46 | "lint-staged": "^7.2.0", 47 | "lodash.range": "^3.0.1", 48 | "prettier": "^1.14.2", 49 | "react": ">=15.5.0", 50 | "react-codemirror": ">=0.1.2", 51 | "react-dom": ">=15.5.0", 52 | "rollup": "^0.62.0", 53 | "rollup-plugin-babel": "^4.3.3", 54 | "rollup-plugin-commonjs": "^9.1.4", 55 | "rollup-plugin-node-resolve": "^3.3.0", 56 | "rollup-plugin-replace": "^2.0.0", 57 | "rollup-plugin-size-snapshot": "^0.6.0", 58 | "rollup-plugin-uglify": "^4.0.0", 59 | "style-loader": "^0.21.0", 60 | "webpack": "^4.14.0", 61 | "webpack-command": "^0.4.0", 62 | "webpack-dev-server": "^3.1.4" 63 | }, 64 | "scripts": { 65 | "start": "node server.js", 66 | "prebuild:dist": "rm -rf build", 67 | "build:dist": "rollup -c", 68 | "prebuild:lib": "rm -rf lib", 69 | "build:lib": "babel src --out-dir lib", 70 | "build:flow": "flow-copy-source -v src lib", 71 | "build": "npm run build:dist && npm run build:lib && npm run build:flow", 72 | "build-demos": "webpack", 73 | "lint": "eslint --ext .js,.jsx .", 74 | "flow_check": "flow check", 75 | "prepublishOnly": "npm run build", 76 | "test": "cross-env NODE_ENV=test karma start ./karma.conf.js --single-run", 77 | "test:travis": "cross-env NODE_ENV=test karma start ./karma.conf.js --single-run", 78 | "test:dev": "cross-env NODE_ENV=test karma start ./karma.conf.js --no-single-run --auto-watch", 79 | "test:cov": "cross-env NODE_ENV=test karma start ./karma.conf.js --single-run --reporters coverage", 80 | "gh-pages": "git fetch origin && git checkout gh-pages && git reset --hard origin/gh-pages && git rebase origin/master --force-rebase && npm run build-demos && git add . && git commit --amend --no-edit && git push origin gh-pages --force && git checkout master", 81 | "precommit": "lint-staged" 82 | }, 83 | "lint-staged": { 84 | "*.js": [ 85 | "prettier --write", 86 | "git add" 87 | ] 88 | }, 89 | "prettier": { 90 | "singleQuote": true, 91 | "trailingComma": "all" 92 | }, 93 | "repository": { 94 | "type": "git", 95 | "url": "https://github.com/chenglou/react-motion.git" 96 | }, 97 | "keywords": [ 98 | "react", 99 | "component", 100 | "react-component", 101 | "transitiongroup", 102 | "spring", 103 | "tween", 104 | "motion", 105 | "animation", 106 | "transition", 107 | "ui" 108 | ], 109 | "author": [ 110 | "nkbt", 111 | "chenglou" 112 | ], 113 | "license": "MIT", 114 | "dependencies": { 115 | "@babel/runtime": "7.7.2", 116 | "performance-now": "^2.1.0", 117 | "prop-types": "^15.5.8", 118 | "raf": "^3.1.0" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/mergeDiff.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { TransitionStyle } from './Types'; 3 | 4 | // core keys merging algorithm. If previous render's keys are [a, b], and the 5 | // next render's [c, b, d], what's the final merged keys and ordering? 6 | 7 | // - c and a must both be before b 8 | // - b before d 9 | // - ordering between a and c ambiguous 10 | 11 | // this reduces to merging two partially ordered lists (e.g. lists where not 12 | // every item has a definite ordering, like comparing a and c above). For the 13 | // ambiguous ordering we deterministically choose to place the next render's 14 | // item after the previous'; so c after a 15 | 16 | // this is called a topological sorting. Except the existing algorithms don't 17 | // work well with js bc of the amount of allocation, and isn't optimized for our 18 | // current use-case bc the runtime is linear in terms of edges (see wiki for 19 | // meaning), which is huge when two lists have many common elements 20 | export default function mergeDiff( 21 | prev: Array, 22 | next: Array, 23 | onRemove: ( 24 | prevIndex: number, 25 | prevStyleCell: TransitionStyle, 26 | ) => ?TransitionStyle, 27 | ): Array { 28 | // bookkeeping for easier access of a key's index below. This is 2 allocations + 29 | // potentially triggering chrome hash map mode for objs (so it might be faster 30 | // to loop through and find a key's index each time), but I no longer care 31 | let prevKeyIndex: { [key: string]: number } = {}; 32 | for (let i = 0; i < prev.length; i++) { 33 | prevKeyIndex[prev[i].key] = i; 34 | } 35 | let nextKeyIndex: { [key: string]: number } = {}; 36 | for (let i = 0; i < next.length; i++) { 37 | nextKeyIndex[next[i].key] = i; 38 | } 39 | 40 | // first, an overly elaborate way of merging prev and next, eliminating 41 | // duplicates (in terms of keys). If there's dupe, keep the item in next). 42 | // This way of writing it saves allocations 43 | let ret = []; 44 | for (let i = 0; i < next.length; i++) { 45 | ret[i] = next[i]; 46 | } 47 | for (let i = 0; i < prev.length; i++) { 48 | if (!Object.prototype.hasOwnProperty.call(nextKeyIndex, prev[i].key)) { 49 | // this is called my TM's `mergeAndSync`, which calls willLeave. We don't 50 | // merge in keys that the user desires to kill 51 | const fill = onRemove(i, prev[i]); 52 | if (fill != null) { 53 | ret.push(fill); 54 | } 55 | } 56 | } 57 | 58 | // now all the items all present. Core sorting logic to have the right order 59 | return ret.sort((a, b) => { 60 | const nextOrderA = nextKeyIndex[a.key]; 61 | const nextOrderB = nextKeyIndex[b.key]; 62 | const prevOrderA = prevKeyIndex[a.key]; 63 | const prevOrderB = prevKeyIndex[b.key]; 64 | 65 | if (nextOrderA != null && nextOrderB != null) { 66 | // both keys in next 67 | return nextKeyIndex[a.key] - nextKeyIndex[b.key]; 68 | } else if (prevOrderA != null && prevOrderB != null) { 69 | // both keys in prev 70 | return prevKeyIndex[a.key] - prevKeyIndex[b.key]; 71 | } else if (nextOrderA != null) { 72 | // key a in next, key b in prev 73 | 74 | // how to determine the order between a and b? We find a "pivot" (term 75 | // abuse), a key present in both prev and next, that is sandwiched between 76 | // a and b. In the context of our above example, if we're comparing a and 77 | // d, b's (the only) pivot 78 | for (let i = 0; i < next.length; i++) { 79 | const pivot = next[i].key; 80 | if (!Object.prototype.hasOwnProperty.call(prevKeyIndex, pivot)) { 81 | continue; 82 | } 83 | 84 | if ( 85 | nextOrderA < nextKeyIndex[pivot] && 86 | prevOrderB > prevKeyIndex[pivot] 87 | ) { 88 | return -1; 89 | } else if ( 90 | nextOrderA > nextKeyIndex[pivot] && 91 | prevOrderB < prevKeyIndex[pivot] 92 | ) { 93 | return 1; 94 | } 95 | } 96 | // pluggable. default to: next bigger than prev 97 | return 1; 98 | } 99 | // prevOrderA, nextOrderB 100 | for (let i = 0; i < next.length; i++) { 101 | const pivot = next[i].key; 102 | if (!Object.prototype.hasOwnProperty.call(prevKeyIndex, pivot)) { 103 | continue; 104 | } 105 | if ( 106 | nextOrderB < nextKeyIndex[pivot] && 107 | prevOrderA > prevKeyIndex[pivot] 108 | ) { 109 | return 1; 110 | } else if ( 111 | nextOrderB > nextKeyIndex[pivot] && 112 | prevOrderA < prevKeyIndex[pivot] 113 | ) { 114 | return -1; 115 | } 116 | } 117 | // pluggable. default to: next bigger than prev 118 | return -1; 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /demos/demo2-draggable-balls/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Motion, spring} from '../../src/react-motion'; 3 | import range from 'lodash.range'; 4 | 5 | const springSetting1 = {stiffness: 180, damping: 10}; 6 | const springSetting2 = {stiffness: 120, damping: 17}; 7 | function reinsert(arr, from, to) { 8 | const _arr = arr.slice(0); 9 | const val = _arr[from]; 10 | _arr.splice(from, 1); 11 | _arr.splice(to, 0, val); 12 | return _arr; 13 | } 14 | 15 | function clamp(n, min, max) { 16 | return Math.max(Math.min(n, max), min); 17 | } 18 | 19 | const allColors = [ 20 | '#EF767A', '#456990', '#49BEAA', '#49DCB1', '#EEB868', '#EF767A', '#456990', 21 | '#49BEAA', '#49DCB1', '#EEB868', '#EF767A', 22 | ]; 23 | const [count, width, height] = [11, 70, 90]; 24 | // indexed by visual position 25 | const layout = range(count).map(n => { 26 | const row = Math.floor(n / 3); 27 | const col = n % 3; 28 | return [width * col, height * row]; 29 | }); 30 | 31 | export default class Demo extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | mouseXY: [0, 0], 36 | mouseCircleDelta: [0, 0], // difference between mouse and circle pos for x + y coords, for dragging 37 | lastPress: null, // key of the last pressed component 38 | isPressed: false, 39 | order: range(count), // index: visual position. value: component key/id 40 | }; 41 | }; 42 | 43 | componentDidMount() { 44 | window.addEventListener('touchmove', this.handleTouchMove); 45 | window.addEventListener('touchend', this.handleMouseUp); 46 | window.addEventListener('mousemove', this.handleMouseMove); 47 | window.addEventListener('mouseup', this.handleMouseUp); 48 | }; 49 | 50 | handleTouchStart = (key, pressLocation, e) => { 51 | this.handleMouseDown(key, pressLocation, e.touches[0]); 52 | }; 53 | 54 | handleTouchMove = (e) => { 55 | e.preventDefault(); 56 | this.handleMouseMove(e.touches[0]); 57 | }; 58 | 59 | handleMouseMove = ({pageX, pageY}) => { 60 | const {order, lastPress, isPressed, mouseCircleDelta: [dx, dy]} = this.state; 61 | if (isPressed) { 62 | const mouseXY = [pageX - dx, pageY - dy]; 63 | const col = clamp(Math.floor(mouseXY[0] / width), 0, 2); 64 | const row = clamp(Math.floor(mouseXY[1] / height), 0, Math.floor(count / 3)); 65 | const index = row * 3 + col; 66 | const newOrder = reinsert(order, order.indexOf(lastPress), index); 67 | this.setState({mouseXY, order: newOrder}); 68 | } 69 | }; 70 | 71 | handleMouseDown = (key, [pressX, pressY], {pageX, pageY}) => { 72 | this.setState({ 73 | lastPress: key, 74 | isPressed: true, 75 | mouseCircleDelta: [pageX - pressX, pageY - pressY], 76 | mouseXY: [pressX, pressY], 77 | }); 78 | }; 79 | 80 | handleMouseUp = () => { 81 | this.setState({isPressed: false, mouseCircleDelta: [0, 0]}); 82 | }; 83 | 84 | render() { 85 | const {order, lastPress, isPressed, mouseXY} = this.state; 86 | return ( 87 |
88 | {order.map((_, key) => { 89 | let style; 90 | let x; 91 | let y; 92 | const visualPosition = order.indexOf(key); 93 | if (key === lastPress && isPressed) { 94 | [x, y] = mouseXY; 95 | style = { 96 | translateX: x, 97 | translateY: y, 98 | scale: spring(1.2, springSetting1), 99 | boxShadow: spring((x - (3 * width - 50) / 2) / 15, springSetting1), 100 | }; 101 | } else { 102 | [x, y] = layout[visualPosition]; 103 | style = { 104 | translateX: spring(x, springSetting2), 105 | translateY: spring(y, springSetting2), 106 | scale: spring(1, springSetting1), 107 | boxShadow: spring((x - (3 * width - 50) / 2) / 15, springSetting1), 108 | }; 109 | } 110 | return ( 111 | 112 | {({translateX, translateY, scale, boxShadow}) => 113 |
125 | } 126 | 127 | ); 128 | })} 129 |
130 | ); 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /demos/demo5-spring-parameters-chooser/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Motion, spring} from '../../src/react-motion'; 3 | import range from 'lodash.range'; 4 | 5 | const gridWidth = 150; 6 | const gridHeight = 150; 7 | const grid = range(4).map(() => range(6)); 8 | 9 | export default class Demo extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | delta: [0, 0], 14 | mouse: [0, 0], 15 | isPressed: false, 16 | firstConfig: [60, 5], 17 | slider: {dragged: null, num: 0}, 18 | lastPressed: [0, 0], 19 | }; 20 | }; 21 | 22 | componentDidMount() { 23 | window.addEventListener('mousemove', this.handleMouseMove); 24 | window.addEventListener('touchmove', this.handleTouchMove); 25 | window.addEventListener('mouseup', this.handleMouseUp); 26 | window.addEventListener('touchend', this.handleMouseUp); 27 | }; 28 | 29 | handleTouchStart = (pos, press, e) => { 30 | this.handleMouseDown(pos, press, e.touches[0]); 31 | }; 32 | 33 | handleMouseDown = (pos, [pressX, pressY], {pageX, pageY}) => { 34 | this.setState({ 35 | delta: [pageX - pressX, pageY - pressY], 36 | mouse: [pressX, pressY], 37 | isPressed: true, 38 | lastPressed: pos, 39 | }); 40 | }; 41 | 42 | handleTouchMove = (e) => { 43 | if (this.state.isPressed) { 44 | e.preventDefault(); 45 | } 46 | this.handleMouseMove(e.touches[0]); 47 | }; 48 | 49 | handleMouseMove = ({pageX, pageY}) => { 50 | const {isPressed, delta: [dx, dy]} = this.state; 51 | if (isPressed) { 52 | this.setState({mouse: [pageX - dx, pageY - dy]}); 53 | } 54 | }; 55 | 56 | handleMouseUp = () => { 57 | this.setState({ 58 | isPressed: false, 59 | delta: [0, 0], 60 | slider: {dragged: null, num: 0}, 61 | }); 62 | }; 63 | 64 | handleChange = (constant, num, {target}) => { 65 | const {firstConfig: [s, d]} = this.state; 66 | if (constant === 'stiffness') { 67 | this.setState({ 68 | firstConfig: [target.value - num * 30, d], 69 | }); 70 | } else { 71 | this.setState({ 72 | firstConfig: [s, target.value - num * 2], 73 | }); 74 | } 75 | }; 76 | 77 | handleMouseDownInput = (constant, num) => { 78 | this.setState({ 79 | slider: {dragged: constant, num: num}, 80 | }); 81 | }; 82 | 83 | render() { 84 | const { 85 | mouse, isPressed, lastPressed, firstConfig: [s0, d0], slider: {dragged, num}, 86 | } = this.state; 87 | return ( 88 |
89 | {grid.map((row, i) => { 90 | return row.map((cell, j) => { 91 | const cellStyle = { 92 | top: gridHeight * i, 93 | left: gridWidth * j, 94 | width: gridWidth, 95 | height: gridHeight, 96 | }; 97 | const stiffness = s0 + i * 30; 98 | const damping = d0 + j * 2; 99 | const motionStyle = isPressed 100 | ? {x: mouse[0], y: mouse[1]} 101 | : { 102 | x: spring(gridWidth / 2 - 25, {stiffness, damping}), 103 | y: spring(gridHeight / 2 - 25, {stiffness, damping}), 104 | }; 105 | 106 | return ( 107 |
108 | 115 | 122 | 123 | {({x, y}) => { 124 | let thing; 125 | if (dragged === 'stiffness') { 126 | thing = i < num ?
-{(num - i) * 30}
127 | : i > num ?
+{(i - num) * 30}
128 | :
0
; 129 | } else { 130 | thing = j < num ?
-{(num - j) * 2}
131 | : j > num ?
+{(j - num) * 2}
132 | :
0
; 133 | } 134 | const active = lastPressed[0] === i && lastPressed[1] === j 135 | ? 'demo5-ball-active' 136 | : ''; 137 | return ( 138 |
146 |
147 | {stiffness}{dragged === 'stiffness' && thing} 148 |
149 |
150 | {damping}{dragged === 'damping' && thing} 151 |
152 |
153 | ); 154 | }} 155 |
156 |
157 | ); 158 | }); 159 | })} 160 |
161 | ); 162 | }; 163 | } 164 | -------------------------------------------------------------------------------- /demos/demo3-todomvc-list-transition/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TransitionMotion, spring, presets} from '../../src/react-motion'; 3 | 4 | export default class Demo extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | todos: [ 9 | // key is creation date 10 | {key: 't1', data: {text: 'Board the plane', isDone: false}}, 11 | {key: 't2', data: {text: 'Sleep', isDone: false}}, 12 | {key: 't3', data: {text: 'Try to finish conference slides', isDone: false}}, 13 | {key: 't4', data: {text: 'Eat cheese and drink wine', isDone: false}}, 14 | {key: 't5', data: {text: 'Go around in Uber', isDone: false}}, 15 | {key: 't6', data: {text: 'Talk with conf attendees', isDone: false}}, 16 | {key: 't7', data: {text: 'Show Demo 1', isDone: false}}, 17 | {key: 't8', data: {text: 'Show Demo 2', isDone: false}}, 18 | {key: 't9', data: {text: 'Lament about the state of animation', isDone: false}}, 19 | {key: 't10', data: {text: 'Show Secret Demo', isDone: false}}, 20 | {key: 't11', data: {text: 'Go home', isDone: false}}, 21 | ], 22 | value: '', 23 | selected: 'all', 24 | }; 25 | }; 26 | 27 | // logic from todo, unrelated to animation 28 | handleChange = ({target: {value}}) => { 29 | this.setState({value}); 30 | }; 31 | 32 | handleSubmit = (e) => { 33 | e.preventDefault(); 34 | const newItem = { 35 | key: 't' + Date.now(), 36 | data: {text: this.state.value, isDone: false}, 37 | }; 38 | // append at head 39 | this.setState({todos: [newItem].concat(this.state.todos)}); 40 | }; 41 | 42 | handleDone = (doneKey) => { 43 | this.setState({ 44 | todos: this.state.todos.map(todo => { 45 | const {key, data: {text, isDone}} = todo; 46 | return key === doneKey 47 | ? {key: key, data: {text: text, isDone: !isDone}} 48 | : todo; 49 | }), 50 | }); 51 | }; 52 | 53 | handleToggleAll = () => { 54 | const allNotDone = this.state.todos.every(({data}) => data.isDone); 55 | this.setState({ 56 | todos: this.state.todos.map(({key, data: {text, isDone}}) => ( 57 | {key: key, data: {text: text, isDone: !allNotDone}} 58 | )), 59 | }); 60 | }; 61 | 62 | handleSelect = (selected) => { 63 | this.setState({selected}); 64 | }; 65 | 66 | handleClearCompleted = () => { 67 | this.setState({todos: this.state.todos.filter(({data}) => !data.isDone)}); 68 | }; 69 | 70 | handleDestroy = (date) => { 71 | this.setState({todos: this.state.todos.filter(({key}) => key !== date)}); 72 | }; 73 | 74 | // actual animation-related logic 75 | getDefaultStyles = () => { 76 | return this.state.todos.map(todo => ({...todo, style: {height: 0, opacity: 1}})); 77 | }; 78 | 79 | getStyles = () => { 80 | const {todos, value, selected} = this.state; 81 | return todos.filter(({data: {isDone, text}}) => { 82 | return text.toUpperCase().indexOf(value.toUpperCase()) >= 0 && 83 | (selected === 'completed' && isDone || 84 | selected === 'active' && !isDone || 85 | selected === 'all'); 86 | }) 87 | .map((todo, i) => { 88 | return { 89 | ...todo, 90 | style: { 91 | height: spring(60, presets.gentle), 92 | opacity: spring(1, presets.gentle), 93 | } 94 | }; 95 | }); 96 | }; 97 | 98 | willEnter() { 99 | return { 100 | height: 0, 101 | opacity: 1, 102 | }; 103 | }; 104 | 105 | willLeave() { 106 | return { 107 | height: spring(0), 108 | opacity: spring(0), 109 | }; 110 | }; 111 | 112 | render() { 113 | const {todos, value, selected} = this.state; 114 | const itemsLeft = todos.filter(({data: {isDone}}) => !isDone).length; 115 | return ( 116 |
117 |
118 |

todos

119 |
120 | 127 |
128 |
129 |
130 | 135 | 140 | {styles => 141 |
    142 | {styles.map(({key, style, data: {isDone, text}}) => 143 |
  • 144 |
    145 | 151 | 152 |
    157 |
  • 158 | )} 159 |
160 | } 161 |
162 |
163 | 196 |
197 | ); 198 | }; 199 | } 200 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Legend: 2 | - [B]: Breaking 3 | - [F]: Fix 4 | - [I]: Improvement 5 | 6 | ### 0.5.1 (August 28th 2017) 7 | - [F] New flow definitions, fixes children typing. 8 | 9 | ### 0.5.0 (April 26th 2017) 10 | - [B] Dropping support for older React. Currently supported versions are `^0.14.9` || `^15.3.0` 11 | - [I] Upgraded all React components to use ES6 classes 12 | - [I] Replace React.PropTypes with prop-types package 13 | 14 | ### 0.4.8 (April 17th 2017) 15 | 16 | - [I] Externalize stripStyle #452 by @bearcott 17 | - [I] Migrated deprecated React.PropTypes and React.createClass #446 by @Andarist 18 | - [F] Fix link to TypeScript types #443 by @pshrmn 19 | - [F] Refactored demo and fixed flow check errors #435 by @therewillbecode 20 | - [F] Fix broken link #430 by @codler 21 | - [F] Unmounted component setState fix #420 by @alleycat-at-git 22 | 23 | ### 0.4.7 (December 15th 2016) 24 | - [I] `didLeave` for `TransitionMotion`! Please check the README for more. 25 | 26 | ### 0.4.4 (June 4th 2016) 27 | - [F] Small fix to component unmounting bug (https://github.com/chenglou/react-motion/commit/49ea396041b0031b95f4941cc7efce200fcca454). It's not clear why this is erroring, but people want the temp fix. 28 | 29 | ### 0.4.3 (April 19th 2016) 30 | - [F] `TransitionMotion` `styles` function not being passed `defaultStyles` value upon first call. #296 31 | - [I] `onRest` callback for `Motion`! 32 | 33 | ### 0.4.2 (January 30th 2016) 34 | - [F] `TransitionMotion` keys merging bug. #264 35 | - [F] `TransitionMotion` rare stale read bug. [https://github.com/chenglou/react-motion/commit/f20dc1b9c8de7b387927b24afdb73e0a5ea0d0a6](https://github.com/chenglou/react-motion/commit/f20dc1b9c8de7b387927b24afdb73e0a5ea0d0a6) 36 | 37 | ### 0.4.1 (January 26th 2016) 38 | - [F] Made a mistake while publishing the bower package; fixed. 39 | 40 | ### 0.4.0 (January 26th 2016) 41 | - [B] `spring` helper's format has changed from `spring(10, [120, 12])` to `spring(10, {stiffness: 120, damping: 12})`. 42 | - [B] `style`, `styles` and `styles` of the three respective components now only accept either a number to interpolate, or a `spring` configuration to interpolate. Previously, it accepted (and ignored) random key/value pairs mixed in, such as `{x: spring(0), y: 'helloWorld'}`. `y` Doesn't belong there and should be placed elsewhere, e.g. directly on the (actual react) style of the component you're assigning the interpolating values on. 43 | - [B] `TransitionMotion` got an all-around clearer API. See the [upgrade guide](https://github.com/chenglou/react-motion/wiki) and [README section](https://github.com/chenglou/react-motion/blob/9877c311cc4a22099eb56fe7c76bad9753519ddb/README.md#transitionmotion-) for more. 44 | - [B] `Motion`'s' `defaultStyle`, informally accepted the format `{x: spring(0)}`. This is now officially unsupported. The correct format has always been `{x: 0}`. Setting a default style of `{x: spring(whatever)}` did not make sense; the configuration only applies for a `style`, aka destination value. Same modification applies to `StaggeredMotion` and `TransitionMotion`'s `defaultStyles` & `willEnter`. 45 | - [B] `TransitionMotion`'s `willEnter`/`willLeave`'s signature has changed. 46 | - [B] The `reorderKeys` helper is no longer needed thanks to the changes to `TransitionMotion`. It's now removed. 47 | - [B] React-Native specific build gone. RN 0.18+ uses the vanilla Npm React package, so there's no more need for us to export a wrapper. 48 | - [F] Bunch of bugs gone: #225, #212, #179, #157, #90, #88. 49 | - [I] `spring` has acquired a new field as part of the new signature: [precision tuning](https://github.com/chenglou/react-motion/blob/9877c311cc4a22099eb56fe7c76bad9753519ddb/README.md#--spring-val-number-config-springhelperconfig--opaqueconfig)! 50 | - [I] [Fully typed](https://github.com/chenglou/react-motion/blob/05d76f5ec7e9722dbca0237a97c41267e297eb2c/src/Types.js) via [Flow types](http://flowtype.org). 51 | - [I] Performance improvements. 52 | 53 | ### 0.3.1 (October 14th 2015) 54 | - [F] Handle `null` and `undefined` in `style`/`styles`. #181 55 | - [I] Library's now partially annotated with [Flow](http://flowtype.org). 56 | - [I] Related to above, the `src/` folder is now exposed on npm so that you can take advantage of Flow by using: `import {Motion} from 'react-motion/src/react-motion'` directly, instead of the old, prebuilt `import {Motion} from 'react-motion'`. **This is experimental** and intentionally undocumented. You'll have to adjust your webpack/browserify configurations to require these original source files correctly. No harm trying of course. It's just some type annotations =). 57 | 58 | ### 0.3.0 (September 30th 2015) 59 | - [B] API revamp! See [https://github.com/chenglou/react-motion/wiki](https://github.com/chenglou/react-motion/wiki) for more details. Thanks! 60 | 61 | ### 0.2.7 (August 6th 2015) 62 | - [F] Small bug where nested springs don't animate. #123 63 | - [I] Support for all React 0.14.0 betas. 64 | 65 | ### 0.2.6 (July 31th 2015) 66 | - [F] React-native warning's now gone, but also put into a separate file path. To require react-motion on react-native, do `require('react-motion/native')`. 67 | - [I] Support for React 0.14.0-beta1. 68 | 69 | ### 0.2.4 (July 29th 2015) 70 | - [I] React-native support! 71 | - [I] Allow returning `null` from children function. #101 72 | - [I] `defaultValue` for specifying a... default value, upon mounting. 73 | - [I] `TransitionSpring`'s `willLeave` API got simplified and now asks for an object as a return value instead of `null`. `null` is still supported, but is deprecated and will be removed in the next version. See the new docs on it [here](https://github.com/chenglou/react-motion/blob/24d6a7284ef61268c0ead67fe43d7e40bf45d381/README.md#transitionspring-). 74 | - [I] Exposed a few tasteful default spring configurations under the new exported `presets`. 75 | 76 | ### 0.2.2 (July 24th 2015) 77 | - [F] Import some internal modules correctly for Ubuntu/Linux node (case-sensitive for them). 78 | - [F] Nested springs work again. 79 | 80 | ### 0.2.0 (July 22th 2015) 81 | - [B] `willLeave` returning `false` will now keep the key. Only `null` and `undefined` will serve as a signal to kill the disappeared key. 82 | - [B] `willLeave` previously failed to expose the second argument `correspondingValueOfKeyThatJustLeft`. It's now exposed correctly. 83 | - [F] Definitively fix the previous problem of mis-detecting React Element as object. 84 | - [F] `willLeave` is now called only once per disappearing key. It was called more than once previously as a implementation detail. Though you should never have put side-effects in `willLeave`. It's still discouraged now. 85 | - [F] If you have some `this.props.handlerThatSetStateAndUnmountsSpringInOwnerRender()` in `Spring`'s `endValue`, Spring's already scheduled `requestAnimationFrame` will no longer cause an extra `setState` since it's unmounted. But in general, _please_ don't put side-effect in `endValue`. 86 | - [I] Stabilize the spring algorithm. No more erratic behavior with a big amount of animated items or tab switching (which usually slows down `requestAnimationFrame`). #57 87 | - [I] Partial (total?) support for IE9 by using a `requestAnimationFrame` polyfill. 88 | 89 | ### 0.1.0 (July 14th 2015) 90 | - [B] Breaking API: `TransitionSpring`'s `willEnter`'s callback signature is now `(keyThatEnters, correspondingValue, endValueYouJustSpecified, currentInterpolatedValue, currentSpeed)` (added `correspondingValue` as the second argument). Same for `willLeave`. 91 | - [B] `Spring` is now no longer exposed as a default, but simply as "Spring": `require('react-motion').Spring`. Or `import {Spring} from 'react-motion'`. 92 | - [B] `Spring` and `TransitionSpring`'s `children` function now expect a ReactElement. The components will no longer wrap the return value in a `div` for you. #44 #20 93 | - [I] Move React to from dependencies to peerDependencies. #35 94 | - [I] Internal cleanups + tests, for happier contributors. 95 | - [F] Mis-detecting React Element as object. 96 | - [F] Accidentally updating values at the first level of `endValue` without `{val: ...}` wrapper. 97 | 98 | ### 0.0.3 (July 9th 2015) 99 | - [I] Initial release. 100 | -------------------------------------------------------------------------------- /src/Motion.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import mapToZero from './mapToZero'; 3 | import stripStyle from './stripStyle'; 4 | import stepper from './stepper'; 5 | import defaultNow from 'performance-now'; 6 | import defaultRaf from 'raf'; 7 | import shouldStopAnimation from './shouldStopAnimation'; 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | 11 | import type { 12 | ReactElement, 13 | PlainStyle, 14 | Style, 15 | Velocity, 16 | MotionProps, 17 | } from './Types'; 18 | 19 | const msPerFrame = 1000 / 60; 20 | 21 | type MotionState = { 22 | currentStyle: PlainStyle, 23 | currentVelocity: Velocity, 24 | lastIdealStyle: PlainStyle, 25 | lastIdealVelocity: Velocity, 26 | }; 27 | 28 | export default class Motion extends React.Component { 29 | static propTypes = { 30 | // TOOD: warn against putting a config in here 31 | defaultStyle: PropTypes.objectOf(PropTypes.number), 32 | style: PropTypes.objectOf( 33 | PropTypes.oneOfType([PropTypes.number, PropTypes.object]), 34 | ).isRequired, 35 | children: PropTypes.func.isRequired, 36 | onRest: PropTypes.func, 37 | }; 38 | 39 | constructor(props: MotionProps) { 40 | super(props); 41 | this.state = this.defaultState(); 42 | } 43 | 44 | unmounting: boolean = false; 45 | wasAnimating: boolean = false; 46 | animationID: ?number = null; 47 | prevTime: number = 0; 48 | accumulatedTime: number = 0; 49 | 50 | defaultState(): MotionState { 51 | const { defaultStyle, style } = this.props; 52 | const currentStyle = defaultStyle || stripStyle(style); 53 | const currentVelocity = mapToZero(currentStyle); 54 | return { 55 | currentStyle, 56 | currentVelocity, 57 | lastIdealStyle: currentStyle, 58 | lastIdealVelocity: currentVelocity, 59 | }; 60 | } 61 | 62 | // it's possible that currentStyle's value is stale: if props is immediately 63 | // changed from 0 to 400 to spring(0) again, the async currentStyle is still 64 | // at 0 (didn't have time to tick and interpolate even once). If we naively 65 | // compare currentStyle with destVal it'll be 0 === 0 (no animation, stop). 66 | // In reality currentStyle should be 400 67 | unreadPropStyle: ?Style = null; 68 | // after checking for unreadPropStyle != null, we manually go set the 69 | // non-interpolating values (those that are a number, without a spring 70 | // config) 71 | clearUnreadPropStyle = (destStyle: Style): void => { 72 | let dirty = false; 73 | let { 74 | currentStyle, 75 | currentVelocity, 76 | lastIdealStyle, 77 | lastIdealVelocity, 78 | } = this.state; 79 | 80 | for (let key in destStyle) { 81 | if (!Object.prototype.hasOwnProperty.call(destStyle, key)) { 82 | continue; 83 | } 84 | 85 | const styleValue = destStyle[key]; 86 | if (typeof styleValue === 'number') { 87 | if (!dirty) { 88 | dirty = true; 89 | currentStyle = { ...currentStyle }; 90 | currentVelocity = { ...currentVelocity }; 91 | lastIdealStyle = { ...lastIdealStyle }; 92 | lastIdealVelocity = { ...lastIdealVelocity }; 93 | } 94 | 95 | currentStyle[key] = styleValue; 96 | currentVelocity[key] = 0; 97 | lastIdealStyle[key] = styleValue; 98 | lastIdealVelocity[key] = 0; 99 | } 100 | } 101 | 102 | if (dirty) { 103 | this.setState({ 104 | currentStyle, 105 | currentVelocity, 106 | lastIdealStyle, 107 | lastIdealVelocity, 108 | }); 109 | } 110 | }; 111 | 112 | startAnimationIfNecessary = (): void => { 113 | if (this.unmounting || this.animationID != null) { 114 | return; 115 | } 116 | 117 | // TODO: when config is {a: 10} and dest is {a: 10} do we raf once and 118 | // call cb? No, otherwise accidental parent rerender causes cb trigger 119 | this.animationID = defaultRaf(timestamp => { 120 | // https://github.com/chenglou/react-motion/pull/420 121 | // > if execution passes the conditional if (this.unmounting), then 122 | // executes async defaultRaf and after that component unmounts and after 123 | // that the callback of defaultRaf is called, then setState will be called 124 | // on unmounted component. 125 | if (this.unmounting) { 126 | return; 127 | } 128 | 129 | // check if we need to animate in the first place 130 | const propsStyle: Style = this.props.style; 131 | if ( 132 | shouldStopAnimation( 133 | this.state.currentStyle, 134 | propsStyle, 135 | this.state.currentVelocity, 136 | ) 137 | ) { 138 | if (this.wasAnimating && this.props.onRest) { 139 | this.props.onRest(); 140 | } 141 | 142 | // no need to cancel animationID here; shouldn't have any in flight 143 | this.animationID = null; 144 | this.wasAnimating = false; 145 | this.accumulatedTime = 0; 146 | return; 147 | } 148 | 149 | this.wasAnimating = true; 150 | 151 | const currentTime = timestamp || defaultNow(); 152 | const timeDelta = currentTime - this.prevTime; 153 | this.prevTime = currentTime; 154 | this.accumulatedTime = this.accumulatedTime + timeDelta; 155 | // more than 10 frames? prolly switched browser tab. Restart 156 | if (this.accumulatedTime > msPerFrame * 10) { 157 | this.accumulatedTime = 0; 158 | } 159 | 160 | if (this.accumulatedTime === 0) { 161 | // no need to cancel animationID here; shouldn't have any in flight 162 | this.animationID = null; 163 | this.startAnimationIfNecessary(); 164 | return; 165 | } 166 | 167 | let currentFrameCompletion = 168 | (this.accumulatedTime - 169 | Math.floor(this.accumulatedTime / msPerFrame) * msPerFrame) / 170 | msPerFrame; 171 | const framesToCatchUp = Math.floor(this.accumulatedTime / msPerFrame); 172 | 173 | let newLastIdealStyle: PlainStyle = {}; 174 | let newLastIdealVelocity: Velocity = {}; 175 | let newCurrentStyle: PlainStyle = {}; 176 | let newCurrentVelocity: Velocity = {}; 177 | 178 | for (let key in propsStyle) { 179 | if (!Object.prototype.hasOwnProperty.call(propsStyle, key)) { 180 | continue; 181 | } 182 | 183 | const styleValue = propsStyle[key]; 184 | if (typeof styleValue === 'number') { 185 | newCurrentStyle[key] = styleValue; 186 | newCurrentVelocity[key] = 0; 187 | newLastIdealStyle[key] = styleValue; 188 | newLastIdealVelocity[key] = 0; 189 | } else { 190 | let newLastIdealStyleValue = this.state.lastIdealStyle[key]; 191 | let newLastIdealVelocityValue = this.state.lastIdealVelocity[key]; 192 | for (let i = 0; i < framesToCatchUp; i++) { 193 | [newLastIdealStyleValue, newLastIdealVelocityValue] = stepper( 194 | msPerFrame / 1000, 195 | newLastIdealStyleValue, 196 | newLastIdealVelocityValue, 197 | styleValue.val, 198 | styleValue.stiffness, 199 | styleValue.damping, 200 | styleValue.precision, 201 | ); 202 | } 203 | const [nextIdealX, nextIdealV] = stepper( 204 | msPerFrame / 1000, 205 | newLastIdealStyleValue, 206 | newLastIdealVelocityValue, 207 | styleValue.val, 208 | styleValue.stiffness, 209 | styleValue.damping, 210 | styleValue.precision, 211 | ); 212 | 213 | newCurrentStyle[key] = 214 | newLastIdealStyleValue + 215 | (nextIdealX - newLastIdealStyleValue) * currentFrameCompletion; 216 | newCurrentVelocity[key] = 217 | newLastIdealVelocityValue + 218 | (nextIdealV - newLastIdealVelocityValue) * currentFrameCompletion; 219 | newLastIdealStyle[key] = newLastIdealStyleValue; 220 | newLastIdealVelocity[key] = newLastIdealVelocityValue; 221 | } 222 | } 223 | 224 | this.animationID = null; 225 | // the amount we're looped over above 226 | this.accumulatedTime -= framesToCatchUp * msPerFrame; 227 | 228 | this.setState({ 229 | currentStyle: newCurrentStyle, 230 | currentVelocity: newCurrentVelocity, 231 | lastIdealStyle: newLastIdealStyle, 232 | lastIdealVelocity: newLastIdealVelocity, 233 | }); 234 | 235 | this.unreadPropStyle = null; 236 | 237 | this.startAnimationIfNecessary(); 238 | }); 239 | }; 240 | 241 | componentDidMount() { 242 | this.prevTime = defaultNow(); 243 | this.startAnimationIfNecessary(); 244 | } 245 | 246 | UNSAFE_componentWillReceiveProps(props: MotionProps) { 247 | if (this.unreadPropStyle != null) { 248 | // previous props haven't had the chance to be set yet; set them here 249 | this.clearUnreadPropStyle(this.unreadPropStyle); 250 | } 251 | 252 | this.unreadPropStyle = props.style; 253 | if (this.animationID == null) { 254 | this.prevTime = defaultNow(); 255 | this.startAnimationIfNecessary(); 256 | } 257 | } 258 | 259 | componentWillUnmount() { 260 | this.unmounting = true; 261 | if (this.animationID != null) { 262 | defaultRaf.cancel(this.animationID); 263 | this.animationID = null; 264 | } 265 | } 266 | 267 | render(): ReactElement { 268 | const renderedChildren = this.props.children(this.state.currentStyle); 269 | return renderedChildren && React.Children.only(renderedChildren); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/StaggeredMotion.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import mapToZero from './mapToZero'; 3 | import stripStyle from './stripStyle'; 4 | import stepper from './stepper'; 5 | import defaultNow from 'performance-now'; 6 | import defaultRaf from 'raf'; 7 | import shouldStopAnimation from './shouldStopAnimation'; 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | 11 | import type { 12 | ReactElement, 13 | PlainStyle, 14 | Style, 15 | Velocity, 16 | StaggeredProps, 17 | } from './Types'; 18 | 19 | const msPerFrame = 1000 / 60; 20 | 21 | type StaggeredMotionState = { 22 | currentStyles: Array, 23 | currentVelocities: Array, 24 | lastIdealStyles: Array, 25 | lastIdealVelocities: Array, 26 | }; 27 | 28 | function shouldStopAnimationAll( 29 | currentStyles: Array, 30 | styles: Array