Repository: chenglou/react-motion Branch: master Commit: 9e3ce95bacaa Files: 68 Total size: 183.5 KB Directory structure: gitextract_zo4f_ak9/ ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .size-snapshot.json ├── .travis.yml ├── AUTHORS ├── HISTORY.md ├── LICENSE ├── README.md ├── bower.json ├── demos/ │ ├── README.md │ ├── demo0-simple-transition/ │ │ ├── Demo.jsx │ │ ├── index.html │ │ └── index.jsx │ ├── demo1-chat-heads/ │ │ ├── Demo.jsx │ │ ├── index.html │ │ └── index.jsx │ ├── demo2-draggable-balls/ │ │ ├── Demo.jsx │ │ ├── index.html │ │ └── index.jsx │ ├── demo3-todomvc-list-transition/ │ │ ├── Demo.jsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.jsx │ ├── demo4-photo-gallery/ │ │ ├── Demo.jsx │ │ ├── index.html │ │ └── index.jsx │ ├── demo5-spring-parameters-chooser/ │ │ ├── Demo.jsx │ │ ├── index.html │ │ └── index.jsx │ ├── demo7-water-ripples/ │ │ ├── Demo.jsx │ │ ├── index.html │ │ └── index.jsx │ └── demo8-draggable-list/ │ ├── Demo.jsx │ ├── index.html │ └── index.jsx ├── karma.conf.js ├── package.json ├── rollup.config.js ├── server.js ├── src/ │ ├── Motion.js │ ├── StaggeredMotion.js │ ├── TransitionMotion.js │ ├── Types.js │ ├── mapToZero.js │ ├── mergeDiff.js │ ├── presets.js │ ├── react-motion.js │ ├── reorderKeys.js │ ├── shouldStopAnimation.js │ ├── spring.js │ ├── stepper.js │ └── stripStyle.js ├── test/ │ ├── Motion-test.js │ ├── StaggeredMotion-test.js │ ├── TransitionMotion-test.js │ ├── createMockRaf.js │ ├── index.js │ ├── integration/ │ │ ├── README.md │ │ ├── bower.html │ │ ├── bower.json │ │ └── package.json │ ├── mergeDiff-test.js │ └── stripStyle-test.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ ["@babel/env", { "modules": "commonjs", "loose": true }], "@babel/flow", "@babel/react" ], "plugins": [ ["@babel/proposal-class-properties", { "loose": true }], ["transform-react-remove-prop-types", { "mode": "unsafe-wrap" }] ], "env": { "test": { "plugins": [ "@babel/transform-modules-commonjs" ] } } } ================================================ FILE: .eslintignore ================================================ build coverage lib node_modules **/*/all.js webpack.*.js server.js karma.*.js test/integration demos/demo6/babel.js demos/*/Demo.jsx ================================================ FILE: .eslintrc ================================================ { "parser": "babel-eslint", "extends": ["airbnb", "prettier"], "env": { "browser": true, "es6": true, "jasmine": true }, "rules": { "eqeqeq": [2, "allow-null"], "id-length": 0, "no-console": 0, // use it for warnings "no-nested-ternary": 0, "prefer-const": 0, "no-multiple-empty-lines": 0, "no-useless-escape": 0, "no-else-return": 0, "comma-dangle": 0, "lines-between-class-members": 0, "indent": 0, "operator-linebreak": 0, "object-curly-newline": 0, "function-paren-newline": 0, "prefer-destructuring": 0, "camelcase": ["error", {"allow": ["^UNSAFE_"]}], "react/jsx-wrap-multilines": 0, "react/jsx-one-expression-per-line": 0, "react/no-unused-state": 0, "react/require-default-props": 0, "react/destructuring-assignment": 0, "react/no-did-mount-set-state": 2, "react/no-multi-comp": 0, "react/jsx-boolean-value": [2, "always"], "react/sort-comp": [ 2, { "order": [ "displayName", "propTypes", "contextTypes", "childContextTypes", "mixins", "statics", "defaultProps", "getDefaultProps", "getInitialState", "getChildContext", "componentWillMount", "UNSAFE_componentWillMount", "componentDidMount", "componentWillReceiveProps", "UNSAFE_componentWillReceiveProps", "shouldComponentUpdate", "componentWillUpdate", "UNSAFE_componentWillUpdate", "componentDidUpdate", "componentWillUnmount", "/^on.+$/", "/^get.+$/", "/^render.+$/", "/^.+$/", // All other methods go here "render" ] } ], "max-len": 0, "no-mixed-operators": 0, "no-continue": 0, "no-restricted-syntax": 0, "no-plusplus": 0, "no-confusing-arrow": 0, "arrow-parens": 0, "arrow-body-style": 0, "react/jsx-indent": 0, "react/jsx-indent-props": 0, "react/jsx-closing-bracket-location": 0, "react/prefer-es6-class": 0, "react/jsx-filename-extension": 0, "react/prefer-stateless-function": 0, "object-curly-spacing": 0, "import/imports-first": 0, "import/no-unresolved": 0, "import/no-extraneous-dependencies": 0, "import/order": 0, "import/no-webpack-loader-syntax": 0, } } ================================================ FILE: .flowconfig ================================================ [ignore] .*/node_modules/fbjs/lib/PromiseMap.js .*/node_modules/fbjs/lib/fetchWithRetries.js .*/node_modules/fbjs/lib/Deferred.js.flow .*/node_modules/fbjs/lib/equalsSet.js.flow .*/node_modules/fbjs/lib/shallowEqual.js.flow .*/node_modules/fbjs/lib/someSet.js.flow .*/node_modules/fbjs/lib/everySet.js.flow .*/node_modules/fbjs/lib/UnicodeBidi.js.flow .*/node_modules/fbjs/lib/UnicodeBidiService.js.flow .*/node_modules/kefir/kefir.js.flow .*/node_modules/kefir/dist/kefir.js.flow .*/test/.* .*/node_modules/@webassemblyjs/.* .*/node_modules/eslint-plugin-jsx-a11y/.* .*/node_modules/rollup-plugin-size-snapshot/.* .*/node_modules/babel-plugin-transform-react-remove-prop-types [include] [libs] [options] ================================================ FILE: .gitignore ================================================ logs *.log pids *.pid *.seed coverage node_modules bower_components .DS_Store /demos/**/all.* /build/ /lib/ ================================================ FILE: .npmignore ================================================ /.* /bower_components/ /coverage/ /demos/ /test/ /karma.conf.js /webpack.* /server.js # sublime /*.sublime-project /*.sublime-workspace /rollup.config.js ================================================ FILE: .size-snapshot.json ================================================ { "build/react-motion.js": { "bundled": 77059, "minified": 22596, "gzipped": 6616 }, "build/react-motion.min.js": { "bundled": 52481, "minified": 15210, "gzipped": 4446 }, "lib/react-motion.esm.js": { "bundled": 43290, "minified": 14594, "gzipped": 3583, "treeshaked": { "rollup": { "code": 6381, "import_statements": 196 }, "webpack": { "code": 7588 } } } } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 10 script: - npm run -s lint - npm run -s flow_check - npm run -s test:travis ================================================ FILE: AUTHORS ================================================ Adrian le Bas Amadeus Junqueira Benjamin San Souci Bishop Zareh Brenton Simpson Cesar Andreu Cheng Lou Dan Abramov Daniel Dunderfelt Dustan Kasten Frederick Fogerty Gaëtan Renaudeau Google, Inc. Henry Zhu Ivan Starkov Jeroen van Aert Jesper Petersson Jevgeni Geimanen Joe Lencioni John Amiah Ford Jon Lebensold Justin Morris Kyle Mathews Ludovico Fischer Michael J Hoffman Mirko Mariani Neil Kistner Nik Butenko Nikhil Baradwaj Olivier Tassinari Paolo Moretti Raymond Zhou Robert Haritonov Sorin Iclanzan Stefan Dombrowski Stephen J. Collings Sundeep Malladi Sunil Pai Travis Arnold Wilfred Denton ================================================ FILE: HISTORY.md ================================================ Legend: - [B]: Breaking - [F]: Fix - [I]: Improvement ### 0.5.1 (August 28th 2017) - [F] New flow definitions, fixes children typing. ### 0.5.0 (April 26th 2017) - [B] Dropping support for older React. Currently supported versions are `^0.14.9` || `^15.3.0` - [I] Upgraded all React components to use ES6 classes - [I] Replace React.PropTypes with prop-types package ### 0.4.8 (April 17th 2017) - [I] Externalize stripStyle #452 by @bearcott - [I] Migrated deprecated React.PropTypes and React.createClass #446 by @Andarist - [F] Fix link to TypeScript types #443 by @pshrmn - [F] Refactored demo and fixed flow check errors #435 by @therewillbecode - [F] Fix broken link #430 by @codler - [F] Unmounted component setState fix #420 by @alleycat-at-git ### 0.4.7 (December 15th 2016) - [I] `didLeave` for `TransitionMotion`! Please check the README for more. ### 0.4.4 (June 4th 2016) - [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. ### 0.4.3 (April 19th 2016) - [F] `TransitionMotion` `styles` function not being passed `defaultStyles` value upon first call. #296 - [I] `onRest` callback for `Motion`! ### 0.4.2 (January 30th 2016) - [F] `TransitionMotion` keys merging bug. #264 - [F] `TransitionMotion` rare stale read bug. [https://github.com/chenglou/react-motion/commit/f20dc1b9c8de7b387927b24afdb73e0a5ea0d0a6](https://github.com/chenglou/react-motion/commit/f20dc1b9c8de7b387927b24afdb73e0a5ea0d0a6) ### 0.4.1 (January 26th 2016) - [F] Made a mistake while publishing the bower package; fixed. ### 0.4.0 (January 26th 2016) - [B] `spring` helper's format has changed from `spring(10, [120, 12])` to `spring(10, {stiffness: 120, damping: 12})`. - [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. - [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. - [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`. - [B] `TransitionMotion`'s `willEnter`/`willLeave`'s signature has changed. - [B] The `reorderKeys` helper is no longer needed thanks to the changes to `TransitionMotion`. It's now removed. - [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. - [F] Bunch of bugs gone: #225, #212, #179, #157, #90, #88. - [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)! - [I] [Fully typed](https://github.com/chenglou/react-motion/blob/05d76f5ec7e9722dbca0237a97c41267e297eb2c/src/Types.js) via [Flow types](http://flowtype.org). - [I] Performance improvements. ### 0.3.1 (October 14th 2015) - [F] Handle `null` and `undefined` in `style`/`styles`. #181 - [I] Library's now partially annotated with [Flow](http://flowtype.org). - [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 =). ### 0.3.0 (September 30th 2015) - [B] API revamp! See [https://github.com/chenglou/react-motion/wiki](https://github.com/chenglou/react-motion/wiki) for more details. Thanks! ### 0.2.7 (August 6th 2015) - [F] Small bug where nested springs don't animate. #123 - [I] Support for all React 0.14.0 betas. ### 0.2.6 (July 31th 2015) - [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')`. - [I] Support for React 0.14.0-beta1. ### 0.2.4 (July 29th 2015) - [I] React-native support! - [I] Allow returning `null` from children function. #101 - [I] `defaultValue` for specifying a... default value, upon mounting. - [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-). - [I] Exposed a few tasteful default spring configurations under the new exported `presets`. ### 0.2.2 (July 24th 2015) - [F] Import some internal modules correctly for Ubuntu/Linux node (case-sensitive for them). - [F] Nested springs work again. ### 0.2.0 (July 22th 2015) - [B] `willLeave` returning `false` will now keep the key. Only `null` and `undefined` will serve as a signal to kill the disappeared key. - [B] `willLeave` previously failed to expose the second argument `correspondingValueOfKeyThatJustLeft`. It's now exposed correctly. - [F] Definitively fix the previous problem of mis-detecting React Element as object. - [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. - [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`. - [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 - [I] Partial (total?) support for IE9 by using a `requestAnimationFrame` polyfill. ### 0.1.0 (July 14th 2015) - [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`. - [B] `Spring` is now no longer exposed as a default, but simply as "Spring": `require('react-motion').Spring`. Or `import {Spring} from 'react-motion'`. - [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 - [I] Move React to from dependencies to peerDependencies. #35 - [I] Internal cleanups + tests, for happier contributors. - [F] Mis-detecting React Element as object. - [F] Accidentally updating values at the first level of `endValue` without `{val: ...}` wrapper. ### 0.0.3 (July 9th 2015) - [I] Initial release. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 React Motion authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # React-Motion [![Build Status](https://travis-ci.org/chenglou/react-motion.svg?branch=master)](https://travis-ci.org/chenglou/react-motion) [![npm version](https://badge.fury.io/js/react-motion.svg)](https://www.npmjs.com/package/react-motion) [![Bower version](https://badge.fury.io/bo/react-motion.svg)](http://badge.fury.io/bo/react-motion) [![react-motion channel on discord](https://img.shields.io/badge/discord-react--motion%40reactiflux-738bd7.svg?style=flat)](https://discordapp.com/invite/0ZcbPKXt5bYzmcI0) ```js import {Motion, spring} from 'react-motion'; // In your render... {value =>
{value.x}
}
``` Animate a counter from `0` to `10`. For more advanced usage, see below. ### Install - Npm: `npm install --save react-motion` - Bower: **do not install with `bower install react-motion`, it won't work**. Use `bower install --save https://unpkg.com/react-motion/bower.zip`. Or in `bower.json`: ```json { "dependencies": { "react-motion": "https://unpkg.com/react-motion/bower.zip" } } ``` then include as ```html ``` - 1998 Script Tag: ```html (Module exposed as `ReactMotion`) ``` **Works with React-Native v0.18+**. ### Demos - [Simple Transition](http://chenglou.github.io/react-motion/demos/demo0-simple-transition) - [Chat Heads](http://chenglou.github.io/react-motion/demos/demo1-chat-heads) - [Draggable Balls](http://chenglou.github.io/react-motion/demos/demo2-draggable-balls) - [TodoMVC List Transition](http://chenglou.github.io/react-motion/demos/demo3-todomvc-list-transition) - [Photo Gallery](http://chenglou.github.io/react-motion/demos/demo4-photo-gallery) - [Spring Parameters Chooser](http://chenglou.github.io/react-motion/demos/demo5-spring-parameters-chooser) - [Water Ripples](http://chenglou.github.io/react-motion/demos/demo7-water-ripples) - [Draggable List](http://chenglou.github.io/react-motion/demos/demo8-draggable-list) [Check the wiki for more!](https://github.com/chenglou/react-motion/wiki/Gallery-of-third-party-React-Motion-demos) ### Try the Demos Locally ```sh git clone https://github.com/chenglou/react-motion.git cd react-motion npm install ``` - With hot reloading (**slow, development version**): run `npm start`. - Without hot reloading (**faster, production version**): run `npm run build-demos` and open the static `demos/demo_name/index.html` file directly. **Don't forget to use production mode when testing your animation's performance**! To build the repo yourself: `npm run prepublish`. ## What does this library try to solve? [My React-Europe talk](https://www.youtube.com/watch?v=1tavDv5hXpo) For 95% of use-cases of animating components, we don't have to resort to using hard-coded easing curves and duration. Set up a stiffness and damping for your UI element, and let the magic of physics take care of the rest. This way, you don't have to worry about petty situations such as interrupted animation behavior. It also greatly simplifies the API. This library also provides an alternative, more powerful API for React's `TransitionGroup`. ## API Exports: - `spring` - `Motion` - `StaggeredMotion` - `TransitionMotion` - `presets` [Here](https://github.com/chenglou/react-motion/blob/9cb90eca20ecf56e77feb816d101a4a9110c7d70/src/Types.js)'s the well-annotated public [Flow type](http://flowtype.org) definition file (you don't have to use Flow with React-motion, but the types help document the API below). P.S. using TypeScript? [Here](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-motion/index.d.ts) are the React-motion TypeScript definitions! --- ### Helpers ##### - spring: (val: number, config?: SpringHelperConfig) => OpaqueConfig Used in conjunction with the components below. Specifies the how to animate to the destination value, e.g. `spring(10, {stiffness: 120, damping: 17})` means "animate to value 10, with a spring of stiffness 120 and damping 17". - `val`: the value. - `config`: optional, for further adjustments. Possible fields: - `stiffness`: optional, defaults to `170`. - `damping`: optional, defaults to `26`. - `precision`: optional, defaults to `0.01`. Specifies both the rounding of the interpolated value and the speed (internal). It's normal not to feel how stiffness and damping affect your spring; use [Spring Parameters Chooser](http://chenglou.github.io/react-motion/demos/demo5-spring-parameters-chooser) to get a feeling. **Usually**, you'd just use the list of tasteful stiffness/damping presets below. ##### - Presets for `{stiffness, damping}` Commonly used spring configurations used like so: `spring(10, presets.wobbly)` or `spring(20, {...presets.gentle, precision: 0.1})`. [See here](https://github.com/chenglou/react-motion/blob/9cb90eca20ecf56e77feb816d101a4a9110c7d70/src/presets.js). --- ### <Motion /> #### Usage ```jsx {interpolatingStyle =>
} ``` #### Props ##### - style: Style Required. The `Style` type is an object that maps to either a `number` or an `OpaqueConfig` returned by `spring()` above. Must keep the same keys throughout component's existence. The meaning of the values: - an `OpaqueConfig` returned from `spring(x)`: interpolate to `x`. - a `number` `x`: jump to `x`, do not interpolate. ##### - defaultStyle?: PlainStyle Optional. The `PlainStyle` type maps to `number`s. Defaults to an object with the same keys as `style` above, whose values are the initial numbers you're interpolating on. **Note that during subsequent renders, this prop is ignored. The values will interpolate from the current ones to the destination ones (specified by `style`)**. ##### - children: (interpolatedStyle: PlainStyle) => ReactElement Required **function**. - `interpolatedStyle`: the interpolated style object passed back to you. E.g. if you gave `style={{x: spring(10), y: spring(20)}}`, you'll receive as `interpolatedStyle`, at a certain time, `{x: 5.2, y: 12.1}`, which you can then apply on your `div` or something else. - Return: must return **one** React element to render. ##### - onRest?: () => void Optional. The callback that fires when the animation comes to a rest. --- ### <StaggeredMotion /> Animates a collection of (**fixed length**) items whose values depend on each other, creating a natural, springy, "staggering" effect [like so](http://chenglou.github.io/react-motion/demos/demo1-chat-heads). This is preferred over hard-coding a delay for an array of `Motions` to achieve a similar (but less natural-looking) effect. #### Usage ```jsx prevInterpolatedStyles.map((_, i) => { return i === 0 ? {h: spring(100)} : {h: spring(prevInterpolatedStyles[i - 1].h)} })}> {interpolatingStyles =>
{interpolatingStyles.map((style, i) =>
) }
} ``` Aka "the current spring's destination value is the interpolating value of the previous spring". Imagine a spring dragging another. Physics, it works! #### Props ##### - styles: (previousInterpolatedStyles: ?Array<PlainStyle>) => Array<Style> Required **function**. **Don't forget the "s"**! - `previousInterpolatedStyles`: the previously interpolating (array of) styles (`undefined` at first render, unless `defaultStyles` is provided). - Return: must return an array of `Style`s containing the destination values, e.g. `[{x: spring(10)}, {x: spring(20)}]`. ##### - defaultStyles?: Array<PlainStyle> Optional. Similar to `Motion`'s `defaultStyle`, but an array of them. ##### - children: (interpolatedStyles: Array<PlainStyle>) => ReactElement Required **function**. Similar to `Motion`'s `children`, but accepts the array of interpolated styles instead, e.g. `[{x: 5}, {x: 6.4}, {x: 8.1}]` (No `onRest` for StaggeredMotion because we haven't found a good semantics for it yet. Voice your support in the issues section.) --- ### <TransitionMotion /> **Helps you to do mounting and unmounting animation**. #### Usage You have items `a`, `b`, `c`, with their respective style configuration, given to `TransitionMotion`'s `styles`. In its `children` function, you're passed the three interpolated styles as params; you map over them and produce three components. All is good. During next render, you give only `a` and `b`, indicating that you want `c` gone, but that you'd like to animate it reaching value `0`, before killing it for good. Fortunately, `TransitionMotion` has kept `c` around and still passes it into the `children` function param. So when you're mapping over these three interpolated styles, you're still producing three components. It'll keep interpolating, while checking `c`'s current value at every frame. Once `c` reaches the specified `0`, `TransitionMotion` will remove it for good (from the interpolated styles passed to your `children` function). This time, when mapping through the two remaining interpolated styles, you'll produce only two components. `c` is gone for real. ```jsx import createReactClass from 'create-react-class'; const Demo = createReactClass({ getInitialState() { return { items: [{key: 'a', size: 10}, {key: 'b', size: 20}, {key: 'c', size: 30}], }; }, componentDidMount() { this.setState({ items: [{key: 'a', size: 10}, {key: 'b', size: 20}], // remove c. }); }, willLeave() { // triggered when c's gone. Keeping c until its width/height reach 0. return {width: spring(0), height: spring(0)}; }, render() { return ( ({ key: item.key, style: {width: item.size, height: item.size}, }))}> {interpolatedStyles => // first render: a, b, c. Second: still a, b, c! Only last one's a, b.
{interpolatedStyles.map(config => { return
})}
} ); }, }); ``` #### Props First, two type definitions to ease the comprehension. - `TransitionStyle`: an object of the format `{key: string, data?: any, style: Style}`. - `key`: required. The ID that `TransitionMotion` uses to track which configuration is which across renders, even when things are reordered. Typically reused as the component `key` when you map over the interpolated styles. - `data`: optional. Anything you'd like to carry along. This is so that when the previous section example's `c` disappears, you still get to access `c`'s related data, such as the text to display along with it. - `style`: required. The actual starting style configuration, similar to what you provide for `Motion`'s `style`. Maps keys to either a number or an `OpaqueConfig` returned by `spring()`. - `TransitionPlainStyle`: similar to above, except the `style` field's value is of type `PlainStyle`, aka an object that maps to numbers. ##### - styles: Array<TransitionStyle> | (previousInterpolatedStyles: ?Array<TransitionPlainStyle>) => Array<TransitionStyle> Required. Accepts either: - an array of `TransitionStyle` configs, e.g. `[{key: 'a', style: {x: spring(0)}}, {key: 'b', style: {x: spring(10)}}]`. - a function similar to `StaggeredMotion`, taking the previously interpolating styles (`undefined` at first call, unless `defaultStyles` is provided), and returning the previously mentioned array of configs. __You can do staggered mounting animation with this__. ##### - defaultStyles?: Array<TransitionPlainStyle> Optional. Similar to the other components' `defaultStyle`/`defaultStyles`. ##### - children: (interpolatedStyles: Array<TransitionPlainStyle>) => ReactElement Required **function**. Similar to other two components' `children`. Receive back an array similar to what you provided for `defaultStyles`, only that each `style` object's number value represent the currently interpolating value. ##### - willLeave?: (styleThatLeft: TransitionStyle) => ?Style Optional. Defaults to `() => null`. **The magic sauce property**. - `styleThatLeft`: the e.g. `{key: ..., data: ..., style: ...}` object from the `styles` array, identified by `key`, that was present during a previous render, and that is now absent, thus triggering the call to `willLeave`. Note that the style property is exactly what you passed in `styles`, and is not interpolated. For example, if you passed a spring for `x` you will receive an object like `{x: {stiffness, damping, val, precision}}`. - Return: `null` to indicate you want the `TransitionStyle` gone immediately. A `Style` object to indicate you want to reach transition to the specified value(s) before killing the `TransitionStyle`. ##### - didLeave?: (styleThatLeft: `{key: string, data?: any}`) => void Optional. Defaults to `() => {}`. - `styleThatLeft`: the `{key:..., data:...}` that was removed after the finished transition. ##### - willEnter?: (styleThatEntered: TransitionStyle) => PlainStyle Optional. Defaults to `styleThatEntered => stripStyle(styleThatEntered.style)`. Where `stripStyle` turns `{x: spring(10), y: spring(20)}` into `{x: 10, y: 20}`. - `styleThatEntered`: similar to `willLeave`'s, except the `TransitionStyle` represents the object whose `key` value was absent during the last `render`, and that is now present. - Return: a `defaultStyle`-like `PlainStyle` configuration, e.g. `{x: 0, y: 0}`, that serves as the starting values of the animation. Under this light, the default provided means "a style config that has the same starting values as the destination values". **Note** that `willEnter` and `defaultStyles` serve different purposes. `willEnter` only triggers when a previously inexistent `TransitionStyle` inside `styles` comes into the new render. (No `onRest` for TransitionMotion because we haven't found a good semantics for it yet. Voice your support in the issues section.) --- ## FAQ - How do I set the duration of my animation? [Hard-coded duration goes against fluid interfaces](https://twitter.com/andy_matuschak/status/566736015188963328). If your animation is interrupted mid-way, you'd get a weird completion animation if you hard-coded the time. That being said, in the demo section there's a great [Spring Parameters Chooser](http://chenglou.github.io/react-motion/demos/demo5-spring-parameters-chooser) for you to have a feel of what spring is appropriate, rather than guessing a duration in the dark. - How do I unmount the `TransitionMotion` container itself? You don't. Unless you put it in another `TransitionMotion`... - How do I do staggering/chained animation where items animate in one after another? See [`StaggeredMotion`](#staggeredmotion-) - My `ref` doesn't work in the children function. React string refs won't work: ```jsx {currentValue =>
} ``` This is how React works. Here's the [callback ref solution](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs). ================================================ FILE: bower.json ================================================ { "name": "react-motion", "version": "0.5.1", "homepage": "https://github.com/chenglou/react-motion", "authors": [ "chenglou" ], "description": "A spring that solves your animation problems.", "main": [ "build/react-motion.js", "build/react-motion.map" ], "dependencies": { "react": "^0.14.9 || ^15.3.0" }, "keywords": [ "react", "component", "react-component", "transitiongroup", "spring", "tween", "motion", "animation", "transition", "ui" ], "license": "MIT", "ignore": [ "**/.*", "node_modules", "test", "demo*", "server.js", "src", "webpack.config.js", "webpack.prod.config.js", "karma.conf.js", "package.json" ] } ================================================ FILE: demos/README.md ================================================ ## Demos folder **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. ================================================ FILE: demos/demo0-simple-transition/Demo.jsx ================================================ import React from 'react'; import {Motion, spring} from '../../src/react-motion'; export default class Demo extends React.Component { constructor(props) { super(props); this.state = {open: false}; }; handleMouseDown = () => { this.setState({open: !this.state.open}); }; handleTouchStart = (e) => { e.preventDefault(); this.handleMouseDown(); }; render() { return (
{({x}) => // children is a callback which should accept the current value of // `style`
}
); }; } ================================================ FILE: demos/demo0-simple-transition/index.html ================================================ Toggle
================================================ FILE: demos/demo0-simple-transition/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo1-chat-heads/Demo.jsx ================================================ import React from 'react'; import {StaggeredMotion, spring, presets} from '../../src/react-motion'; import range from 'lodash.range'; export default class Demo extends React.Component { constructor(props) { super(props); this.state = {x: 250, y: 300}; }; componentDidMount() { window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('touchmove', this.handleTouchMove); }; handleMouseMove = ({pageX: x, pageY: y}) => { this.setState({x, y}); }; handleTouchMove = ({touches}) => { this.handleMouseMove(touches[0]); }; getStyles = (prevStyles) => { // `prevStyles` is the interpolated value of the last tick const endValue = prevStyles.map((_, i) => { return i === 0 ? this.state : { x: spring(prevStyles[i - 1].x, presets.gentle), y: spring(prevStyles[i - 1].y, presets.gentle), }; }); return endValue; }; render() { return ( ({x: 0, y: 0}))} styles={this.getStyles}> {balls =>
{balls.map(({x, y}, i) =>
)}
} ); }; } ================================================ FILE: demos/demo1-chat-heads/index.html ================================================ Chat Heads
================================================ FILE: demos/demo1-chat-heads/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo2-draggable-balls/Demo.jsx ================================================ import React from 'react'; import {Motion, spring} from '../../src/react-motion'; import range from 'lodash.range'; const springSetting1 = {stiffness: 180, damping: 10}; const springSetting2 = {stiffness: 120, damping: 17}; function reinsert(arr, from, to) { const _arr = arr.slice(0); const val = _arr[from]; _arr.splice(from, 1); _arr.splice(to, 0, val); return _arr; } function clamp(n, min, max) { return Math.max(Math.min(n, max), min); } const allColors = [ '#EF767A', '#456990', '#49BEAA', '#49DCB1', '#EEB868', '#EF767A', '#456990', '#49BEAA', '#49DCB1', '#EEB868', '#EF767A', ]; const [count, width, height] = [11, 70, 90]; // indexed by visual position const layout = range(count).map(n => { const row = Math.floor(n / 3); const col = n % 3; return [width * col, height * row]; }); export default class Demo extends React.Component { constructor(props) { super(props); this.state = { mouseXY: [0, 0], mouseCircleDelta: [0, 0], // difference between mouse and circle pos for x + y coords, for dragging lastPress: null, // key of the last pressed component isPressed: false, order: range(count), // index: visual position. value: component key/id }; }; componentDidMount() { window.addEventListener('touchmove', this.handleTouchMove); window.addEventListener('touchend', this.handleMouseUp); window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('mouseup', this.handleMouseUp); }; handleTouchStart = (key, pressLocation, e) => { this.handleMouseDown(key, pressLocation, e.touches[0]); }; handleTouchMove = (e) => { e.preventDefault(); this.handleMouseMove(e.touches[0]); }; handleMouseMove = ({pageX, pageY}) => { const {order, lastPress, isPressed, mouseCircleDelta: [dx, dy]} = this.state; if (isPressed) { const mouseXY = [pageX - dx, pageY - dy]; const col = clamp(Math.floor(mouseXY[0] / width), 0, 2); const row = clamp(Math.floor(mouseXY[1] / height), 0, Math.floor(count / 3)); const index = row * 3 + col; const newOrder = reinsert(order, order.indexOf(lastPress), index); this.setState({mouseXY, order: newOrder}); } }; handleMouseDown = (key, [pressX, pressY], {pageX, pageY}) => { this.setState({ lastPress: key, isPressed: true, mouseCircleDelta: [pageX - pressX, pageY - pressY], mouseXY: [pressX, pressY], }); }; handleMouseUp = () => { this.setState({isPressed: false, mouseCircleDelta: [0, 0]}); }; render() { const {order, lastPress, isPressed, mouseXY} = this.state; return (
{order.map((_, key) => { let style; let x; let y; const visualPosition = order.indexOf(key); if (key === lastPress && isPressed) { [x, y] = mouseXY; style = { translateX: x, translateY: y, scale: spring(1.2, springSetting1), boxShadow: spring((x - (3 * width - 50) / 2) / 15, springSetting1), }; } else { [x, y] = layout[visualPosition]; style = { translateX: spring(x, springSetting2), translateY: spring(y, springSetting2), scale: spring(1, springSetting1), boxShadow: spring((x - (3 * width - 50) / 2) / 15, springSetting1), }; } return ( {({translateX, translateY, scale, boxShadow}) =>
} ); })}
); }; } ================================================ FILE: demos/demo2-draggable-balls/index.html ================================================ Grid of Balls
================================================ FILE: demos/demo2-draggable-balls/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo3-todomvc-list-transition/Demo.jsx ================================================ import React from 'react'; import {TransitionMotion, spring, presets} from '../../src/react-motion'; export default class Demo extends React.Component { constructor(props) { super(props); this.state = { todos: [ // key is creation date {key: 't1', data: {text: 'Board the plane', isDone: false}}, {key: 't2', data: {text: 'Sleep', isDone: false}}, {key: 't3', data: {text: 'Try to finish conference slides', isDone: false}}, {key: 't4', data: {text: 'Eat cheese and drink wine', isDone: false}}, {key: 't5', data: {text: 'Go around in Uber', isDone: false}}, {key: 't6', data: {text: 'Talk with conf attendees', isDone: false}}, {key: 't7', data: {text: 'Show Demo 1', isDone: false}}, {key: 't8', data: {text: 'Show Demo 2', isDone: false}}, {key: 't9', data: {text: 'Lament about the state of animation', isDone: false}}, {key: 't10', data: {text: 'Show Secret Demo', isDone: false}}, {key: 't11', data: {text: 'Go home', isDone: false}}, ], value: '', selected: 'all', }; }; // logic from todo, unrelated to animation handleChange = ({target: {value}}) => { this.setState({value}); }; handleSubmit = (e) => { e.preventDefault(); const newItem = { key: 't' + Date.now(), data: {text: this.state.value, isDone: false}, }; // append at head this.setState({todos: [newItem].concat(this.state.todos)}); }; handleDone = (doneKey) => { this.setState({ todos: this.state.todos.map(todo => { const {key, data: {text, isDone}} = todo; return key === doneKey ? {key: key, data: {text: text, isDone: !isDone}} : todo; }), }); }; handleToggleAll = () => { const allNotDone = this.state.todos.every(({data}) => data.isDone); this.setState({ todos: this.state.todos.map(({key, data: {text, isDone}}) => ( {key: key, data: {text: text, isDone: !allNotDone}} )), }); }; handleSelect = (selected) => { this.setState({selected}); }; handleClearCompleted = () => { this.setState({todos: this.state.todos.filter(({data}) => !data.isDone)}); }; handleDestroy = (date) => { this.setState({todos: this.state.todos.filter(({key}) => key !== date)}); }; // actual animation-related logic getDefaultStyles = () => { return this.state.todos.map(todo => ({...todo, style: {height: 0, opacity: 1}})); }; getStyles = () => { const {todos, value, selected} = this.state; return todos.filter(({data: {isDone, text}}) => { return text.toUpperCase().indexOf(value.toUpperCase()) >= 0 && (selected === 'completed' && isDone || selected === 'active' && !isDone || selected === 'all'); }) .map((todo, i) => { return { ...todo, style: { height: spring(60, presets.gentle), opacity: spring(1, presets.gentle), } }; }); }; willEnter() { return { height: 0, opacity: 1, }; }; willLeave() { return { height: spring(0), opacity: spring(0), }; }; render() { const {todos, value, selected} = this.state; const itemsLeft = todos.filter(({data: {isDone}}) => !isDone).length; return (

todos

{styles =>
    {styles.map(({key, style, data: {isDone, text}}) =>
  • )}
}
); }; } ================================================ FILE: demos/demo3-todomvc-list-transition/index.css ================================================ hr { margin: 20px 0; border: 0; border-top: 1px dashed #c5c5c5; border-bottom: 1px dashed #f7f7f7; } .learn a { font-weight: normal; text-decoration: none; color: #b83f45; } .learn a:hover { text-decoration: underline; color: #787e7e; } .learn h3, .learn h4, .learn h5 { margin: 10px 0; font-weight: 500; line-height: 1.2; color: #000; } .learn h3 { font-size: 24px; } .learn h4 { font-size: 18px; } .learn h5 { margin-bottom: 0; font-size: 14px; } .learn ul { padding: 0; margin: 0 0 30px 25px; } .learn li { line-height: 20px; } .learn p { font-size: 15px; font-weight: 300; line-height: 1.3; margin-top: 0; margin-bottom: 0; } #issue-count { display: none; } .quote { border: none; margin: 20px 0 60px 0; } .quote p { font-style: italic; } .quote p:before { content: '“'; font-size: 50px; opacity: .15; position: absolute; top: -20px; left: 3px; } .quote p:after { content: '”'; font-size: 50px; opacity: .15; position: absolute; bottom: -42px; right: 3px; } .quote footer { position: absolute; bottom: -40px; right: 0; } .quote footer img { border-radius: 3px; } .quote footer a { margin-left: 5px; vertical-align: middle; } .speech-bubble { position: relative; padding: 10px; background: rgba(0, 0, 0, .04); border-radius: 5px; } .speech-bubble:after { content: ''; position: absolute; top: 100%; right: 30px; border: 13px solid transparent; border-top-color: rgba(0, 0, 0, .04); } .learn-bar > .learn { position: absolute; width: 272px; top: 8px; left: -300px; padding: 10px; border-radius: 5px; background-color: rgba(255, 255, 255, .6); transition-property: left; transition-duration: 500ms; } @media (min-width: 899px) { .learn-bar { width: auto; padding-left: 300px; } .learn-bar > .learn { left: 8px; } } html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; font-weight: 300; } button, input[type="checkbox"] { outline: none; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; outline: none; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } label[for='toggle-all'] { display: none; } .toggle-all { position: absolute; top: -55px; left: -12px; width: 60px; height: 34px; text-align: center; border: none; /* Mobile Safari */ } .toggle-all:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; box-shadow: 0 -1px 0 #ededed; overflow: hidden; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: 506px; padding: 13px 17px 12px 17px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle:after { content: url('data:image/svg+xml;utf8,'); } .todo-list li .toggle:checked:after { content: url('data:image/svg+xml;utf8,'); } .todo-list li label { white-space: pre; word-break: break-word; padding: 15px 60px 15px 15px; margin-left: 45px; display: block; line-height: 1.2; transition: color 0.4s; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { color: #777; padding: 10px 15px; height: 20px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a.selected, .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; position: relative; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } .toggle-all { -webkit-transform: rotate(90deg); transform: rotate(90deg); -webkit-appearance: none; appearance: none; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } ================================================ FILE: demos/demo3-todomvc-list-transition/index.html ================================================ RedoMVC
================================================ FILE: demos/demo3-todomvc-list-transition/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo4-photo-gallery/Demo.jsx ================================================ import React from 'react'; import {Motion, spring} from '../../src/react-motion'; const springSettings = {stiffness: 170, damping: 26}; const NEXT = 'show-next'; export default class Demo extends React.Component { constructor(props) { super(props); this.state = { photos: [[500, 350], [800, 600], [800, 400], [700, 500], [200, 650], [600, 600]], currPhoto: 0, }; }; handleChange = ({target: {value}}) => { this.setState({currPhoto: value}); }; clickHandler = (btn) => { let photoIndex = btn === NEXT ? this.state.currPhoto+1 : this.state.currPhoto-1; photoIndex = photoIndex >= 0 ? photoIndex : this.state.photos.length - 1; photoIndex = photoIndex >= this.state.photos.length ? 0 : photoIndex; this.setState({ currPhoto: photoIndex }) }; render() { const {photos, currPhoto} = this.state; const [currWidth, currHeight] = photos[currPhoto]; const widths = photos.map(([origW, origH]) => currHeight / origH * origW); const leftStartCoords = widths .slice(0, currPhoto) .reduce((sum, width) => sum - width, 0); let configs = []; photos.reduce((prevLeft, [origW, origH], i) => { configs.push({ left: spring(prevLeft, springSettings), height: spring(currHeight, springSettings), width: spring(widths[i], springSettings), }); return prevLeft + widths[i]; }, leftStartCoords); return (
Scroll Me
{container =>
{configs.map((style, i) => {style => } )}
}
); }; } ================================================ FILE: demos/demo4-photo-gallery/index.html ================================================ How Many Demos Do You Need
================================================ FILE: demos/demo4-photo-gallery/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo5-spring-parameters-chooser/Demo.jsx ================================================ import React from 'react'; import {Motion, spring} from '../../src/react-motion'; import range from 'lodash.range'; const gridWidth = 150; const gridHeight = 150; const grid = range(4).map(() => range(6)); export default class Demo extends React.Component { constructor(props) { super(props); this.state = { delta: [0, 0], mouse: [0, 0], isPressed: false, firstConfig: [60, 5], slider: {dragged: null, num: 0}, lastPressed: [0, 0], }; }; componentDidMount() { window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('touchmove', this.handleTouchMove); window.addEventListener('mouseup', this.handleMouseUp); window.addEventListener('touchend', this.handleMouseUp); }; handleTouchStart = (pos, press, e) => { this.handleMouseDown(pos, press, e.touches[0]); }; handleMouseDown = (pos, [pressX, pressY], {pageX, pageY}) => { this.setState({ delta: [pageX - pressX, pageY - pressY], mouse: [pressX, pressY], isPressed: true, lastPressed: pos, }); }; handleTouchMove = (e) => { if (this.state.isPressed) { e.preventDefault(); } this.handleMouseMove(e.touches[0]); }; handleMouseMove = ({pageX, pageY}) => { const {isPressed, delta: [dx, dy]} = this.state; if (isPressed) { this.setState({mouse: [pageX - dx, pageY - dy]}); } }; handleMouseUp = () => { this.setState({ isPressed: false, delta: [0, 0], slider: {dragged: null, num: 0}, }); }; handleChange = (constant, num, {target}) => { const {firstConfig: [s, d]} = this.state; if (constant === 'stiffness') { this.setState({ firstConfig: [target.value - num * 30, d], }); } else { this.setState({ firstConfig: [s, target.value - num * 2], }); } }; handleMouseDownInput = (constant, num) => { this.setState({ slider: {dragged: constant, num: num}, }); }; render() { const { mouse, isPressed, lastPressed, firstConfig: [s0, d0], slider: {dragged, num}, } = this.state; return (
{grid.map((row, i) => { return row.map((cell, j) => { const cellStyle = { top: gridHeight * i, left: gridWidth * j, width: gridWidth, height: gridHeight, }; const stiffness = s0 + i * 30; const damping = d0 + j * 2; const motionStyle = isPressed ? {x: mouse[0], y: mouse[1]} : { x: spring(gridWidth / 2 - 25, {stiffness, damping}), y: spring(gridHeight / 2 - 25, {stiffness, damping}), }; return (
{({x, y}) => { let thing; if (dragged === 'stiffness') { thing = i < num ?
-{(num - i) * 30}
: i > num ?
+{(i - num) * 30}
:
0
; } else { thing = j < num ?
-{(num - j) * 2}
: j > num ?
+{(j - num) * 2}
:
0
; } const active = lastPressed[0] === i && lastPressed[1] === j ? 'demo5-ball-active' : ''; return (
{stiffness}{dragged === 'stiffness' && thing}
{damping}{dragged === 'damping' && thing}
); }}
); }); })}
); }; } ================================================ FILE: demos/demo5-spring-parameters-chooser/index.html ================================================ Choose your weapon
Default: {stiffness: 170, damping: 26}

Drag a circle to see the differences in animation behavior

================================================ FILE: demos/demo5-spring-parameters-chooser/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo7-water-ripples/Demo.jsx ================================================ import React from 'react'; import {TransitionMotion, spring} from '../../src/react-motion'; const leavingSpringConfig = {stiffness: 60, damping: 15}; export default class Demo extends React.Component { constructor(props) { super(props); this.state = {mouse: [], now: 't' + 0}; }; handleMouseMove = ({pageX, pageY}) => { // Make sure the state is queued and not batched. this.setState(() => { return { mouse: [pageX - 25, pageY - 25], now: 't' + Date.now(), }; }); }; handleTouchMove = (e) => { e.preventDefault(); this.handleMouseMove(e.touches[0]); }; willLeave = (styleCell) => { return { ...styleCell.style, opacity: spring(0, leavingSpringConfig), scale: spring(2, leavingSpringConfig), }; }; render() { const {mouse: [mouseX, mouseY], now} = this.state; const styles = mouseX == null ? [] : [{ key: now, style: { opacity: spring(1), scale: spring(0), x: spring(mouseX), y: spring(mouseY), } }]; return ( {circles =>
{circles.map(({key, style: {opacity, scale, x, y}}) =>
)}
} ); }; } ================================================ FILE: demos/demo7-water-ripples/index.html ================================================ Ripples
================================================ FILE: demos/demo7-water-ripples/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: demos/demo8-draggable-list/Demo.jsx ================================================ import React from 'react'; import {Motion, spring} from '../../src/react-motion'; import range from 'lodash.range'; function reinsert(arr, from, to) { const _arr = arr.slice(0); const val = _arr[from]; _arr.splice(from, 1); _arr.splice(to, 0, val); return _arr; } function clamp(n, min, max) { return Math.max(Math.min(n, max), min); } const springConfig = {stiffness: 300, damping: 50}; const itemsCount = 4; export default class Demo extends React.Component { constructor(props) { super(props); this.state = { topDeltaY: 0, mouseY: 0, isPressed: false, originalPosOfLastPressed: 0, order: range(itemsCount), }; }; componentDidMount() { window.addEventListener('touchmove', this.handleTouchMove); window.addEventListener('touchend', this.handleMouseUp); window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('mouseup', this.handleMouseUp); }; handleTouchStart = (key, pressLocation, e) => { this.handleMouseDown(key, pressLocation, e.touches[0]); }; handleTouchMove = (e) => { e.preventDefault(); this.handleMouseMove(e.touches[0]); }; handleMouseDown = (pos, pressY, {pageY}) => { this.setState({ topDeltaY: pageY - pressY, mouseY: pressY, isPressed: true, originalPosOfLastPressed: pos, }); }; handleMouseMove = ({pageY}) => { const {isPressed, topDeltaY, order, originalPosOfLastPressed} = this.state; if (isPressed) { const mouseY = pageY - topDeltaY; const currentRow = clamp(Math.round(mouseY / 100), 0, itemsCount - 1); let newOrder = order; if (currentRow !== order.indexOf(originalPosOfLastPressed)){ newOrder = reinsert(order, order.indexOf(originalPosOfLastPressed), currentRow); } this.setState({mouseY: mouseY, order: newOrder}); } }; handleMouseUp = () => { this.setState({isPressed: false, topDeltaY: 0}); }; render() { const {mouseY, isPressed, originalPosOfLastPressed, order} = this.state; return (
{range(itemsCount).map(i => { const style = originalPosOfLastPressed === i && isPressed ? { scale: spring(1.1, springConfig), shadow: spring(16, springConfig), y: mouseY, } : { scale: spring(1, springConfig), shadow: spring(1, springConfig), y: spring(order.indexOf(i) * 100, springConfig), }; return ( {({scale, shadow, y}) =>
{order.indexOf(i) + 1}
}
); })}
); }; } ================================================ FILE: demos/demo8-draggable-list/index.html ================================================ Framer cards ================================================ FILE: demos/demo8-draggable-list/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Demo from './Demo'; ReactDOM.render(, document.querySelector('#content')); ================================================ FILE: karma.conf.js ================================================ 'use strict'; var path = require('path'); var withCoverage = process.argv.indexOf('coverage') !== -1 || process.env.COVERAGE; var webpackConfig = { mode: 'development', module: { rules: withCoverage ? [ { test: /\.js$/, loader: 'babel-loader', include: [path.resolve('./test')] }, { test: /\.js$/, loader: 'isparta-loader', include: [path.resolve('./src')] }, ] : [ { test: /\.js$/, loader: 'babel-loader', include: [path.resolve('./src'), path.resolve('./test')], }, ], }, stats: { colors: true, } }; module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine'], files: [ './node_modules/@babel/polyfill/browser.js', 'test/index.js', ], webpack: webpackConfig, webpackMiddleware: { stats: { chunkModules: false, colors: true, }, }, exclude: [], preprocessors: { 'test/index.js': ['webpack'], }, reporters: ['jasmine-diff', 'progress'], jasmineDiffReporter: { pretty: true, color: { expectedBg: '', expectedFg: 'red', actualBg: '', actualFg: 'green', defaultBg: '', defaultFg: 'grey' } }, coverageReporter: { dir: './coverage/', subdir: '.', reporters: [ {type: 'html'}, {type: 'lcovonly'}, {type: 'text', file: 'text.txt'}, {type: 'text-summary', file: 'text-summary.txt'}, ], }, captureTimeout: 90000, browserNoActivityTimeout: 60000, port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: false, browsers: ['ChromeHeadless'], singleRun: true, }); }; ================================================ FILE: package.json ================================================ { "name": "react-motion", "version": "0.5.2", "description": "A spring that solves your animation problems.", "main": "lib/react-motion.js", "module": "lib/react-motion.esm.js", "peerDependencies": { "react": "^0.14.9 || ^15.3.0 || ^16.0.0" }, "devDependencies": { "@babel/cli": "^7.0.0-rc.1", "@babel/core": "^7.7.2", "@babel/plugin-proposal-class-properties": "^7.7.0", "@babel/plugin-transform-modules-commonjs": "^7.7.0", "@babel/plugin-transform-runtime": "^7.6.2", "@babel/polyfill": "^7.7.0", "@babel/preset-env": "^7.7.1", "@babel/preset-flow": "^7.0.0", "@babel/preset-react": "^7.7.0", "babel-eslint": "^10.0.3", "babel-loader": "^8.0.6", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "codemirror": "^5.5.0", "cross-env": "^5.2.0", "css-loader": "^0.28.11", "eslint": "^5.16.0", "eslint-config-airbnb": "^17.0.0", "eslint-config-prettier": "^2.9.0", "eslint-loader": "^2.0.0", "eslint-plugin-flowtype": "^2.49.3", "eslint-plugin-import": "^2.13.0", "eslint-plugin-jsx-a11y": "^6.1.0", "eslint-plugin-react": "^7.10.0", "flow-bin": "^0.53.1", "flow-copy-source": "^1.1.0", "husky": "^0.14.3", "inject-loader": "^4.0.1", "isparta-loader": "^2.0.0", "jasmine-core": "^3.1.0", "karma": "^2.0.4", "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.2", "karma-jasmine": "^1.1.2", "karma-jasmine-diff-reporter": "^1.2.0", "karma-webpack": "^3.0.0", "lint-staged": "^7.2.0", "lodash.range": "^3.0.1", "prettier": "^1.14.2", "react": ">=15.5.0", "react-codemirror": ">=0.1.2", "react-dom": ">=15.5.0", "rollup": "^0.62.0", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-commonjs": "^9.1.4", "rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-replace": "^2.0.0", "rollup-plugin-size-snapshot": "^0.6.0", "rollup-plugin-uglify": "^4.0.0", "style-loader": "^0.21.0", "webpack": "^4.14.0", "webpack-command": "^0.4.0", "webpack-dev-server": "^3.1.4" }, "scripts": { "start": "node server.js", "prebuild:dist": "rm -rf build", "build:dist": "rollup -c", "prebuild:lib": "rm -rf lib", "build:lib": "babel src --out-dir lib", "build:flow": "flow-copy-source -v src lib", "build": "npm run build:dist && npm run build:lib && npm run build:flow", "build-demos": "webpack", "lint": "eslint --ext .js,.jsx .", "flow_check": "flow check", "prepublishOnly": "npm run build", "test": "cross-env NODE_ENV=test karma start ./karma.conf.js --single-run", "test:travis": "cross-env NODE_ENV=test karma start ./karma.conf.js --single-run", "test:dev": "cross-env NODE_ENV=test karma start ./karma.conf.js --no-single-run --auto-watch", "test:cov": "cross-env NODE_ENV=test karma start ./karma.conf.js --single-run --reporters coverage", "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", "precommit": "lint-staged" }, "lint-staged": { "*.js": [ "prettier --write", "git add" ] }, "prettier": { "singleQuote": true, "trailingComma": "all" }, "repository": { "type": "git", "url": "https://github.com/chenglou/react-motion.git" }, "keywords": [ "react", "component", "react-component", "transitiongroup", "spring", "tween", "motion", "animation", "transition", "ui" ], "author": [ "nkbt", "chenglou" ], "license": "MIT", "dependencies": { "@babel/runtime": "7.7.2", "performance-now": "^2.1.0", "prop-types": "^15.5.8", "raf": "^3.1.0" } } ================================================ FILE: rollup.config.js ================================================ import nodeResolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import babel from 'rollup-plugin-babel'; import replace from 'rollup-plugin-replace'; import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; import { uglify } from 'rollup-plugin-uglify'; import pkg from './package.json'; const input = './src/react-motion.js'; const name = 'ReactMotion'; const globals = { react: 'React', }; // treat as external "module/path" modules and reserved rollup paths const external = id => !id.startsWith('\0') && !id.startsWith('.') && !id.startsWith('/'); const getBabelOptions = () => ({ babelrc: false, exclude: '**/node_modules/**', runtimeHelpers: true, plugins: [ ['@babel/proposal-class-properties', { loose: true }], ['transform-react-remove-prop-types', { mode: 'unsafe-wrap' }], ['@babel/transform-runtime', { useESModules: true }], ], presets: [ ['@babel/env', { modules: false, loose: true }], '@babel/flow', '@babel/react', ], }); const commonjsOptions = { include: '**/node_modules/**', }; export default [ { input, output: { file: 'build/react-motion.js', format: 'umd', name, globals }, external: Object.keys(globals), plugins: [ nodeResolve(), babel(getBabelOptions()), commonjs(commonjsOptions), replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), sizeSnapshot(), ], }, { input, output: { file: 'build/react-motion.min.js', format: 'umd', name, globals }, external: Object.keys(globals), plugins: [ nodeResolve(), babel(getBabelOptions()), commonjs(commonjsOptions), replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), sizeSnapshot(), uglify(), ], }, { input, output: { file: pkg.module, format: 'esm' }, external, plugins: [babel(getBabelOptions()), sizeSnapshot()], }, ]; ================================================ FILE: server.js ================================================ 'use strict'; process.env.NODE_ENV = 'development'; var webpack = require('webpack'); var WebpackDevServer = require('webpack-dev-server'); var config = require('./webpack.config'); var port = process.env.PORT || 3000; new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, stats: { chunkModules: false, colors: true, } }).listen(port, '0.0.0.0', function (err) { if (err) { console.log(err); } console.log('Listening at 0.0.0.0:' + port); }); ================================================ FILE: src/Motion.js ================================================ /* @flow */ import mapToZero from './mapToZero'; import stripStyle from './stripStyle'; import stepper from './stepper'; import defaultNow from 'performance-now'; import defaultRaf from 'raf'; import shouldStopAnimation from './shouldStopAnimation'; import React from 'react'; import PropTypes from 'prop-types'; import type { ReactElement, PlainStyle, Style, Velocity, MotionProps, } from './Types'; const msPerFrame = 1000 / 60; type MotionState = { currentStyle: PlainStyle, currentVelocity: Velocity, lastIdealStyle: PlainStyle, lastIdealVelocity: Velocity, }; export default class Motion extends React.Component { static propTypes = { // TOOD: warn against putting a config in here defaultStyle: PropTypes.objectOf(PropTypes.number), style: PropTypes.objectOf( PropTypes.oneOfType([PropTypes.number, PropTypes.object]), ).isRequired, children: PropTypes.func.isRequired, onRest: PropTypes.func, }; constructor(props: MotionProps) { super(props); this.state = this.defaultState(); } unmounting: boolean = false; wasAnimating: boolean = false; animationID: ?number = null; prevTime: number = 0; accumulatedTime: number = 0; defaultState(): MotionState { const { defaultStyle, style } = this.props; const currentStyle = defaultStyle || stripStyle(style); const currentVelocity = mapToZero(currentStyle); return { currentStyle, currentVelocity, lastIdealStyle: currentStyle, lastIdealVelocity: currentVelocity, }; } // it's possible that currentStyle's value is stale: if props is immediately // changed from 0 to 400 to spring(0) again, the async currentStyle is still // at 0 (didn't have time to tick and interpolate even once). If we naively // compare currentStyle with destVal it'll be 0 === 0 (no animation, stop). // In reality currentStyle should be 400 unreadPropStyle: ?Style = null; // after checking for unreadPropStyle != null, we manually go set the // non-interpolating values (those that are a number, without a spring // config) clearUnreadPropStyle = (destStyle: Style): void => { let dirty = false; let { currentStyle, currentVelocity, lastIdealStyle, lastIdealVelocity, } = this.state; for (let key in destStyle) { if (!Object.prototype.hasOwnProperty.call(destStyle, key)) { continue; } const styleValue = destStyle[key]; if (typeof styleValue === 'number') { if (!dirty) { dirty = true; currentStyle = { ...currentStyle }; currentVelocity = { ...currentVelocity }; lastIdealStyle = { ...lastIdealStyle }; lastIdealVelocity = { ...lastIdealVelocity }; } currentStyle[key] = styleValue; currentVelocity[key] = 0; lastIdealStyle[key] = styleValue; lastIdealVelocity[key] = 0; } } if (dirty) { this.setState({ currentStyle, currentVelocity, lastIdealStyle, lastIdealVelocity, }); } }; startAnimationIfNecessary = (): void => { if (this.unmounting || this.animationID != null) { return; } // TODO: when config is {a: 10} and dest is {a: 10} do we raf once and // call cb? No, otherwise accidental parent rerender causes cb trigger this.animationID = defaultRaf(timestamp => { // https://github.com/chenglou/react-motion/pull/420 // > if execution passes the conditional if (this.unmounting), then // executes async defaultRaf and after that component unmounts and after // that the callback of defaultRaf is called, then setState will be called // on unmounted component. if (this.unmounting) { return; } // check if we need to animate in the first place const propsStyle: Style = this.props.style; if ( shouldStopAnimation( this.state.currentStyle, propsStyle, this.state.currentVelocity, ) ) { if (this.wasAnimating && this.props.onRest) { this.props.onRest(); } // no need to cancel animationID here; shouldn't have any in flight this.animationID = null; this.wasAnimating = false; this.accumulatedTime = 0; return; } this.wasAnimating = true; const currentTime = timestamp || defaultNow(); const timeDelta = currentTime - this.prevTime; this.prevTime = currentTime; this.accumulatedTime = this.accumulatedTime + timeDelta; // more than 10 frames? prolly switched browser tab. Restart if (this.accumulatedTime > msPerFrame * 10) { this.accumulatedTime = 0; } if (this.accumulatedTime === 0) { // no need to cancel animationID here; shouldn't have any in flight this.animationID = null; this.startAnimationIfNecessary(); return; } let currentFrameCompletion = (this.accumulatedTime - Math.floor(this.accumulatedTime / msPerFrame) * msPerFrame) / msPerFrame; const framesToCatchUp = Math.floor(this.accumulatedTime / msPerFrame); let newLastIdealStyle: PlainStyle = {}; let newLastIdealVelocity: Velocity = {}; let newCurrentStyle: PlainStyle = {}; let newCurrentVelocity: Velocity = {}; for (let key in propsStyle) { if (!Object.prototype.hasOwnProperty.call(propsStyle, key)) { continue; } const styleValue = propsStyle[key]; if (typeof styleValue === 'number') { newCurrentStyle[key] = styleValue; newCurrentVelocity[key] = 0; newLastIdealStyle[key] = styleValue; newLastIdealVelocity[key] = 0; } else { let newLastIdealStyleValue = this.state.lastIdealStyle[key]; let newLastIdealVelocityValue = this.state.lastIdealVelocity[key]; for (let i = 0; i < framesToCatchUp; i++) { [newLastIdealStyleValue, newLastIdealVelocityValue] = stepper( msPerFrame / 1000, newLastIdealStyleValue, newLastIdealVelocityValue, styleValue.val, styleValue.stiffness, styleValue.damping, styleValue.precision, ); } const [nextIdealX, nextIdealV] = stepper( msPerFrame / 1000, newLastIdealStyleValue, newLastIdealVelocityValue, styleValue.val, styleValue.stiffness, styleValue.damping, styleValue.precision, ); newCurrentStyle[key] = newLastIdealStyleValue + (nextIdealX - newLastIdealStyleValue) * currentFrameCompletion; newCurrentVelocity[key] = newLastIdealVelocityValue + (nextIdealV - newLastIdealVelocityValue) * currentFrameCompletion; newLastIdealStyle[key] = newLastIdealStyleValue; newLastIdealVelocity[key] = newLastIdealVelocityValue; } } this.animationID = null; // the amount we're looped over above this.accumulatedTime -= framesToCatchUp * msPerFrame; this.setState({ currentStyle: newCurrentStyle, currentVelocity: newCurrentVelocity, lastIdealStyle: newLastIdealStyle, lastIdealVelocity: newLastIdealVelocity, }); this.unreadPropStyle = null; this.startAnimationIfNecessary(); }); }; componentDidMount() { this.prevTime = defaultNow(); this.startAnimationIfNecessary(); } UNSAFE_componentWillReceiveProps(props: MotionProps) { if (this.unreadPropStyle != null) { // previous props haven't had the chance to be set yet; set them here this.clearUnreadPropStyle(this.unreadPropStyle); } this.unreadPropStyle = props.style; if (this.animationID == null) { this.prevTime = defaultNow(); this.startAnimationIfNecessary(); } } componentWillUnmount() { this.unmounting = true; if (this.animationID != null) { defaultRaf.cancel(this.animationID); this.animationID = null; } } render(): ReactElement { const renderedChildren = this.props.children(this.state.currentStyle); return renderedChildren && React.Children.only(renderedChildren); } } ================================================ FILE: src/StaggeredMotion.js ================================================ /* @flow */ import mapToZero from './mapToZero'; import stripStyle from './stripStyle'; import stepper from './stepper'; import defaultNow from 'performance-now'; import defaultRaf from 'raf'; import shouldStopAnimation from './shouldStopAnimation'; import React from 'react'; import PropTypes from 'prop-types'; import type { ReactElement, PlainStyle, Style, Velocity, StaggeredProps, } from './Types'; const msPerFrame = 1000 / 60; type StaggeredMotionState = { currentStyles: Array, currentVelocities: Array, lastIdealStyles: Array, lastIdealVelocities: Array, }; function shouldStopAnimationAll( currentStyles: Array, styles: Array