├── .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 | [![npm version](https://badge.fury.io/js/react-ui-animate.svg)](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 | 22 | 23 | 24 | 34 | 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 |
19 |
20 |
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 | --------------------------------------------------------------------------------