├── .eslintrc.yaml
├── .gitignore
├── .prettierrc.yaml
├── README-v3.md
├── README.md
├── assets
├── autoshuffle.gif
├── inout.gif
├── inoutpic2.gif
├── logo.gif
├── nav-spring.gif
├── nav.gif
├── sharedlayout.gif
└── simpleshuffle.gif
├── jest.config.js
├── package.json
├── src
├── AnimateInOut.tsx
├── FlipProvider.tsx
├── const.ts
├── createKeyframes.ts
├── easings.ts
├── helpers.ts
├── index.ts
├── keyframes.ts
├── syncLayout.ts
├── useFlip.ts
└── useLayoutEffect.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | parserOptions:
2 | ecmaVersion: 2018
3 | sourceType: module
4 | env:
5 | es6: true
6 | browser: true
7 | node: true
8 | plugins:
9 | - prettier
10 | - react
11 | - react-hooks
12 | extends:
13 | - plugin:prettier/recommended
14 | - eslint:recommended
15 | - plugin:react/recommended
16 | - react-app
17 | rules:
18 | no-console: warn
19 | no-eval: error
20 | prettier/prettier: error
21 | react-hooks/rules-of-hooks: error
22 | react-hooks/exhaustive-deps: warn
23 | react/prop-types: [2, { ignore: ['children'] }]
24 | react/react-in-jsx-scope: off
25 | settings:
26 | react:
27 | version: detect
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules/
5 | /.pnp
6 | .pnp.js
7 |
8 | # VSCode settings and caches
9 | .*
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # package
18 | /lib
19 |
20 | # misc
21 | .DS_Store
22 | .env.local
23 | .env.development.local
24 | .env.test.local
25 | .env.production.local
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | .npmignore
32 |
33 | public/
34 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | trailingComma: none
2 | tabWidth: 2
3 | semi: false
4 | singleQuote: true
5 | arrowParens: always
6 |
--------------------------------------------------------------------------------
/README-v3.md:
--------------------------------------------------------------------------------
1 | # React Easy Flip
2 |
3 | ⚛ A lightweight React library for smooth FLIP animations
4 |
5 | ## Demo
6 |
7 | https://demo.jlkiri.now.sh/
8 |
9 | ## Install
10 |
11 | `npm install react-easy-flip`
12 |
13 | ## Get started
14 |
15 | The library consists of two independent hooks: `useSimpleFlip` and `useFlipGroup`. Use `useFlipGroup` if you need to animate position or size of an indefinite number of children (see examples below). Use `useSimpleFlip` for everything else.
16 |
17 | 1. Import `useSimpleFlip` hook:
18 |
19 | ```javascript
20 | import { useSimpleFlip } from 'react-easy-flip'
21 | ```
22 |
23 | 2. Pick a unique `data-flip-id` and assign it to the element you want to animate
24 | 3. Use the hook by passing it the id and dependencies that you would normally pass to `useEffect` (e.g. an array that is used to render children):
25 |
26 | ```javascript
27 | useSimpleFlip({ flipId: myId, flag: myDeps })
28 | ```
29 |
30 | ## Usage
31 |
32 | ### useSimpleFlip
33 |
34 | `useSimpleAnimation` requires one argument, which is minimally an object with the `id` of your animated element and `boolean` hook [dependencies](https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect). You may optionally provide:
35 |
36 | a) a callback to be executed after an animation is done: `onTransitionEnd`
37 | b) CSS transition options
38 | c) a special flag that tells the hook whether the transition is "shared"
39 |
40 | | Field | Required | Type | Details |
41 | | :---------------: | :------: | :--------: | :-------------------------------------------------------------------------------: |
42 | | `flipId` | `true` | `string` | A React reference to a parent element which contains children you want to animate |
43 | | `flag` | `true` | `boolean` | Hook dependencies |
44 | | `opts` | `false` | `object` | Animation options object |
45 | | `onTransitionEnd` | `false` | `function` | A callback to be executed after an animation is done |
46 | | `isShared` | `false` | `boolean` | A special flag that tells the hook whether the transition is "shared" |
47 |
48 | ### useFlipGroup
49 |
50 | `useFlipGroup` requires one argument, which is minimally an object with the `id` of your animated element and hook [dependencies](https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect). You _must_ also attach a unique `data-id` to every child that you want to animate (see examples below). The `data-id` can be the same as a `key` prop.
51 |
52 | Just like in `useSimpleFlip` you may optionally provide:
53 |
54 | a) a callback to be executed after an animation is done: `onTransitionEnd`
55 | b) CSS transition options
56 |
57 | | Field | Required | Type | Details |
58 | | :---------------: | :------: | :--------: | :-------------------------------------------------------------------------------: |
59 | | `flipId` | `true` | `string` | A React reference to a parent element which contains children you want to animate |
60 | | `deps` | `true` | `any` | Hook dependencies |
61 | | `opts` | `false` | `object` | Animation options object |
62 | | `onTransitionEnd` | `false` | `function` | A callback to be executed after an animation is done |
63 |
64 | ### Options in detail
65 |
66 | You may add an `opts` options object to the argument of `useSimpleFlip` or `useFlipGroup`. It allows you to specify CSS transition duration, easing function and animation delay:
67 |
68 | | Field | Default | Type | Details |
69 | | :----------: | :------: | :------: | :---------------------------------------------: |
70 | | `transition` | `700` | `number` | Transition duration (in milliseconds) |
71 | | `easing` | `"ease"` | `string` | Animation easing function (any valid CSS value) |
72 | | `delay` | `0` | `number` | Animation delay (in milliseconds) |
73 |
74 | Usage example:
75 |
76 | ```javascript
77 | const opts = {
78 | transition: 200,
79 | easing: 'cubic-bezier(0.39, 0.575, 0.565, 1)',
80 | delay: 300
81 | }
82 |
83 | useSimpleFlip({ flipId: 'uniqueId', opts, flag: isClicked })
84 | ```
85 |
86 | ## Comparison with other libraries
87 |
88 | Among similar libraries such as [`react-overdrive`](https://github.com/berzniz/react-overdrive), [`react-flip-move`](https://github.com/joshwcomeau/react-flip-move) or [`react-flip-toolkit`](https://github.com/aholachek/react-flip-toolkit) that are based on a [FLIP technique](https://aerotwist.com/blog/flip-your-animations/), this library's capabilities match those of `react-flip-toolkit`.
89 |
90 | `react-easy-flip` can animate both position and scale, as well as prevent distortion of children of an animated element when its scale is changed. It also allows you to easily do so-called ["shared element transitions"](https://guides.codepath.com/android/shared-element-activity-transition) (e.g. smoothly translate an element from one page to another). The examples are given below.
91 |
92 | Additionally, `react-easy-flip` is the **only** FLIP library for React that provides animation via a hook. `react-easy-flip` has the **smallest bundle size**. It does not use React class components and lifecycle methods that are considered unsafe in latest releases of React.
93 |
94 | ## Examples
95 |
96 | The code for the demo above can be found in this repository [here](https://github.com/jlkiri/react-easy-flip/tree/master/demo).
97 | Below are some simple Codesandbox examples:
98 |
99 | ### Translate and scale (with child scale adjustment)
100 |
101 | This examples shows the difference between the same transition done in CSS and with `react-easy-flip`:
102 |
103 | https://codesandbox.io/s/css-vs-js-child-warping-t7f95
104 |
105 | 
106 |
107 | ### Shuffle children
108 |
109 | This is a good usecase for `useFlipGroup`:
110 |
111 | https://codesandbox.io/s/list-shuffling-flip-hlguz
112 |
113 | 
114 |
115 | ### Shared element transition
116 |
117 | This is an example of so-called shared element transition. Click on any square with the Moon in it. Note that the black background and the Moon are different elements (with different parents) after the click, yet the animation remains smooth despite the DOM unmount. This technique is done fairly easiy with `useSimpleFlip` and can even be used to animate elements across pages in SPA.
118 |
119 | https://codesandbox.io/s/shared-element-transition-flip-9orsy
120 |
121 | 
122 |
123 | ## Tips
124 |
125 | - You may need to think about how you structure your HTML before you try to "customize" animations with your own CSS (it will probably not work)
126 | - `useFlipGroup` is best used with a `transitionend` callback: disable animation-triggering actions until animation is finished to prevent possible layout problems
127 | - It is possible to use more than one instance of each hooks within one React component but make sure to:
128 | a) Keep `id`s unique
129 | b) Avoid making one `id` a target for more than one animation
130 |
131 | ## Requirements
132 |
133 | This library requires React version 16.8.0 or higher (the one with Hooks).
134 |
135 | ## Done in 3.0
136 |
137 | - [x] Full Typescript support
138 | - [x] Add support for animating scale and shared element transitions
139 | - [x] Add comprehensive examples
140 | - [x] Add tests
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 |
5 |
6 |
7 |
8 | # React Easy Flip
9 |
10 | ⚛ A lightweight React library for smooth FLIP animations
11 |
12 | ## Features
13 |
14 | - Animates the unanimatable (DOM positions, mounts/unmounts)
15 |
16 | - One hook for many usecases
17 |
18 | - Uses the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) (WAAPI)
19 |
20 | * Stable and smooth 60fps animations
21 |
22 | - SSR-friendly
23 |
24 | * Built-in easing functions
25 |
26 | - Lightweight
27 |
28 | ## Previous README versions
29 |
30 | This is a README for v4. The v3 README can be found [here](./README-v3.md).
31 |
32 | ## Demo
33 |
34 | https://react-easy-flip-demo.now.sh/
35 |
36 | Repository: [react-easy-flip-demo](https://github.com/jlkiri/react-easy-flip-demo)
37 |
38 | You can also [read about how it works in detail here](https://css-tricks.com/everything-you-need-to-know-about-flip-animations-in-react/).
39 |
40 | ## Install
41 |
42 | `npm install react-easy-flip@beta`
43 |
44 | ## Get started
45 |
46 | 1. Import `useFlip` hook and `FlipProvider`:
47 |
48 | ```javascript
49 | import { useFlip, FlipProvider } from 'react-easy-flip'
50 | ```
51 |
52 | 2. Wrap your app (or at least a component that contains animated children) with a `FlipProvider`
53 |
54 | ```jsx
55 |
56 |
57 |
58 | ```
59 |
60 | 3. Assign a `data-flip-root-id` to any parent of the element(s) you want to animate
61 |
62 | ```jsx
63 |
66 | ```
67 |
68 | 4. Pick a unique `data-flip-id` and assign it to the element(s) you want to animate. It can be the same as a `key` prop
69 |
70 | ```jsx
71 |
72 | ```
73 |
74 | 5. Use the hook by passing it the root ID you picked in (3)
75 |
76 | ```javascript
77 | useFlip(rootId)
78 | ```
79 |
80 | And that's it!
81 |
82 | ## Usage details
83 |
84 | ### useFlip
85 |
86 | `useFlip` requires one argument, which is an ID of the root, i.e. any parent whose children you want to animate. You can optionally pass an options object with animation options (see details below) as a second argument. Third argument is the optional dependencies which you would normally pass to a `useEffect` hook: use it if you need to explicitly tell `react-easy-flip` that items you want to animate changed.
87 |
88 | ```
89 | useFlip(rootId, animationOptions, deps)
90 | ```
91 |
92 | #### Animation optons
93 |
94 | Animation options is an object.
95 |
96 | | Property | Default | Required | Type | Details |
97 | | :------------: | :------------: | :------: | :--------: | :-----------------------------------------------------------: |
98 | | `duration` | 400 | `false` | `number` | Animation duration (ms) |
99 | | `easing` | `easeOutCubic` | `false` | `function` | Easing function (that can be imported from `react-easy-flip`) |
100 | | `delay` | 0 | `false` | `number` | Animation delay |
101 | | `animateColor` | false | `false` | `boolean` | Animate background color of the animated element |
102 |
103 | Example:
104 |
105 | ```javascript
106 | import { easeInOutQuint } from 'react-easy-flip`
107 |
108 | const SomeReactComponent = () => {
109 | const animationOptions = {
110 | duration: 2000,
111 | easing: easeInOutQuint,
112 | }
113 |
114 | useFlip(rootId, animationOptions)
115 |
116 | return (
117 |
120 | )
121 | }
122 | ```
123 |
124 | ### Exported easings
125 |
126 | `react-easy-flip` exports ready-to-use easing functions. You can [see the examples here](https://easings.net/).
127 |
128 | - linear
129 | - easeInSine
130 | - easeOutSine
131 | - easeInOutSine
132 | - easeInCubic
133 | - easeOutCubic
134 | - easeInOutCubic
135 | - easeInQuint
136 | - easeOutQuint
137 | - easeInOutQuint
138 | - easeInBack
139 | - easeOutBack
140 | - easeInOutBack
141 |
142 | ### AnimateInOut (experimental)
143 |
144 | While `useFlip` can animate all kinds of position changes, it does not animate mount/unmount animations (e.g. fade in/out). For this purpose the `` component is also exported. To use it, simple wrap with it the components/elements which you want to be animated. By default the initial render is not animated, but this can be changed with a prop.
145 |
146 | Every element wrapped with a `` **must** have a unique key prop.
147 |
148 | Example:
149 |
150 | ```javascript
151 | import { AnimateInOut } from 'react-easy-flip`
152 |
153 | const SomeReactComponent = () => {
154 | return (
155 |
156 |
157 |
158 |
159 |
160 | )
161 | }
162 | ```
163 |
164 | Here are all props that you can pass to ``:
165 |
166 | | Property | Default | Required | Type | Details |
167 | | :-----------------: | :---------: | :------: | :------------------: | :------------------------------------------------------------: |
168 | | `in` | `fadeIn` | `false` | `AnimationKeyframes` | Mount animation options |
169 | | `out` | `fadeOut` | `false` | `AnimationKeyframes` | Unmount animation options |
170 | | `playOnFirstRender` | `false` | `false` | `boolean` | Animate on first render |
171 | | `itemAmount` | `undefined` | `false` | `number` | An explicit amount of current children (see explanation below) |
172 |
173 | What is `itemAmount` for? In most cases this is not needed. But if your element is animated with a shared layout transition (such as moving from one list to another), this means that it doesn't need an unmount animation. In order to avoid two animations being applied to one element, provide the amount. For example, if this is a todo-app-like application, keep the number of both todo and done items. Moving from todo to done doesn't change the total amount of items, but `` does not know that until you tell it. See the recipes below.
174 |
175 | ## Comparison with other libraries
176 |
177 | - `react-easy-flip` uses Web Animations API (WAAPI) for animations. No other library based on a [FLIP technique](https://aerotwist.com/blog/flip-your-animations/) currently does that.
178 |
179 | - Similar to existing libraries such as [`react-overdrive`](https://github.com/berzniz/react-overdrive), [`react-flip-move`](https://github.com/joshwcomeau/react-flip-move) or [`react-flip-toolkit`](https://github.com/aholachek/react-flip-toolkit) (although only the latter seems to be maintained).
180 |
181 | - Allows you to easily do so-called [shared layout animations](https://guides.codepath.com/android/shared-element-activity-transition) (e.g. smoothly move an element from one page/parent to another). Some examples are given below. This is what heavier libraries like [`framer-motion`](https://github.com/framer/motion) call Magic Motion.
182 |
183 | - Additionally, `react-easy-flip` is the **only** lightweight FLIP library for React that provides animation via a hook. Currently `react-easy-flip` has the **smallest bundle size**. It also does not use React class components and lifecycle methods that are considered unsafe in latest releases of React.
184 |
185 | ## Recipes
186 |
187 | ### List sort/shuffle animation
188 |
189 | [Go to code](https://github.com/jlkiri/react-easy-flip-demo/blob/master/pages/Shuffle.tsx)
190 |
191 |
192 |
193 |
194 |
195 | ### Both x and y coordinate shuffle
196 |
197 | [Go to code](https://github.com/jlkiri/react-easy-flip-demo/blob/master/pages/auto-shuffle.tsx)
198 |
199 |
200 |
201 |
202 |
203 | ### Shared layout animation
204 |
205 | This is an todo-app-like example of shared layout animations. Click on any rectangle to move it to another parent. Note that on every click an item is actually unmounted from DOM and re-mounted in the other position, but having the same `data-flip-id` allows to be smoothly animated from one to another position.
206 |
207 | [Go to code](https://github.com/jlkiri/react-easy-flip-demo/blob/master/pages/shared-layout.tsx)
208 |
209 |
210 |
211 |
212 |
213 | ### Shared layout animation (navigation)
214 |
215 | One nice usecase for shared layout animation is navigation bars where we want to move the highlighting indicator smoothly between tabs.
216 |
217 | [Go to code](https://github.com/jlkiri/react-easy-flip-demo/blob/master/pages/magic-nav.tsx)
218 |
219 |
220 |
221 |
222 |
223 | ### In/out (mount/unmount) animation (opacity)
224 |
225 | The fade in and out keyframes are default and work out of box (= you do not need to explicitly pass them).
226 |
227 | [Go to code](https://github.com/jlkiri/react-easy-flip-demo/blob/master/pages/in-out.tsx)
228 |
229 |
230 |
231 |
232 |
233 | ### In/out (mount/unmount) animation (translation)
234 |
235 | An example of passing custom animation options to ``. Here the images are moved in and out instead of simply fading in and out.
236 |
237 | [Go to code](https://github.com/jlkiri/react-easy-flip-demo/blob/master/pages/in-out-pic.tsx)
238 |
239 |
240 |
241 |
242 |
243 | ## Requirements
244 |
245 | This library requires React version 16.8.0 or higher (the one with Hooks).
246 |
247 | ## Contribution
248 |
249 | Any kind of contribution is welcome!
250 |
251 | 1. Open an issue or pick an existing one that you want to work on
252 | 2. Fork this repository
253 | 3. Clone your fork to work on it locally
254 | 4. Make changes
255 | 5. Run `yarn build:dev` and make sure that it builds without crash
256 |
--------------------------------------------------------------------------------
/assets/autoshuffle.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/autoshuffle.gif
--------------------------------------------------------------------------------
/assets/inout.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/inout.gif
--------------------------------------------------------------------------------
/assets/inoutpic2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/inoutpic2.gif
--------------------------------------------------------------------------------
/assets/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/logo.gif
--------------------------------------------------------------------------------
/assets/nav-spring.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/nav-spring.gif
--------------------------------------------------------------------------------
/assets/nav.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/nav.gif
--------------------------------------------------------------------------------
/assets/sharedlayout.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/sharedlayout.gif
--------------------------------------------------------------------------------
/assets/simpleshuffle.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlkiri/react-easy-flip/10f1f0976e2fe9031718a53029e7fbd26b623b9c/assets/simpleshuffle.gif
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | globals: {
5 | 'ts-jest': {
6 | tsConfig: '/tsconfig.test.json'
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-easy-flip",
3 | "version": "4.0.3",
4 | "description": "A lightweight React library for smooth FLIP animations",
5 | "source": "src/index.ts",
6 | "types": "lib/index.d.ts",
7 | "main": "lib/index.js",
8 | "module": "lib/index.es.js",
9 | "scripts": {
10 | "build": "microbundle -f es,cjs --jsx React.createElement --no-sourcemap",
11 | "build:dev": "microbundle -f es,cjs --jsx React.createElement",
12 | "prepare": "yarn build",
13 | "format": "prettier --write src/**/*.{ts,tsx,js,jsx}",
14 | "lint": "eslint src/ --ext .js,.ts,.tsx,.jsx",
15 | "lint:fix": "eslint --fix src/ --ext .js,.ts,.tsx,.jsx",
16 | "develop": "yarn start"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+ssh://git@github.com/jlkiri/react-easy-flip.git"
21 | },
22 | "keywords": [
23 | "react",
24 | "FLIP",
25 | "animation",
26 | "transition"
27 | ],
28 | "author": "Kirill Vasiltsov",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/jlkiri/react-easy-flip/issues"
32 | },
33 | "homepage": "https://github.com/jlkiri/react-easy-flip#readme",
34 | "peerDependencies": {
35 | "react": ">= 16.8.0",
36 | "react-dom": ">= 16.8.0"
37 | },
38 | "devDependencies": {
39 | "@testing-library/react-hooks": "^3.1.1",
40 | "@types/node": "^12.12.5",
41 | "@types/react": "^16.9.11",
42 | "@types/react-dom": "^16.9.3",
43 | "eslint": "^6.6.0",
44 | "eslint-config-prettier": "^6.4.0",
45 | "eslint-plugin-prettier": "^3.1.1",
46 | "eslint-plugin-react": "^7.16.0",
47 | "eslint-plugin-react-hooks": "^2.2.0",
48 | "microbundle": "^0.12.0",
49 | "prettier": "^1.19.1",
50 | "react": "^16.13.1",
51 | "react-dom": "^16.13.1",
52 | "react-scripts": "3.2.0",
53 | "typescript": "^3.9.7"
54 | },
55 | "files": [
56 | "lib/**/*"
57 | ],
58 | "browserslist": {
59 | "production": [
60 | "> 1%",
61 | "last 2 versions",
62 | "not dead"
63 | ],
64 | "development": [
65 | "last 1 chrome version",
66 | "last 1 firefox version",
67 | "last 1 safari version"
68 | ]
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/AnimateInOut.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FlipContext } from './FlipProvider'
3 | import { isRunning, getComputedBgColor, getRect } from './helpers'
4 | import { fadeIn, fadeOut } from './keyframes'
5 |
6 | export { fadeIn, fadeOut }
7 | export { AnimateInOut }
8 |
9 | interface CustomKeyframe {
10 | [property: string]: string | number
11 | }
12 |
13 | interface AnimationKeyframes {
14 | from: CustomKeyframe
15 | to: CustomKeyframe
16 | duration: number
17 | easing?: string
18 | }
19 |
20 | interface AnimateInOutProps {
21 | children: React.ReactNode
22 | in?: AnimationKeyframes
23 | out?: AnimationKeyframes
24 | itemAmount?: number
25 | playOnFirstRender?: boolean
26 | }
27 |
28 | interface InOutChildProps {
29 | children: React.ReactElement
30 | childProps: React.ReactElement['props']
31 | callback?: () => void
32 | keyframes: { in: AnimationKeyframes; out: AnimationKeyframes }
33 | preventAnimation?: boolean
34 | isCached?: boolean
35 | isExiting: boolean
36 | }
37 |
38 | const getChildKey = (child: React.ReactElement) => {
39 | return `${child.key}` || ''
40 | }
41 |
42 | const onlyValidElements = (children: React.ReactNode) => {
43 | const filtered: React.ReactElement[] = []
44 |
45 | React.Children.forEach(children, (child) => {
46 | if (React.isValidElement(child)) {
47 | filtered.push(child)
48 | }
49 | })
50 |
51 | return filtered
52 | }
53 |
54 | const InOutChild = (props: InOutChildProps) => {
55 | const ref = React.useRef(null)
56 | const hasRendered = React.useRef(false)
57 | const localCachedAnimation = React.useRef(null)
58 | const { cachedStyles } = React.useContext(FlipContext)
59 |
60 | React.useLayoutEffect(() => {
61 | // Skip animations on non-relevant renders (neither exiting nor appearing)
62 | if (props.preventAnimation) {
63 | hasRendered.current = true
64 | return
65 | }
66 |
67 | if (hasRendered.current && !props.isExiting) return
68 |
69 | if (!ref.current) return
70 |
71 | if (!props.isExiting && props.isCached) return // TODO: is this condition needed?
72 |
73 | const cachedAnimation = localCachedAnimation.current
74 |
75 | // If currently playing exiting animation keep playing
76 | if (cachedAnimation && isRunning(cachedAnimation)) return
77 |
78 | const { in: inKfs, out } = props.keyframes
79 | const keyframes = props.isExiting ? out : inKfs
80 |
81 | const kfe = new KeyframeEffect(
82 | ref.current,
83 | [keyframes.from, keyframes.to],
84 | {
85 | duration: keyframes.duration,
86 | easing: keyframes.easing || 'ease',
87 | fill: 'both'
88 | }
89 | )
90 |
91 | const animation = new Animation(kfe, document.timeline)
92 |
93 | const flipId = props.children.props['data-flip-id']
94 |
95 | // Delete from common cache on exit
96 | if (props.isExiting) {
97 | animation.onfinish = () => {
98 | cachedStyles.delete(flipId)
99 | props.callback && props.callback()
100 | }
101 | }
102 |
103 | // Set position only after entering animation is finished. If not
104 | // it may have 0 width and height and cause scale problems
105 | if (!props.isExiting) {
106 | animation.onfinish = () => {
107 | cachedStyles.set(flipId, {
108 | styles: {
109 | bgColor: getComputedBgColor(ref.current!)
110 | },
111 | rect: getRect(ref.current!)
112 | })
113 | }
114 | }
115 |
116 | animation.play()
117 |
118 | localCachedAnimation.current = animation
119 |
120 | hasRendered.current = true
121 | }, [props, cachedStyles])
122 |
123 | // Prevent interactions with exiting elements (treat as non-existent)
124 | const style = { ...(props.children.props.style || {}), pointerEvents: 'none' }
125 |
126 | return props.isExiting
127 | ? React.cloneElement(props.children, {
128 | ...props.children.props,
129 | style,
130 | 'data-flip-id': undefined, // Prevent trigger of shared layout animations on exiting elements
131 | ref
132 | })
133 | : React.cloneElement(props.children, { ...props.children.props, ref })
134 | }
135 |
136 | const AnimateInOut = ({
137 | children,
138 | in: inKeyframes = fadeIn,
139 | out: outKeyframes = fadeOut,
140 | playOnFirstRender = false,
141 | itemAmount = undefined
142 | }: AnimateInOutProps): any => {
143 | const { forceRender, childKeyCache } = React.useContext(FlipContext)
144 | const exiting = React.useRef(new Set()).current
145 | const previousAmount = React.useRef(itemAmount)
146 | const initialRender = React.useRef(true)
147 |
148 | // TODO: If there is a playing flip animation during an entering animation
149 | // the behaviour of the flipped element is weird.
150 |
151 | const kfs = { in: inKeyframes, out: outKeyframes }
152 |
153 | // Use an optional explicit hint to know when an element truly is removed
154 | // and not moved to other position in DOM (shared layout transition)
155 | const amountChanged =
156 | itemAmount === undefined || itemAmount !== previousAmount.current
157 |
158 | previousAmount.current = itemAmount
159 |
160 | const filteredChildren = onlyValidElements(children)
161 |
162 | const presentChildren = React.useRef(filteredChildren)
163 |
164 | React.useEffect(() => {
165 | React.Children.forEach(filteredChildren, (child) => {
166 | childKeyCache.set(getChildKey(child), child)
167 | })
168 | })
169 |
170 | // On initial render just wrap everything with InOutChild
171 | if (initialRender.current) {
172 | initialRender.current = false
173 | return filteredChildren.map((child) => (
174 |
182 | {child}
183 |
184 | ))
185 | }
186 |
187 | // If render is caused by shared layout animation do not play exit animations
188 | // but keep those already playing
189 | if (!amountChanged) {
190 | if (exiting.size !== 0) {
191 | return presentChildren.current
192 | }
193 |
194 | let renderedChildren = filteredChildren.map((child) => {
195 | if (exiting.has(getChildKey(child))) {
196 | return child
197 | }
198 |
199 | return (
200 |
207 | {child}
208 |
209 | )
210 | })
211 |
212 | presentChildren.current = renderedChildren
213 |
214 | return renderedChildren
215 | }
216 |
217 | const presentKeys = presentChildren.current.map(getChildKey)
218 | const targetKeys = filteredChildren.map(getChildKey)
219 |
220 | const removeFromDOM = (key: string) => {
221 | // Avoid bugs when callback is called twice (i.e. not exiting anymore)
222 | if (!exiting.has(key)) return
223 |
224 | childKeyCache.delete(key)
225 | exiting.delete(key)
226 |
227 | // Do not force render if multiple exit animations are playing
228 | const removeIndex = presentChildren.current.findIndex(
229 | (child) => child.key === key
230 | )
231 |
232 | presentChildren.current.splice(removeIndex, 1)
233 |
234 | // Only force render when the last exit animation is finished
235 | if (exiting.size === 0) {
236 | presentChildren.current = filteredChildren
237 | forceRender()
238 | }
239 | }
240 |
241 | for (const key of presentKeys) {
242 | if (!targetKeys.includes(key)) {
243 | exiting.add(key)
244 | } else {
245 | // In case this key has re-entered, remove from the exiting list
246 | exiting.delete(key)
247 | }
248 | }
249 |
250 | let renderedChildren = [...filteredChildren]
251 |
252 | exiting.forEach((key) => {
253 | // If this component is actually entering again, early return.
254 | // Copied from framer-motion. Not sure what the usecase is but just in case.
255 | if (targetKeys.indexOf(key) !== -1) return
256 |
257 | const child = childKeyCache.get(key)
258 |
259 | if (!child) return
260 |
261 | // This is an animation onfinish callback
262 | const removeFromCache = () => removeFromDOM(key)
263 |
264 | const index = presentKeys.indexOf(key)
265 |
266 | renderedChildren.splice(
267 | index,
268 | 0,
269 |
277 | {child}
278 |
279 | )
280 | })
281 |
282 | // Wrap children in InOutChild except those that are exiting
283 | renderedChildren = renderedChildren.map((child) => {
284 | const childKey = getChildKey(child)
285 | if (exiting.has(childKey)) {
286 | return child
287 | }
288 |
289 | return (
290 |
297 | {child}
298 |
299 | )
300 | })
301 |
302 | presentChildren.current = renderedChildren
303 |
304 | return renderedChildren
305 | }
306 |
--------------------------------------------------------------------------------
/src/FlipProvider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { isPaused, isRunning } from './helpers'
3 |
4 | export type Rect = DOMRect | ClientRect
5 |
6 | export type CachedStyles = Map<
7 | string,
8 | { styles: { bgColor: string }; rect: Rect }
9 | >
10 | export type Animations = Map
11 | export type ChildKeyCache = Map
12 |
13 | interface FlipContext {
14 | forceRender: () => void
15 | pauseAll: () => void
16 | resumeAll: () => void
17 | cachedAnimations: Animations
18 | cachedStyles: CachedStyles
19 | childKeyCache: ChildKeyCache
20 | }
21 |
22 | export const FlipContext = React.createContext({
23 | forceRender: () => {},
24 | pauseAll: () => {},
25 | resumeAll: () => {},
26 | cachedAnimations: new Map(),
27 | cachedStyles: new Map(),
28 | childKeyCache: new Map()
29 | })
30 |
31 | export const FlipProvider = ({ children }: { children: React.ReactNode }) => {
32 | const [forcedRenders, setForcedRenders] = React.useState(0)
33 | const cachedAnimations = React.useRef(new Map()).current
34 | const cachedStyles = React.useRef(new Map()).current
35 | const childKeyCache = React.useRef(new Map()).current
36 |
37 | const ctx = React.useMemo(() => {
38 | return {
39 | forceRender: () => {
40 | setForcedRenders(forcedRenders + 1)
41 | },
42 | pauseAll: () => {
43 | for (const animation of cachedAnimations.values()) {
44 | if (isRunning(animation)) {
45 | animation.pause()
46 | }
47 | }
48 | },
49 | resumeAll: () => {
50 | for (const animation of cachedAnimations.values()) {
51 | if (isPaused(animation)) {
52 | animation.play()
53 | }
54 | }
55 | },
56 | cachedAnimations,
57 | cachedStyles,
58 | childKeyCache
59 | }
60 | }, [forcedRenders, childKeyCache, cachedStyles, cachedAnimations])
61 |
62 | return {children}
63 | }
64 |
--------------------------------------------------------------------------------
/src/const.ts:
--------------------------------------------------------------------------------
1 | import { easeOutCubic } from './easings'
2 |
3 | export const DEFAULT_DURATION = 400
4 | export const DEFAULT_DELAY = 0
5 | export const DEFAULT_EASING = easeOutCubic
6 |
--------------------------------------------------------------------------------
/src/createKeyframes.ts:
--------------------------------------------------------------------------------
1 | // See https://developers.google.com/web/updates/2017/03/performant-expand-and-collapse
2 |
3 | type CreateKeyframes = {
4 | sx: number
5 | sy: number
6 | dx?: number
7 | dy?: number
8 | easeFn: (x: number) => number
9 | calculateInverse?: boolean
10 | }
11 |
12 | let cachedEasings = new Map()
13 | let cachedLoops = new Map()
14 |
15 | export const createKeyframes = ({
16 | sx = 1,
17 | sy = 1,
18 | dx = 0,
19 | dy = 0,
20 | easeFn,
21 | calculateInverse = false
22 | }: CreateKeyframes) => {
23 | const cacheKey = `${Math.round(sx)}${Math.round(sy)}${Math.round(
24 | dx
25 | )}${Math.round(dy)}`
26 | const cachedLoop = cachedLoops.get(cacheKey)
27 |
28 | if (cachedLoop) return cachedLoop
29 |
30 | // Figure out the size of the element when collapsed.
31 | let animations = []
32 | let inverseAnimations = []
33 |
34 | // Increase by 5, since by 1 works very poor in Firefox (but not in Chromium)
35 | for (let step = 0; step <= 100; step = step + 5) {
36 | // Remap the step value to an eased one.
37 | const nStep = step / 100
38 | const cachedV = cachedEasings.get(nStep)
39 | const easedStep = cachedV ? cachedV : easeFn(nStep)
40 |
41 | !cachedV && cachedEasings.set(nStep, easedStep)
42 |
43 | // Calculate the scale of the element.
44 | // easedStep grows from 0 to 1 according to the easing function.
45 | // To prevent changes when the scale value is 1 we substract it from 1 so that the multiplier is always 0
46 | const scaleX = sx + (1 - sx) * easedStep
47 | const scaleY = sy + (1 - sy) * easedStep
48 | const translateX = dx - dx * easedStep
49 | const translateY = dy - dy * easedStep
50 |
51 | animations.push({
52 | transform: `scale(${scaleX}, ${scaleY}) translate(${translateX}px, ${translateY}px)`
53 | })
54 |
55 | if (calculateInverse) {
56 | // And now the inverse for the contents.
57 | const invXScale = 1 / scaleX
58 | const invYScale = 1 / scaleY
59 |
60 | // TODO: Counter-translations?
61 | inverseAnimations.push({
62 | transform: `scale(${invXScale}, ${invYScale}) translate(0px, 0px)`
63 | })
64 | }
65 | }
66 |
67 | cachedLoops.set(cacheKey, { animations, inverseAnimations })
68 |
69 | return {
70 | animations,
71 | inverseAnimations
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/easings.ts:
--------------------------------------------------------------------------------
1 | // https://easings.net/
2 |
3 | export function linear(x: number) {
4 | return x
5 | }
6 |
7 | export function easeInSine(x: number): number {
8 | return 1 - Math.cos((x * Math.PI) / 2)
9 | }
10 |
11 | export function easeOutSine(x: number): number {
12 | return Math.sin((x * Math.PI) / 2)
13 | }
14 |
15 | export function easeInOutSine(x: number): number {
16 | return -(Math.cos(Math.PI * x) - 1) / 2
17 | }
18 |
19 | export function easeInCubic(x: number): number {
20 | return x * x * x
21 | }
22 |
23 | export function easeOutCubic(x: number): number {
24 | return 1 - Math.pow(1 - x, 3)
25 | }
26 |
27 | export function easeInOutCubic(x: number): number {
28 | return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2
29 | }
30 |
31 | export function easeInQuint(x: number): number {
32 | return x * x * x * x * x
33 | }
34 |
35 | export function easeOutQuint(x: number): number {
36 | return 1 - Math.pow(1 - x, 5)
37 | }
38 |
39 | export function easeInOutQuint(x: number): number {
40 | return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2
41 | }
42 |
43 | export function easeInBack(x: number): number {
44 | const c1 = 1.70158
45 | const c3 = c1 + 1
46 | return c3 * x * x * x - c1 * x * x
47 | }
48 |
49 | export function easeOutBack(x: number): number {
50 | const c1 = 1.70158
51 | const c3 = c1 + 1
52 | return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2)
53 | }
54 |
55 | export function easeInOutBack(x: number): number {
56 | const c1 = 1.70158
57 | const c2 = c1 * 1.525
58 |
59 | return x < 0.5
60 | ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
61 | : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2
62 | }
63 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { FlipID, FlipHtmlElement } from './useFlip'
2 |
3 | export interface FlipKeyframeEffectOptions extends KeyframeEffectOptions {
4 | staggerStep?: number
5 | stagger?: number
6 | }
7 |
8 | export function debounce any>(
9 | cb: F,
10 | wait: number
11 | ) {
12 | let timer: any
13 | return function _debounce(...args: Parameters) {
14 | clearTimeout(timer)
15 | timer = setTimeout(() => cb(...args), wait)
16 | }
17 | }
18 |
19 | export const isRunning = (animation: Animation) =>
20 | animation.playState === 'running'
21 |
22 | export const isPaused = (animation: Animation) =>
23 | animation.playState === 'paused'
24 |
25 | export const not = (bool: boolean) => !bool
26 | export const emptyMap = (map: Map) => map.size === 0
27 |
28 | export const getRect = (element: Element) => element.getBoundingClientRect()
29 |
30 | export const getFlipId = (el: Element & FlipHtmlElement) => el.dataset.flipId
31 |
32 | export const getComputedBgColor = (element: Element) =>
33 | getComputedStyle(element).getPropertyValue('background-color')
34 |
35 | export const getElementByFlipId = (flipId: FlipID) =>
36 | document.querySelector(`[data-flip-id=${flipId}]`) as FlipHtmlElement
37 |
38 | export const getElementsByRootId = (rootId: string) =>
39 | document.querySelectorAll(`[data-flip-root-id=${rootId}]`)
40 |
41 | export const getTranslateX = (cachedRect: DOMRect, nextRect: DOMRect) =>
42 | cachedRect.x - nextRect.x
43 |
44 | export const getTranslateY = (cachedRect: DOMRect, nextRect: DOMRect) =>
45 | cachedRect.y - nextRect.y
46 |
47 | export const getScaleX = (
48 | cachedRect: DOMRect | ClientRect,
49 | nextRect: DOMRect | ClientRect
50 | ) => cachedRect.width / Math.max(nextRect.width, 0.001)
51 |
52 | export const getScaleY = (
53 | cachedRect: DOMRect | ClientRect,
54 | nextRect: DOMRect | ClientRect
55 | ) => cachedRect.height / Math.max(nextRect.height, 0.001)
56 |
57 | export const createAnimation = (
58 | element: FlipHtmlElement,
59 | keyframes: Keyframe[],
60 | options: FlipKeyframeEffectOptions
61 | ) => {
62 | const { duration, delay = 0, stagger = 0, staggerStep = 0 } = options
63 | const effect = new KeyframeEffect(element, keyframes, {
64 | duration,
65 | easing: 'linear',
66 | delay: delay + stagger * staggerStep,
67 | fill: 'both'
68 | })
69 |
70 | // TODO: figure out what to do when position must be updated after animation
71 | // e.g. class has actually changed
72 | return new Animation(effect, document.timeline)
73 | }
74 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { useFlip, FlipContext, FlipProvider } from './useFlip'
2 | export { AnimateInOut } from './AnimateInOut'
3 | export * from './easings'
4 |
--------------------------------------------------------------------------------
/src/keyframes.ts:
--------------------------------------------------------------------------------
1 | export const fadeOut = {
2 | from: { opacity: 1, transform: 'scale(1,1)' },
3 | to: { opacity: 0, transform: 'scale(0,0)' },
4 | duration: 500,
5 | easing: 'ease-out'
6 | }
7 |
8 | export const fadeIn = {
9 | from: { opacity: 0, transform: 'scale(0,0)' },
10 | to: { opacity: 1, transform: 'scale(1,1)' },
11 | duration: 500,
12 | easing: 'ease-out'
13 | }
14 |
--------------------------------------------------------------------------------
/src/syncLayout.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from './useLayoutEffect'
2 |
3 | type Callback = () => void
4 |
5 | type LayoutStep = 'prewrite' | 'read' | 'render'
6 |
7 | type CallbackLists = {
8 | prewrite: Callback[]
9 | read: Callback[]
10 | render: Callback[]
11 | }
12 |
13 | const jobs: CallbackLists = {
14 | prewrite: [],
15 | read: [],
16 | render: []
17 | }
18 |
19 | const flushCallbackList = (jobs: Callback[]) => {
20 | for (const job of jobs) {
21 | job()
22 | }
23 |
24 | jobs.length = 0
25 | }
26 |
27 | const flushAllJobs = () => {
28 | flushCallbackList(jobs.prewrite)
29 | flushCallbackList(jobs.read)
30 | flushCallbackList(jobs.render)
31 | }
32 |
33 | const registerSyncCallback = (stepName: LayoutStep) => (
34 | callback?: Callback
35 | ) => {
36 | if (!callback) return
37 |
38 | jobs[stepName].push(callback)
39 | }
40 |
41 | export const syncLayout = {
42 | prewrite: registerSyncCallback('prewrite'),
43 | read: registerSyncCallback('read'),
44 | render: registerSyncCallback('render'),
45 | flush: flushAllJobs,
46 | jobLength: () => [jobs.prewrite.length, jobs.read.length, jobs.render.length]
47 | }
48 |
49 | export function useSyncLayout() {
50 | return useLayoutEffect(flushAllJobs)
51 | }
52 |
--------------------------------------------------------------------------------
/src/useFlip.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { useLayoutEffect } from './useLayoutEffect'
3 | import { FlipProvider, FlipContext } from './FlipProvider'
4 | import {
5 | isRunning,
6 | getElementByFlipId,
7 | emptyMap,
8 | not,
9 | getElementsByRootId,
10 | getComputedBgColor,
11 | getTranslateY,
12 | getTranslateX,
13 | getScaleX,
14 | getScaleY,
15 | getRect,
16 | createAnimation
17 | } from './helpers'
18 | import { DEFAULT_DURATION, DEFAULT_DELAY, DEFAULT_EASING } from './const'
19 | import { createKeyframes } from './createKeyframes'
20 | import { syncLayout, useSyncLayout } from './syncLayout'
21 |
22 | export { FlipProvider, FlipContext }
23 |
24 | export type FlipID = string
25 |
26 | export interface AnimationOptions {
27 | duration?: number
28 | easing?: (x: number) => number
29 | delay?: number
30 | animateColor?: boolean
31 | }
32 |
33 | export interface FlipHtmlElement extends Element {
34 | dataset: {
35 | flipId: FlipID
36 | }
37 | }
38 |
39 | type Transforms = Map<
40 | FlipID,
41 | {
42 | elm: FlipHtmlElement
43 | kfs: any
44 | }
45 | >
46 |
47 | export const useFlip = (
48 | rootId: string,
49 | options: AnimationOptions = {},
50 | deps?: any
51 | ) => {
52 | const {
53 | cachedAnimations,
54 | cachedStyles,
55 | pauseAll,
56 | resumeAll
57 | } = React.useContext(FlipContext)
58 | const transforms = React.useRef(new Map()).current
59 |
60 | const {
61 | delay = DEFAULT_DELAY,
62 | duration = DEFAULT_DURATION,
63 | easing = DEFAULT_EASING,
64 | animateColor = false
65 | } = options
66 |
67 | // If render happened during animation, do not wait for useLayoutEffect
68 | // and finish all animations, but cache their midflight position for next animation.
69 | // getBoundingClientRect will return correct values only here and not in useLayoutEffect!
70 | for (const flipId of cachedStyles.keys()) {
71 | const element = getElementByFlipId(flipId)
72 |
73 | if (not(emptyMap(cachedAnimations)) && element) {
74 | const cachedAnimation = cachedAnimations.get(flipId)
75 |
76 | if (cachedAnimation && isRunning(cachedAnimation)) {
77 | const v = cachedStyles.get(flipId)
78 | if (v) {
79 | syncLayout.prewrite(() => {
80 | cachedStyles.set(flipId, {
81 | rect: getRect(element),
82 | styles: {
83 | bgColor: getComputedBgColor(getElementByFlipId(flipId))
84 | }
85 | })
86 | })
87 | syncLayout.render(() => {
88 | cachedAnimation.finish()
89 | })
90 | }
91 | }
92 | }
93 | }
94 |
95 | syncLayout.flush()
96 |
97 | React.useEffect(() => {
98 | // Cache element positions on initial render for subsequent calculations
99 | for (const root of getElementsByRootId(rootId)) {
100 | // Select all root children that are supposed to be animated
101 | const flippableElements = root.querySelectorAll(`[data-flip-id]`)
102 |
103 | for (const element of flippableElements) {
104 | const { flipId } = (element as FlipHtmlElement).dataset
105 |
106 | cachedStyles.set(flipId, {
107 | styles: {
108 | bgColor: getComputedBgColor(element)
109 | },
110 | rect: getRect(element)
111 | })
112 | }
113 | }
114 | }, [rootId, deps, cachedStyles])
115 |
116 | useLayoutEffect(() => {
117 | // Do not do anything on initial render
118 | if (emptyMap(cachedStyles)) return
119 |
120 | const cachedStyleEntries = cachedStyles.entries()
121 |
122 | for (const [flipId, value] of cachedStyleEntries) {
123 | const { rect: cachedRect, styles } = value
124 |
125 | // Select by data-flip-id which makes it possible to animate the element
126 | // that re-mounted in some other DOM location (i.e. shared layout transition)
127 | const flipElement = getElementByFlipId(flipId)
128 |
129 | if (flipElement) {
130 | syncLayout.read(() => {
131 | const nextRect = getRect(flipElement)
132 |
133 | const translateY = getTranslateY(
134 | cachedRect as DOMRect,
135 | nextRect as DOMRect
136 | )
137 | const translateX = getTranslateX(
138 | cachedRect as DOMRect,
139 | nextRect as DOMRect
140 | )
141 | const scaleX = getScaleX(cachedRect, nextRect)
142 | const scaleY = getScaleY(cachedRect, nextRect)
143 |
144 | // Update the cached position
145 | cachedStyles.get(flipId)!.rect = nextRect
146 |
147 | const nextColor = getComputedBgColor(flipElement)
148 |
149 | // Do not animate if there is no need to
150 | if (
151 | translateX === 0 &&
152 | translateY === 0 &&
153 | scaleX === 1 &&
154 | scaleY === 1
155 | ) {
156 | return
157 | }
158 |
159 | const kfs = createKeyframes({
160 | sx: scaleX,
161 | sy: scaleY,
162 | dx: translateX,
163 | dy: translateY,
164 | easeFn: easing,
165 | calculateInverse: true
166 | })
167 |
168 | if (animateColor) {
169 | kfs.animations[0].background = styles.bgColor
170 | kfs.animations[20].background = nextColor
171 | }
172 |
173 | transforms.set(flipId, {
174 | elm: flipElement,
175 | kfs: kfs.animations
176 | })
177 |
178 | // Cache the color value
179 | styles.bgColor = nextColor
180 | })
181 | }
182 | }
183 |
184 | const animationOptions = {
185 | duration,
186 | easing: 'linear',
187 | delay: delay,
188 | fill: 'both' as 'both'
189 | }
190 |
191 | for (const flipId of cachedStyles.keys()) {
192 | syncLayout.render(() => {
193 | const transform = transforms.get(flipId)
194 |
195 | if (!transform) return
196 |
197 | const animation = createAnimation(
198 | transform.elm,
199 | transform.kfs,
200 | animationOptions
201 | )
202 |
203 | cachedAnimations.set(flipId, animation)
204 | transforms.delete(flipId)
205 |
206 | animation.play()
207 | })
208 | }
209 | })
210 |
211 | useSyncLayout()
212 |
213 | return { pause: pauseAll, resume: resumeAll }
214 | }
215 |
--------------------------------------------------------------------------------
/src/useLayoutEffect.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | declare const window: any
4 |
5 | export const useLayoutEffect =
6 | typeof window !== 'undefined' &&
7 | window.document &&
8 | window.document.createElement
9 | ? React.useLayoutEffect
10 | : React.useEffect
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react",
16 | "lib": ["dom.iterable", "es5", "es6", "esnext", "dom"],
17 | "downlevelIteration": true
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------