├── .editorconfig ├── .eslintrc.js ├── .fatherrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .umirc.ts ├── HISTORY.md ├── LICENSE ├── README.md ├── animate-param.md ├── assets └── index.less ├── docs ├── changeLog.md ├── demo │ ├── animParam.md │ ├── attr.md │ ├── blurFilter.md │ ├── component.md │ ├── componentFragment.md │ ├── control.md │ ├── easingPath.md │ ├── followMouse.md │ ├── group.md │ ├── group:useInTable.md │ ├── plugin-gradient.md │ ├── plugin-number-dance.md │ ├── plugin-path-motion.md │ ├── plugin-svg-draw.md │ ├── plugin-svg-morph.md │ ├── resetStyle.md │ ├── simple.md │ └── type.md ├── examples │ ├── animParam.tsx │ ├── attr.tsx │ ├── blurFilter.tsx │ ├── component.tsx │ ├── componentFragment.tsx │ ├── control.tsx │ ├── easingPath.tsx │ ├── followMouse.tsx │ ├── group.tsx │ ├── numberDance.tsx │ ├── pathMotion.tsx │ ├── resetStyle.tsx │ ├── simple.tsx │ ├── svg.tsx │ ├── svgDraw.tsx │ ├── type.tsx │ ├── useInTable.less │ └── useInTable.tsx └── index.md ├── index.d.ts ├── now.json ├── package.json ├── src ├── TweenOne.tsx ├── TweenOneGroup.tsx ├── index.tsx ├── plugin │ ├── ChildrenPlugin.ts │ ├── PathMotionPlugin.ts │ ├── SvgDrawPlugin.ts │ └── SvgMorphPlugin.ts ├── type.ts └── utils │ ├── common.ts │ ├── group.ts │ └── index.ts ├── tests ├── tweenOne.test.jsx └── tweenOneGroup.test.jsx ├── tsconfig.json └── typings └── global ├── import.d.ts └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@umijs/fabric/dist/eslint'); 2 | 3 | module.exports = { 4 | ...base, 5 | rules: { 6 | ...base.rules, 7 | 'import/no-extraneous-dependencies': 0, 8 | 'import/no-named-as-default': 0, 9 | 'no-template-curly-in-string': 0, 10 | 'prefer-promise-reject-errors': 0, 11 | 'react/no-array-index-key': 0, 12 | 'react/require-default-props': 0, 13 | 'react/sort-comp': 0, 14 | 'react/no-find-dom-node': 1, 15 | '@typescript-eslint/no-explicit-any': 0, 16 | 'jsx-a11y/label-has-associated-control': 0, 17 | 'jsx-a11y/label-has-for': 0, 18 | 'no-param-reassign': 0, 19 | 'prefer-destructuring': 0, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | cjs: 'babel', 4 | esm: { type: 'babel', importLibToEs: true }, 5 | runtimeHelpers: true, 6 | preCommit: { 7 | eslint: true, 8 | prettier: true, 9 | }, 10 | }; -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master, 3.0] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | setup: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@master 15 | 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '12' 19 | 20 | - name: cache package-lock.json 21 | uses: actions/cache@v2 22 | with: 23 | path: package-temp-dir 24 | key: lock-${{ github.sha }} 25 | 26 | - name: create package-lock.json 27 | run: npm i --package-lock-only 28 | 29 | - name: hack for singe file 30 | run: | 31 | if [ ! -d "package-temp-dir" ]; then 32 | mkdir package-temp-dir 33 | fi 34 | cp package-lock.json package-temp-dir 35 | - name: cache node_modules 36 | id: node_modules_cache_id 37 | uses: actions/cache@v2 38 | with: 39 | path: node_modules 40 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 41 | 42 | - name: install 43 | if: steps.node_modules_cache_id.outputs.cache-hit != 'true' 44 | run: npm ci 45 | 46 | lint: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: checkout 50 | uses: actions/checkout@master 51 | 52 | - name: restore cache from package-lock.json 53 | uses: actions/cache@v2 54 | with: 55 | path: package-temp-dir 56 | key: lock-${{ github.sha }} 57 | 58 | - name: restore cache from node_modules 59 | uses: actions/cache@v2 60 | with: 61 | path: node_modules 62 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 63 | 64 | - name: lint 65 | run: npm run lint 66 | 67 | needs: setup 68 | 69 | compile: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: checkout 73 | uses: actions/checkout@master 74 | 75 | - name: restore cache from package-lock.json 76 | uses: actions/cache@v2 77 | with: 78 | path: package-temp-dir 79 | key: lock-${{ github.sha }} 80 | 81 | - name: restore cache from node_modules 82 | uses: actions/cache@v2 83 | with: 84 | path: node_modules 85 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 86 | 87 | - name: compile 88 | run: npm run compile 89 | 90 | needs: setup 91 | 92 | coverage: 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: checkout 96 | uses: actions/checkout@master 97 | 98 | - name: restore cache from package-lock.json 99 | uses: actions/cache@v2 100 | with: 101 | path: package-temp-dir 102 | key: lock-${{ github.sha }} 103 | 104 | - name: restore cache from node_modules 105 | uses: actions/cache@v2 106 | with: 107 | path: node_modules 108 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 109 | 110 | - name: coverage 111 | run: npm test -- --coverage && bash <(curl -s https://codecov.io/bash) 112 | 113 | needs: setup 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | *.iml 3 | *.log 4 | .idea/ 5 | .ipr 6 | .iws 7 | *~ 8 | ~* 9 | *.diff 10 | *.patch 11 | *.bak 12 | .DS_Store 13 | Thumbs.db 14 | .project 15 | .*proj 16 | .svn/ 17 | *.swp 18 | *.swo 19 | *.pyc 20 | *.pyo 21 | .build 22 | node_modules 23 | .cache 24 | dist 25 | assets/**/*.css 26 | build 27 | lib 28 | es 29 | coverage 30 | yarn.lock 31 | package-lock.json 32 | .vscode 33 | .doc 34 | # production 35 | /dist 36 | /docs-dist 37 | 38 | # misc 39 | .DS_Store 40 | # umi 41 | .umi 42 | .umi-production 43 | .umi-test 44 | .env.local 45 | src/.umi 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.cfg 3 | nohup.out 4 | *.iml 5 | .idea/ 6 | .ipr 7 | .iws 8 | *~ 9 | ~* 10 | *.diff 11 | *.log 12 | *.patch 13 | *.bak 14 | .DS_Store 15 | Thumbs.db 16 | .project 17 | .*proj 18 | .svn/ 19 | *.swp 20 | out/ 21 | .build 22 | node_modules 23 | .cache 24 | examples 25 | tests 26 | src 27 | /index.js 28 | .* 29 | assets/**/*.less -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json 5 | .umi 6 | .umi-production 7 | .umi-test -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | // more config: https://d.umijs.org/config 2 | import { defineConfig } from 'dumi'; 3 | const path = require('path'); 4 | 5 | export default defineConfig({ 6 | title: 'rc-tween-one', 7 | favicon: 'https://zos.alipayobjects.com/rmsportal/TOXWfHIUGHvZIyb.svg', 8 | logo: 'https://zos.alipayobjects.com/rmsportal/TOXWfHIUGHvZIyb.svg', 9 | outputPath: '.doc', 10 | alias: { 11 | 'rc-tween-one/es': path.join(__dirname, 'src'), 12 | 'rc-tween-one/lib': path.join(__dirname, 'src'), 13 | }, 14 | }); -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 2.2.0 5 | 1. resetStyleBool 改名成 resetStyle; 6 | 2. TweenOne 删除 updateReStart, 现在默认是。 7 | 3. Group 重构动画逻辑,以队列形式切换,如果在动画时做切换,需将动画完成后再执切换动画。 8 | 4. Group 增加 exclusive, 在队列动画时强行执行切换动画。 9 | 10 | ## 2.0.0 11 | 12 | add repeat and yoyo to tag; 13 | 14 | ## 1.8.0 15 | 16 | add children plugin. 17 | 18 | ## 0.4 19 | 20 | 1. filter 拆分为 'grayScale', 'sepia', 'hueRotate', 'invert', 'brightness', 'contrast', 'blur', 与transform一致; 21 | 22 | 2. 忧化时间轴 23 | 24 | 3. 忧化 yoyo 在数组最后个卡顿的问题。 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # rc-tween-one 3 | --- 4 | 5 | React TweenOne Component 6 | 7 | 8 | [![NPM version][npm-image]][npm-url] 9 | [![build status][github-actions-image]][github-actions-url] 10 | [![Codecov][codecov-image]][codecov-url] 11 | [![node version][node-image]][node-url] 12 | [![npm download][download-image]][download-url] 13 | 14 | [npm-image]: http://img.shields.io/npm/v/rc-tween-one.svg?style=flat-square 15 | [npm-url]: http://npmjs.org/package/rc-tween-one 16 | [github-actions-image]: https://github.com/react-component/tween-one/workflows/CI/badge.svg 17 | [github-actions-url]: https://github.com/react-component/tween-one/actions 18 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/tween-one/master.svg?style=flat-square 19 | [codecov-url]: https://codecov.io/gh/react-component/tween-one/branch/master 20 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 21 | [node-url]: http://nodejs.org/download/ 22 | [download-image]: https://img.shields.io/npm/dm/rc-tween-one.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/rc-tween-one 24 | 25 | 26 | ## Browser Support 27 | 28 | |![IE](https://github.com/alrra/browser-logos/blob/master/src/edge/edge_48x48.png?raw=true) | ![Chrome](https://github.com/alrra/browser-logos/blob/master/src/chrome/chrome_48x48.png?raw=true) | ![Firefox](https://github.com/alrra/browser-logos/blob/master/src/firefox/firefox_48x48.png?raw=true) | ![Opera](https://github.com/alrra/browser-logos/blob/master/src/opera/opera_48x48.png?raw=true) | ![Safari](https://github.com/alrra/browser-logos/blob/master/src/safari/safari_48x48.png?raw=true)| 29 | | --- | --- | --- | --- | --- | 30 | | IE 10+ ✔ | Chrome 31.0+ ✔ | Firefox 31.0+ ✔ | Opera 30.0+ ✔ | Safari 7.0+ ✔ | 31 | 32 | ## Development 33 | 34 | ``` 35 | npm install 36 | npm start 37 | ``` 38 | 39 | ## Example 40 | 41 | http://localhost:8100/examples/ 42 | 43 | 2.x: http://react-component.github.io/tween-one/ 44 | 45 | 3.x: https://tween-one.vercel.app/ 46 | 47 | ## install 48 | 49 | 50 | [![rc-tween-one](https://nodei.co/npm/rc-tween-one.png)](https://npmjs.org/package/rc-tween-one) 51 | 52 | 53 | ## Usage 54 | 55 | ```js | pure 56 | var TweenOne = require('rc-tween-one'); 57 | var React = require('react'); 58 | React.render( 59 | demo 60 | , container); 61 | ``` 62 | 63 | ### Plugin 64 | 65 | ```js | pure 66 | var TweenOne = require('rc-tween-one'); 67 | var React = require('react'); 68 | var SvgDrawPlugin = require('rc-tween-one/lib/plugin/SvgDrawPlugin'); 69 | TweenOne.plugins.push(SvgDrawPlugin); 70 | React.render( 71 | 77 | , container); 78 | ``` 79 | 80 | ### TweenOneGroup 81 | ```js | pure 82 | var TweenOne = require('rc-tween-one'); 83 | var React = require('react'); 84 | var TweenOneGroup = TweenOne.TweenOneGroup; 85 | React.render( 86 |
demo
87 |
demo
88 |
, container); 89 | ``` 90 | 91 | ## API 92 | 93 | 中文文档 94 | 95 | ### props 96 | 97 | | name | type | default | description | 98 | |------------|----------------|---------|----------------| 99 | | animation | object / array | null | animate configure parameters | 100 | | paused | boolean | false | animate timeline pause | 101 | | reverse | boolean | false | animate timeline revers | 102 | | delay | number | 0 | animate timeline delay | 103 | | repeat | number | 0 | `animation` all data repeat, To repeat indefinitely, use -1 | 104 | | repeatDelay | number | 0 | animate timeline repeat delay | 105 | | yoyo | boolean | false | `animation` all data alternating backward and forward on each repeat. | 106 | | onChange | func | null | when the animation change called, callback({ moment, targets, index, mode, ratio, vars, index, repeat }) | 107 | | onChangeTimeline | func | null | when the animation change called, callback({ mode, targets, vars, moment, totalTime, repeat }) | 108 | | moment | number | null | set the current frame | 109 | | regionStartTime | number | 0 | Set the start time of the animation region | 110 | | regionEndTime | number | null | Set the end time of the animation region | 111 | | attr | boolean | false | attribute animation is `true`, when morph SVG must be `true`. | 112 | | resetStyle | boolean | false | update animation data, reset init style | 113 | | component | string / React.Element | `div` | component tag | 114 | | componentProps | object | null | component is React.Element, component tag props, not add `style` | 115 | 116 | 117 | ### animation = { } 118 | 119 | > Basic animation param. please view [animation terms](https://motion.ant.design/language/animate-term) 120 | 121 | | name | type | default | description | 122 | |------------|----------------|---------|----------------| 123 | | [key: string] | `string` `number` `array` | null | All variables based on number, such as left, x, color, shadow | 124 | | type | string | `to` | play type: `to` `from` `set`| 125 | | duration | number | 450 | animate duration | 126 | | delay | number | 0 | animate delay | 127 | | repeat | number | 0 | animate repeat, To repeat indefinitely, use -1 | 128 | | repeatDelay| number | 0 | repeat start delay | 129 | | appearTo | number | null | Add to the specified time | 130 | | yoyo | boolean | false | `true`: alternating backward and forward on each repeat. | 131 | | ease | string | `easeInOutQuad` | animate ease [refer](http://easings.net/en) or svg path `M0,100 C30,60 0,20 50,50 C70,70 60,0 100,0` | 132 | | bezier | object | null | bezier curve animate | 133 | | onStart | func | null | A function that should be called when the tween begins, callback(e), e: { index, target } | 134 | | onUpdate | func | null | A function that should be called every time the animate updates, callback(e), e: { index, targets, ratio } | 135 | | onComplete | func | null | A function that should be called when the animate has completed, callback(e), e: { index, targets } | 136 | | onRepeat | func | null | A function that should be called each time the animate repeats, callback(e), e: { index, targets } | 137 | 138 | > Cannot be used at the same time `reverse` and `repeat: -1`. 139 | 140 | ### animation =[ ] is timeline 141 | ```js | pure 142 | 143 | ``` 144 | ## Plugins 145 | ### SvgDrawPlugin 146 | 147 | ```js | pure 148 | import { Plugins } from 'rc-tween-one'; 149 | import SvgDrawPlugin from 'rc-tween-one/es/plugin/SvgDrawPlugin'; 150 | Plugins.push(SvgDrawPlugin); 151 | 152 | 153 | ``` 154 | 155 | SVGDraw = string or number; 156 | 157 | { SVGDraw: 30 } or { SVGDraw: 'start end' } start and end values can be `%`; 158 | 159 | ### SvgMorphPlugin 160 | 161 | ```js | pure 162 | import { Plugins } from 'rc-tween-one'; 163 | import SvgMorphPlugin from 'rc-tween-one/es/plugin/SvgMorphPlugin'; 164 | Plugins.push(SvgMorphPlugin); 165 | 166 | 167 | ``` 168 | #### SvgMorphPlugin API 169 | 170 | | name | type | default | description | 171 | |------------------|--------|---------|----------------| 172 | | path | string | null | svg path, ref: `M0,0L100,0`;| 173 | | attr | string | null | Svg tag attributes, example: `polygon` is ` points`, `path` is `d`. | 174 | | maxSegmentLength | number | 0.5 | The lower the value, the smoother the generated animation will be, but at the expense of performance;| 175 | 176 | 177 | ### PathPlugin 178 | 179 | ```js | pure 180 | import { Plugins } from 'rc-tween-one'; 181 | import PathMotionPlugin from 'rc-tween-one/es/plugin/PathMotionPlugin'; 182 | Plugins.push(PathMotionPlugin); 183 | 184 | 185 | ``` 186 | #### PathMotion API 187 | 188 | | name | type | default | description | 189 | | ------ | ------------------- | --------------- | ----------------------------- | 190 | | path | string / {x,y}[] | null | svg path, ref: `M0,0L100,0`; | 191 | | pathVars | IPathVars | null | Only valid if path is array `[{x, y}, {x, y}]` | 192 | | center | `number \ string[]` | `['50%','50%']` | center point, ref: `[50px, 50px]`; | 193 | | x | boolean | true | x follow the path. | 194 | | y | boolean | true | y follow the path. | 195 | | rotate | boolean | true | rotate follow the path. | 196 | 197 | ##### IPathVars 198 | | name | type | default | description | 199 | | ------ | ------------------- | --------------- | ----------------------------- | 200 | | type | `thru \ soft \ cubic` | `thru` | path type. `thru` same as the path; `soft` with the curve of attraction facing them, but not through the point; `cubic` allows you to define standard Cubic Bezier, example: `[start, control, control, end]`. | 201 | | curviness | 0-2 | 1 | This determines how "curvy" the resulting path is. `0` is lines, `1` is curved path, `2` would make it much more curvy. It can be `1.5`. | 202 | | relative | boolean | false | Increase relative to current value. example: if the target's x starts at 100 and the path is `[{x:5}, {x:10}, {x:-2}]` , it would first move to `105`, then `115`, and finally end at `113`. | 203 | 204 | ### ChildrenPlugin 205 | 206 | #### Children = { value:, floatLength, formatMoney }; 207 | 208 | | name | type | default | description | 209 | |---|---|---|---| 210 | | value | number | null | children number to value. | 211 | | floatLength | number | null | float precision length | 212 | | formatMoney | `true` \ { thousand, decimal } | null | format number to money. | 213 | 214 | #### formatMoney = { thousand, decimal } 215 | 216 | | name | type | default | description | 217 | |---|---|---|---| 218 | | thousand | string | `,` | no explanation. | 219 | | decimal | string | `.` | no explanation. | 220 | 221 | ## TweenOneGroup 222 | 223 | | name | type | default | description | 224 | |------------|----------------|---------|----------------| 225 | | appear | boolean | true | whether support appear anim | 226 | | enter | object / array / func | `{ x: 30, opacity: 0, type: 'from' }` | enter anim twee-one data. when array is tween-one timeline, func refer to queue-anim | 227 | | leave | object / array / func | `{ x: 30, opacity: 0 }` | leave anim twee-one data. when array is tween-one timeline, func refer to queue-anim | 228 | | onEnd | func | - | one animation end callback | 229 | | animatingClassName | array | `['tween-one-entering', 'tween-one-leaving']` | className to every element of animating | 230 | | resetStyle | boolean | true | TweenOne resetStyle, reset the initial style when changing animation. | 231 | | exclusive | boolean | false | Whether to allow a new animate to execute immediately when switching. `enter => leave`: execute immediately leave | 232 | | component | React.Element/String | div | component tag | 233 | | componentProps | object | - | component tag props | 234 | -------------------------------------------------------------------------------- /animate-param.md: -------------------------------------------------------------------------------- 1 | ## 动画基本参数说明 2 | 3 | tween-one 里的可支持 style 动画参数说明。 4 | 5 | 动画默认参数: duration: 450毫秒, ease: 'easeInOutQuad'; 以下文本中出现的`到`为默认动画。 6 | 7 | ### 基本 style 可动画参数 8 | 9 | | 参数名称 | 说明 | 10 | |-------------------|---------------------| 11 | | width | `{ width: 100 }` 元素当前宽度到 100px | 12 | | maxWidth | `{ maxWidth: 100 }` 元素当前最大宽度到 100px | 13 | | minWidth | `{ minWidth: 100 }` 元素当前最小宽度到 100px | 14 | | height | `{ height: 100 }` 元素当前高度到 100px | 15 | | maxHeight | `{ maxHeight: 100 }` 元素当前最大高度到 100px | 16 | | minHeight | `{ mimHeight: 100 }` 元素当前最小高度到 100px | 17 | | lineHeight | `{ lineHeight: 100 }` 区块行高到 100px | 18 | | opacity | `{ opacity: 0 }` 元素当前透明度到 0 | 19 | | top | `{ top: 100 }` 元素当前顶部距离到 100px, 需配合 `position: relative | absolute` | 20 | | right | `{ right: 100 }` 元素当前右部距离到 100px, 需配合 `position: relative | absolute` | 21 | | bottom | `{ bottom: 100 }` 元素当前下部距离到 100px, 需配合 `position: relative | absolute` | 22 | | left | `{ left: 100 }` 元素当前左部距离到 100px, 需配合 `position: relative | absolute` | 23 | | marginTop | `{ marginTop: 100 }` 元素当前顶部外边距离到 100px | 24 | | marginRight | `{ marginRight: 100 }` 元素当前右部外边距离到 100px | 25 | | marginBottom | `{ marginBottom: 100 }` 元素当前下部外边距离到 100px | 26 | | marginLeft | `{ marginLeft: 100 }` 元素当前左部外边距离到 100px | 27 | | paddingTop | `{ paddingTop: 100 }` 元素当前顶部内边距离到 100px | 28 | | paddingRight | `{ paddingRight: 100 }` 元素当前右部内边距离到 100px | 29 | | paddingBottom | `{ paddingBottom: 100 }` 元素当前下部内边距离到 100px | 30 | | paddingLeft | `{ paddingLeft: 100 }` 元素当前左部内边距离到 100px | 31 | | color | `{ color: '#FFFFFF' }` 元素当前文字颜色到白色 | 32 | | backgroundColor | `{ backgroundColor: '#FFFFFF' }` 元素当前背景颜色到白色 | 33 | | borderWidth | `{ borderWidth: 2 }` 元素当前边框宽度到 2px,同样可用 `borderTopWidth` `borderRightWidth` `borderBottomWidth` `borderLeftWidth` | 34 | | borderRadius | `{ borderRadius: 5 }` 元素当前圆角到 5px, 同上, 同样可用 `上 左 下 右` | 35 | | borderColor | `{ borderColor: '#FFFFFF' }` 元素当前边框颜色到白色 | 36 | |   boxShadow       | `{ boxShadow: '0 0 10px #000' }` 元素当前阴影模糊到 10px | 37 | |   textShadow     | `{ textShadow: '0 0 10px #000' }` 元素当前文字内容阴影模糊到 10px | 38 | 39 | 40 | ### transform 参数 41 | 42 | | 参数名称 | 说明 | 43 | |-------------------|---------------------| 44 | | translateX / X | `{ translateX: 10 } or { x: 10 } => transform: translateX(10px)`, x 方向的位置移动 10px | 45 | | translateY / y | `{ translateY: 10 } or { y: 10 } => transform: translateY(10px)`, y 方向的位置移动 10px | 46 | | translateZ / z | `{ translateZ: 10 } or { z: 10 } => transform: translateZ(10px)`, z 方向的位置移动 100px | 47 | | rotate | `{ rotate: 10 } => transform: rotate(10deg)` 元素以 transformOrigin 的中心点旋转 10deg | 48 | | rotateX | `{ rotateX: 10 } => transform: rotateX(10deg)` 元素以 transformOrigin 的中心点向 X 旋转 10deg | 49 | | rotateY | `{ rotateY: 10 } => transform: rotateY(10deg)` 元素以 transformOrigin 的中心点向 Y 旋转 10deg | 50 | | scale | `{ scale: 0 } => transform: scale(0)` 元素以 transformOrigin 的中心点缩放到 0 | 51 | | scaleX | `{ scaleX: 0 } => transform: scaleX(0)` 元素以 transformOrigin 的中心点 X 缩放到 0 | 52 | | scaleY | `{ scaleY: 0 } => transform: scaleY(0)` 元素以 transformOrigin 的中心点 Y 缩放到 0 | 53 | | transformOrigin | `{ transformOrigin: '50px 50px'}` 元素当前中心点到 x: 50px y: 50px | 54 | 55 | ### filter 参数 56 | 57 | | 参数名称 | 说明 | 58 | |-------------------|---------------------| 59 | | grayScale | `{ grayScale: 1 }` 元素 filter 灰度到 100% | 60 | | sepia | `{ sepia: 1 }` 元素 filter 颜色到 100% | 61 | | hueRotate | `{ hueRotate: '90deg' }` 元素 filter 色相盘旋转 90 度 | 62 | | invert | `{ invert: 1 }` 元素 filter 色值反相到 100% | 63 | | brightness | `{ brightness: 2 }` 元素 filter 亮度到 200% | 64 | | contrast | `{ contrast: 2 }` 对比度到 200% | 65 | | saturate | `{ saturate: 2 }` 饱和度到 200% | 66 | | blur | `{ blur: '20px' }` 模糊到 20px | 67 | 68 | > 以上为 tween-one 里的动画可支持参数,如有其它样式可动画或以上有误,烦请 PR 来修改。。 69 | 70 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @zero: 0; -------------------------------------------------------------------------------- /docs/changeLog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: changelog 3 | order: 2 4 | --- 5 | 6 | # History 7 | 8 | --- 9 | 10 | ## 3.0.0-beta.x 11 | 12 | - hooks 重构 rc-tween-one; 13 | - 拆离动画库与组件, 动画库 https://www.npmjs.com/package/tween-one 14 | 15 | #### API 变更 16 | 17 | - 删除 `currentRef`, hooks 如果 component 是组件,ref 返回为组件的 ref; 18 | - 增加对 hooks 下的 React.fragment 的支持。 19 | - 增加全局动画播放时的 `delay`; 20 | - 删除 `reverseDelay`; 21 | - 增加 `repeatDelay`; 22 | - `attr` 改为 `boolean` 类型; 23 | - 更新 `onChange` 和回调,cb: { moment, targets, index, mode, ratio, vars, index, repeat } 24 | - 新增 `onChangeTimeline`, cb: { mode, targets, vars, moment, totalTime, repeat } 25 | - 其它回调如 `onStart`、`onUpdate`、`onComplete` 等的 cb 的 target 全部更新为 targets; 26 | - 删除 `BezierPlugin`,合进 PathMotionPlugin;; 27 | - `PathMotionPlugin` 更改用法,使用 `PathMotion: { path, center, x, y, rotate }`, 详细参考 pathMotion demo; 28 | - `SvgMorph` 依赖更改为使用 `flubber`; 29 | - 滤境使用,改成 `import { Plugins } from 'rc-tween-one'; Plugins.push()`, 保留 `TweenOne.plugins.push()`; 30 | - 删除 `TweenOne.easing.path(path)` 使用,直接用 `ease: 'M0,0L100,100'`; 31 | - 增加动画区域播放 `regionStartTime`, `regionEndTime`; 32 | - 增加对背景渐变的动画支持,`backgroundImage: 'linear-gradient(to left, #000fff 0%, red 20%, #fff000 100%)'` 33 | - TweenOneGroup 使用改成 `import TweenOneGroup from 'rc-tween-one/lib/TweenOneGroup';` 34 | --- 35 | 36 | ## 2.2.0 37 | 38 | 1. resetStyleBool 改名成 resetStyle; 39 | 2. TweenOne 删除 updateReStart, 现在默认是。 40 | 3. Group 重构动画逻辑,以队列形式切换,如果在动画时做切换,需将动画完成后再执切换动画。 41 | 4. Group 增加 exclusive, 在队列动画时强行执行切换动画。 42 | 43 | ## 2.0.0 44 | 45 | add repeat and yoyo to tag; 46 | 47 | ## 1.8.0 48 | 49 | add children plugin. 50 | 51 | ## 0.4 52 | 53 | 1. filter 拆分为 'grayScale', 'sepia', 'hueRotate', 'invert', 'brightness', 'contrast', 'blur', 与 transform 一致; 54 | 55 | 2. 忧化时间轴 56 | 57 | 3. 忧化 yoyo 在数组最后个卡顿的问题。 58 | -------------------------------------------------------------------------------- /docs/demo/animParam.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: anim-params 3 | order: 2 4 | --- 5 | 6 | ## anim-params 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/attr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: api:attr 3 | order: 4 4 | --- 5 | 6 | ## attr 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/blurFilter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: filter 3 | order: 3 4 | --- 5 | 6 | ## css filter 7 | 8 | css filter animate 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/demo/component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: api:component 3 | order: 4 4 | --- 5 | 6 | ## component 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/componentFragment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: api:componentFragment 3 | order: 4 4 | --- 5 | 6 | ## component 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/control.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: control 3 | order: 1 4 | --- 5 | 6 | ## control 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/easingPath.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: api:easing-path 3 | order: 4 4 | --- 5 | 6 | ## easingPath 7 | 8 | ease path 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/demo/followMouse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: follow-mouse 3 | order: 5 4 | --- 5 | 6 | ## follow mouse 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/group.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TweenOneGroup 3 | order: 8 4 | --- 5 | 6 | ### appear = false 7 | 8 | ```jsx 9 | import TweenOneGroup from 'rc-tween-one/es/TweenOneGroup'; 10 | import React from 'react'; 11 | import { Button } from 'antd'; 12 | import 'antd/dist/antd.css'; 13 | 14 | const imgArray = [ 15 | 'https://os.alipayobjects.com/rmsportal/IhCNTqPpLeTNnwr.jpg', 16 | 'https://os.alipayobjects.com/rmsportal/uaQVvDrCwryVlbb.jpg', 17 | ]; 18 | export default () => { 19 | const [int, setInt] = React.useState(0); 20 | return ( 21 |
22 | 34 | 35 |
36 | img 37 |
38 |
39 |
40 | ); 41 | }; 42 | ``` 43 | 44 | ## basic 45 | 46 | 47 | 48 | ### exclusive = true 49 | 50 | ```jsx 51 | import TweenOneGroup from 'rc-tween-one/es/TweenOneGroup'; 52 | import React from 'react'; 53 | import { Button } from 'antd'; 54 | import 'antd/dist/antd.css'; 55 | 56 | const imgArray = [ 57 | 'https://os.alipayobjects.com/rmsportal/IhCNTqPpLeTNnwr.jpg', 58 | 'https://os.alipayobjects.com/rmsportal/uaQVvDrCwryVlbb.jpg', 59 | ]; 60 | export default () => { 61 | const [int, setInt] = React.useState(0); 62 | return ( 63 |
64 | 76 | 77 |
78 | img 79 |
80 |
81 |
82 | ); 83 | }; 84 | ``` 85 | 86 | ### children change 87 | 88 | ```jsx 89 | import TweenOneGroup from 'rc-tween-one/es/TweenOneGroup'; 90 | import React from 'react'; 91 | import { Button } from 'antd'; 92 | import 'antd/dist/antd.css'; 93 | 94 | const imgArray = [ 95 | 'https://os.alipayobjects.com/rmsportal/IhCNTqPpLeTNnwr.jpg', 96 | 'https://os.alipayobjects.com/rmsportal/uaQVvDrCwryVlbb.jpg', 97 | ]; 98 | export default () => { 99 | const [children, setChildren] = React.useState( 100 | [
101 | img 102 |
] 103 | ) 104 | return ( 105 |
106 | 118 | 127 | 128 | {children} 129 | 130 |
131 | ); 132 | }; 133 | ``` 134 | 135 | 出场交叉样式 136 | 137 | ```css 138 | .demo-group .tween-one-leaving { 139 | position: absolute; 140 | top: 0; 141 | left: 0; 142 | } 143 | ```` 144 | 145 | 152 | -------------------------------------------------------------------------------- /docs/demo/group:useInTable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useInTable 3 | order: 8 4 | --- 5 | 6 | ## use in table 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/plugin-gradient.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: plugin:gradient 3 | order: 6 4 | --- 5 | 6 | # gradient 7 | 8 | ```jsx 9 | import React from 'react'; 10 | import TweenOne from 'rc-tween-one'; 11 | 12 | export default () => { 13 | return ( 14 |
15 | 24 | 33 |
34 | ); 35 | }; 36 | ``` 37 | 38 | ### 背景渐变使用示例 39 | 40 | ```js 41 | 46 | ``` 47 | 48 | > 注意:渐变的类型(type)、渐变的范围(extent-keyword)、颜色位置的单位请保持一致,否则以 animation 里的优先; 49 | 50 | > 渐变色上面尽量加位置单位,示例 `linear-gradient(to left, #000fff 0%, red 20%, #fff000 100%)` 51 | 52 | 66 | -------------------------------------------------------------------------------- /docs/demo/plugin-number-dance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: plugin:number-dance 3 | order: 7 4 | --- 5 | 6 | ## number-dance 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/plugin-path-motion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: plugin:path-motion 3 | order: 7 4 | --- 5 | 6 | ## path-motion 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/plugin-svg-draw.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: plugin:svg-draw 3 | order: 7 4 | --- 5 | 6 | ## svg-draw 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/plugin-svg-morph.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: plugin:svg-morph 3 | order: 7 4 | --- 5 | 6 | ## svg-morph 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/resetStyle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: api:resetStyle 3 | order: 4 4 | --- 5 | 6 | ## type use 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/simple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: simple 3 | order: 0 4 | --- 5 | 6 | ## basic 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/type.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: api:type 3 | order: 4 4 | --- 5 | 6 | ## type use 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/animParam.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | import { Form, InputNumber, Input, Button, Checkbox, Select, AutoComplete } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | 6 | const { Option } = Select; 7 | 8 | const ValueComp = (props: any) => { 9 | const { value, onChange } = props; 10 | const [name, setName] = React.useState(value.name || 'x'); 11 | const [param, setParam] = React.useState(parseFloat(value.value) || 300); 12 | const [uint, setUint] = React.useState(value.uint || 'px'); 13 | 14 | const onNameChange = (e: any) => { 15 | setName(e); 16 | const v: any = { 17 | name: e, 18 | }; 19 | switch (e) { 20 | case 'x': 21 | case 'y': 22 | case 'margin': 23 | case 'left': 24 | setParam(300); 25 | setUint('px'); 26 | v.value = '300px'; 27 | break; 28 | case 'color': 29 | v.value = '#fff000'; 30 | setParam(v.value); 31 | setUint(null); 32 | break; 33 | case 'textShadow': 34 | v.value = '5px 5px 5px rgba(0,0,0,0.3)'; 35 | setParam(v.value); 36 | setUint(null); 37 | break; 38 | default: 39 | break; 40 | } 41 | onChange(v); 42 | }; 43 | const onValueChange = (e: any) => { 44 | setParam(e); 45 | onChange({ ...value, value: e }); 46 | }; 47 | const onUintChange = (e: any) => { 48 | setUint(e); 49 | onChange({ ...value, uint: e }); 50 | }; 51 | return ( 52 | 53 | 61 | 62 | {uint && ( 63 | 68 | )} 69 | 70 | ); 71 | }; 72 | 73 | export default () => { 74 | const [anim, setAnim] = React.useState(); 75 | const [paused, setPaused] = React.useState(true); 76 | 77 | const onFinish = (values: any) => { 78 | console.log('Received values from form: ', values); 79 | setPaused(false); 80 | const { value, ...v } = values; 81 | setAnim({ 82 | [value.name]: `${value.value}${value.uint || ''}`, 83 | ...v, 84 | }); 85 | }; 86 | 87 | return ( 88 |
89 | 90 |
执行动效
91 |
92 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | yoyo 110 | 111 | 112 | 115 | 116 |
117 |
118 | 注: 119 |
120 | 1. 更多动画参数请参考{' '} 121 | 122 | ant motion 动画参数 123 | 124 |
125 | 2. 动画值可以为 '+=300' 或 '-=300',表示当前值增加或减少。 126 |
127 | 3. repeat 为 n + 1 次,如 2 则播放 3 次动画, -1 时为无限循环 128 |
129 | 4. 参数不变,开始动画无效,请保证每次动画的参数 130 |
131 |
132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /docs/examples/attr.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | 4 | export default () => { 5 | return ( 6 | 7 | { 10 | console.log(e.vars); 11 | }} 12 | attr 13 | component="circle" 14 | cx="300" 15 | cy="55" 16 | r="50" 17 | style={{ fill: '#fff000', strokeWidth: 5, stroke: '#000fff' }} 18 | > 19 |
执行动效
20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /docs/examples/blurFilter.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | 4 | export default function Demo() { 5 | return ( 6 |
7 |

8 | filter 里的滤镜,'grayScale', 'sepia', 9 | 'hueRotate', 'invert', 'brightness', 10 | 'contrast', 'blur' 11 |

12 | 16 | img 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /docs/examples/component.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import { Menu } from 'antd'; 6 | import { 7 | PieChartOutlined, 8 | DesktopOutlined, 9 | ContainerOutlined, 10 | MailOutlined, 11 | } from '@ant-design/icons'; 12 | import 'antd/dist/antd.css'; 13 | 14 | const { SubMenu, Item } = Menu; 15 | 16 | const T = React.forwardRef((_, ref: any) =>
Function Component
); 17 | class C extends React.Component { 18 | render() { 19 | return
Class Component
; 20 | } 21 | } 22 | 23 | const anim = { x: 300 }; 24 | 25 | export default () => { 26 | const dom = React.useRef(); 27 | 28 | React.useEffect(() => { 29 | console.log('useRef:', dom); 30 | }); 31 | 32 | return ( 33 |
34 | { 36 | // It can also be used React.useRef(); 37 | console.log('function component HTMLElement:', c); 38 | }} 39 | component={T} 40 | animation={anim} 41 | /> 42 | 43 | { 45 | // It can also be used React.useRef(); 46 | console.log('class component:', c, ReactDOM.findDOMNode(c)); 47 | }} 48 | component={C} 49 | animation={anim} 50 | /> 51 | 52 | UseRef Component is string 53 | 54 |

ant design menu

55 |
56 | 67 | }} 71 | key="1" 72 | > 73 | Option 1 74 | 75 | }> 76 | Option 2 77 | 78 | }> 79 | Option 3 80 | 81 | , title: 'Navigation One' }} 86 | > 87 | Option 5 88 | Option 6 89 | Option 7 90 | Option 8 91 | 92 | 93 |
94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /docs/examples/componentFragment.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | 4 | class D extends React.Component { 5 | render() { 6 | return
d
; 7 | } 8 | } 9 | 10 | const C = React.forwardRef((_, ref: any) => { 11 | return
c
; 12 | }); 13 | 14 | const Comp = React.forwardRef((_, ref: any) => { 15 | const domRef = React.useRef([]); 16 | const refFunc = (c: any) => { 17 | if (domRef.current.some((a) => a === c)) { 18 | return; 19 | } 20 | domRef.current.push(c); 21 | ref(domRef.current); 22 | }; 23 | return ( 24 | <> 25 |
a
26 | 27 | 28 | 29 | ); 30 | }); 31 | 32 | export default () => { 33 | return ( 34 | { 36 | console.log(c); 37 | }} 38 | animation={{ x: 300 }} 39 | style={{ position: 'relative' }} 40 | component={Comp} 41 | > 42 |
执行动效
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /docs/examples/control.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | import { Button, Space } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | 6 | const animation = [{ translateX: '500px', duration: 1000 }, { y: 100 }]; 7 | export default () => { 8 | const [paused, setPaused] = React.useState(true); 9 | const [reverse, setReverse] = React.useState(false); 10 | const [moment, setMoment] = React.useState(0); 11 | 12 | return ( 13 |
14 |
15 | { console.log(e)}} 21 | style={{ opacity: 1, width: 100, transform: 'translate(50px,30px)' }} 22 | > 23 |
执行动效
24 |
25 |
26 | 27 | 35 | 42 | 50 | 60 | 67 | 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /docs/examples/easingPath.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | 4 | const p1 = 'M0,100 L25,100 C34,20 40,0 100,0'; 5 | const p = 6 | 'M0,100 C5,120 25,130 25,100 C30,60 40,75 58,90 C69,98.5 83,99.5 100,100'; 7 | 8 | const anim = [ 9 | { 10 | repeatDelay: 1000, 11 | duration: 1000, 12 | scaleX: 0, 13 | scaleY: 2, 14 | repeat: -1, 15 | yoyo: true, 16 | ease: p, 17 | }, 18 | { 19 | repeatDelay: 1000, 20 | duration: 1000, 21 | y: 0, 22 | appearTo: 0, 23 | repeat: -1, 24 | yoyo: true, 25 | ease: p1, 26 | }, 27 | ]; 28 | export default () => { 29 | return ( 30 |
31 | 43 |
51 | 52 | 53 | 54 | 55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /docs/examples/followMouse.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | 4 | export default () => { 5 | const [animation, setAnim] = React.useState(); 6 | const mouseMove = (e: any) => { 7 | const x = e.clientX; 8 | setAnim({ x, duration: 1000, ease: 'easeOutQuad' }); 9 | }; 10 | React.useEffect(() => { 11 | window.addEventListener('mousemove', mouseMove); 12 | }, []) 13 | return ( 14 | 15 | 执行动效 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /docs/examples/group.tsx: -------------------------------------------------------------------------------- 1 | import TweenOneGroup from 'rc-tween-one/es/TweenOneGroup'; 2 | import React from 'react'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import '../../assets/index.less'; 6 | 7 | const imgArray = [ 8 | 'https://os.alipayobjects.com/rmsportal/IhCNTqPpLeTNnwr.jpg', 9 | 'https://os.alipayobjects.com/rmsportal/uaQVvDrCwryVlbb.jpg', 10 | ]; 11 | export default () => { 12 | const [int, setInt] = React.useState(0); 13 | return ( 14 |
15 | 27 | 28 |
29 | img 30 |
31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /docs/examples/numberDance.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | import ChildrenPlugin from 'rc-tween-one/es/plugin/ChildrenPlugin'; 4 | 5 | 6 | Tween.plugins.push(ChildrenPlugin); 7 | export default function Demo() { 8 | return ( 9 |
10 |

子级的数值变化的动画 - children plugin

11 |
12 | 基本数字: 13 | 14 |
15 |
16 | 设置开始数字: 17 | 9990 18 |
19 |
20 | 只取整数: 21 | 22 |
23 |
24 | 基本数字, 小数后取两位, float length 2: 25 | 26 |
27 |
28 | 格式化钱符: 29 |
30 | ¥ 31 | 37 | 20,000.12 38 | 39 |
40 |
41 |
42 | 自定义钱符格式: 43 |
44 | ¥ 45 | 55 | 20.000,12 56 | 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /docs/examples/pathMotion.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | import PathPlugin from 'rc-tween-one/es/plugin/PathMotionPlugin'; 4 | 5 | Tween.plugins.push(PathPlugin); 6 | 7 | const array = [ 8 | { x: 50, y: 50 }, 9 | { x: 200, y: 250 }, 10 | { x: 350, y: 50 }, 11 | { x: 500, y: 250 }, 12 | { x: 650, y: 50 } /* 13 | { x: 800, y: 250 }, 14 | { x: 950, y: 50 }, */, 15 | ]; 16 | 17 | export default function Demo() { 18 | const p = `M50.952,85.619C31.729,84.841,23.557,73.62,24.095,42.952 19 | c0.381-21.714,6.667-33.714,30.286-34.476 20 | c36.572-1.18,59.81,77.714,102.667,76.381c30.108-0.937,34.268-32.381,34.095-41.714 21 | C190.762,22.571,180.493,6.786,159.524,6C113.81,4.286,98,87.524,50.952,85.619z`; 22 | 23 | const p2 = 'M0,0,L100, 0L100, 100L0, 100Z'; 24 | return ( 25 |
26 |
27 | 42 | a 43 | 44 | 59 | a 60 | 61 | 62 | 63 | 64 | 65 |
66 |
75 | {array.map((item, i) => { 76 | return ( 77 |
90 | ); 91 | })} 92 | c 111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /docs/examples/resetStyle.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React, { useEffect } from 'react'; 3 | import 'antd/dist/antd.css'; 4 | 5 | export default () => { 6 | const [anim, setAnim] = React.useState({ 7 | x: 500, 8 | duration: 1000, 9 | }); 10 | useEffect(() => { 11 | setTimeout(() => { 12 | setAnim({ 13 | y: 100, 14 | duration: 1000, 15 | }); 16 | }, 500); 17 | }, []); 18 | 19 | return ( 20 |
21 |
22 | 27 |
执行动效
28 |
29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /docs/examples/simple.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | 4 | export default () => { 5 | const bbb = (e: any) => { 6 | console.log(e); // eslint-disable-line no-console 7 | }; 8 | return ( 9 | 14 |
执行动效
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/examples/svg.tsx: -------------------------------------------------------------------------------- 1 | import Tween, { Plugins } from 'rc-tween-one'; 2 | import React from 'react'; 3 | import SvgMorphPlugin from 'rc-tween-one/es/plugin/SvgMorphPlugin'; 4 | 5 | // Tween.plugins.push(SvgMorphPlugin); 6 | Plugins.push(SvgMorphPlugin); 7 | 8 | export default function Demo() { 9 | return ( 10 |
11 | 12 | 13 | 22 | 23 | 24 | 25 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /docs/examples/svgDraw.tsx: -------------------------------------------------------------------------------- 1 | import Tween, { Plugins } from 'rc-tween-one'; 2 | import React from 'react'; 3 | import SvgDrawPlugin from 'rc-tween-one/es/plugin/SvgDrawPlugin'; 4 | 5 | Plugins.push(SvgDrawPlugin); 6 | 7 | const dataStartArr = ['100%', '30 450', '50% 50%', '30% 400', '50 30%', 0]; 8 | let i = 0; 9 | 10 | export default () => { 11 | const [tweenData, setTweenData] = React.useState('50 30%'); 12 | const onClick = () => { 13 | setTweenData(dataStartArr[i]); 14 | i++; 15 | i = i >= dataStartArr.length ? 0 : i; 16 | }; 17 | return ( 18 |
19 | 20 |

当前参数:{tweenData}

21 | 22 | 31 | 32 | 47 | 56 | 67 | 76 | 85 | 86 | 87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /docs/examples/type.tsx: -------------------------------------------------------------------------------- 1 | import Tween from 'rc-tween-one'; 2 | import React from 'react'; 3 | import { Button, Space } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | 6 | export default () => { 7 | const [anim, setAnim] = React.useState({ 8 | x: 500, 9 | duration: 1000, 10 | }); 11 | const [paused, setPaused] = React.useState(true); 12 | 13 | return ( 14 |
15 |
16 | { console.log(e)}} 20 | style={{ opacity: 1, width: 100, transform: 'translate(50px,30px)' }} 21 | > 22 |
执行动效
23 |
24 |
25 |
播放类型(type):
26 | 27 | 38 | 50 | 61 | 62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /docs/examples/useInTable.less: -------------------------------------------------------------------------------- 1 | .table-enter-leave-demo-nav { 2 | height: 50px; 3 | background: #080B20; 4 | line-height: 50px; 5 | } 6 | 7 | .table-enter-leave-demo-nav span { 8 | margin-left: 20px; 9 | } 10 | 11 | .table-enter-leave-demo-nav img:first-child { 12 | filter: grayscale(1) brightness(3); 13 | } 14 | 15 | .table-enter-leave-demo-nav img:last-child { 16 | margin-left: 10px; 17 | } 18 | 19 | .table-enter-leave-demo-list { 20 | width: 20%; 21 | height: calc(100% - 82px); 22 | display: inline-block; 23 | background: #171C3E; 24 | } 25 | 26 | .table-enter-leave-demo-list ul li { 27 | width: 80%; 28 | height: 15px; 29 | background: #23274A; 30 | margin: 20px auto; 31 | border-radius: 4px; 32 | } 33 | 34 | .table-enter-leave-demo-table-wrapper { 35 | width: 80%; 36 | display: inline-block; 37 | overflow: auto; 38 | height: calc(100% - 82px); 39 | } 40 | 41 | .table-enter-leave-demo-action-bar { 42 | margin: 20px; 43 | background: #fff; 44 | line-height: 40px; 45 | height: 40px; 46 | position: relative; 47 | border-radius: 4px; 48 | } 49 | 50 | .table-enter-leave-demo-action-bar:before, .table-enter-leave-demo-action-bar:after { 51 | content: ''; 52 | display: inline-block; 53 | background: #E5E5E5; 54 | vertical-align: middle; 55 | } 56 | 57 | .table-enter-leave-demo-action-bar:before { 58 | width: 20px; 59 | height: 20px; 60 | border-radius: 100%; 61 | margin-left: 10px; 62 | } 63 | 64 | .table-enter-leave-demo-action-bar:after { 65 | position: absolute; 66 | width: 100px; 67 | height: 10px; 68 | top: 0; 69 | bottom: 0; 70 | margin: auto; 71 | left: 40px; 72 | } 73 | 74 | .table-enter-leave-demo-action-bar button { 75 | position: absolute; 76 | right: 10px; 77 | top: 0; 78 | bottom: 0; 79 | margin: auto; 80 | width: 80px; 81 | border-radius: 4px; 82 | line-height: 1; 83 | } 84 | 85 | .table-enter-leave-demo-delete { 86 | color: #019BF0; 87 | cursor: pointer; 88 | } 89 | 90 | .table-enter-leave-demo-table { 91 | margin: 0 20px; 92 | overflow: hidden; 93 | } 94 | .table-enter-leave-demo-table .ant-table-thead > tr > th { 95 | display: inline-block; 96 | padding: 10px 8px; 97 | background: #e5e5e5; 98 | } 99 | .table-enter-leave-demo-table .ant-table-tbody > tr { 100 | background: #fff; 101 | /* 不能用 102 | border-bottom: 1px solid #e9e9e9; 103 | */ 104 | } 105 | .table-enter-leave-demo-table .ant-table-tbody > tr > td { 106 | display: inline-block; 107 | padding: 10px 8px; 108 | vertical-align: bottom; 109 | } 110 | 111 | .table-enter-leave-demo-table .ant-table-thead > tr, 112 | .table-enter-leave-demo-table .ant-table-tbody > tr, 113 | .table-enter-leave-demo-table .ant-table-tbody { 114 | display: block; 115 | transition: none; 116 | overflow: hidden; 117 | } 118 | 119 | .table-enter-leave-demo-table .ant-table-thead > tr > th:nth-child(1), 120 | .table-enter-leave-demo-table .ant-table-tbody > tr > td:nth-child(1) { 121 | width: 25%; 122 | } 123 | .table-enter-leave-demo-table .ant-table-thead > tr > th:nth-child(2), 124 | .table-enter-leave-demo-table .ant-table-tbody > tr > td:nth-child(2) { 125 | width: 15%; 126 | } 127 | .table-enter-leave-demo-table .ant-table-thead > tr > th:nth-child(3), 128 | .table-enter-leave-demo-table .ant-table-tbody > tr > td:nth-child(3) { 129 | width: 40%; 130 | } 131 | .table-enter-leave-demo-table .ant-table-thead > tr > th:nth-child(4), 132 | .table-enter-leave-demo-table .ant-table-tbody > tr > td:nth-child(4) { 133 | width: 20%; 134 | } 135 | 136 | @media screen and (max-width: 414px) { 137 | .table-enter-leave-demo { 138 | transform: scale(0.65) translateX(12px); 139 | transform-origin: left center; 140 | } 141 | } 142 | 143 | @media screen and (max-width: 375px) { 144 | .table-enter-leave-demo { 145 | transform: scale(0.6) translateX(7px); 146 | } 147 | } 148 | 149 | @media screen and (max-width: 320px) { 150 | .table-enter-leave-demo { 151 | transform: scale(0.5) translateX(12px); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /docs/examples/useInTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Table from 'antd/lib/table'; 3 | import Button from 'antd/lib/button'; 4 | import TweenOneGroup from 'rc-tween-one/es/TweenOneGroup'; 5 | import 'antd/dist/antd.css'; 6 | import './useInTable.less'; 7 | 8 | const TableContext = React.createContext<{ enter: any; leave: any }>({ enter: null, leave: null }); 9 | 10 | const data: { key: string; name: string; age: number; address: string; tags?: string[] }[] = [ 11 | { 12 | key: '1', 13 | name: 'John Brown', 14 | age: 32, 15 | address: 'New York No. 1 Lake Park', 16 | tags: ['nice', 'developer'], 17 | }, 18 | { 19 | key: '2', 20 | name: 'Jim Green', 21 | age: 42, 22 | address: 'London No. 1 Lake Park', 23 | tags: ['loser'], 24 | }, 25 | { 26 | key: '3', 27 | name: 'Joe Black', 28 | age: 32, 29 | address: 'Sidney No. 1 Lake Park', 30 | tags: ['cool', 'teacher'], 31 | }, 32 | ]; 33 | 34 | // 动画标签,页面切换时改用 context 传递参数; 35 | const animTag = ($props: any) => { 36 | return ( 37 | 38 | {({ enter, leave }) => { 39 | console.log(enter); 40 | const children = React.Children.toArray($props.children); 41 | 42 | return ( 43 | 51 | {children.map((item: any) => ( 52 | 53 | {item} 54 | 55 | ))} 56 | 57 | ); 58 | }} 59 | 60 | ); 61 | }; 62 | 63 | export default ({ className = 'table-enter-leave-demo' }) => { 64 | const [isPageTween, setPageTween] = useState(false); 65 | const [tableData, setTableData] = useState(data); 66 | 67 | const onEnd = () => { 68 | setPageTween(false); 69 | }; 70 | 71 | const rmStyle = (e: any) => { 72 | const { targets } = e; 73 | // callback in render before 74 | setTimeout(() => { 75 | targets.style = ''; 76 | }); 77 | }; 78 | 79 | const onAdd = () => { 80 | const newData = [...tableData]; 81 | const i = Math.round(Math.random() * (tableData.length - 1)); 82 | newData.unshift({ 83 | key: `${Date.now()}`, 84 | name: tableData[i].name, 85 | age: tableData[i].age, 86 | address: tableData[i].address, 87 | }); 88 | setTableData(newData); 89 | }; 90 | 91 | const onDelete = (key: string, e: any) => { 92 | e.preventDefault(); 93 | const newData = tableData.filter((item) => item.key !== key); 94 | setTableData(newData); 95 | setPageTween(false); 96 | }; 97 | 98 | const pageChange = () => { 99 | setPageTween(true); 100 | }; 101 | 102 | const columns = [ 103 | { title: 'Name', dataIndex: 'name', key: 'name' }, 104 | { title: 'Age', dataIndex: 'age', key: 'age' }, 105 | { title: 'Address', dataIndex: 'address', key: 'address' }, 106 | { 107 | title: 'Action', 108 | dataIndex: '', 109 | key: 'x', 110 | render: (text: string, record: any) => ( 111 | { 114 | onDelete(record.key, e); 115 | }} 116 | > 117 | Delete 118 | 119 | ), 120 | }, 121 | ]; 122 | const enterAnim = [ 123 | { 124 | opacity: 0, 125 | x: 30, 126 | // backgroundColor: '#fffeee', // tbody no backgroundColor 127 | duration: 0, 128 | }, 129 | { 130 | height: 0, 131 | duration: 200, 132 | type: 'from', 133 | delay: 250, 134 | ease: 'easeOutQuad', 135 | onComplete: onEnd, 136 | }, 137 | { 138 | opacity: 1, 139 | x: 0, 140 | duration: 250, 141 | ease: 'easeOutQuad', 142 | onComplete: rmStyle, 143 | }, 144 | // { delay: 1000, backgroundColor: '#fff', onComplete: rmStyle }, 145 | ]; 146 | const pageEnterAnim = [ 147 | { 148 | opacity: 0, 149 | duration: 0, 150 | }, 151 | { 152 | height: 0, 153 | duration: 150, 154 | type: 'from', 155 | delay: 150, 156 | ease: 'easeOutQuad', 157 | onComplete: onEnd, 158 | }, 159 | { 160 | opacity: 1, 161 | duration: 150, 162 | ease: 'easeOutQuad', 163 | onComplete: rmStyle, 164 | }, 165 | ]; 166 | const leaveAnim = [ 167 | { duration: 250, opacity: 0 }, 168 | { height: 0, duration: 200, ease: 'easeOutQuad' }, 169 | ]; 170 | const pageLeaveAnim = [ 171 | { duration: 150, opacity: 0 }, 172 | { height: 0, duration: 150, ease: 'easeOutQuad' }, 173 | ]; 174 | 175 | return ( 176 |
177 |
178 | 181 |
182 | 188 | 196 | 197 | 198 | ); 199 | }; 200 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: rc-tween-one 3 | order: 0 4 | --- 5 | 6 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "rc-tween-one", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": ".doc" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-tween-one", 3 | "version": "3.0.6", 4 | "description": "tween-one anim component for react", 5 | "typings": "es/index.d.ts", 6 | "engines": { 7 | "node": ">=8.x" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "react-component", 12 | "component", 13 | "react-tween-one", 14 | "tween", 15 | "react-tween", 16 | "tween-one", 17 | "animation", 18 | "animate", 19 | "rc-animation", 20 | "react-animation", 21 | "rc-animate", 22 | "react-animate", 23 | "motion", 24 | "rc-motion", 25 | "ant-motion" 26 | ], 27 | "homepage": "https://github.com/react-component/tween-one", 28 | "author": "155259966@qq.com", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/react-component/tween-one.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/react-component/tween-one/issues" 35 | }, 36 | "files": [ 37 | "es", 38 | "lib", 39 | "assets/*.css", 40 | "assets/*.less" 41 | ], 42 | "licenses": "MIT", 43 | "main": "./lib/index", 44 | "module": "./es/index", 45 | "sideEffects": false, 46 | "scripts": { 47 | "start": "dumi dev", 48 | "docs:build": "dumi build", 49 | "docs:deploy": "gh-pages -d docs-dist", 50 | "compile": "father-build", 51 | "deploy": "npm run docs:build && npm run docs:deploy", 52 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 53 | "test": "umi-test test", 54 | "test:coverage": "umi-test --coverage", 55 | "prepublishOnly": "npm run compile && np --tag=beta --no-cleanup --yolo --no-publish --any-branch", 56 | "lint": "eslint src/ --fix --ext .tsx,.ts", 57 | "lint:tsc": "tsc -p tsconfig.json --noEmit", 58 | "now-build": "npm run docs:build" 59 | }, 60 | "devDependencies": { 61 | "@ant-design/icons": "^4.3.0", 62 | "@types/enzyme": "^3.10.5", 63 | "@types/jest": "^25.2.1", 64 | "@types/lodash": "^4.14.135", 65 | "@types/react": "^16.8.19", 66 | "@types/react-dom": "^16.8.4", 67 | "@umijs/test": "^3.2.28", 68 | "antd": "^4.8.4", 69 | "dumi": "^1.1.0-rc.1", 70 | "enzyme": "^3.3.0", 71 | "enzyme-adapter-react-16": "^1.0.2", 72 | "enzyme-to-json": "^3.4.0", 73 | "eslint": "^7.14.0", 74 | "father": "^2.22.6", 75 | "father-build": "^1.18.6", 76 | "gh-pages": "^3.1.0", 77 | "husky": "^4.3.0", 78 | "np": "^6.0.3", 79 | "prettier": "^2.1.2", 80 | "react": "^16.9.0", 81 | "react-dom": "^16.9.0", 82 | "regenerator-runtime": "^0.13.7", 83 | "typescript": "^4.0.2" 84 | }, 85 | "peerDependencies": { 86 | "react": ">=16.9.0", 87 | "react-dom": ">=16.9.0" 88 | }, 89 | "dependencies": { 90 | "@babel/runtime": "^7.11.1", 91 | "style-utils": "^0.3.4", 92 | "tween-one": "^1.0.50" 93 | }, 94 | "husky": { 95 | "hooks": { 96 | "pre-commit": [ 97 | "npm run lint" 98 | ] 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/TweenOne.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, createElement, useEffect } from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import type { Tween } from 'tween-one'; 4 | import TweenOneJS from 'tween-one'; 5 | import { toStyleUpperCase, stylesToCss } from 'style-utils'; 6 | 7 | import type { IAnimProps, IAnimObject, TweenOneRef } from './type'; 8 | import { objectEqual, dataToArray } from './utils'; 9 | import { useIsomorphicLayoutEffect } from './utils/common'; 10 | 11 | const TweenOne: TweenOneRef = React.forwardRef( 12 | ( 13 | { 14 | component = 'div', 15 | componentProps, 16 | animation, 17 | attr, 18 | paused, 19 | reverse, 20 | repeat, 21 | repeatDelay, 22 | yoyo, 23 | moment, 24 | onChange, 25 | onChangeTimeline, 26 | resetStyle, 27 | killPrevAnim = true, 28 | ...props 29 | }, 30 | ref, 31 | ) => { 32 | const { children, className, style = {} } = props || {}; 33 | 34 | const domRef = useRef(); 35 | const prevAnim = useRef(); 36 | const animRef = useRef(); 37 | const commonFunc = ( 38 | key: 'paused' | 'moment' | 'reverse', 39 | value: boolean | number | undefined, 40 | ) => { 41 | const tween: Tween = animRef.current!; 42 | if (tween) { 43 | if (key === 'moment') { 44 | if (typeof value === 'number') { 45 | tween.goto(value, paused); 46 | } 47 | return; 48 | } 49 | tween[key] = !!value; 50 | } 51 | }; 52 | useIsomorphicLayoutEffect(() => { 53 | commonFunc('paused', paused); 54 | }, [paused]); 55 | // yoyo, moment, reverse, repeat, repeatDelay 56 | useIsomorphicLayoutEffect(() => { 57 | commonFunc('moment', moment); 58 | }, [moment]); 59 | 60 | useIsomorphicLayoutEffect(() => { 61 | commonFunc('reverse', reverse); 62 | }, [reverse]); 63 | useIsomorphicLayoutEffect(() => { 64 | if (!domRef.current) { 65 | return console.warn('Warning: TweenOne domRef is error.'); 66 | } 67 | // 动画写在标签上,手动对比; 68 | if (!objectEqual(animation, prevAnim.current)) { 69 | const doms: any[] = dataToArray(domRef.current) 70 | .map((item) => 71 | item instanceof Element || !(item instanceof React.Component) 72 | ? item 73 | : findDOMNode(item), 74 | ) 75 | .filter((item, i) => { 76 | if (!(item instanceof Element)) { 77 | console.warn(`Warning: TweenOne tag[${i}] is not dom.`); 78 | return false; 79 | } 80 | return item; 81 | }); 82 | 83 | if (animRef.current && killPrevAnim) { 84 | animRef.current.kill(); 85 | } 86 | if (resetStyle && animRef.current) { 87 | const s = !component ? { ...style, ...children.props.style } : style 88 | const styleStr = Object.keys(s) 89 | .map((key: string) => `${toStyleUpperCase(key)}:${stylesToCss(key, s[key])}`) 90 | .join(';'); 91 | doms.forEach((item: Element) => { 92 | item.setAttribute('style', styleStr); 93 | // dom.style.cssText = styleStr; 94 | delete item._tweenOneVars; // eslint-disable-line no-underscore-dangle 95 | }); 96 | } 97 | animRef.current = 98 | animation && 99 | TweenOneJS(doms, { 100 | animation, 101 | attr, 102 | yoyo, 103 | moment, 104 | repeat, 105 | reverse, 106 | paused, 107 | repeatDelay, 108 | onChange, 109 | onChangeTimeline, 110 | }); 111 | prevAnim.current = animation; 112 | } 113 | }, [animation]); 114 | useEffect( 115 | () => () => { 116 | if (animRef.current && animRef.current.kill) { 117 | animRef.current.kill(); 118 | } 119 | }, 120 | [], 121 | ); 122 | 123 | const refFunc = (c: any) => { 124 | domRef.current = c; 125 | if (ref && 'current' in ref) { 126 | ref.current = c; 127 | } else if (typeof ref === 'function') { 128 | ref(c); 129 | } 130 | }; 131 | 132 | if ( 133 | !component && 134 | children && 135 | typeof children !== 'string' && 136 | typeof children !== 'boolean' && 137 | typeof children !== 'number' 138 | ) { 139 | const childrenProps = children.props; 140 | const { style: childStyle, className: childClass = '' } = childrenProps || {}; 141 | // 合并 style 与 className。 142 | const newStyle = { ...childStyle, ...style }; 143 | const newClassName = className ? `${className} ${childClass}`.trim() : childClass; 144 | return React.cloneElement(children, { 145 | style: newStyle, 146 | ref: refFunc, 147 | className: [...new Set(newClassName.split(/\s+/))].join(' ').trim() || undefined, 148 | }); 149 | } 150 | if (!component) { 151 | console.warn('Warning: component is null, children must be ReactElement.'); 152 | return children; 153 | } 154 | return createElement(component, { 155 | ref: refFunc, 156 | ...props, 157 | ...componentProps, 158 | }); 159 | }, 160 | ); 161 | TweenOne.isTweenOne = true; 162 | TweenOne.displayName = 'TweenOne'; 163 | 164 | export default TweenOne; 165 | -------------------------------------------------------------------------------- /src/TweenOneGroup.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement, ReactText } from 'react'; 2 | import { cloneElement } from 'react'; 3 | import React, { useRef, useEffect, useState, createElement } from 'react'; 4 | import type { 5 | IGroupProps, 6 | IAnimObject, 7 | TweenOneGroupRef, 8 | ITimelineCallBack, 9 | IObject, 10 | } from './type'; 11 | import { 12 | getChildrenFromProps, 13 | toArrayChildren, 14 | transformArguments, 15 | mergeChildren, 16 | findChildInChildrenByKey, 17 | } from './utils/group'; 18 | import { useIsomorphicLayoutEffect, windowIsUndefined } from './utils/common'; 19 | 20 | import TweenOne from './TweenOne'; 21 | 22 | const TweenOneGroup: TweenOneGroupRef = React.forwardRef((props, ref) => { 23 | const { 24 | component = 'div', 25 | componentProps = {}, 26 | leave: leaveAnim = { x: -50, opacity: 0 }, 27 | enter: enterAnim = { x: 50, opacity: 0, type: 'from' }, 28 | appear: appearBool = true, 29 | resetStyle = true, 30 | animatingClassName = ['tween-one-entering', 'tween-one-leaving'], 31 | onEnd = () => {}, 32 | exclusive = false, 33 | ...tagProps 34 | } = props; 35 | const keysToEnter = useRef([]); 36 | const keysToLeave = useRef([]); 37 | const saveTweenTag = useRef({}); 38 | const oneEnter = useRef(false); 39 | const animQueue = useRef([]); 40 | const isTween = useRef({}); 41 | const cChild = toArrayChildren(getChildrenFromProps(props)); 42 | const currentChildren = useRef(cChild); 43 | const [children, setChild] = useState(cChild); 44 | 45 | const getTweenChild = (child: ReactElement, p = {}) => { 46 | const key: string | number = child.key as string; 47 | saveTweenTag.current[key] = React.createElement( 48 | TweenOne, 49 | { 50 | ...p, 51 | key, 52 | component: null, 53 | }, 54 | child, 55 | ); 56 | return saveTweenTag.current[key]; 57 | }; 58 | const setClassName = (name: string, isEnter: boolean) => { 59 | let className = name.replace(animatingClassName[isEnter ? 1 : 0], '').trim(); 60 | if (className.indexOf(animatingClassName[isEnter ? 0 : 1]) === -1) { 61 | className = `${className} ${animatingClassName[isEnter ? 0 : 1]}`.trim(); 62 | } 63 | return className; 64 | }; 65 | const changeChildren = (nextChildren: ReactElement[], currentChild: ReactElement[]) => { 66 | const newChildren: ReactElement[] = mergeChildren(currentChild, nextChildren); 67 | keysToEnter.current = []; 68 | keysToLeave.current = []; 69 | nextChildren.forEach((c) => { 70 | if (!c) { 71 | return; 72 | } 73 | const { key } = c; 74 | const hasPrev = findChildInChildrenByKey(currentChild, key); 75 | // 如果当前 key 已存在 saveTweenTag 里,,刷新 child; 76 | if (key && saveTweenTag.current[key]) { 77 | saveTweenTag.current[key] = React.cloneElement(saveTweenTag.current[key], {}, c); 78 | } 79 | if (!hasPrev && key) { 80 | keysToEnter.current.push(key); 81 | } 82 | }); 83 | 84 | currentChild.forEach((c) => { 85 | if (!c) { 86 | return; 87 | } 88 | const { key } = c; 89 | const hasNext = findChildInChildrenByKey(nextChildren, key); 90 | if (!hasNext && key) { 91 | keysToLeave.current.push(key); 92 | delete saveTweenTag.current[key]; 93 | } 94 | }); 95 | return newChildren; 96 | }; 97 | 98 | const reAnimQueue = () => { 99 | if (!Object.keys(isTween.current).length && animQueue.current.length) { 100 | // 取最后一个继续动画; 101 | const child = changeChildren( 102 | animQueue.current[animQueue.current.length - 1], 103 | currentChildren.current, 104 | ); 105 | setChild(child); 106 | animQueue.current = []; 107 | } 108 | }; 109 | 110 | const onChange = (key: string | number | null, type: string, obj: ITimelineCallBack) => { 111 | const tag = obj.targets as IObject; 112 | const classIsSvg = typeof tag!.className === 'object' && 'baseVal' in tag!.className; 113 | const isEnter = type === 'enter' || type === 'appear'; 114 | if (key && obj.mode === 'onTimelineComplete') { 115 | delete isTween.current[key]; 116 | if (classIsSvg) { 117 | tag.className.baseVal = tag.className.baseVal 118 | .replace(animatingClassName[isEnter ? 0 : 1], '') 119 | .trim(); 120 | } else { 121 | tag.className = tag.className.replace(animatingClassName[isEnter ? 0 : 1], '').trim(); 122 | } 123 | if (isEnter) { 124 | keysToEnter.current.splice(keysToEnter.current.indexOf(key), 1); 125 | if (!keysToEnter.current.length) { 126 | // enter 不会触发 did update, 手动触发一次; 127 | reAnimQueue(); 128 | } 129 | } else if (type === 'leave') { 130 | keysToLeave.current.splice(keysToLeave.current.indexOf(key), 1); 131 | currentChildren.current = currentChildren.current.filter((child) => key !== child.key); 132 | if (!keysToLeave.current.length) { 133 | const currentChildrenKeys = currentChildren.current.map((item) => item.key); 134 | Object.keys(saveTweenTag.current).forEach(($key) => { 135 | if (currentChildrenKeys.indexOf($key) === -1) { 136 | delete saveTweenTag.current[$key]; 137 | } 138 | }); 139 | 140 | setChild(currentChildren.current); 141 | } 142 | } 143 | onEnd({ key, type, target: obj.targets as any }); 144 | } 145 | }; 146 | const getCoverAnimation = (child: ReactElement, i: number, type: string) => { 147 | let animation: IAnimObject = type === 'leave' ? leaveAnim : enterAnim; 148 | if (type === 'appear') { 149 | const appear = transformArguments(appearBool, child.key, i); 150 | animation = (appear && enterAnim) || null; 151 | } 152 | const animate = transformArguments(animation, child.key, i); 153 | const onChangeCb = (obj: ITimelineCallBack) => { 154 | onChange(child.key, type, obj); 155 | }; 156 | const className = 157 | type === 'appear' && !appearBool 158 | ? child.props.className 159 | : setClassName(child.props.className || '', type === 'enter' || type === 'appear') || 160 | undefined; 161 | const p = { 162 | key: child.key, 163 | animation: animate, 164 | onChangeTimeline: onChangeCb, 165 | resetStyle, 166 | className, 167 | }; 168 | if ( 169 | (child.key && keysToEnter.current.concat(keysToLeave.current).indexOf(child.key) >= 0) || 170 | (!oneEnter.current && animation) 171 | ) { 172 | if (child.key && !saveTweenTag.current[child.key]) { 173 | isTween.current[child.key] = type; 174 | } 175 | } 176 | 177 | return getTweenChild(child, p); 178 | }; 179 | useIsomorphicLayoutEffect(() => { 180 | if (oneEnter.current) { 181 | const nextChild = toArrayChildren(props.children).filter((c) => c); 182 | const currentChild = toArrayChildren(currentChildren.current); 183 | // 如果还在动画,暂存动画队列里,等前一次动画结束后再启动最后次的更新动画 184 | if (Object.keys(isTween.current).length && !exclusive) { 185 | animQueue.current.push(nextChild); 186 | } else { 187 | setChild(changeChildren(nextChild, currentChild)); 188 | } 189 | } 190 | }, [props.children]); 191 | useIsomorphicLayoutEffect(() => { 192 | reAnimQueue(); 193 | }); 194 | useEffect(() => { 195 | oneEnter.current = true; 196 | }, []); 197 | 198 | currentChildren.current = children; 199 | 200 | const childrenToRender = children.map((child: ReactElement, i: number) => { 201 | if (!child || !child.key) { 202 | return child; 203 | } 204 | const { key } = child; 205 | if (keysToLeave.current.indexOf(key) >= 0) { 206 | return getCoverAnimation(child, keysToLeave.current.indexOf(key), 'leave'); 207 | } 208 | if ( 209 | (keysToEnter.current.indexOf(key) >= 0 || 210 | (isTween.current[key] && keysToLeave.current.indexOf(key) === -1)) && 211 | !(isTween.current[key] === 'enter' && saveTweenTag.current[key]) 212 | ) { 213 | /** 214 | * 1. 在 key 在 enter 里。 215 | * 2. 出场未结束,触发进场, this.isTween[key] 为 leave, key 在 enter 里。 216 | * 3. 状态为 enter 且 tweenTag 里有值时,不执行重载动画属性,直接调用 tweenTag 里的。 217 | */ 218 | return getCoverAnimation(child, keysToEnter.current.indexOf(key), 'enter'); 219 | } 220 | if (!oneEnter.current) { 221 | return getCoverAnimation(child, i, 'appear'); 222 | } 223 | return saveTweenTag.current[key]; 224 | }); 225 | if (windowIsUndefined) { 226 | if (!component) { 227 | return <>{props.children}; 228 | } 229 | return createElement(component, { ...tagProps, ...componentProps, ref }, props.children); 230 | } 231 | if (!component) { 232 | return childrenToRender[0] ? cloneElement(childrenToRender[0], { ref }) : null; 233 | } 234 | return createElement(component, { ...tagProps, ...componentProps, ref }, childrenToRender); 235 | }); 236 | 237 | TweenOneGroup.displayName = 'TweenOneGroup'; 238 | TweenOneGroup.isTweenOneGroup = true; 239 | 240 | export default TweenOneGroup; 241 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Ticker, Plugins, Easing } from 'tween-one'; 2 | import TweenOne from './TweenOne'; 3 | import TweenOneGroup from './TweenOneGroup'; 4 | import ChildrenPlugin from './plugin/ChildrenPlugin'; 5 | import PathMotionPlugin from './plugin/PathMotionPlugin'; 6 | import SvgDrawPlugin from './plugin/SvgDrawPlugin'; 7 | import SvgMorphPlugin from './plugin/SvgMorphPlugin'; 8 | 9 | export * from './type'; 10 | 11 | export { 12 | Easing, 13 | Ticker, 14 | Plugins, 15 | TweenOneGroup, 16 | ChildrenPlugin, 17 | PathMotionPlugin, 18 | SvgDrawPlugin, 19 | SvgMorphPlugin, 20 | }; 21 | 22 | TweenOne.plugins = Plugins; 23 | TweenOne.ticker = Ticker; 24 | TweenOne.easing = Easing; 25 | 26 | export default TweenOne; 27 | -------------------------------------------------------------------------------- /src/plugin/ChildrenPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { IObject } from '../type'; 2 | 3 | interface IFormatMoney { 4 | thousand?: string; 5 | decimal?: string; 6 | } 7 | interface IVars { 8 | value: number; 9 | floatLength: number; 10 | formatMoney?: IFormatMoney; 11 | } 12 | 13 | class ChildrenPlugin { 14 | static key: string = 'innerHTML'; 15 | 16 | static className: string = 'Children'; 17 | 18 | start?: IVars; 19 | 20 | startAt?: IObject; 21 | 22 | target?: HTMLElement; 23 | 24 | vars: IVars; 25 | key: string; 26 | // eslint-disable-next-line no-useless-constructor,no-empty-function 27 | constructor(vars: IVars, key: string) { 28 | this.vars = vars; 29 | this.key = key; 30 | } 31 | 32 | getAnimStart = () => { 33 | const { target, vars, startAt, key } = this; 34 | const { formatMoney } = vars; 35 | const opts = { 36 | thousand: (formatMoney && formatMoney.thousand) || ',', 37 | decimal: (formatMoney && formatMoney.decimal) || '.', 38 | }; 39 | const rep = new RegExp(`\\${opts.thousand}`, 'g'); 40 | 41 | this.start = startAt![key] || { 42 | value: parseFloat(target!.innerHTML.replace(rep, '').replace(opts.decimal, '.')) || 0, 43 | }; 44 | return this.start; 45 | }; 46 | 47 | toMoney = (v: string, _opts: IFormatMoney) => { 48 | const opts = { 49 | thousand: _opts.thousand || ',', 50 | decimal: _opts.decimal || '.', 51 | }; 52 | const negative = parseFloat(v) < 0 ? '-' : ''; 53 | const numberArray = v.split('.'); 54 | const base = Math.abs(parseInt(numberArray[0], 10)).toString(); 55 | const mod = base.length > 3 ? base.length % 3 : 0; 56 | const decimal = numberArray[1]; 57 | return `${negative}${mod ? `${base.substr(0, mod)}${opts.thousand}` : ''}${base 58 | .substr(mod) 59 | .replace(/(\d{3})(?=\d)/g, `$1${opts.thousand}`)}${ 60 | decimal ? `${opts.decimal}${decimal}` : '' 61 | }`; 62 | }; 63 | 64 | render = (ratio: number) => { 65 | const { value, floatLength, formatMoney } = this.vars; 66 | let v: number | string = (value - this.start!.value) * ratio + this.start!.value; 67 | if (typeof floatLength === 'number') { 68 | if (floatLength) { 69 | v = v.toFixed(floatLength); 70 | const numberArray = v.toString().split('.'); 71 | let decimal = numberArray[1] || ''; 72 | decimal = decimal.length > floatLength ? decimal.substring(0, floatLength) : decimal; 73 | const l = floatLength - decimal.length; 74 | if (l) { 75 | Array(l) 76 | .fill(0) 77 | .forEach((num) => { 78 | decimal += `${num}`; 79 | }); 80 | } 81 | v = `${numberArray[0]}.${decimal}`; 82 | } else { 83 | v = Math.round(v); 84 | } 85 | } 86 | v = formatMoney ? this.toMoney(`${v}`, formatMoney) : v; 87 | return v; 88 | }; 89 | } 90 | 91 | export default ChildrenPlugin; 92 | -------------------------------------------------------------------------------- /src/plugin/PathMotionPlugin.ts: -------------------------------------------------------------------------------- 1 | import PathMotionPlugin from 'tween-one/es/plugins/PathMotionPlugin'; 2 | 3 | export default PathMotionPlugin; 4 | -------------------------------------------------------------------------------- /src/plugin/SvgDrawPlugin.ts: -------------------------------------------------------------------------------- 1 | import SvgDrawPlugin from 'tween-one/es/plugins/SvgDrawPlugin'; 2 | 3 | export default SvgDrawPlugin; 4 | -------------------------------------------------------------------------------- /src/plugin/SvgMorphPlugin.ts: -------------------------------------------------------------------------------- 1 | import SvgMorph from 'tween-one/es/plugins/SvgMorphPlugin'; 2 | 3 | export default SvgMorph; 4 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | import type { IAnimObject as Anim, IMode, ITimelineCallBack as TimelineCallBack } from 'tween-one'; 3 | 4 | export type IObject = Record; 5 | 6 | export type ITimelineCallBack = TimelineCallBack; 7 | export interface ICallBack { 8 | mode?: IMode; 9 | moment?: number; 10 | ratio?: number; 11 | index: number; 12 | repeat?: number; 13 | timelineMoment?: number; 14 | vars?: IObject | IObject[]; 15 | targets?: IObject | IObject[]; 16 | } 17 | 18 | interface AnimObject extends Anim { 19 | Children?: { 20 | value?: number; 21 | floatLength?: number; 22 | formatMoney?: true | { thousand?: string; decimal?: string }; 23 | }; 24 | } 25 | 26 | export type AnimObjectOrArray = AnimObject | AnimObject[]; 27 | 28 | export type IAnimObject = AnimObjectOrArray | ((e: { key: string; index: number }) => AnimObject); 29 | 30 | interface AllHTMLAttributes 31 | extends Omit, 'crossOrigin'>, 32 | React.AllHTMLAttributes {} 33 | export interface IAnimProps extends Omit { 34 | style?: React.CSSProperties; 35 | children?: any; 36 | animation?: AnimObjectOrArray; 37 | paused?: boolean; 38 | delay?: number; 39 | reverse?: boolean; 40 | repeatDelay?: number; 41 | repeat?: number; 42 | yoyo?: boolean; 43 | ref?: React.Ref; 44 | onChange?: (v: ICallBack) => void; 45 | onChangeTimeline?: (v: ITimelineCallBack) => void; 46 | moment?: number; 47 | attr?: boolean; 48 | resetStyle?: boolean; 49 | component?: string | null | React.FC | React.ComponentClass; 50 | componentProps?: IObject; 51 | forcedJudg?: IObject; 52 | killPrevAnim?: boolean; 53 | regionStartTime?: number; 54 | regionEndTime?: number; 55 | } 56 | 57 | export interface IEndCallback { 58 | key?: string | React.ReactText; 59 | type?: string; 60 | target?: HTMLElement | HTMLElement[]; 61 | } 62 | 63 | export interface IGroupProps extends Omit { 64 | ref?: React.Ref; 65 | appear?: boolean; 66 | enter?: IAnimObject; 67 | leave?: IAnimObject; 68 | animatingClassName?: string[]; 69 | exclusive?: boolean; 70 | resetStyle?: boolean; 71 | onEnd?: (e: IEndCallback) => void; 72 | component?: string | null | React.FC | React.ComponentClass; 73 | componentProps?: IObject; 74 | } 75 | 76 | export interface TweenOneRef extends React.ForwardRefExoticComponent { 77 | isTweenOne?: boolean; 78 | plugins?: any; 79 | ticker?: any; 80 | easing?: any; 81 | } 82 | 83 | export interface TweenOneGroupRef extends React.ForwardRefExoticComponent { 84 | isTweenOneGroup?: boolean; 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const windowIsUndefined = !( 4 | typeof window !== 'undefined' && 5 | window.document && 6 | window.document.createElement 7 | ); 8 | 9 | export const useIsomorphicLayoutEffect = windowIsUndefined 10 | ? React.useEffect 11 | : React.useLayoutEffect; 12 | -------------------------------------------------------------------------------- /src/utils/group.ts: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from 'react'; 2 | import React from 'react'; 3 | 4 | import type { IObject } from '../type'; 5 | 6 | 7 | export function toArrayChildren(children: any) { 8 | const ret: any[] = []; 9 | React.Children.forEach(children, (c) => { 10 | ret.push(c); 11 | }); 12 | return ret; 13 | } 14 | 15 | export function findChildInChildrenByKey(children: ReactElement[], key: string | number | null) { 16 | let ret: any = null; 17 | if (children) { 18 | children.forEach((c) => { 19 | if (ret || !c) { 20 | return; 21 | } 22 | if (c.key === key) { 23 | ret = c; 24 | } 25 | }); 26 | } 27 | return ret; 28 | } 29 | 30 | export function mergeChildren(prev: ReactElement[], next: ReactElement[]) { 31 | let ret: any[] = []; 32 | // For each key of `next`, the list of keys to insert before that key in 33 | // the combined list 34 | const nextChildrenPending: any = {}; 35 | let pendingChildren: any[] = []; 36 | let followChildrenKey: string | number | null = null; 37 | prev.forEach((c) => { 38 | if (!c) { 39 | return; 40 | } 41 | if (c.key && findChildInChildrenByKey(next, c.key)) { 42 | if (pendingChildren.length) { 43 | nextChildrenPending[c.key] = pendingChildren; 44 | pendingChildren = []; 45 | } 46 | followChildrenKey = c.key; 47 | } else if (c.key) { 48 | pendingChildren.push(c); 49 | } 50 | }); 51 | if (!followChildrenKey) { 52 | ret = ret.concat(pendingChildren); 53 | } 54 | 55 | next.forEach((c) => { 56 | if (!c) { 57 | return; 58 | } 59 | if (c.key && nextChildrenPending.hasOwnProperty(c.key)) { 60 | // eslint-disable-line no-prototype-builtins 61 | ret = ret.concat(nextChildrenPending[c.key]); 62 | } 63 | ret.push(c); 64 | if (c.key === followChildrenKey) { 65 | ret = ret.concat(pendingChildren); 66 | } 67 | }); 68 | 69 | return ret; 70 | } 71 | 72 | export function transformArguments(arg: any, key: string | number | null, i: number) { 73 | let result; 74 | if (typeof arg === 'function') { 75 | result = arg({ 76 | key, 77 | index: i, 78 | }); 79 | } else { 80 | result = arg; 81 | } 82 | return result; 83 | } 84 | 85 | export function getChildrenFromProps(props: IObject) { 86 | return props && props.children; 87 | } 88 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function dataToArray(vars: any) { 2 | if (!vars && vars !== 0) { 3 | return []; 4 | } 5 | if (Array.isArray(vars)) { 6 | return vars; 7 | } 8 | return [vars]; 9 | } 10 | 11 | function deepEql(a: any, b: any) { 12 | if (!a || !b) { 13 | return false; 14 | } 15 | const $a = Object.keys(a); 16 | const $b = Object.keys(b); 17 | if ($a.length && $b.length && $a.length === $b.length) { 18 | return !$a.some((key) => { 19 | let aa = a[key]; 20 | let bb = b[key]; 21 | if (Array.isArray(aa) && Array.isArray(bb)) { 22 | const aaa = aa.join(); 23 | const bbb = bb.join(); 24 | if (aaa === bbb && !aaa.match(/\[object object\]/gi)) { 25 | aa = aaa; 26 | bb = bbb; 27 | } 28 | } 29 | return aa !== bb; 30 | }); 31 | } 32 | return false; 33 | } 34 | 35 | export function objectEqual(obj1: any, obj2: any) { 36 | if (obj1 === obj2 || deepEql(obj1, obj2)) { 37 | return true; 38 | } 39 | if (!obj1 || !obj2 || Object.keys(obj1).length !== Object.keys(obj2).length) { 40 | return false; 41 | } 42 | // animation 写在标签上的进行判断是否相等, 判断每个参数有没有 function; 43 | let equalBool = true; 44 | const setEqualBool = ($a: any, $b: any) => { 45 | const objA = Object.keys($a).length > Object.keys($b).length ? $a : $b; 46 | const objB = Object.keys($a).length > Object.keys($b).length ? $b : $a; 47 | Object.keys(objA).forEach((key) => { 48 | // 如果前面有参数匹配不相同则直接返回; 49 | if (!equalBool) { 50 | return; 51 | } 52 | if (!(key in objB)) { 53 | equalBool = false; 54 | } 55 | 56 | if (typeof objA[key] === 'object' && typeof objB[key] === 'object') { 57 | equalBool = objectEqual(objA[key], objB[key]); 58 | } else if (typeof objA[key] === 'function' && typeof objB[key] === 'function') { 59 | if (objA[key].toString().replace(/\s+/g, '') !== objB[key].toString().replace(/\s+/g, '')) { 60 | equalBool = false; 61 | } 62 | } else if (objA[key] !== objB[key]) { 63 | equalBool = false; 64 | } 65 | }); 66 | }; 67 | 68 | if (Array.isArray(obj1) && Array.isArray(obj2)) { 69 | obj1.forEach((item, i) => { 70 | setEqualBool(item, obj2[i]); 71 | }); 72 | } else { 73 | setEqualBool(obj1, obj2); 74 | } 75 | return equalBool; 76 | } 77 | -------------------------------------------------------------------------------- /tests/tweenOne.test.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | import TweenOne, { Plugins, Ticker } from '../src'; 7 | 8 | import ChildrenPlugin from '../src/plugin/ChildrenPlugin'; 9 | 10 | import PathMotionPlugin from '../src/plugin/PathMotionPlugin'; 11 | 12 | Plugins.push(ChildrenPlugin); 13 | 14 | Plugins.push(PathMotionPlugin); 15 | 16 | Enzyme.configure({ adapter: new Adapter() }); 17 | 18 | const TweenComp = (props) => { 19 | return ( 20 |
21 | 22 |
动画
23 |
24 |
25 | ); 26 | }; 27 | 28 | describe('rc-tween-one', () => { 29 | let instance; 30 | it('single tween-one', (done) => { 31 | instance = mount( 32 |
33 | 49 |
动画
50 |
51 |
, 52 | ); 53 | const TweenNode = instance.instance().children[0]; 54 | console.log('start:', TweenNode.style.top); 55 | expect(parseFloat(TweenNode.style.top)).toBe(0); 56 | Ticker.timeout(() => { 57 | // 默认时间为450,用500是肯定过值; 58 | console.log('end:', TweenNode.style.top); 59 | expect(parseFloat(TweenNode.style.top)).toBe(100); 60 | done(); 61 | }, 600); 62 | }); 63 | it('tween-one repeat -1 and yoyo', (done) => { 64 | instance = mount( 65 |
66 | 74 |
动画
75 |
76 |
, 77 | ); 78 | const TweenNode = instance.instance().children[0]; 79 | console.log('start:', TweenNode.style.top); 80 | expect(parseFloat(TweenNode.style.top)).toBe(0); 81 | Ticker.timeout(() => { 82 | console.log('end:', TweenNode.style.top); 83 | expect(parseFloat(TweenNode.style.top)).toBeLessThan(2); 84 | done(); 85 | }, 900); 86 | }); 87 | it('single tween-one duration is 0', (done) => { 88 | let complete; 89 | instance = mount( 90 |
91 | 100 |
动画
101 |
102 |
, 103 | ); 104 | Ticker.timeout(() => { 105 | console.log(complete); 106 | expect(complete).toBe(1); 107 | done(); 108 | }, 34); 109 | }); 110 | it('timeline tween-one', (done) => { 111 | instance = mount( 112 |
113 | 117 |
动画
118 |
119 |
, 120 | ); 121 | const child = instance.instance().children[0]; 122 | 123 | Ticker.timeout(() => { 124 | console.log('top===100,data:', child.style.top, '########', 'left>0,data:', child.style.left); 125 | expect(parseFloat(child.style.top)).toBe(100); 126 | expect(parseFloat(child.style.left)).toBeGreaterThan(0); 127 | Ticker.timeout(() => { 128 | console.log( 129 | 'top<100,data:', 130 | child.style.top, 131 | '########', 132 | 'left===0,data:', 133 | child.style.left, 134 | ); 135 | expect(parseFloat(child.style.left)).toBe(100); 136 | expect(parseFloat(child.style.top)).toBeLessThan(100); 137 | Ticker.timeout(() => { 138 | console.log( 139 | 'top===0,data:', 140 | child.style.top, 141 | '########', 142 | 'left<100,data:', 143 | child.style.left, 144 | ); 145 | expect(parseFloat(child.style.left)).toBeLessThan(100); 146 | expect(parseFloat(child.style.top)).toBe(0); 147 | Ticker.timeout(() => { 148 | console.log( 149 | 'top===0,data:', 150 | child.style.top, 151 | '########', 152 | 'left===0,data:', 153 | child.style.left, 154 | ); 155 | expect(parseFloat(child.style.top)).toBe(0); 156 | expect(parseFloat(child.style.left)).toBe(0); 157 | done(); 158 | }, 450); 159 | }, 450); 160 | }, 450); 161 | }, 500); 162 | }); 163 | it('repeat tween-one', (done) => { 164 | instance = mount( 165 |
166 | 170 |
动画
171 |
172 |
, 173 | ); 174 | const child = instance.instance().children[0]; 175 | console.log('start:', child.style.top); 176 | expect(parseFloat(child.style.top)).toBe(0); 177 | Ticker.timeout(() => { 178 | expect(parseFloat(child.style.top)).toBeGreaterThan(95); 179 | console.log('20 milliseconds before the first repeat end, top>95, data:', child.style.top); 180 | Ticker.timeout(() => { 181 | expect(parseFloat(child.style.top)).toBeLessThan(10); 182 | console.log( 183 | '20 ms after the beginning of the second repeat, top<10, data', 184 | child.style.top, 185 | ); 186 | Ticker.timeout(() => { 187 | expect(parseFloat(child.style.top)).toBe(100); 188 | console.log('repeat end,top:', child.style.top); 189 | done(); 190 | }, 450); 191 | }, 340); 192 | }, 430); 193 | }); 194 | it('attr is true tween-one', (done) => { 195 | instance = mount( 196 | 197 | 206 | , 207 | ); 208 | const child = instance.instance().children[0]; 209 | Ticker.timeout(() => { 210 | const cx = child.getAttribute('cx'); 211 | console.log('svg cx:', cx); 212 | expect(parseFloat(cx)).toBe(50); 213 | done(); 214 | }, 500); 215 | }); 216 | it('type is from tween-one', (done) => { 217 | instance = mount( 218 |
219 | 220 |
动画
221 |
222 |
, 223 | ); 224 | const child = instance.instance().children[0]; 225 | console.log(`default:${child.style.top}`); 226 | expect(parseFloat(child.style.top)).toBe(100); 227 | Ticker.timeout(() => { 228 | expect(parseFloat(child.style.top)).toBeGreaterThan(95); 229 | console.log(`start:${child.style.top}`); 230 | Ticker.timeout(() => { 231 | expect(parseFloat(child.style.top)).toBe(0); 232 | console.log(`end:${child.style.top}`); 233 | done(); 234 | }, 450); 235 | }, 30); 236 | }); 237 | it('is update Animation', (done) => { 238 | instance = mount( 239 | , 243 | ); 244 | let child = instance.find('.wrapper').instance().children[0]; 245 | Ticker.timeout(() => { 246 | expect(parseFloat(child.style.top)).toBeGreaterThan(99); 247 | console.log(`child top:${child.style.top}`); 248 | instance.setProps({ 249 | animation: { left: 100, y: 100 }, 250 | }); 251 | expect(instance.props().animation.left).toBe(100); 252 | child = instance.find('.wrapper').instance().children[0]; 253 | Ticker.timeout(() => { 254 | expect(parseFloat(child.style.left)).toBeGreaterThan(99); 255 | console.log(`child left:${child.style.left}`); 256 | done(); 257 | }, 540); 258 | }, 1040); 259 | }); 260 | it('is update Animation2', (done) => { 261 | instance = mount(); 262 | let child = instance.find('.wrapper').instance().children[0]; 263 | Ticker.timeout(() => { 264 | expect(parseFloat(child.style.top)).toBeGreaterThan(99); 265 | console.log(`child top:${child.style.top}`); 266 | instance.setProps({ 267 | animation: { left: 100 }, 268 | }); 269 | expect(instance.props().animation.left).toBe(100); 270 | child = instance.find('.wrapper').instance().children[0]; 271 | Ticker.timeout(() => { 272 | expect(parseFloat(child.style.left)).toBeGreaterThan(99); 273 | console.log(`child left:${child.style.left}`); 274 | done(); 275 | }, 540); 276 | }, 1040); 277 | }); 278 | it('is update Animation no change', (done) => { 279 | instance = mount( 280 | { 286 | console.log('start'); 287 | }, 288 | }, 289 | { y: 100, a: [100, 100] }, 290 | ]} 291 | style={{ position: 'relative' }} 292 | />, 293 | ); 294 | const child = instance.find('.wrapper').instance().children[0]; 295 | Ticker.timeout(() => { 296 | console.log(`child top:${child.style.top}`); 297 | expect(parseFloat(child.style.top)).toBeGreaterThan(90); 298 | 299 | instance.setProps({ 300 | animation: [ 301 | { 302 | top: 100, 303 | color: 'red', 304 | onStart: () => { 305 | console.log('start'); 306 | }, 307 | }, 308 | { y: 100, a: [100, 100] }, 309 | ], 310 | }); 311 | 312 | Ticker.timeout(() => { 313 | console.log(`child top:${child.style.top}`); 314 | expect(parseFloat(child.style.top)).toBe(100); 315 | done(); 316 | }, 50); 317 | }, 440); 318 | }); 319 | it('component is null tween-one', (done) => { 320 | instance = mount( 321 |
322 | 323 |
动画
324 |
325 |
, 326 | ); 327 | const child = instance.find('.child').instance(); 328 | console.log(`start: ${child.style.top}`); 329 | expect(parseFloat(child.style.top)).toBe(0); 330 | Ticker.timeout(() => { 331 | expect(parseFloat(child.style.top)).toBe(100); 332 | console.log(`end: ${child.style.top}`); 333 | done(); 334 | }, 450); 335 | }); 336 | it('component is null children is string warning', () => { 337 | instance = mount( 338 |
339 | 340 | 动画 341 | 342 |
, 343 | ); 344 | const child = instance.find('div').instance().children; 345 | console.log(child.length, instance.text()); 346 | 347 | expect(child.length).toBe(0); 348 | expect(instance.text()).toEqual('动画'); 349 | }); 350 | it('is resetStyle tween-one', (done) => { 351 | instance = mount( 352 | , 353 | ); 354 | let child = instance.find('.wrapper').instance().children[0]; 355 | // eslint-disable-next-line no-underscore-dangle 356 | console.log('top', child._tweenOneVars, child.style, child.id); 357 | Ticker.timeout(() => { 358 | instance.setProps({ 359 | animation: [{ left: 100 }, { opacity: 0 }], 360 | }); 361 | Ticker.timeout(() => { 362 | child = instance.find('.wrapper').instance().children[0]; 363 | // eslint-disable-next-line no-underscore-dangle 364 | console.log('top', child._tweenOneVars, child.style, child.id); 365 | // eslint-disable-next-line no-underscore-dangle 366 | expect(child._tweenOneVars.style.top).toEqual(undefined); 367 | done(); 368 | }, 500); 369 | }, 100); 370 | }); 371 | it('is reverse', (done) => { 372 | instance = mount( 373 | , 379 | ); 380 | 381 | Ticker.timeout(() => { 382 | instance.setProps({ 383 | reverse: true, 384 | animation: { 385 | top: 100, 386 | }, 387 | }); 388 | Ticker.timeout(() => { 389 | const child = instance.find('.wrapper').instance().children[0]; 390 | console.log(parseFloat(child.style.top)); 391 | expect(parseFloat(child.style.top)).toBe(0); 392 | done(); 393 | }, 350); 394 | }, 300); 395 | }); 396 | it('is paused', (done) => { 397 | instance = mount( 398 | , 404 | ); 405 | 406 | Ticker.timeout(() => { 407 | let child = instance.find('.wrapper').instance().children[0]; 408 | const top = parseFloat(child.style.top); 409 | expect(top).toBeGreaterThan(50); 410 | instance.setProps({ 411 | paused: true, 412 | }); 413 | Ticker.timeout(() => { 414 | child = instance.find('.wrapper').instance().children[0]; 415 | console.log('top:', child.style.top); 416 | expect(parseFloat(child.style.top)).toBe(top); 417 | done(); 418 | }, 100); 419 | }, 300); 420 | }); 421 | it('is moment', (done) => { 422 | instance = mount( 423 | , 431 | ); 432 | const child = instance.find('.wrapper').instance().children[0]; 433 | Ticker.timeout(() => { 434 | instance.setProps({ 435 | moment: 1000, 436 | }); 437 | Ticker.timeout(() => { 438 | console.log(child.style.top); 439 | expect(parseFloat(child.style.top)).toBe(100); 440 | done(); 441 | }, 10); 442 | }, 100); 443 | }); 444 | 445 | it('plugin: children plugin tween-one', (done) => { 446 | instance = mount( 447 |
448 | 0 449 |
, 450 | ); 451 | console.log(`default:${instance.text()}`); 452 | expect(instance.text()).toEqual('0'); 453 | Ticker.timeout(() => { 454 | expect(instance.text()).toEqual('1000'); 455 | console.log(`end:${instance.text()}`); 456 | done(); 457 | }, 450); 458 | }); 459 | it('plugin: children plugin toMoney tween-one', (done) => { 460 | instance = mount( 461 |
462 | 463 | 0 464 | 465 |
, 466 | ); 467 | console.log(`default:${instance.text()}`); 468 | expect(instance.text()).toEqual('0.00'); 469 | Ticker.timeout(() => { 470 | expect(instance.text()).toEqual('1,000.00'); 471 | console.log(`end:${instance.text()}`); 472 | done(); 473 | }, 450); 474 | }); 475 | 476 | it('plugin: pathMotion plugin tween-one', (done) => { 477 | instance = mount( 478 | , 490 | ); 491 | Ticker.timeout(() => { 492 | const child = instance.find('.wrapper').instance().children[0]; 493 | console.log('transform:', child.style.transform); 494 | expect(child.style.transform).toEqual('translate(-15px,-15px) rotate(90deg)'); 495 | done(); 496 | }, 450); 497 | }); 498 | }); 499 | -------------------------------------------------------------------------------- /tests/tweenOneGroup.test.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | import TweenOneGroup from '../src/TweenOneGroup'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | 10 | const TweenOneGroupComp = (props) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | describe('rc-tween-one-group', () => { 19 | let instance; 20 | function getFloat(str) { 21 | return parseFloat(str); 22 | } 23 | 24 | it('should render children', (done) => { 25 | instance = mount( 26 | 27 |

a

28 |
, 29 | ); 30 | const children = instance.find('p'); 31 | console.log(children.length, instance.find('p')); 32 | expect(children.length).toBe(1); 33 | done(); 34 | }); 35 | it('appear is false', () => { 36 | instance = mount( 37 | 38 |

a

39 |
, 40 | ); 41 | const children = instance.find('p').instance(); 42 | console.log('opacity:', children.style.opacity); 43 | expect(children.style.opacity).toBe(''); 44 | }); 45 | it('component is null', () => { 46 | instance = mount( 47 |
48 | 49 |

a

50 |
51 |
, 52 | ); 53 | const children = instance.find('.wrapper').instance().children[0]; 54 | console.log('tagName:', children.tagName); 55 | expect(children.tagName).toEqual('P'); 56 | }); 57 | it('is normal tween', (done) => { 58 | instance = mount( 59 | 60 |

a

61 |
, 62 | ); 63 | 64 | setTimeout(() => { 65 | const children = instance.find('p').instance(); 66 | console.log('marginLeft < 100:', children.style.marginLeft); 67 | expect(getFloat(children.style.marginLeft)).toBeLessThan(100); 68 | setTimeout(() => { 69 | console.log('marginLeft is 0:', children.style.marginLeft); 70 | expect(getFloat(children.style.marginLeft)).toBe(0); 71 | done(); 72 | }, 500); 73 | }, 50); 74 | }); 75 | it('is switch children', (done) => { 76 | instance = mount(); 77 | let children = instance.find('p').children(); 78 | console.log('children length:', children.length); 79 | expect(children.length).toBe(0); 80 | setTimeout(() => { 81 | instance.setProps({ 82 | children: [

a

,

b

], 83 | }); 84 | 85 | setTimeout(() => { 86 | children = instance.find('.wrapper').instance().children[0].children; 87 | console.log('children length:', children.length); 88 | expect(children.length).toBe(2); 89 | 90 | instance.setProps({ 91 | children: [

a

], 92 | }); 93 | setTimeout(() => { 94 | children = instance.find('.wrapper').instance().children[0].children; 95 | console.log('children length:', children.length); 96 | expect(children.length).toBe(1); 97 | done(); 98 | }, 500); 99 | }, 500); 100 | }, 50); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "typeRoots": ["./typings"], 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "baseUrl": "./", 13 | "strict": true, 14 | "paths": { 15 | "@/*": ["src/*"], 16 | "@@/*": ["src/.umi/*"], 17 | "rc-tween-one": ["src/index.tsx"] 18 | }, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "lib", 24 | "es", 25 | "dist", 26 | "typings", 27 | "**/__test__", 28 | "test" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /typings/global/import.d.ts: -------------------------------------------------------------------------------- 1 | import * as CSS from 'csstype'; 2 | declare module 'csstype' { 3 | interface Properties { 4 | [key: string]: any; 5 | } 6 | } -------------------------------------------------------------------------------- /typings/global/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module 'style-utils'; 5 | declare module 'tween-functions'; 6 | declare module 'raf'; 7 | declare module 'flubber'; 8 | 9 | 10 | // declare module 'rc-tween-one'; 11 | declare module 'rc-tween-one/es/plugin/ChildrenPlugin'; 12 | declare module 'rc-tween-one/es/plugin/PathMotionPlugin'; 13 | declare module 'rc-tween-one/es/plugin/SvgDrawPlugin'; 14 | declare module 'rc-tween-one/es/plugin/SvgMorphPlugin'; 15 | declare module 'rc-tween-one/es/TweenOneGroup'; 16 | 17 | 18 | interface Element { 19 | _tweenOneVars: any; 20 | } 21 | --------------------------------------------------------------------------------