├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── .travis.yml
├── LICENSE
├── README.md
├── gif
├── bouncing-ball.gif
├── gradients.gif
├── notifications.gif
├── observer.gif
├── rotating-circles.gif
├── rotating-svg.gif
├── svg-path.gif
└── transition.gif
├── package-lock.json
├── package.json
├── site
├── assets
│ ├── pause-button.svg
│ ├── play-button.svg
│ ├── react-tweenful.svg
│ └── refresh-button.svg
├── components
│ ├── App.js
│ ├── Header.js
│ └── Links.js
├── demo
│ ├── BouncingBalls.js
│ ├── Gradients.js
│ ├── LoadingCircles.js
│ ├── NotificationsDemo.js
│ ├── ObserverDemo.js
│ ├── RotatingSvg.js
│ ├── RouteTransitionDemo.js
│ └── SvgDemo.js
├── highlight
│ └── index.js
├── html
│ ├── 404.html
│ └── index.html
├── index.js
└── style
│ ├── _buttons.scss
│ ├── _normalize.scss
│ ├── demo.scss
│ ├── global.scss
│ └── reset.css
├── src
├── Observer.js
├── ObserverGroup.js
├── SVG.js
├── Tweenful.js
├── easings
│ └── index.js
├── engine
│ ├── index.js
│ └── utils.js
├── helpers
│ ├── constants.js
│ ├── index.js
│ ├── svg-utils.js
│ └── utils.js
├── index.js
└── parser
│ └── index.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.site.dev.js
└── webpack.site.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "transform-class-properties",
8 | [
9 | "module-resolver",
10 | {
11 | "root": ["./"],
12 | "alias": {
13 | "src": "./src",
14 | "site": "./site"
15 | }
16 | }
17 | ]
18 | ]
19 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | "parser": "babel-eslint",
5 | "parserOptions": {
6 | "ecmaVersion": 6,
7 | "sourceType": "module"
8 | },
9 | "globals": {
10 | "process": true
11 | },
12 | "settings": {
13 | "react": {
14 | version: "16.4.2"
15 | },
16 | "import/resolver": {
17 | alias: {
18 | map: [
19 | ["src", path.resolve(__dirname, "src")],
20 | ["site", path.resolve(__dirname, "site")]
21 | ],
22 | extensions: [".js", ".css", ".scss"]
23 | }
24 | }
25 | },
26 | "extends": [
27 | "eslint:recommended",
28 | "plugin:react/recommended"
29 | ],
30 | "env": {
31 | "es6": true,
32 | "browser": true,
33 | "jest": true
34 | },
35 | "plugins": [
36 | "react"
37 | ],
38 | "rules": {
39 | "quotes": "off",
40 | "linebreak-style": "off",
41 | "comma-dangle": "off",
42 | "react/prop-types": "off",
43 | "react/no-find-dom-node": "off",
44 | "react/jsx-uses-vars": "error",
45 | "no-constant-condition": "off",
46 | "no-console": ["error", {
47 | allow: ["warn", "error", "trace", "log"]
48 | }]
49 | }
50 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | dist/
4 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | gif/
3 | node_modules/
4 | site/
5 | src/
6 | tests/
7 | build/
8 | .babelrc
9 | .gitignore
10 | .eslintrc.js
11 | .editorconfig
12 | jest.config.js
13 | webpack.*.js
14 | *.log
15 | .prettierrc.js
16 | github-preview.gif
17 | .travis.yml
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | trailingComma: 'none',
4 | useTabs: false,
5 | tabWidth: 2,
6 | semi: true,
7 | singleQuote: true,
8 | quoteProps: 'consistent',
9 | jsxBracketSameLine: false
10 | };
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 11.1.0
3 | branches:
4 | only:
5 | - master
6 | install:
7 | - npm install
8 | script:
9 | - npm run build
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Rares Mardare
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/react-tweenful) [](https://bundlephobia.com/result?p=react-ttweenful)
2 |
3 | # react-tweenful
4 |
5 | Looking for an amazing React library for animating stuff? Look no more, we've got you covered!
6 |
7 | ## Demo
8 |
9 | https://teodosii.github.io/react-tweenful/
10 |
11 | ## Features
12 |
13 | * Loop support (infinite or up to a specific number)
14 | * Wide variety of easings (bezier, predefined and custom easing)
15 | * Delayed animations (before and after)
16 | * Events support
17 | * Negative delay support to mimic CSS animations
18 | * Percent based animations to mimic CSS animations (e.g. `0%`, `50%`, `100%`)
19 | * `Tweenful` component for animating DOM nodes
20 | * `SVG` component to animate SVG nodes
21 | * `Observer` component for mount/unmount animations
22 | * `ObserverGroup` component to handle child transition (list removal/insertion, page transition etc)
23 |
24 | ## Getting started
25 |
26 | ```
27 | npm install react-tweenful
28 | ```
29 |
30 | ### Development
31 |
32 | In order to build the project and run it on your local machine, you'll need to build both the `site` project and the library itself. The `site` project will have a dependency of `react-tweenful` and there you'll be able to play with the examples.
33 |
34 | ```
35 | npm run build:library:dev
36 | npm run start
37 | ```
38 |
39 | Or if you want to be able to modify both projects at once and let webpack watch the changes and do the build for you, then open 2 separate terminals and run the following commands
40 |
41 | ```
42 | npm run watch:library:dev
43 | ```
44 | And in a separate terminal run
45 | ```
46 | npm run start
47 | ```
48 |
49 | Both commands will instruct webpack to watch for changes.
50 | Navigate to `http://localhost:8080/react-tweenful` or whatever port you're running the application on.
51 |
52 | ## Usage
53 |
54 | `react-tweenful` exports the following:
55 |
56 | * `Tweenful` - component to animate DOM elements. It requires a DOM node to perform animation on.
57 | * `SVG` - component to animate SVG elements. It requires a SVG node to perform animation on.
58 | * `Observer` - component to animate mounting and unmounting of an element.
59 | * `ObserverGroup` - component to watch over a list of `Observer` elements such as list removal/insertion or route transition
60 |
61 | A couple of utility functions are also exported to help you out animating:
62 |
63 | * `percentage` for percentage based animations
64 | * `bezier` for bezier easings
65 | * `elastic` for elastic easing
66 |
67 | Import the needed component, for example `Tweenful`
68 |
69 | ```jsx
70 | import Tweenful, { elastic } from 'react-tweenful';
71 | ```
72 |
73 | `Tweenful` requires a node to render on which it will perform the animation. We've got most of the DOM nodes covered in the form of namespacing such as `Tweenful.div`, `Tweenful.span` and so on.
74 |
75 | ```jsx
76 | const Example = () => (
77 |
84 | );
85 | ```
86 |
87 | Voila!
88 |
89 | ## Examples
90 |
91 | ### Observer
92 |
93 | View animation [here](https://teodosii.github.io/react-tweenful/observer)
94 |
95 | Usage of `Observer` component together with `Tweenful`. Animate movement with `Tweenful` and subscribe to mount and unmount with `Observer`.
96 |
97 | 
98 |
99 | ```jsx
100 | import React, { useEffect, useState } from 'react';
101 | import Tweenful, { Observer, elastic } from 'react-tweenful';
102 |
103 | const props = {
104 | delay: 200,
105 | render: true,
106 | duration: 1600,
107 | easing: elastic(1, 0.1),
108 | loop: false,
109 | animate: { translateX: '400px' },
110 | events: {
111 | onAnimationStart: () => console.log('AnimationStart'),
112 | onAnimationEnd: () => console.log('AnimationEnd')
113 | }
114 | };
115 |
116 | const ObserverDemo = () => {
117 | const [shouldRender, setShouldRender] = useState(true);
118 |
119 | useEffect(() => {
120 | setTimeout(() => setShouldRender(false), 3000);
121 | }, []);
122 |
123 | return (
124 |
132 |
133 |
134 |
135 |
136 | );
137 | };
138 | ```
139 |
140 | ### ObserverGroup
141 |
142 | View animation [here](https://teodosii.github.io/react-tweenful/notifications)
143 |
144 | Usage of `ObserverGroup` component to watch for mounting and unmounting over a list of notifications
145 |
146 | 
147 |
148 | ```jsx
149 |
159 | {this.state.notifications.map(notification => (
160 |
165 | ))}
166 |
167 | ```
168 |
169 | ### Animate route transition
170 |
171 | View animation [here](https://teodosii.github.io/react-tweenful/transition)
172 |
173 | Usage of `ObserverGroup` to animate route change
174 |
175 | 
176 |
177 | ```jsx
178 | import React from 'react';
179 | import { Route, Switch, NavLink } from 'react-router-dom';
180 | import ObserverGroup from 'react-tweenful/ObserverGroup';
181 | import Observer from 'react-tweenful/Observer';
182 |
183 | const colors = {
184 | red: '#EE4266',
185 | yellow: '#FDB833',
186 | blue: '#296EB4',
187 | green: '#0EAD69'
188 | };
189 |
190 | const Red = () =>
;
191 | const Yellow = () => ;
192 | const Blue = () => ;
193 | const Green = () => ;
194 |
195 | class RouteTransitionDemo extends React.Component {
196 | constructor(props) {
197 | super(props);
198 | }
199 |
200 | render() {
201 | const { location } = this.props;
202 |
203 | return (
204 |
205 |
206 | - Red
207 | - Yellow
208 | - Blue
209 | - Green
210 |
211 |
212 |
213 |
222 |
223 |
224 |
225 |
226 |
227 | Not Found
} />
228 |
229 |
230 |
231 |
232 |
233 | );
234 | }
235 | }
236 |
237 | export default RouteTransitionDemo;
238 |
239 | ```
240 |
241 | ### Prism
242 |
243 | View animation [here](https://teodosii.github.io/react-tweenful/rotating-svg)
244 |
245 | `react-tweenful` in action powering a super awesome looking animation.
246 |
247 | 
248 |
249 | ```jsx
250 | import React from 'react';
251 | import { SVG } from 'react-tweenful';
252 |
253 | const WAVE_COUNT = 16;
254 | const offset = 40;
255 | const waveLength = 375;
256 | const duration = 1500;
257 |
258 | const waves = new Array(WAVE_COUNT).fill(0).map((wave, i) => ({
259 | key: i + 1,
260 | style: {
261 | transformOrigin: '500px 500px',
262 | opacity: 4 / WAVE_COUNT,
263 | mixBlendMode: 'screen',
264 | fill: `hsl(${(360 / WAVE_COUNT) * (i + 1)}, 100%, 50%)`,
265 | transform: `rotate(${(360 / WAVE_COUNT) * i}deg) translate(${waveLength}px, ${offset}px)`
266 | },
267 | rotate: `${(360 / WAVE_COUNT) * (i + 1)}deg`,
268 | translate: `${waveLength}px ${offset}px`,
269 | angle: `${(360 / WAVE_COUNT) * (i + 1)}deg`,
270 | delay: (duration / WAVE_COUNT) * (i + 1) * -3,
271 | path:
272 | 'M-1000,1000V388c86-2,111-38,187-38s108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,109,38,188,38,110-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,109,38,188,38c0,96,0,612,0,612Z'
273 | }));
274 |
275 | const RotatingSvg = () => {
276 | return (
277 |
309 | );
310 | };
311 | ```
312 |
313 | ### SVG
314 |
315 | View animation [here](https://teodosii.github.io/react-tweenful/svg)
316 |
317 | An example animating SVG `path` and `fill` properties one after the other.
318 |
319 | 
320 |
321 | ### Gradients
322 |
323 | View animation [here](https://teodosii.github.io/react-tweenful/gradients)
324 |
325 | An example using `elastic` easing to animate gradient boxes.
326 |
327 | 
328 |
329 | ```jsx
330 | import React from 'react';
331 | import Tweenful, { elastic } from 'react-tweenful';
332 |
333 | const Gradients = () => {
334 | const elements = new Array(10)
335 | .fill(0)
336 | .map((_e, i) => (
337 |
347 | ));
348 |
349 | return (
350 |
353 | );
354 | };
355 | ```
356 |
357 | ### Loader
358 |
359 | View animation [here](https://teodosii.github.io/react-tweenful/loading-circles)
360 |
361 | 
362 |
363 | ```jsx
364 | import React from 'react';
365 | import Tweenful, { percentage } from 'react-tweenful';
366 |
367 | const rotate = percentage({
368 | '0%': { translate: '-50% -50%', rotate: '0deg' },
369 | '50%': { translate: '-50% -50%', rotate: '0deg' },
370 | '80%': { translate: '-50% -50%', rotate: '360deg' },
371 | '100%': { translate: '-50% -50%', rotate: '360deg' }
372 | });
373 | const dot1Animate = percentage({
374 | '0%': { scale: 1 },
375 | '20%': { scale: 1 },
376 | '45%': { translate: '16px 12px', scale: 0.45 },
377 | '60%': { translate: '160px 150px', scale: 0.45 },
378 | '80%': { translate: '160px 150px', scale: 0.45 },
379 | '100%': { translate: '0px 0px', scale: 1 }
380 | });
381 | const dot2Animate = percentage({
382 | '0%': { scale: 1 },
383 | '20%': { scale: 1 },
384 | '45%': { translate: '-16px 12px', scale: 0.45 },
385 | '60%': { translate: '-160px 150px', scale: 0.45 },
386 | '80%': { translate: '-160px 150px', scale: 0.45 },
387 | '100%': { translate: '0px 0px', scale: 1 }
388 | });
389 | const dot3Animate = percentage({
390 | '0%': { scale: 1 },
391 | '20%': { scale: 1 },
392 | '45%': { translateY: '-18px', scale: 0.45 },
393 | '60%': { translateY: '-180px', scale: 0.45 },
394 | '80%': { translateY: '-180px', scale: 0.45 },
395 | '100%': { translateY: '0px', scale: 1 }
396 | });
397 |
398 | const LoadingCircles = () => {
399 | return (
400 |
401 |
409 |
417 |
425 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
444 |
445 |
446 |
447 |
448 | );
449 | };
450 |
451 | ```
452 |
453 | ### Bouncing Balls
454 |
455 | View animation [here](https://teodosii.github.io/react-tweenful/bouncing-balls)
456 |
457 | Bouncing balls illustrate negative delay support in `react-tweenful`
458 |
459 | 
460 |
461 | ```jsx
462 | import React from 'react';
463 | import { SVG, percentage, elastic } from 'react-tweenful';
464 |
465 | const circles = new Array(10).fill(0).map((_e, i) => ({
466 | loop: true,
467 | fill: `hsl(${(i + 1) * 20 - 20}, 70%, 70%)`,
468 | delay: ((i + 1) * 1500) / -10,
469 | duration: 1500,
470 | easing: elastic(2, 0.9),
471 | transform: {
472 | translate: '0 100px'
473 | },
474 | style: {
475 | transformOrigin: `${-200 + 120 * (i + 1)}px 250px`
476 | },
477 | animate: percentage({
478 | '0%': { translate: '0px 100px', scale: 1 },
479 | '50%': { translate: '0px -100px', scale: 0.3 },
480 | '100%': { translate: '0px 100px', scale: 1 }
481 | }),
482 | r: 35,
483 | cx: 100 * i + 50,
484 | cy: 250
485 | }));
486 |
487 | const BouncingBalls = () => {
488 | return (
489 |
490 |
491 | {circles.map((circle, i) => (
492 |
493 | ))}
494 |
495 |
496 | );
497 | };
498 | ```
499 |
500 | ## API
501 |
502 | **Update in progress. Stay in touch!**
503 |
504 | ## Note
505 |
506 | A couple of the animations shown here have been initially created for CodePen by other folks. My only contribution to them was to convert them to `react-tweenful` to show real world examples on animating stuff.
507 |
508 | ## License
509 |
510 | MIT
511 |
--------------------------------------------------------------------------------
/gif/bouncing-ball.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/bouncing-ball.gif
--------------------------------------------------------------------------------
/gif/gradients.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/gradients.gif
--------------------------------------------------------------------------------
/gif/notifications.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/notifications.gif
--------------------------------------------------------------------------------
/gif/observer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/observer.gif
--------------------------------------------------------------------------------
/gif/rotating-circles.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/rotating-circles.gif
--------------------------------------------------------------------------------
/gif/rotating-svg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/rotating-svg.gif
--------------------------------------------------------------------------------
/gif/svg-path.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/svg-path.gif
--------------------------------------------------------------------------------
/gif/transition.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teodosii/react-tweenful/2443c4a430dd73559d62e0a95e6f4c1a0fac6ccd/gif/transition.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tweenful",
3 | "version": "0.1.0",
4 | "homepage": "http://teodosii.github.io/react-tweenful",
5 | "description": "Animation engine for React",
6 | "main": "dist/react-tweenful.js",
7 | "scripts": {
8 | "build": "npm run build:library:prod",
9 | "build:library:dev": "webpack --config webpack.dev.js",
10 | "build:library:prod": "webpack --config webpack.prod.js",
11 | "build:site:dev": "webpack --config webpack.site.dev.js",
12 | "build:site:prod": "webpack --config webpack.site.prod.js",
13 | "watch:library": "webpack -w --config webpack.dev.js",
14 | "start": "webpack-dev-server --config ./webpack.site.dev.js",
15 | "predeploy": "npm run build:site:prod",
16 | "deploy": "gh-pages -d dist",
17 | "publish:npm": "npm run build && npm publish"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/teodosii/react-tweenful"
22 | },
23 | "bugs": {
24 | "url": "https://github.com/teodosii/react-tweenful/issues"
25 | },
26 | "keywords": [
27 | "react",
28 | "react-animation",
29 | "react-animation-component",
30 | "react-tween",
31 | "animation",
32 | "animation-engine",
33 | "react-component",
34 | "tween"
35 | ],
36 | "author": "Rares Mardare",
37 | "license": "MIT",
38 | "devDependencies": {
39 | "@babel/core": "^7.8.3",
40 | "@babel/polyfill": "^7.8.3",
41 | "@babel/preset-env": "^7.8.3",
42 | "@babel/preset-react": "^7.8.3",
43 | "@babel/runtime": "^7.8.3",
44 | "babel-eslint": "^10.0.1",
45 | "babel-jest": "^25.1.0",
46 | "babel-loader": "^8.0.1",
47 | "babel-plugin-module-resolver": "^4.0.0",
48 | "babel-plugin-transform-class-properties": "^6.24.1",
49 | "clean-webpack-plugin": "^3.0.0",
50 | "copy-webpack-plugin": "^5.1.1",
51 | "css-loader": "^3.4.2",
52 | "css-select-base-adapter": "^0.1.1",
53 | "eslint": "^6.8.0",
54 | "eslint-config-airbnb": "^18.0.1",
55 | "eslint-config-standard": "^14.1.0",
56 | "eslint-import-resolver-alias": "^1.1.0",
57 | "eslint-loader": "^3.0.3",
58 | "eslint-plugin-import": "^2.20.0",
59 | "eslint-plugin-jsx-a11y": "^6.1.0",
60 | "eslint-plugin-node": "^11.0.0",
61 | "eslint-plugin-promise": "^4.2.1",
62 | "eslint-plugin-react": "^7.18.0",
63 | "eslint-plugin-react-hooks": "^2.3.0",
64 | "eslint-plugin-standard": "^4.0.1",
65 | "extract-text-webpack-plugin": "^3.0.2",
66 | "file-loader": "^5.0.2",
67 | "font-awesome": "^4.7.0",
68 | "gh-pages": "^2.0.1",
69 | "html-webpack-plugin": "^3.2.0",
70 | "mini-css-extract-plugin": "^0.9.0",
71 | "node-sass": "^4.13.1",
72 | "object-assign": "^4.1.1",
73 | "optimize-css-assets-webpack-plugin": "^5.0.3",
74 | "prettier": "^1.18.2",
75 | "react": "^16.12.0",
76 | "react-addons-create-fragment": "^15.6.2",
77 | "react-dom": "^16.12.0",
78 | "react-github-button": "^0.1.11",
79 | "react-github-corner": "^2.3.0",
80 | "react-router": "^5.1.2",
81 | "react-router-dom": "^5.1.2",
82 | "react-syntax-highlighter": "^12.2.1",
83 | "react-test-renderer": "^16.12.0",
84 | "regenerator-runtime": "^0.13.1",
85 | "sass-loader": "^8.0.2",
86 | "style-loader": "^1.1.3",
87 | "terser-webpack-plugin": "^2.3.5",
88 | "url-loader": "^3.0.0",
89 | "webpack": "^4.41.5",
90 | "webpack-cli": "^3.3.10",
91 | "webpack-dev-server": "^3.10.1"
92 | },
93 | "peerDependencies": {
94 | "react": "^16.0.0"
95 | },
96 | "dependencies": {
97 | "bezier-easing": "^2.1.0",
98 | "color-parser": "^0.1.0",
99 | "prop-types": "^15.6.2"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/site/assets/pause-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/site/assets/play-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/site/assets/react-tweenful.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/assets/refresh-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/site/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3 | import Header from 'site/components/Header';
4 | import Links from 'site/components/Links';
5 | import ObserverDemo from 'site/demo/ObserverDemo';
6 | import SvgDemo from 'site/demo/SvgDemo';
7 | import LoadingCircles from 'site/demo/LoadingCircles';
8 | import RouteTransitionDemo from 'site/demo/RouteTransitionDemo';
9 | import NotificationsDemo from 'site/demo/NotificationsDemo';
10 | import RotatingSvg from 'site/demo/RotatingSvg';
11 | import Gradients from 'site/demo/Gradients';
12 | import BouncingBalls from 'site/demo/BouncingBalls';
13 | import 'site/style/demo.scss';
14 |
15 | const App = () => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/site/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Header = () => {
4 | return (
5 |
6 |
7 |
react-tweenful
8 |
GitHub
9 |
10 |
11 | );
12 | };
13 |
14 | export default Header;
--------------------------------------------------------------------------------
/site/components/Links.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const Links = () => {
5 | return (
6 |
7 |
Navigate to an example
8 |
9 | -
10 | Notifications
11 |
12 | -
13 | Route transition
14 |
15 | -
16 | Observer
17 |
18 | -
19 | SVG
20 |
21 | -
22 | Loader
23 |
24 | -
25 | Prism
26 |
27 | -
28 | Gradients
29 |
30 | -
31 | Bouncing Balls
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Links;
39 |
--------------------------------------------------------------------------------
/site/demo/BouncingBalls.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SVG, percentage, elastic } from 'react-tweenful';
3 |
4 | const circles = new Array(10).fill(0).map((_e, i) => ({
5 | loop: true,
6 | fill: `hsl(${(i + 1) * 20 - 20}, 70%, 70%)`,
7 | delay: ((i + 1) * 1500) / -10,
8 | duration: 1500,
9 | easing: elastic(2, 0.9),
10 | transform: {
11 | translate: '0 100px'
12 | },
13 | style: {
14 | transformOrigin: `${-200 + 120 * (i + 1)}px 250px`
15 | },
16 | animate: percentage({
17 | '0%': { translate: '0px 100px', scale: 1 },
18 | '50%': { translate: '0px -100px', scale: 0.3 },
19 | '100%': { translate: '0px 100px', scale: 1 }
20 | }),
21 | r: 35,
22 | cx: 100 * i + 50,
23 | cy: 250
24 | }));
25 |
26 | const BouncingBalls = () => {
27 | return (
28 |
29 |
30 | {circles.map((circle, i) => (
31 |
32 | ))}
33 |
34 |
35 | );
36 | };
37 |
38 | export default BouncingBalls;
39 |
--------------------------------------------------------------------------------
/site/demo/Gradients.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tweenful, { elastic } from 'react-tweenful';
3 |
4 | const Gradients = () => {
5 | const elements = new Array(10)
6 | .fill(0)
7 | .map((_e, i) => (
8 |
18 | ));
19 |
20 | return (
21 |
24 | );
25 | };
26 |
27 | export default Gradients;
28 |
--------------------------------------------------------------------------------
/site/demo/LoadingCircles.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tweenful, { percentage } from 'react-tweenful';
3 |
4 | const rotate = percentage({
5 | '0%': { translate: '-50% -50%', rotate: '0deg' },
6 | '50%': { translate: '-50% -50%', rotate: '0deg' },
7 | '80%': { translate: '-50% -50%', rotate: '360deg' },
8 | '100%': { translate: '-50% -50%', rotate: '360deg' }
9 | });
10 | const dot1Animate = percentage({
11 | '0%': { scale: 1 },
12 | '20%': { scale: 1 },
13 | '45%': { translate: '16px 12px', scale: 0.45 },
14 | '60%': { translate: '160px 150px', scale: 0.45 },
15 | '80%': { translate: '160px 150px', scale: 0.45 },
16 | '100%': { translate: '0px 0px', scale: 1 }
17 | });
18 | const dot2Animate = percentage({
19 | '0%': { scale: 1 },
20 | '20%': { scale: 1 },
21 | '45%': { translate: '-16px 12px', scale: 0.45 },
22 | '60%': { translate: '-160px 150px', scale: 0.45 },
23 | '80%': { translate: '-160px 150px', scale: 0.45 },
24 | '100%': { translate: '0px 0px', scale: 1 }
25 | });
26 | const dot3Animate = percentage({
27 | '0%': { scale: 1 },
28 | '20%': { scale: 1 },
29 | '45%': { translateY: '-18px', scale: 0.45 },
30 | '60%': { translateY: '-180px', scale: 0.45 },
31 | '80%': { translateY: '-180px', scale: 0.45 },
32 | '100%': { translateY: '0px', scale: 1 }
33 | });
34 |
35 | const LoadingCircles = () => {
36 | return (
37 |
38 |
46 |
54 |
62 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default LoadingCircles;
89 |
--------------------------------------------------------------------------------
/site/demo/NotificationsDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ObserverGroup from 'react-tweenful/ObserverGroup';
3 |
4 | const messages = [
5 | 'All your data has been successfully updated',
6 | 'Your meeting has been successfully attended',
7 | 'Document has been successfully updated',
8 | 'You have no access rights',
9 | 'An error occurred while saving',
10 | 'Document has been permanently removed'
11 | ];
12 |
13 | const types = ['default', 'success', 'danger'];
14 | const number = (start, end) => Math.floor(Math.random() * end) + start;
15 |
16 | const Notification = ({ notification, onClick, style }) => {
17 | const { id, message, type } = notification;
18 |
19 | return (
20 |
21 |
onClick(id)}
23 | className={`notification-item notification-${type}`}
24 | >
25 |
29 |
30 |
31 | );
32 | };
33 |
34 | class Notifications extends React.Component {
35 | constructor(props) {
36 | super(props);
37 |
38 | this.state = {
39 | notifications: new Array(5).fill(0).map(() => {
40 | const id = number(1100, 9900);
41 | return {
42 | id,
43 | message: `${id} ${messages[number(0, messages.length)]}`,
44 | type: types[number(0, types.length)]
45 | };
46 | })
47 | };
48 |
49 | this.removeNotification = this.removeNotification.bind(this);
50 | this.appendNotification = this.appendNotification.bind(this);
51 | }
52 |
53 | appendNotification() {
54 | const id = number(1100, 9900);
55 | this.setState(prevState => ({
56 | notifications: [
57 | ...prevState.notifications,
58 | {
59 | id,
60 | message: `${id} ${messages[number(0, messages.length - 1)]}`,
61 | type: types[number(0, types.length - 1)]
62 | }
63 | ]
64 | }));
65 | }
66 |
67 | removeNotification(id) {
68 | this.setState(prevState => ({
69 | notifications: prevState.notifications.filter(notif => notif.id !== id)
70 | }));
71 | }
72 |
73 | render() {
74 | return (
75 |
76 |
81 |
82 |
92 | {this.state.notifications.map(notification => (
93 |
98 | ))}
99 |
100 |
101 |
102 | );
103 | }
104 | }
105 |
106 | export default Notifications;
107 |
--------------------------------------------------------------------------------
/site/demo/ObserverDemo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Tweenful, { Observer, elastic } from 'react-tweenful';
3 |
4 | const props = {
5 | delay: 200,
6 | render: true,
7 | duration: 1600,
8 | easing: elastic(1, 0.1),
9 | loop: false,
10 | animate: { translateX: '400px' },
11 | events: {
12 | onAnimationStart: () => console.log('AnimationStart'),
13 | onAnimationEnd: () => console.log('AnimationEnd')
14 | }
15 | };
16 |
17 | const ObserverDemo = () => {
18 | const [shouldRender, setShouldRender] = useState(true);
19 |
20 | useEffect(() => {
21 | setTimeout(() => setShouldRender(false), 3000);
22 | }, []);
23 |
24 | return (
25 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default ObserverDemo;
41 |
--------------------------------------------------------------------------------
/site/demo/RotatingSvg.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SVG } from 'react-tweenful';
3 |
4 | const WAVE_COUNT = 16;
5 | const offset = 40;
6 | const waveLength = 375;
7 | const duration = 1500;
8 |
9 | const waves = new Array(WAVE_COUNT).fill(0).map((wave, i) => ({
10 | key: i + 1,
11 | style: {
12 | transformOrigin: '500px 500px',
13 | opacity: 4 / WAVE_COUNT,
14 | mixBlendMode: 'screen',
15 | fill: `hsl(${(360 / WAVE_COUNT) * (i + 1)}, 100%, 50%)`,
16 | transform: `rotate(${(360 / WAVE_COUNT) * i}deg) translate(${waveLength}px, ${offset}px)`
17 | },
18 | rotate: `${(360 / WAVE_COUNT) * (i + 1)}deg`,
19 | translate: `${waveLength}px ${offset}px`,
20 | angle: `${(360 / WAVE_COUNT) * (i + 1)}deg`,
21 | delay: (duration / WAVE_COUNT) * (i + 1) * -3,
22 | path:
23 | 'M-1000,1000V388c86-2,111-38,187-38s108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,109,38,188,38,110-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,109,38,188,38c0,96,0,612,0,612Z'
24 | }));
25 |
26 | const RotatingSvg = () => {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
44 | {waves.map(wave => (
45 |
57 | ))}
58 |
59 |
60 | );
61 | };
62 |
63 | export default RotatingSvg;
64 |
--------------------------------------------------------------------------------
/site/demo/RouteTransitionDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch, NavLink } from 'react-router-dom';
3 | import ObserverGroup from 'react-tweenful/ObserverGroup';
4 | import Observer from 'react-tweenful/Observer';
5 |
6 | const colors = {
7 | red: '#EE4266',
8 | yellow: '#FDB833',
9 | blue: '#296EB4',
10 | green: '#0EAD69'
11 | };
12 |
13 | const Red = () => ;
14 | const Yellow = () => ;
15 | const Blue = () => ;
16 | const Green = () => ;
17 |
18 | class RouteTransitionDemo extends React.Component {
19 | constructor(props) {
20 | super(props);
21 | }
22 |
23 | render() {
24 | const { location } = this.props;
25 |
26 | return (
27 |
28 |
29 | - Red
30 | - Yellow
31 | - Blue
32 | - Green
33 |
34 |
35 |
36 | console.log('onMountStart'),
45 | onUnmountEnd: () => console.log('onUnmountEnd')
46 | }}
47 | easing={'easeOutQuad'}
48 | >
49 |
50 |
51 |
52 |
53 |
54 | Not Found
} />
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | export default RouteTransitionDemo;
65 |
--------------------------------------------------------------------------------
/site/demo/SvgDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SVG from 'react-tweenful/SVG';
3 |
4 | const SvgDemo = () => {
5 | return (
6 |
13 |
23 |
24 | );
25 | };
26 |
27 | export default SvgDemo;
--------------------------------------------------------------------------------
/site/highlight/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: `import React from "react";
3 | import uniquePropHOC from "./lib/unique-prop-hoc";
4 |
5 | class Expire extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = { component: props.children };
9 | }
10 | componentDidMount() {
11 | setTimeout(() => {
12 | this.setState({
13 | component: null
14 | });
15 | }, this.props.time || this.props.seconds * 1000);
16 | }
17 | render() {
18 | return this.state.component;
19 | }
20 | }
21 |
22 | export default uniquePropHOC(["time", "seconds"])(Expire);`
23 | };
24 |
--------------------------------------------------------------------------------
/site/html/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404
6 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/site/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-tweenful
8 |
9 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/site/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
4 | import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
5 | import Application from './components/App';
6 | import './style/global.scss';
7 |
8 | window.addEventListener('DOMContentLoaded', () => {
9 | SyntaxHighlighter.registerLanguage('jsx', jsx);
10 | });
11 |
12 | document.addEventListener('readystatechange', event => {
13 | if (event.target.readyState === "interactive") {
14 | // DOM accessible
15 | }
16 |
17 | if (event.target.readyState === "complete") {
18 | ReactDOM.render(, document.getElementsByClassName('app')[0]);
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/site/style/_buttons.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | color: #fff;
3 | display: inline-flex;
4 | align-items: center;
5 | justify-content: center;
6 | vertical-align: middle;
7 | text-align: center;
8 | font-family: inherit;
9 | font-size: 0.9375em;
10 | text-decoration: none;
11 | text-transform: none;
12 | border-radius: 4px;
13 | background-color: #2355f3;
14 | border: 0 solid transparent;
15 | min-height: 2.7em;
16 | padding: 0.5em 1.6em;
17 | box-shadow: none;
18 | cursor: pointer;
19 | line-height: 1.2;
20 |
21 | &:hover {
22 | transition: all linear 0.2s;
23 | outline: 0;
24 | text-decoration: none;
25 | color: rgba(255, 255, 255, 0.95);
26 | background-color: #2355f3;
27 | }
28 | }
29 |
30 | a.primary,
31 | button.primary {
32 | font-size: 0.9375em;
33 | font-weight: 500;
34 | text-decoration: none;
35 | text-transform: none;
36 | min-height: 2.66667em;
37 | box-shadow: none;
38 | color: #0c3dd7;
39 | background: 0 0;
40 | border-radius: 4px;
41 | border: 2px solid #0c3dd7;
42 | padding: 0.5em 1.6em;
43 |
44 | &:hover {
45 | color: rgba(255, 255, 255, 0.95);
46 | background: #3060f9;
47 | border-color: #3060f9;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/site/style/_normalize.scss:
--------------------------------------------------------------------------------
1 |
2 | *,
3 | *::before,
4 | *::after {
5 | box-sizing: border-box;
6 | }
7 |
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6 {
14 | font-weight: bold;
15 | }
16 |
17 | h1 {
18 | font-size: 2.5em;
19 | }
20 |
21 | h2 {
22 | font-size: 1.75em;
23 | }
24 |
25 | h3 {
26 | font-size: 1.5em;
27 | }
28 |
29 | h4 {
30 | font-size: 1.25em;
31 | }
32 |
33 | h5,
34 | li li {
35 | font-size: 1em;
36 | }
37 |
38 | h6 {
39 | font-size: 0.75em;
40 | letter-spacing: 0.01em;
41 | }
42 |
43 | h1,
44 | h2,
45 | h3,
46 | h4,
47 | h5,
48 | h6,
49 | p,
50 | li {
51 | color: #333;
52 | line-height: 120%;
53 | }
54 |
55 | a {
56 | color: #158df7;
57 | text-decoration: none;
58 | .active,
59 | &:hover {
60 | color: #ee2455;
61 | }
62 | }
--------------------------------------------------------------------------------
/site/style/demo.scss:
--------------------------------------------------------------------------------
1 | $default: #007bff;
2 | $default_dark: #0562c7;
3 | $success: #28a745;
4 | $success_dark: #1f8838;
5 | $danger: #dc3545;
6 | $danger_dark: #bd1120;
7 |
8 | .observer-demo {
9 | padding: 20px 30px;
10 |
11 | .box-demo {
12 | top: 0;
13 | left: 0;
14 | width: 100px;
15 | height: 100px;
16 | border-radius: 0%;
17 | background-color: #167fe0;
18 | position: relative;
19 | opacity: 1;
20 | }
21 | }
22 |
23 | .route-transition-demo {
24 | width: 100%;
25 | height: 100%;
26 | padding: 10px;
27 |
28 | .observer,
29 | .key-wrapper,
30 | .route-transition-demo,
31 | .color-block {
32 | width: 100%;
33 | height: 100%;
34 | position: relative;
35 | }
36 |
37 | .key-wrapper {
38 | z-index: 10;
39 | position: absolute;
40 | max-width: 400px;
41 | max-height: 600px;
42 | &:last-child {
43 | z-index: 100;
44 | }
45 | }
46 |
47 | .nav-links {
48 | list-style: none;
49 | margin-bottom: 10px;
50 | li {
51 | font-size: 18px;
52 | margin-right: 10px;
53 | display: inline-block;
54 | }
55 | }
56 | }
57 |
58 | .notifications-demo {
59 | width: 360px;
60 | margin: 0 auto;
61 |
62 | .actions {
63 | padding-top: 15px;
64 | padding-bottom: 15px;
65 | display: flex;
66 | justify-content: center;
67 | }
68 |
69 | .notification-content {
70 | padding: 8px 15px;
71 | display: inline-block;
72 | width: 100%;
73 | }
74 |
75 | .notification-default {
76 | background-color: $default;
77 | border-left: 8px solid $default_dark;
78 | .notification-close {
79 | background-color: $default;
80 | }
81 | }
82 |
83 | .notification-success {
84 | background-color: $success;
85 | border-left: 8px solid $success_dark;
86 | .notification-close {
87 | background-color: $success;
88 | }
89 | }
90 |
91 | .notification-danger {
92 | background-color: $danger;
93 | border-left: 8px solid $danger_dark;
94 | .notification-close {
95 | background-color: $danger;
96 | }
97 | }
98 |
99 | .notification-item {
100 | display: flex;
101 | position: relative;
102 | border-radius: 3px;
103 | margin-bottom: 15px;
104 | box-shadow: 1px 3px 4px rgba(0, 0, 0, 0.2);
105 | max-width: 100%;
106 | cursor: default;
107 |
108 | .notification-message {
109 | color: #fff;
110 | max-width: calc(100% - 15px);
111 | font-size: 14px;
112 | line-height: 150%;
113 | word-wrap: break-word;
114 | margin-bottom: 0;
115 | margin-top: 0;
116 | }
117 |
118 | .notification-close {
119 | width: 18px;
120 | height: 18px;
121 | border-radius: 50%;
122 | display: inline-block;
123 | position: absolute;
124 | right: 10px;
125 | top: 10px;
126 |
127 | &::after {
128 | content: '\D7';
129 | cursor: pointer;
130 | position: absolute;
131 | transform: translate(-50%, -50%);
132 | color: #fff;
133 | font-size: 12px;
134 | left: 50%;
135 | top: 50%;
136 | }
137 | }
138 | }
139 | }
140 |
141 | .loading-wrapper {
142 | width: 100%;
143 | height: 100%;
144 | background-color: #f8f4d5;
145 | }
146 |
147 | .loading-circles-container {
148 | width: 400px;
149 | height: 400px;
150 | top: 50%;
151 | left: 50%;
152 | position: absolute;
153 | transform: translate(-50%, -50%);
154 | margin: auto;
155 | filter: url('#goo');
156 |
157 | .dot {
158 | width: 70px;
159 | height: 70px;
160 | border-radius: 50%;
161 | background-color: #000;
162 | position: absolute;
163 | top: 0;
164 | bottom: 0;
165 | left: 0;
166 | right: 0;
167 | margin: auto;
168 |
169 | &.dot-1 {
170 | background-color: #ffe386;
171 | }
172 | &.dot-3 {
173 | background-color: #f74d75;
174 | }
175 | &.dot-2 {
176 | background-color: #10beae;
177 | }
178 | }
179 | }
180 |
181 | .gradients-container {
182 | height: 100%;
183 |
184 | .row {
185 | height: 100%;
186 | display: flex;
187 | flex-direction: row;
188 | padding-left: 10px;
189 | padding-right: 10px;
190 | padding-top: 10px;
191 | }
192 |
193 | .box {
194 | height: 500px;
195 | max-height: 60%;
196 | width: 10%;
197 | float: left;
198 | margin-left: 5px;
199 | margin-right: 5px;
200 | border-radius: 4px;
201 | }
202 | }
203 |
204 | .gradiant1 {
205 | background-image: linear-gradient(to bottom, #f8e739, #f1d42d, #eac222, #e1b019, #d89e10);
206 | }
207 | .gradiant2 {
208 | background-image: linear-gradient(to bottom, #d89e10, #e28f00, #ec7f00, #f66b00, #ff5200);
209 | }
210 | .gradiant3 {
211 | background-image: linear-gradient(to bottom, #ff5200, #f84603, #f13806, #e92909, #e2130b);
212 | }
213 | .gradiant4 {
214 | background-image: linear-gradient(to bottom, #e2130b, #e1110c, #e00f0d, #df0c0d, #de090e);
215 | }
216 | .gradiant5 {
217 | background-image: linear-gradient(to bottom, #de090e, #df002a, #dc003e, #d60051, #ce0061);
218 | }
219 | .gradiant6 {
220 | background-image: linear-gradient(to bottom, #ce0061, #d50079, #d60094, #d000b3, #c115d4);
221 | }
222 | .gradiant7 {
223 | background-image: linear-gradient(to bottom, #c115d4, #af0cd6, #9b07d8, #8508da, #6a0ddc);
224 | }
225 | .gradiant8 {
226 | background-image: linear-gradient(to bottom, #6a0ddc, #5f0ddd, #530edd, #450fde, #3210de);
227 | }
228 | .gradiant9 {
229 | background-image: linear-gradient(to bottom, #3210de, #2a1cdc, #2124d9, #172ad7, #0c2fd4);
230 | }
231 | .gradiant10 {
232 | background-image: linear-gradient(to bottom, #0c2fd4, #0045d8, #0056d9, #0065d8, #1b73d5);
233 | }
234 |
235 | .bouncing-balls {
236 | width: 100%;
237 | height: 100%;
238 | background: hsl(40, 50%, 95%);
239 | display: flex;
240 | align-items: center;
241 | justify-content: center;
242 | flex-direction: column;
243 |
244 | svg {
245 | width: 100vw;
246 | height: 80vh;
247 | }
248 | circle {
249 | transform-origin: 50% 50%;
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/site/style/global.scss:
--------------------------------------------------------------------------------
1 | @import 'reset.css';
2 | @import 'normalize';
3 | @import 'buttons';
4 |
5 | html,
6 | body,
7 | div.app {
8 | display: block;
9 | color: #333;
10 | background: #fff;
11 | width: 100%;
12 | height: 100%;
13 | margin: 0;
14 | font-family: 'PT Sans', sans-serif;
15 | }
16 |
17 | .header {
18 | height: 55px;
19 | width: 100%;
20 | position: fixed;
21 | background-color: #333;
22 | padding-left: 20px;
23 | padding-right: 20px;
24 |
25 | .header-container {
26 | height: 100%;
27 | display: flex;
28 | align-items: center;
29 | max-width: 960px;
30 | margin: 0 auto;
31 | }
32 |
33 | .logo {
34 | font-size: 28px;
35 | color: #fff;
36 | margin-right: auto;
37 | }
38 | .github-link {
39 | color: #fff;
40 | &:hover {
41 | text-decoration: underline;
42 | }
43 | }
44 | }
45 |
46 | .links {
47 | width: 100%;
48 | height: 100%;
49 | display: flex;
50 | justify-content: center;
51 | align-items: center;
52 | flex-direction: column;
53 |
54 | .links-list {
55 | list-style: disc;
56 | }
57 | .links-heading {
58 | margin-bottom: 15px;
59 | }
60 |
61 | a {
62 | font-size: 18px;
63 | margin-top: 6px;
64 | margin-bottom: 6px;
65 | display: block;
66 | }
67 | }
68 |
69 | section.container {
70 | display: flex;
71 | max-width: 960px;
72 | margin: 0 auto;
73 | padding-top: 80px;
74 | padding-bottom: 80px;
75 | }
76 |
77 | .sidebar {
78 | min-width: 220px;
79 | padding-top: 20px;
80 | }
81 |
82 | nav {
83 | position: fixed;
84 | }
85 | nav ul {
86 | margin-top: 8px;
87 | }
88 | nav ul + h4 {
89 | margin-top: 22px;
90 | }
91 | nav li {
92 | padding: 5px 0;
93 | }
94 |
95 | .main-content {
96 | padding-left: 30px;
97 | padding-right: 30px;
98 |
99 | .spacer-top {
100 | margin-top: 20px;
101 | }
102 |
103 | p,
104 | li,
105 | h1,
106 | h2,
107 | h3,
108 | h4,
109 | h5,
110 | h6 {
111 | line-height: 150%;
112 | }
113 | }
114 |
115 | .heading {
116 | margin-top: 20px;
117 | margin-bottom: 20px;
118 | }
119 |
120 | ul.list {
121 | margin-top: 10px;
122 | margin-bottom: 10px;
123 | list-style: disc;
124 | padding-left: 20px;
125 | }
126 |
127 | .animate-box {
128 | width: 100%;
129 | min-height: 160px;
130 | background-color: #fafafa;
131 | border-bottom: 2px solid #333;
132 | display: flex;
133 | justify-content: center;
134 | align-items: center;
135 |
136 | .box,
137 | .box-shadow {
138 | width: 40px;
139 | height: 40px;
140 | border-radius: 50%;
141 | }
142 | .box {
143 | z-index: 105;
144 | position: relative;
145 | background-color: #158df7;
146 | }
147 | .box-shadow {
148 | z-index: 100;
149 | position: absolute;
150 | background-color: #07345d;
151 | }
152 | }
153 |
154 | div.code-highlight > pre {
155 | margin: 0 !important;
156 | padding: 20px !important;
157 | border-radius: 0 !important;
158 | }
--------------------------------------------------------------------------------
/site/style/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | hgroup,
77 | menu,
78 | nav,
79 | output,
80 | ruby,
81 | section,
82 | summary,
83 | time,
84 | mark,
85 | audio,
86 | video {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font: inherit;
92 | vertical-align: baseline;
93 | }
94 | /* HTML5 display-role reset for older browsers */
95 | article,
96 | aside,
97 | details,
98 | figcaption,
99 | figure,
100 | footer,
101 | header,
102 | hgroup,
103 | menu,
104 | nav,
105 | section {
106 | display: block;
107 | }
108 | body {
109 | line-height: 1;
110 | }
111 | ol,
112 | ul {
113 | list-style: none;
114 | }
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 | blockquote:before,
120 | blockquote:after,
121 | q:before,
122 | q:after {
123 | content: '';
124 | content: none;
125 | }
126 | table {
127 | border-collapse: collapse;
128 | border-spacing: 0;
129 | }
--------------------------------------------------------------------------------
/src/Observer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import PropTypes from 'prop-types';
4 | import Engine from './engine';
5 | import { is } from './helpers';
6 | import Parser from './parser';
7 |
8 | class Observer extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | style: props.style,
13 | render: props.render
14 | };
15 |
16 | this.unmounted = false;
17 | this.onComplete = this.onComplete.bind(this);
18 | this.updateAnimationProgress = this.updateAnimationProgress.bind(this);
19 | }
20 |
21 | componentDidMount() {
22 | document.addEventListener('visibilitychange', this.handleVisibilityChange);
23 |
24 | if (this.state.render) {
25 | this.animateMounting();
26 | }
27 | }
28 |
29 | componentDidUpdate(prevProps) {
30 | const { render } = this.props;
31 |
32 | if (!prevProps.render && render) {
33 | // queue mount animation after state update
34 | this.setState({ render: true }, this.animateMounting);
35 | }
36 |
37 | if (prevProps.render && !render) {
38 | this.animateUnmounting();
39 | }
40 | }
41 |
42 | componentWillUnmount() {
43 | this.unmounted = true;
44 | this.engine.stop();
45 | document.removeEventListener('visibilitychange', this.handleVisibilityChange);
46 | }
47 |
48 | handleVisibilityChange() {
49 | if (!this.engine) return;
50 | this.engine.handleVisibility(document.visibilityState);
51 | }
52 |
53 | parseMount() {
54 | const el = ReactDOM.findDOMNode(this);
55 | return Parser.parse(el, { ...this.props, animate: this.props.mount });
56 | }
57 |
58 | parseUnmount() {
59 | const options = {
60 | ...this.props,
61 | from: {},
62 | animate: this.props.unmount
63 | };
64 |
65 | const el = ReactDOM.findDOMNode(this);
66 | return Parser.parse(el, options);
67 | }
68 |
69 | animateMounting() {
70 | if (!this.props.mount) return;
71 | this.mount = this.parseMount();
72 | this.mount.events.onMountStart();
73 | this.animate(this.mount);
74 | }
75 |
76 | animateUnmounting() {
77 | if (!this.props.unmount) return;
78 | this.unmount = this.parseUnmount();
79 | this.unmount.events.onUnmountStart();
80 | this.animate(this.unmount);
81 | }
82 |
83 | animate(instance) {
84 | instance.progress = 0;
85 |
86 | this.engine = new Engine({
87 | instance,
88 | animate: this.updateAnimationProgress,
89 | onComplete: this.onComplete
90 | });
91 |
92 | this.engine.play();
93 | }
94 |
95 | onComplete(instance) {
96 | // unmounting animation has completed
97 | if (instance === this.unmount) {
98 | instance.events.onUnmountEnd();
99 | if (!this.unmounted) {
100 | this.setState({ render: false });
101 | }
102 | } else {
103 | // mounting animation has completed
104 | instance.events.onMountEnd();
105 | }
106 | }
107 |
108 | updateAnimationProgress(instance, animatedProps) {
109 | this.setState(prevState => ({
110 | style: {
111 | ...prevState.style,
112 | ...animatedProps
113 | }
114 | }));
115 | }
116 |
117 | renderTag() {
118 | const { type: Type, children, ...propsToPass } = this.props;
119 |
120 | delete propsToPass.mount;
121 | delete propsToPass.unmount;
122 | delete propsToPass.render;
123 | delete propsToPass.children;
124 | delete propsToPass.events;
125 |
126 | return (
127 |
128 | {children}
129 |
130 | );
131 | }
132 |
133 | render() {
134 | const { type } = this.props;
135 | const { style, render } = this.state;
136 |
137 | if (!render) return null;
138 |
139 | let clonedElement;
140 | if (is.null(type)) {
141 | const childEl = React.Children.only(this.props.children);
142 | return React.cloneElement(childEl, { style });
143 | }
144 |
145 | return type ? this.renderTag() : clonedElement;
146 | }
147 | }
148 |
149 | Observer.defaultProps = {
150 | render: true
151 | };
152 |
153 | Observer.propTypes = {
154 | type: PropTypes.string,
155 | render: PropTypes.bool,
156 | mount: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
157 | unmount: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
158 | children: PropTypes.node
159 | };
160 |
161 | ['div', 'span', 'a', 'button', 'li', 'img'].forEach(type => {
162 | const func = props => ;
163 | Observer[type] = func;
164 | Observer[type].displayName = `Observer.${type}`;
165 | Observer[type].tweenful = true;
166 | });
167 |
168 | export default Observer;
169 |
--------------------------------------------------------------------------------
/src/ObserverGroup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Observer from './Observer';
4 | import { toArray, find } from './helpers';
5 |
6 | class ObserverGroup extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | children: toArray(props.children).map(child => this.mapChild(child))
11 | };
12 |
13 | this.didRender = false;
14 | }
15 |
16 | mapChild(child) {
17 | const { config, skipInitial } = this.props;
18 |
19 | const childEvents = child.props.events || {};
20 | const skipMountAnimation = !this.didRender && skipInitial;
21 |
22 | const events = {
23 | ...childEvents,
24 | onUnmountEnd: () => {
25 | (childEvents.onUnmountEnd || function() {})();
26 | this.onChildUnmount(child.key);
27 | }
28 | };
29 |
30 | if (!child.type.tweenful) {
31 | const mount = { ...config.mount, duration: 10 };
32 | const passedProps = {
33 | ...config,
34 | mount: skipMountAnimation ? mount : config.mount,
35 | key: child.key,
36 | events
37 | };
38 |
39 | return React.createElement(Observer, passedProps, child);
40 | }
41 |
42 | if (skipMountAnimation) {
43 | const propsToPass = { events, mount: { ...child.props.mount, duration: 10 } };
44 | return React.cloneElement(child, propsToPass);
45 | }
46 |
47 | return React.cloneElement(child, { events });
48 | }
49 |
50 | filterChildren(prevData, currentData) {
51 | const prev = toArray(prevData);
52 | const current = toArray(currentData);
53 |
54 | const added = current.filter(child => !prev.find(({ key }) => key === child.key));
55 | const removed = prev.filter(child => !current.find(({ key }) => key === child.key));
56 |
57 | const left = prev.filter(e => !find(removed, e));
58 | const right = current.filter(e => !find(removed, e) && !find(added, e));
59 |
60 | if (left.find((_e, i) => right[i].key !== left[i].key)) {
61 | throw new Error('Order has changed');
62 | }
63 |
64 | const data = [];
65 | let prevIndex = 0;
66 | let currentIndex = 0;
67 |
68 | while (true) {
69 | if (prevIndex === prev.length) {
70 | data.push(...current.filter((_el, i) => i >= currentIndex).map(r => this.mapChild(r)));
71 | return data;
72 | }
73 |
74 | if (currentIndex === current.length) {
75 | data.push(...prev.filter((_el, i) => i >= prevIndex).map(r => this.mapRemovedChild(r)));
76 | return data;
77 | }
78 |
79 | const prevEl = prev[prevIndex];
80 | const currEl = current[currentIndex];
81 | if (prevEl.key === currEl.key) {
82 | data.push(prevEl);
83 | prevIndex++;
84 | currentIndex++;
85 | } else if (find(added, currEl)) {
86 | data.push(this.mapChild(currEl));
87 | currentIndex++;
88 | } else {
89 | data.push(this.mapRemovedChild(prevEl));
90 | prevIndex++;
91 | }
92 | }
93 | }
94 |
95 | mapRemovedChild(child) {
96 | const stateMappedChild = this.state.children.find(c => c.key === child.key);
97 | return React.cloneElement(stateMappedChild, { render: false });
98 | }
99 |
100 | componentDidUpdate(prevProps) {
101 | this.didRender = true;
102 |
103 | if (this.props.children !== prevProps.children) {
104 | this.setState(prevState => ({
105 | children: this.filterChildren(prevState.children, this.props.children)
106 | }));
107 | }
108 | }
109 |
110 | onChildUnmount(key) {
111 | this.setState(prevState => ({
112 | children: prevState.children.filter(child => child.key !== key)
113 | }));
114 | }
115 |
116 | render() {
117 | const { type: Type } = this.props;
118 | const { children } = this.state;
119 |
120 | if (Type) {
121 | const { ...propsToPass } = this.props;
122 | delete propsToPass.children;
123 | delete propsToPass.config;
124 | return {children};
125 | }
126 |
127 | return children;
128 | }
129 | }
130 |
131 | ObserverGroup.propTypes = {
132 | skipInitial: PropTypes.bool,
133 | children: PropTypes.oneOfType([
134 | PropTypes.node,
135 | PropTypes.instanceOf(Observer),
136 | PropTypes.arrayOf(PropTypes.instanceOf(Observer)),
137 | PropTypes.arrayOf(PropTypes.node)
138 | ])
139 | };
140 |
141 | ObserverGroup.div = props => ;
142 | ObserverGroup.span = props => ;
143 | ObserverGroup.ul = props => ;
144 |
145 | ObserverGroup.div.displayName = 'ObserverGroup.div';
146 | ObserverGroup.span.displayName = 'ObserverGroup.span';
147 | ObserverGroup.ul.displayName = 'ObserverGroup.ul';
148 |
149 | export default ObserverGroup;
150 |
--------------------------------------------------------------------------------
/src/SVG.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { parseStartingTransform } from './helpers';
4 | import { getSvgElLength } from './helpers';
5 | import Parser from './parser';
6 | import Engine from './engine';
7 |
8 | class SVG extends React.Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | const transformFrom = parseStartingTransform(props);
13 | this.transformFrom = transformFrom;
14 | this.state = {
15 | style: {
16 | ...props.style
17 | },
18 | render: props.render
19 | };
20 |
21 | this.el = React.createRef();
22 | this.updateAnimationProgress = this.updateAnimationProgress.bind(this);
23 | this.onComplete = this.onComplete.bind(this);
24 | this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
25 | }
26 |
27 | handleVisibilityChange() {
28 | if (!this.engine) return;
29 | this.engine.handleVisibility(document.visibilityState);
30 | }
31 |
32 | componentDidMount() {
33 | document.addEventListener('visibilitychange', this.handleVisibilityChange);
34 | if (!this.state.render) return;
35 |
36 | this.setState(
37 | prevState => ({
38 | style: {
39 | ...prevState.style,
40 | strokeDasharray: getSvgElLength(this.el.current),
41 | strokeDashoffset: 1500
42 | }
43 | }),
44 | () => this.play(true)
45 | );
46 | }
47 |
48 | play(reset = false) {
49 | if (this.engine) {
50 | this.engine.stop();
51 | }
52 |
53 | if (reset) {
54 | this.instance = Parser.parse(this.el.current, this.props, this.transformFrom);
55 | }
56 |
57 | this.engine = new Engine({
58 | id: this.props.id,
59 | instance: this.instance,
60 | animate: this.updateAnimationProgress,
61 | el: this.el.current,
62 | onComplete: this.onComplete
63 | });
64 |
65 | this.engine.play();
66 | }
67 |
68 | stop() {
69 | this.engine.stop();
70 | }
71 |
72 | componentDidUpdate(prevProps) {
73 | const { render } = this.props;
74 |
75 | if (prevProps.render && !render) {
76 | this.setState({ render }, () => this.stop());
77 | }
78 |
79 | if (!prevProps.render && render) {
80 | this.setState({ render }, () => this.play(true));
81 | }
82 | }
83 |
84 | componentWillUnmount() {
85 | this.stop();
86 | document.removeEventListener('visibilitychange', this.handleVisibilityChange);
87 | }
88 |
89 | resetSvgPath() {
90 | this.setState(prevState => ({
91 | style: {
92 | ...prevState.style,
93 | strokeDasharray: 0,
94 | strokeDashoffset: null
95 | }
96 | }));
97 | }
98 |
99 | onComplete(instance) {
100 | const { loop } = instance;
101 | if (loop === false) {
102 | return this.resetSvgPath();
103 | } else if (loop === true) {
104 | return this.play();
105 | } else if (instance.timesCompleted < loop) {
106 | return this.play();
107 | }
108 | }
109 |
110 | updateAnimationProgress(instance, animatedProps) {
111 | this.setState(prevState => ({
112 | style: {
113 | ...prevState.style,
114 | ...animatedProps
115 | }
116 | }));
117 | }
118 |
119 | render() {
120 | const { style, render } = this.state;
121 | const { type: Type, children, ...propsToPass } = this.props;
122 |
123 | if (!render) return null;
124 |
125 | delete propsToPass.delay;
126 | delete propsToPass.endDelay;
127 | delete propsToPass.animate;
128 | delete propsToPass.keyframes;
129 | delete propsToPass.duration;
130 | delete propsToPass.easing;
131 | delete propsToPass.from;
132 | delete propsToPass.type;
133 | delete propsToPass.animate;
134 | delete propsToPass.render;
135 | delete propsToPass.children;
136 | delete propsToPass.transform;
137 |
138 | return (
139 |
140 | {children}
141 |
142 | );
143 | }
144 | }
145 |
146 | SVG.defaultProps = {
147 | render: true
148 | };
149 |
150 | SVG.propTypes = {
151 | render: PropTypes.bool,
152 | type: PropTypes.string,
153 | children: PropTypes.node,
154 | animate: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.func])
155 | };
156 |
157 | ['circle', 'ellipse', 'path', 'line', 'polygon', 'polyline', 'rect', 'text', 'g'].forEach(type => {
158 | const func = props => ;
159 | SVG[type] = func;
160 | SVG[type].displayName = `SVG.${type}`;
161 | SVG[type].tweenful = true;
162 | });
163 |
164 | export default SVG;
165 |
--------------------------------------------------------------------------------
/src/Tweenful.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { parseStartingTransform } from './helpers';
4 | import Parser from './parser';
5 | import Engine from './engine';
6 |
7 | class Tweenful extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | const transformFrom = parseStartingTransform(props);
12 | const transform = transformFrom.order.map(prop => ({
13 | prop,
14 | args: transformFrom.domProperties[prop]
15 | }));
16 | const mappedArgs = args => `${args.map(arg => `${arg.value}${arg.unit}`).join(', ')}`;
17 | const transformFunctions = transform.map(t => `${t.prop}(${mappedArgs(t.args)})`);
18 |
19 | this.transformFrom = transformFrom;
20 | this.element = React.createRef();
21 | this.state = {
22 | style: {
23 | ...props.style,
24 | transform: transformFunctions.join(' ')
25 | },
26 | render: props.render
27 | };
28 |
29 | this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
30 | this.updateAnimationProgress = this.updateAnimationProgress.bind(this);
31 | this.onComplete = this.onComplete.bind(this);
32 | }
33 |
34 | handleVisibilityChange() {
35 | if (!this.engine) return;
36 | this.engine.handleVisibility(document.visibilityState);
37 | }
38 |
39 | componentDidMount() {
40 | document.addEventListener('visibilitychange', this.handleVisibilityChange);
41 |
42 | if (this.state.render) {
43 | this.play(true);
44 | }
45 | }
46 |
47 | componentDidUpdate(prevProps) {
48 | const { render, paused, animate } = this.props;
49 |
50 | if (prevProps.animate !== animate) {
51 | return this.play(true);
52 | }
53 |
54 | if (prevProps.render ^ render) {
55 | this.setState({ render }, () => (render ? this.play(true) : this.stop()));
56 | }
57 |
58 | if (prevProps.paused ^ paused) {
59 | (paused && this.pause()) || this.resume();
60 | }
61 | }
62 |
63 | componentWillUnmount() {
64 | this.stop();
65 | document.removeEventListener('visibilitychange', this.handleVisibilityChange);
66 | }
67 |
68 | onComplete(instance) {
69 | const { loop, timesCompleted } = instance;
70 |
71 | if (!loop) return;
72 | if (loop === true) {
73 | return this.play();
74 | }
75 |
76 | if (timesCompleted < loop) {
77 | return this.play();
78 | }
79 | }
80 |
81 | play(reset = false) {
82 | if (this.engine) {
83 | this.engine.stop();
84 | }
85 |
86 | if (reset) {
87 | this.instance = Parser.parse(this.element.current, this.props, this.transformFrom);
88 | }
89 |
90 | this.engine = new Engine({
91 | instance: this.instance,
92 | animate: this.updateAnimationProgress,
93 | onComplete: this.onComplete
94 | });
95 |
96 | this.engine.play();
97 | }
98 |
99 | pause() {
100 | this.engine.pause();
101 | }
102 |
103 | resume() {
104 | this.engine.resume();
105 | }
106 |
107 | stop() {
108 | this.engine.stop();
109 | }
110 |
111 | updateAnimationProgress(instance, animatedProps) {
112 | this.setState(prevState => {
113 | return {
114 | style: {
115 | ...prevState.style,
116 | ...animatedProps
117 | }
118 | };
119 | });
120 | }
121 |
122 | render() {
123 | const {
124 | props: { type: Type, ...passedProps },
125 | state: { render, style }
126 | } = this;
127 |
128 | if (!render) return null;
129 |
130 | delete passedProps.delay;
131 | delete passedProps.endDelay;
132 | delete passedProps.animate;
133 | delete passedProps.keyframes;
134 | delete passedProps.duration;
135 | delete passedProps.easing;
136 | delete passedProps.from;
137 | delete passedProps.loop;
138 | delete passedProps.direction;
139 | delete passedProps.render;
140 | delete passedProps.running;
141 | delete passedProps.paused;
142 | delete passedProps.context;
143 | delete passedProps.events;
144 | delete passedProps.transform;
145 |
146 | return ;
147 | }
148 | }
149 |
150 | Tweenful.defaultProps = {
151 | render: true,
152 | running: true,
153 | paused: false
154 | };
155 |
156 | Tweenful.propTypes = {
157 | type: PropTypes.string,
158 | render: PropTypes.bool,
159 | running: PropTypes.bool,
160 | paused: PropTypes.bool,
161 | children: PropTypes.node
162 | };
163 |
164 | ['div', 'span', 'a', 'button', 'li', 'img'].forEach(type => {
165 | const func = props => ;
166 | Tweenful[type] = func;
167 | Tweenful[type].displayName = `Tweenful.${type}`;
168 | Tweenful[type].tweenful = true;
169 | });
170 |
171 | export default Tweenful;
172 |
--------------------------------------------------------------------------------
/src/easings/index.js:
--------------------------------------------------------------------------------
1 | const pow = Math.pow;
2 | const c1 = 1.70158;
3 | const c2 = c1 * 1.525;
4 |
5 | export const elastic = (amplitude, period) => t => {
6 | const a = minMax(amplitude, 1, 10);
7 | const p = minMax(period, 0.1, 2);
8 |
9 | var s = Math.asin((1 / a) * p);
10 | return (
11 | ((t = t * 2 - 1) < 0
12 | ? a * Math.pow(2, 10 * t) * Math.sin((s - t) / p)
13 | : 2 - a * Math.pow(2, -10 * t) * Math.sin((s + t) / p)) / 2
14 | );
15 | };
16 |
17 | const minMax = (val, min, max) => {
18 | return Math.min(Math.max(val, min), max);
19 | };
20 |
21 | const bounceOut = x => {
22 | const n1 = 7.5625;
23 | const d1 = 2.75;
24 |
25 | if (x < 1 / d1) {
26 | return n1 * x * x;
27 | } else if (x < 2 / d1) {
28 | return n1 * (x -= 1.5 / d1) * x + 0.75;
29 | } else if (x < 2.5 / d1) {
30 | return n1 * (x -= 2.25 / d1) * x + 0.9375;
31 | } else {
32 | return n1 * (x -= 2.625 / d1) * x + 0.984375;
33 | }
34 | };
35 |
36 | export default {
37 | // no easing, no acceleration
38 | linear: t => {
39 | return t;
40 | },
41 | // accelerating from zero velocity
42 | easeInQuad: t => {
43 | return t * t;
44 | },
45 | // decelerating to zero velocity
46 | easeOutQuad: t => {
47 | return t * (2 - t);
48 | },
49 | // acceleration until halfway, then deceleration
50 | easeInOutQuad: t => {
51 | return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
52 | },
53 | // accelerating from zero velocity
54 | easeInCubic: t => {
55 | return t * t * t;
56 | },
57 | // decelerating to zero velocity
58 | easeOutCubic: t => {
59 | return --t * t * t + 1;
60 | },
61 | // acceleration until halfway, then deceleration
62 | easeInOutCubic: t => {
63 | return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
64 | },
65 | // accelerating from zero velocity
66 | easeInQuart: t => {
67 | return t * t * t * t;
68 | },
69 | // decelerating to zero velocity
70 | easeOutQuart: t => {
71 | return 1 - --t * t * t * t;
72 | },
73 | // acceleration until halfway, then deceleration
74 | easeInOutQuart: t => {
75 | return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;
76 | },
77 | // accelerating from zero velocity
78 | easeInQuint: t => {
79 | return t * t * t * t * t;
80 | },
81 | // decelerating to zero velocity
82 | easeOutQuint: t => {
83 | return 1 + --t * t * t * t * t;
84 | },
85 | // acceleration until halfway, then deceleration
86 | easeInOutQuint: t => {
87 | return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t;
88 | },
89 | easeInOutBack: x => {
90 | return x < 0.5
91 | ? (pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
92 | : (pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
93 | },
94 | easeInBounce: x => {
95 | return 1 - bounceOut(1 - x);
96 | },
97 | sine: t => 1 - Math.cos((t * Math.PI) / 2),
98 | circ: t => 1 - Math.sqrt(1 - t * t),
99 | back: t => t * t * (3 * t - 2),
100 | bounce: t => {
101 | let pow2;
102 | let b = 4;
103 | while (t < ((pow2 = Math.pow(2, --b)) - 1) / 11);
104 | return 1 / Math.pow(4, 3 - b) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - t, 2);
105 | },
106 | easeOutBounce: bounceOut
107 | };
108 |
--------------------------------------------------------------------------------
/src/engine/index.js:
--------------------------------------------------------------------------------
1 | import { getAnimationProgress, calculateProgress } from '../helpers';
2 | import { computeProgressTick, updateTimesCompleted } from './utils';
3 |
4 | class Engine {
5 | constructor(options) {
6 | this.frame = null;
7 | this.options = options;
8 | this.didComplete = false;
9 | this.lastTime = 0;
10 | this.startTime = 0;
11 | this.lastTick = 0;
12 |
13 | this.progress = this.progress.bind(this);
14 | }
15 |
16 | play() {
17 | this.frame = requestAnimationFrame(this.progress);
18 | }
19 |
20 | progress(now) {
21 | const {
22 | el,
23 | instance,
24 | animate,
25 | onComplete,
26 | instance: { duration, animations }
27 | } = this.options;
28 |
29 | if (!this.startTime) {
30 | this.didComplete = false;
31 | this.startTime = now;
32 | }
33 |
34 | const progressTick = computeProgressTick(instance);
35 | const tick = now + progressTick + (this.lastTime - this.startTime);
36 |
37 | instance.time = tick;
38 | instance.progress = calculateProgress(tick, duration);
39 | const animatedProps = getAnimationProgress(tick, this.lastTick, animations, el);
40 |
41 | animate(instance, animatedProps);
42 |
43 | if (instance.progress === 1) {
44 | instance.events.onAnimationEnd();
45 | updateTimesCompleted(instance);
46 | onComplete(instance);
47 | }
48 |
49 | this.lastTick = tick;
50 | if (tick < duration) {
51 | this.play();
52 | } else {
53 | this.didComplete = true;
54 | this.reset();
55 | }
56 | }
57 |
58 | handleVisibility(state) {
59 | state === 'visible' ? this.resume() : this.pause();
60 | }
61 |
62 | pause() {
63 | if (!this.didComplete) {
64 | cancelAnimationFrame(this.frame);
65 | }
66 | }
67 |
68 | resume() {
69 | if (!this.didComplete) {
70 | this.startTime = 0;
71 | this.lastTime = this.options.instance.time;
72 | this.play();
73 | }
74 | }
75 |
76 | stop() {
77 | cancelAnimationFrame(this.frame);
78 | this.reset();
79 | }
80 |
81 | reset() {
82 | this.frame = null;
83 | this.lastTime = 0;
84 | this.startTime = 0;
85 | this.lastTick = 0;
86 | this.tick = 0;
87 | }
88 | }
89 |
90 | export default Engine;
91 |
--------------------------------------------------------------------------------
/src/engine/utils.js:
--------------------------------------------------------------------------------
1 | export const modulo = (delay, duration) => {
2 | while (delay >= duration) {
3 | delay -= duration;
4 | }
5 |
6 | return delay;
7 | };
8 |
9 | export const computeProgressTick = instance => {
10 | // ignore negative delay after the first animation occurred
11 | if (instance.delay >= 0 || instance.timesCompleted > 0) return 0;
12 |
13 | const { duration, loop } = instance;
14 | const delay = Math.abs(instance.delay);
15 |
16 | if (loop === true) {
17 | return modulo(delay, duration);
18 | } else if (loop === false) {
19 | return delay > duration ? duration : modulo(delay, duration);
20 | } else if (loop > 0) {
21 | // delay might last longer than the animation itself
22 | // else we'll just calculate the modulo
23 | const totalDuration = loop * duration;
24 | return delay > totalDuration ? duration : modulo(delay, duration);
25 | }
26 |
27 | return 0;
28 | };
29 |
30 | export const updateTimesCompleted = instance => {
31 | const { duration, delay, loop } = instance;
32 |
33 | if (!instance.timesCompleted && delay < 0 && loop > 0) {
34 | let absDelay = Math.abs(delay);
35 | if (absDelay < duration) {
36 | instance.timesCompleted++;
37 | }
38 |
39 | while (absDelay >= duration) {
40 | absDelay -= duration;
41 | instance.timesCompleted++;
42 | }
43 | } else {
44 | instance.timesCompleted++;
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/src/helpers/constants.js:
--------------------------------------------------------------------------------
1 | export const validDOMProperties = [
2 | 'left',
3 | 'right',
4 | 'top',
5 | 'fill',
6 | 'bottom',
7 | 'opacity',
8 | 'strokeDashoffset',
9 | 'backgroundColor',
10 | 'borderRadius',
11 | 'height',
12 | 'width',
13 | 'marginBottom',
14 | 'marginTop'
15 | ];
16 |
17 | export const colorProps = ['fill', 'color', 'backgroundColor'];
18 |
19 | export const transformProps = [
20 | 'translate',
21 | 'translate3d',
22 | 'translateX',
23 | 'translateY',
24 | 'translateZ',
25 | 'skew',
26 | 'skewX',
27 | 'skewY',
28 | 'scale',
29 | 'scale3d',
30 | 'scaleX',
31 | 'scaleY',
32 | 'scaleZ',
33 | 'rotate',
34 | 'rotate3d',
35 | 'rotateX',
36 | 'rotateY',
37 | 'rotateZ'
38 | ];
39 |
40 | export const validKeyframeProps = [
41 | ...validDOMProperties.slice(0, validDOMProperties.length - 1),
42 | ...transformProps
43 | ];
44 |
45 | export const svgProps = [
46 | 'stroke',
47 | 'strokeDasharray',
48 | 'strokeDashoffset',
49 | 'strokeDashOpacity',
50 | 'fill',
51 | 'fillOpacity'
52 | ];
53 |
54 | export const validTransforms = {
55 | translate: { argsMin: 1, argsMax: 2, start: 0 },
56 | translate3d: { argsMin: 3, argsMax: 3, start: 0 },
57 | translateX: { argsMin: 1, argsMax: 1, start: 0 },
58 | translateY: { argsMin: 1, argsMax: 1, start: 0 },
59 | translateZ: { argsMin: 1, argsMax: 1, start: 0 },
60 | skew: { argsMin: 1, argsMax: 2, start: 0 },
61 | skewX: { argsMin: 1, argsMax: 1, start: 0 },
62 | skewY: { argsMin: 1, argsMax: 1, start: 0 },
63 | scale: { argsMin: 1, argsMax: 2, start: 1 },
64 | scale3d: { argsMin: 3, argsMax: 3, start: 1 },
65 | scaleX: { argsMin: 1, argsMax: 1, start: 1 },
66 | scaleY: { argsMin: 1, argsMax: 1, start: 1 },
67 | scaleZ: { argsMin: 1, argsMax: 1, start: 1 },
68 | rotate: { argsMin: 1, argsMax: 1, start: 0 },
69 | rotate3d: { argsMin: 4, argsMax: 4, start: 0 },
70 | rotateX: { argsMin: 1, argsMax: 1, start: 0 },
71 | rotateY: { argsMin: 1, argsMax: 1, start: 0 },
72 | rotateZ: { argsMin: 1, argsMax: 1, start: 0 }
73 | };
74 |
75 | export const regexExpressions = {
76 | // 100px, 25%, 30deg
77 | functionArguments: () => /[ \t]*([-]{0,1}\d+(\.*\d*))(px|%|deg|){1}[ \t]*/g,
78 | // 100px
79 | unit: () => /(([-]?[0-9]+.[0-9]+)|([-]?[0-9]+))(%|[a-zA-Z]+|)/g,
80 | // 100% 25rem 30px
81 | valueUnitPair: () =>
82 | /[+-]?\d*\.?\d+(?:\.\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vargsMin|vargsMax|deg|rad)?/g
83 | };
84 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export * from './utils';
2 | export * from './svg-utils';
3 |
--------------------------------------------------------------------------------
/src/helpers/svg-utils.js:
--------------------------------------------------------------------------------
1 | const getDistance = (p1, p2) => {
2 | const x = Math.pow(p2.x - p1.x, 2);
3 | const y = Math.pow(p2.y - p1.y, 2);
4 | return Math.sqrt(x + y);
5 | };
6 |
7 | const getCircleLength = el => {
8 | return Math.PI * 2 * el.getAttribute('r');
9 | };
10 |
11 | const getRectLength = el => {
12 | const width = el.getAttribute('width') * 2;
13 | const height = el.getAttribute('height') * 2;
14 | return width + height;
15 | };
16 |
17 | const getLineLength = el => {
18 | return getDistance(
19 | { x: el.getAttribute('x1'), y: el.getAttribute('y1') },
20 | { x: el.getAttribute('x2'), y: el.getAttribute('y2') }
21 | );
22 | };
23 |
24 | const getPolylineLength = el => {
25 | const points = el.points;
26 | let totalLength = 0;
27 | let previousPos;
28 | for (let i = 0; i < points.numberOfItems; i++) {
29 | const currentPos = points.getItem(i);
30 | if (i > 0) {
31 | totalLength += getDistance(previousPos, currentPos);
32 | }
33 | previousPos = currentPos;
34 | }
35 | return totalLength;
36 | };
37 |
38 | const getPolygonLength = el => {
39 | const points = el.points;
40 | const distance = getDistance(points.getItem(points.numberOfItems - 1), points.getItem(0));
41 | return getPolylineLength(el) + distance;
42 | };
43 |
44 | export const getSvgElLength = el => {
45 | switch (el.tagName.toLowerCase()) {
46 | case 'path':
47 | return el.getTotalLength();
48 | case 'circle':
49 | return getCircleLength(el);
50 | case 'rect':
51 | return getRectLength(el);
52 | case 'line':
53 | return getLineLength(el);
54 | case 'polyline':
55 | return getPolylineLength(el);
56 | case 'polygon':
57 | return getPolygonLength(el);
58 | default:
59 | return 0;
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/src/helpers/utils.js:
--------------------------------------------------------------------------------
1 | import parseColor from 'color-parser';
2 | import easings from '../easings';
3 | import { getSvgElLength } from './svg-utils';
4 | import {
5 | colorProps,
6 | validTransforms,
7 | validDOMProperties,
8 | validKeyframeProps,
9 | regexExpressions,
10 | transformProps
11 | } from './constants';
12 |
13 | export const percentage = object => duration => {
14 | const keys = Object.keys(object)
15 | .map(i => parseInt(i.slice(0, -1), 10))
16 | .sort((a, b) => {
17 | if (a < b) return -1;
18 | if (a > b) return 1;
19 | return 0;
20 | });
21 |
22 | const calculateDiff = index => {
23 | const curr = keys[index];
24 | const next = keys[index + 1];
25 | return next - curr;
26 | };
27 |
28 | const parsedAnimate = [];
29 | keys.forEach((key, index) => {
30 | if (index + 1 === keys.length) return;
31 |
32 | if (index === 0) {
33 | parsedAnimate.push({
34 | duration: 0,
35 | ...object[`${key}%`]
36 | });
37 | }
38 |
39 | const nextKey = keys[index + 1];
40 | const next = object[`${nextKey}%`];
41 | const percentRange = calculateDiff(index);
42 | parsedAnimate.push({
43 | duration: (percentRange / 100) * duration,
44 | ...next
45 | });
46 | });
47 |
48 | return parsedAnimate;
49 | };
50 |
51 | export const find = (arr, e) => arr.find(a => e.key === a.key);
52 |
53 | export const toArray = object => {
54 | if (is.null(object)) return [];
55 | if (Array.isArray(object)) return object;
56 | return [object];
57 | };
58 |
59 | export const is = {
60 | svg: el => el instanceof SVGElement,
61 | null: value => value === undefined || value === null,
62 | array: el => Array.isArray(el)
63 | };
64 |
65 | export const toUnit = val => {
66 | const result = regexExpressions.valueUnitPair().exec(val);
67 | return result ? result[1] : '';
68 | };
69 |
70 | export const auto = () => [{ value: 'auto', unit: '', auto: true }];
71 |
72 | export const unitToNumber = string => {
73 | if (is.null(string) || string === '') return null;
74 | if (string === 'auto') return auto();
75 |
76 | const groups = getRegexGroups(regexExpressions.valueUnitPair(), string);
77 | return groups.map(group => {
78 | const match = regexExpressions.valueUnitPair().exec(group);
79 | return {
80 | value: parseFloat(match[0]),
81 | unit: match[1] || ''
82 | };
83 | });
84 | };
85 |
86 | export const normalizeTweenUnit = (el, tween) => {
87 | const { from: tweenFrom, to: tweenTo } = tween;
88 |
89 | for (let i = 0; i < tweenFrom.length; i += 1) {
90 | const from = tweenFrom[i];
91 | const to = tweenTo[i];
92 |
93 | // color will return an array such as [255, 255, 255, 255]
94 | if (is.null(from.unit) || is.null(to.unit)) continue;
95 | // skip pixel to pixel conversion
96 | if (from.unit === 'px' && to.unit === 'px') continue;
97 | // skip unitless conversion for now, such as opacity
98 | if (from.unit === '' && to.unit === '') continue;
99 |
100 | if (to.unit === 'px' && from.auto) continue;
101 | if (from.unit === 'px' && to.auto) continue;
102 |
103 | if (from.unit !== 'px') {
104 | const convertedValue = convertUnitToPixels(el, from.value, from.unit);
105 | from.value = convertedValue;
106 | from.unit = 'px';
107 | }
108 |
109 | if (to.unit !== 'px') {
110 | const convertedValue = convertUnitToPixels(el, to.value, to.unit);
111 | to.value = convertedValue;
112 | to.unit = 'px';
113 | }
114 | }
115 | };
116 |
117 | export const parseStartingTransform = ({ transform }) => {
118 | if (!transform) return { order: [], domProperties: {} };
119 |
120 | const keys = Object.keys(transform);
121 | const domProperties = getValidDOMProperties(keys, transformProps);
122 | const props = {};
123 | domProperties.forEach(prop => (props[prop] = unitToNumber(transform[prop])));
124 |
125 | return {
126 | order: domProperties,
127 | domProperties: props
128 | };
129 | };
130 |
131 | export const convertUnitToPixels = (el, value, conversionUnit) => {
132 | const unit = toUnit(value);
133 | if (['deg', 'rad', 'turn'].includes(unit)) return;
134 |
135 | const baseline = 100;
136 | const tempEl = el.cloneNode();
137 | const parentEl = el.parentNode && el.parentNode !== document ? el.parentNode : document.body;
138 |
139 | parentEl.appendChild(tempEl);
140 | tempEl.style.position = 'relative';
141 | tempEl.style.height = `${baseline}${conversionUnit}`;
142 | const offset = tempEl.offsetHeight;
143 | parentEl.removeChild(tempEl);
144 |
145 | return (parseFloat(value) * offset) / 100;
146 | };
147 |
148 | export const getValidDOMProperties = (properties, lookupList = validDOMProperties) => {
149 | const validProperties = [];
150 | properties.forEach(property => {
151 | if (lookupList.indexOf(property) > -1) {
152 | validProperties.push(property);
153 | }
154 | });
155 |
156 | return validProperties;
157 | };
158 |
159 | export const getPropertyProgress = (tween, easing) => {
160 | const eased = (from, to) => from + easing * (to - from);
161 | const { from, to } = tween;
162 |
163 | if (tween.color) {
164 | const rgb = {
165 | r: eased(from[0], to[0]),
166 | g: eased(from[1], to[1]),
167 | b: eased(from[2], to[2]),
168 | a: eased(from[3], to[3])
169 | };
170 | return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
171 | } else if (tween.path) {
172 | return `${eased(from[0].value, to[0].value)}`;
173 | }
174 |
175 | const [{ unit }] = from;
176 | return `${eased(from[0].value, to[0].value)}${unit}`;
177 | };
178 |
179 | export const getAnimationProgress = (tick, lastTick, animations, el) => {
180 | const getTween = (tweens, tick) => tweens.find(({ start, end }) => tick >= start && tick <= end);
181 |
182 | const animatedProps = {};
183 | const transforms = [];
184 |
185 | animations.forEach(animate => {
186 | const { property, tweens } = animate;
187 | const tween = getTween(tweens, tick) || getTween(tweens, lastTick);
188 | // check if lastTick matches any tween in order to complete animation at 100%
189 | if (!tween) return;
190 |
191 | const tweenProgress = calculateProgress(tick - tween.start, tween.duration);
192 | const easing = tween.easing(tweenProgress);
193 |
194 | if (transformProps.indexOf(property) > -1) {
195 | const easedTransformValues = tween.to.map(({ value: to, unit }, index) => {
196 | const { value: from } = tween.from[index];
197 | const eased = from + easing * (to - from);
198 | return `${eased}${unit}`;
199 | });
200 |
201 | transforms.push(`${property}(${easedTransformValues.map(i => i)})`);
202 | } else if (property === 'strokeDashoffset') {
203 | const pathLength = getSvgElLength(el);
204 | animatedProps['strokeDasharray'] = pathLength;
205 | animatedProps['strokeDashoffset'] = (getPropertyProgress(tween, easing) / 100) * pathLength;
206 | } else {
207 | animatedProps[property] = getPropertyProgress(tween, easing);
208 | }
209 | });
210 |
211 | if (transforms.length > 0) {
212 | animatedProps.transform = transforms.reduce((funcs, t) => `${funcs} ${t}`);
213 | }
214 |
215 | return animatedProps;
216 | };
217 |
218 | export const getAnimatableProperties = array => {
219 | const animatableProps = [];
220 | array.forEach(elem => {
221 | const keys = Object.keys(elem);
222 | const domProperties = getValidDOMProperties(keys, validKeyframeProps);
223 | domProperties.forEach(prop => {
224 | if (animatableProps.indexOf(prop) === -1) {
225 | animatableProps.push(prop);
226 | }
227 | });
228 | });
229 |
230 | return animatableProps;
231 | };
232 |
233 | const getTransformMapping = (transformFrom, mappedTransform, property) => {
234 | if (is.null(transformFrom.domProperties[property])) {
235 | // will help knowing where to start animating from (e.g. 0, 1)
236 | return new Array(mappedTransform.argsMax).fill(0).map(() => ({
237 | value: mappedTransform.start,
238 | unit: ''
239 | }));
240 | }
241 |
242 | return transformFrom.domProperties[property];
243 | };
244 |
245 | export const getStartingValues = (element, transformFrom, animatableProps) => {
246 | const defaultProperties = {};
247 | const computed = getComputedStyle(element);
248 |
249 | animatableProps.forEach(property => {
250 | const mappedTransform = validTransforms[property];
251 | if (mappedTransform) {
252 | defaultProperties[property] = getTransformMapping(transformFrom, mappedTransform, property);
253 | return;
254 | }
255 |
256 | let val = computed[property];
257 | if (property === 'height') {
258 | val = element.scrollHeight;
259 | } else if (property === 'width') {
260 | val = element.scrollWidth;
261 | }
262 |
263 | if (colorProps.indexOf(property) > -1) {
264 | const { r, g, b, a } = parseColor(val);
265 | defaultProperties[property] = [r, g, b, a];
266 | return;
267 | }
268 |
269 | const unit = property === 'opacity' ? '' : 'px';
270 | defaultProperties[property] = unitToNumber(`${val}${unit}`);
271 | });
272 |
273 | return defaultProperties;
274 | };
275 |
276 | export const getRegexGroups = (regex, string) => {
277 | let groups = [];
278 | let match = regex.exec(string);
279 | while (match) {
280 | groups.push(match[0]);
281 | match = regex.exec(string);
282 | }
283 |
284 | return groups;
285 | };
286 |
287 | export const parseFunctionArguments = string => {
288 | const regex = regexExpressions.functionArguments();
289 | return getRegexGroups(regex, string).map(value => value.trim());
290 | };
291 |
292 | export const parseEasing = easing => {
293 | if (typeof easing === 'function') {
294 | return easing;
295 | }
296 |
297 | return easings[easing];
298 | };
299 |
300 | export const pickFirstNotNull = (...values) => {
301 | if (!values || !values.length) return null;
302 | return values.find(val => !is.null(val));
303 | };
304 |
305 | export const calculateProgress = (tick, duration) => {
306 | const progress = getProgress(tick, duration);
307 | return Math.min(progress, 1);
308 | };
309 |
310 | export const getProgress = (tick, duration) => (tick === 0 ? 0 : tick / duration);
311 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import bezier from 'bezier-easing';
2 | import Tweenful from './Tweenful';
3 | import Observer from './Observer';
4 | import ObserverGroup from './ObserverGroup';
5 | import SVG from './SVG';
6 | import { percentage } from './helpers';
7 | import { elastic } from './easings';
8 |
9 | export { SVG, Observer, ObserverGroup, percentage, bezier, elastic };
10 |
11 | export default Tweenful;
12 |
--------------------------------------------------------------------------------
/src/parser/index.js:
--------------------------------------------------------------------------------
1 | import parseColor from 'color-parser';
2 | import { colorProps, transformProps, validKeyframeProps } from '../helpers/constants';
3 | import {
4 | is,
5 | getSvgElLength,
6 | normalizeTweenUnit,
7 | getValidDOMProperties,
8 | getAnimatableProperties,
9 | unitToNumber,
10 | getStartingValues,
11 | pickFirstNotNull,
12 | parseEasing
13 | } from '../helpers';
14 |
15 | class Parser {
16 | parse(el, options, transformFrom) {
17 | const animations = this.parseOptions(el, options, transformFrom);
18 | const duration = Math.max(...animations.map(({ tweens: t }) => t[t.length - 1].end));
19 | const events = this.parseEvents(options);
20 |
21 | return {
22 | duration,
23 | progress: 0,
24 | lastTick: 0,
25 | timesCompleted: 0,
26 | paused: false,
27 | delay: options.delay || 0,
28 | loop: options.loop,
29 | direction: options.direction || 'normal',
30 | animations,
31 | transformFrom,
32 | events
33 | };
34 | }
35 |
36 | parseEvents(options) {
37 | const events = options.events || {};
38 | const func = () => {};
39 |
40 | const parsedEvents = {
41 | // Observer events
42 | onMountStart: events.onMountStart || func,
43 | onMountEnd: events.onMountEnd || func,
44 | onUnmountStart: events.onUnmountStart || func,
45 | onUnmountEnd: events.onUnmountEnd || func,
46 |
47 | // Tweenful events
48 | onAnimationStart: events.onAnimationStart || func,
49 | onAnimationEnd: events.onAnimationEnd || func
50 | };
51 |
52 | return parsedEvents;
53 | }
54 |
55 | parseAnimate({ animate, duration }) {
56 | const result = [];
57 | const arr = is.array(animate) ? animate : [animate];
58 |
59 | arr.forEach(anim => {
60 | if (typeof anim === 'function') {
61 | result.push(...anim(duration));
62 | } else {
63 | result.push(anim);
64 | }
65 | });
66 |
67 | return result;
68 | }
69 |
70 | parseOptions(el, options, transformFrom) {
71 | const args = {
72 | el,
73 | options,
74 | transformFrom,
75 | animatable: this.parseAnimate(options)
76 | };
77 |
78 | return this.getAnimations(args);
79 | }
80 |
81 | parseHeightPercentage(el, tween, property) {
82 | const offset = property === 'width' ? el.scrollWidth : el.scrollHeight;
83 | const [{ value: from }] = tween.from;
84 | const [{ value: to }] = tween.to;
85 |
86 | if (from === 'auto') {
87 | tween.from[0].value = offset;
88 | tween.from[0].unit = 'px';
89 | }
90 |
91 | if (to === 'auto') {
92 | tween.to[0].value = offset;
93 | tween.to[0].unit = 'px';
94 | }
95 | }
96 |
97 | parseTween(el, property, animate, from, animation, missingProps, options) {
98 | const { duration, easing, endDelay, pathLength } = options;
99 |
100 | const isTransformProperty = transformProps.includes(property);
101 | const isPropertyTweenable = !missingProps.includes(property);
102 | const isColor = colorProps.includes(property);
103 | const delay = Math.max(0, options.delay);
104 | const end = duration + delay + endDelay;
105 | const tween = {
106 | duration,
107 | easing: parseEasing(easing),
108 | startDelay: delay,
109 | endDelay
110 | };
111 |
112 | if (isPropertyTweenable && property === 'strokeDashoffset') {
113 | const [from, to] = animate['strokeDashoffset'];
114 | tween.from = unitToNumber(`${(from / 100) * pathLength}`);
115 | tween.to = unitToNumber(`${(to / 100) * pathLength}`);
116 | tween.path = true;
117 | }
118 |
119 | if (isPropertyTweenable && isColor) {
120 | tween.color = true;
121 | tween.to = tween.from;
122 |
123 | if (animate[property]) {
124 | const { r, g, b, a } = parseColor(animate[property]);
125 | tween.to = [r, g, b, a];
126 | }
127 | }
128 |
129 | if (is.array(animate[property])) {
130 | const [from, to] = animate[property];
131 | tween.from = unitToNumber(from);
132 | tween.to = unitToNumber(to);
133 |
134 | const isAuto = [from, to].includes('auto');
135 | const isAutoProp = ['height', 'width'].includes(property);
136 |
137 | if (isAutoProp && isAuto) {
138 | this.parseHeightPercentage(el, tween);
139 | }
140 | }
141 |
142 | if (animation) {
143 | // tween is already part of an animation
144 | const lastTween = animation.tweens[animation.tweens.length - 1];
145 | tween.from = tween.from || lastTween.to;
146 | tween.to =
147 | tween.to || (isPropertyTweenable ? unitToNumber(animate[property] || 0) : lastTween.to);
148 | tween.start = lastTween.end + tween.startDelay;
149 | tween.end = lastTween.end + end;
150 |
151 | if (!isTransformProperty) {
152 | normalizeTweenUnit(el, tween);
153 | }
154 |
155 | return tween;
156 | }
157 |
158 | tween.from = tween.from || from[property];
159 | tween.to =
160 | tween.to || (is.null(animate[property]) ? from[property] : unitToNumber(animate[property]));
161 | tween.start = 0 + tween.startDelay;
162 | tween.end = end;
163 |
164 | if (!isTransformProperty) {
165 | normalizeTweenUnit(el, tween);
166 | }
167 |
168 | return tween;
169 | }
170 |
171 | getAnimations({ el, options, animatable, transformFrom }) {
172 | const animatableProps = getAnimatableProperties(animatable);
173 | const from = getStartingValues(el, transformFrom, animatableProps);
174 | const animations = [];
175 |
176 | animatable.forEach(animate => {
177 | const domProps = getValidDOMProperties(Object.keys(animate), validKeyframeProps);
178 | const missingProps = animatableProps.filter(p => !domProps.includes(p));
179 | const iterableDOMProperties = [...domProps, ...missingProps];
180 |
181 | const config = {
182 | loop: options.loop,
183 | duration: pickFirstNotNull(animate.duration, options.duration / animatable.length),
184 | delay: pickFirstNotNull(animate.delay, options.delay, 0),
185 | endDelay: pickFirstNotNull(animate.endDelay, options.endDelay, 0),
186 | easing: animate.easing || options.easing,
187 | // supply pathLength for path animations
188 | pathLength: is.svg(el) ? getSvgElLength(el) : 0
189 | };
190 |
191 | iterableDOMProperties.forEach(property => {
192 | const animation = animations.find(anim => anim.property === property);
193 | const tween = this.parseTween(el, property, animate, from, animation, missingProps, config);
194 |
195 | if (animation) {
196 | animation.tweens.push(tween);
197 | } else {
198 | animations.push({
199 | property,
200 | tweens: [tween]
201 | });
202 | }
203 | });
204 | });
205 |
206 | return animations;
207 | }
208 | }
209 |
210 | export default new Parser();
211 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | entry: './src/index.js',
8 | devtool: 'cheap-module-source-map',
9 |
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'js/react-tweenful.js',
13 | library: 'Tweenful',
14 | libraryTarget: 'commonjs2'
15 | },
16 |
17 | plugins: [
18 | new CleanWebpackPlugin({}),
19 | new webpack.DefinePlugin({
20 | 'process.env.NODE_ENV': JSON.stringify('development')
21 | })
22 | ],
23 |
24 | resolve: {
25 | extensions: ['.js', '.jsx', '.json'],
26 | alias: {
27 | src: path.resolve(__dirname, 'src'),
28 | samples: path.resolve(__dirname, 'site')
29 | }
30 | },
31 |
32 | module: {
33 | rules: [
34 | {
35 | test: /\.(js|jsx)$/,
36 | use: ['babel-loader'],
37 | include: /src/
38 | },
39 | {
40 | test: /\.(js|jsx)$/,
41 | use: ['eslint-loader'],
42 | include: /src/
43 | },
44 | {
45 | test: /\.(css|scss)$/,
46 | use: [{ loader: 'css-loader' }, { loader: 'sass-loader' }],
47 | include: /src/
48 | }
49 | ]
50 | },
51 |
52 | externals: {
53 | react: {
54 | commonjs: 'react',
55 | commonjs2: 'react',
56 | amd: 'react',
57 | root: 'React'
58 | },
59 | 'react-dom': {
60 | commonjs: 'react-dom',
61 | commonjs2: 'react-dom',
62 | amd: 'react-dom',
63 | root: 'ReactDOM'
64 | }
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
2 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
3 | const TerserPlugin = require('terser-webpack-plugin');
4 | const webpack = require('webpack');
5 | const path = require('path');
6 |
7 | module.exports = {
8 | mode: 'production',
9 | entry: './src/index.js',
10 | devtool: 'source-map',
11 |
12 | output: {
13 | path: path.resolve(__dirname, 'dist'),
14 | filename: 'react-tweenful.js',
15 | library: 'Tweenful',
16 | libraryTarget: 'commonjs2'
17 | },
18 |
19 | optimization: {
20 | minimizer: [new TerserPlugin(), new OptimizeCSSAssetsPlugin({})]
21 | },
22 |
23 | resolve: {
24 | extensions: ['.js', '.scss'],
25 | alias: {
26 | src: path.resolve(__dirname, 'src'),
27 | samples: path.resolve(__dirname, 'site')
28 | }
29 | },
30 |
31 | module: {
32 | rules: [
33 | {
34 | test: /\.(js|jsx)$/,
35 | use: ['babel-loader'],
36 | include: /src/
37 | },
38 | {
39 | test: /\.(js|jsx)$/,
40 | use: ['eslint-loader'],
41 | include: /src/
42 | }
43 | ]
44 | },
45 |
46 | plugins: [
47 | new CleanWebpackPlugin({
48 | watch: true,
49 | beforeEmit: true
50 | }),
51 | new webpack.DefinePlugin({
52 | 'process.env.NODE_ENV': JSON.stringify('production')
53 | })
54 | ],
55 |
56 | externals: {
57 | 'react': {
58 | commonjs: 'react',
59 | commonjs2: 'react',
60 | amd: 'react',
61 | root: 'React'
62 | },
63 | 'react-dom': {
64 | commonjs: 'react-dom',
65 | commonjs2: 'react-dom',
66 | amd: 'react-dom',
67 | root: 'ReactDOM'
68 | }
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/webpack.site.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | mode: 'development',
8 |
9 | entry: './site/index.js',
10 |
11 | devtool: 'cheap-module-source-map',
12 |
13 | output: {
14 | publicPath: "/",
15 | path: path.resolve(__dirname, 'dist'),
16 | filename: 'site-bundle.js'
17 | },
18 |
19 | devServer: {
20 | open: true,
21 | compress: true,
22 | historyApiFallback: true,
23 | contentBase: path.join(__dirname, 'dist')
24 | },
25 |
26 | resolve: {
27 | alias: {
28 | 'react-tweenful': path.resolve(__dirname, 'src'),
29 | 'site': path.resolve(__dirname, 'site')
30 | },
31 | extensions: ['.js', '.css', '.scss']
32 | },
33 |
34 | module: {
35 | rules: [
36 | {
37 | test: /\.(js|jsx)$/,
38 | use: ['babel-loader'],
39 | exclude: /node_modules/
40 | },
41 | {
42 | test: /\.(js|jsx)$/,
43 | use: ['eslint-loader'],
44 | include: /samples/
45 | },
46 | {
47 | test: /\.(css|scss)$/,
48 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'sass-loader' }]
49 | },
50 | {
51 | test: /\.(png|svg|jpg|gif)$/,
52 | use: ['file-loader']
53 | },
54 | {
55 | test: /\.(woff|woff2|eot|ttf|otf)$/,
56 | use: ['file-loader']
57 | }
58 | ]
59 | },
60 |
61 | plugins: [
62 | new CleanWebpackPlugin({
63 | watch: true,
64 | beforeEmit: true
65 | }),
66 | new HtmlWebpackPlugin({
67 | inject: true,
68 | template: './site/html/index.html'
69 | }),
70 | new webpack.DefinePlugin({
71 | 'process.env.NODE_ENV': JSON.stringify('development')
72 | }),
73 | new webpack.NamedModulesPlugin()
74 | ]
75 | };
--------------------------------------------------------------------------------
/webpack.site.prod.js:
--------------------------------------------------------------------------------
1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CopyPlugin = require('copy-webpack-plugin');
5 | const TerserPlugin = require('terser-webpack-plugin');
6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
7 | const webpack = require('webpack');
8 | const path = require('path');
9 |
10 | module.exports = {
11 | mode: 'production',
12 | entry: './site/index.js',
13 |
14 | output: {
15 | path: path.resolve(__dirname, 'dist'),
16 | filename: '[name].js'
17 | },
18 |
19 | resolve: {
20 | alias: {
21 | 'react-tweenful': path.resolve(__dirname, 'src'),
22 | 'site': path.resolve(__dirname, 'site')
23 | },
24 | extensions: ['.js', '.css', '.scss']
25 | },
26 |
27 | module: {
28 | rules: [
29 | {
30 | test: /\.(js|jsx)$/,
31 | use: ['babel-loader'],
32 | exclude: /node_modules/
33 | },
34 | {
35 | test: /\.(js|jsx)$/,
36 | use: ['eslint-loader'],
37 | include: /samples/
38 | },
39 | {
40 | test: /\.(css|scss)$/,
41 | use: [
42 | { loader: MiniCssExtractPlugin.loader },
43 | { loader: 'css-loader' },
44 | { loader: 'sass-loader' }
45 | ]
46 | },
47 | {
48 | test: /\.(png|svg|jpg|gif)$/,
49 | use: ['file-loader']
50 | },
51 | {
52 | test: /\.(woff|woff2|eot|ttf|otf)$/,
53 | use: ['file-loader']
54 | }
55 | ]
56 | },
57 |
58 | optimization: {
59 | minimizer: [
60 | new TerserPlugin(),
61 | new OptimizeCSSAssetsPlugin({})
62 | ],
63 | splitChunks: {
64 | cacheGroups: {
65 | commons: {
66 | test: /[\\/]node_modules[\\/]/,
67 | name: 'vendors',
68 | chunks: 'all'
69 | }
70 | }
71 | }
72 | },
73 |
74 | plugins: [
75 | new CleanWebpackPlugin({
76 | watch: true,
77 | beforeEmit: true
78 | }),
79 | new CopyPlugin([{ from: './site/html/404.html', to: './' }]),
80 | new HtmlWebpackPlugin({
81 | inject: true,
82 | template: './site/html/index.html'
83 | }),
84 | new TerserPlugin(),
85 | new webpack.DefinePlugin({
86 | 'process.env.NODE_ENV': JSON.stringify('production')
87 | }),
88 | new MiniCssExtractPlugin({
89 | filename: '[name].css'
90 | })
91 | ]
92 | };
--------------------------------------------------------------------------------