├── .gitignore
├── .npmignore
├── .prettierrc
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── example
├── .gitignore
├── .storybook
│ ├── main.ts
│ └── preview.ts
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── index.css
│ ├── index.tsx
│ └── stories
│ │ ├── core
│ │ ├── useAnimatedList
│ │ │ ├── useAnimatedList.stories.ts
│ │ │ └── useAnimatedList.tsx
│ │ ├── useMount
│ │ │ ├── UseMount.stories.ts
│ │ │ └── useMount.tsx
│ │ └── useValue
│ │ │ ├── DynamicAnimation.stories.ts
│ │ │ ├── DynamicAnimation.tsx
│ │ │ ├── Loop.stories.ts
│ │ │ ├── Loop.tsx
│ │ │ ├── SequenceTransition.stories.ts
│ │ │ ├── SequenceTransition.tsx
│ │ │ ├── UseValue.stories.ts
│ │ │ └── useValue.tsx
│ │ ├── demo
│ │ ├── Modal.stories.ts
│ │ ├── Modal.tsx
│ │ ├── SharedElement.stories.ts
│ │ ├── SharedElement.tsx
│ │ ├── Sorting.stories.ts
│ │ ├── Sorting.tsx
│ │ ├── Toast.stories.ts
│ │ └── Toast.tsx
│ │ ├── examples
│ │ ├── Interpolation.stories.ts
│ │ ├── Interpolation.tsx
│ │ ├── Ripple.stories.ts
│ │ ├── Ripple.tsx
│ │ ├── SnapTo.stories.ts
│ │ ├── SnapTo.tsx
│ │ ├── Stagger.stories.ts
│ │ ├── Stagger.tsx
│ │ ├── SvgLine.stories.ts
│ │ └── SvgLine.tsx
│ │ └── gestures
│ │ ├── Decay.stories.ts
│ │ ├── Decay.tsx
│ │ ├── Draggable.stories.ts
│ │ ├── Draggable.tsx
│ │ ├── Gestures.stories.ts
│ │ ├── Gestures.tsx
│ │ ├── MouseMove.stories.ts
│ │ ├── MouseMove.tsx
│ │ ├── Scroll.stories.ts
│ │ ├── Scroll.tsx
│ │ ├── Wheel.stories.ts
│ │ └── Wheel.tsx
└── tsconfig.json
├── package-lock.json
├── package.json
├── rollup.config.mjs
├── src
├── animation
│ ├── AnimationConfig.ts
│ ├── Value.ts
│ ├── controllers.ts
│ ├── hooks
│ │ ├── index.ts
│ │ ├── useAnimatedList.ts
│ │ ├── useMount.ts
│ │ └── useValue.ts
│ ├── index.ts
│ ├── interpolation
│ │ ├── colors.ts
│ │ ├── index.ts
│ │ └── interpolateNumbers.ts
│ └── types.ts
├── gestures
│ ├── controllers
│ │ ├── DragGesture.ts
│ │ ├── Gesture.ts
│ │ ├── MouseMoveGesture.ts
│ │ ├── ScrollGesture.ts
│ │ ├── WheelGesture.ts
│ │ └── index.ts
│ ├── helpers
│ │ ├── eventAttacher.ts
│ │ ├── index.ts
│ │ ├── math.ts
│ │ └── withDefault.ts
│ ├── hooks
│ │ ├── index.ts
│ │ ├── useDrag.ts
│ │ ├── useGesture.ts
│ │ ├── useMouseMove.ts
│ │ ├── useRecognizer.ts
│ │ ├── useScroll.ts
│ │ └── useWheel.ts
│ └── types
│ │ └── index.ts
├── hooks
│ ├── index.ts
│ ├── useMeasure.ts
│ ├── useOutsideClick.ts
│ └── useWindowDimension.ts
└── index.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # IntelliJ IDEA files
107 | .idea/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | example/
3 | .git/
4 | .github/
5 | .vscode/
6 | *.log
7 | .gitignore
8 | .npmignore
9 | /src
10 | package-lock.json
11 | tsconfig.json
12 | *.mjs
13 | .prettierrc
14 | CHANGELOG.md
15 | .idea/
16 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "bracketSpacing": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "compile-hero.disable-compile-files-on-did-save-code": false
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [3.3.0] - 2024-07-15
6 |
7 | ### Added
8 |
9 | - New `withConfig` function added for passing default configuration.
10 | - Decay animation implemented with configurations `velocity`, `deceleration` and `decay`.
11 |
12 | ```js
13 | const animation = useValue(0, { decay: true, velocity: 1 });
14 | ```
15 |
16 | ### Changed
17 |
18 | - Refactored `withSpring`, `withTiming` and `withEase` to use `withConfig` internally.
19 | - Updated `makeFluid` to `makeAnimated` and `fluid` to `animate`.
20 |
21 | ### Fixed
22 |
23 | - Gestures hooks issue with not showing the suggestions on callback arguments fixed.
24 |
25 | ## [3.3.1] - 2024-07-20
26 |
27 | ### Fixed
28 |
29 | - Gestures hooks doesn't get applied on the re-mounted elements (After the mount).
30 | - Animation values not animating when applied on the re-mounted elements.
31 |
32 | ## [4.0.0]
33 |
34 | ### Removed
35 |
36 | - Removed `AnimatedBlock`, `AnimatedInline` and `AnimatedImage` HOCs.
37 |
38 | ### Added
39 |
40 | - `useValues` can accept array of numbers
41 |
42 | ```jsx
43 | const animations = useValues([0, 100, 200]);
44 | ```
45 |
46 | And it can be updated like
47 |
48 | ```jsx
49 | animations.value = [200, 400, 500];
50 | ```
51 |
52 | Or it also can be used with animation modifiers
53 |
54 | ```jsx
55 | animations.value = [
56 | withSpring(200),
57 | withTiming(400),
58 | withConfig(500, AnimationConfigUtils.BOUNCE),
59 | ];
60 | ```
61 |
62 | ### Fixed
63 |
64 | - with\* function config overrides bug fixed.
65 | - `useOutsideClick` hook immediate callback fire issue with modal fixed.
66 |
67 | ## [4.1.1] - 2024-09-13
68 |
69 | ### Fixed
70 |
71 | - `spring` animation with delay value crashes the page bug fixed (Re-motion).
72 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Dipesh Rai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React UI Animate
2 |
3 | [](https://badge.fury.io/js/react-ui-animate)
4 |
5 | > Create smooth animations and interactive gestures in React applications effortlessly.
6 |
7 | ### Install
8 |
9 | You can install `react-ui-animate` via `npm` or `yarn`:
10 |
11 | ```sh
12 | npm install react-ui-animate
13 | ```
14 |
15 | ```sh
16 | yarn add react-ui-animate
17 | ```
18 |
19 | ---
20 |
21 | ## Getting Started
22 |
23 | The `react-ui-animate` library provides a straightforward way to add animations and gestures to your React components. Below are some common use cases.
24 |
25 | ### 1. useValue
26 |
27 | Use `useValue` to initialize and update an animated value.
28 |
29 | ```tsx
30 | import React from 'react';
31 | import {
32 | animate,
33 | useValue,
34 | withSpring,
35 | withTiming,
36 | withSequence,
37 | } from 'react-ui-animate';
38 |
39 | export const UseValue: React.FC = () => {
40 | const [width, setWidth] = useValue(100);
41 |
42 | return (
43 | <>
44 |
51 |
58 |
65 |
66 |
75 | >
76 | );
77 | };
78 | ```
79 |
80 | ### 2. useMount
81 |
82 | Use `useMount` to animate component mount and unmount transitions.
83 |
84 | ```tsx
85 | import React from 'react';
86 | import {
87 | animate,
88 | useMount,
89 | withDecay,
90 | withSequence,
91 | withSpring,
92 | withTiming,
93 | } from 'react-ui-animate';
94 |
95 | export const UseMount: React.FC = () => {
96 | const [open, setOpen] = React.useState(true);
97 | const mounted = useMount(open, { from: 0, enter: 1, exit: 0 });
98 |
99 | return (
100 | <>
101 | {mounted(
102 | (animation, isMounted) =>
103 | isMounted && (
104 |
112 | )
113 | )}
114 |
115 |
116 | >
117 | );
118 | };
119 | ```
120 |
121 | ### 3. Interpolation
122 |
123 | Interpolate values for complex mappings like color transitions or movement.
124 |
125 | ```tsx
126 | import React, { useLayoutEffect, useState } from 'react';
127 | import { animate, useValue, withSpring } from 'react-ui-animate';
128 |
129 | export const Interpolation: React.FC = () => {
130 | const [open, setOpen] = useState(false);
131 | const [x, setX] = useValue(0);
132 |
133 | useLayoutEffect(() => {
134 | setX(withSpring(open ? 500 : 0));
135 | }, [open, setX]);
136 |
137 | return (
138 | <>
139 |
147 |
148 |
149 | >
150 | );
151 | };
152 | ```
153 |
154 | ---
155 |
156 | ## API Overview
157 |
158 | - **`useValue(initial)`**: Initializes an animated value.
159 | - **`animate`**: JSX wrapper for animatable elements (`animate.div`, `animate.span`, etc.).
160 | - **Modifiers**: `withSpring`, `withTiming`, `withDecay`, `withSequence`, `withEase` — functions to define animation behavior.
161 | - **`useMount(state, config)`**: Manages mount/unmount transitions. `config` includes `from`, `enter`, and `exit` values.
162 |
163 | ## Gestures
164 |
165 | `react-ui-animate` also provides hooks for handling gestures:
166 |
167 | - `useDrag`
168 | - `useMouseMove`
169 | - `useScroll`
170 | - `useWheel`
171 | - `useGesture`
172 |
173 | **Example: `useDrag`**
174 |
175 | ```tsx
176 | import React from 'react';
177 | import { useValue, animate, useDrag, withSpring } from 'react-ui-animate';
178 |
179 | export const Draggable: React.FC = () => {
180 | const [translateX, setTranslateX] = useValue(0);
181 |
182 | const bind = useDrag(({ down, movementX }) => {
183 | setTranslateX(down ? movementX : withSpring(0));
184 | });
185 |
186 | return (
187 |
196 | );
197 | };
198 | ```
199 |
200 | ## Documentation
201 |
202 | For detailed documentation and examples, visit the official [react-ui-animate documentation](https://react-ui-animate.js.org/).
203 |
204 | ## License
205 |
206 | This library is licensed under the MIT License.
207 |
--------------------------------------------------------------------------------
/example/.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 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | *storybook.log
--------------------------------------------------------------------------------
/example/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from '@storybook/react-webpack5';
2 |
3 | const config: StorybookConfig = {
4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5 | addons: [
6 | '@storybook/preset-create-react-app',
7 | '@storybook/addon-onboarding',
8 | '@storybook/addon-links',
9 | '@storybook/addon-essentials',
10 | '@chromatic-com/storybook',
11 | '@storybook/addon-interactions',
12 | ],
13 | framework: {
14 | name: '@storybook/react-webpack5',
15 | options: {},
16 | },
17 | staticDirs: ['../public'],
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/example/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from '@storybook/react';
2 |
3 | import '../src/index.css';
4 |
5 | const preview: Preview = {
6 | parameters: {
7 | controls: {
8 | matchers: {
9 | color: /(background|color)$/i,
10 | date: /Date$/i,
11 | },
12 | },
13 | },
14 | };
15 |
16 | export default preview;
17 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
7 | "@testing-library/jest-dom": "^5.17.0",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "@types/jest": "^27.5.2",
11 | "@types/node": "^16.18.101",
12 | "@types/react": "^18.3.3",
13 | "@types/react-dom": "^18.3.0",
14 | "react": "../node_modules/react",
15 | "react-dom": "^18.3.1",
16 | "react-router-dom": "^6.25.1",
17 | "react-scripts": "^5.0.1",
18 | "react-ui-animate": "..",
19 | "typescript": "^4.9.5",
20 | "web-vitals": "^2.1.4"
21 | },
22 | "scripts": {
23 | "start": "npm run storybook",
24 | "build": "npm run build-storybook",
25 | "storybook": "storybook dev -p 6006",
26 | "build-storybook": "storybook build"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest",
32 | "plugin:storybook/recommended"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {
48 | "@chromatic-com/storybook": "^1.6.1",
49 | "@storybook/addon-essentials": "^8.2.8",
50 | "@storybook/addon-interactions": "^8.2.8",
51 | "@storybook/addon-links": "^8.2.8",
52 | "@storybook/addon-onboarding": "^8.2.8",
53 | "@storybook/blocks": "^8.2.8",
54 | "@storybook/preset-create-react-app": "^8.2.8",
55 | "@storybook/react": "^8.2.8",
56 | "@storybook/react-webpack5": "^8.2.8",
57 | "@storybook/test": "^8.2.8",
58 | "eslint-plugin-storybook": "^0.8.0",
59 | "prop-types": "^15.8.1",
60 | "storybook": "^8.2.8",
61 | "webpack": "^5.93.0"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dipeshrai123/react-ui-animate/7da92a46f67b492b42637d89f99d294bc8fbdcdb/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | React UI Animate Examples
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dipeshrai123/react-ui-animate/7da92a46f67b492b42637d89f99d294bc8fbdcdb/example/public/logo192.png
--------------------------------------------------------------------------------
/example/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dipeshrai123/react-ui-animate/7da92a46f67b492b42637d89f99d294bc8fbdcdb/example/public/logo512.png
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/example/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | }
9 |
--------------------------------------------------------------------------------
/example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 |
3 | import './index.css';
4 |
5 | const App = () => RUN WITH STORYBOOK
;
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 |
11 | root.render();
12 |
--------------------------------------------------------------------------------
/example/src/stories/core/useAnimatedList/useAnimatedList.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { UseAnimatedList as Example } from './useAnimatedList';
4 |
5 | const meta = {
6 | title: 'Core/useAnimatedList',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const UseAnimatedList: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/core/useAnimatedList/useAnimatedList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | animate,
4 | withSpring,
5 | useAnimatedList,
6 | withTiming,
7 | withSequence,
8 | } from 'react-ui-animate';
9 |
10 | type Item = {
11 | id: string;
12 | name: string;
13 | };
14 |
15 | export const UseAnimatedList: React.FC = () => {
16 | const [items, setItems] = useState- ([]);
17 |
18 | // const animatedList = useAnimatedList<
19 | // Item,
20 | // { opacity: number; translateX: number }
21 | // >(items, (item) => item.id, {
22 | // from: {
23 | // opacity: 0,
24 | // translateX: 0,
25 | // },
26 | // enter: {
27 | // opacity: withSpring(1),
28 | // translateX: withSpring(100),
29 | // },
30 | // exit: {
31 | // translateX: withSequence([
32 | // withSpring(50),
33 | // withTiming(0, { duration: 2000 }),
34 | // ]),
35 | // opacity: withSpring(1),
36 | // },
37 | // });
38 |
39 | const animatedList = useAnimatedList(items, (item) => item.id, {
40 | from: { translateX: 0 },
41 | enter: {
42 | translateX: withSequence([
43 | withSpring(100),
44 | withTiming(200, { duration: 2000 }),
45 | ]),
46 | },
47 | exit: { translateX: 0 },
48 | });
49 |
50 | const addItem = () => {
51 | const id = Math.random().toString(36).slice(2, 7);
52 | setItems([...items, { id, name: `Item ${id.toUpperCase()}` }]);
53 | };
54 |
55 | const removeItem = (id: string) => {
56 | setItems((prev) => prev.filter((item) => item.id !== id));
57 | };
58 |
59 | return (
60 | <>
61 |
62 | {animatedList
63 | .map(({ animation, item }) => (
64 | removeItem(item.id)}
67 | style={{
68 | height: 100,
69 | translateX: animation.translateX,
70 | width: 100,
71 | backgroundColor: 'red',
72 | left: 0,
73 | top: 0,
74 | border: '1px solid black',
75 | }}
76 | />
77 | ))
78 | .reverse()}
79 | >
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/example/src/stories/core/useMount/UseMount.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { UseMount as Example } from './useMount';
4 |
5 | const meta = {
6 | title: 'Core/useMount',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Basic: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/core/useMount/useMount.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | animate,
4 | useMount,
5 | withDecay,
6 | withSequence,
7 | withSpring,
8 | withTiming,
9 | } from 'react-ui-animate';
10 |
11 | export const UseMount: React.FC = () => {
12 | const [open, setOpen] = React.useState(true);
13 | // const mountedValue = useMount(open, {
14 | // from: { width: 200, opacity: 0, translateX: 0 },
15 | // enter: {
16 | // width: 300,
17 | // opacity: 1,
18 | // translateX: withTiming(200, { duration: 5000 }),
19 | // },
20 | // exit: {
21 | // width: 100,
22 | // opacity: 1,
23 | // translateX: withSequence([
24 | // withTiming(0, { duration: 2000 }),
25 | // withDecay({ velocity: 1 }),
26 | // withSpring(100),
27 | // ]),
28 | // },
29 | // });
30 |
31 | const mounted = useMount(open, { from: 0, enter: 1, exit: 0 });
32 |
33 | return (
34 | <>
35 | {mounted(
36 | (a, m) =>
37 | m && (
38 |
46 | )
47 | )}
48 |
49 |
56 |
57 | {/* {mountedValue(({ width, opacity, translateX }, mounted) => {
58 | return (
59 | mounted && (
60 |
69 | )
70 | );
71 | })} */}
72 | >
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/DynamicAnimation.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { DynamicAnimation as Example } from './DynamicAnimation';
4 |
5 | const meta = {
6 | title: 'Core/useValue',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const DynamicAnimation: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/DynamicAnimation.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | animate,
3 | AnimationConfig,
4 | useValue,
5 | withTiming,
6 | } from 'react-ui-animate';
7 |
8 | export const DynamicAnimation = () => {
9 | const [x, setX] = useValue(0);
10 |
11 | return (
12 | <>
13 |
21 |
22 |
29 |
30 |
37 | >
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/Loop.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Loop as Example } from './Loop';
4 |
5 | const meta = {
6 | title: 'Core/useValue',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Loop: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/Loop.tsx:
--------------------------------------------------------------------------------
1 | import { animate, useValue, withSpring, withLoop } from 'react-ui-animate';
2 |
3 | export function Loop() {
4 | const [translateX, setTranslateX] = useValue(0);
5 |
6 | const runAnimation = () => {
7 | // Basic Loop
8 | setTranslateX(withLoop(withSpring(100), 5));
9 |
10 | // Loop with sequence
11 | // translateX.value = withLoop(
12 | // withSequence([withSpring(100), withSpring(0)]),
13 | // 5
14 | // );
15 |
16 | // Loop and Sequence nested
17 | // translateX.value = withSequence([
18 | // withSpring(100),
19 | // withSpring(0),
20 | // withLoop(withSequence([withTiming(100), withSpring(50)]), 5),
21 | // ]);
22 | };
23 |
24 | return (
25 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/SequenceTransition.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { SequenceTransition as Example } from './SequenceTransition';
4 |
5 | const meta = {
6 | title: 'Core/useValue',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const SequenceTransition: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/SequenceTransition.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | animate,
3 | useValue,
4 | withSpring,
5 | withTiming,
6 | Easing,
7 | withDecay,
8 | withSequence,
9 | } from 'react-ui-animate';
10 |
11 | export const SequenceTransition = () => {
12 | const [x, setX] = useValue(0);
13 |
14 | return (
15 | <>
16 |
24 |
25 |
32 |
33 |
46 | >
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/UseValue.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { UseValue as Example } from './useValue';
4 |
5 | const meta = {
6 | title: 'Core/useValue',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const UpdatingValue: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/core/useValue/useValue.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | animate,
4 | useValue,
5 | withSpring,
6 | withTiming,
7 | withSequence,
8 | } from 'react-ui-animate';
9 |
10 | export const UseValue: React.FC = () => {
11 | const [width, setWidth] = useValue(100);
12 | const [backgroundColor, setBackgroundColor] = useValue('black');
13 |
14 | return (
15 | <>
16 |
19 |
26 |
27 |
34 |
35 |
42 |
49 |
56 |
57 |
66 | >
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/example/src/stories/demo/Modal.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { ModalComp as Example } from './Modal';
4 |
5 | const meta = {
6 | title: 'Demo/Modal',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Modal: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/demo/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 |
3 | import { useMount, animate, useOutsideClick } from 'react-ui-animate';
4 |
5 | const Modal = ({
6 | visible,
7 | onClose,
8 | }: {
9 | visible: boolean;
10 | onClose: () => void;
11 | }) => {
12 | const ref = useRef(null);
13 | useOutsideClick(ref, onClose);
14 |
15 | const mount = useMount(visible);
16 |
17 | return (
18 | <>
19 | {mount(
20 | (a, m) =>
21 | m && (
22 |
36 |
47 |
48 |
MODAL CONTENT
49 |
50 |
51 | )
52 | )}
53 | >
54 | );
55 | };
56 |
57 | export const ModalComp = () => {
58 | const [modalOpen, setModalOpen] = useState(false);
59 |
60 | return (
61 | <>
62 |
63 | setModalOpen(false)} />
64 | >
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/example/src/stories/demo/SharedElement.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { SharedElement as Example } from './SharedElement';
4 |
5 | const meta = {
6 | title: 'Demo/SharedElement',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const SharedElement: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/demo/SharedElement.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | useValue,
4 | clamp,
5 | animate,
6 | withTiming,
7 | withSequence,
8 | withSpring,
9 | useDrag,
10 | useMount,
11 | } from 'react-ui-animate';
12 |
13 | const BOX_SIZE = 200;
14 |
15 | const IMAGES = [
16 | 'https://images.unsplash.com/photo-1502082553048-f009c37129b9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
17 | 'https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
18 | 'https://images.unsplash.com/photo-1470770903676-69b98201ea1c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
19 | 'https://images.unsplash.com/photo-1444464666168-49d633b86797?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1469&q=80',
20 | ];
21 |
22 | export function SharedElement() {
23 | const [activeIndex, setActiveIndex] = React.useState(null);
24 |
25 | const [{ left, top, width, height, translateY }, setValue] = useValue({
26 | left: 0,
27 | top: 0,
28 | width: 0,
29 | height: 0,
30 | translateY: 0,
31 | });
32 |
33 | const bind = useDrag(({ down, movementY }) => {
34 | setValue({
35 | translateY: down ? withSpring(clamp(movementY, 0, 300)) : withSpring(0),
36 | });
37 |
38 | if (!down && movementY > 200) {
39 | closeSharedElement();
40 | }
41 | });
42 |
43 | React.useLayoutEffect(() => {
44 | if (activeIndex !== null) {
45 | const activeBox = document.getElementById(`box-${activeIndex}`);
46 | const clientRect = activeBox!.getBoundingClientRect();
47 |
48 | setValue({
49 | left: withSequence([
50 | withTiming(clientRect.left, { duration: 0 }),
51 | withSpring(0),
52 | ]),
53 | top: withSequence([
54 | withTiming(clientRect.top, { duration: 0 }),
55 | withSpring(0),
56 | ]),
57 | width: withSequence([
58 | withTiming(clientRect.width, { duration: 0 }),
59 | withSpring(window.innerWidth),
60 | ]),
61 | height: withSequence([
62 | withTiming(clientRect.height, { duration: 0 }),
63 | withSpring(window.innerHeight),
64 | ]),
65 | });
66 | }
67 | }, [activeIndex, setValue]);
68 |
69 | const closeSharedElement = () => {
70 | if (activeIndex !== null) {
71 | const activeBox = document.getElementById(`box-${activeIndex}`);
72 | const clientRect = activeBox!.getBoundingClientRect();
73 |
74 | setValue({
75 | left: withSpring(clientRect.left),
76 | top: withSpring(clientRect.top),
77 | width: withSpring(clientRect.width),
78 | height: withSpring(clientRect.height, {
79 | onRest: () => setActiveIndex(null),
80 | }),
81 | });
82 | }
83 | };
84 |
85 | const mount = useMount(activeIndex !== null);
86 |
87 | return (
88 | <>
89 |
96 | {IMAGES.map((image, index) => {
97 | const imageStyle =
98 | activeIndex === index
99 | ? {
100 | backgroundColor: 'white',
101 | }
102 | : {
103 | backgroundImage: `url(${image})`,
104 | backgroundSize: 'cover',
105 | };
106 |
107 | return (
108 |
setActiveIndex(index)}
118 | />
119 | );
120 | })}
121 |
122 |
123 | {mount(
124 | (_, m) =>
125 | m && (
126 |
136 |
154 | Pull Down
155 |
156 |
157 | )
158 | )}
159 | >
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/example/src/stories/demo/Sorting.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Sorting as Example } from './Sorting';
4 |
5 | const meta = {
6 | title: 'Demo/Sorting',
7 | component: Example,
8 | } satisfies Meta
;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Sorting: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/demo/Sorting.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import {
3 | animate,
4 | useDrag,
5 | clamp,
6 | move,
7 | withEase,
8 | useValue,
9 | } from 'react-ui-animate';
10 |
11 | const ITEMS = ['Please!', 'Can you', 'order', 'me ?'];
12 |
13 | export const Sorting = () => {
14 | const originalIndex = useRef(ITEMS.map((_, i) => i));
15 | const [animationY, setAnimationY] = useValue(ITEMS.map((_, i) => i * 70));
16 | const [zIndex, setZIndex] = useValue(ITEMS.map(() => 0));
17 |
18 | const bind = useDrag(({ args: [i], down, movementY: my, movementX: mx }) => {
19 | const index = originalIndex.current.indexOf(i!);
20 | const newIndex = clamp(
21 | Math.round((index * 70 + my) / 70),
22 | 0,
23 | ITEMS.length - 1
24 | );
25 | const newOrder = move(originalIndex.current, index, newIndex);
26 |
27 | if (!down) {
28 | originalIndex.current = newOrder;
29 | }
30 |
31 | const a = [];
32 | const v = [];
33 | for (let j = 0; j < ITEMS.length; j++) {
34 | const isActive = down && j === i;
35 | a[j] = withEase(isActive ? index * 70 + my : newOrder.indexOf(j) * 70);
36 | v[j] = isActive ? 1 : 0;
37 | }
38 |
39 | setAnimationY(a);
40 | setZIndex(v);
41 | });
42 |
43 | return (
44 |
45 | {animationY.map((y, i) => (
46 |
70 | {ITEMS[i]}
71 |
72 | ))}
73 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/example/src/stories/demo/Toast.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { ToastComp as Example } from './Toast';
4 |
5 | const meta = {
6 | title: 'Demo/Toast',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Toast: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/demo/Toast.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import {
3 | animate,
4 | useMount,
5 | withEase,
6 | withSequence,
7 | withTiming,
8 | } from 'react-ui-animate';
9 |
10 | const Toast = ({ id, onEnd }: any) => {
11 | const [visible, setVisible] = useState(true);
12 |
13 | const mount = useMount(visible, {
14 | enter: withSequence([
15 | withEase(1),
16 | withEase(2),
17 | withTiming(3, { duration: 2000, onRest: () => setVisible(false) }),
18 | ]),
19 | exit: withEase(4, { onRest: () => onEnd(id) }),
20 | });
21 |
22 | return (
23 | <>
24 | {mount(
25 | (a, m) =>
26 | m && (
27 |
37 |
50 |
51 | )
52 | )}
53 | >
54 | );
55 | };
56 |
57 | var uniqueId = 0;
58 |
59 | export const ToastComp = () => {
60 | const [elements, setElements] = useState<{ id: number }[]>([]);
61 |
62 | const generateToast = () => {
63 | setElements((prev) => [...prev, { id: uniqueId++ }]);
64 | };
65 |
66 | const handleEnd = useCallback((id: number) => {
67 | setElements((els) => els.filter((e) => e.id !== id));
68 | }, []);
69 |
70 | return (
71 | <>
72 |
84 |
85 |
95 | {elements.map((e) => {
96 | return ;
97 | })}
98 |
99 | >
100 | );
101 | };
102 |
--------------------------------------------------------------------------------
/example/src/stories/examples/Interpolation.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Interpolation as Example } from './Interpolation';
4 |
5 | const meta = {
6 | title: 'Examples/Interpolation',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Interpolation: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/examples/Interpolation.tsx:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from 'react';
2 | import { animate, useValue, withSpring } from 'react-ui-animate';
3 |
4 | export const Interpolation = () => {
5 | const [open, setOpen] = useState(false);
6 | const [x, setX] = useValue(0);
7 |
8 | useLayoutEffect(() => {
9 | setX(withSpring(open ? 500 : 0));
10 | }, [open, setX]);
11 |
12 | return (
13 | <>
14 |
22 |
23 |
24 | >
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/example/src/stories/examples/Ripple.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { RippleButton as Example } from './Ripple';
4 |
5 | const meta = {
6 | title: 'Examples/RippleButton',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const RippleButton: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/examples/Ripple.tsx:
--------------------------------------------------------------------------------
1 | import { MouseEvent, useLayoutEffect, useState } from 'react';
2 | import { animate, useValue, withTiming } from 'react-ui-animate';
3 |
4 | import '../../index.css';
5 |
6 | const RIPPLE_SIZE = 50;
7 |
8 | function Ripple({
9 | id,
10 | x,
11 | y,
12 | onRemove,
13 | }: {
14 | id: number;
15 | x: number;
16 | y: number;
17 | onRemove: (id: number) => void;
18 | }) {
19 | const [animation, setAnimation] = useValue(0);
20 |
21 | useLayoutEffect(() => {
22 | setAnimation(
23 | withTiming(1, {
24 | duration: 800,
25 | onRest: () => {
26 | onRemove(id);
27 | },
28 | })
29 | );
30 | }, [id, setAnimation, onRemove]);
31 |
32 | return (
33 |
47 | );
48 | }
49 |
50 | let _uniqueId = 0;
51 |
52 | export function RippleButton() {
53 | const [ripples, setRipples] = useState<
54 | Array<{ id: number; x: number; y: number }>
55 | >([]);
56 |
57 | const addRipple = ({
58 | clientX,
59 | clientY,
60 | currentTarget,
61 | }: MouseEvent) => {
62 | const { left, top } = currentTarget.getBoundingClientRect();
63 |
64 | setRipples((previousRipples) => [
65 | ...previousRipples,
66 | {
67 | id: _uniqueId++,
68 | x: clientX - left - RIPPLE_SIZE / 2,
69 | y: clientY - top - RIPPLE_SIZE / 2,
70 | },
71 | ]);
72 | };
73 |
74 | const removeRipple = (id: number) => {
75 | setRipples((prevRipples) =>
76 | prevRipples.filter((ripple) => ripple.id !== id)
77 | );
78 | };
79 |
80 | return (
81 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/example/src/stories/examples/SnapTo.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { SnapTo as Example } from './SnapTo';
4 |
5 | const meta = {
6 | title: 'Examples/SnapTo',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const SnapTo: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/examples/SnapTo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import {
3 | animate,
4 | useDrag,
5 | useValue,
6 | snapTo,
7 | withSpring,
8 | } from 'react-ui-animate';
9 |
10 | import '../../index.css';
11 |
12 | export function SnapTo() {
13 | const [{ x, y }, setXY] = useValue({ x: 0, y: 0 });
14 | const offset = useRef({ x: 0, y: 0 });
15 |
16 | const bind = useDrag(
17 | ({ movementX, movementY, velocityX, velocityY, down }) => {
18 | if (!down) {
19 | offset.current = {
20 | x: movementX + offset.current.x,
21 | y: movementY + offset.current.y,
22 | };
23 |
24 | const snapX = snapTo(offset.current.x, velocityX, [0, 600]);
25 | const snapY = snapTo(offset.current.y, velocityY, [0, 600]);
26 |
27 | setXY({ x: withSpring(snapX), y: withSpring(snapY) });
28 |
29 | offset.current = { x: snapX, y: snapY };
30 | } else {
31 | setXY({
32 | x: movementX + offset.current.x,
33 | y: movementY + offset.current.y,
34 | });
35 | }
36 | }
37 | );
38 |
39 | return (
40 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/example/src/stories/examples/Stagger.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Example from './Stagger';
4 |
5 | const meta = {
6 | title: 'Examples/Stagger',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Stagger: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/examples/Stagger.tsx:
--------------------------------------------------------------------------------
1 | import { Children, useLayoutEffect, useState } from 'react';
2 | import {
3 | useValue,
4 | useScroll,
5 | animate,
6 | withSequence,
7 | withSpring,
8 | withDelay,
9 | withTiming,
10 | } from 'react-ui-animate';
11 |
12 | const StaggerItem = ({
13 | y,
14 | index,
15 | content,
16 | }: {
17 | y: number;
18 | index: number;
19 | content: string;
20 | }) => {
21 | const [top, setTop] = useValue(0);
22 |
23 | useLayoutEffect(() => {
24 | setTop(
25 | withSequence([withDelay(index * 50), withTiming(y, { duration: 0 })])
26 | );
27 | }, [y, index, setTop]);
28 |
29 | return (
30 |
38 | {content}
39 |
40 | );
41 | };
42 |
43 | const Stagger = ({ y, children }: any) => {
44 | const childs = Children.toArray(children);
45 |
46 | return (
47 |
48 | {childs.map((child: any, i) => (
49 |
50 | ))}
51 |
52 | );
53 | };
54 |
55 | export default function App() {
56 | const [y, setY] = useState(0);
57 |
58 | useScroll(({ scrollY }) => {
59 | setY(scrollY);
60 | });
61 |
62 | return (
63 |
68 |
75 |
76 | Hello 👋
77 | I'm
78 | Dipesh
79 | Rai
80 | Welcome
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/example/src/stories/examples/SvgLine.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { SVGLine as Example } from './SvgLine';
4 |
5 | const meta = {
6 | title: 'Examples/SVGLine',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const SVGLine: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/examples/SvgLine.tsx:
--------------------------------------------------------------------------------
1 | import { useValue, useDrag, animate, withSpring } from 'react-ui-animate';
2 |
3 | export function SVGLine() {
4 | const [dragX, setDragX] = useValue(0);
5 | const [followX, setFollowX] = useValue(0);
6 |
7 | const circleBind = useDrag(({ movementX }) => {
8 | setDragX(movementX);
9 | setFollowX(withSpring(movementX));
10 | });
11 |
12 | return (
13 |
14 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Decay.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Decay as Example } from './Decay';
4 |
5 | const meta = {
6 | title: 'Gestures/Decay Animation',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const DecayAnimation: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Decay.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { useDrag, useValue, animate, withDecay } from 'react-ui-animate';
3 |
4 | export const Decay = () => {
5 | const [translateX, setTranslateX] = useValue(0);
6 | const offsetX = useRef(0);
7 | const [animatedVelocityX, setAnimatedVelocityX] = useValue(0);
8 |
9 | const bind = useDrag(({ down, movementX, velocityX }) => {
10 | setAnimatedVelocityX(velocityX);
11 |
12 | setTranslateX(
13 | down
14 | ? movementX + offsetX.current
15 | : withDecay({
16 | velocity: velocityX,
17 | onChange: (v) => (offsetX.current = v as number),
18 | clamp: [0, 400],
19 | })
20 | );
21 | });
22 |
23 | return (
24 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Draggable.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Draggable as Example } from './Draggable';
4 |
5 | const meta = {
6 | title: 'Gestures/Conditional Binding',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const ConditionalBinding: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Draggable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useValue, animate, useDrag } from 'react-ui-animate';
3 |
4 | export const Draggable = () => {
5 | const [open, setOpen] = React.useState(true);
6 | const [translateX, setTranslateX] = useValue(0);
7 |
8 | const bind = useDrag(function ({ down, movementX }) {
9 | if (open) {
10 | setTranslateX(down ? movementX : 0);
11 | }
12 | });
13 |
14 | return (
15 | <>
16 |
19 |
20 |
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Gestures.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Gestures as Example } from './Gestures';
4 |
5 | const meta = {
6 | title: 'Gestures/MultipleGesture',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const MultipleGesture: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Gestures.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | useValue,
4 | animate,
5 | useGesture,
6 | clamp,
7 | withSpring,
8 | } from 'react-ui-animate';
9 |
10 | export const Gestures = () => {
11 | const [x, setX] = useValue(0);
12 | const [y, setY] = useValue(0);
13 | const [s, setS] = useValue(1);
14 | const [rotate, setRotate] = useValue(0);
15 | const scaleRef = React.useRef(1);
16 | const rotateRef = React.useRef(0);
17 |
18 | const bind = useGesture({
19 | onDrag: function ({ offsetX, offsetY }) {
20 | setX(withSpring(offsetX));
21 | setY(withSpring(offsetY));
22 | },
23 | onWheel: function ({ deltaY }) {
24 | scaleRef.current += deltaY * -0.001;
25 | scaleRef.current = clamp(scaleRef.current, 0.125, 4);
26 |
27 | setS(withSpring(scaleRef.current));
28 | },
29 | });
30 |
31 | return (
32 | <>
33 |
34 |
42 |
50 |
51 |
52 |
53 |
73 | DIPESH
74 |
75 |
76 | >
77 | );
78 | };
79 |
80 | // import React from "react";
81 | // import { useSpring, animated } from "react-spring";
82 | // import { useGesture, clamp } from "react-ui-animate";
83 |
84 | // export const Gestures = () => {
85 | // const [{ x, y, s, rotate }, api] = useSpring(() => ({
86 | // x: 0,
87 | // y: 0,
88 | // s: 1,
89 | // rotate: 0,
90 | // }));
91 |
92 | // const scaleRef = React.useRef(0);
93 | // const rotateRef = React.useRef(0);
94 |
95 | // const bind = useGesture({
96 | // onDrag: function ({ offsetX, offsetY }) {
97 | // api.start({ x: offsetX, y: offsetY });
98 | // },
99 | // onWheel: function ({ deltaY }) {
100 | // scaleRef.current += deltaY * -0.01;
101 | // scaleRef.current = clamp(scaleRef.current, 0.125, 4);
102 |
103 | // api.start({ s: scaleRef.current });
104 | // },
105 | // });
106 |
107 | // return (
108 | // <>
109 | //
110 | //
118 | //
126 | //
127 |
128 | //
148 | // DIPESH
149 | //
150 | // >
151 | // );
152 | // };
153 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/MouseMove.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { MouseMove as Example } from './MouseMove';
4 |
5 | const meta = {
6 | title: 'Gestures/MouseMove',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const MouseMove: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/MouseMove.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { animate, useValue, useMouseMove } from 'react-ui-animate';
3 |
4 | export const MouseMove = () => {
5 | const [open, setOpen] = React.useState(true);
6 | const [x, setX] = useValue(0);
7 | const [y, setY] = useValue(0);
8 |
9 | const bind = useMouseMove(function ({ mouseX, mouseY }) {
10 | if (open) {
11 | setX(mouseX);
12 | setY(mouseY);
13 | }
14 | });
15 |
16 | return (
17 | <>
18 |
24 |
25 |
38 |
39 |
40 | {Array(5)
41 | .fill(null)
42 | .map((_, i) => (
43 |
53 | ))}
54 | >
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Scroll.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Scroll as Example } from './Scroll';
4 |
5 | const meta = {
6 | title: 'Gestures/Scroll',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Scroll: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Scroll.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useScroll,
3 | useValue,
4 | interpolateNumbers,
5 | animate,
6 | } from 'react-ui-animate';
7 |
8 | export const Scroll = () => {
9 | const [x, setX] = useValue(100);
10 | const [color, setColor] = useValue('yellow');
11 | const [position, setPosition] = useValue('fixed');
12 | const bind = useScroll(function (event) {
13 | setX(
14 | interpolateNumbers(event.scrollY, [0, 200], [100, 300], {
15 | extrapolate: 'clamp',
16 | })
17 | );
18 |
19 | if (event.scrollY > 100) {
20 | setPosition('absolute');
21 | setColor('red');
22 | } else {
23 | setPosition('fixed');
24 | setColor('yellow');
25 | }
26 | });
27 |
28 | return (
29 | <>
30 |
40 |
setColor('red')}
42 | style={{
43 | width: 100,
44 | height: 100,
45 | backgroundColor: color,
46 | top: 100,
47 | left: x,
48 | position,
49 | }}
50 | />
51 |
52 |
53 | >
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Wheel.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Wheel as Example } from './Wheel';
4 |
5 | const meta = {
6 | title: 'Gestures/Wheel',
7 | component: Example,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Wheel: Story = {};
14 |
--------------------------------------------------------------------------------
/example/src/stories/gestures/Wheel.tsx:
--------------------------------------------------------------------------------
1 | import { useWheel } from 'react-ui-animate';
2 |
3 | export const Wheel = () => {
4 | const bind = useWheel(function (event) {
5 | console.log('WHEEL', event);
6 | });
7 |
8 | return (
9 | <>
10 |
21 |
22 |
23 |
24 | {Array(5)
25 | .fill(null)
26 | .map((_, i) => (
27 |
36 | ))}
37 | >
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "esModuleInterop": true,
5 | "lib": [
6 | "dom",
7 | "es2015"
8 | ],
9 | "downlevelIteration": true,
10 | "jsx": "react-jsx",
11 | "target": "es5",
12 | "allowJs": true,
13 | "skipLibCheck": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noEmit": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "allowSyntheticDefaultImports": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-ui-animate",
3 | "version": "5.0.0-rc.1",
4 | "description": "React library for gestures and animation",
5 | "main": "dist/index.js",
6 | "peerDependencies": {
7 | "react": ">=16.8.0 || >=17.0.0 || >=18.0.0"
8 | },
9 | "dependencies": {
10 | "@raidipesh78/re-motion": "^5.1.0"
11 | },
12 | "devDependencies": {
13 | "@rollup/plugin-terser": "^0.4.4",
14 | "@types/jest": "^29.5.12",
15 | "@types/node": "^20.14.9",
16 | "@types/react": "^18.3.3",
17 | "@types/react-dom": "^18.3.0",
18 | "@types/resize-observer-browser": "^0.1.11",
19 | "babel-core": "^5.8.38",
20 | "babel-runtime": "^6.26.0",
21 | "react": "^18.3.1",
22 | "react-dom": "^18.3.1",
23 | "rimraf": "^6.0.1",
24 | "rollup": "^4.18.0",
25 | "rollup-plugin-typescript2": "^0.36.0",
26 | "typescript": "^5.5.2"
27 | },
28 | "scripts": {
29 | "clean": "rimraf -rf dist",
30 | "build": "npm run clean && rollup -c",
31 | "start": "npm run clean && rollup -c -w",
32 | "test": "echo \"Error: no test specified\" && exit 1",
33 | "version:minor": "npm version --no-git-tag-version minor",
34 | "version:major": "npm version --no-git-tag-version major",
35 | "version:patch": "npm version --no-git-tag-version patch",
36 | "publish:next": "npm publish --tag next",
37 | "publish:latest": "npm publish --tag latest"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/dipeshrai123/react-ui-animate.git"
42 | },
43 | "keywords": [
44 | "gesture",
45 | "animation",
46 | "react-ui-animate"
47 | ],
48 | "author": "Dipesh Rai",
49 | "license": "MIT",
50 | "bugs": {
51 | "url": "https://github.com/dipeshrai123/react-ui-animate/issues"
52 | },
53 | "homepage": "https://github.com/dipeshrai123/react-ui-animate#readme"
54 | }
55 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import typescript from "rollup-plugin-typescript2";
2 | import terser from '@rollup/plugin-terser';
3 |
4 | import pkg from "./package.json" with { type: "json" };
5 |
6 | export default {
7 | input: "src/index.ts",
8 | output: [
9 | {
10 | file: pkg.main,
11 | format: "cjs",
12 | exports: "named",
13 | sourcemap: true,
14 | strict: false,
15 | },
16 | ],
17 | plugins: [typescript(), terser()],
18 | external: ["react", "react-dom"],
19 | };
20 |
--------------------------------------------------------------------------------
/src/animation/AnimationConfig.ts:
--------------------------------------------------------------------------------
1 | import { Easing } from '@raidipesh78/re-motion';
2 |
3 | export const AnimationConfig = {
4 | Timing: {
5 | BOUNCE: { duration: 500, easing: Easing.bounce },
6 | EASE_IN: { duration: 500, easing: Easing.in(Easing.ease) },
7 | EASE_OUT: { duration: 500, easing: Easing.out(Easing.ease) },
8 | EASE_IN_OUT: { duration: 500, easing: Easing.inOut(Easing.ease) },
9 | POWER1: { duration: 500, easing: Easing.bezier(0.17, 0.42, 0.51, 0.97) },
10 | POWER2: { duration: 500, easing: Easing.bezier(0.07, 0.11, 0.13, 1) },
11 | POWER3: { duration: 500, easing: Easing.bezier(0.09, 0.7, 0.16, 1.04) },
12 | POWER4: { duration: 500, easing: Easing.bezier(0.05, 0.54, 0, 1.03) },
13 | LINEAR: { duration: 500, easing: Easing.linear },
14 | },
15 | Spring: {
16 | ELASTIC: { mass: 1, friction: 18, tension: 250 },
17 | EASE: { mass: 1, friction: 26, tension: 170 },
18 | STIFF: { mass: 1, friction: 18, tension: 350 },
19 | WOBBLE: { mass: 1, friction: 8, tension: 250 },
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/src/animation/Value.ts:
--------------------------------------------------------------------------------
1 | import {
2 | decay,
3 | delay,
4 | loop,
5 | MotionValue,
6 | sequence,
7 | spring,
8 | timing,
9 | } from '@raidipesh78/re-motion';
10 |
11 | import { DriverConfig, ToValue } from './types';
12 |
13 | export class Value {
14 | private animation: MotionValue;
15 | private unsubscribe?: () => void;
16 |
17 | constructor(initial: V) {
18 | this.animation = new MotionValue(initial);
19 | }
20 |
21 | set(u: MotionValue | ToValue) {
22 | if (u instanceof MotionValue) return;
23 |
24 | this.unsubscribe?.();
25 | this.unsubscribe = undefined;
26 |
27 | if (typeof u === 'object' && u !== null) {
28 | const { type, to, options } = u;
29 |
30 | if (options?.onChange) {
31 | this.unsubscribe = this.animation.subscribe(options.onChange);
32 | }
33 |
34 | if (type === 'sequence') {
35 | const steps = options?.steps ?? [];
36 | const controllers = steps.map((step) => this.buildDriver(step));
37 | const ctrl = sequence(controllers);
38 |
39 | // Handle onComplete manually here, until we fix on re-motion
40 | if (options?.onComplete) {
41 | ctrl.setOnComplete?.(options?.onComplete);
42 | }
43 |
44 | ctrl.start();
45 | return;
46 | }
47 |
48 | if (type === 'loop') {
49 | const inner = this.buildDriver(options!.controller!);
50 | const ctrl = loop(inner, options?.iterations!);
51 |
52 | // Handle onComplete manually here, until we fix on re-motion
53 | if (options?.onComplete) {
54 | ctrl.setOnComplete?.(options?.onComplete);
55 | }
56 |
57 | ctrl.start();
58 | return;
59 | }
60 |
61 | this.buildDriver({ type, to, options }).start?.();
62 | } else {
63 | this.animation.set(u as V);
64 | }
65 | }
66 |
67 | private buildDriver(cfg: DriverConfig) {
68 | const anim = this.animation as MotionValue;
69 |
70 | switch (cfg.type) {
71 | case 'spring':
72 | return spring(anim, cfg.to!, cfg.options);
73 | case 'timing':
74 | return timing(anim, cfg.to!, cfg.options);
75 | case 'decay':
76 | return decay(anim, cfg.options?.velocity!, cfg.options);
77 | case 'delay':
78 | return delay(cfg.options?.delay!);
79 | case 'sequence':
80 | return sequence(cfg.options!.steps!.map(this.buildDriver.bind(this)));
81 | default:
82 | throw new Error(`Unsupported driver type "${cfg.type}"`);
83 | }
84 | }
85 |
86 | get value(): MotionValue {
87 | return this.animation;
88 | }
89 |
90 | get current(): V {
91 | return this.animation.current;
92 | }
93 |
94 | destroy() {
95 | this.unsubscribe?.();
96 | this.unsubscribe = undefined;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/animation/controllers.ts:
--------------------------------------------------------------------------------
1 | import { AnimationConfig } from './AnimationConfig';
2 | import { DriverConfig, Primitive } from './types';
3 |
4 | interface WithSpringOptions {
5 | mass?: number;
6 | tension?: number;
7 | friction?: number;
8 | onStart?: () => void;
9 | onChange?: (v: number | string) => void;
10 | onRest?: () => void;
11 | }
12 |
13 | export const withSpring = (
14 | to: Primitive,
15 | options?: WithSpringOptions
16 | ): DriverConfig => {
17 | return {
18 | type: 'spring',
19 | to,
20 | options: {
21 | stiffness: options?.tension ?? AnimationConfig.Spring.ELASTIC.tension,
22 | damping: options?.friction ?? AnimationConfig.Spring.ELASTIC.friction,
23 | mass: options?.mass ?? AnimationConfig.Spring.ELASTIC.mass,
24 | onStart: options?.onStart,
25 | onChange: options?.onChange,
26 | onComplete: options?.onRest,
27 | },
28 | };
29 | };
30 |
31 | export const withEase = (
32 | to: Primitive,
33 | options?: WithSpringOptions
34 | ): DriverConfig =>
35 | withSpring(to, { ...options, ...AnimationConfig.Spring.EASE });
36 |
37 | interface WithTimingOptions {
38 | duration?: number;
39 | easing?: (t: number) => number;
40 | onStart?: () => void;
41 | onChange?: (v: number | string) => void;
42 | onRest?: () => void;
43 | }
44 |
45 | export const withTiming = (
46 | to: Primitive,
47 | options?: WithTimingOptions
48 | ): DriverConfig => ({
49 | type: 'timing',
50 | to,
51 | options: {
52 | duration: options?.duration ?? 300,
53 | easing: options?.easing,
54 | onStart: options?.onStart,
55 | onChange: options?.onChange,
56 | onComplete: options?.onRest,
57 | },
58 | });
59 |
60 | interface WithDecayOptions {
61 | velocity: number;
62 | onStart?: () => void;
63 | onChange?: (v: number | string) => void;
64 | onRest?: () => void;
65 | clamp?: [number, number];
66 | }
67 |
68 | export const withDecay = (options: WithDecayOptions): DriverConfig => ({
69 | type: 'decay',
70 | options: {
71 | velocity: options.velocity,
72 | onStart: options?.onStart,
73 | onChange: options?.onChange,
74 | onComplete: options?.onRest,
75 | clamp: options?.clamp,
76 | },
77 | });
78 |
79 | interface WithSequenceOptions {
80 | onStart?: () => void;
81 | onChange?: (v: number | string) => void;
82 | onRest?: () => void;
83 | }
84 |
85 | export const withSequence = (
86 | steps: DriverConfig[],
87 | options?: WithSequenceOptions
88 | ): DriverConfig => ({
89 | type: 'sequence',
90 | options: {
91 | steps,
92 | onStart: options?.onStart,
93 | onChange: options?.onChange,
94 | onComplete: options?.onRest,
95 | },
96 | });
97 |
98 | export const withDelay = (delay: number): DriverConfig => ({
99 | type: 'delay',
100 | options: {
101 | delay,
102 | },
103 | });
104 |
105 | interface WithLoopOptions {
106 | onStart?: () => void;
107 | onChange?: (v: number | string) => void;
108 | onRest?: () => void;
109 | }
110 |
111 | export const withLoop = (
112 | controller: DriverConfig,
113 | iterations: number,
114 | options?: WithLoopOptions
115 | ): DriverConfig => ({
116 | type: 'loop',
117 | options: {
118 | controller,
119 | iterations,
120 | onStart: options?.onStart,
121 | onChange: options?.onChange,
122 | onComplete: options?.onRest,
123 | },
124 | });
125 |
--------------------------------------------------------------------------------
/src/animation/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useValue';
2 | export * from './useMount';
3 | export * from './useAnimatedList';
4 |
--------------------------------------------------------------------------------
/src/animation/hooks/useAnimatedList.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useRef, useState } from 'react';
2 | import { MotionValue } from '@raidipesh78/re-motion';
3 |
4 | import { Value } from '../Value';
5 | import { withSpring } from '../controllers';
6 | import type { DriverConfig, ToValue } from '../types';
7 |
8 | export function useAnimatedList(
9 | items: T[],
10 | getKey: (item: T) => string,
11 | config?: {
12 | from?: number;
13 | enter?: ToValue;
14 | exit?: ToValue;
15 | }
16 | ): Array<{ key: string; item: T; animation: MotionValue }>;
17 |
18 | export function useAnimatedList>(
19 | items: T[],
20 | getKey: (item: T) => string,
21 | config: {
22 | from: I;
23 | enter?: Partial<{ [K in keyof I]: ToValue }>;
24 | exit?: Partial<{ [K in keyof I]: ToValue }>;
25 | }
26 | ): Array<{
27 | key: string;
28 | item: T;
29 | animation: Record>;
30 | }>;
31 |
32 | export function useAnimatedList(
33 | items: any[],
34 | getKey: (item: any) => string,
35 | config: any = {}
36 | ) {
37 | const isMulti = typeof config.from === 'object' && config.from !== null;
38 | const fromObj: Record = isMulti
39 | ? config.from
40 | : { value: config.from ?? 0 };
41 |
42 | const enterObj: Record = {};
43 | const exitObj: Record = {};
44 |
45 | Object.keys(fromObj).forEach((key) => {
46 | const rawEnter = isMulti ? config.enter?.[key] : config.enter;
47 | if (typeof rawEnter === 'number') {
48 | enterObj[key] = withSpring(rawEnter);
49 | } else if (rawEnter) {
50 | enterObj[key] = rawEnter;
51 | } else {
52 | enterObj[key] = withSpring(1);
53 | }
54 |
55 | const rawExit = isMulti ? config.exit?.[key] : config.exit;
56 | if (typeof rawExit === 'number') {
57 | exitObj[key] = withSpring(rawExit);
58 | } else if (rawExit) {
59 | exitObj[key] = rawExit;
60 | } else {
61 | exitObj[key] = withSpring(0);
62 | }
63 | });
64 |
65 | const itemsRef = useRef(
66 | new Map>; item: any }>()
67 | );
68 | const exitingRef = useRef(new Set());
69 | const [, forceUpdate] = useState(0);
70 |
71 | useLayoutEffect(() => {
72 | const nextKeys = new Set(items.map(getKey));
73 |
74 | for (const item of items) {
75 | const key = getKey(item);
76 | if (!itemsRef.current.has(key)) {
77 | const values: Record> = {};
78 | Object.entries(fromObj).forEach(([prop, fromVal]) => {
79 | const val = new Value(fromVal);
80 | val.set(enterObj[prop]);
81 | values[prop] = val;
82 | });
83 | itemsRef.current.set(key, { values, item });
84 | forceUpdate((c) => c + 1);
85 | } else {
86 | itemsRef.current.get(key)!.item = item;
87 | }
88 | }
89 |
90 | itemsRef.current.forEach(({ values }, key) => {
91 | if (!nextKeys.has(key) && !exitingRef.current.has(key)) {
92 | exitingRef.current.add(key);
93 | const props = Object.keys(values);
94 | props.forEach((prop, index) => {
95 | const base = exitObj[prop];
96 | values[prop].set({
97 | ...base,
98 | options: {
99 | ...base.options,
100 | onComplete: () => {
101 | if (index === props.length - 1) {
102 | itemsRef.current.delete(key);
103 | exitingRef.current.delete(key);
104 | forceUpdate((c) => c + 1);
105 | base.options?.onComplete?.();
106 | values[prop].destroy();
107 | }
108 | },
109 | },
110 | });
111 | });
112 | }
113 | });
114 | }, [
115 | items,
116 | getKey,
117 | JSON.stringify(fromObj),
118 | JSON.stringify(enterObj),
119 | JSON.stringify(exitObj),
120 | ]);
121 |
122 | return Array.from(itemsRef.current.entries()).map(
123 | ([key, { values, item }]) => {
124 | if (!isMulti) {
125 | return {
126 | key,
127 | item,
128 | animation: values['value'].value,
129 | };
130 | }
131 | const anims: Record> = {};
132 | Object.entries(values).forEach(([prop, val]) => {
133 | anims[prop] = val.value;
134 | });
135 | return {
136 | key,
137 | item,
138 | animation: anims as Record>,
139 | };
140 | }
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/src/animation/hooks/useMount.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from 'react';
2 | import { MotionValue } from '@raidipesh78/re-motion';
3 |
4 | import { withSpring } from '../controllers';
5 | import { useValue } from './useValue';
6 | import type { DriverConfig, ToValue } from '../types';
7 |
8 | export function useMount(
9 | isOpen: boolean,
10 | config?: { from?: number; enter?: ToValue; exit?: ToValue }
11 | ): (
12 | fn: (value: MotionValue, mounted: boolean) => React.ReactNode
13 | ) => React.ReactNode;
14 |
15 | export function useMount>(
16 | isOpen: boolean,
17 | config: {
18 | from: I;
19 | enter?: Partial>>;
20 | exit?: Partial>>;
21 | }
22 | ): (
23 | fn: (
24 | values: Record>,
25 | mounted: boolean
26 | ) => React.ReactNode
27 | ) => React.ReactNode;
28 |
29 | export function useMount(isOpen: boolean, config: any = {}) {
30 | const [mounted, setMounted] = useState(isOpen);
31 |
32 | const isMulti = typeof config.from === 'object' && config.from !== null;
33 | const fromObj: Record = isMulti
34 | ? config.from
35 | : { value: config.from ?? 0 };
36 |
37 | const enterRaw: Record> = {};
38 | const exitRaw: Record> = {};
39 | Object.keys(fromObj).forEach((key) => {
40 | enterRaw[key] = isMulti ? config.enter?.[key] : config.enter;
41 | if (enterRaw[key] == null) enterRaw[key] = 1;
42 |
43 | exitRaw[key] = isMulti ? config.exit?.[key] : config.exit;
44 | if (exitRaw[key] == null) exitRaw[key] = 0;
45 | });
46 |
47 | const [values, setValues] = useValue(fromObj) as [
48 | Record>,
49 | (to: Record | DriverConfig>) => void
50 | ];
51 |
52 | useLayoutEffect(() => {
53 | const keys = Object.keys(fromObj);
54 |
55 | if (isOpen) {
56 | setMounted(true);
57 | queueMicrotask(() => {
58 | const drivers: Record = {};
59 | keys.forEach((key) => {
60 | const param = enterRaw[key]!;
61 | drivers[key] =
62 | typeof param === 'object' && 'type' in param
63 | ? (param as DriverConfig)
64 | : withSpring(param as number);
65 | });
66 | setValues(drivers);
67 | });
68 | } else {
69 | queueMicrotask(() => {
70 | const drivers: Record = {};
71 | keys.forEach((key, i) => {
72 | const param = exitRaw[key]!;
73 | const base =
74 | typeof param === 'object' && 'type' in param
75 | ? (param as DriverConfig)
76 | : withSpring(param as number);
77 |
78 | drivers[key] = {
79 | ...base,
80 | options: {
81 | ...base.options,
82 | onComplete: () => {
83 | if (i === keys.length - 1) {
84 | setMounted(false);
85 | base.options?.onComplete?.();
86 | }
87 | },
88 | },
89 | };
90 | });
91 | setValues(drivers);
92 | });
93 | }
94 | }, [isOpen, JSON.stringify(enterRaw), JSON.stringify(exitRaw)]);
95 |
96 | if (!isMulti) {
97 | const single = values['value'] as MotionValue;
98 | return (fn: (v: MotionValue, m: boolean) => React.ReactNode) =>
99 | fn(single, mounted);
100 | }
101 |
102 | return (
103 | fn: (
104 | vals: Record>,
105 | m: boolean
106 | ) => React.ReactNode
107 | ) => fn(values, mounted);
108 | }
109 |
--------------------------------------------------------------------------------
/src/animation/hooks/useValue.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import { MotionValue } from '@raidipesh78/re-motion';
3 |
4 | import { Value } from '../Value';
5 | import { Primitive, ToValue } from '../types';
6 |
7 | type Input = V | V[] | Record;
8 |
9 | type Output> = I extends V
10 | ? MotionValue
11 | : I extends V[]
12 | ? { [K in keyof I]: MotionValue }
13 | : I extends Record
14 | ? { [K in keyof I]: MotionValue }
15 | : never;
16 |
17 | type SetterParam> = I extends V
18 | ? MotionValue | ToValue
19 | : I extends V[]
20 | ? Partial<{ [K in keyof I]: MotionValue | ToValue }>
21 | : I extends Record
22 | ? Partial<{ [K in keyof I]: MotionValue | ToValue }>
23 | : never;
24 |
25 | export function useValue>(
26 | initial: I
27 | ): [Output, (to: SetterParam) => void] {
28 | const storeRef = useRef]> | null>(null);
29 | if (storeRef.current === null) {
30 | const entries: Array<[string, Value]> = [];
31 |
32 | if (Array.isArray(initial)) {
33 | (initial as V[]).forEach((v, i) => {
34 | entries.push([String(i), new Value(v)]);
35 | });
36 | } else if (typeof initial === 'object') {
37 | for (const [k, v] of Object.entries(initial as Record)) {
38 | entries.push([k, new Value(v)]);
39 | }
40 | } else {
41 | entries.push(['__0', new Value(initial as V)]);
42 | }
43 |
44 | storeRef.current = entries;
45 | }
46 |
47 | useEffect(() => {
48 | return () => {
49 | storeRef.current!.forEach(([, val]) => val.destroy());
50 | storeRef.current = null;
51 | };
52 | }, []);
53 |
54 | const values = (() => {
55 | const entries = storeRef.current!;
56 | if (Array.isArray(initial)) {
57 | return entries.map(([, val]) => val.value) as Output;
58 | }
59 | if (typeof initial === 'object') {
60 | const out: Record> = {};
61 | for (const [k, val] of entries) out[k] = val.value;
62 | return out as Output;
63 | }
64 | return entries[0][1].value as Output;
65 | })();
66 |
67 | const set = ((
68 | to:
69 | | MotionValue
70 | | ToValue
71 | | Array | ToValue>
72 | | Record | ToValue>
73 | ) => {
74 | const entries = storeRef.current!;
75 | if (Array.isArray(initial)) {
76 | const updates = to as Partial | ToValue>>;
77 | Object.entries(updates).forEach(([i, val]) => {
78 | const index = Number(i);
79 | if (!isNaN(index) && val !== undefined) {
80 | entries[index]?.[1].set(val);
81 | }
82 | });
83 | } else if (typeof initial === 'object' && initial !== null) {
84 | const updates = to as Partial<
85 | Record | ToValue>
86 | >;
87 | for (const [k, v] of Object.entries(updates)) {
88 | const entry = entries.find(([ek]) => ek === k);
89 | if (entry && v !== undefined) entry[1].set(v);
90 | }
91 | } else {
92 | entries[0][1].set(to as MotionValue | ToValue);
93 | }
94 | }) as (to: SetterParam) => void;
95 |
96 | return [values, set];
97 | }
98 |
--------------------------------------------------------------------------------
/src/animation/index.ts:
--------------------------------------------------------------------------------
1 | export { useValue, useMount, useAnimatedList } from './hooks';
2 | export {
3 | withSpring,
4 | withTiming,
5 | withSequence,
6 | withDelay,
7 | withDecay,
8 | withLoop,
9 | withEase,
10 | } from './controllers';
11 | export { AnimationConfig } from './AnimationConfig';
12 | export { interpolateNumbers } from './interpolation';
13 |
--------------------------------------------------------------------------------
/src/animation/interpolation/colors.ts:
--------------------------------------------------------------------------------
1 | export const COLOR_NUMBER_REGEX =
2 | /[+-]?\d+(\.\d+)?|[\s]?\.\d+|#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/gi;
3 | export const HEX_NAME_COLOR =
4 | /#[a-f\d]{3,}|transparent|aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|burntsienna|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen/gi;
5 |
6 | interface classNameType {
7 | [name: string]: string;
8 | }
9 |
10 | // Named colors
11 | export const colorNames: classNameType = {
12 | transparent: '#00000000',
13 | aliceblue: '#f0f8ffff',
14 | antiquewhite: '#faebd7ff',
15 | aqua: '#00ffffff',
16 | aquamarine: '#7fffd4ff',
17 | azure: '#f0ffffff',
18 | beige: '#f5f5dcff',
19 | bisque: '#ffe4c4ff',
20 | black: '#000000ff',
21 | blanchedalmond: '#ffebcdff',
22 | blue: '#0000ffff',
23 | blueviolet: '#8a2be2ff',
24 | brown: '#a52a2aff',
25 | burlywood: '#deb887ff',
26 | burntsienna: '#ea7e5dff',
27 | cadetblue: '#5f9ea0ff',
28 | chartreuse: '#7fff00ff',
29 | chocolate: '#d2691eff',
30 | coral: '#ff7f50ff',
31 | cornflowerblue: '#6495edff',
32 | cornsilk: '#fff8dcff',
33 | crimson: '#dc143cff',
34 | cyan: '#00ffffff',
35 | darkblue: '#00008bff',
36 | darkcyan: '#008b8bff',
37 | darkgoldenrod: '#b8860bff',
38 | darkgray: '#a9a9a9ff',
39 | darkgreen: '#006400ff',
40 | darkgrey: '#a9a9a9ff',
41 | darkkhaki: '#bdb76bff',
42 | darkmagenta: '#8b008bff',
43 | darkolivegreen: '#556b2fff',
44 | darkorange: '#ff8c00ff',
45 | darkorchid: '#9932ccff',
46 | darkred: '#8b0000ff',
47 | darksalmon: '#e9967aff',
48 | darkseagreen: '#8fbc8fff',
49 | darkslateblue: '#483d8bff',
50 | darkslategray: '#2f4f4fff',
51 | darkslategrey: '#2f4f4fff',
52 | darkturquoise: '#00ced1ff',
53 | darkviolet: '#9400d3ff',
54 | deeppink: '#ff1493ff',
55 | deepskyblue: '#00bfffff',
56 | dimgray: '#696969ff',
57 | dimgrey: '#696969ff',
58 | dodgerblue: '#1e90ffff',
59 | firebrick: '#b22222ff',
60 | floralwhite: '#fffaf0ff',
61 | forestgreen: '#228b22ff',
62 | fuchsia: '#ff00ffff',
63 | gainsboro: '#dcdcdcff',
64 | ghostwhite: '#f8f8ffff',
65 | gold: '#ffd700ff',
66 | goldenrod: '#daa520ff',
67 | gray: '#808080ff',
68 | green: '#008000ff',
69 | greenyellow: '#adff2fff',
70 | grey: '#808080ff',
71 | honeydew: '#f0fff0ff',
72 | hotpink: '#ff69b4ff',
73 | indianred: '#cd5c5cff',
74 | indigo: '#4b0082ff',
75 | ivory: '#fffff0ff',
76 | khaki: '#f0e68cff',
77 | lavender: '#e6e6faff',
78 | lavenderblush: '#fff0f5ff',
79 | lawngreen: '#7cfc00ff',
80 | lemonchiffon: '#fffacdff',
81 | lightblue: '#add8e6ff',
82 | lightcoral: '#f08080ff',
83 | lightcyan: '#e0ffffff',
84 | lightgoldenrodyellow: '#fafad2ff',
85 | lightgray: '#d3d3d3ff',
86 | lightgreen: '#90ee90ff',
87 | lightgrey: '#d3d3d3ff',
88 | lightpink: '#ffb6c1ff',
89 | lightsalmon: '#ffa07aff',
90 | lightseagreen: '#20b2aaff',
91 | lightskyblue: '#87cefaff',
92 | lightslategray: '#778899ff',
93 | lightslategrey: '#778899ff',
94 | lightsteelblue: '#b0c4deff',
95 | lightyellow: '#ffffe0ff',
96 | lime: '#00ff00ff',
97 | limegreen: '#32cd32ff',
98 | linen: '#faf0e6ff',
99 | magenta: '#ff00ffff',
100 | maroon: '#800000ff',
101 | mediumaquamarine: '#66cdaaff',
102 | mediumblue: '#0000cdff',
103 | mediumorchid: '#ba55d3ff',
104 | mediumpurple: '#9370dbff',
105 | mediumseagreen: '#3cb371ff',
106 | mediumslateblue: '#7b68eeff',
107 | mediumspringgreen: '#00fa9aff',
108 | mediumturquoise: '#48d1ccff',
109 | mediumvioletred: '#c71585ff',
110 | midnightblue: '#191970ff',
111 | mintcream: '#f5fffaff',
112 | mistyrose: '#ffe4e1ff',
113 | moccasin: '#ffe4b5ff',
114 | navajowhite: '#ffdeadff',
115 | navy: '#000080ff',
116 | oldlace: '#fdf5e6ff',
117 | olive: '#808000ff',
118 | olivedrab: '#6b8e23ff',
119 | orange: '#ffa500ff',
120 | orangered: '#ff4500ff',
121 | orchid: '#da70d6ff',
122 | palegoldenrod: '#eee8aaff',
123 | palegreen: '#98fb98ff',
124 | paleturquoise: '#afeeeeff',
125 | palevioletred: '#db7093ff',
126 | papayawhip: '#ffefd5ff',
127 | peachpuff: '#ffdab9ff',
128 | peru: '#cd853fff',
129 | pink: '#ffc0cbff',
130 | plum: '#dda0ddff',
131 | powderblue: '#b0e0e6ff',
132 | purple: '#800080ff',
133 | rebeccapurple: '#663399ff',
134 | red: '#ff0000ff',
135 | rosybrown: '#bc8f8fff',
136 | royalblue: '#4169e1ff',
137 | saddlebrown: '#8b4513ff',
138 | salmon: '#fa8072ff',
139 | sandybrown: '#f4a460ff',
140 | seagreen: '#2e8b57ff',
141 | seashell: '#fff5eeff',
142 | sienna: '#a0522dff',
143 | silver: '#c0c0c0ff',
144 | skyblue: '#87ceebff',
145 | slateblue: '#6a5acdff',
146 | slategray: '#708090ff',
147 | slategrey: '#708090ff',
148 | snow: '#fffafaff',
149 | springgreen: '#00ff7fff',
150 | steelblue: '#4682b4ff',
151 | tan: '#d2b48cff',
152 | teal: '#008080ff',
153 | thistle: '#d8bfd8ff',
154 | tomato: '#ff6347ff',
155 | turquoise: '#40e0d0ff',
156 | violet: '#ee82eeff',
157 | wheat: '#f5deb3ff',
158 | white: '#ffffffff',
159 | whitesmoke: '#f5f5f5ff',
160 | yellow: '#ffff00ff',
161 | yellowgreen: '#9acd32ff',
162 | };
163 |
164 | function conv3to6(hex: string) {
165 | const regex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
166 |
167 | return hex.replace(regex, function (_, r, g, b) {
168 | return '#' + r + r + g + g + b + b;
169 | });
170 | }
171 |
172 | function conv6to8(hex: string) {
173 | if (hex.length === 7) {
174 | return hex + 'FF';
175 | }
176 |
177 | return hex;
178 | }
179 |
180 | export function hexToRgba(hex: string) {
181 | const hex6: string = conv3to6(hex);
182 | const hex8: string = conv6to8(hex6);
183 | const hexRgba: any =
184 | /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex8);
185 |
186 | return {
187 | r: parseInt(hexRgba[1], 16),
188 | g: parseInt(hexRgba[2], 16),
189 | b: parseInt(hexRgba[3], 16),
190 | a: parseInt(hexRgba[4], 16) / 255,
191 | };
192 | }
193 |
194 | export function rgbaToHex(rgba: {
195 | r: number;
196 | g: number;
197 | b: number;
198 | a: number;
199 | }) {
200 | const { r, g, b, a } = rgba;
201 |
202 | const hexR = (r | (1 << 8)).toString(16).slice(1);
203 | const hexG = (g | (1 << 8)).toString(16).slice(1);
204 | const hexB = (b | (1 << 8)).toString(16).slice(1);
205 | const hexA = ((a * 255) | (1 << 8)).toString(16).slice(1);
206 |
207 | return '#' + hexR + hexG + hexB + hexA;
208 | }
209 |
210 | export function processColor(color: number | string) {
211 | if (typeof color === 'number') {
212 | const alpha = ((color >> 24) & 255) / 255;
213 | const red = (color >> 16) & 255;
214 | const green = (color >> 8) & 255;
215 | const blue = color & 255;
216 |
217 | return { r: red, g: green, b: blue, a: alpha };
218 | } else {
219 | // If string then check whether it has # in 0 index
220 | if (color[0] === '#') {
221 | return hexToRgba(color);
222 | } else {
223 | // It is string color
224 | const hexColorName: string = colorNames[color];
225 | if (hexColorName) {
226 | return hexToRgba(hexColorName);
227 | } else {
228 | throw new Error('String cannot be parsed!');
229 | }
230 | }
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/animation/interpolation/index.ts:
--------------------------------------------------------------------------------
1 | export { interpolateNumbers } from './interpolateNumbers';
2 |
--------------------------------------------------------------------------------
/src/animation/interpolation/interpolateNumbers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | rgbaToHex,
3 | hexToRgba,
4 | COLOR_NUMBER_REGEX,
5 | HEX_NAME_COLOR,
6 | colorNames,
7 | } from './colors';
8 |
9 | type ExtrapolateType = 'identity' | 'extend' | 'clamp';
10 |
11 | type ExtrapolateConfig = {
12 | extrapolate?: ExtrapolateType;
13 | extrapolateLeft?: ExtrapolateType;
14 | extrapolateRight?: ExtrapolateType;
15 | };
16 |
17 | const interpolateValue = (
18 | val: number,
19 | arr: any,
20 | extrapolateLeft: ExtrapolateType,
21 | extrapolateRight: ExtrapolateType
22 | ) => {
23 | const [inputMin, inputMax, outputMin, outputMax] = arr;
24 | let result: number = val;
25 |
26 | // EXTRAPOLATE
27 | if (result < inputMin) {
28 | if (extrapolateLeft === 'identity') {
29 | return result;
30 | } else if (extrapolateLeft === 'clamp') {
31 | result = inputMin;
32 | } else if (extrapolateLeft === 'extend') {
33 | // noop
34 | }
35 | }
36 |
37 | if (result > inputMax) {
38 | if (extrapolateRight === 'identity') {
39 | return result;
40 | } else if (extrapolateRight === 'clamp') {
41 | result = inputMax;
42 | } else if (extrapolateRight === 'extend') {
43 | // noop
44 | }
45 | }
46 |
47 | if (outputMin === outputMax) {
48 | return outputMin;
49 | }
50 |
51 | if (inputMin === inputMax) {
52 | if (val <= inputMin) {
53 | return outputMin;
54 | }
55 | return outputMax;
56 | }
57 |
58 | // Input Range
59 | if (inputMin === -Infinity) {
60 | result = -result;
61 | } else if (inputMax === Infinity) {
62 | result = result - inputMin;
63 | } else {
64 | result = (result - inputMin) / (inputMax - inputMin);
65 | }
66 |
67 | // Output Range
68 | if (outputMin === -Infinity) {
69 | result = -result;
70 | } else if (outputMax === Infinity) {
71 | result = result + outputMin;
72 | } else {
73 | result = result * (outputMax - outputMin) + outputMin;
74 | }
75 |
76 | return result;
77 | };
78 |
79 | const getNarrowedInput = function (
80 | x: number,
81 | input: number[],
82 | output: Array
83 | ): Array {
84 | const length = input.length;
85 | let narrowedInput: Array = [];
86 |
87 | // Boundaries
88 | if (x < input[0]) {
89 | narrowedInput = [input[0], input[1], output[0], output[1]];
90 | } else if (x > input[length - 1]) {
91 | narrowedInput = [
92 | input[length - 2],
93 | input[length - 1],
94 | output[length - 2],
95 | output[length - 1],
96 | ];
97 | }
98 |
99 | // Narrow the input and output ranges
100 | for (let i = 1; i < length; ++i) {
101 | if (x <= input[i]) {
102 | narrowedInput = [input[i - 1], input[i], output[i - 1], output[i]];
103 | break;
104 | }
105 | }
106 |
107 | return narrowedInput;
108 | };
109 |
110 | const interpolateColor = (value: number, narrowedInput: string[]) => {
111 | const [inputMin, inputMax, outputMin, outputMax] = narrowedInput;
112 |
113 | const outputMinProcessed = hexToRgba(outputMin);
114 | const outputMaxProcessed = hexToRgba(outputMax);
115 |
116 | const red = interpolateValue(
117 | value,
118 | [inputMin, inputMax, outputMinProcessed.r, outputMaxProcessed.r],
119 | 'clamp',
120 | 'clamp'
121 | );
122 |
123 | const green = interpolateValue(
124 | value,
125 | [inputMin, inputMax, outputMinProcessed.g, outputMaxProcessed.g],
126 | 'clamp',
127 | 'clamp'
128 | );
129 |
130 | const blue = interpolateValue(
131 | value,
132 | [inputMin, inputMax, outputMinProcessed.b, outputMaxProcessed.b],
133 | 'clamp',
134 | 'clamp'
135 | );
136 |
137 | const alpha = interpolateValue(
138 | value,
139 | [inputMin, inputMax, outputMinProcessed.a, outputMaxProcessed.a],
140 | 'clamp',
141 | 'clamp'
142 | );
143 |
144 | return rgbaToHex({ r: red, g: green, b: blue, a: alpha });
145 | };
146 |
147 | const _getArrayInterpolate = (
148 | value: number,
149 | narrowedInput: Array,
150 | _extrapolateLeft: ExtrapolateType,
151 | _extrapolateRight: ExtrapolateType
152 | ) => {
153 | const [inputMin, inputMax, outputMin, outputMax] = narrowedInput;
154 |
155 | if (outputMin.length === outputMax.length) {
156 | return outputMin.map((val: any, index: number) => {
157 | if (typeof val === 'string') {
158 | // IF IT IS STRING THEN IT MUST BE HEX COLOR
159 | return interpolateColor(value, [
160 | inputMin,
161 | inputMax,
162 | val,
163 | outputMax[index],
164 | ]);
165 | } else {
166 | return interpolateValue(
167 | value,
168 | [inputMin, inputMax, val, outputMax[index]],
169 | _extrapolateLeft,
170 | _extrapolateRight
171 | );
172 | }
173 | });
174 | } else {
175 | throw new Error("Array length doesn't match");
176 | }
177 | };
178 |
179 | const getTemplateString = (str: string) => {
180 | return str.replace(COLOR_NUMBER_REGEX, '$');
181 | };
182 |
183 | const _getParsedStringArray = (str: any) => {
184 | return str.match(COLOR_NUMBER_REGEX).map((v: string) => {
185 | if (v.indexOf('#') !== -1) {
186 | return v;
187 | } else {
188 | return Number(v);
189 | }
190 | });
191 | };
192 |
193 | const stringMatched = (str1: string, str2: string) =>
194 | getTemplateString(str1).trim().replace(/\s/g, '') ===
195 | getTemplateString(str2).trim().replace(/\s/g, '');
196 |
197 | /**
198 | * Function which proccess the
199 | * hexadecimal colors to its proper formats
200 | * @param str - string
201 | * @returns hex color string
202 | */
203 | const getProcessedColor = (str: string) => {
204 | return str.replace(HEX_NAME_COLOR, function (match: any) {
205 | if (match.indexOf('#') !== -1) {
206 | return rgbaToHex(hexToRgba(match));
207 | } else if (Object.prototype.hasOwnProperty.call(colorNames, match)) {
208 | return colorNames[match];
209 | } else {
210 | throw new Error('String cannot be parsed!');
211 | }
212 | });
213 | };
214 |
215 | export function interpolateNumbers(
216 | value: number,
217 | inputRange: Array,
218 | outputRange: Array,
219 | extrapolateConfig?: ExtrapolateConfig
220 | ) {
221 | const extrapolate = extrapolateConfig?.extrapolate;
222 | const extrapolateLeft = extrapolateConfig?.extrapolateLeft;
223 | const extrapolateRight = extrapolateConfig?.extrapolateRight;
224 |
225 | const narrowedInput = getNarrowedInput(value, inputRange, outputRange);
226 |
227 | let _extrapolateLeft: ExtrapolateType = 'extend';
228 | if (extrapolateLeft !== undefined) {
229 | _extrapolateLeft = extrapolateLeft;
230 | } else if (extrapolate !== undefined) {
231 | _extrapolateLeft = extrapolate;
232 | }
233 |
234 | let _extrapolateRight: ExtrapolateType = 'extend';
235 | if (extrapolateRight !== undefined) {
236 | _extrapolateRight = extrapolateRight;
237 | } else if (extrapolate !== undefined) {
238 | _extrapolateRight = extrapolate;
239 | }
240 |
241 | if (outputRange.length) {
242 | if (typeof outputRange[0] === 'number') {
243 | return interpolateValue(
244 | value,
245 | narrowedInput,
246 | _extrapolateLeft,
247 | _extrapolateRight
248 | );
249 | } else if (Array.isArray(outputRange[0])) {
250 | return _getArrayInterpolate(
251 | value,
252 | narrowedInput,
253 | _extrapolateLeft,
254 | _extrapolateRight
255 | );
256 | } else {
257 | const [inputMin, inputMax, outputMin, outputMax] = narrowedInput;
258 |
259 | const processedOutputMin = getProcessedColor(outputMin as string);
260 | const processedOutputMax = getProcessedColor(outputMax as string);
261 |
262 | let templateString = getTemplateString(processedOutputMin);
263 |
264 | if (stringMatched(processedOutputMin, processedOutputMax)) {
265 | const outputMinParsed = _getParsedStringArray(processedOutputMin);
266 | const outputMaxParsed = _getParsedStringArray(processedOutputMax);
267 |
268 | const result = _getArrayInterpolate(
269 | value,
270 | [inputMin, inputMax, outputMinParsed, outputMaxParsed],
271 | _extrapolateLeft,
272 | _extrapolateRight
273 | );
274 |
275 | for (const v of result) templateString = templateString.replace('$', v);
276 | return templateString;
277 | } else {
278 | throw new Error("Output range doesn't match string format!");
279 | }
280 | }
281 | } else {
282 | throw new Error('Output range cannot be Empty');
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/src/animation/types.ts:
--------------------------------------------------------------------------------
1 | export type Primitive = number | string;
2 |
3 | export interface WithCallbacks {
4 | onStart?: (value: number | string) => void;
5 | onChange?: (value: number | string) => void;
6 | onRest?: (value: number | string) => void;
7 | }
8 |
9 | export type DriverConfig = {
10 | type: 'spring' | 'timing' | 'decay' | 'sequence' | 'delay' | 'loop';
11 | to?: Primitive;
12 | options?: {
13 | controller?: DriverConfig;
14 | iterations?: number;
15 | delay?: number;
16 | duration?: number;
17 | easing?: (t: number) => number;
18 | stiffness?: number;
19 | damping?: number;
20 | mass?: number;
21 | velocity?: number;
22 | clamp?: [number, number];
23 | steps?: DriverConfig[];
24 | onStart?: () => void;
25 | onChange?: (v: string | number) => void;
26 | onComplete?: () => void;
27 | };
28 | };
29 |
30 | export type ToValue = DriverConfig | V;
31 |
--------------------------------------------------------------------------------
/src/gestures/controllers/DragGesture.ts:
--------------------------------------------------------------------------------
1 | import { attachEvents } from '../helpers/eventAttacher';
2 | import { clamp } from '../helpers/math';
3 | import { withDefault } from '../helpers/withDefault';
4 | import { Gesture } from './Gesture';
5 |
6 | import type { Vector2 } from '../types';
7 |
8 | export class DragGesture extends Gesture {
9 | movementStart: Vector2 = withDefault(0, 0);
10 | initialMovement: Vector2 = withDefault(0, 0);
11 | movement: Vector2 = withDefault(0, 0);
12 | previousMovement: Vector2 = withDefault(0, 0);
13 | translation: Vector2 = withDefault(0, 0);
14 | offset: Vector2 = withDefault(0, 0);
15 | velocity: Vector2 = withDefault(0, 0);
16 |
17 | // @override
18 | // initialize the events
19 | _initEvents() {
20 | if (this.targetElement || this.targetElements.length > 0) {
21 | this._subscribe = attachEvents(
22 | [window],
23 | [
24 | ['mousedown', this.pointerDown.bind(this)],
25 | ['mousemove', this.pointerMove.bind(this)],
26 | ['mouseup', this.pointerUp.bind(this)],
27 | ['touchstart', this.pointerDown.bind(this), { passive: false }],
28 | ['touchmove', this.pointerMove.bind(this), { passive: false }],
29 | ['touchend', this.pointerUp.bind(this)],
30 | ]
31 | );
32 | }
33 | }
34 |
35 | // @override - cancel events
36 | // we only canceled down and move events because mouse up
37 | // will not be triggered
38 | _cancelEvents() {
39 | if (this._subscribe) {
40 | this._subscribe(['mousedown', 'mousemove', 'touchstart', 'touchmove']);
41 | }
42 | }
43 |
44 | _handleCallback() {
45 | if (this.callback) {
46 | this.callback({
47 | args: [this.currentIndex],
48 | down: this.isActive,
49 | movementX: this.movement.x,
50 | movementY: this.movement.y,
51 | offsetX: this.translation.x,
52 | offsetY: this.translation.y,
53 | velocityX: this.velocity.x,
54 | velocityY: this.velocity.y,
55 | distanceX: Math.abs(this.movement.x),
56 | distanceY: Math.abs(this.movement.y),
57 | directionX: Math.sign(this.movement.x),
58 | directionY: Math.sign(this.movement.y),
59 | cancel: () => {
60 | this._cancelEvents();
61 | },
62 | });
63 | }
64 | }
65 |
66 | pointerDown(e: any) {
67 | if (e.type === 'touchstart') {
68 | this.movementStart = {
69 | x: e.touches[0].clientX,
70 | y: e.touches[0].clientY,
71 | };
72 | } else {
73 | this.movementStart = { x: e.clientX, y: e.clientY };
74 | }
75 |
76 | this.movement = { x: 0, y: 0 };
77 | this.offset = { x: this.translation.x, y: this.translation.y };
78 | this.previousMovement = { x: 0, y: 0 };
79 | this.velocity = { x: 0, y: 0 };
80 |
81 | // find current selected element
82 | const currElem = this.targetElements.find((elem: any) => elem === e.target);
83 |
84 | if (e.target === this.targetElement || currElem) {
85 | this.isActive = true;
86 | e.preventDefault();
87 |
88 | // set args
89 | if (currElem) {
90 | this.currentIndex = this.targetElements.indexOf(currElem);
91 | }
92 |
93 | // if initial function is defined then call it to get initial movementX and movementY
94 | // if only select to bounded draggable element
95 | const initial = this.config?.initial && this.config.initial();
96 | const initialMovementX = initial?.movementX;
97 | const initialMovementY = initial?.movementY;
98 |
99 | this.initialMovement = {
100 | x: initialMovementX ?? 0,
101 | y: initialMovementY ?? 0,
102 | };
103 |
104 | this.movement = {
105 | x: this.initialMovement.x,
106 | y: this.initialMovement.y,
107 | };
108 |
109 | this.previousMovement = {
110 | x: this.initialMovement.x,
111 | y: this.initialMovement.y,
112 | };
113 |
114 | this._handleCallback();
115 | }
116 | }
117 |
118 | pointerMove(e: any) {
119 | if (this.isActive) {
120 | e.preventDefault();
121 | const now = Date.now();
122 | const deltaTime = clamp(now - this.lastTimeStamp, 0.1, 64);
123 | this.lastTimeStamp = now;
124 |
125 | const t = deltaTime / 1000;
126 |
127 | if (e.type === 'touchmove') {
128 | this.movement = {
129 | x:
130 | this.initialMovement.x +
131 | (e.touches[0].clientX - this.movementStart.x),
132 | y:
133 | this.initialMovement.y +
134 | (e.touches[0].clientY - this.movementStart.y),
135 | };
136 | } else {
137 | this.movement = {
138 | x: this.initialMovement.x + (e.clientX - this.movementStart.x),
139 | y: this.initialMovement.y + (e.clientY - this.movementStart.y),
140 | };
141 | }
142 |
143 | this.translation = {
144 | x: this.offset.x + this.movement.x,
145 | y: this.offset.y + this.movement.y,
146 | };
147 |
148 | this.velocity = {
149 | x: clamp(
150 | (this.movement.x - this.previousMovement.x) / t / 1000,
151 | -1 * Gesture._VELOCITY_LIMIT,
152 | Gesture._VELOCITY_LIMIT
153 | ),
154 | y: clamp(
155 | (this.movement.y - this.previousMovement.y) / t / 1000,
156 | -1 * Gesture._VELOCITY_LIMIT,
157 | Gesture._VELOCITY_LIMIT
158 | ),
159 | };
160 |
161 | this.previousMovement = {
162 | x: this.movement.x,
163 | y: this.movement.y,
164 | };
165 |
166 | this._handleCallback();
167 | }
168 | }
169 |
170 | pointerUp() {
171 | if (this.isActive) {
172 | this.isActive = false;
173 | this._handleCallback();
174 | this._cancelEvents();
175 | this._initEvents();
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/gestures/controllers/Gesture.ts:
--------------------------------------------------------------------------------
1 | export class Gesture {
2 | currentIndex?: number;
3 | lastTimeStamp: number = Date.now();
4 | isActive: boolean = false;
5 | targetElement?: HTMLElement; // represents the bounded element
6 | targetElements: Array = []; // represents the bounded elements
7 | config?: any;
8 | callback?: (event: T) => void;
9 | _subscribe?: (eventKeys?: Array) => void;
10 | static _VELOCITY_LIMIT: number = 20;
11 |
12 | // it must be overridden by other child classes
13 | _initEvents() {}
14 |
15 | // cancel events
16 | // we only canceled down and move events because mouse up
17 | // will not be triggered
18 | _cancelEvents() {
19 | if (this._subscribe) {
20 | this._subscribe();
21 | }
22 | }
23 |
24 | // re-apply new callback
25 | applyCallback(callback: (event: T) => void) {
26 | this.callback = callback;
27 | }
28 |
29 | // apply gesture
30 | applyGesture({
31 | targetElement,
32 | targetElements,
33 | callback,
34 | config,
35 | }: {
36 | targetElement?: any;
37 | targetElements?: any;
38 | callback: (event: T) => void;
39 | config?: any;
40 | }) {
41 | this.targetElement = targetElement;
42 | this.targetElements = targetElements.map(
43 | (element: { current: any }) => element.current
44 | );
45 | this.callback = callback;
46 | this.config = config;
47 |
48 | // initialize events
49 | this._initEvents();
50 |
51 | // unbind
52 | return () => this._subscribe && this._subscribe();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/gestures/controllers/MouseMoveGesture.ts:
--------------------------------------------------------------------------------
1 | import { attachEvents } from '../helpers/eventAttacher';
2 | import { Vector2 } from '../types';
3 | import { clamp } from '../helpers/math';
4 | import { withDefault } from '../helpers/withDefault';
5 | import { Gesture } from './Gesture';
6 |
7 | export class MouseMoveGesture extends Gesture {
8 | event?: MouseEvent;
9 | isActiveID?: any;
10 | movement: Vector2 = withDefault(0, 0);
11 | previousMovement: Vector2 = withDefault(0, 0);
12 | velocity: Vector2 = withDefault(0, 0);
13 | direction: Vector2 = withDefault(0, 0);
14 |
15 | // @override
16 | // initialize the events
17 | _initEvents() {
18 | if (this.targetElement) {
19 | this._subscribe = attachEvents(
20 | [this.targetElement],
21 | [['mousemove', this.onMouseMove.bind(this)]]
22 | );
23 | } else if (this.targetElements.length > 0) {
24 | this._subscribe = attachEvents(this.targetElements, [
25 | ['mousemove', this.onMouseMove.bind(this)],
26 | ]);
27 | } else {
28 | this._subscribe = attachEvents(
29 | [window],
30 | [['mousemove', this.onMouseMove.bind(this)]]
31 | );
32 | }
33 | }
34 |
35 | _handleCallback() {
36 | if (this.callback) {
37 | this.callback({
38 | args: [this.currentIndex],
39 | event: this.event,
40 | isMoving: this.isActive,
41 | target: this.event?.target,
42 | mouseX: this.movement.x,
43 | mouseY: this.movement.y,
44 | velocityX: this.velocity.x,
45 | velocityY: this.velocity.y,
46 | directionX: this.direction.x,
47 | directionY: this.direction.y,
48 | });
49 | }
50 | }
51 |
52 | onMouseMove(e: MouseEvent) {
53 | // find current selected element
54 | const currElem = this.targetElements.find((elem: any) => elem === e.target);
55 |
56 | // set args
57 | if (currElem) {
58 | this.currentIndex = this.targetElements.indexOf(currElem);
59 | }
60 |
61 | this.event = e;
62 |
63 | const now: number = Date.now();
64 | const deltaTime = Math.min(now - this.lastTimeStamp, 64);
65 | this.lastTimeStamp = now;
66 | const t = deltaTime / 1000; // seconds
67 |
68 | const x = e.clientX;
69 | const y = e.clientY;
70 |
71 | this.movement = { x, y };
72 |
73 | if (this.isActiveID !== -1) {
74 | this.isActive = true;
75 | clearTimeout(this.isActiveID);
76 | }
77 |
78 | this.isActiveID = setTimeout(() => {
79 | this.isActive = false;
80 | this.direction = { x: 0, y: 0 };
81 | this.velocity = { x: 0, y: 0 };
82 |
83 | this._handleCallback();
84 | }, 250); // Debounce 250 milliseconds
85 |
86 | const diffX = this.movement.x - this.previousMovement.x;
87 | const diffY = this.movement.y - this.previousMovement.y;
88 |
89 | this.direction = {
90 | x: Math.sign(diffX),
91 | y: Math.sign(diffY),
92 | };
93 |
94 | this.velocity = {
95 | x: clamp(
96 | diffX / t / 1000,
97 | -1 * Gesture._VELOCITY_LIMIT,
98 | Gesture._VELOCITY_LIMIT
99 | ),
100 | y: clamp(
101 | diffY / t / 1000,
102 | -1 * Gesture._VELOCITY_LIMIT,
103 | Gesture._VELOCITY_LIMIT
104 | ),
105 | };
106 |
107 | this.previousMovement = { x: this.movement.x, y: this.movement.y };
108 |
109 | this._handleCallback();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/gestures/controllers/ScrollGesture.ts:
--------------------------------------------------------------------------------
1 | import { attachEvents } from '../helpers/eventAttacher';
2 | import { Vector2 } from '../types';
3 | import { clamp } from '../helpers/math';
4 | import { withDefault } from '../helpers/withDefault';
5 | import { Gesture } from './Gesture';
6 |
7 | export class ScrollGesture extends Gesture {
8 | isActiveID?: any;
9 | movement: Vector2 = withDefault(0, 0);
10 | previousMovement: Vector2 = withDefault(0, 0);
11 | direction: Vector2 = withDefault(0, 0);
12 | velocity: Vector2 = withDefault(0, 0);
13 |
14 | // @override
15 | // initialize the events
16 | _initEvents() {
17 | if (this.targetElement) {
18 | this._subscribe = attachEvents(
19 | [this.targetElement],
20 | [['scroll', this.scrollElementListener.bind(this)]]
21 | );
22 | } else {
23 | this._subscribe = attachEvents(
24 | [window],
25 | [['scroll', this.scrollListener.bind(this)]]
26 | );
27 | }
28 | }
29 |
30 | _handleCallback() {
31 | if (this.callback) {
32 | this.callback({
33 | isScrolling: this.isActive,
34 | scrollX: this.movement.x,
35 | scrollY: this.movement.y,
36 | velocityX: this.velocity.x,
37 | velocityY: this.velocity.y,
38 | directionX: this.direction.x,
39 | directionY: this.direction.y,
40 | });
41 | }
42 | }
43 |
44 | onScroll({ x, y }: Vector2) {
45 | const now: number = Date.now();
46 | const deltaTime = Math.min(now - this.lastTimeStamp, 64);
47 | this.lastTimeStamp = now;
48 | const t = deltaTime / 1000; // seconds
49 |
50 | this.movement = { x, y };
51 |
52 | // Clear if scrolling
53 | if (this.isActiveID !== -1) {
54 | this.isActive = true;
55 | clearTimeout(this.isActiveID);
56 | }
57 |
58 | this.isActiveID = setTimeout(() => {
59 | this.isActive = false;
60 | this.direction = { x: 0, y: 0 };
61 |
62 | // Reset Velocity
63 | this.velocity = { x: 0, y: 0 };
64 |
65 | this._handleCallback(); // Debounce 250milliseconds
66 | }, 250);
67 |
68 | const diffX = this.movement.x - this.previousMovement.x;
69 | const diffY = this.movement.y - this.previousMovement.y;
70 |
71 | this.direction = {
72 | x: Math.sign(diffX),
73 | y: Math.sign(diffY),
74 | };
75 |
76 | this.velocity = {
77 | x: clamp(
78 | diffX / t / 1000,
79 | -1 * Gesture._VELOCITY_LIMIT,
80 | Gesture._VELOCITY_LIMIT
81 | ),
82 | y: clamp(
83 | diffY / t / 1000,
84 | -1 * Gesture._VELOCITY_LIMIT,
85 | Gesture._VELOCITY_LIMIT
86 | ),
87 | };
88 |
89 | this.previousMovement = {
90 | x: this.movement.x,
91 | y: this.movement.y,
92 | };
93 |
94 | this._handleCallback();
95 | }
96 |
97 | scrollListener() {
98 | const { pageYOffset: y, pageXOffset: x } = window;
99 | this.onScroll({ x, y });
100 | }
101 |
102 | scrollElementListener() {
103 | const x = this.targetElement?.scrollLeft || 0;
104 | const y = this.targetElement?.scrollTop || 0;
105 | this.onScroll({ x, y });
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/gestures/controllers/WheelGesture.ts:
--------------------------------------------------------------------------------
1 | import { attachEvents } from '../helpers/eventAttacher';
2 | import { Vector2 } from '../types';
3 | import { clamp } from '../helpers/math';
4 | import { withDefault } from '../helpers/withDefault';
5 | import { Gesture } from './Gesture';
6 |
7 | const LINE_HEIGHT = 40;
8 | const PAGE_HEIGHT = 800;
9 |
10 | export class WheelGesture extends Gesture {
11 | isActiveID?: any;
12 | movement: Vector2 = withDefault(0, 0);
13 | previousMovement: Vector2 = withDefault(0, 0);
14 | direction: Vector2 = withDefault(0, 0);
15 | velocity: Vector2 = withDefault(0, 0);
16 | delta: Vector2 = withDefault(0, 0);
17 |
18 | // Holds offsets
19 | offset: Vector2 = withDefault(0, 0);
20 | translation: Vector2 = withDefault(0, 0);
21 |
22 | // @override
23 | // initialize the events
24 | _initEvents() {
25 | if (this.targetElement) {
26 | this._subscribe = attachEvents(
27 | [this.targetElement],
28 | [['wheel', this.onWheel.bind(this)]]
29 | );
30 | }
31 | }
32 |
33 | _handleCallback() {
34 | if (this.callback) {
35 | this.callback({
36 | target: this.targetElement,
37 | isWheeling: this.isActive,
38 | deltaX: this.delta.x,
39 | deltaY: this.delta.y,
40 | directionX: this.direction.x,
41 | directionY: this.direction.y,
42 | movementX: this.movement.x,
43 | movementY: this.movement.y,
44 | offsetX: this.offset.x,
45 | offsetY: this.offset.y,
46 | velocityX: this.velocity.x,
47 | velocityY: this.velocity.y,
48 | });
49 | }
50 | }
51 |
52 | onWheel(event: WheelEvent) {
53 | let { deltaX, deltaY, deltaMode } = event;
54 |
55 | const now: number = Date.now();
56 | const deltaTime = Math.min(now - this.lastTimeStamp, 64);
57 | this.lastTimeStamp = now;
58 | const t = deltaTime / 1000; // seconds
59 |
60 | this.isActive = true;
61 |
62 | if (this.isActiveID !== -1) {
63 | this.isActive = true;
64 | clearTimeout(this.isActiveID);
65 | }
66 |
67 | this.isActiveID = setTimeout(() => {
68 | this.isActive = false;
69 | this.translation = { x: this.offset.x, y: this.offset.y };
70 | this._handleCallback();
71 |
72 | this.velocity = { x: 0, y: 0 }; // Reset Velocity
73 | this.movement = { x: 0, y: 0 };
74 | }, 200);
75 |
76 | // normalize wheel values, especially for Firefox
77 | if (deltaMode === 1) {
78 | deltaX *= LINE_HEIGHT;
79 | deltaY *= LINE_HEIGHT;
80 | } else if (deltaMode === 2) {
81 | deltaX *= PAGE_HEIGHT;
82 | deltaY *= PAGE_HEIGHT;
83 | }
84 |
85 | this.delta = { x: deltaX, y: deltaY };
86 | this.movement = {
87 | x: this.movement.x + deltaX,
88 | y: this.movement.y + deltaY,
89 | };
90 | this.offset = {
91 | x: this.translation.x + this.movement.x,
92 | y: this.translation.y + this.movement.y,
93 | };
94 |
95 | const diffX = this.movement.x - this.previousMovement.x;
96 | const diffY = this.movement.y - this.previousMovement.y;
97 |
98 | this.direction = {
99 | x: Math.sign(diffX),
100 | y: Math.sign(diffY),
101 | };
102 |
103 | this.velocity = {
104 | x: clamp(
105 | diffX / t / 1000,
106 | -1 * Gesture._VELOCITY_LIMIT,
107 | Gesture._VELOCITY_LIMIT
108 | ),
109 | y: clamp(
110 | diffY / t / 1000,
111 | -1 * Gesture._VELOCITY_LIMIT,
112 | Gesture._VELOCITY_LIMIT
113 | ),
114 | };
115 |
116 | this.previousMovement = {
117 | x: this.movement.x,
118 | y: this.movement.y,
119 | };
120 |
121 | this._handleCallback();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/gestures/controllers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './DragGesture';
2 | export * from './MouseMoveGesture';
3 | export * from './ScrollGesture';
4 | export * from './WheelGesture';
5 |
--------------------------------------------------------------------------------
/src/gestures/helpers/eventAttacher.ts:
--------------------------------------------------------------------------------
1 | type MouseEventType =
2 | | 'click'
3 | | 'dblclick'
4 | | 'mousedown'
5 | | 'mousemove'
6 | | 'mouseup'
7 | | 'touchstart'
8 | | 'touchmove'
9 | | 'touchend'
10 | | 'mouseenter'
11 | | 'mouseleave'
12 | | 'mouseout'
13 | | 'mouseover'
14 | | 'scroll'
15 | | 'wheel'
16 | | 'contextmenu';
17 |
18 | type DomTargetTypes = Array;
19 |
20 | /**
21 | * Attach single document / window event / HTMLElement
22 | */
23 | function attachEvent(
24 | domTargets: DomTargetTypes,
25 | event: MouseEventType,
26 | callback: (e: any) => void,
27 | capture: any = false
28 | ) {
29 | domTargets.forEach((target) => {
30 | target.addEventListener(event, callback, capture);
31 | });
32 |
33 | return function () {
34 | domTargets.forEach((target) => {
35 | target.removeEventListener(event, callback, capture);
36 | });
37 | };
38 | }
39 |
40 | /**
41 | * Attach multiple document / window event / HTMLElement
42 | */
43 | export function attachEvents(
44 | domTargets: DomTargetTypes,
45 | events: Array<
46 | [event: MouseEventType, callback: (e: any) => void, capture?: any]
47 | >
48 | ) {
49 | const subscribers = new Map();
50 |
51 | events.forEach(function ([event, callback, capture = false]) {
52 | subscribers.set(event, attachEvent(domTargets, event, callback, capture));
53 | });
54 |
55 | return function (eventKeys?: Array) {
56 | for (const [eventKey, subscriber] of subscribers.entries()) {
57 | if (!eventKeys) {
58 | subscriber();
59 | return;
60 | }
61 |
62 | if (eventKeys.indexOf(eventKey) !== -1) {
63 | subscriber();
64 | }
65 | }
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/src/gestures/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './math';
2 |
--------------------------------------------------------------------------------
/src/gestures/helpers/math.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * bin(booleanValue)
3 | * returns 1 if booleanValue == true and 0 if booleanValue == false
4 | */
5 | export function bin(bool: boolean) {
6 | return bool ? 1 : 0;
7 | }
8 |
9 | /**
10 | * mix(progress, a, b)
11 | * linear interpolation between a and b
12 | */
13 | export function mix(perc: number, val1: number, val2: number) {
14 | return val1 * (1 - perc) + val2 * perc;
15 | }
16 |
17 | /**
18 | * clamp(value, min, max)
19 | * clamps value for min and max bounds
20 | */
21 | export function clamp(value: number, lowerbound: number, upperbound: number) {
22 | return Math.min(Math.max(value, lowerbound), upperbound);
23 | }
24 |
25 | function rubber2(distanceFromEdge: number, constant: number) {
26 | return Math.pow(distanceFromEdge, constant * 5);
27 | }
28 |
29 | function rubber(distanceFromEdge: number, dimension: number, constant: number) {
30 | if (dimension === 0 || Math.abs(dimension) === Infinity)
31 | return rubber2(distanceFromEdge, constant);
32 | return (
33 | (distanceFromEdge * dimension * constant) /
34 | (dimension + constant * distanceFromEdge)
35 | );
36 | }
37 |
38 | /**
39 | * rubberClamp(value, min, max, constant?)
40 | * constant is optional : default 0.15
41 | * clamps the value for min and max value and
42 | * extends beyond min and max values with constant
43 | * factor to create elastic rubber band effect
44 | */
45 | export function rubberClamp(
46 | value: number,
47 | lowerbound: number,
48 | upperbound: number,
49 | constant: number = 0.15
50 | ) {
51 | if (constant === 0) return clamp(value, lowerbound, upperbound);
52 |
53 | if (value < lowerbound) {
54 | return (
55 | -rubber(lowerbound - value, upperbound - lowerbound, constant) +
56 | lowerbound
57 | );
58 | }
59 |
60 | if (value > upperbound) {
61 | return (
62 | +rubber(value - upperbound, upperbound - lowerbound, constant) +
63 | upperbound
64 | );
65 | }
66 |
67 | return value;
68 | }
69 |
70 | /**
71 | * snapTo(value, velocity, snapPoints[])
72 | * Calculates the final snapPoint according to given current value,
73 | * velocity and snapPoints array
74 | */
75 | export function snapTo(
76 | value: number,
77 | velocity: number,
78 | snapPoints: Array
79 | ): number {
80 | const finalValue = value + velocity * 0.2;
81 | const getDiff = (point: number) => Math.abs(point - finalValue);
82 | const deltas = snapPoints.map(getDiff);
83 | const minDelta = Math.min(...deltas);
84 |
85 | return snapPoints.reduce(function (acc, point) {
86 | if (getDiff(point) === minDelta) {
87 | return point;
88 | } else {
89 | return acc;
90 | }
91 | });
92 | }
93 |
94 | /**
95 | * move(array, moveIndex, toIndex)
96 | * move array item from moveIndex to toIndex without array modification
97 | */
98 | export function move(array: Array, moveIndex: number, toIndex: number) {
99 | const item = array[moveIndex];
100 | const length = array.length;
101 | const diff = moveIndex - toIndex;
102 |
103 | if (diff > 0) {
104 | return [
105 | ...array.slice(0, toIndex),
106 | item,
107 | ...array.slice(toIndex, moveIndex),
108 | ...array.slice(moveIndex + 1, length),
109 | ];
110 | } else if (diff < 0) {
111 | const targetIndex = toIndex + 1;
112 | return [
113 | ...array.slice(0, moveIndex),
114 | ...array.slice(moveIndex + 1, targetIndex),
115 | item,
116 | ...array.slice(targetIndex, length),
117 | ];
118 | }
119 | return array;
120 | }
121 |
--------------------------------------------------------------------------------
/src/gestures/helpers/withDefault.ts:
--------------------------------------------------------------------------------
1 | export const withDefault = (x: number, y: number) => {
2 | return { x, y };
3 | };
4 |
--------------------------------------------------------------------------------
/src/gestures/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDrag';
2 | export * from './useMouseMove';
3 | export * from './useScroll';
4 | export * from './useWheel';
5 | export * from './useGesture';
6 |
--------------------------------------------------------------------------------
/src/gestures/hooks/useDrag.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { DragEventType, UseDragConfig } from '../types';
4 | import { DragGesture } from '../controllers';
5 | import { useRecognizer } from './useRecognizer';
6 |
7 | export function useDrag(
8 | callback: (event: DragEventType) => void,
9 | config?: UseDragConfig
10 | ) {
11 | const gesture = React.useRef(new DragGesture()).current;
12 |
13 | return useRecognizer([['drag', gesture, callback, config]]);
14 | }
15 |
--------------------------------------------------------------------------------
/src/gestures/hooks/useGesture.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | DragGesture,
4 | MouseMoveGesture,
5 | ScrollGesture,
6 | WheelGesture,
7 | } from '../controllers';
8 | import {
9 | DragEventType,
10 | WheelEventType,
11 | ScrollEventType,
12 | MouseMoveEventType,
13 | } from '../types';
14 | import { useRecognizer } from './useRecognizer';
15 |
16 | export function useGesture({
17 | onDrag,
18 | onWheel,
19 | onScroll,
20 | onMouseMove,
21 | }: {
22 | onDrag?: (event: DragEventType) => void;
23 | onWheel?: (event: WheelEventType) => void;
24 | onScroll?: (event: ScrollEventType) => void;
25 | onMouseMove?: (event: MouseMoveEventType) => void;
26 | }) {
27 | const dragGesture = React.useRef(new DragGesture()).current;
28 | const wheelGesture = React.useRef(new WheelGesture()).current;
29 | const scrollGesture = React.useRef(new ScrollGesture()).current;
30 | const mouseMoveGesture = React.useRef(new MouseMoveGesture()).current;
31 |
32 | return useRecognizer([
33 | ['drag', dragGesture, onDrag],
34 | ['wheel', wheelGesture, onWheel],
35 | ['scroll', scrollGesture, onScroll],
36 | ['move', mouseMoveGesture, onMouseMove],
37 | ]);
38 | }
39 |
--------------------------------------------------------------------------------
/src/gestures/hooks/useMouseMove.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { MouseMoveEventType } from '../types';
4 | import { MouseMoveGesture } from '../controllers';
5 | import { useRecognizer } from './useRecognizer';
6 |
7 | export function useMouseMove(callback: (event: MouseMoveEventType) => void) {
8 | const gesture = React.useRef(new MouseMoveGesture()).current;
9 |
10 | return useRecognizer([['move', gesture, callback]]);
11 | }
12 |
--------------------------------------------------------------------------------
/src/gestures/hooks/useRecognizer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | import * as React from 'react';
3 |
4 | type UseRecognizerHandlerType = Array<
5 | [
6 | key: 'drag' | 'wheel' | 'move' | 'scroll',
7 | gesture: any,
8 | callback: any,
9 | config?: any
10 | ]
11 | >;
12 |
13 | export const useRecognizer = (handlers: UseRecognizerHandlerType) => {
14 | const ref = React.useRef();
15 | const elementRefs = React.useRef>([]);
16 | const subscribers = React.useRef<
17 | Map
18 | >(new Map()).current;
19 |
20 | // re-initiate callback on change
21 | React.useEffect(() => {
22 | for (let [, { keyIndex, gesture }] of subscribers.entries()) {
23 | const [, , callback] = handlers[keyIndex];
24 | gesture.applyCallback(callback);
25 | }
26 | }, [handlers]);
27 |
28 | React.useEffect(() => {
29 | handlers.forEach(([key, gesture, callback, config], keyIndex) => {
30 | queueMicrotask(() =>
31 | subscribers.set(key, {
32 | keyIndex,
33 | gesture,
34 | unsubscribe: gesture.applyGesture({
35 | targetElement: ref.current,
36 | targetElements: elementRefs.current,
37 | callback,
38 | config,
39 | }),
40 | })
41 | );
42 | });
43 |
44 | return () => {
45 | for (let [, { unsubscribe }] of subscribers.entries()) {
46 | unsubscribe && unsubscribe();
47 | }
48 | };
49 | });
50 |
51 | return (index?: number) => {
52 | if (index === null || index === undefined) {
53 | return { ref };
54 | } else {
55 | elementRefs.current[index] =
56 | elementRefs.current[index] || React.createRef();
57 |
58 | return { ref: elementRefs.current[index] };
59 | }
60 | };
61 | };
62 |
--------------------------------------------------------------------------------
/src/gestures/hooks/useScroll.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ScrollEventType } from '../types';
4 | import { ScrollGesture } from '../controllers';
5 | import { useRecognizer } from './useRecognizer';
6 |
7 | export function useScroll(callback: (event: ScrollEventType) => void) {
8 | const gesture = React.useRef(new ScrollGesture()).current;
9 |
10 | return useRecognizer([['scroll', gesture, callback]]);
11 | }
12 |
--------------------------------------------------------------------------------
/src/gestures/hooks/useWheel.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { WheelEventType } from '../types';
4 | import { WheelGesture } from '../controllers';
5 | import { useRecognizer } from './useRecognizer';
6 |
7 | export function useWheel(callback: (event: WheelEventType) => void) {
8 | const gesture = React.useRef(new WheelGesture()).current;
9 |
10 | return useRecognizer([['wheel', gesture, callback]]);
11 | }
12 |
--------------------------------------------------------------------------------
/src/gestures/types/index.ts:
--------------------------------------------------------------------------------
1 | type GenericEventType = {
2 | velocityX: number;
3 | velocityY: number;
4 | directionX: number;
5 | directionY: number;
6 | };
7 |
8 | export type DragEventType = {
9 | args: Array;
10 | down: boolean;
11 | movementX: number;
12 | movementY: number;
13 | offsetX: number;
14 | offsetY: number;
15 | distanceX: number;
16 | distanceY: number;
17 | cancel: () => void;
18 | } & GenericEventType;
19 |
20 | export type MouseMoveEventType = {
21 | args: Array;
22 | event: MouseEvent;
23 | target: EventTarget | undefined | null;
24 | isMoving: boolean;
25 | mouseX: number;
26 | mouseY: number;
27 | } & GenericEventType;
28 |
29 | export type ScrollEventType = {
30 | isScrolling: boolean;
31 | scrollX: number;
32 | scrollY: number;
33 | } & GenericEventType;
34 |
35 | export type WheelEventType = {
36 | target: HTMLElement | undefined | null;
37 | isWheeling: boolean;
38 | movementX: number;
39 | movementY: number;
40 | offsetX: number;
41 | offsetY: number;
42 | deltaX: number;
43 | deltaY: number;
44 | } & GenericEventType;
45 |
46 | export type UseDragConfig = {
47 | initial?: () => { movementX: number; movementY: number };
48 | };
49 | export type Vector2 = { x: number; y: number };
50 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useOutsideClick";
2 | export * from "./useMeasure";
3 | export * from "./useWindowDimension";
4 |
--------------------------------------------------------------------------------
/src/hooks/useMeasure.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, DependencyList, createRef } from 'react';
2 |
3 | type MeasurementValue = number | Array;
4 |
5 | type MeasurementType = {
6 | left: MeasurementValue;
7 | top: MeasurementValue;
8 | width: MeasurementValue;
9 | height: MeasurementValue;
10 | vLeft: MeasurementValue;
11 | vTop: MeasurementValue;
12 | };
13 |
14 | export function useMeasure(
15 | callback: (event: MeasurementType) => void,
16 | deps?: DependencyList
17 | ) {
18 | const ref = useRef(null);
19 | const elementRefs = useRef([]);
20 | const callbackRef = useRef<(event: MeasurementType) => void>(callback);
21 |
22 | // Re-initiate callback when dependency change
23 | useEffect(() => {
24 | callbackRef.current = callback;
25 |
26 | return () => {
27 | callbackRef.current = () => false;
28 | };
29 | }, deps);
30 |
31 | useEffect(() => {
32 | const _refElement = ref.current || document.documentElement;
33 | const _refElementsMultiple = elementRefs.current;
34 |
35 | const resizeObserver = new ResizeObserver(([entry]) => {
36 | const { left, top, width, height } = entry.target.getBoundingClientRect();
37 | const { pageXOffset, pageYOffset } = window;
38 |
39 | if (callbackRef) {
40 | if (_refElement === document.documentElement) {
41 | return; // no-op for document
42 | } else {
43 | callbackRef.current({
44 | left: left + pageXOffset,
45 | top: top + pageYOffset,
46 | width,
47 | height,
48 | vLeft: left,
49 | vTop: top,
50 | });
51 | }
52 | }
53 | });
54 |
55 | const resizeObserverMultiple = new ResizeObserver((entries) => {
56 | const left: Array = [];
57 | const top: Array = [];
58 | const width: Array = [];
59 | const height: Array = [];
60 | const vLeft: Array = [];
61 | const vTop: Array = [];
62 |
63 | entries.forEach((entry) => {
64 | const {
65 | left: _left,
66 | top: _top,
67 | width: _width,
68 | height: _height,
69 | } = entry.target.getBoundingClientRect();
70 | const { pageXOffset, pageYOffset } = window;
71 | const _pageLeft = _left + pageXOffset;
72 | const _pageTop = _top + pageYOffset;
73 |
74 | left.push(_pageLeft);
75 | top.push(_pageTop);
76 | width.push(_width);
77 | height.push(_height);
78 | vLeft.push(_left);
79 | vTop.push(_top);
80 | });
81 |
82 | if (callbackRef) {
83 | callbackRef.current({
84 | left,
85 | top,
86 | width,
87 | height,
88 | vLeft,
89 | vTop,
90 | });
91 | }
92 | });
93 |
94 | if (_refElement) {
95 | if (
96 | _refElement === document.documentElement &&
97 | _refElementsMultiple.length > 0
98 | ) {
99 | _refElementsMultiple.forEach((element: any) => {
100 | resizeObserverMultiple.observe(element.current);
101 | });
102 | } else {
103 | resizeObserver.observe(_refElement);
104 | }
105 | }
106 |
107 | return () => {
108 | if (_refElement) {
109 | if (
110 | _refElement === document.documentElement &&
111 | _refElementsMultiple.length > 0
112 | ) {
113 | _refElementsMultiple.forEach((element: any) => {
114 | resizeObserverMultiple.unobserve(element.current);
115 | });
116 | } else {
117 | resizeObserver.unobserve(_refElement);
118 | }
119 | }
120 | };
121 | }, []);
122 |
123 | return (index?: number) => {
124 | if (index === null || index === undefined) {
125 | return { ref };
126 | } else {
127 | elementRefs.current[index] = elementRefs.current[index] || createRef();
128 |
129 | return { ref: elementRefs.current[index] };
130 | }
131 | }; // ...bind() or ...bind(index) for multiple
132 | }
133 |
--------------------------------------------------------------------------------
/src/hooks/useOutsideClick.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, RefObject, DependencyList } from 'react';
2 |
3 | import { attachEvents } from '../gestures/helpers/eventAttacher';
4 |
5 | export function useOutsideClick(
6 | elementRef: RefObject,
7 | callback: (event: MouseEvent) => void,
8 | deps?: DependencyList
9 | ) {
10 | const callbackRef = useRef<(event: MouseEvent) => void>();
11 |
12 | if (!callbackRef.current) {
13 | callbackRef.current = callback;
14 | }
15 |
16 | // Re-initiate callback when dependency change
17 | useEffect(() => {
18 | callbackRef.current = callback;
19 |
20 | return () => {
21 | callbackRef.current = () => false;
22 | };
23 | }, deps);
24 |
25 | useEffect(() => {
26 | const handleOutsideClick = (e: MouseEvent) => {
27 | const target = e.target as Node;
28 |
29 | if (!target || !target.isConnected) {
30 | return;
31 | }
32 |
33 | const isOutside =
34 | elementRef.current && !elementRef.current.contains(target);
35 |
36 | if (isOutside) {
37 | callbackRef.current && callbackRef.current(e);
38 | }
39 | };
40 |
41 | const subscribe = attachEvents(
42 | [document],
43 | [['mousedown', handleOutsideClick]]
44 | );
45 |
46 | return () => subscribe && subscribe();
47 | }, []);
48 | }
49 |
--------------------------------------------------------------------------------
/src/hooks/useWindowDimension.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, DependencyList } from 'react';
2 |
3 | type WindowDimensionType = {
4 | width: number;
5 | height: number;
6 | innerWidth: number;
7 | innerHeight: number;
8 | };
9 |
10 | export function useWindowDimension(
11 | callback: (event: WindowDimensionType) => void,
12 | deps?: DependencyList
13 | ) {
14 | const windowDimensionsRef = useRef({
15 | width: 0,
16 | height: 0,
17 | innerWidth: 0,
18 | innerHeight: 0,
19 | });
20 | const callbackRef = useRef<(event: WindowDimensionType) => void>(callback);
21 |
22 | const handleCallback: () => void = () => {
23 | if (callbackRef) {
24 | callbackRef.current({
25 | ...windowDimensionsRef.current,
26 | });
27 | }
28 | };
29 |
30 | // Re-initiate callback when dependency change
31 | useEffect(() => {
32 | callbackRef.current = callback;
33 |
34 | return () => {
35 | callbackRef.current = () => false;
36 | };
37 | }, deps);
38 |
39 | useEffect(() => {
40 | const resizeObserver = new ResizeObserver(([entry]) => {
41 | const { clientWidth, clientHeight } = entry.target;
42 | const { innerWidth, innerHeight } = window;
43 |
44 | windowDimensionsRef.current = {
45 | width: clientWidth,
46 | height: clientHeight,
47 | innerWidth,
48 | innerHeight,
49 | };
50 |
51 | handleCallback();
52 | });
53 |
54 | resizeObserver.observe(document.documentElement);
55 |
56 | return () => resizeObserver.unobserve(document.documentElement);
57 | }, []);
58 | }
59 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | Easing,
3 | makeMotion as makeAnimated,
4 | motion as animate,
5 | combine,
6 | } from '@raidipesh78/re-motion';
7 | export {
8 | withSpring,
9 | withTiming,
10 | withSequence,
11 | withDelay,
12 | withDecay,
13 | withLoop,
14 | withEase,
15 | useValue,
16 | useMount,
17 | useAnimatedList,
18 | AnimationConfig,
19 | interpolateNumbers,
20 | } from './animation';
21 | export { useMeasure, useOutsideClick, useWindowDimension } from './hooks';
22 | export {
23 | useDrag,
24 | useGesture,
25 | useMouseMove,
26 | useScroll,
27 | useWheel,
28 | } from './gestures/hooks';
29 | export { bin, clamp, mix, rubberClamp, move, snapTo } from './gestures/helpers';
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "target": "es5",
6 | "lib": ["es6", "dom", "es2016", "es2017", "es2019"],
7 | "downlevelIteration": true,
8 | "sourceMap": true,
9 | "allowJs": false,
10 | "jsx": "react-jsx",
11 | "declaration": true,
12 | "moduleResolution": "node",
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "types": ["resize-observer-browser", "node", "@types/jest"]
21 | },
22 | "include": ["src"],
23 | "exclude": ["node_modules", "dist", "example", "rollup.config.js"]
24 | }
25 |
--------------------------------------------------------------------------------