├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── demo
├── Gossip
│ ├── app.js
│ └── index.html
├── GroupAnimation
│ ├── app.js
│ └── index.html
├── Pendulum
│ ├── app.js
│ └── index.html
├── SimpleAnimation
│ ├── app.js
│ └── index.html
├── index.html
└── webpack.config.js
├── jest.config.js
├── package-lock.json
├── package.json
├── scripts
├── build.js
└── release.sh
├── src
├── Animate.js
├── AnimateGroup.js
├── AnimateGroupChild.js
├── AnimateManager.js
├── configUpdate.js
├── easing.js
├── index.js
├── setRafTimeout.js
└── util.js
├── test
└── index.spec.js
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain
2 | # consistent coding styles between different editors and IDEs.
3 |
4 | root = true
5 |
6 | [*]
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | test/**
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["eslint-config-airbnb", "plugin:prettier/recommended", "prettier"],
3 | "env": {
4 | "browser": true,
5 | "jest": true,
6 | "node": true
7 | },
8 | "parser": "@babel/eslint-parser",
9 | "rules": {
10 | "no-restricted-globals": [1, "isFinite"],
11 | "valid-jsdoc": 2,
12 | "react/jsx-uses-react": 2,
13 | "react/jsx-uses-vars": 2,
14 | "react/react-in-jsx-scope": 2,
15 | "no-var": 0,
16 | "vars-on-top": 0,
17 | "prefer-destructuring": "off",
18 | "no-mixed-operators": "off",
19 | "no-plusplus": "off",
20 | "no-continue": "off",
21 | "react/require-default-props": "off",
22 | "react/jsx-filename-extension": "off",
23 | "react/forbid-prop-types": "off",
24 | "react/jsx-props-no-spreading": "off"
25 | },
26 | "plugins": ["react"]
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: 'Recharts React Smooth'
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | smooth-job:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: '14'
19 |
20 | - name: Installing deps
21 | run: npm install
22 |
23 | - name: Testing & Linting
24 | run: |
25 | npm run lint
26 | npm run test
27 |
28 | - name: Building packages
29 | run: npm run build
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | umd
5 | lib
6 | es6
7 | coverage
8 | npm-debug.log
9 | *.orig
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "printWidth": 120,
4 | "semi": true,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "useTabs": false,
8 | "trailingComma": "all"
9 | }
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "14"
4 | services:
5 | - xvfb
6 | before_script:
7 | - export DISPLAY=:99.0
8 | script:
9 | - npm run test
10 | - npm run lint
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 3.0.0 / 2023-10-18
2 |
3 | ### refactor / chore
4 |
5 | - upgrade dependencies
6 |
7 | # BREAKING CHANGE
8 |
9 | Remove some unused/unneeded code which drops support for older browser versions. Results in slightly decreased bundle size.
10 |
11 | - remove unneeded polyfill for translateStyle - [browser support since 2017](https://caniuse.com/?search=transforms)
12 | - remove unneeded polyfill for `Number.isFinite` - [browser support since 2015](https://caniuse.com/?search=Number.isFinite) AND polyfilled by babel/core-js
13 |
14 | ## 2.0.5 / 2023-10-10
15 |
16 | ### fix
17 |
18 | - Check if `requestAnimationFrame` is defined in shouldUpdate
19 |
20 | ## 2.0.4 / 2023-09-12
21 |
22 | ### fix
23 |
24 | - call `onAnimationEnd` on unmount
25 |
26 | ## 2.0.3 / 2023-05-08
27 |
28 | ### fix
29 |
30 | - treat `duration={0}` as if animation is not active by doing a check and returning early. This fixes a bug where NaN can cause a crash in the browser.
31 |
32 | ## 2.0.2 / 2023-02-23
33 |
34 | ### chore
35 |
36 | - upgrade `fast-equals` to latest. No breaking changes - see https://github.com/planttheidea/fast-equals/blob/master/CHANGELOG.md
37 | - don't upgrade `react-transition-group` in minor release as this requires dropping support for react <16.6
38 | - upgrade devDependencies
39 | - update babel config
40 | - update deprecated eslint parser
41 |
42 | ## 2.0.1 / 2022-06-27
43 |
44 | ### feat
45 |
46 | - feat: allow React 18
47 | - Remove raf polyfill for IE9
48 |
49 | ## 2.0.0 / 2021-03-21
50 |
51 | ### chore (#48)
52 |
53 | - Changed peerDeps to react 15,16,17
54 | - Removed karma,chai,enzyme blablabla... and used only Jest and Testing-Library.
55 | - Updated devDependencies and cleared some.
56 |
57 | ## 1.0.2 / 2018-10-02
58 |
59 | ### fix
60 |
61 | - fix babelrc
62 |
63 | ## 1.0.1 / 2018-10-02
64 |
65 | ### fix
66 |
67 | - update babel, webpack, karma, etc.
68 | - fix import error
69 |
70 | ## 1.0.0 / 2017-11-06
71 |
72 | ### feat
73 |
74 | - Support React 16
75 |
76 | ## 0.1.17 / 2016-12-02
77 |
78 | ### fix
79 |
80 | - change scripts
81 |
82 | ## 0.1.16 / 2016-11-25
83 |
84 | ### fix
85 |
86 | - update lodash
87 |
88 | ## 0.1.15 / 2016-10-31
89 |
90 | ### fix
91 |
92 | - fix isMounted to mounted
93 |
94 | ## 0.1.14 / 2016-10-28
95 |
96 | ### fix
97 |
98 | - fix: judge isMounted
99 |
100 | ## 0.1.12-0.1.13 / 2016-10-27
101 |
102 | ### fix
103 |
104 | - fix script
105 |
106 | ## 0.1.10 / 2016-07-07
107 |
108 | ### fix
109 |
110 | - add onAnimationReStart validation
111 |
112 | ## 0.1.8-0.1.9 / 2016-05-05
113 |
114 | ### feat
115 |
116 | - add onAniamtionStart prop
117 |
118 | ## 0.1.7 / 2016-04-21
119 |
120 | ### fix
121 |
122 | - fix Animate trigger animate when isActive is false
123 |
124 | ## 0.1.6 / 2016-04-15
125 |
126 | ### fix
127 |
128 | - fix Animate not pipe props when Animate not active
129 |
130 | ## 0.1.5 / 2016-04-13
131 |
132 | ### fix
133 |
134 | - remove pure-render-decorator
135 |
136 | ## 0.1.4 / 2016-04-12
137 |
138 | ### fix
139 |
140 | - change transition-group addons to dependencies
141 |
142 | ## 0.1.3 / 2016-04-12
143 |
144 | ### refactor
145 |
146 | - refactor AnimateManager
147 |
148 | ## 0.1.2 / 2016-04-12
149 |
150 | ### feat
151 |
152 | - use owe PureRender util
153 |
154 | ### fix
155 |
156 | - update react to 15.0.0
157 |
158 | ## 0.1.1 / 2016-04-05
159 |
160 | ### feat
161 |
162 | - add shouldReAnimate prop
163 |
164 | ## 0.1.0 / 2016-03-16
165 |
166 | ### feat
167 |
168 | - use webpack 2
169 |
170 | ## 0.0.13-0.0.15 / 2016-03-15
171 |
172 | ### fix
173 |
174 | - using isEqual in lodash and remove isEqual in utils
175 |
176 | ## 0.0.13-0.0.14 / 2016-03-15
177 |
178 | ### fix
179 |
180 | - fix update animation judgement
181 |
182 | ## 0.0.12 / 2016-03-15
183 |
184 | ### fix
185 |
186 | - fix compatable prefix in transition property
187 |
188 | ### refactor
189 |
190 | - refactor some function in utils
191 | - using JSX instead of createElement
192 |
193 | ## 0.0.11 / 2016-03-01
194 |
195 | ### fix
196 |
197 | - fix haven't unsubscribe handleStyleChange
198 |
199 | ## 0.0.10 / 2016-02-17
200 |
201 | ### refactor
202 |
203 | - remove lodash compose method
204 | - refactor configUpdate.js
205 |
206 | ## 0.0.9 / 2016-02-05
207 |
208 | ### fix
209 |
210 | - fix don't build on npm publish
211 |
212 | ## 0.0.8 / 2016-02-05
213 |
214 | ### fix
215 |
216 | - fix lodash minify problem
217 |
218 | ## 0.0.7 / 2016-02-04
219 |
220 | ### fix
221 |
222 | - optimize npm script commands
223 |
224 | ## 0.0.6 / 2016-02-04
225 |
226 | ### fix
227 |
228 | - set min time longer
229 |
230 | ## 0.0.5 / 2016-02-04
231 |
232 | ### fix
233 |
234 | - fix animation not valid for set css styles too quick.
235 |
236 | ## 0.0.4 / 2016-02-02
237 |
238 | ### fix
239 |
240 | - support onAnimationEnd in js animation
241 |
242 | ## 0.0.3 / 2016-02-02
243 |
244 | ### refactor
245 |
246 | - refactor the import path of lodash function
247 | - update webpack.config.js
248 |
249 | ## 0.0.2 / 2016-02-02
250 |
251 | ### feat
252 |
253 | - support js animation
254 | - support bezier and spring timing function
255 | - support group animation
256 |
257 | ## 0.0.1 / 2016-01-21
258 |
259 | - Init the project
260 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 recharts
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-smooth
2 |
3 | react-smooth is a animation library work on React.
4 |
5 | [](https://badge.fury.io/js/react-smooth)
6 | [](https://travis-ci.org/recharts/react-smooth)
7 | [](https://www.npmjs.com/package/react-smooth)
8 | [](https://gitter.im/recharts/react-smooth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
9 |
10 | ## install
11 | ```
12 | npm install --save react-smooth
13 | ```
14 |
15 | ## Usage
16 | simple animation
17 |
18 | ```jsx
19 |
20 |
21 |
22 | ```
23 | steps animation
24 |
25 | ```jsx
26 | const steps = [{
27 | style: {
28 | opacity: 0,
29 | },
30 | duration: 400,
31 | }, {
32 | style: {
33 | opacity: 1,
34 | transform: 'translate(0, 0)',
35 | },
36 | duration: 1000,
37 | }, {
38 | style: {
39 | transform: 'translate(100px, 100px)',
40 | },
41 | duration: 1200,
42 | }];
43 |
44 |
45 |
46 |
47 | ```
48 | children can be a function
49 |
50 | ```jsx
51 |
55 | {
56 | ({ opacity }) =>
57 | }
58 |
59 | ```
60 |
61 | you can configure js timing function
62 |
63 | ```js
64 | const easing = configureBezier(0.1, 0.1, 0.5, 0.8);
65 | const easing = configureSpring({ stiff: 170, damping: 20 });
66 | ```
67 |
68 | group animation
69 |
70 | ```jsx
71 | const appear = {
72 | from: 0,
73 | to: 1,
74 | attributeName: 'opacity',
75 | };
76 |
77 | const leave = {
78 | steps: [{
79 | style: {
80 | transform: 'translateX(0)',
81 | },
82 | }, {
83 | duration: 1000,
84 | style: {
85 | transform: 'translateX(300)',
86 | height: 50,
87 | },
88 | }, {
89 | duration: 2000,
90 | style: {
91 | height: 0,
92 | },
93 | }]
94 | }
95 |
96 |
97 | { list }
98 |
99 |
100 | /*
101 | * @description: add compatible prefix in style
102 | *
103 | * style = { transform: xxx, ...others };
104 | *
105 | * translatedStyle = {
106 | * WebkitTransform: xxx,
107 | * MozTransform: xxx,
108 | * OTransform: xxx,
109 | * msTransform: xxx,
110 | * ...others,
111 | * };
112 | */
113 |
114 | const translatedStyle = translateStyle(style);
115 |
116 |
117 | ```
118 |
119 | ## API
120 |
121 | ### Animate
122 |
123 |
124 |
125 |
126 | name |
127 | type |
128 | default |
129 | description |
130 |
131 |
132 |
133 |
134 | from |
135 | string or object |
136 | '' |
137 | set the initial style of the children |
138 |
139 |
140 | to |
141 | string or object |
142 | '' |
143 | set the final style of the children |
144 |
145 |
146 | canBegin |
147 | boolean |
148 | true |
149 | whether the animation is start |
150 |
151 |
152 | begin |
153 | number |
154 | 0 |
155 | animation delay time |
156 |
157 |
158 | duration |
159 | number |
160 | 1000 |
161 | animation duration |
162 |
163 |
164 | steps |
165 | array |
166 | [] |
167 | animation keyframes |
168 |
169 |
170 | onAnimationEnd |
171 | function |
172 | () => null |
173 | called when animation finished |
174 |
175 |
176 | attributeName |
177 | string |
178 | '' |
179 | style property |
180 |
181 |
182 | easing |
183 | string |
184 | 'ease' |
185 | the animation timing function, support css timing function temporary |
186 |
187 |
188 | isActive |
189 | boolean |
190 | true |
191 | whether the animation is active |
192 |
193 |
194 | children |
195 | element |
196 | |
197 | support only child temporary |
198 |
199 |
200 |
201 |
202 | ### AnimateGroup
203 |
204 |
205 |
206 |
207 | name |
208 | type |
209 | default |
210 | description |
211 |
212 |
213 |
214 |
215 | appear |
216 | object |
217 | undefined |
218 | configure element appear animation |
219 |
220 |
221 | enter |
222 | object |
223 | undefined |
224 | configure element appear animation |
225 |
226 |
227 | leave |
228 | object |
229 | undefined |
230 | configure element appear animation |
231 |
232 |
233 |
234 |
235 | ## License
236 |
237 | [MIT](http://opensource.org/licenses/MIT)
238 |
239 | Copyright (c) 2015-2021 Recharts Group
240 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const BABEL_ENV = process.env.BABEL_ENV;
2 |
3 | const plugins = [
4 | '@babel/plugin-proposal-export-default-from',
5 | '@babel/plugin-proposal-export-namespace-from',
6 | ['@babel/plugin-proposal-decorators', { version: '2023-01' }],
7 | ['@babel/plugin-proposal-class-properties'],
8 | '@babel/plugin-proposal-object-rest-spread',
9 | ];
10 |
11 | if (BABEL_ENV === 'umd') {
12 | plugins.push('@babel/plugin-external-helpers');
13 | }
14 |
15 | // eslint-disable-next-line no-nested-ternary
16 | const babelModules = BABEL_ENV === 'commonjs' ? 'commonjs' : BABEL_ENV === 'test' ? 'auto' : false;
17 |
18 | module.exports = {
19 | plugins,
20 | presets: [
21 | [
22 | '@babel/preset-env',
23 | {
24 | modules: babelModules,
25 | targets: {
26 | browsers: ['last 2 versions'],
27 | },
28 | },
29 | ],
30 | '@babel/preset-react',
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/demo/Gossip/app.js:
--------------------------------------------------------------------------------
1 | import Animate from 'react-smooth';
2 | import React, { Component } from 'react';
3 | import ReactDom from 'react-dom';
4 |
5 | const getSTEPS = onAnimationEnd => [{
6 | duration: 1000,
7 | style: {
8 | opacity: 0,
9 | },
10 | }, {
11 | duration: 1000,
12 | style: {
13 | opacity: 1,
14 | transformOrigin: '110px 110px',
15 | transform: 'rotate(0deg) translate(0px, 0px)',
16 | },
17 | easing: 'ease-in',
18 | }, {
19 | duration: 1000,
20 | style: {
21 | transform: 'rotate(500deg) translate(0px, 0px)',
22 | },
23 | easing: 'ease-in-out',
24 | }, {
25 | duration: 2000,
26 | style: {
27 | transformOrigin: '610px 610px',
28 | transform: 'rotate(1440deg) translate(500px, 500px)',
29 | },
30 | }, {
31 | duration: 50,
32 | style: {
33 | transformOrigin: 'center center',
34 | transform: 'translate(500px, 500px) scale(1)',
35 | },
36 | onAnimationEnd,
37 | }, {
38 | duration: 1000,
39 | style: {
40 | transformOrigin: 'center center',
41 | transform: 'translate(500px, 500px) scale(1.6)',
42 | },
43 | }];
44 |
45 | const createPoint = (x, y) => {
46 | const currX = x;
47 | const currY = y;
48 |
49 | return {
50 | getPath: cmd => [cmd, currX, currY].join(' '),
51 | getCircle: props => ,
52 | x: currX,
53 | y: currY,
54 | };
55 | };
56 |
57 | const getArcPath = (radius, rotation, isLarge, isSweep, dx, dy) => {
58 | return ['A', radius, radius, rotation, isLarge, isSweep, dx, dy].join(' ');
59 | };
60 |
61 | class Gossip extends Component {
62 | static displayName = 'Gossip';
63 |
64 | constructor(props, ctx) {
65 | super(props, ctx);
66 |
67 | this.state = { canBegin: false };
68 | this.handleTextAniamtionBegin = this.handleTextAniamtionBegin.bind(this);
69 | this.STEPS = getSTEPS(this.handleTextAniamtionBegin);
70 | }
71 |
72 | handleTextAniamtionBegin() {
73 | this.setState({
74 | canBegin: true,
75 | });
76 | }
77 |
78 | renderPath() {
79 | const cx = 110;
80 | const cy = 110;
81 | const r = 100;
82 | const sr = r / 2;
83 |
84 | const beginPoint = createPoint(cx, cy - r);
85 | const endPoint = createPoint(cx, cy + r);
86 | const move = beginPoint.getPath('M');
87 | const A = getArcPath(sr, 0, 0, 0, cx, cy);
88 | const A2 = getArcPath(sr, 0, 0, 1, endPoint.x, endPoint.y);
89 | const A3 = getArcPath(r, 0, 0, 1, beginPoint.x, beginPoint.y);
90 |
91 | return ;
92 | }
93 |
94 | renderSmallCircles() {
95 | const cx = 110;
96 | const cy = 110;
97 | const r = 100;
98 | const sr = r / 2;
99 | const tr = 5;
100 |
101 | const centers = [createPoint(cx, cy - sr), createPoint(cx, cy + sr)];
102 | const circles = centers.map((p, i) =>
103 | p.getCircle({
104 | r: tr,
105 | fill: i ? 'white' : 'black',
106 | key: i,
107 | })
108 | );
109 |
110 | return {circles};
111 | }
112 |
113 | renderText() {
114 | return (
115 |
121 |
122 | May you no bug this year
123 |
124 |
125 | );
126 | }
127 |
128 | render() {
129 | return (
130 |
140 | );
141 | }
142 | }
143 |
144 | ReactDom.render(, document.getElementById('app'));
145 |
--------------------------------------------------------------------------------
/demo/Gossip/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Gossip
6 |
7 |
8 | back
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/GroupAnimation/app.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDom from 'react-dom';
3 | import { AnimateGroup } from 'react-smooth';
4 |
5 | class GroupAnimation extends Component {
6 | state = {
7 | list: [{
8 | text: 'first...',
9 | }, {
10 | text: 'second...',
11 | }, {
12 | text: 'third...',
13 | }],
14 | };
15 |
16 | handleDel(index) {
17 | const { list } = this.state;
18 |
19 | this.setState({
20 | list: [...list.slice(0, index), ...list.slice(index + 1, list.length)],
21 | });
22 | }
23 |
24 | renderList() {
25 | const { list } = this.state;
26 |
27 | const items = list.map((item, index) => {
28 | const requestDel = this.handleDel.bind(this, index);
29 |
30 | return (
31 |
67 | );
68 | });
69 |
70 | const leaveSteps = [{
71 | duration: 0,
72 | style: {
73 | transform: 'translateX(0)',
74 | },
75 | }, {
76 | duration: 1000,
77 | style: {
78 | transform: 'translateX(302px)',
79 | height: 50,
80 | },
81 | }, {
82 | duration: 1000,
83 | style: {
84 | height: 0,
85 | },
86 | }];
87 |
88 | return (
89 |
90 | { items }
91 |
92 | );
93 | }
94 |
95 | render() {
96 | return (
97 |
104 | {this.renderList()}
105 |
106 | );
107 | }
108 | }
109 |
110 | ReactDom.render(, document.getElementById('app'));
111 |
--------------------------------------------------------------------------------
/demo/GroupAnimation/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Group Animation
6 |
7 |
8 | back
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/Pendulum/app.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import ReactDom from 'react-dom';
4 | import { translateStyle } from 'react-smooth';
5 |
6 | const g = 9.8;
7 |
8 | function Circle(props) {
9 | const { r, currTheta, ropeLength, ...others } = props;
10 | const cx = (ropeLength - r) * Math.sin(currTheta) + ropeLength - r;
11 | const cy = (ropeLength - r) * Math.cos(currTheta) - r;
12 | const translate = `translate(${cx}px, ${cy}px)`;
13 |
14 | const style = {
15 | width: 2 * r,
16 | height: 2 * r,
17 | borderRadius: r,
18 | transform: translate,
19 | WebkitTransform: translate,
20 | background: `radial-gradient(circle at ${r * 2 / 3}px ${r * 2 / 3}px,#5cabff,#000)`,
21 | position: 'absolute',
22 | top: 0,
23 | left: 0,
24 | };
25 |
26 | return (
27 |
32 | );
33 | }
34 |
35 | Circle.prototype.propTypes = {
36 | r: PropTypes.number,
37 | currTheta: PropTypes.number,
38 | ropeLength: PropTypes.number,
39 | };
40 |
41 | function Line(props) {
42 | const { ropeLength, currTheta } = props;
43 | const x1 = ropeLength;
44 | const x2 = x1;
45 | const y2 = ropeLength;
46 |
47 | return (
48 |
67 | );
68 | }
69 |
70 | Line.prototype.propTypes = {
71 | ropeLength: PropTypes.number,
72 | currTheta: PropTypes.number,
73 | };
74 |
75 | class Pendulum extends Component {
76 | static propTypes = {
77 | ropeLength: PropTypes.number,
78 | theta: PropTypes.number,
79 | radius: PropTypes.number,
80 | };
81 |
82 | state = {
83 | currTheta: this.props.theta,
84 | };
85 |
86 | componentDidMount() {
87 | this.cafId = requestAnimationFrame(this.update.bind(this));
88 | }
89 |
90 | componentWillUnmount() {
91 | if (this.cafId) {
92 | cancelAnimationFrame(this.cafId);
93 | }
94 | }
95 |
96 | update(now) {
97 | if (!this.initialTime) {
98 | this.initialTime = now;
99 |
100 | this.cafId = requestAnimationFrame(this.update.bind(this));
101 | }
102 |
103 | const { ropeLength, theta } = this.props;
104 | const { currTheta } = this.state;
105 | const A = theta;
106 | const omiga = Math.sqrt(g / ropeLength * 200);
107 |
108 | this.setState({
109 | currTheta: theta * Math.cos(omiga * (now - this.initialTime) / 1000),
110 | });
111 |
112 | this.cafId = requestAnimationFrame(this.update.bind(this));
113 | }
114 |
115 | render() {
116 | const { currTheta } = this.state;
117 | const { ropeLength, radius } = this.props;
118 |
119 | return (
120 |
121 |
122 |
123 |
124 | );
125 | }
126 | }
127 |
128 | class App extends Component {
129 | state = {
130 | ropeLength: 300,
131 | theta: 18,
132 | };
133 |
134 | handleThetaChange(e) {
135 | this.setState({
136 | theta: e.target.value,
137 | });
138 | }
139 |
140 | handleRopeChange(e) {
141 | this.setState({
142 | ropeLength: e.target.value,
143 | });
144 | }
145 |
146 | render() {
147 | const { theta, ropeLength } = this.state;
148 |
149 | return (
150 |
160 | );
161 | }
162 | }
163 |
164 | ReactDom.render(, document.getElementById('app'));
165 |
--------------------------------------------------------------------------------
/demo/Pendulum/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pendulum
6 |
7 |
8 | back
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/SimpleAnimation/app.js:
--------------------------------------------------------------------------------
1 | import Animate from 'react-smooth';
2 | import React, { Component } from 'react';
3 | import ReactDom from 'react-dom';
4 |
5 | class Simple extends Component {
6 | state = {
7 | to: 100,
8 | };
9 |
10 | handleClick = () => {
11 | this.setState({
12 | to: this.state.to + 100,
13 | });
14 | }
15 |
16 | render() {
17 | return (
18 |
19 |
21 |
22 | {({ y }) => (
23 |
30 | )}
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | ReactDom.render(, document.getElementById('app'));
39 |
--------------------------------------------------------------------------------
/demo/SimpleAnimation/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Simple
6 |
7 |
8 | back
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | mode: 'development',
5 | devtool: 'inline-source-map',
6 | entry: {
7 | Gossip: path.join(__dirname, '/Gossip/app.js'),
8 | GroupAnimation: path.join(__dirname, '/GroupAnimation/app.js'),
9 | SimpleAnimation: path.join(__dirname, '/SimpleAnimation/app.js'),
10 | Pendulum: path.join(__dirname, '/Pendulum/app.js'),
11 | },
12 | output: {
13 | path: __dirname,
14 | filename: '[name]/build.js',
15 | },
16 | resolve: {
17 | alias: {
18 | 'react-smooth': path.join(__dirname, '../src/index.js'),
19 | },
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.js$/,
25 | loader: 'babel-loader',
26 | include: [
27 | __dirname,
28 | path.join(__dirname, '../src'),
29 | path.join(__dirname, '../node_modules'),
30 | ],
31 | },
32 | ],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | testEnvironment: 'jsdom',
4 | verbose: true,
5 | };
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-smooth",
3 | "version": "4.0.4",
4 | "description": "react animation library",
5 | "main": "lib/index",
6 | "module": "es6/index",
7 | "files": [
8 | "*.md",
9 | "es6",
10 | "lib",
11 | "umd",
12 | "src"
13 | ],
14 | "keywords": [
15 | "react",
16 | "reactjs",
17 | "animation",
18 | "react-component"
19 | ],
20 | "scripts": {
21 | "build": "npm run build-cjs && npm run build-es6 && rimraf umd && npm run build-umd && npm run build-min",
22 | "build-cjs": "rimraf lib && cross-env BABEL_ENV=commonjs babel ./src -d lib",
23 | "build-es6": "rimraf es6 && babel ./src -d es6",
24 | "build-umd": "cross-env NODE_ENV=development BABEL_ENV=commonjs webpack --entry ./src/index.js -o umd",
25 | "build-min": "cross-env NODE_ENV=production BABEL_ENV=commonjs webpack --entry ./src/index.js -o umd",
26 | "test": "cross-env BABEL_ENV=test jest",
27 | "demo": "webpack serve --config demo/webpack.config.js --port 4000 --host 127.0.0.1 --progress --profile --content-base demo/",
28 | "autofix": "eslint src --fix",
29 | "lint": "eslint src"
30 | },
31 | "pre-commit": [
32 | "lint"
33 | ],
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/recharts/react-smooth.git"
37 | },
38 | "author": "JasonHzq",
39 | "bugs": {
40 | "url": "https://github.com/recharts/react-smooth/issues"
41 | },
42 | "homepage": "https://github.com/recharts/react-smooth#readme",
43 | "peerDependencies": {
44 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
45 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
46 | },
47 | "dependencies": {
48 | "prop-types": "^15.8.1",
49 | "react-fast-compare": "^3.2.2",
50 | "react-transition-group": "^4.4.5"
51 | },
52 | "devDependencies": {
53 | "@babel/cli": "^7.23.0",
54 | "@babel/core": "^7.23.2",
55 | "@babel/eslint-parser": "^7.22.15",
56 | "@babel/plugin-proposal-class-properties": "^7.18.6",
57 | "@babel/plugin-proposal-decorators": "^7.23.2",
58 | "@babel/plugin-proposal-export-default-from": "^7.22.17",
59 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
60 | "@babel/plugin-proposal-function-bind": "^7.22.5",
61 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
62 | "@babel/plugin-transform-runtime": "^7.23.2",
63 | "@babel/preset-env": "^7.23.2",
64 | "@babel/preset-react": "^7.22.15",
65 | "@babel/runtime": "^7.23.2",
66 | "@testing-library/dom": "^9.3.3",
67 | "@testing-library/jest-dom": "^5.17.0",
68 | "@testing-library/react": "^14.0.0",
69 | "babel-loader": "^9.1.3",
70 | "core-js": "^3.33.0",
71 | "cross-env": "^7.0.3",
72 | "eslint": "^8.51.0",
73 | "eslint-config-airbnb": "^19.0.4",
74 | "eslint-config-prettier": "^7.2.0",
75 | "eslint-plugin-import": "^2.28.1",
76 | "eslint-plugin-jsx-a11y": "^6.7.1",
77 | "eslint-plugin-prettier": "^3.4.1",
78 | "eslint-plugin-react": "^7.33.2",
79 | "jest": "^29.7.0",
80 | "jest-environment-jsdom": "^29.7.0",
81 | "json-loader": "^0.5.7",
82 | "pre-commit": "^1.2.2",
83 | "prettier": "^2.8.8",
84 | "react": "^18.2.0",
85 | "react-dom": "^18.2.0",
86 | "webpack": "^5.89.0",
87 | "webpack-bundle-analyzer": "^4.9.1",
88 | "webpack-cli": "^5.1.4",
89 | "webpack-dev-server": "^4.15.1"
90 | },
91 | "sideEffects": false,
92 | "license": "MIT"
93 | }
94 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | var execSync = require('child_process').execSync;
2 |
3 | function exec(command) {
4 | execSync(command, { stdio: [0, 1, 2] });
5 | }
6 |
7 | exec('npm run build');
8 | exec('npm run build-umd');
9 | exec('npm run build-min');
10 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 | export RELEASE=1
3 |
4 | if ! [ -e scripts/release.sh ]; then
5 | echo >&2 "Please run scripts/release.sh from the repo root"
6 | exit 1
7 | fi
8 |
9 | update_version() {
10 | echo "$(node -p "p=require('./${1}');p.version='${2}';JSON.stringify(p,null,2)")" > $1
11 | echo "Updated ${1} version to ${2}"
12 | }
13 |
14 | validate_semver() {
15 | if ! [[ $1 =~ ^[0-9]\.[0-9]+\.[0-9](-.+)? ]]; then
16 | echo >&2 "Version $1 is not valid! It must be a valid semver string like 1.0.2 or 2.3.0-beta.1"
17 | exit 1
18 | fi
19 | }
20 |
21 | current_version=$(node -p "require('./package').version")
22 |
23 | printf "Next version (current is $current_version)? "
24 | read next_version
25 |
26 | validate_semver $next_version
27 |
28 | next_ref="v$next_version"
29 |
30 | npm test
31 |
32 | update_version 'package.json' $next_version
33 |
34 | git commit -am "Version $next_version"
35 |
36 | # push first to make sure we're up-to-date
37 | git push origin master
38 |
39 | git tag $next_ref
40 | #git tag latest -f
41 |
42 | git push origin $next_ref
43 | #git push origin latest -f
44 |
45 | node scripts/build.js
46 |
47 | npm publish
48 |
--------------------------------------------------------------------------------
/src/Animate.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, cloneElement, Children } from 'react';
2 | import PropTypes from 'prop-types';
3 | import deepEqual from 'react-fast-compare';
4 | import createAnimateManager from './AnimateManager';
5 | import { configEasing } from './easing';
6 | import configUpdate from './configUpdate';
7 | import { getTransitionVal, identity } from './util';
8 |
9 | class Animate extends PureComponent {
10 | constructor(props, context) {
11 | super(props, context);
12 |
13 | const { isActive, attributeName, from, to, steps, children, duration } = this.props;
14 |
15 | this.handleStyleChange = this.handleStyleChange.bind(this);
16 | this.changeStyle = this.changeStyle.bind(this);
17 |
18 | if (!isActive || duration <= 0) {
19 | this.state = { style: {} };
20 |
21 | // if children is a function and animation is not active, set style to 'to'
22 | if (typeof children === 'function') {
23 | this.state = { style: to };
24 | }
25 |
26 | return;
27 | }
28 |
29 | if (steps && steps.length) {
30 | this.state = { style: steps[0].style };
31 | } else if (from) {
32 | if (typeof children === 'function') {
33 | this.state = {
34 | style: from,
35 | };
36 |
37 | return;
38 | }
39 | this.state = {
40 | style: attributeName ? { [attributeName]: from } : from,
41 | };
42 | } else {
43 | this.state = { style: {} };
44 | }
45 | }
46 |
47 | componentDidMount() {
48 | const { isActive, canBegin } = this.props;
49 |
50 | this.mounted = true;
51 |
52 | if (!isActive || !canBegin) {
53 | return;
54 | }
55 |
56 | this.runAnimation(this.props);
57 | }
58 |
59 | componentDidUpdate(prevProps) {
60 | const { isActive, canBegin, attributeName, shouldReAnimate, to, from: currentFrom } = this.props;
61 | const { style } = this.state;
62 |
63 | if (!canBegin) {
64 | return;
65 | }
66 |
67 | if (!isActive) {
68 | const newState = {
69 | style: attributeName ? { [attributeName]: to } : to,
70 | };
71 | if (this.state && style) {
72 | if ((attributeName && style[attributeName] !== to) || (!attributeName && style !== to)) {
73 | // eslint-disable-next-line react/no-did-update-set-state
74 | this.setState(newState);
75 | }
76 | }
77 | return;
78 | }
79 |
80 | if (deepEqual(prevProps.to, to) && prevProps.canBegin && prevProps.isActive) {
81 | return;
82 | }
83 |
84 | const isTriggered = !prevProps.canBegin || !prevProps.isActive;
85 |
86 | if (this.manager) {
87 | this.manager.stop();
88 | }
89 |
90 | if (this.stopJSAnimation) {
91 | this.stopJSAnimation();
92 | }
93 |
94 | const from = isTriggered || shouldReAnimate ? currentFrom : prevProps.to;
95 |
96 | if (this.state && style) {
97 | const newState = {
98 | style: attributeName ? { [attributeName]: from } : from,
99 | };
100 | if ((attributeName && style[attributeName] !== from) || (!attributeName && style !== from)) {
101 | // eslint-disable-next-line react/no-did-update-set-state
102 | this.setState(newState);
103 | }
104 | }
105 |
106 | this.runAnimation({
107 | ...this.props,
108 | from,
109 | begin: 0,
110 | });
111 | }
112 |
113 | componentWillUnmount() {
114 | this.mounted = false;
115 | const { onAnimationEnd } = this.props;
116 |
117 | if (this.unSubscribe) {
118 | this.unSubscribe();
119 | }
120 |
121 | if (this.manager) {
122 | this.manager.stop();
123 | this.manager = null;
124 | }
125 |
126 | if (this.stopJSAnimation) {
127 | this.stopJSAnimation();
128 | }
129 |
130 | if (onAnimationEnd) {
131 | onAnimationEnd();
132 | }
133 | }
134 |
135 | handleStyleChange(style) {
136 | this.changeStyle(style);
137 | }
138 |
139 | changeStyle(style) {
140 | if (this.mounted) {
141 | this.setState({
142 | style,
143 | });
144 | }
145 | }
146 |
147 | runJSAnimation(props) {
148 | const { from, to, duration, easing, begin, onAnimationEnd, onAnimationStart } = props;
149 | const startAnimation = configUpdate(from, to, configEasing(easing), duration, this.changeStyle);
150 |
151 | const finalStartAnimation = () => {
152 | this.stopJSAnimation = startAnimation();
153 | };
154 |
155 | this.manager.start([onAnimationStart, begin, finalStartAnimation, duration, onAnimationEnd]);
156 | }
157 |
158 | runStepAnimation(props) {
159 | const { steps, begin, onAnimationStart } = props;
160 | const { style: initialStyle, duration: initialTime = 0 } = steps[0];
161 |
162 | const addStyle = (sequence, nextItem, index) => {
163 | if (index === 0) {
164 | return sequence;
165 | }
166 |
167 | const { duration, easing = 'ease', style, properties: nextProperties, onAnimationEnd } = nextItem;
168 |
169 | const preItem = index > 0 ? steps[index - 1] : nextItem;
170 | const properties = nextProperties || Object.keys(style);
171 |
172 | if (typeof easing === 'function' || easing === 'spring') {
173 | return [
174 | ...sequence,
175 | this.runJSAnimation.bind(this, {
176 | from: preItem.style,
177 | to: style,
178 | duration,
179 | easing,
180 | }),
181 | duration,
182 | ];
183 | }
184 |
185 | const transition = getTransitionVal(properties, duration, easing);
186 | const newStyle = {
187 | ...preItem.style,
188 | ...style,
189 | transition,
190 | };
191 |
192 | return [...sequence, newStyle, duration, onAnimationEnd].filter(identity);
193 | };
194 |
195 | return this.manager.start([
196 | onAnimationStart,
197 | ...steps.reduce(addStyle, [initialStyle, Math.max(initialTime, begin)]),
198 | props.onAnimationEnd,
199 | ]);
200 | }
201 |
202 | runAnimation(props) {
203 | if (!this.manager) {
204 | this.manager = createAnimateManager();
205 | }
206 | const {
207 | begin,
208 | duration,
209 | attributeName,
210 | to: propsTo,
211 | easing,
212 | onAnimationStart,
213 | onAnimationEnd,
214 | steps,
215 | children,
216 | } = props;
217 |
218 | const manager = this.manager;
219 |
220 | this.unSubscribe = manager.subscribe(this.handleStyleChange);
221 |
222 | if (typeof easing === 'function' || typeof children === 'function' || easing === 'spring') {
223 | this.runJSAnimation(props);
224 | return;
225 | }
226 |
227 | if (steps.length > 1) {
228 | this.runStepAnimation(props);
229 | return;
230 | }
231 |
232 | const to = attributeName ? { [attributeName]: propsTo } : propsTo;
233 | const transition = getTransitionVal(Object.keys(to), duration, easing);
234 |
235 | manager.start([onAnimationStart, begin, { ...to, transition }, duration, onAnimationEnd]);
236 | }
237 |
238 | render() {
239 | const {
240 | children,
241 | begin,
242 | duration,
243 | attributeName,
244 | easing,
245 | isActive,
246 | steps,
247 | from,
248 | to,
249 | canBegin,
250 | onAnimationEnd,
251 | shouldReAnimate,
252 | onAnimationReStart,
253 | ...others
254 | } = this.props;
255 | const count = Children.count(children);
256 | // eslint-disable-next-line react/destructuring-assignment
257 | const stateStyle = this.state.style;
258 |
259 | if (typeof children === 'function') {
260 | return children(stateStyle);
261 | }
262 |
263 | if (!isActive || count === 0 || duration <= 0) {
264 | return children;
265 | }
266 |
267 | const cloneContainer = container => {
268 | const { style = {}, className } = container.props;
269 |
270 | const res = cloneElement(container, {
271 | ...others,
272 | style: {
273 | ...style,
274 | ...stateStyle,
275 | },
276 | className,
277 | });
278 | return res;
279 | };
280 |
281 | if (count === 1) {
282 | return cloneContainer(Children.only(children));
283 | }
284 |
285 | return {Children.map(children, child => cloneContainer(child))}
;
286 | }
287 | }
288 |
289 | Animate.displayName = 'Animate';
290 |
291 | Animate.defaultProps = {
292 | begin: 0,
293 | duration: 1000,
294 | from: '',
295 | to: '',
296 | attributeName: '',
297 | easing: 'ease',
298 | isActive: true,
299 | canBegin: true,
300 | steps: [],
301 | onAnimationEnd: () => {},
302 | onAnimationStart: () => {},
303 | };
304 |
305 | Animate.propTypes = {
306 | from: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
307 | to: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
308 | attributeName: PropTypes.string,
309 | // animation duration
310 | duration: PropTypes.number,
311 | begin: PropTypes.number,
312 | easing: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
313 | steps: PropTypes.arrayOf(
314 | PropTypes.shape({
315 | duration: PropTypes.number.isRequired,
316 | style: PropTypes.object.isRequired,
317 | easing: PropTypes.oneOfType([
318 | PropTypes.oneOf(['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear']),
319 | PropTypes.func,
320 | ]),
321 | // transition css properties(dash case), optional
322 | properties: PropTypes.arrayOf('string'),
323 | onAnimationEnd: PropTypes.func,
324 | }),
325 | ),
326 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
327 | isActive: PropTypes.bool,
328 | canBegin: PropTypes.bool,
329 | onAnimationEnd: PropTypes.func,
330 | // decide if it should reanimate with initial from style when props change
331 | shouldReAnimate: PropTypes.bool,
332 | onAnimationStart: PropTypes.func,
333 | onAnimationReStart: PropTypes.func,
334 | };
335 |
336 | export default Animate;
337 |
--------------------------------------------------------------------------------
/src/AnimateGroup.js:
--------------------------------------------------------------------------------
1 | import React, { Children } from 'react';
2 | import { TransitionGroup } from 'react-transition-group';
3 | import PropTypes from 'prop-types';
4 | import AnimateGroupChild from './AnimateGroupChild';
5 |
6 | function AnimateGroup(props) {
7 | const { component, children, appear, enter, leave } = props;
8 |
9 | return (
10 |
11 | {Children.map(children, (child, index) => (
12 |
18 | {child}
19 |
20 | ))}
21 |
22 | );
23 | }
24 |
25 | AnimateGroup.propTypes = {
26 | appear: PropTypes.object,
27 | enter: PropTypes.object,
28 | leave: PropTypes.object,
29 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]),
30 | component: PropTypes.any,
31 | };
32 |
33 | AnimateGroup.defaultProps = {
34 | component: 'span',
35 | };
36 |
37 | export default AnimateGroup;
38 |
--------------------------------------------------------------------------------
/src/AnimateGroupChild.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Children } from 'react';
2 | import { Transition } from 'react-transition-group';
3 | import PropTypes from 'prop-types';
4 | import Animate from './Animate';
5 |
6 | const parseDurationOfSingleTransition = (options = {}) => {
7 | const { steps, duration } = options;
8 |
9 | if (steps && steps.length) {
10 | return steps.reduce(
11 | (result, entry) => result + (Number.isFinite(entry.duration) && entry.duration > 0 ? entry.duration : 0),
12 | 0,
13 | );
14 | }
15 |
16 | if (Number.isFinite(duration)) {
17 | return duration;
18 | }
19 |
20 | return 0;
21 | };
22 |
23 | class AnimateGroupChild extends Component {
24 | constructor() {
25 | super();
26 | this.state = {
27 | isActive: false,
28 | };
29 | }
30 |
31 | handleStyleActive(style) {
32 | if (style) {
33 | const onAnimationEnd = style.onAnimationEnd
34 | ? () => {
35 | style.onAnimationEnd();
36 | }
37 | : null;
38 |
39 | this.setState({
40 | ...style,
41 | onAnimationEnd,
42 | isActive: true,
43 | });
44 | }
45 | }
46 |
47 | handleEnter = (node, isAppearing) => {
48 | const { appearOptions, enterOptions } = this.props;
49 |
50 | this.handleStyleActive(isAppearing ? appearOptions : enterOptions);
51 | };
52 |
53 | handleExit = () => {
54 | const { leaveOptions } = this.props;
55 | this.handleStyleActive(leaveOptions);
56 | };
57 |
58 | parseTimeout() {
59 | const { appearOptions, enterOptions, leaveOptions } = this.props;
60 |
61 | return (
62 | parseDurationOfSingleTransition(appearOptions) +
63 | parseDurationOfSingleTransition(enterOptions) +
64 | parseDurationOfSingleTransition(leaveOptions)
65 | );
66 | }
67 |
68 | render() {
69 | const { children, appearOptions, enterOptions, leaveOptions, ...props } = this.props;
70 |
71 | return (
72 |
73 | {() => {Children.only(children)}}
74 |
75 | );
76 | }
77 | }
78 |
79 | AnimateGroupChild.propTypes = {
80 | appearOptions: PropTypes.object,
81 | enterOptions: PropTypes.object,
82 | leaveOptions: PropTypes.object,
83 | children: PropTypes.element,
84 | };
85 |
86 | export default AnimateGroupChild;
87 |
--------------------------------------------------------------------------------
/src/AnimateManager.js:
--------------------------------------------------------------------------------
1 | import setRafTimeout from './setRafTimeout';
2 |
3 | export default function createAnimateManager() {
4 | let currStyle = {};
5 | let handleChange = () => null;
6 | let shouldStop = false;
7 |
8 | const setStyle = _style => {
9 | if (shouldStop) {
10 | return;
11 | }
12 |
13 | if (Array.isArray(_style)) {
14 | if (!_style.length) {
15 | return;
16 | }
17 |
18 | const styles = _style;
19 | const [curr, ...restStyles] = styles;
20 |
21 | if (typeof curr === 'number') {
22 | setRafTimeout(setStyle.bind(null, restStyles), curr);
23 |
24 | return;
25 | }
26 |
27 | setStyle(curr);
28 | setRafTimeout(setStyle.bind(null, restStyles));
29 | return;
30 | }
31 |
32 | if (typeof _style === 'object') {
33 | currStyle = _style;
34 | handleChange(currStyle);
35 | }
36 |
37 | if (typeof _style === 'function') {
38 | _style();
39 | }
40 | };
41 |
42 | return {
43 | stop: () => {
44 | shouldStop = true;
45 | },
46 | start: style => {
47 | shouldStop = false;
48 | setStyle(style);
49 | },
50 | subscribe: _handleChange => {
51 | handleChange = _handleChange;
52 |
53 | return () => {
54 | handleChange = () => null;
55 | };
56 | },
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/configUpdate.js:
--------------------------------------------------------------------------------
1 | import { getIntersectionKeys, mapObject } from './util';
2 |
3 | const alpha = (begin, end, k) => begin + (end - begin) * k;
4 | const needContinue = ({ from, to }) => from !== to;
5 |
6 | /*
7 | * @description: cal new from value and velocity in each stepper
8 | * @return: { [styleProperty]: { from, to, velocity } }
9 | */
10 | const calStepperVals = (easing, preVals, steps) => {
11 | const nextStepVals = mapObject((key, val) => {
12 | if (needContinue(val)) {
13 | const [newX, newV] = easing(val.from, val.to, val.velocity);
14 | return {
15 | ...val,
16 | from: newX,
17 | velocity: newV,
18 | };
19 | }
20 |
21 | return val;
22 | }, preVals);
23 |
24 | if (steps < 1) {
25 | return mapObject((key, val) => {
26 | if (needContinue(val)) {
27 | return {
28 | ...val,
29 | velocity: alpha(val.velocity, nextStepVals[key].velocity, steps),
30 | from: alpha(val.from, nextStepVals[key].from, steps),
31 | };
32 | }
33 |
34 | return val;
35 | }, preVals);
36 | }
37 |
38 | return calStepperVals(easing, nextStepVals, steps - 1);
39 | };
40 |
41 | // configure update function
42 | export default (from, to, easing, duration, render) => {
43 | const interKeys = getIntersectionKeys(from, to);
44 | const timingStyle = interKeys.reduce(
45 | (res, key) => ({
46 | ...res,
47 | [key]: [from[key], to[key]],
48 | }),
49 | {},
50 | );
51 |
52 | let stepperStyle = interKeys.reduce(
53 | (res, key) => ({
54 | ...res,
55 | [key]: {
56 | from: from[key],
57 | velocity: 0,
58 | to: to[key],
59 | },
60 | }),
61 | {},
62 | );
63 | let cafId = -1;
64 | let preTime;
65 | let beginTime;
66 | let update = () => null;
67 |
68 | const getCurrStyle = () => mapObject((key, val) => val.from, stepperStyle);
69 | const shouldStopAnimation = () => !Object.values(stepperStyle).filter(needContinue).length;
70 |
71 | // stepper timing function like spring
72 | const stepperUpdate = now => {
73 | if (!preTime) {
74 | preTime = now;
75 | }
76 | const deltaTime = now - preTime;
77 | const steps = deltaTime / easing.dt;
78 |
79 | stepperStyle = calStepperVals(easing, stepperStyle, steps);
80 | // get union set and add compatible prefix
81 | render({
82 | ...from,
83 | ...to,
84 | ...getCurrStyle(stepperStyle),
85 | });
86 |
87 | preTime = now;
88 |
89 | if (!shouldStopAnimation()) {
90 | cafId = requestAnimationFrame(update);
91 | }
92 | };
93 |
94 | // t => val timing function like cubic-bezier
95 | const timingUpdate = now => {
96 | if (!beginTime) {
97 | beginTime = now;
98 | }
99 |
100 | const t = (now - beginTime) / duration;
101 | const currStyle = mapObject((key, val) => alpha(...val, easing(t)), timingStyle);
102 |
103 | // get union set and add compatible prefix
104 | render({
105 | ...from,
106 | ...to,
107 | ...currStyle,
108 | });
109 |
110 | if (t < 1) {
111 | cafId = requestAnimationFrame(update);
112 | } else {
113 | const finalStyle = mapObject((key, val) => alpha(...val, easing(1)), timingStyle);
114 |
115 | render({
116 | ...from,
117 | ...to,
118 | ...finalStyle,
119 | });
120 | }
121 | };
122 |
123 | update = easing.isStepper ? stepperUpdate : timingUpdate;
124 |
125 | // return start animation method
126 | return () => {
127 | requestAnimationFrame(update);
128 |
129 | // return stop animation method
130 | return () => {
131 | cancelAnimationFrame(cafId);
132 | };
133 | };
134 | };
135 |
--------------------------------------------------------------------------------
/src/easing.js:
--------------------------------------------------------------------------------
1 | import { warn } from './util';
2 |
3 | const ACCURACY = 1e-4;
4 |
5 | const cubicBezierFactor = (c1, c2) => [0, 3 * c1, 3 * c2 - 6 * c1, 3 * c1 - 3 * c2 + 1];
6 |
7 | const multyTime = (params, t) => params.map((param, i) => param * t ** i).reduce((pre, curr) => pre + curr);
8 |
9 | const cubicBezier = (c1, c2) => t => {
10 | const params = cubicBezierFactor(c1, c2);
11 |
12 | return multyTime(params, t);
13 | };
14 |
15 | const derivativeCubicBezier = (c1, c2) => t => {
16 | const params = cubicBezierFactor(c1, c2);
17 | const newParams = [...params.map((param, i) => param * i).slice(1), 0];
18 |
19 | return multyTime(newParams, t);
20 | };
21 |
22 | // calculate cubic-bezier using Newton's method
23 | export const configBezier = (...args) => {
24 | let [x1, y1, x2, y2] = args;
25 |
26 | if (args.length === 1) {
27 | switch (args[0]) {
28 | case 'linear':
29 | [x1, y1, x2, y2] = [0.0, 0.0, 1.0, 1.0];
30 | break;
31 | case 'ease':
32 | [x1, y1, x2, y2] = [0.25, 0.1, 0.25, 1.0];
33 | break;
34 | case 'ease-in':
35 | [x1, y1, x2, y2] = [0.42, 0.0, 1.0, 1.0];
36 | break;
37 | case 'ease-out':
38 | [x1, y1, x2, y2] = [0.42, 0.0, 0.58, 1.0];
39 | break;
40 | case 'ease-in-out':
41 | [x1, y1, x2, y2] = [0.0, 0.0, 0.58, 1.0];
42 | break;
43 | default: {
44 | const easing = args[0].split('(');
45 | if (easing[0] === 'cubic-bezier' && easing[1].split(')')[0].split(',').length === 4) {
46 | [x1, y1, x2, y2] = easing[1]
47 | .split(')')[0]
48 | .split(',')
49 | .map(x => parseFloat(x));
50 | } else {
51 | warn(
52 | false,
53 | '[configBezier]: arguments should be one of ' +
54 | "oneOf 'linear', 'ease', 'ease-in', 'ease-out', " +
55 | "'ease-in-out','cubic-bezier(x1,y1,x2,y2)', instead received %s",
56 | args,
57 | );
58 | }
59 | }
60 | }
61 | }
62 |
63 | warn(
64 | [x1, x2, y1, y2].every(num => typeof num === 'number' && num >= 0 && num <= 1),
65 | '[configBezier]: arguments should be x1, y1, x2, y2 of [0, 1] instead received %s',
66 | args,
67 | );
68 |
69 | const curveX = cubicBezier(x1, x2);
70 | const curveY = cubicBezier(y1, y2);
71 | const derCurveX = derivativeCubicBezier(x1, x2);
72 | const rangeValue = value => {
73 | if (value > 1) {
74 | return 1;
75 | }
76 | if (value < 0) {
77 | return 0;
78 | }
79 |
80 | return value;
81 | };
82 |
83 | const bezier = _t => {
84 | const t = _t > 1 ? 1 : _t;
85 | let x = t;
86 |
87 | for (let i = 0; i < 8; ++i) {
88 | const evalT = curveX(x) - t;
89 | const derVal = derCurveX(x);
90 |
91 | if (Math.abs(evalT - t) < ACCURACY || derVal < ACCURACY) {
92 | return curveY(x);
93 | }
94 |
95 | x = rangeValue(x - evalT / derVal);
96 | }
97 |
98 | return curveY(x);
99 | };
100 |
101 | bezier.isStepper = false;
102 |
103 | return bezier;
104 | };
105 |
106 | export const configSpring = (config = {}) => {
107 | const { stiff = 100, damping = 8, dt = 17 } = config;
108 | const stepper = (currX, destX, currV) => {
109 | const FSpring = -(currX - destX) * stiff;
110 | const FDamping = currV * damping;
111 | const newV = currV + ((FSpring - FDamping) * dt) / 1000;
112 | const newX = (currV * dt) / 1000 + currX;
113 |
114 | if (Math.abs(newX - destX) < ACCURACY && Math.abs(newV) < ACCURACY) {
115 | return [destX, 0];
116 | }
117 | return [newX, newV];
118 | };
119 |
120 | stepper.isStepper = true;
121 | stepper.dt = dt;
122 |
123 | return stepper;
124 | };
125 |
126 | export const configEasing = (...args) => {
127 | const [easing] = args;
128 |
129 | if (typeof easing === 'string') {
130 | switch (easing) {
131 | case 'ease':
132 | case 'ease-in-out':
133 | case 'ease-out':
134 | case 'ease-in':
135 | case 'linear':
136 | return configBezier(easing);
137 | case 'spring':
138 | return configSpring();
139 | default:
140 | if (easing.split('(')[0] === 'cubic-bezier') {
141 | return configBezier(easing);
142 | }
143 | warn(
144 | false,
145 | "[configEasing]: first argument should be one of 'ease', 'ease-in', " +
146 | "'ease-out', 'ease-in-out','cubic-bezier(x1,y1,x2,y2)', 'linear' and 'spring', instead received %s",
147 | args,
148 | );
149 | }
150 | }
151 |
152 | if (typeof easing === 'function') {
153 | return easing;
154 | }
155 |
156 | warn(false, '[configEasing]: first argument type should be function or string, instead received %s', args);
157 |
158 | return null;
159 | };
160 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Animate from './Animate';
2 | import { configBezier, configSpring } from './easing';
3 | import AnimateGroup from './AnimateGroup';
4 |
5 | export { configSpring, configBezier, AnimateGroup };
6 |
7 | export default Animate;
8 |
--------------------------------------------------------------------------------
/src/setRafTimeout.js:
--------------------------------------------------------------------------------
1 | function safeRequestAnimationFrame(callback) {
2 | if (typeof requestAnimationFrame !== 'undefined') requestAnimationFrame(callback);
3 | }
4 |
5 | export default function setRafTimeout(callback, timeout = 0) {
6 | let currTime = -1;
7 |
8 | const shouldUpdate = now => {
9 | if (currTime < 0) {
10 | currTime = now;
11 | }
12 |
13 | if (now - currTime > timeout) {
14 | callback(now);
15 | currTime = -1;
16 | } else {
17 | safeRequestAnimationFrame(shouldUpdate);
18 | }
19 | };
20 |
21 | requestAnimationFrame(shouldUpdate);
22 | }
23 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: 0 */
2 |
3 | export const getIntersectionKeys = (preObj, nextObj) =>
4 | [Object.keys(preObj), Object.keys(nextObj)].reduce((a, b) => a.filter(c => b.includes(c)));
5 |
6 | export const identity = param => param;
7 |
8 | /*
9 | * @description: convert camel case to dash case
10 | * string => string
11 | */
12 | export const getDashCase = name => name.replace(/([A-Z])/g, v => `-${v.toLowerCase()}`);
13 |
14 | export const log = (...args) => {
15 | console.log(...args);
16 | };
17 |
18 | /*
19 | * @description: log the value of a varible
20 | * string => any => any
21 | */
22 | export const debug = name => item => {
23 | log(name, item);
24 |
25 | return item;
26 | };
27 |
28 | /*
29 | * @description: log name, args, return value of a function
30 | * function => function
31 | */
32 | export const debugf =
33 | (tag, f) =>
34 | (...args) => {
35 | const res = f(...args);
36 | const name = tag || f.name || 'anonymous function';
37 | const argNames = `(${args.map(JSON.stringify).join(', ')})`;
38 |
39 | log(`${name}: ${argNames} => ${JSON.stringify(res)}`);
40 |
41 | return res;
42 | };
43 |
44 | /*
45 | * @description: map object on every element in this object.
46 | * (function, object) => object
47 | */
48 | export const mapObject = (fn, obj) =>
49 | Object.keys(obj).reduce(
50 | (res, key) => ({
51 | ...res,
52 | [key]: fn(key, obj[key]),
53 | }),
54 | {},
55 | );
56 |
57 | export const getTransitionVal = (props, duration, easing) =>
58 | props.map(prop => `${getDashCase(prop)} ${duration}ms ${easing}`).join(',');
59 |
60 | const isDev = process.env.NODE_ENV !== 'production';
61 |
62 | export const warn = (condition, format, a, b, c, d, e, f) => {
63 | if (isDev && typeof console !== 'undefined' && console.warn) {
64 | if (format === undefined) {
65 | console.warn('LogUtils requires an error message argument');
66 | }
67 |
68 | if (!condition) {
69 | if (format === undefined) {
70 | console.warn(
71 | 'Minified exception occurred; use the non-minified dev environment ' +
72 | 'for the full error message and additional helpful warnings.',
73 | );
74 | } else {
75 | const args = [a, b, c, d, e, f];
76 | let argIndex = 0;
77 |
78 | console.warn(format.replace(/%s/g, () => args[argIndex++]));
79 | }
80 | }
81 | }
82 | };
83 |
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import React from 'react';
3 | import { render } from '@testing-library/react';
4 | import Animate from '../src';
5 |
6 | describe('Animate', () => {
7 | it('Should change the style of children', (done) => {
8 | const { container } = render(
9 |
10 |
11 | ,
12 | );
13 | const element = container.getElementsByClassName('test-wrapper')[0];
14 | expect(element).toHaveStyle({
15 | opacity: 1,
16 | });
17 |
18 | setTimeout(() => {
19 | expect(element).toHaveStyle({
20 | opacity: 0,
21 | });
22 | done();
23 | }, 700);
24 | });
25 |
26 | it('Should called onAnimationEnd', (done) => {
27 | let num = 0;
28 | const handleAnimationEnd = () => {
29 | num += 1;
30 | };
31 |
32 | render(
33 |
40 |
41 | ,
42 | );
43 |
44 | expect(num).toEqual(0);
45 | setTimeout(() => {
46 | expect(num).toEqual(1);
47 | done();
48 | }, 700);
49 | });
50 |
51 | it('Should change style as steps', (done) => {
52 | let firstStatus = 'no';
53 | let secondStatus = 'no';
54 |
55 | const firstHandleAnimationEnd = () => {
56 | firstStatus = 'yes';
57 | };
58 | const secondHandleAnimationEnd = () => {
59 | secondStatus = 'yes';
60 | };
61 |
62 | render(
63 |
81 |
82 | ,
83 | );
84 |
85 | expect(firstStatus).toEqual('no');
86 | expect(secondStatus).toEqual('no');
87 | setTimeout(() => {
88 | expect(firstStatus).toEqual('yes');
89 | expect(secondStatus).toEqual('no');
90 | }, 700);
91 |
92 | setTimeout(() => {
93 | expect(firstStatus).toEqual('yes');
94 | expect(secondStatus).toEqual('yes');
95 | done();
96 | }, 1400);
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
4 |
5 | const env = process.env.NODE_ENV;
6 |
7 | const config = {
8 | entry: path.join(__dirname, './src/index.js'),
9 |
10 | output: {
11 | filename: `ReactSmooth${env === 'production' ? '.min' : ''}.js`,
12 | },
13 |
14 | module: {
15 | rules: [
16 | {
17 | use: 'babel-loader',
18 | test: /\.(js|jsx)$/,
19 | exclude: /node_modules/,
20 | include: [path.resolve(__dirname, 'src')],
21 | },
22 | ],
23 | },
24 |
25 | resolve: {
26 | alias: {
27 | react: path.join(__dirname, './node_modules/react'),
28 | 'react-dom': path.join(__dirname, './node_modules/react-dom'),
29 | 'react-transition-group': path.join(__dirname, './node_modules/react-transition-group'),
30 | },
31 | },
32 |
33 | externals: {
34 | react: {
35 | root: 'React',
36 | commonjs2: 'react',
37 | commonjs: 'react',
38 | amd: 'react',
39 | },
40 | 'react-transition-group': {
41 | root: ['ReactTransitionGroup'],
42 | commonjs2: 'react-transition-group',
43 | commonjs: 'react-transition-group',
44 | amd: 'react-transition-group',
45 | },
46 | },
47 |
48 | plugins: [
49 | new webpack.DefinePlugin({
50 | 'process.env.NODE_ENV': JSON.stringify(env),
51 | }),
52 | ],
53 | };
54 |
55 | if (env === 'analyse') {
56 | config.plugins.push(new BundleAnalyzerPlugin());
57 | }
58 |
59 | if (env === 'development') {
60 | config.mode = 'development';
61 | }
62 |
63 | if (env === 'production') {
64 | config.mode = 'production';
65 | }
66 |
67 | module.exports = config;
68 |
--------------------------------------------------------------------------------