├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── check.yml │ └── demo.yml ├── .gitignore ├── .size-limit.json ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── demo.gif ├── docs ├── API.md └── interfaces │ ├── AnimationFunctionHandle.md │ ├── AnimationFunctionOptions.md │ ├── AnimationHandle.md │ ├── AnimationOptions.md │ ├── BaseAnimationHandle.md │ ├── ScrollTimelineOpts.md │ ├── TimelineDefinition.md │ ├── TransitionAnimationHandle.md │ ├── TransitionAnimationOptions.md │ ├── TransitionGroupProps.md │ ├── TypedKeyframeEffectOptions.md │ └── ViewTimelineOpts.md ├── images ├── demo-chart.gif └── demo-scroll.gif ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── @types │ └── index.d.ts ├── core │ ├── index.ts │ ├── utils.spec.ts │ ├── utils.ts │ ├── waapi.spec.ts │ └── waapi.ts ├── index.ts └── react │ ├── components │ ├── TransitionGroup.tsx │ └── index.ts │ ├── hooks │ ├── __snapshots__ │ │ ├── useAnimation.ssr.spec.tsx.snap │ │ └── useAnimationFunction.ssr.spec.tsx.snap │ ├── index.ts │ ├── state.ts │ ├── useAnimation.ssr.spec.tsx │ ├── useAnimation.ts │ ├── useAnimationFunction.ssr.spec.tsx │ ├── useAnimationFunction.ts │ ├── useIsomorphicLayoutEffect.ts │ ├── useLatestRef.ts │ ├── useScrollTimeline.ts │ ├── useStatic.ts │ ├── useTransitionAnimation.ts │ └── useViewTimeline.ts │ ├── index.ts │ └── types │ ├── index.ts │ └── internal.ts ├── stories ├── hooks │ ├── useAnimation.stories.tsx │ ├── useAnimationFunction.stories.tsx │ ├── useScrollTimeline.stories.tsx │ ├── useTransitionAnimation.stories.tsx │ └── useViewTimeline.stories.tsx └── with-libraries │ ├── css-in-js │ ├── emotion.stories.tsx │ ├── linaria.stories.tsx │ ├── styled-components.stories.tsx │ ├── vanilla-extract.css.ts │ └── vanilla-extract.stories.tsx │ └── ui-components │ ├── Ant Design.stories.tsx │ ├── Chakra UI.stories.tsx │ ├── Fluent UI.stories.tsx │ ├── Material UI.stories.tsx │ └── mantine.stories.tsx ├── tsconfig.json ├── typedoc.json └── vitest.config.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Platform:** 20 | - OS: [Windows, MacOS, Linux, Android, iOS] 21 | - Browser: [Chrome, Firefox, Safari, Edge] 22 | - Version of this package: [x.x.x] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | setup: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: "lts/*" 22 | cache: "npm" 23 | 24 | - run: npm ci 25 | - run: npm run tsc 26 | - run: npm run test 27 | - run: npm run build 28 | - run: npm run size 29 | # - run: npm run storybook:test 30 | -------------------------------------------------------------------------------- /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | name: demo 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: "lts/*" 21 | cache: "npm" 22 | 23 | - run: npm ci 24 | - run: npm run storybook:build 25 | 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./storybook-static 31 | publish_branch: demo 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "useAnimation", 4 | "path": "lib/index.mjs", 5 | "import": "{ useAnimation }", 6 | "limit": "1.0 kB" 7 | }, 8 | { 9 | "name": "useAnimationFunction", 10 | "path": "lib/index.mjs", 11 | "import": "{ useAnimationFunction }", 12 | "limit": "1.0 kB" 13 | }, 14 | { 15 | "name": "useScrollTimeline", 16 | "path": "lib/index.mjs", 17 | "import": "{ useScrollTimeline }", 18 | "limit": "0.3 kB" 19 | }, 20 | { 21 | "name": "useViewTimeline", 22 | "path": "lib/index.mjs", 23 | "import": "{ useViewTimeline }", 24 | "limit": "0.3 kB" 25 | }, 26 | { 27 | "name": "useTransitionAnimation", 28 | "path": "lib/index.mjs", 29 | "import": "{ useTransitionAnimation }", 30 | "limit": "1.3 kB" 31 | }, 32 | { 33 | "name": "TransitionGroup", 34 | "path": "lib/index.mjs", 35 | "import": "{ TransitionGroup }", 36 | "limit": "0.5 kB" 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const { VanillaExtractPlugin } = require("@vanilla-extract/webpack-plugin"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | /** @type { import('@storybook/react-webpack5').StorybookConfig } */ 5 | export default { 6 | stories: ["../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"], 7 | addons: ["@storybook/addon-storysource", "@storybook/addon-webpack5-compiler-swc"], 8 | framework: { 9 | name: "@storybook/react-webpack5", 10 | options: { builder: {} }, 11 | }, 12 | refs: { 13 | "@chakra-ui/react": { 14 | disable: true, 15 | }, 16 | }, 17 | webpackFinal: async (config) => { 18 | // for vanilla-extract 19 | config.plugins.push(new VanillaExtractPlugin(), new MiniCssExtractPlugin()); 20 | config.module.rules.push({ 21 | test: /\.vanilla-extract\.css$/i, 22 | use: [ 23 | MiniCssExtractPlugin.loader, 24 | { 25 | loader: "css-loader", 26 | options: { 27 | url: false, 28 | }, 29 | }, 30 | ], 31 | }); 32 | // for linaria 33 | config.module.rules.push({ 34 | test: /\/linaria\.stories.tsx$/, 35 | use: [ 36 | { 37 | loader: "@linaria/webpack-loader", 38 | options: { 39 | sourceMap: false, 40 | babelOptions: { 41 | presets: [ 42 | "@babel/preset-typescript", 43 | "@babel/preset-react", 44 | "@linaria/babel-preset", 45 | ], 46 | }, 47 | }, 48 | }, 49 | ], 50 | }); 51 | return config; 52 | }, 53 | swc: (config) => { 54 | return { 55 | ...config, 56 | jsc: { 57 | ...config.jsc, 58 | transform: { 59 | ...config.jsc?.tranform, 60 | react: { ...config.jsc?.tranform?.react, runtime: "automatic" }, 61 | }, 62 | }, 63 | }; 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // // For testing web-animations-js 2 | // window.Animation = undefined; 3 | // window.KeyframeEffect = undefined; 4 | // Element.prototype.animate = undefined; 5 | // import("web-animations-js"); 6 | 7 | /** @type { import('@storybook/react').Preview } */ 8 | export default { 9 | parameters: { 10 | actions: { argTypesRegex: "^on[A-Z].*" }, 11 | controls: { 12 | matchers: { 13 | color: /(background|color)$/i, 14 | date: /Date$/, 15 | }, 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 inokawa 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-animatable 2 | 3 | ![npm](https://img.shields.io/npm/v/react-animatable) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-animatable) [![check](https://github.com/inokawa/react-animatable/actions/workflows/check.yml/badge.svg)](https://github.com/inokawa/react-animatable/actions/workflows/check.yml) [![demo](https://github.com/inokawa/react-animatable/actions/workflows/demo.yml/badge.svg)](https://github.com/inokawa/react-animatable/actions/workflows/demo.yml) 4 | 5 | Tiny(~1kB) animation hooks for [React](https://github.com/facebook/react), built on [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). 6 | 7 | 8 | 9 | ## Features 10 | 11 | - Easy integration with a few lines of code. Works with any UI component libraries or CSS-in-JS libraries. 12 | - Performant animation driven by native [Web Animations API (WAAPI)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). Also supporting [ScrollTimeline](https://developer.mozilla.org/en-US/docs/Web/API/ScrollTimeline) and [ViewTimeline](https://developer.mozilla.org/en-US/docs/Web/API/ViewTimeline). 13 | - Fully typed with [TypeScript](https://www.typescriptlang.org/), which is stricter than TypeScript's [lib.dom.d.ts](https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts). 14 | - Tiny. Each hooks are ~1kB gzipped and [the total is ~2kB gzipped](https://bundlephobia.com/package/react-animatable). 15 | - HTML, SVG, Canvas and anything can be animated. 16 | 17 | ## Motivation 18 | 19 | Animating something in React can be complicated than we expected, even with today's popular libraries. [Web Animations API (WAAPI)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) looks like a brand-new promising way, because it's performant, it doesn't need JS runtime, it doesn't mutate style of DOM so it will not conflict with React's state, and it will become more convenient in the future ([ScrollTimeline](https://developer.mozilla.org/en-US/docs/Web/API/ScrollTimeline) and [ViewTimeline](https://developer.mozilla.org/en-US/docs/Web/API/ViewTimeline) is an example). However using raw WAAPI with React is bit tricky and having risks of memory leak. This library is what to solve the problem. 20 | 21 | ## Demo 22 | 23 | https://inokawa.github.io/react-animatable/ 24 | 25 | ## Install 26 | 27 | ```sh 28 | npm install react-animatable 29 | ``` 30 | 31 | ### Requirements 32 | 33 | - react >= 16.14 34 | 35 | If you use ESM and webpack 5, use react >= 18 to avoid [Can't resolve `react/jsx-runtime` error](https://github.com/facebook/react/issues/20235). 36 | 37 | And in some legacy browsers that does not support Web Animations API, [you may need to use polyfill](#use-polyfill). 38 | 39 | If you use [ScrollTimeline](https://caniuse.com/?search=scrolltimeline) or [ViewTimeline](https://caniuse.com/?search=viewtimeline), check browser support. 40 | 41 | ## Usage 42 | 43 | 1. Define your animation with `useAnimation` hook. 44 | 45 | > The hooks accepts [canonical keyframe format objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats#syntax) and [KeyframeEffect's options](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect#parameters) as arguments, so check them before using this library. 46 | 47 | 2. Pass the return value of `useAnimation` to `ref` of element you want to control. 48 | 49 | 3. Call `play()`! 50 | 51 | ```tsx 52 | import { useEffect } from "react"; 53 | import { useAnimation } from "react-animatable"; 54 | 55 | export const App = () => { 56 | // 1. Define your animation in WAAPI way 57 | const animate = useAnimation( 58 | [{ transform: "rotate(0deg)" }, { transform: "rotate(720deg)" }], 59 | { 60 | duration: 1000, 61 | easing: "ease-in-out", 62 | } 63 | ); 64 | 65 | return ( 66 | 76 | ); 77 | }; 78 | ``` 79 | 80 | ### Dynamic keyframe 81 | 82 | Use `prev` and `args` for dynamic keyframe generation. 83 | 84 | ```tsx 85 | import { useEffect } from "react"; 86 | import { useAnimation } from "react-animatable"; 87 | 88 | export const App = () => { 89 | // Define argument type 90 | const animate = useAnimation<{ x: number; y: number }>( 91 | (prev, args) => [ 92 | // You can get current style from 1st argument 93 | { transform: prev.transform }, 94 | // Get passed position from 2nd argument 95 | { transform: `translate(${args.x}px, ${args.y}px)` }, 96 | ], 97 | { 98 | duration: 400, 99 | easing: "ease-in-out", 100 | } 101 | ); 102 | 103 | useEffect(() => { 104 | // If you click somewhere, the circle follows you! 105 | 106 | const onClick = (e: MouseEvent) => { 107 | // Pass mouse position when animate 108 | animate.play({ args: { x: e.clientX, y: e.clientY } }); 109 | }; 110 | window.addEventListener("click", onClick); 111 | return () => { 112 | window.removeEventListener("click", onClick); 113 | }; 114 | }, []); 115 | 116 | return ( 117 |
129 | ); 130 | }; 131 | ``` 132 | 133 | ### Animation without CSS 134 | 135 | Use `useAnimationFunction` for JS only animation. 136 | 137 | ```tsx 138 | import { useState } from "react"; 139 | import { useAnimationFunction } from "react-animatable"; 140 | 141 | export const App = () => { 142 | const [value, setValue] = useState(0); 143 | const animate = useAnimationFunction( 144 | ({ progress }, arg) => { 145 | // Do anything here! 146 | setValue(progress * arg); 147 | }, 148 | { 149 | duration: 600, 150 | easing: "ease-in-out", 151 | } 152 | ); 153 | useEffect(() => { 154 | animate.play({ args: 100 }); 155 | }, []); 156 | 157 | return ; 158 | }; 159 | ``` 160 | 161 | And see [examples](./stories) for more usages. 162 | 163 | ## Documentation 164 | 165 | - [API reference](./docs/API.md) 166 | 167 | ## Use polyfill 168 | 169 | 1. [browsers that have KeyframeEffect](https://caniuse.com/mdn-api_keyframeeffect) 170 | 1. [browsers that have Element.animate()](https://caniuse.com/mdn-api_element_animate) 171 | 1. browsers that have no Web Animations APIs 172 | 173 | In 1, you can use all functions of this library without polyfill. Some of the newer features like [composite mode](https://caniuse.com/web-animation) and [CSS Motion Path](https://caniuse.com/css-motion-paths) may be ignored in some browsers though. 174 | 175 | In 2, you can use this library but `useAnimationFuction` would not work. 176 | 177 | In 3, you have to setup [Web Animations API polyfill](https://github.com/web-animations/web-animations-js) to use this library. 178 | 179 | ### Setup web-animations-js 180 | 181 | ```sh 182 | npm install web-animations-js 183 | ``` 184 | 185 | ```js 186 | // You can polyfill always 187 | import "web-animations-js"; 188 | ReactDOM.render(); 189 | 190 | // or polyfill only if browser does not support Web Animations API 191 | (async () => { 192 | if (!("animate" in document.body)) { 193 | await import("web-animations-js"); 194 | } 195 | ReactDOM.render(); 196 | })(); 197 | ``` 198 | 199 | #### `Partial keyframes are not supported` error was thrown 200 | 201 | web-animations-js does not support partial keyframes, so you have to write animation definitions like below. 202 | 203 | https://github.com/PolymerElements/paper-ripple/issues/28#issuecomment-266945027 204 | 205 | ```jsx 206 | // valid 207 | const animate = useAnimation( 208 | [ 209 | { transform: "translate3d(0px, 0, 0)" }, 210 | { transform: "translate3d(400px, 0, 0)" }, 211 | ], 212 | { duration: 800, easing: "ease-in-out" } 213 | ); 214 | // invalid 215 | const animate = useAnimation( 216 | { transform: "translate3d(400px, 0, 0)" }, 217 | { duration: 800, easing: "ease-in-out" } 218 | ); 219 | 220 | // valid 221 | const animate = useAnimation( 222 | [ 223 | { transform: "translateX(0px)", fill: "blue" }, 224 | { transform: "translateX(100px)", fill: "red" }, 225 | { transform: "translateX(0px)", fill: "blue" }, 226 | ], 227 | { duration: 800, easing: "ease-in-out" } 228 | ); 229 | // invalid 230 | const animate = useAnimation( 231 | [ 232 | { transform: "translateX(0px)" }, 233 | { transform: "translateX(100px)", fill: "red" }, 234 | { fill: "blue" }, 235 | ], 236 | { duration: 800, easing: "ease-in-out" } 237 | ); 238 | ``` 239 | 240 | ## Contribute 241 | 242 | All contributions are welcome. 243 | If you find a problem, feel free to create an [issue](https://github.com/inokawa/react-animatable/issues) or a [PR](https://github.com/inokawa/react-animatable/pulls). 244 | 245 | ### Making a Pull Request 246 | 247 | 1. Fork this repo. 248 | 2. Run `npm install`. 249 | 3. Commit your fix. 250 | 4. Make a PR and confirm all the CI checks passed. 251 | 252 | ## My previous experiments (deprecated) 253 | 254 | - [tweened](https://github.com/inokawa/tweened) 255 | - [react-use-d3](https://github.com/inokawa/react-use-d3) 256 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inokawa/react-animatable/67622b5b57c1721c8aaed8317e9d4a1e7fe1b459/demo.gif -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Table of contents 4 | 5 | ### Functions 6 | 7 | - [TransitionGroup](API.md#transitiongroup) 8 | - [useAnimation](API.md#useanimation) 9 | - [useAnimationFunction](API.md#useanimationfunction) 10 | - [useTransitionAnimation](API.md#usetransitionanimation) 11 | - [useScrollTimeline](API.md#usescrolltimeline) 12 | - [useViewTimeline](API.md#useviewtimeline) 13 | 14 | ### Interfaces 15 | 16 | - [TypedKeyframeEffectOptions](interfaces/TypedKeyframeEffectOptions.md) 17 | - [TransitionGroupProps](interfaces/TransitionGroupProps.md) 18 | - [BaseAnimationHandle](interfaces/BaseAnimationHandle.md) 19 | - [AnimationHandle](interfaces/AnimationHandle.md) 20 | - [AnimationFunctionHandle](interfaces/AnimationFunctionHandle.md) 21 | - [AnimationFunctionOptions](interfaces/AnimationFunctionOptions.md) 22 | - [TransitionAnimationHandle](interfaces/TransitionAnimationHandle.md) 23 | - [TransitionAnimationOptions](interfaces/TransitionAnimationOptions.md) 24 | - [ScrollTimelineOpts](interfaces/ScrollTimelineOpts.md) 25 | - [ViewTimelineOpts](interfaces/ViewTimelineOpts.md) 26 | - [AnimationOptions](interfaces/AnimationOptions.md) 27 | - [TimelineDefinition](interfaces/TimelineDefinition.md) 28 | 29 | ### Type Aliases 30 | 31 | - [TypedKeyframe](API.md#typedkeyframe) 32 | - [TypedEasing](API.md#typedeasing) 33 | - [GetKeyframeFunction](API.md#getkeyframefunction) 34 | - [AnimatableCSSProperties](API.md#animatablecssproperties) 35 | - [PlayOptions](API.md#playoptions) 36 | - [WaitingAnimationEventName](API.md#waitinganimationeventname) 37 | - [PlayOptionsWithArgs](API.md#playoptionswithargs) 38 | - [ComputedTimingContext](API.md#computedtimingcontext) 39 | - [AnimationFunction](API.md#animationfunction) 40 | - [TransitionAnimationDefinition](API.md#transitionanimationdefinition) 41 | - [AnimationDefinition](API.md#animationdefinition) 42 | - [ScrollTimelineAxis](API.md#scrolltimelineaxis) 43 | - [ViewTimelineInset](API.md#viewtimelineinset) 44 | 45 | ## Functions 46 | 47 | ### TransitionGroup 48 | 49 | ▸ **TransitionGroup**(`«destructured»`): `ReactElement`<`any`, `string` \| `JSXElementConstructor`<`any`\>\> 50 | 51 | A component to manage enter/update/exit of its children by key, that works similar to [TransitionGroup of react-transition-group](https://reactcommunity.org/react-transition-group/transition-group). 52 | 53 | #### Parameters 54 | 55 | | Name | Type | 56 | | :------ | :------ | 57 | | `«destructured»` | [`TransitionGroupProps`](interfaces/TransitionGroupProps.md) | 58 | 59 | #### Returns 60 | 61 | `ReactElement`<`any`, `string` \| `JSXElementConstructor`<`any`\>\> 62 | 63 | #### Defined in 64 | 65 | [src/react/components/TransitionGroup.tsx:81](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/components/TransitionGroup.tsx#L81) 66 | 67 | ___ 68 | 69 | ### useAnimation 70 | 71 | ▸ **useAnimation**<`Args`\>(`...args`): [`AnimationHandle`](interfaces/AnimationHandle.md)<`Args`\> 72 | 73 | A basic hook to use Web Animations API. See [AnimationHandle](interfaces/AnimationHandle.md). 74 | 75 | #### Type parameters 76 | 77 | | Name | Type | Description | 78 | | :------ | :------ | :------ | 79 | | `Args` | `void` | argument type | 80 | 81 | #### Parameters 82 | 83 | | Name | Type | 84 | | :------ | :------ | 85 | | `...args` | [`AnimationDefinition`](API.md#animationdefinition)<`Args`\> | 86 | 87 | #### Returns 88 | 89 | [`AnimationHandle`](interfaces/AnimationHandle.md)<`Args`\> 90 | 91 | #### Defined in 92 | 93 | [src/react/hooks/useAnimation.ts:104](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L104) 94 | 95 | ___ 96 | 97 | ### useAnimationFunction 98 | 99 | ▸ **useAnimationFunction**<`Args`\>(`onUpdate`, `options?`): [`AnimationFunctionHandle`](interfaces/AnimationFunctionHandle.md)<`Args`\> 100 | 101 | Same as [useAnimation](API.md#useanimation), but it drives function not React element. See [AnimationFunctionHandle](interfaces/AnimationFunctionHandle.md). 102 | 103 | #### Type parameters 104 | 105 | | Name | Type | Description | 106 | | :------ | :------ | :------ | 107 | | `Args` | `void` | argument type | 108 | 109 | #### Parameters 110 | 111 | | Name | Type | 112 | | :------ | :------ | 113 | | `onUpdate` | [`AnimationFunction`](API.md#animationfunction)<`Args`\> | 114 | | `options?` | [`AnimationFunctionOptions`](interfaces/AnimationFunctionOptions.md) | 115 | 116 | #### Returns 117 | 118 | [`AnimationFunctionHandle`](interfaces/AnimationFunctionHandle.md)<`Args`\> 119 | 120 | #### Defined in 121 | 122 | [src/react/hooks/useAnimationFunction.ts:69](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimationFunction.ts#L69) 123 | 124 | ___ 125 | 126 | ### useTransitionAnimation 127 | 128 | ▸ **useTransitionAnimation**(`keyframes`): [`TransitionAnimationHandle`](interfaces/TransitionAnimationHandle.md) 129 | 130 | A hook to compose multiple [useAnimation](API.md#useanimation) and plays them when element enter/update/exits. 131 | This hook must be used under [TransitionGroup](API.md#transitiongroup) component. 132 | 133 | #### Parameters 134 | 135 | | Name | Type | 136 | | :------ | :------ | 137 | | `keyframes` | `Object` | 138 | | `keyframes.enter?` | [`TransitionAnimationDefinition`](API.md#transitionanimationdefinition) | 139 | | `keyframes.update?` | [`TransitionAnimationDefinition`](API.md#transitionanimationdefinition) | 140 | | `keyframes.exit?` | [`TransitionAnimationDefinition`](API.md#transitionanimationdefinition) | 141 | 142 | #### Returns 143 | 144 | [`TransitionAnimationHandle`](interfaces/TransitionAnimationHandle.md) 145 | 146 | #### Defined in 147 | 148 | [src/react/hooks/useTransitionAnimation.ts:38](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useTransitionAnimation.ts#L38) 149 | 150 | ___ 151 | 152 | ### useScrollTimeline 153 | 154 | ▸ **useScrollTimeline**(`opts?`): [`TimelineDefinition`](interfaces/TimelineDefinition.md) 155 | 156 | #### Parameters 157 | 158 | | Name | Type | 159 | | :------ | :------ | 160 | | `opts` | [`ScrollTimelineOpts`](interfaces/ScrollTimelineOpts.md) | 161 | 162 | #### Returns 163 | 164 | [`TimelineDefinition`](interfaces/TimelineDefinition.md) 165 | 166 | #### Defined in 167 | 168 | [src/react/hooks/useScrollTimeline.ts:11](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useScrollTimeline.ts#L11) 169 | 170 | ___ 171 | 172 | ### useViewTimeline 173 | 174 | ▸ **useViewTimeline**(`opts?`): [`TimelineDefinition`](interfaces/TimelineDefinition.md) 175 | 176 | #### Parameters 177 | 178 | | Name | Type | 179 | | :------ | :------ | 180 | | `opts` | [`ViewTimelineOpts`](interfaces/ViewTimelineOpts.md) | 181 | 182 | #### Returns 183 | 184 | [`TimelineDefinition`](interfaces/TimelineDefinition.md) 185 | 186 | #### Defined in 187 | 188 | [src/react/hooks/useViewTimeline.ts:16](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useViewTimeline.ts#L16) 189 | 190 | ## Type Aliases 191 | 192 | ### TypedKeyframe 193 | 194 | Ƭ **TypedKeyframe**: `Pick`<`Keyframe`, ``"composite"`` \| ``"easing"`` \| ``"offset"``\> & { `[key: `--${string}`]`: `string` \| `number`; } & [`AnimatableCSSProperties`](API.md#animatablecssproperties) 195 | 196 | Strictly typed [Keyframe](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats) 197 | 198 | #### Defined in 199 | 200 | [src/core/waapi.ts:17](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L17) 201 | 202 | ___ 203 | 204 | ### TypedEasing 205 | 206 | Ƭ **TypedEasing**: `Exclude`<`CSSProperties`[``"animationTimingFunction"``], `CSSProperties`[``"all"``] \| `undefined`\> 207 | 208 | #### Defined in 209 | 210 | [src/core/waapi.ts:22](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L22) 211 | 212 | ___ 213 | 214 | ### GetKeyframeFunction 215 | 216 | Ƭ **GetKeyframeFunction**<`Args`\>: `Args` extends `void` ? (`prev`: `CSSStyleDeclaration`) => [`TypedKeyframe`](API.md#typedkeyframe)[] : (`prev`: `CSSStyleDeclaration`, `args`: `Args`) => [`TypedKeyframe`](API.md#typedkeyframe)[] 217 | 218 | A function to define keyframe dynamically 219 | - `prev`: current style 220 | - `args`: any argument passed from play 221 | 222 | #### Type parameters 223 | 224 | | Name | Type | 225 | | :------ | :------ | 226 | | `Args` | `void` | 227 | 228 | #### Defined in 229 | 230 | [src/core/waapi.ts:32](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L32) 231 | 232 | ___ 233 | 234 | ### AnimatableCSSProperties 235 | 236 | Ƭ **AnimatableCSSProperties**: `Omit`<`CSSProperties`, ``"offset"`` \| ``"float"``\> & { `cssOffset?`: `CSSProperties`[``"offset"``] ; `cssFloat?`: `CSSProperties`[``"float"``] ; `d?`: `string` } 237 | 238 | #### Defined in 239 | 240 | [src/core/waapi.ts:4](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L4) 241 | 242 | ___ 243 | 244 | ### PlayOptions 245 | 246 | Ƭ **PlayOptions**: `Object` 247 | 248 | #### Type declaration 249 | 250 | | Name | Type | Description | 251 | | :------ | :------ | :------ | 252 | | `restart?` | `boolean` | If true, plays from the start. It's similar to GSAP's `restart()`. | 253 | 254 | #### Defined in 255 | 256 | [src/core/waapi.ts:85](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L85) 257 | 258 | ___ 259 | 260 | ### WaitingAnimationEventName 261 | 262 | Ƭ **WaitingAnimationEventName**: ``"finish"`` \| ``"reverseFinish"`` 263 | 264 | #### Defined in 265 | 266 | [src/core/waapi.ts:185](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L185) 267 | 268 | ___ 269 | 270 | ### PlayOptionsWithArgs 271 | 272 | Ƭ **PlayOptionsWithArgs**<`Args`\>: [`PlayOptions`](API.md#playoptions) & { `args`: `Args` } 273 | 274 | #### Type parameters 275 | 276 | | Name | Type | 277 | | :------ | :------ | 278 | | `Args` | `void` | 279 | 280 | #### Defined in 281 | 282 | [src/react/hooks/useAnimation.ts:38](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L38) 283 | 284 | ___ 285 | 286 | ### ComputedTimingContext 287 | 288 | Ƭ **ComputedTimingContext**: `Required`<{ [key in keyof ComputedEffectTiming]: NonNullable }\> 289 | 290 | Non nullable [ComputedEffectTiming](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming) 291 | 292 | #### Defined in 293 | 294 | [src/react/hooks/useAnimationFunction.ts:32](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimationFunction.ts#L32) 295 | 296 | ___ 297 | 298 | ### AnimationFunction 299 | 300 | Ƭ **AnimationFunction**<`Args`\>: `Args` extends `void` ? (`ctx`: [`ComputedTimingContext`](API.md#computedtimingcontext)) => `void` : (`ctx`: [`ComputedTimingContext`](API.md#computedtimingcontext), `args`: `Args`) => `void` 301 | 302 | An argument of [useAnimationFunction](API.md#useanimationfunction). 303 | In this callback you can update any state or ref in JS. 304 | - `ctx`: current animation state 305 | - `args`: any argument passed from play 306 | 307 | #### Type parameters 308 | 309 | | Name | Type | 310 | | :------ | :------ | 311 | | `Args` | `void` | 312 | 313 | #### Defined in 314 | 315 | [src/react/hooks/useAnimationFunction.ts:42](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimationFunction.ts#L42) 316 | 317 | ___ 318 | 319 | ### TransitionAnimationDefinition 320 | 321 | Ƭ **TransitionAnimationDefinition**: [keyframe: TypedKeyframe \| TypedKeyframe[] \| GetKeyframeFunction, options?: TransitionAnimationOptions] 322 | 323 | #### Defined in 324 | 325 | [src/react/hooks/useTransitionAnimation.ts:28](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useTransitionAnimation.ts#L28) 326 | 327 | ___ 328 | 329 | ### AnimationDefinition 330 | 331 | Ƭ **AnimationDefinition**<`Args`\>: [keyframe: TypedKeyframe \| TypedKeyframe[] \| GetKeyframeFunction, options?: AnimationOptions] 332 | 333 | #### Type parameters 334 | 335 | | Name | 336 | | :------ | 337 | | `Args` | 338 | 339 | #### Defined in 340 | 341 | [src/react/types/index.ts:12](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/types/index.ts#L12) 342 | 343 | ___ 344 | 345 | ### ScrollTimelineAxis 346 | 347 | Ƭ **ScrollTimelineAxis**: ``"block"`` \| ``"inline"`` \| ``"y"`` \| ``"x"`` 348 | 349 | #### Defined in 350 | 351 | [src/react/types/index.ts:29](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/types/index.ts#L29) 352 | 353 | ___ 354 | 355 | ### ViewTimelineInset 356 | 357 | Ƭ **ViewTimelineInset**: ``"auto"`` \| `string` & {} 358 | 359 | #### Defined in 360 | 361 | [src/react/types/index.ts:31](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/types/index.ts#L31) 362 | -------------------------------------------------------------------------------- /docs/interfaces/AnimationFunctionHandle.md: -------------------------------------------------------------------------------- 1 | # Interface: AnimationFunctionHandle 2 | 3 | Handle of [useAnimationFunction](../API.md#useanimationfunction). 4 | 5 | ## Type parameters 6 | 7 | | Name | Type | Description | 8 | | :------ | :------ | :------ | 9 | | `Args` | `void` | argument type | 10 | 11 | ## Hierarchy 12 | 13 | - [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 14 | 15 | ↳ **`AnimationFunctionHandle`** 16 | 17 | ## Table of contents 18 | 19 | ### Methods 20 | 21 | - [play](AnimationFunctionHandle.md#play) 22 | - [reverse](AnimationFunctionHandle.md#reverse) 23 | - [cancel](AnimationFunctionHandle.md#cancel) 24 | - [finish](AnimationFunctionHandle.md#finish) 25 | - [pause](AnimationFunctionHandle.md#pause) 26 | - [setTime](AnimationFunctionHandle.md#settime) 27 | - [setPlaybackRate](AnimationFunctionHandle.md#setplaybackrate) 28 | - [waitFor](AnimationFunctionHandle.md#waitfor) 29 | 30 | ## Methods 31 | 32 | ### play 33 | 34 | ▸ **play**(`...opts`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 35 | 36 | A wrapper of Web Animations API's [play](https://developer.mozilla.org/en-US/docs/Web/API/Animation/play). It's similar to GSAP's `play()`. 37 | 38 | #### Parameters 39 | 40 | | Name | Type | 41 | | :------ | :------ | 42 | | `...opts` | `Args` extends `void` ? [PlayOptions?] : [[`PlayOptionsWithArgs`](../API.md#playoptionswithargs)<`Args`\>] | 43 | 44 | #### Returns 45 | 46 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 47 | 48 | #### Inherited from 49 | 50 | [BaseAnimationHandle](BaseAnimationHandle.md).[play](BaseAnimationHandle.md#play) 51 | 52 | #### Defined in 53 | 54 | [src/react/hooks/useAnimation.ts:44](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L44) 55 | 56 | ___ 57 | 58 | ### reverse 59 | 60 | ▸ **reverse**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 61 | 62 | A wrapper of Web Animations API's [reverse](https://developer.mozilla.org/en-US/docs/Web/API/Animation/reverse). It's similar to GSAP's `reverse()`. 63 | 64 | #### Returns 65 | 66 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 67 | 68 | #### Inherited from 69 | 70 | [BaseAnimationHandle](BaseAnimationHandle.md).[reverse](BaseAnimationHandle.md#reverse) 71 | 72 | #### Defined in 73 | 74 | [src/react/hooks/useAnimation.ts:50](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L50) 75 | 76 | ___ 77 | 78 | ### cancel 79 | 80 | ▸ **cancel**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 81 | 82 | A wrapper of Web Animations API's [cancel](https://developer.mozilla.org/en-US/docs/Web/API/Animation/cancel). It's similar to GSAP's `kill()`. 83 | 84 | #### Returns 85 | 86 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 87 | 88 | #### Inherited from 89 | 90 | [BaseAnimationHandle](BaseAnimationHandle.md).[cancel](BaseAnimationHandle.md#cancel) 91 | 92 | #### Defined in 93 | 94 | [src/react/hooks/useAnimation.ts:54](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L54) 95 | 96 | ___ 97 | 98 | ### finish 99 | 100 | ▸ **finish**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 101 | 102 | A wrapper of Web Animations API's [finish](https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish). 103 | 104 | #### Returns 105 | 106 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 107 | 108 | #### Inherited from 109 | 110 | [BaseAnimationHandle](BaseAnimationHandle.md).[finish](BaseAnimationHandle.md#finish) 111 | 112 | #### Defined in 113 | 114 | [src/react/hooks/useAnimation.ts:58](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L58) 115 | 116 | ___ 117 | 118 | ### pause 119 | 120 | ▸ **pause**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 121 | 122 | A wrapper of Web Animations API's [pause](https://developer.mozilla.org/en-US/docs/Web/API/Animation/pause). It's similar to GSAP's `pause()`. 123 | 124 | #### Returns 125 | 126 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 127 | 128 | #### Inherited from 129 | 130 | [BaseAnimationHandle](BaseAnimationHandle.md).[pause](BaseAnimationHandle.md#pause) 131 | 132 | #### Defined in 133 | 134 | [src/react/hooks/useAnimation.ts:62](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L62) 135 | 136 | ___ 137 | 138 | ### setTime 139 | 140 | ▸ **setTime**(`time`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 141 | 142 | A setter of Web Animations API's [currentTime](https://developer.mozilla.org/en-US/docs/Web/API/Animation/currentTime). It's similar to GSAP's `seek()`. 143 | 144 | If you pass function, you can get [endTime](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming#return_value) from its argument. 145 | 146 | #### Parameters 147 | 148 | | Name | Type | 149 | | :------ | :------ | 150 | | `time` | `number` \| (`endTime`: `number`) => `number` | 151 | 152 | #### Returns 153 | 154 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 155 | 156 | #### Inherited from 157 | 158 | [BaseAnimationHandle](BaseAnimationHandle.md).[setTime](BaseAnimationHandle.md#settime) 159 | 160 | #### Defined in 161 | 162 | [src/react/hooks/useAnimation.ts:68](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L68) 163 | 164 | ___ 165 | 166 | ### setPlaybackRate 167 | 168 | ▸ **setPlaybackRate**(`rate`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 169 | 170 | A wrapper of Web Animations API's [updatePlaybackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/updatePlaybackRate). It's similar to GSAP's `timeScale()`. 171 | 172 | If you pass function, you can get current [playbackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/playbackRate) from its argument. 173 | 174 | #### Parameters 175 | 176 | | Name | Type | 177 | | :------ | :------ | 178 | | `rate` | `number` \| (`prevRate`: `number`) => `number` | 179 | 180 | #### Returns 181 | 182 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 183 | 184 | #### Inherited from 185 | 186 | [BaseAnimationHandle](BaseAnimationHandle.md).[setPlaybackRate](BaseAnimationHandle.md#setplaybackrate) 187 | 188 | #### Defined in 189 | 190 | [src/react/hooks/useAnimation.ts:76](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L76) 191 | 192 | ___ 193 | 194 | ### waitFor 195 | 196 | ▸ **waitFor**(`event`): `Promise`<[`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\>\> 197 | 198 | A getter of Promise that will be resolved in specified timing. 199 | 200 | - `finished`: resolved when animation is finished and its playback direction is normal. 201 | - `reverseFinished`: resolved when animation is finished and its playback direction is reversed. 202 | 203 | #### Parameters 204 | 205 | | Name | Type | 206 | | :------ | :------ | 207 | | `event` | [`WaitingAnimationEventName`](../API.md#waitinganimationeventname) | 208 | 209 | #### Returns 210 | 211 | `Promise`<[`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\>\> 212 | 213 | #### Inherited from 214 | 215 | [BaseAnimationHandle](BaseAnimationHandle.md).[waitFor](BaseAnimationHandle.md#waitfor) 216 | 217 | #### Defined in 218 | 219 | [src/react/hooks/useAnimation.ts:85](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L85) 220 | -------------------------------------------------------------------------------- /docs/interfaces/AnimationFunctionOptions.md: -------------------------------------------------------------------------------- 1 | # Interface: AnimationFunctionOptions 2 | 3 | Extended [options of KeyframeEffect](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect) 4 | 5 | ## Hierarchy 6 | 7 | - [`TypedKeyframeEffectOptions`](TypedKeyframeEffectOptions.md) 8 | 9 | ↳ **`AnimationFunctionOptions`** 10 | 11 | ## Table of contents 12 | 13 | ### Properties 14 | 15 | - [easing](AnimationFunctionOptions.md#easing) 16 | - [fill](AnimationFunctionOptions.md#fill) 17 | - [direction](AnimationFunctionOptions.md#direction) 18 | - [composite](AnimationFunctionOptions.md#composite) 19 | - [iterationComposite](AnimationFunctionOptions.md#iterationcomposite) 20 | - [pseudoElement](AnimationFunctionOptions.md#pseudoelement) 21 | - [delay](AnimationFunctionOptions.md#delay) 22 | - [duration](AnimationFunctionOptions.md#duration) 23 | - [endDelay](AnimationFunctionOptions.md#enddelay) 24 | - [iterationStart](AnimationFunctionOptions.md#iterationstart) 25 | - [iterations](AnimationFunctionOptions.md#iterations) 26 | - [playbackRate](AnimationFunctionOptions.md#playbackrate) 27 | 28 | ## Properties 29 | 30 | ### easing 31 | 32 | • `Optional` **easing**: [`TypedEasing`](../API.md#typedeasing) 33 | 34 | #### Inherited from 35 | 36 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[easing](TypedKeyframeEffectOptions.md#easing) 37 | 38 | #### Defined in 39 | 40 | [src/core/waapi.ts:41](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L41) 41 | 42 | ___ 43 | 44 | ### fill 45 | 46 | • `Optional` **fill**: `FillMode` 47 | 48 | #### Inherited from 49 | 50 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[fill](TypedKeyframeEffectOptions.md#fill) 51 | 52 | #### Defined in 53 | 54 | node_modules/typescript/lib/lib.dom.d.ts:449 55 | 56 | ___ 57 | 58 | ### direction 59 | 60 | • `Optional` **direction**: `PlaybackDirection` 61 | 62 | #### Inherited from 63 | 64 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[direction](TypedKeyframeEffectOptions.md#direction) 65 | 66 | #### Defined in 67 | 68 | node_modules/typescript/lib/lib.dom.d.ts:445 69 | 70 | ___ 71 | 72 | ### composite 73 | 74 | • `Optional` **composite**: `CompositeOperation` 75 | 76 | #### Inherited from 77 | 78 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[composite](TypedKeyframeEffectOptions.md#composite) 79 | 80 | #### Defined in 81 | 82 | node_modules/typescript/lib/lib.dom.d.ts:757 83 | 84 | ___ 85 | 86 | ### iterationComposite 87 | 88 | • `Optional` **iterationComposite**: `IterationCompositeOperation` 89 | 90 | #### Inherited from 91 | 92 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterationComposite](TypedKeyframeEffectOptions.md#iterationcomposite) 93 | 94 | #### Defined in 95 | 96 | node_modules/typescript/lib/lib.dom.d.ts:758 97 | 98 | ___ 99 | 100 | ### pseudoElement 101 | 102 | • `Optional` **pseudoElement**: ``null`` \| `string` 103 | 104 | #### Inherited from 105 | 106 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[pseudoElement](TypedKeyframeEffectOptions.md#pseudoelement) 107 | 108 | #### Defined in 109 | 110 | node_modules/typescript/lib/lib.dom.d.ts:759 111 | 112 | ___ 113 | 114 | ### delay 115 | 116 | • `Optional` **delay**: `number` 117 | 118 | #### Inherited from 119 | 120 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[delay](TypedKeyframeEffectOptions.md#delay) 121 | 122 | #### Defined in 123 | 124 | node_modules/typescript/lib/lib.dom.d.ts:444 125 | 126 | ___ 127 | 128 | ### duration 129 | 130 | • `Optional` **duration**: `string` \| `number` \| `CSSNumericValue` 131 | 132 | #### Inherited from 133 | 134 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[duration](TypedKeyframeEffectOptions.md#duration) 135 | 136 | #### Defined in 137 | 138 | node_modules/typescript/lib/lib.dom.d.ts:446 139 | 140 | ___ 141 | 142 | ### endDelay 143 | 144 | • `Optional` **endDelay**: `number` 145 | 146 | #### Inherited from 147 | 148 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[endDelay](TypedKeyframeEffectOptions.md#enddelay) 149 | 150 | #### Defined in 151 | 152 | node_modules/typescript/lib/lib.dom.d.ts:448 153 | 154 | ___ 155 | 156 | ### iterationStart 157 | 158 | • `Optional` **iterationStart**: `number` 159 | 160 | #### Inherited from 161 | 162 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterationStart](TypedKeyframeEffectOptions.md#iterationstart) 163 | 164 | #### Defined in 165 | 166 | node_modules/typescript/lib/lib.dom.d.ts:450 167 | 168 | ___ 169 | 170 | ### iterations 171 | 172 | • `Optional` **iterations**: `number` 173 | 174 | #### Inherited from 175 | 176 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterations](TypedKeyframeEffectOptions.md#iterations) 177 | 178 | #### Defined in 179 | 180 | node_modules/typescript/lib/lib.dom.d.ts:451 181 | 182 | ___ 183 | 184 | ### playbackRate 185 | 186 | • `Optional` **playbackRate**: `number` 187 | 188 | #### Inherited from 189 | 190 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[playbackRate](TypedKeyframeEffectOptions.md#playbackrate) 191 | 192 | #### Defined in 193 | 194 | node_modules/typescript/lib/lib.dom.d.ts:452 195 | -------------------------------------------------------------------------------- /docs/interfaces/AnimationHandle.md: -------------------------------------------------------------------------------- 1 | # Interface: AnimationHandle 2 | 3 | Handle of [useAnimation](../API.md#useanimation). 4 | 5 | ## Type parameters 6 | 7 | | Name | Type | Description | 8 | | :------ | :------ | :------ | 9 | | `Args` | `void` | argument type | 10 | 11 | ## Hierarchy 12 | 13 | - [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 14 | 15 | ↳ **`AnimationHandle`** 16 | 17 | ## Callable 18 | 19 | ### AnimationHandle 20 | 21 | ▸ **AnimationHandle**(`ref`): `void` 22 | 23 | You have to pass this callback to ref of element you want to control. 24 | 25 | #### Parameters 26 | 27 | | Name | Type | 28 | | :------ | :------ | 29 | | `ref` | ``null`` \| `Element` | 30 | 31 | #### Returns 32 | 33 | `void` 34 | 35 | #### Defined in 36 | 37 | [src/react/hooks/useAnimation.ts:97](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L97) 38 | 39 | ## Table of contents 40 | 41 | ### Methods 42 | 43 | - [play](AnimationHandle.md#play) 44 | - [reverse](AnimationHandle.md#reverse) 45 | - [cancel](AnimationHandle.md#cancel) 46 | - [finish](AnimationHandle.md#finish) 47 | - [pause](AnimationHandle.md#pause) 48 | - [setTime](AnimationHandle.md#settime) 49 | - [setPlaybackRate](AnimationHandle.md#setplaybackrate) 50 | - [waitFor](AnimationHandle.md#waitfor) 51 | 52 | ## Methods 53 | 54 | ### play 55 | 56 | ▸ **play**(`...opts`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 57 | 58 | A wrapper of Web Animations API's [play](https://developer.mozilla.org/en-US/docs/Web/API/Animation/play). It's similar to GSAP's `play()`. 59 | 60 | #### Parameters 61 | 62 | | Name | Type | 63 | | :------ | :------ | 64 | | `...opts` | `Args` extends `void` ? [PlayOptions?] : [[`PlayOptionsWithArgs`](../API.md#playoptionswithargs)<`Args`\>] | 65 | 66 | #### Returns 67 | 68 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 69 | 70 | #### Inherited from 71 | 72 | [BaseAnimationHandle](BaseAnimationHandle.md).[play](BaseAnimationHandle.md#play) 73 | 74 | #### Defined in 75 | 76 | [src/react/hooks/useAnimation.ts:44](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L44) 77 | 78 | ___ 79 | 80 | ### reverse 81 | 82 | ▸ **reverse**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 83 | 84 | A wrapper of Web Animations API's [reverse](https://developer.mozilla.org/en-US/docs/Web/API/Animation/reverse). It's similar to GSAP's `reverse()`. 85 | 86 | #### Returns 87 | 88 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 89 | 90 | #### Inherited from 91 | 92 | [BaseAnimationHandle](BaseAnimationHandle.md).[reverse](BaseAnimationHandle.md#reverse) 93 | 94 | #### Defined in 95 | 96 | [src/react/hooks/useAnimation.ts:50](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L50) 97 | 98 | ___ 99 | 100 | ### cancel 101 | 102 | ▸ **cancel**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 103 | 104 | A wrapper of Web Animations API's [cancel](https://developer.mozilla.org/en-US/docs/Web/API/Animation/cancel). It's similar to GSAP's `kill()`. 105 | 106 | #### Returns 107 | 108 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 109 | 110 | #### Inherited from 111 | 112 | [BaseAnimationHandle](BaseAnimationHandle.md).[cancel](BaseAnimationHandle.md#cancel) 113 | 114 | #### Defined in 115 | 116 | [src/react/hooks/useAnimation.ts:54](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L54) 117 | 118 | ___ 119 | 120 | ### finish 121 | 122 | ▸ **finish**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 123 | 124 | A wrapper of Web Animations API's [finish](https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish). 125 | 126 | #### Returns 127 | 128 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 129 | 130 | #### Inherited from 131 | 132 | [BaseAnimationHandle](BaseAnimationHandle.md).[finish](BaseAnimationHandle.md#finish) 133 | 134 | #### Defined in 135 | 136 | [src/react/hooks/useAnimation.ts:58](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L58) 137 | 138 | ___ 139 | 140 | ### pause 141 | 142 | ▸ **pause**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 143 | 144 | A wrapper of Web Animations API's [pause](https://developer.mozilla.org/en-US/docs/Web/API/Animation/pause). It's similar to GSAP's `pause()`. 145 | 146 | #### Returns 147 | 148 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 149 | 150 | #### Inherited from 151 | 152 | [BaseAnimationHandle](BaseAnimationHandle.md).[pause](BaseAnimationHandle.md#pause) 153 | 154 | #### Defined in 155 | 156 | [src/react/hooks/useAnimation.ts:62](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L62) 157 | 158 | ___ 159 | 160 | ### setTime 161 | 162 | ▸ **setTime**(`time`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 163 | 164 | A setter of Web Animations API's [currentTime](https://developer.mozilla.org/en-US/docs/Web/API/Animation/currentTime). It's similar to GSAP's `seek()`. 165 | 166 | If you pass function, you can get [endTime](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming#return_value) from its argument. 167 | 168 | #### Parameters 169 | 170 | | Name | Type | 171 | | :------ | :------ | 172 | | `time` | `number` \| (`endTime`: `number`) => `number` | 173 | 174 | #### Returns 175 | 176 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 177 | 178 | #### Inherited from 179 | 180 | [BaseAnimationHandle](BaseAnimationHandle.md).[setTime](BaseAnimationHandle.md#settime) 181 | 182 | #### Defined in 183 | 184 | [src/react/hooks/useAnimation.ts:68](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L68) 185 | 186 | ___ 187 | 188 | ### setPlaybackRate 189 | 190 | ▸ **setPlaybackRate**(`rate`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 191 | 192 | A wrapper of Web Animations API's [updatePlaybackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/updatePlaybackRate). It's similar to GSAP's `timeScale()`. 193 | 194 | If you pass function, you can get current [playbackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/playbackRate) from its argument. 195 | 196 | #### Parameters 197 | 198 | | Name | Type | 199 | | :------ | :------ | 200 | | `rate` | `number` \| (`prevRate`: `number`) => `number` | 201 | 202 | #### Returns 203 | 204 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 205 | 206 | #### Inherited from 207 | 208 | [BaseAnimationHandle](BaseAnimationHandle.md).[setPlaybackRate](BaseAnimationHandle.md#setplaybackrate) 209 | 210 | #### Defined in 211 | 212 | [src/react/hooks/useAnimation.ts:76](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L76) 213 | 214 | ___ 215 | 216 | ### waitFor 217 | 218 | ▸ **waitFor**(`event`): `Promise`<[`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\>\> 219 | 220 | A getter of Promise that will be resolved in specified timing. 221 | 222 | - `finished`: resolved when animation is finished and its playback direction is normal. 223 | - `reverseFinished`: resolved when animation is finished and its playback direction is reversed. 224 | 225 | #### Parameters 226 | 227 | | Name | Type | 228 | | :------ | :------ | 229 | | `event` | [`WaitingAnimationEventName`](../API.md#waitinganimationeventname) | 230 | 231 | #### Returns 232 | 233 | `Promise`<[`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\>\> 234 | 235 | #### Inherited from 236 | 237 | [BaseAnimationHandle](BaseAnimationHandle.md).[waitFor](BaseAnimationHandle.md#waitfor) 238 | 239 | #### Defined in 240 | 241 | [src/react/hooks/useAnimation.ts:85](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L85) 242 | -------------------------------------------------------------------------------- /docs/interfaces/AnimationOptions.md: -------------------------------------------------------------------------------- 1 | # Interface: AnimationOptions 2 | 3 | Extended [options of KeyframeEffect](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect) 4 | 5 | ## Hierarchy 6 | 7 | - [`TypedKeyframeEffectOptions`](TypedKeyframeEffectOptions.md) 8 | 9 | ↳ **`AnimationOptions`** 10 | 11 | ## Table of contents 12 | 13 | ### Properties 14 | 15 | - [timeline](AnimationOptions.md#timeline) 16 | - [easing](AnimationOptions.md#easing) 17 | - [fill](AnimationOptions.md#fill) 18 | - [direction](AnimationOptions.md#direction) 19 | - [composite](AnimationOptions.md#composite) 20 | - [iterationComposite](AnimationOptions.md#iterationcomposite) 21 | - [pseudoElement](AnimationOptions.md#pseudoelement) 22 | - [delay](AnimationOptions.md#delay) 23 | - [duration](AnimationOptions.md#duration) 24 | - [endDelay](AnimationOptions.md#enddelay) 25 | - [iterationStart](AnimationOptions.md#iterationstart) 26 | - [iterations](AnimationOptions.md#iterations) 27 | - [playbackRate](AnimationOptions.md#playbackrate) 28 | 29 | ## Properties 30 | 31 | ### timeline 32 | 33 | • `Optional` **timeline**: [`TimelineDefinition`](TimelineDefinition.md) 34 | 35 | #### Defined in 36 | 37 | [src/react/types/index.ts:9](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/types/index.ts#L9) 38 | 39 | ___ 40 | 41 | ### easing 42 | 43 | • `Optional` **easing**: [`TypedEasing`](../API.md#typedeasing) 44 | 45 | #### Inherited from 46 | 47 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[easing](TypedKeyframeEffectOptions.md#easing) 48 | 49 | #### Defined in 50 | 51 | [src/core/waapi.ts:41](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L41) 52 | 53 | ___ 54 | 55 | ### fill 56 | 57 | • `Optional` **fill**: `FillMode` 58 | 59 | #### Inherited from 60 | 61 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[fill](TypedKeyframeEffectOptions.md#fill) 62 | 63 | #### Defined in 64 | 65 | node_modules/typescript/lib/lib.dom.d.ts:449 66 | 67 | ___ 68 | 69 | ### direction 70 | 71 | • `Optional` **direction**: `PlaybackDirection` 72 | 73 | #### Inherited from 74 | 75 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[direction](TypedKeyframeEffectOptions.md#direction) 76 | 77 | #### Defined in 78 | 79 | node_modules/typescript/lib/lib.dom.d.ts:445 80 | 81 | ___ 82 | 83 | ### composite 84 | 85 | • `Optional` **composite**: `CompositeOperation` 86 | 87 | #### Inherited from 88 | 89 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[composite](TypedKeyframeEffectOptions.md#composite) 90 | 91 | #### Defined in 92 | 93 | node_modules/typescript/lib/lib.dom.d.ts:757 94 | 95 | ___ 96 | 97 | ### iterationComposite 98 | 99 | • `Optional` **iterationComposite**: `IterationCompositeOperation` 100 | 101 | #### Inherited from 102 | 103 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterationComposite](TypedKeyframeEffectOptions.md#iterationcomposite) 104 | 105 | #### Defined in 106 | 107 | node_modules/typescript/lib/lib.dom.d.ts:758 108 | 109 | ___ 110 | 111 | ### pseudoElement 112 | 113 | • `Optional` **pseudoElement**: ``null`` \| `string` 114 | 115 | #### Inherited from 116 | 117 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[pseudoElement](TypedKeyframeEffectOptions.md#pseudoelement) 118 | 119 | #### Defined in 120 | 121 | node_modules/typescript/lib/lib.dom.d.ts:759 122 | 123 | ___ 124 | 125 | ### delay 126 | 127 | • `Optional` **delay**: `number` 128 | 129 | #### Inherited from 130 | 131 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[delay](TypedKeyframeEffectOptions.md#delay) 132 | 133 | #### Defined in 134 | 135 | node_modules/typescript/lib/lib.dom.d.ts:444 136 | 137 | ___ 138 | 139 | ### duration 140 | 141 | • `Optional` **duration**: `string` \| `number` \| `CSSNumericValue` 142 | 143 | #### Inherited from 144 | 145 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[duration](TypedKeyframeEffectOptions.md#duration) 146 | 147 | #### Defined in 148 | 149 | node_modules/typescript/lib/lib.dom.d.ts:446 150 | 151 | ___ 152 | 153 | ### endDelay 154 | 155 | • `Optional` **endDelay**: `number` 156 | 157 | #### Inherited from 158 | 159 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[endDelay](TypedKeyframeEffectOptions.md#enddelay) 160 | 161 | #### Defined in 162 | 163 | node_modules/typescript/lib/lib.dom.d.ts:448 164 | 165 | ___ 166 | 167 | ### iterationStart 168 | 169 | • `Optional` **iterationStart**: `number` 170 | 171 | #### Inherited from 172 | 173 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterationStart](TypedKeyframeEffectOptions.md#iterationstart) 174 | 175 | #### Defined in 176 | 177 | node_modules/typescript/lib/lib.dom.d.ts:450 178 | 179 | ___ 180 | 181 | ### iterations 182 | 183 | • `Optional` **iterations**: `number` 184 | 185 | #### Inherited from 186 | 187 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterations](TypedKeyframeEffectOptions.md#iterations) 188 | 189 | #### Defined in 190 | 191 | node_modules/typescript/lib/lib.dom.d.ts:451 192 | 193 | ___ 194 | 195 | ### playbackRate 196 | 197 | • `Optional` **playbackRate**: `number` 198 | 199 | #### Inherited from 200 | 201 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[playbackRate](TypedKeyframeEffectOptions.md#playbackrate) 202 | 203 | #### Defined in 204 | 205 | node_modules/typescript/lib/lib.dom.d.ts:452 206 | -------------------------------------------------------------------------------- /docs/interfaces/BaseAnimationHandle.md: -------------------------------------------------------------------------------- 1 | # Interface: BaseAnimationHandle 2 | 3 | ## Type parameters 4 | 5 | | Name | Type | 6 | | :------ | :------ | 7 | | `Args` | `void` | 8 | 9 | ## Hierarchy 10 | 11 | - **`BaseAnimationHandle`** 12 | 13 | ↳ [`AnimationHandle`](AnimationHandle.md) 14 | 15 | ↳ [`AnimationFunctionHandle`](AnimationFunctionHandle.md) 16 | 17 | ## Table of contents 18 | 19 | ### Methods 20 | 21 | - [play](BaseAnimationHandle.md#play) 22 | - [reverse](BaseAnimationHandle.md#reverse) 23 | - [cancel](BaseAnimationHandle.md#cancel) 24 | - [finish](BaseAnimationHandle.md#finish) 25 | - [pause](BaseAnimationHandle.md#pause) 26 | - [setTime](BaseAnimationHandle.md#settime) 27 | - [setPlaybackRate](BaseAnimationHandle.md#setplaybackrate) 28 | - [waitFor](BaseAnimationHandle.md#waitfor) 29 | 30 | ## Methods 31 | 32 | ### play 33 | 34 | ▸ **play**(`...opts`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 35 | 36 | A wrapper of Web Animations API's [play](https://developer.mozilla.org/en-US/docs/Web/API/Animation/play). It's similar to GSAP's `play()`. 37 | 38 | #### Parameters 39 | 40 | | Name | Type | 41 | | :------ | :------ | 42 | | `...opts` | `Args` extends `void` ? [PlayOptions?] : [[`PlayOptionsWithArgs`](../API.md#playoptionswithargs)<`Args`\>] | 43 | 44 | #### Returns 45 | 46 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 47 | 48 | #### Defined in 49 | 50 | [src/react/hooks/useAnimation.ts:44](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L44) 51 | 52 | ___ 53 | 54 | ### reverse 55 | 56 | ▸ **reverse**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 57 | 58 | A wrapper of Web Animations API's [reverse](https://developer.mozilla.org/en-US/docs/Web/API/Animation/reverse). It's similar to GSAP's `reverse()`. 59 | 60 | #### Returns 61 | 62 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 63 | 64 | #### Defined in 65 | 66 | [src/react/hooks/useAnimation.ts:50](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L50) 67 | 68 | ___ 69 | 70 | ### cancel 71 | 72 | ▸ **cancel**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 73 | 74 | A wrapper of Web Animations API's [cancel](https://developer.mozilla.org/en-US/docs/Web/API/Animation/cancel). It's similar to GSAP's `kill()`. 75 | 76 | #### Returns 77 | 78 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 79 | 80 | #### Defined in 81 | 82 | [src/react/hooks/useAnimation.ts:54](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L54) 83 | 84 | ___ 85 | 86 | ### finish 87 | 88 | ▸ **finish**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 89 | 90 | A wrapper of Web Animations API's [finish](https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish). 91 | 92 | #### Returns 93 | 94 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 95 | 96 | #### Defined in 97 | 98 | [src/react/hooks/useAnimation.ts:58](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L58) 99 | 100 | ___ 101 | 102 | ### pause 103 | 104 | ▸ **pause**(): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 105 | 106 | A wrapper of Web Animations API's [pause](https://developer.mozilla.org/en-US/docs/Web/API/Animation/pause). It's similar to GSAP's `pause()`. 107 | 108 | #### Returns 109 | 110 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 111 | 112 | #### Defined in 113 | 114 | [src/react/hooks/useAnimation.ts:62](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L62) 115 | 116 | ___ 117 | 118 | ### setTime 119 | 120 | ▸ **setTime**(`time`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 121 | 122 | A setter of Web Animations API's [currentTime](https://developer.mozilla.org/en-US/docs/Web/API/Animation/currentTime). It's similar to GSAP's `seek()`. 123 | 124 | If you pass function, you can get [endTime](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming#return_value) from its argument. 125 | 126 | #### Parameters 127 | 128 | | Name | Type | 129 | | :------ | :------ | 130 | | `time` | `number` \| (`endTime`: `number`) => `number` | 131 | 132 | #### Returns 133 | 134 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 135 | 136 | #### Defined in 137 | 138 | [src/react/hooks/useAnimation.ts:68](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L68) 139 | 140 | ___ 141 | 142 | ### setPlaybackRate 143 | 144 | ▸ **setPlaybackRate**(`rate`): [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 145 | 146 | A wrapper of Web Animations API's [updatePlaybackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/updatePlaybackRate). It's similar to GSAP's `timeScale()`. 147 | 148 | If you pass function, you can get current [playbackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/playbackRate) from its argument. 149 | 150 | #### Parameters 151 | 152 | | Name | Type | 153 | | :------ | :------ | 154 | | `rate` | `number` \| (`prevRate`: `number`) => `number` | 155 | 156 | #### Returns 157 | 158 | [`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\> 159 | 160 | #### Defined in 161 | 162 | [src/react/hooks/useAnimation.ts:76](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L76) 163 | 164 | ___ 165 | 166 | ### waitFor 167 | 168 | ▸ **waitFor**(`event`): `Promise`<[`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\>\> 169 | 170 | A getter of Promise that will be resolved in specified timing. 171 | 172 | - `finished`: resolved when animation is finished and its playback direction is normal. 173 | - `reverseFinished`: resolved when animation is finished and its playback direction is reversed. 174 | 175 | #### Parameters 176 | 177 | | Name | Type | 178 | | :------ | :------ | 179 | | `event` | [`WaitingAnimationEventName`](../API.md#waitinganimationeventname) | 180 | 181 | #### Returns 182 | 183 | `Promise`<[`BaseAnimationHandle`](BaseAnimationHandle.md)<`Args`\>\> 184 | 185 | #### Defined in 186 | 187 | [src/react/hooks/useAnimation.ts:85](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useAnimation.ts#L85) 188 | -------------------------------------------------------------------------------- /docs/interfaces/ScrollTimelineOpts.md: -------------------------------------------------------------------------------- 1 | # Interface: ScrollTimelineOpts 2 | 3 | ## Table of contents 4 | 5 | ### Properties 6 | 7 | - [axis](ScrollTimelineOpts.md#axis) 8 | 9 | ## Properties 10 | 11 | ### axis 12 | 13 | • `Optional` **axis**: [`ScrollTimelineAxis`](../API.md#scrolltimelineaxis) 14 | 15 | #### Defined in 16 | 17 | [src/react/hooks/useScrollTimeline.ts:8](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useScrollTimeline.ts#L8) 18 | -------------------------------------------------------------------------------- /docs/interfaces/TimelineDefinition.md: -------------------------------------------------------------------------------- 1 | # Interface: TimelineDefinition 2 | 3 | ## Callable 4 | 5 | ### TimelineDefinition 6 | 7 | ▸ **TimelineDefinition**(`el`): `void` 8 | 9 | #### Parameters 10 | 11 | | Name | Type | 12 | | :------ | :------ | 13 | | `el` | ``null`` \| `Element` | 14 | 15 | #### Returns 16 | 17 | `void` 18 | 19 | #### Defined in 20 | 21 | [src/react/types/index.ts:18](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/types/index.ts#L18) 22 | -------------------------------------------------------------------------------- /docs/interfaces/TransitionAnimationHandle.md: -------------------------------------------------------------------------------- 1 | # Interface: TransitionAnimationHandle 2 | 3 | ## Callable 4 | 5 | ### TransitionAnimationHandle 6 | 7 | ▸ **TransitionAnimationHandle**(`ref`): `void` 8 | 9 | #### Parameters 10 | 11 | | Name | Type | 12 | | :------ | :------ | 13 | | `ref` | ``null`` \| `Element` | 14 | 15 | #### Returns 16 | 17 | `void` 18 | 19 | #### Defined in 20 | 21 | [src/react/hooks/useTransitionAnimation.ts:22](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useTransitionAnimation.ts#L22) 22 | -------------------------------------------------------------------------------- /docs/interfaces/TransitionAnimationOptions.md: -------------------------------------------------------------------------------- 1 | # Interface: TransitionAnimationOptions 2 | 3 | Extended [options of KeyframeEffect](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect) 4 | 5 | ## Hierarchy 6 | 7 | - [`TypedKeyframeEffectOptions`](TypedKeyframeEffectOptions.md) 8 | 9 | ↳ **`TransitionAnimationOptions`** 10 | 11 | ## Table of contents 12 | 13 | ### Properties 14 | 15 | - [easing](TransitionAnimationOptions.md#easing) 16 | - [fill](TransitionAnimationOptions.md#fill) 17 | - [direction](TransitionAnimationOptions.md#direction) 18 | - [composite](TransitionAnimationOptions.md#composite) 19 | - [iterationComposite](TransitionAnimationOptions.md#iterationcomposite) 20 | - [pseudoElement](TransitionAnimationOptions.md#pseudoelement) 21 | - [delay](TransitionAnimationOptions.md#delay) 22 | - [duration](TransitionAnimationOptions.md#duration) 23 | - [endDelay](TransitionAnimationOptions.md#enddelay) 24 | - [iterationStart](TransitionAnimationOptions.md#iterationstart) 25 | - [iterations](TransitionAnimationOptions.md#iterations) 26 | - [playbackRate](TransitionAnimationOptions.md#playbackrate) 27 | 28 | ## Properties 29 | 30 | ### easing 31 | 32 | • `Optional` **easing**: [`TypedEasing`](../API.md#typedeasing) 33 | 34 | #### Inherited from 35 | 36 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[easing](TypedKeyframeEffectOptions.md#easing) 37 | 38 | #### Defined in 39 | 40 | [src/core/waapi.ts:41](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L41) 41 | 42 | ___ 43 | 44 | ### fill 45 | 46 | • `Optional` **fill**: `FillMode` 47 | 48 | #### Inherited from 49 | 50 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[fill](TypedKeyframeEffectOptions.md#fill) 51 | 52 | #### Defined in 53 | 54 | node_modules/typescript/lib/lib.dom.d.ts:449 55 | 56 | ___ 57 | 58 | ### direction 59 | 60 | • `Optional` **direction**: `PlaybackDirection` 61 | 62 | #### Inherited from 63 | 64 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[direction](TypedKeyframeEffectOptions.md#direction) 65 | 66 | #### Defined in 67 | 68 | node_modules/typescript/lib/lib.dom.d.ts:445 69 | 70 | ___ 71 | 72 | ### composite 73 | 74 | • `Optional` **composite**: `CompositeOperation` 75 | 76 | #### Inherited from 77 | 78 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[composite](TypedKeyframeEffectOptions.md#composite) 79 | 80 | #### Defined in 81 | 82 | node_modules/typescript/lib/lib.dom.d.ts:757 83 | 84 | ___ 85 | 86 | ### iterationComposite 87 | 88 | • `Optional` **iterationComposite**: `IterationCompositeOperation` 89 | 90 | #### Inherited from 91 | 92 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterationComposite](TypedKeyframeEffectOptions.md#iterationcomposite) 93 | 94 | #### Defined in 95 | 96 | node_modules/typescript/lib/lib.dom.d.ts:758 97 | 98 | ___ 99 | 100 | ### pseudoElement 101 | 102 | • `Optional` **pseudoElement**: ``null`` \| `string` 103 | 104 | #### Inherited from 105 | 106 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[pseudoElement](TypedKeyframeEffectOptions.md#pseudoelement) 107 | 108 | #### Defined in 109 | 110 | node_modules/typescript/lib/lib.dom.d.ts:759 111 | 112 | ___ 113 | 114 | ### delay 115 | 116 | • `Optional` **delay**: `number` 117 | 118 | #### Inherited from 119 | 120 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[delay](TypedKeyframeEffectOptions.md#delay) 121 | 122 | #### Defined in 123 | 124 | node_modules/typescript/lib/lib.dom.d.ts:444 125 | 126 | ___ 127 | 128 | ### duration 129 | 130 | • `Optional` **duration**: `string` \| `number` \| `CSSNumericValue` 131 | 132 | #### Inherited from 133 | 134 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[duration](TypedKeyframeEffectOptions.md#duration) 135 | 136 | #### Defined in 137 | 138 | node_modules/typescript/lib/lib.dom.d.ts:446 139 | 140 | ___ 141 | 142 | ### endDelay 143 | 144 | • `Optional` **endDelay**: `number` 145 | 146 | #### Inherited from 147 | 148 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[endDelay](TypedKeyframeEffectOptions.md#enddelay) 149 | 150 | #### Defined in 151 | 152 | node_modules/typescript/lib/lib.dom.d.ts:448 153 | 154 | ___ 155 | 156 | ### iterationStart 157 | 158 | • `Optional` **iterationStart**: `number` 159 | 160 | #### Inherited from 161 | 162 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterationStart](TypedKeyframeEffectOptions.md#iterationstart) 163 | 164 | #### Defined in 165 | 166 | node_modules/typescript/lib/lib.dom.d.ts:450 167 | 168 | ___ 169 | 170 | ### iterations 171 | 172 | • `Optional` **iterations**: `number` 173 | 174 | #### Inherited from 175 | 176 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[iterations](TypedKeyframeEffectOptions.md#iterations) 177 | 178 | #### Defined in 179 | 180 | node_modules/typescript/lib/lib.dom.d.ts:451 181 | 182 | ___ 183 | 184 | ### playbackRate 185 | 186 | • `Optional` **playbackRate**: `number` 187 | 188 | #### Inherited from 189 | 190 | [TypedKeyframeEffectOptions](TypedKeyframeEffectOptions.md).[playbackRate](TypedKeyframeEffectOptions.md#playbackrate) 191 | 192 | #### Defined in 193 | 194 | node_modules/typescript/lib/lib.dom.d.ts:452 195 | -------------------------------------------------------------------------------- /docs/interfaces/TransitionGroupProps.md: -------------------------------------------------------------------------------- 1 | # Interface: TransitionGroupProps 2 | 3 | ## Table of contents 4 | 5 | ### Properties 6 | 7 | - [children](TransitionGroupProps.md#children) 8 | 9 | ## Properties 10 | 11 | ### children 12 | 13 | • **children**: `ReactElement`<`any`, `string` \| `JSXElementConstructor`<`any`\>\> \| `ReactElement`<`any`, `string` \| `JSXElementConstructor`<`any`\>\>[] 14 | 15 | #### Defined in 16 | 17 | [src/react/components/TransitionGroup.tsx:75](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/components/TransitionGroup.tsx#L75) 18 | -------------------------------------------------------------------------------- /docs/interfaces/TypedKeyframeEffectOptions.md: -------------------------------------------------------------------------------- 1 | # Interface: TypedKeyframeEffectOptions 2 | 3 | Extended [options of KeyframeEffect](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect) 4 | 5 | ## Hierarchy 6 | 7 | - `Omit`<`KeyframeEffectOptions`, ``"easing"``\> 8 | 9 | ↳ **`TypedKeyframeEffectOptions`** 10 | 11 | ↳↳ [`AnimationFunctionOptions`](AnimationFunctionOptions.md) 12 | 13 | ↳↳ [`TransitionAnimationOptions`](TransitionAnimationOptions.md) 14 | 15 | ↳↳ [`AnimationOptions`](AnimationOptions.md) 16 | 17 | ## Table of contents 18 | 19 | ### Properties 20 | 21 | - [easing](TypedKeyframeEffectOptions.md#easing) 22 | - [fill](TypedKeyframeEffectOptions.md#fill) 23 | - [direction](TypedKeyframeEffectOptions.md#direction) 24 | - [composite](TypedKeyframeEffectOptions.md#composite) 25 | - [iterationComposite](TypedKeyframeEffectOptions.md#iterationcomposite) 26 | - [pseudoElement](TypedKeyframeEffectOptions.md#pseudoelement) 27 | - [delay](TypedKeyframeEffectOptions.md#delay) 28 | - [duration](TypedKeyframeEffectOptions.md#duration) 29 | - [endDelay](TypedKeyframeEffectOptions.md#enddelay) 30 | - [iterationStart](TypedKeyframeEffectOptions.md#iterationstart) 31 | - [iterations](TypedKeyframeEffectOptions.md#iterations) 32 | - [playbackRate](TypedKeyframeEffectOptions.md#playbackrate) 33 | 34 | ## Properties 35 | 36 | ### easing 37 | 38 | • `Optional` **easing**: [`TypedEasing`](../API.md#typedeasing) 39 | 40 | #### Defined in 41 | 42 | [src/core/waapi.ts:41](https://github.com/inokawa/react-animatable/blob/3ce964a/src/core/waapi.ts#L41) 43 | 44 | ___ 45 | 46 | ### fill 47 | 48 | • `Optional` **fill**: `FillMode` 49 | 50 | #### Inherited from 51 | 52 | Omit.fill 53 | 54 | #### Defined in 55 | 56 | node_modules/typescript/lib/lib.dom.d.ts:449 57 | 58 | ___ 59 | 60 | ### direction 61 | 62 | • `Optional` **direction**: `PlaybackDirection` 63 | 64 | #### Inherited from 65 | 66 | Omit.direction 67 | 68 | #### Defined in 69 | 70 | node_modules/typescript/lib/lib.dom.d.ts:445 71 | 72 | ___ 73 | 74 | ### composite 75 | 76 | • `Optional` **composite**: `CompositeOperation` 77 | 78 | #### Inherited from 79 | 80 | Omit.composite 81 | 82 | #### Defined in 83 | 84 | node_modules/typescript/lib/lib.dom.d.ts:757 85 | 86 | ___ 87 | 88 | ### iterationComposite 89 | 90 | • `Optional` **iterationComposite**: `IterationCompositeOperation` 91 | 92 | #### Inherited from 93 | 94 | Omit.iterationComposite 95 | 96 | #### Defined in 97 | 98 | node_modules/typescript/lib/lib.dom.d.ts:758 99 | 100 | ___ 101 | 102 | ### pseudoElement 103 | 104 | • `Optional` **pseudoElement**: ``null`` \| `string` 105 | 106 | #### Inherited from 107 | 108 | Omit.pseudoElement 109 | 110 | #### Defined in 111 | 112 | node_modules/typescript/lib/lib.dom.d.ts:759 113 | 114 | ___ 115 | 116 | ### delay 117 | 118 | • `Optional` **delay**: `number` 119 | 120 | #### Inherited from 121 | 122 | Omit.delay 123 | 124 | #### Defined in 125 | 126 | node_modules/typescript/lib/lib.dom.d.ts:444 127 | 128 | ___ 129 | 130 | ### duration 131 | 132 | • `Optional` **duration**: `string` \| `number` \| `CSSNumericValue` 133 | 134 | #### Inherited from 135 | 136 | Omit.duration 137 | 138 | #### Defined in 139 | 140 | node_modules/typescript/lib/lib.dom.d.ts:446 141 | 142 | ___ 143 | 144 | ### endDelay 145 | 146 | • `Optional` **endDelay**: `number` 147 | 148 | #### Inherited from 149 | 150 | Omit.endDelay 151 | 152 | #### Defined in 153 | 154 | node_modules/typescript/lib/lib.dom.d.ts:448 155 | 156 | ___ 157 | 158 | ### iterationStart 159 | 160 | • `Optional` **iterationStart**: `number` 161 | 162 | #### Inherited from 163 | 164 | Omit.iterationStart 165 | 166 | #### Defined in 167 | 168 | node_modules/typescript/lib/lib.dom.d.ts:450 169 | 170 | ___ 171 | 172 | ### iterations 173 | 174 | • `Optional` **iterations**: `number` 175 | 176 | #### Inherited from 177 | 178 | Omit.iterations 179 | 180 | #### Defined in 181 | 182 | node_modules/typescript/lib/lib.dom.d.ts:451 183 | 184 | ___ 185 | 186 | ### playbackRate 187 | 188 | • `Optional` **playbackRate**: `number` 189 | 190 | #### Inherited from 191 | 192 | Omit.playbackRate 193 | 194 | #### Defined in 195 | 196 | node_modules/typescript/lib/lib.dom.d.ts:452 197 | -------------------------------------------------------------------------------- /docs/interfaces/ViewTimelineOpts.md: -------------------------------------------------------------------------------- 1 | # Interface: ViewTimelineOpts 2 | 3 | ## Table of contents 4 | 5 | ### Properties 6 | 7 | - [axis](ViewTimelineOpts.md#axis) 8 | - [inset](ViewTimelineOpts.md#inset) 9 | 10 | ## Properties 11 | 12 | ### axis 13 | 14 | • `Optional` **axis**: [`ScrollTimelineAxis`](../API.md#scrolltimelineaxis) 15 | 16 | #### Defined in 17 | 18 | [src/react/hooks/useViewTimeline.ts:12](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useViewTimeline.ts#L12) 19 | 20 | ___ 21 | 22 | ### inset 23 | 24 | • `Optional` **inset**: [`ViewTimelineInset`](../API.md#viewtimelineinset) 25 | 26 | #### Defined in 27 | 28 | [src/react/hooks/useViewTimeline.ts:13](https://github.com/inokawa/react-animatable/blob/3ce964a/src/react/hooks/useViewTimeline.ts#L13) 29 | -------------------------------------------------------------------------------- /images/demo-chart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inokawa/react-animatable/67622b5b57c1721c8aaed8317e9d4a1e7fe1b459/images/demo-chart.gif -------------------------------------------------------------------------------- /images/demo-scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inokawa/react-animatable/67622b5b57c1721c8aaed8317e9d4a1e7fe1b459/images/demo-scroll.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-animatable", 3 | "version": "0.15.2", 4 | "description": "Tiny(~1kB) animation hooks for React, built on Web Animations API.", 5 | "main": "lib/index.js", 6 | "module": "lib/index.mjs", 7 | "types": "lib/index.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "types": "./lib/index.d.ts", 12 | "import": "./lib/index.mjs", 13 | "default": "./lib/index.js" 14 | } 15 | }, 16 | "files": [ 17 | "lib" 18 | ], 19 | "sideEffects": false, 20 | "scripts": { 21 | "build": "rollup -c", 22 | "tsc": "tsc -p . --noEmit", 23 | "test": "vitest run", 24 | "storybook": "storybook dev -p 6006", 25 | "storybook:build": "storybook build", 26 | "storybook:test": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"npm run storybook -- --no-open\" \"wait-on tcp:6006 && test-storybook\"", 27 | "typedoc": "typedoc", 28 | "size": "size-limit", 29 | "prepublishOnly": "npm run typedoc && rimraf lib && npm run build" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.22.11", 33 | "@babel/plugin-transform-react-pure-annotations": "^7.22.5", 34 | "@chakra-ui/react": "^2.8.2", 35 | "@emotion/css": "^11.10.5", 36 | "@emotion/react": "^11.10.5", 37 | "@emotion/styled": "^11.10.5", 38 | "@fluentui/react": "^8.103.3", 39 | "@linaria/babel-preset": "^4.3.0", 40 | "@linaria/core": "^4.2.8", 41 | "@linaria/react": "^4.3.6", 42 | "@linaria/webpack-loader": "^4.1.15", 43 | "@mantine/core": "^5.8.4", 44 | "@mantine/hooks": "^5.8.4", 45 | "@mui/material": "^5.10.16", 46 | "@rollup/plugin-babel": "^6.0.4", 47 | "@rollup/plugin-terser": "^0.4.4", 48 | "@rollup/plugin-typescript": "^11.1.6", 49 | "@size-limit/preset-small-lib": "^11.1.2", 50 | "@storybook/addon-storysource": "^8.4.0", 51 | "@storybook/addon-webpack5-compiler-swc": "^1.0.2", 52 | "@storybook/react": "^8.4.0", 53 | "@storybook/react-webpack5": "^8.4.0", 54 | "@storybook/source-loader": "^8.4.0", 55 | "@storybook/test-runner": "^0.20.0", 56 | "@types/react": "^18.3.1", 57 | "@types/react-dom": "^18.3.0", 58 | "@vanilla-extract/css": "^1.9.2", 59 | "@vanilla-extract/webpack-plugin": "^2.2.0", 60 | "antd": "^5.0.2", 61 | "babel-loader": "^9.1.3", 62 | "concurrently": "^7.2.2", 63 | "framer-motion": "^10.16.16", 64 | "jsdom": "^25.0.1", 65 | "mini-css-extract-plugin": "^2.7.7", 66 | "react": "^18.3.1", 67 | "react-dom": "^18.3.1", 68 | "react-merge-refs": "^2.0.2", 69 | "rimraf": "^5.0.5", 70 | "rollup": "^4.17.2", 71 | "rollup-plugin-banner2": "^1.2.3", 72 | "size-limit": "^11.1.2", 73 | "storybook": "^8.4.0", 74 | "styled-components": "^6.0.7", 75 | "typedoc": "^0.27.0", 76 | "typedoc-plugin-markdown": "^3.16.0", 77 | "typescript": "^5.4.5", 78 | "vitest": "^2.1.4", 79 | "wait-on": "^7.0.1", 80 | "web-animations-js": "^2.3.2", 81 | "webpack": "^5.88.2" 82 | }, 83 | "peerDependencies": { 84 | "react": ">=16.14.0" 85 | }, 86 | "repository": { 87 | "type": "git", 88 | "url": "git+https://github.com/inokawa/react-animatable.git" 89 | }, 90 | "keywords": [ 91 | "react", 92 | "react-component", 93 | "react-hooks", 94 | "hooks", 95 | "animation", 96 | "tween", 97 | "transition", 98 | "timeline", 99 | "scroll", 100 | "view", 101 | "visualization", 102 | "svg", 103 | "css", 104 | "canvas", 105 | "d3", 106 | "web-animations" 107 | ], 108 | "author": "inokawa (https://github.com/inokawa/)", 109 | "license": "MIT", 110 | "bugs": { 111 | "url": "https://github.com/inokawa/react-animatable/issues" 112 | }, 113 | "homepage": "https://github.com/inokawa/react-animatable#readme" 114 | } 115 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import { getBabelOutputPlugin } from "@rollup/plugin-babel"; 3 | import terser from "@rollup/plugin-terser"; 4 | import banner from "rollup-plugin-banner2"; 5 | import pkg from "./package.json" with { type: "json" }; 6 | 7 | const keys = (p) => Object.keys(p || {}); 8 | 9 | export default { 10 | input: "src/index.ts", 11 | output: [ 12 | { 13 | file: pkg.main, 14 | format: "cjs", 15 | sourcemap: true, 16 | }, 17 | { 18 | file: pkg.module, 19 | format: "es", 20 | sourcemap: true, 21 | }, 22 | ], 23 | external: (id) => 24 | [...keys(pkg.dependencies), ...keys(pkg.devDependencies)].some((d) => 25 | id.startsWith(d) 26 | ), 27 | plugins: [ 28 | typescript({ 29 | tsconfig: "./tsconfig.json", 30 | outDir: ".", 31 | declaration: true, 32 | exclude: ["src/**/*.spec.*"], 33 | }), 34 | getBabelOutputPlugin({ 35 | plugins: ["@babel/plugin-transform-react-pure-annotations"], 36 | }), 37 | terser({ 38 | ecma: 2015, 39 | module: true, 40 | compress: { passes: 5, unsafe: true, keep_fargs: false }, 41 | mangle: { properties: { regex: "^_" } }, 42 | format: { 43 | preserve_annotations: true, 44 | }, 45 | }), 46 | banner(() => '"use client";\n'), 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TODO replace if added to lib.dom.d.ts 2 | declare var ScrollTimeline: { 3 | prototype: AnimationTimeline; 4 | new (opts: { 5 | source: Element; 6 | axis?: "block" | "inline" | "y" | "x" | undefined; 7 | }): AnimationTimeline; 8 | }; 9 | 10 | // TODO replace if added to lib.dom.d.ts 11 | declare var ViewTimeline: { 12 | prototype: AnimationTimeline; 13 | new (opts: { 14 | subject: Element; 15 | axis?: "block" | "inline" | "y" | "x" | undefined; 16 | inset?: "auto" | (string & {}) | undefined; 17 | }): AnimationTimeline; 18 | }; 19 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | TypedKeyframe, 3 | TypedEasing, 4 | GetKeyframeFunction, 5 | TypedKeyframeEffectOptions, 6 | AnimatableCSSProperties, 7 | PlayOptions, 8 | WaitingAnimationEventName, 9 | } from "./waapi"; 10 | -------------------------------------------------------------------------------- /src/core/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { isSameObject, isSameObjectArray } from "./utils"; 3 | 4 | describe("isSameObject", () => { 5 | it("should return true if the objects are same", () => { 6 | expect( 7 | isSameObject( 8 | { transform: "translateX(0px)", fill: "lightskyblue" }, 9 | { transform: "translateX(0px)", fill: "lightskyblue" } 10 | ) 11 | ).toBe(true); 12 | }); 13 | 14 | it("should return true if the fields order is changed", () => { 15 | expect( 16 | isSameObject( 17 | { transform: "translateX(0px)", fill: "lightskyblue" }, 18 | { fill: "lightskyblue", transform: "translateX(0px)" } 19 | ) 20 | ).toBe(true); 21 | }); 22 | 23 | it("should return true if the objects are empty", () => { 24 | expect(isSameObject({}, {})).toBe(true); 25 | }); 26 | 27 | it("should return false if a field is changed", () => { 28 | expect( 29 | isSameObject( 30 | { transform: "translateX(0px)", fill: "lightskyblue" }, 31 | { transform: "translateX(100px)", fill: "lightskyblue" } 32 | ) 33 | ).toBe(false); 34 | }); 35 | 36 | it("should return false if a field name is changed", () => { 37 | expect( 38 | isSameObject( 39 | { transform: "translateX(0px)", fill: "lightskyblue" }, 40 | { transform: "translateX(0px)", color: "lightskyblue" } 41 | ) 42 | ).toBe(false); 43 | }); 44 | 45 | it("should return false if a field is added", () => { 46 | expect( 47 | isSameObject( 48 | { transform: "translateX(0px)", fill: "lightskyblue" }, 49 | { transform: "translateX(0px)", fill: "lightskyblue", color: "red" } 50 | ) 51 | ).toBe(false); 52 | }); 53 | 54 | it("should return false if a field is deleted", () => { 55 | expect( 56 | isSameObject( 57 | { transform: "translateX(0px)", fill: "lightskyblue" }, 58 | { transform: "translateX(0px)" } 59 | ) 60 | ).toBe(false); 61 | }); 62 | }); 63 | 64 | describe("isSameObjectArray", () => { 65 | it("should return true if the arrays are same", () => { 66 | expect( 67 | isSameObjectArray( 68 | [ 69 | { transform: "translateX(0px)", fill: "lightskyblue" }, 70 | { transform: "translateX(100px)", fill: "red" }, 71 | ], 72 | [ 73 | { transform: "translateX(0px)", fill: "lightskyblue" }, 74 | { transform: "translateX(100px)", fill: "red" }, 75 | ] 76 | ) 77 | ).toBe(true); 78 | }); 79 | 80 | it("should return true if the arrays are empty", () => { 81 | expect(isSameObjectArray([], [])).toBe(true); 82 | }); 83 | 84 | it("should return false if a field is updated", () => { 85 | expect( 86 | isSameObjectArray( 87 | [ 88 | { transform: "translateX(0px)", fill: "lightskyblue" }, 89 | { transform: "translateX(100px)", fill: "red" }, 90 | ], 91 | [ 92 | { transform: "translateX(0px)", fill: "lightskyblue" }, 93 | { transform: "translateX(50px)", fill: "red" }, 94 | ] 95 | ) 96 | ).toBe(false); 97 | }); 98 | 99 | it("should return false if a field is deleted", () => { 100 | expect( 101 | isSameObjectArray( 102 | [ 103 | { transform: "translateX(0px)", fill: "lightskyblue" }, 104 | { transform: "translateX(100px)", fill: "red" }, 105 | ], 106 | [ 107 | { transform: "translateX(0px)", fill: "lightskyblue" }, 108 | { transform: "translateX(100px)" }, 109 | ] 110 | ) 111 | ).toBe(false); 112 | }); 113 | 114 | it("should return false if a field is added", () => { 115 | expect( 116 | isSameObjectArray( 117 | [ 118 | { transform: "translateX(0px)", fill: "lightskyblue" }, 119 | { transform: "translateX(100px)", fill: "red" }, 120 | ], 121 | [ 122 | { transform: "translateX(0px)", fill: "lightskyblue" }, 123 | { transform: "translateX(100px)", fill: "red", color: "blue" }, 124 | ] 125 | ) 126 | ).toBe(false); 127 | }); 128 | 129 | it("should return false if an object is added", () => { 130 | expect( 131 | isSameObjectArray( 132 | [ 133 | { transform: "translateX(0px)", fill: "lightskyblue" }, 134 | { transform: "translateX(100px)", fill: "red" }, 135 | ], 136 | [ 137 | { transform: "translateX(0px)", fill: "lightskyblue" }, 138 | { transform: "translateX(100px)", fill: "red" }, 139 | { transform: "translateX(100px)", fill: "red" }, 140 | ] 141 | ) 142 | ).toBe(false); 143 | }); 144 | 145 | it("should return false if an object is added", () => { 146 | expect( 147 | isSameObjectArray( 148 | [ 149 | { transform: "translateX(0px)", fill: "lightskyblue" }, 150 | { transform: "translateX(100px)", fill: "red" }, 151 | ], 152 | [{ transform: "translateX(0px)", fill: "lightskyblue" }] 153 | ) 154 | ).toBe(false); 155 | }); 156 | 157 | it("should return false if the object order is changed", () => { 158 | expect( 159 | isSameObjectArray( 160 | [ 161 | { transform: "translateX(0px)", fill: "lightskyblue" }, 162 | { transform: "translateX(100px)", fill: "red" }, 163 | ], 164 | [ 165 | { transform: "translateX(100px)", fill: "red" }, 166 | { transform: "translateX(0px)", fill: "lightskyblue" }, 167 | ] 168 | ) 169 | ).toBe(false); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const noop = () => {}; 3 | /** @internal */ 4 | export const getKeys: (item: T) => (keyof T)[] = Object.keys; 5 | /** @internal */ 6 | export const assign = Object.assign; 7 | /** @internal */ 8 | export const isArray = Array.isArray; 9 | /** @internal */ 10 | export const getStyle = (e: Element) => getComputedStyle(e); 11 | 12 | /** 13 | * @internal 14 | */ 15 | export const isSameObject = ( 16 | target: object = {}, 17 | prev: object = {} 18 | ): boolean => { 19 | const keys = getKeys(target); 20 | if (keys.length !== getKeys(prev).length) return false; 21 | return keys.every((k) => (target as any)[k] === (prev as any)[k]); 22 | }; 23 | 24 | /** 25 | * @internal 26 | */ 27 | export const isSameObjectArray = ( 28 | target: object[], 29 | prev: object[] 30 | ): boolean => { 31 | if (target.length !== prev.length) return false; 32 | return target.every((t, i) => isSameObject(t, prev[i])); 33 | }; 34 | 35 | /** 36 | * @internal 37 | */ 38 | export const toArray = (items: T | T[]): T[] => 39 | isArray(items) ? items : [items]; 40 | 41 | /** 42 | * @internal 43 | */ 44 | export const uniq = (items: T[]): T[] => { 45 | return Array.from(new Set(items)); 46 | }; 47 | -------------------------------------------------------------------------------- /src/core/waapi.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { getKeyframeKeys } from "./waapi"; 3 | 4 | describe("getKeyframeKeys", () => { 5 | it("normal", () => { 6 | expect( 7 | getKeyframeKeys([ 8 | { transform: "rotate(-720deg) translateX(0px)" }, 9 | { transform: "rotate(-360deg) translateX(-250px)", offset: 0.25 }, 10 | { 11 | transform: "rotate(0deg) translateX(0px)", 12 | fill: "red", 13 | fontSize: "48px", 14 | fontWeight: "bold", 15 | offset: 0.75, 16 | }, 17 | { transform: "rotate(360deg) translateX(0px)", fill: "lightskyblue" }, 18 | ]) 19 | ).toEqual(["transform", "fill", "fontSize", "fontWeight"]); 20 | }); 21 | 22 | it("ignore offset, easing and composite", () => { 23 | expect( 24 | getKeyframeKeys([ 25 | { transform: "rotate(-720deg) translateX(0px)", easing: "ease" }, 26 | { 27 | transform: "rotate(0deg) translateX(0px)", 28 | offset: 0.75, 29 | composite: "accumulate", 30 | }, 31 | { transform: "rotate(360deg) translateX(0px)" }, 32 | ]) 33 | ).toEqual(["transform"]); 34 | }); 35 | 36 | it("map cssFloat and cssOffset", () => { 37 | expect(getKeyframeKeys([{ cssFloat: "left", cssOffset: 10 }])).toEqual([ 38 | "float", 39 | "offset", 40 | ]); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/core/waapi.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from "react"; 2 | import { getKeys, uniq } from "./utils"; 3 | 4 | export type AnimatableCSSProperties = Omit< 5 | CSSProperties, 6 | "offset" | "float" 7 | > & { 8 | cssOffset?: CSSProperties["offset"]; 9 | cssFloat?: CSSProperties["float"]; 10 | // CSS Motion Path for SVG 11 | d?: string; 12 | }; 13 | 14 | /** 15 | * Strictly typed [Keyframe](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats) 16 | */ 17 | export type TypedKeyframe = Pick< 18 | Keyframe, 19 | "composite" | "easing" | "offset" 20 | > & { [key: `--${string}`]: string | number } & AnimatableCSSProperties; 21 | 22 | export type TypedEasing = Exclude< 23 | CSSProperties["animationTimingFunction"], 24 | CSSProperties["all"] | undefined 25 | >; 26 | 27 | /** 28 | * A function to define keyframe dynamically 29 | * - `prev`: current style 30 | * - `args`: any argument passed from play 31 | */ 32 | export type GetKeyframeFunction = Args extends void 33 | ? (prev: CSSStyleDeclaration) => TypedKeyframe[] 34 | : (prev: CSSStyleDeclaration, args: Args) => TypedKeyframe[]; 35 | 36 | /** 37 | * Extended [options of KeyframeEffect](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect) 38 | */ 39 | export interface TypedKeyframeEffectOptions 40 | extends Omit { 41 | easing?: TypedEasing; 42 | } 43 | 44 | /** 45 | * @internal 46 | */ 47 | export const getKeyframeKeys = (keyframes: TypedKeyframe[]): string[] => 48 | uniq(keyframes.flatMap(getKeys)).reduce((acc, k) => { 49 | if (["offset", "easing", "composite"].includes(k)) { 50 | // Ignore 51 | } else if (k === "cssFloat") { 52 | acc.push("float"); 53 | } else if (k === "cssOffset") { 54 | acc.push("offset"); 55 | } else { 56 | acc.push(k); 57 | } 58 | return acc; 59 | }, [] as string[]); 60 | 61 | /** 62 | * @internal 63 | */ 64 | export const createAnimation = ( 65 | el: Element | null, 66 | keyframes: Keyframe[] | null, 67 | options: KeyframeEffectOptions | undefined, 68 | timeline?: AnimationTimeline 69 | ): Animation => { 70 | const modifiedOptions: KeyframeEffectOptions = { 71 | fill: "both", 72 | ...options, 73 | }; 74 | try { 75 | return new Animation( 76 | new KeyframeEffect(el, keyframes, modifiedOptions), 77 | timeline 78 | ); 79 | } catch (e) { 80 | // Fallback to Element.animate() 81 | return el!.animate(keyframes, modifiedOptions); 82 | } 83 | }; 84 | 85 | export type PlayOptions = { 86 | /** 87 | * If true, plays from the start. It's similar to GSAP's `restart()`. 88 | */ 89 | restart?: boolean; 90 | }; 91 | 92 | /** 93 | * @internal 94 | */ 95 | export const _play = (animation: Animation, opts: PlayOptions = {}) => { 96 | // Reset reversed playback direction if completed 97 | if (animation.playbackRate < 0 && animation.playState === "finished") { 98 | _setRate(animation, (p) => -p); 99 | } 100 | if (opts.restart) { 101 | _setTime(animation, 0); 102 | } 103 | animation.play(); 104 | }; 105 | 106 | /** 107 | * @internal 108 | */ 109 | export const _reverse = (animation: Animation | undefined) => { 110 | if (!animation) return; 111 | animation.reverse(); 112 | }; 113 | 114 | /** 115 | * @internal 116 | */ 117 | export const _cancel = (animation: Animation | undefined) => { 118 | if (!animation) return; 119 | animation.cancel(); 120 | }; 121 | 122 | /** 123 | * @internal 124 | */ 125 | export const _finish = (animation: Animation | undefined) => { 126 | if (!animation) return; 127 | animation.finish(); 128 | }; 129 | 130 | /** 131 | * @internal 132 | */ 133 | export const _pause = (animation: Animation | undefined) => { 134 | if (!animation) return; 135 | animation.pause(); 136 | }; 137 | // /** 138 | // * @internal 139 | // */ 140 | // export const _persist = ( 141 | // animation: Animation | undefined, 142 | // el: Element, 143 | // keyframes: TypedKeyframe[] 144 | // ) => { 145 | // if (!animation) return; 146 | // // https://www.w3.org/TR/web-animations-1/#fill-behavior 147 | // if (animation.commitStyles) { 148 | // animation.commitStyles(); 149 | // } else { 150 | // // Fallback for commitStyles 151 | // const computedStyle = getStyle(el); 152 | // getKeyframeKeys(keyframes).forEach((k) => { 153 | // ((el as HTMLElement).style as any)[k] = (computedStyle as any)[k]; 154 | // }); 155 | // } 156 | // animation.cancel(); 157 | // }; 158 | /** 159 | * @internal 160 | */ 161 | export const _setTime = ( 162 | animation: Animation | undefined, 163 | arg: number | ((endTime: number) => number) 164 | ) => { 165 | if (!animation) return; 166 | animation.currentTime = 167 | typeof arg === "function" 168 | ? arg(animation.effect!.getComputedTiming().endTime! as number) 169 | : arg; 170 | }; 171 | 172 | /** 173 | * @internal 174 | */ 175 | export const _setRate = ( 176 | animation: Animation | undefined, 177 | arg: number | ((prevRate: number) => number) 178 | ) => { 179 | if (!animation) return; 180 | animation.updatePlaybackRate( 181 | typeof arg === "function" ? arg(animation.playbackRate) : arg 182 | ); 183 | }; 184 | 185 | export type WaitingAnimationEventName = "finish" | "reverseFinish"; 186 | 187 | /** 188 | * @internal 189 | */ 190 | export const _waitFor = ( 191 | animation: Animation | undefined, 192 | name: WaitingAnimationEventName 193 | ): Promise => { 194 | if (!animation) return Promise.resolve(); 195 | 196 | return new Promise((resolve) => { 197 | animation.onfinish = () => { 198 | if ( 199 | (name === "finish" && animation.playbackRate > 0) || 200 | (name === "reverseFinish" && animation.playbackRate < 0) 201 | ) { 202 | resolve(); 203 | } 204 | }; 205 | }); 206 | }; 207 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core"; 2 | export * from "./react"; 3 | -------------------------------------------------------------------------------- /src/react/components/TransitionGroup.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | Fragment, 4 | useRef, 5 | Children, 6 | useEffect, 7 | useState, 8 | ReactElement, 9 | useCallback, 10 | } from "react"; 11 | import { noop } from "../../core/utils"; 12 | 13 | const toMap = (elements: ReactElement[]) => 14 | elements.reduce((acc, e, i) => { 15 | acc[e.key || i] = e; 16 | return acc; 17 | }, {} as { [key: string]: ReactElement }); 18 | 19 | /** @internal */ 20 | export type TransitionState = "update" | "enter" | "exit"; 21 | /** @internal */ 22 | export const TransitionStateContext = createContext("update"); 23 | 24 | /** @internal */ 25 | export const NOT_EXIT = 0; 26 | /** @internal */ 27 | export const EXITING = 1; 28 | /** @internal */ 29 | export const EXITED = 2; 30 | /** @internal */ 31 | export type TransitionExitState = 32 | | typeof NOT_EXIT 33 | | typeof EXITING 34 | | typeof EXITED; 35 | /** @internal */ 36 | export const TransitionNotifierContext = 37 | createContext<(state: TransitionExitState) => void>(noop); 38 | 39 | const Provider = ({ 40 | _state: state, 41 | _element: element, 42 | }: { 43 | _state: TransitionState; 44 | _element: ReactElement; 45 | }): ReactElement => { 46 | const [show, setShow] = useState(true); 47 | const hasExitRef = useRef(false); 48 | 49 | const showChildren = 50 | state === "exit" ? (hasExitRef.current ? show : false) : true; 51 | 52 | useEffect(() => { 53 | if (state === "enter") { 54 | setShow(true); 55 | } 56 | }, [state]); 57 | return ( 58 | 59 | { 61 | if (s === EXITED) { 62 | setShow(false); 63 | } else { 64 | hasExitRef.current = s === EXITING; 65 | } 66 | }, [])} 67 | > 68 | {showChildren ? element : null} 69 | 70 | 71 | ); 72 | }; 73 | 74 | export interface TransitionGroupProps { 75 | children: ReactElement | ReactElement[]; 76 | } 77 | 78 | /** 79 | * A component to manage enter/update/exit of its children by key, that works similar to [TransitionGroup of react-transition-group](https://reactcommunity.org/react-transition-group/transition-group). 80 | */ 81 | export const TransitionGroup = ({ 82 | children, 83 | }: TransitionGroupProps): ReactElement => { 84 | const elemsRef = useRef(null!); 85 | const prevElems = elemsRef.current || []; 86 | const elems = Children.map(children, (c) => c); 87 | 88 | useEffect(() => { 89 | elemsRef.current = elems; 90 | }); 91 | 92 | const elemsByKey = toMap(elems); 93 | const prevElemsByKey = toMap(prevElems); 94 | 95 | const res: ReactElement[] = []; 96 | prevElems.forEach((v, i) => { 97 | const key = v.key || i; 98 | if (elemsByKey[key]) { 99 | // update 100 | res.push( 101 | 102 | ); 103 | } else { 104 | // exit 105 | res.push(); 106 | } 107 | }); 108 | elems.forEach((v, i) => { 109 | const key = v.key || i; 110 | if (prevElemsByKey[key]) { 111 | // update 112 | } else { 113 | // enter 114 | res.push(); 115 | } 116 | }); 117 | 118 | return {res}; 119 | }; 120 | -------------------------------------------------------------------------------- /src/react/components/index.ts: -------------------------------------------------------------------------------- 1 | export { TransitionGroup } from "./TransitionGroup"; 2 | export type { TransitionGroupProps } from "./TransitionGroup"; 3 | -------------------------------------------------------------------------------- /src/react/hooks/__snapshots__/useAnimation.ssr.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`SSR > should succeed with renderToStaticMarkup 1`] = `"
test
"`; 4 | 5 | exports[`SSR > should succeed with renderToString 1`] = `"
test
"`; 6 | -------------------------------------------------------------------------------- /src/react/hooks/__snapshots__/useAnimationFunction.ssr.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`SSR > should succeed with renderToStaticMarkup 1`] = `"
0
"`; 4 | 5 | exports[`SSR > should succeed with renderToString 1`] = `"
0
"`; 6 | -------------------------------------------------------------------------------- /src/react/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useAnimation"; 2 | export * from "./useAnimationFunction"; 3 | export * from "./useTransitionAnimation"; 4 | export * from "./useScrollTimeline"; 5 | export * from "./useViewTimeline"; 6 | -------------------------------------------------------------------------------- /src/react/hooks/state.ts: -------------------------------------------------------------------------------- 1 | import { isSameObject, isSameObjectArray } from "../../core/utils"; 2 | import { createAnimation } from "../../core/waapi"; 3 | 4 | /** 5 | * @internal 6 | */ 7 | export interface AnimationObject { 8 | readonly _keyframes: Keyframe[]; 9 | readonly _options: KeyframeEffectOptions | undefined; 10 | readonly _timeline: AnimationTimeline | undefined; 11 | } 12 | 13 | const animations = new WeakMap(); 14 | 15 | /** 16 | * @internal 17 | */ 18 | export const getAnimation = (target: AnimationObject) => animations.get(target); 19 | 20 | /** 21 | * @internal 22 | */ 23 | export const deleteAnimation = (target: AnimationObject) => { 24 | getAnimation(target)?.cancel(); 25 | animations.delete(target); 26 | }; 27 | 28 | const isEqual = (a: AnimationObject, b: AnimationObject): boolean => { 29 | return ( 30 | isSameObjectArray(a._keyframes, b._keyframes) && 31 | isSameObject(a._options, b._options) 32 | ); 33 | }; 34 | 35 | /** 36 | * @internal 37 | */ 38 | export const initAnimation = ( 39 | el: Element, 40 | target: AnimationObject, 41 | prevTarget: AnimationObject | undefined 42 | ): Animation => { 43 | const prevAnimation = prevTarget && getAnimation(prevTarget); 44 | 45 | if (prevAnimation) { 46 | animations.delete(prevTarget); 47 | 48 | // Reuse animation if possible 49 | if (isEqual(target, prevTarget)) { 50 | animations.set(target, prevAnimation); 51 | return prevAnimation; 52 | } 53 | prevAnimation.cancel(); 54 | } 55 | 56 | const animation = createAnimation( 57 | el, 58 | target._keyframes, 59 | target._options, 60 | target._timeline 61 | ); 62 | animations.set(target, animation); 63 | return animation; 64 | }; 65 | -------------------------------------------------------------------------------- /src/react/hooks/useAnimation.ssr.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment node 3 | */ 4 | import { it, describe, expect } from "vitest"; 5 | import { useEffect } from "react"; 6 | import { renderToString, renderToStaticMarkup } from "react-dom/server"; 7 | import { useAnimation } from "./useAnimation"; 8 | 9 | describe("SSR", () => { 10 | const Comp = () => { 11 | const animate = useAnimation( 12 | { width: 100 }, 13 | { duration: 600, easing: "ease-in-out" } 14 | ); 15 | useEffect(() => { 16 | animate.play(); 17 | }, []); 18 | return ( 19 |
20 | test 21 |
22 | ); 23 | }; 24 | 25 | it("should succeed with renderToString", () => { 26 | expect(renderToString()).toMatchSnapshot(); 27 | }); 28 | 29 | it("should succeed with renderToStaticMarkup", () => { 30 | expect(renderToStaticMarkup()).toMatchSnapshot(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/react/hooks/useAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { assign, getStyle, toArray } from "../../core/utils"; 3 | import { 4 | PlayOptions, 5 | _cancel, 6 | _waitFor, 7 | _finish, 8 | _pause, 9 | _play, 10 | _reverse, 11 | _setRate, 12 | _setTime, 13 | WaitingAnimationEventName, 14 | TypedKeyframe, 15 | GetKeyframeFunction, 16 | } from "../../core/waapi"; 17 | import { useStatic } from "./useStatic"; 18 | import { useLatestRef } from "./useLatestRef"; 19 | import { AnimationDefinition } from "../types"; 20 | import { 21 | deleteAnimation, 22 | getAnimation, 23 | AnimationObject, 24 | initAnimation, 25 | } from "./state"; 26 | 27 | const normalizeKeyframe = ( 28 | el: Element, 29 | keyframe: TypedKeyframe | TypedKeyframe[] | GetKeyframeFunction, 30 | args: Args 31 | ): TypedKeyframe[] => { 32 | if (typeof keyframe === "function") { 33 | return keyframe(getStyle(el), args); 34 | } 35 | return toArray(keyframe); 36 | }; 37 | 38 | export type PlayOptionsWithArgs = PlayOptions & { args: Args }; 39 | 40 | export interface BaseAnimationHandle { 41 | /** 42 | * A wrapper of Web Animations API's [play](https://developer.mozilla.org/en-US/docs/Web/API/Animation/play). It's similar to GSAP's `play()`. 43 | */ 44 | play( 45 | ...opts: Args extends void ? [PlayOptions?] : [PlayOptionsWithArgs] 46 | ): BaseAnimationHandle; 47 | /** 48 | * A wrapper of Web Animations API's [reverse](https://developer.mozilla.org/en-US/docs/Web/API/Animation/reverse). It's similar to GSAP's `reverse()`. 49 | */ 50 | reverse(): BaseAnimationHandle; 51 | /** 52 | * A wrapper of Web Animations API's [cancel](https://developer.mozilla.org/en-US/docs/Web/API/Animation/cancel). It's similar to GSAP's `kill()`. 53 | */ 54 | cancel(): BaseAnimationHandle; 55 | /** 56 | * A wrapper of Web Animations API's [finish](https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish). 57 | */ 58 | finish(): BaseAnimationHandle; 59 | /** 60 | * A wrapper of Web Animations API's [pause](https://developer.mozilla.org/en-US/docs/Web/API/Animation/pause). It's similar to GSAP's `pause()`. 61 | */ 62 | pause(): BaseAnimationHandle; 63 | /** 64 | * A setter of Web Animations API's [currentTime](https://developer.mozilla.org/en-US/docs/Web/API/Animation/currentTime). It's similar to GSAP's `seek()`. 65 | * 66 | * If you pass function, you can get [endTime](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming#return_value) from its argument. 67 | */ 68 | setTime( 69 | time: number | ((endTime: number) => number) 70 | ): BaseAnimationHandle; 71 | /** 72 | * A wrapper of Web Animations API's [updatePlaybackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/updatePlaybackRate). It's similar to GSAP's `timeScale()`. 73 | * 74 | * If you pass function, you can get current [playbackRate](https://developer.mozilla.org/en-US/docs/Web/API/Animation/playbackRate) from its argument. 75 | */ 76 | setPlaybackRate( 77 | rate: number | ((prevRate: number) => number) 78 | ): BaseAnimationHandle; 79 | /** 80 | * A getter of Promise that will be resolved in specified timing. 81 | * 82 | * - `finished`: resolved when animation is finished and its playback direction is normal. 83 | * - `reverseFinished`: resolved when animation is finished and its playback direction is reversed. 84 | */ 85 | waitFor(event: WaitingAnimationEventName): Promise>; 86 | } 87 | 88 | /** 89 | * Handle of {@link useAnimation}. 90 | * @typeParam Args - argument type 91 | */ 92 | export interface AnimationHandle 93 | extends BaseAnimationHandle { 94 | /** 95 | * You have to pass this callback to ref of element you want to control. 96 | */ 97 | (ref: Element | null): void; 98 | } 99 | 100 | /** 101 | * A basic hook to use Web Animations API. See {@link AnimationHandle}. 102 | * @typeParam Args - argument type 103 | */ 104 | export const useAnimation = ( 105 | ...args: AnimationDefinition 106 | ): AnimationHandle => { 107 | const argsRef = useLatestRef(args); 108 | 109 | const [handle, mount] = useStatic( 110 | (): [AnimationHandle, () => () => void] => { 111 | let _target: Element | null = null; 112 | let _active: AnimationObject | undefined; 113 | 114 | const init = (args: Args) => { 115 | if (!_target) return; 116 | const [keyframe, _options = {}] = argsRef.current; 117 | const { timeline, ...options } = _options; 118 | 119 | const prevActive = _active; 120 | 121 | return initAnimation( 122 | _target, 123 | (_active = { 124 | _keyframes: normalizeKeyframe(_target, keyframe, args), 125 | _options: options, 126 | _timeline: timeline?._get(), 127 | }), 128 | prevActive 129 | ); 130 | }; 131 | const clean = () => { 132 | if (_active) { 133 | deleteAnimation(_active); 134 | } 135 | }; 136 | 137 | const externalHandle: AnimationHandle = assign( 138 | (ref: Element | null) => { 139 | if (!(_target = ref)) { 140 | clean(); 141 | } 142 | }, 143 | >{ 144 | play: (...opts) => { 145 | const animation = init(((opts[0] || {}) as { args?: Args }).args!); 146 | if (animation) { 147 | _play(animation); 148 | } 149 | return externalHandle; 150 | }, 151 | reverse: () => { 152 | if (_active) { 153 | _reverse(getAnimation(_active)); 154 | } 155 | return externalHandle; 156 | }, 157 | cancel: () => { 158 | if (_active) { 159 | _cancel(getAnimation(_active)); 160 | } 161 | return externalHandle; 162 | }, 163 | finish: () => { 164 | if (_active) { 165 | _finish(getAnimation(_active)); 166 | } 167 | return externalHandle; 168 | }, 169 | pause: () => { 170 | if (_active) { 171 | _pause(getAnimation(_active)); 172 | } 173 | return externalHandle; 174 | }, 175 | setTime: (time) => { 176 | let animation = _active && getAnimation(_active); 177 | if (!animation) { 178 | const [keyframe] = argsRef.current; 179 | if (typeof keyframe === "function") { 180 | return externalHandle; 181 | } 182 | // Init animation in setTime to start animation without calling play 183 | animation = init(undefined!); 184 | } 185 | _setTime(animation, time); 186 | 187 | return externalHandle; 188 | }, 189 | setPlaybackRate: (rate) => { 190 | if (_active) { 191 | _setRate(getAnimation(_active), rate); 192 | } 193 | return externalHandle; 194 | }, 195 | waitFor: (event) => { 196 | return _waitFor(_active && getAnimation(_active), event).then( 197 | () => externalHandle 198 | ); 199 | }, 200 | } 201 | ); 202 | 203 | return [externalHandle, () => clean]; 204 | } 205 | ); 206 | 207 | useEffect(mount, []); 208 | 209 | return handle; 210 | }; 211 | -------------------------------------------------------------------------------- /src/react/hooks/useAnimationFunction.ssr.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment node 3 | */ 4 | import { it, describe, expect } from "vitest"; 5 | import { useEffect, useState } from "react"; 6 | import { renderToString, renderToStaticMarkup } from "react-dom/server"; 7 | import { 8 | useAnimationFunction, 9 | ComputedTimingContext, 10 | } from "./useAnimationFunction"; 11 | 12 | describe("SSR", () => { 13 | const Comp = () => { 14 | const [value, setValue] = useState(0); 15 | const animate = useAnimationFunction( 16 | (ctx: ComputedTimingContext) => { 17 | setValue(ctx.progress); 18 | }, 19 | { 20 | duration: 600, 21 | easing: "ease-in-out", 22 | } 23 | ); 24 | useEffect(() => { 25 | animate.play(); 26 | }, []); 27 | return
{value}
; 28 | }; 29 | 30 | it("should succeed with renderToString", () => { 31 | expect(renderToString()).toMatchSnapshot(); 32 | }); 33 | 34 | it("should succeed with renderToStaticMarkup", () => { 35 | expect(renderToStaticMarkup()).toMatchSnapshot(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/react/hooks/useAnimationFunction.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { isSameObject } from "../../core/utils"; 3 | import { 4 | TypedKeyframeEffectOptions, 5 | createAnimation, 6 | _cancel, 7 | _waitFor, 8 | _finish, 9 | _pause, 10 | _play, 11 | _reverse, 12 | _setRate, 13 | _setTime, 14 | WaitingAnimationEventName, 15 | } from "../../core/waapi"; 16 | import type { BaseAnimationHandle } from "./useAnimation"; 17 | import { useStatic } from "./useStatic"; 18 | import { useLatestRef } from "./useLatestRef"; 19 | 20 | /** 21 | * Handle of {@link useAnimationFunction}. 22 | * @typeParam Args - argument type 23 | */ 24 | export interface AnimationFunctionHandle 25 | extends BaseAnimationHandle {} 26 | 27 | export interface AnimationFunctionOptions extends TypedKeyframeEffectOptions {} 28 | 29 | /** 30 | * Non nullable [ComputedEffectTiming](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming) 31 | */ 32 | export type ComputedTimingContext = Required<{ 33 | [key in keyof ComputedEffectTiming]: NonNullable; 34 | }>; 35 | 36 | /** 37 | * An argument of {@link useAnimationFunction}. 38 | * In this callback you can update any state or ref in JS. 39 | * - `ctx`: current animation state 40 | * - `args`: any argument passed from play 41 | */ 42 | export type AnimationFunction = Args extends void 43 | ? (ctx: ComputedTimingContext) => void 44 | : (ctx: ComputedTimingContext, args: Args) => void; 45 | 46 | const bindUpdateFunction = ( 47 | animation: Animation, 48 | getUpdateFunction: () => AnimationFunction, 49 | args: Args 50 | ) => { 51 | const update = () => { 52 | const timing = animation.effect?.getComputedTiming(); 53 | if (!timing) return; 54 | const progress = timing.progress; 55 | if (progress != null) { 56 | getUpdateFunction()(timing as ComputedTimingContext, args); 57 | } 58 | if (animation.playState === "running") { 59 | requestAnimationFrame(update); 60 | } 61 | }; 62 | animation.ready.then(update); 63 | }; 64 | 65 | /** 66 | * Same as {@link useAnimation}, but it drives function not React element. See {@link AnimationFunctionHandle}. 67 | * @typeParam Args - argument type 68 | */ 69 | export const useAnimationFunction = ( 70 | onUpdate: AnimationFunction, 71 | options?: AnimationFunctionOptions 72 | ): AnimationFunctionHandle => { 73 | const onUpdateRef = useLatestRef(onUpdate); 74 | const optionsRef = useLatestRef(options); 75 | 76 | const [handle, cleanup] = useStatic( 77 | (): [AnimationFunctionHandle, () => void] => { 78 | const getOnUpdate = () => onUpdateRef.current; 79 | 80 | let cache: [Animation, AnimationFunctionOptions | undefined] | undefined; 81 | const initAnimation = (opts: { args?: Args } = {}): Animation => { 82 | const options = optionsRef.current; 83 | if (cache) { 84 | const [prevAnimation, prevOptions] = cache; 85 | // Reuse animation if possible 86 | if (isSameObject(options, prevOptions)) { 87 | if (prevAnimation.playState !== "running") { 88 | bindUpdateFunction(prevAnimation, getOnUpdate, opts.args!); 89 | } 90 | return prevAnimation; 91 | } 92 | prevAnimation.cancel(); 93 | } 94 | const animation = createAnimation(null, null, options); 95 | bindUpdateFunction(animation, getOnUpdate, opts.args!); 96 | cache = [animation, options]; 97 | return animation; 98 | }; 99 | const getAnimation = () => cache?.[0]; 100 | 101 | const externalHandle: AnimationFunctionHandle = { 102 | play: (...opts) => { 103 | _play(initAnimation(opts[0] as { args?: Args }), opts[0]); 104 | return externalHandle; 105 | }, 106 | reverse: () => { 107 | _reverse(initAnimation()); 108 | return externalHandle; 109 | }, 110 | cancel: () => { 111 | _cancel(getAnimation()); 112 | return externalHandle; 113 | }, 114 | finish: () => { 115 | _finish(getAnimation()); 116 | return externalHandle; 117 | }, 118 | pause: () => { 119 | _pause(getAnimation()); 120 | return externalHandle; 121 | }, 122 | setTime: (time) => { 123 | _setTime(getAnimation(), time); 124 | return externalHandle; 125 | }, 126 | setPlaybackRate: (rate) => { 127 | _setRate(getAnimation(), rate); 128 | return externalHandle; 129 | }, 130 | waitFor: (event: WaitingAnimationEventName) => 131 | _waitFor(getAnimation(), event).then(() => externalHandle), 132 | }; 133 | return [externalHandle, externalHandle.cancel]; 134 | } 135 | ); 136 | 137 | useEffect(() => cleanup, []); 138 | 139 | return handle; 140 | }; 141 | -------------------------------------------------------------------------------- /src/react/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from "react"; 2 | 3 | /** 4 | * https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 5 | * @internal 6 | */ 7 | export const useIsomorphicLayoutEffect = 8 | typeof window !== "undefined" ? useLayoutEffect : useEffect; 9 | -------------------------------------------------------------------------------- /src/react/hooks/useLatestRef.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; 3 | 4 | /** 5 | * @internal 6 | */ 7 | export const useLatestRef = (value: T) => { 8 | const ref = useRef(value); 9 | 10 | useIsomorphicLayoutEffect(() => { 11 | ref.current = value; 12 | }, [value]); 13 | 14 | return ref; 15 | }; 16 | -------------------------------------------------------------------------------- /src/react/hooks/useScrollTimeline.ts: -------------------------------------------------------------------------------- 1 | import { assign } from "../../core/utils"; 2 | import { ScrollTimelineAxis, TimelineDefinition } from "../types"; 3 | import { SCROLL_TIMELINE } from "../types/internal"; 4 | import { useLatestRef } from "./useLatestRef"; 5 | import { useStatic } from "./useStatic"; 6 | 7 | export interface ScrollTimelineOpts { 8 | axis?: ScrollTimelineAxis; 9 | } 10 | 11 | export const useScrollTimeline = ( 12 | opts: ScrollTimelineOpts = {} 13 | ): TimelineDefinition => { 14 | const optsRef = useLatestRef(opts); 15 | 16 | return useStatic(() => { 17 | let _target: Element | null; 18 | return assign( 19 | (ref: Element | null) => { 20 | _target = ref; 21 | }, 22 | { 23 | _type: SCROLL_TIMELINE, 24 | _get: () => { 25 | const { axis } = optsRef.current; 26 | return new ScrollTimeline({ 27 | source: _target || document.documentElement, 28 | axis, 29 | }); 30 | }, 31 | } as const 32 | ); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/react/hooks/useStatic.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | /** 4 | * @internal 5 | */ 6 | export const useStatic = (init: () => T): T => { 7 | const ref = useRef(); 8 | return ref.current || (ref.current = init()); 9 | }; 10 | -------------------------------------------------------------------------------- /src/react/hooks/useTransitionAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { 3 | EXITED, 4 | EXITING, 5 | NOT_EXIT, 6 | TransitionNotifierContext, 7 | TransitionState, 8 | TransitionStateContext, 9 | } from "../components/TransitionGroup"; 10 | import { AnimationHandle, useAnimation } from "./useAnimation"; 11 | import { getKeys, noop } from "../../core/utils"; 12 | import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; 13 | import type { 14 | TypedKeyframeEffectOptions, 15 | GetKeyframeFunction, 16 | TypedKeyframe, 17 | } from "../../core"; 18 | import { useStatic } from "./useStatic"; 19 | import { useLatestRef } from "./useLatestRef"; 20 | 21 | export interface TransitionAnimationHandle { 22 | (ref: Element | null): void; 23 | } 24 | 25 | export interface TransitionAnimationOptions 26 | extends TypedKeyframeEffectOptions {} 27 | 28 | export type TransitionAnimationDefinition = [ 29 | keyframe: TypedKeyframe | TypedKeyframe[] | GetKeyframeFunction, 30 | options?: TransitionAnimationOptions 31 | ]; 32 | 33 | /** 34 | * 35 | * A hook to compose multiple {@link useAnimation} and plays them when element enter/update/exits. 36 | * This hook must be used under {@link TransitionGroup} component. 37 | */ 38 | export const useTransitionAnimation = (keyframes: { 39 | enter?: TransitionAnimationDefinition; 40 | update?: TransitionAnimationDefinition; 41 | exit?: TransitionAnimationDefinition; 42 | }): TransitionAnimationHandle => { 43 | const keys = getKeys(keyframes); 44 | const animations = keys.reduce((acc, k) => { 45 | const def = keyframes[k]; 46 | if (!def) return acc; 47 | acc[k] = useAnimation(def[0], def[1]); 48 | return acc; 49 | }, {} as { [key in TransitionState]: AnimationHandle | undefined }); 50 | 51 | const animationsRef = useLatestRef(animations); 52 | 53 | const [animation, cleanup] = useStatic( 54 | (): [TransitionAnimationHandle, () => void] => { 55 | const forAllHandle = (fn: (handle: AnimationHandle) => void) => { 56 | getKeys(animationsRef.current).forEach((name) => 57 | fn(animationsRef.current[name]!) 58 | ); 59 | }; 60 | 61 | const externalHandle: TransitionAnimationHandle = ( 62 | ref: Element | null 63 | ) => { 64 | forAllHandle((h) => { 65 | h(ref); 66 | }); 67 | }; 68 | return [ 69 | externalHandle, 70 | () => { 71 | forAllHandle((handle) => { 72 | handle.cancel(); 73 | }); 74 | }, 75 | ]; 76 | } 77 | ); 78 | 79 | useEffect(() => cleanup, []); 80 | 81 | const currentState = useContext(TransitionStateContext); 82 | const notify = useContext(TransitionNotifierContext); 83 | 84 | useIsomorphicLayoutEffect(() => { 85 | // Decide if the parent should animate children on exit or not 86 | // State must change like enter (-> update) -> exit so it's ok to use ref 87 | if (keys.includes("exit")) { 88 | notify(EXITING); 89 | } else { 90 | notify(NOT_EXIT); 91 | } 92 | }, keys); 93 | 94 | useIsomorphicLayoutEffect(() => { 95 | if (currentState !== "update") return; 96 | 97 | animationsRef.current[currentState]?.play(); 98 | }); 99 | 100 | useIsomorphicLayoutEffect(() => { 101 | if (currentState === "update") return; 102 | 103 | animationsRef.current[currentState] 104 | ?.play() 105 | .waitFor("finish") 106 | .then(() => { 107 | if (currentState === "exit") { 108 | notify(EXITED); 109 | } 110 | }) 111 | .catch( 112 | noop // ignore uncaught promise error 113 | ); 114 | }, [currentState]); 115 | 116 | return animation; 117 | }; 118 | -------------------------------------------------------------------------------- /src/react/hooks/useViewTimeline.ts: -------------------------------------------------------------------------------- 1 | import { assign } from "../../core/utils"; 2 | import { 3 | ScrollTimelineAxis, 4 | TimelineDefinition, 5 | ViewTimelineInset, 6 | } from "../types"; 7 | import { VIEW_TIMELINE } from "../types/internal"; 8 | import { useLatestRef } from "./useLatestRef"; 9 | import { useStatic } from "./useStatic"; 10 | 11 | export interface ViewTimelineOpts { 12 | axis?: ScrollTimelineAxis; 13 | inset?: ViewTimelineInset; 14 | } 15 | 16 | export const useViewTimeline = ( 17 | opts: ViewTimelineOpts = {} 18 | ): TimelineDefinition => { 19 | const optsRef = useLatestRef(opts); 20 | 21 | return useStatic(() => { 22 | let _target: Element | null; 23 | return assign( 24 | (ref: Element | null) => { 25 | _target = ref; 26 | }, 27 | { 28 | _type: VIEW_TIMELINE, 29 | _get: () => { 30 | const { axis, inset } = optsRef.current; 31 | return new ViewTimeline({ 32 | subject: _target || document.documentElement, 33 | axis, 34 | inset, 35 | }); 36 | }, 37 | } as const 38 | ); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/react/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./hooks"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /src/react/types/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetKeyframeFunction, 3 | TypedKeyframe, 4 | TypedKeyframeEffectOptions, 5 | } from "../../core"; 6 | import { SCROLL_TIMELINE, VIEW_TIMELINE } from "./internal"; 7 | 8 | export interface AnimationOptions extends TypedKeyframeEffectOptions { 9 | timeline?: TimelineDefinition; 10 | } 11 | 12 | export type AnimationDefinition = [ 13 | keyframe: TypedKeyframe | TypedKeyframe[] | GetKeyframeFunction, 14 | options?: AnimationOptions 15 | ]; 16 | 17 | export interface TimelineDefinition { 18 | (el: Element | null): void; 19 | /** 20 | * @internal 21 | */ 22 | _type: typeof SCROLL_TIMELINE | typeof VIEW_TIMELINE; 23 | /** 24 | * @internal 25 | */ 26 | _get: () => AnimationTimeline; 27 | } 28 | 29 | export type ScrollTimelineAxis = "block" | "inline" | "y" | "x"; 30 | 31 | export type ViewTimelineInset = "auto" | (string & {}); 32 | -------------------------------------------------------------------------------- /src/react/types/internal.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const SCROLL_TIMELINE = 1; 3 | /** @internal */ 4 | export const VIEW_TIMELINE = 2; 5 | -------------------------------------------------------------------------------- /stories/hooks/useAnimation.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useCallback, useEffect, useState } from "react"; 3 | import { 4 | AnimationOptions, 5 | TypedEasing, 6 | TypedKeyframe, 7 | useAnimation, 8 | } from "../../src"; 9 | import { mergeRefs } from "react-merge-refs"; 10 | 11 | const debounce = void>(fn: T, ms: number) => { 12 | let id: NodeJS.Timeout | null = null; 13 | return (...args: Parameters) => { 14 | if (id != null) { 15 | clearTimeout(id); 16 | } 17 | id = setTimeout(() => { 18 | id = null; 19 | fn(...args); 20 | }, ms); 21 | }; 22 | }; 23 | 24 | export default { component: useAnimation }; 25 | 26 | export const Playground: StoryObj = { 27 | render: () => { 28 | const [duration, setDuration] = useState(1000); 29 | const [iteration, setIteration] = useState(2); 30 | const [direction, setDirection] = useState("alternate"); 31 | const [easing, setEasing] = useState("cubic-bezier"); 32 | const [cubicBezierValues, setCubicBezierValues] = useState< 33 | [number, number, number, number] 34 | >([0.65, 0, 0.35, 1]); 35 | const [stepsValues, setStepsValues] = useState<[number, string]>([ 36 | 20, 37 | "end", 38 | ]); 39 | const [delay, setDelay] = useState(0); 40 | const [endDelay, setEndDelay] = useState(0); 41 | 42 | const animate = useAnimation( 43 | [ 44 | { transform: "translateX(0px)" }, 45 | { 46 | transform: "translateX(100px) scale(2.0)", 47 | fill: "red", 48 | }, 49 | ], 50 | { 51 | duration: duration, 52 | easing: 53 | easing === "cubic-bezier" 54 | ? `cubic-bezier(${cubicBezierValues.join(",")})` 55 | : easing === "steps" 56 | ? `steps(${stepsValues.join(",")})` 57 | : easing, 58 | direction: direction, 59 | iterations: iteration, 60 | delay, 61 | endDelay, 62 | } 63 | ); 64 | 65 | useEffect(() => { 66 | animate.play(); 67 | }, [ 68 | duration, 69 | easing, 70 | cubicBezierValues, 71 | stepsValues, 72 | iteration, 73 | direction, 74 | delay, 75 | ]); 76 | 77 | return ( 78 |
79 | 80 | 81 | 82 | Hello world 83 | 84 | 85 | 86 |
87 | 88 | 89 | 90 | 91 |
92 |
93 | 105 |
106 |
107 | 118 |
119 |
120 | easing: 121 | {["linear", "ease", "ease-in", "ease-out", "ease-in-out"].map((v) => ( 122 | 133 | ))} 134 | 172 | 217 |
218 |
219 | direction: 220 | {["normal", "reverse", "alternate", "alternate-reverse"].map((v) => ( 221 | 232 | ))} 233 |
234 |
235 | 247 |
248 |
249 | 261 |
262 |
263 | 275 |
276 |
277 | 289 |
290 |
291 | ); 292 | }, 293 | }; 294 | 295 | const Bar = ({ 296 | value, 297 | i, 298 | height, 299 | keep, 300 | }: { 301 | value: number; 302 | i: number; 303 | height: number; 304 | keep: boolean; 305 | }) => { 306 | const target: TypedKeyframe = { 307 | height: `${value}px`, 308 | transform: `translateY(-${value}px)`, 309 | opacity: String(1 - i * 0.025), 310 | }; 311 | const animate = useAnimation( 312 | keep 313 | ? (prev) => [ 314 | { 315 | height: prev.height, 316 | transform: prev.transform, 317 | opacity: prev.opacity, 318 | }, 319 | target, 320 | ] 321 | : [target], 322 | { duration: 150, easing: "ease-out", delay: i * 100 } 323 | ); 324 | 325 | useEffect(() => { 326 | animate.play(); 327 | }, [value, keep]); 328 | 329 | return ( 330 | 339 | ); 340 | }; 341 | 342 | export const Bars: StoryObj = { 343 | render: () => { 344 | const init = () => 345 | Array.from({ length: 30 }).map(() => 300 * Math.random() ** 2); 346 | const [rects, setRects] = useState(init); 347 | const [keep, setKeep] = useState(false); 348 | 349 | const width = 800; 350 | const height = 400; 351 | const margin = 10; 352 | const maxBarHeight = height - margin * 2; 353 | 354 | const refresh = () => setRects(init()); 355 | 356 | return ( 357 | <> 358 |
359 | 360 | 371 |
372 |
373 | 374 | 375 | {rects.map((v, i) => ( 376 | 383 | ))} 384 | 385 | 386 |
387 | 388 | ); 389 | }, 390 | }; 391 | 392 | export const Mouse: StoryObj = { 393 | render: () => { 394 | const rotate = useAnimation( 395 | [ 396 | { transform: "rotate(0deg)", borderRadius: "1rem" }, 397 | { transform: "rotate(360deg)", borderRadius: "50%" }, 398 | { transform: "rotate(720deg)", borderRadius: "1rem" }, 399 | ], 400 | { 401 | duration: 1000, 402 | iterations: Infinity, 403 | easing: "ease-in-out", 404 | } 405 | ); 406 | const move = useAnimation<{ x: number; y: number }>( 407 | (prev, pos) => [ 408 | { transform: prev.transform }, 409 | { transform: `translate(${pos.x}px, ${pos.y}px)` }, 410 | ], 411 | { 412 | duration: 400, 413 | easing: "ease-in-out", 414 | } 415 | ); 416 | 417 | useEffect(() => { 418 | rotate.play(); 419 | 420 | const onPointerMove = debounce((e: PointerEvent) => { 421 | move.play({ args: { x: e.clientX, y: e.clientY } }); 422 | }, 100); 423 | window.addEventListener("pointermove", onPointerMove); 424 | return () => { 425 | window.removeEventListener("pointermove", onPointerMove); 426 | }; 427 | }, []); 428 | 429 | return ( 430 |
431 |
Move mouse cursor.
432 |
433 |
444 |
445 |
446 | ); 447 | }, 448 | }; 449 | 450 | export const Toggle: StoryObj = { 451 | render: () => { 452 | const animate = useAnimation( 453 | { transform: "translate3d(400px, 0, 0)" }, 454 | { duration: 800, easing: "ease-in-out" } 455 | ); 456 | 457 | useEffect(() => { 458 | animate.play(); 459 | }, [animate]); 460 | 461 | return ( 462 |
463 | 464 |
465 |
466 |
467 | 486 |
487 | ); 488 | }, 489 | }; 490 | 491 | export const Path: StoryObj = { 492 | render: () => { 493 | const animate = useAnimation( 494 | [ 495 | { d: "path('M50,50L150,50L150,150L50,150z')" }, 496 | { d: "path('M100,30L170,170L30,170L30,170z')" }, 497 | ], 498 | { 499 | duration: 1000, 500 | easing: "ease-in-out", 501 | iterations: Infinity, 502 | direction: "alternate", 503 | } 504 | ); 505 | 506 | useEffect(() => { 507 | animate.play(); 508 | }, []); 509 | 510 | const width = 400; 511 | const height = 300; 512 | 513 | return ( 514 | 515 | 516 | 517 | ); 518 | }, 519 | }; 520 | 521 | export const ThreeDimentional: StoryObj = { 522 | render: () => { 523 | const [pos, setPos] = useState({ x: 1, y: 1 }); 524 | const animate = useAnimation( 525 | { transform: `rotate3d(${pos.y / 100}, ${pos.x / 100}, 0, 360deg)` }, 526 | { 527 | duration: 1000, 528 | iterations: Infinity, 529 | } 530 | ); 531 | 532 | useEffect(() => { 533 | animate.play(); 534 | }, [pos]); 535 | 536 | useEffect(() => { 537 | const onPointerMove = (e: PointerEvent) => { 538 | setPos({ x: e.clientX, y: e.clientY }); 539 | }; 540 | window.addEventListener("pointermove", onPointerMove); 541 | return () => { 542 | window.removeEventListener("pointermove", onPointerMove); 543 | }; 544 | }, []); 545 | 546 | return ( 547 | <> 548 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 | 603 | 604 | ); 605 | }, 606 | name: "3D", 607 | }; 608 | 609 | export const Sequence: StoryObj = { 610 | render: () => { 611 | const animate = useAnimation( 612 | (prev, color) => [{ fill: prev.fill }, { fill: color }], 613 | { duration: 600, easing: "ease-out" } 614 | ); 615 | 616 | const onClickAll = useCallback(async () => { 617 | try { 618 | await animate.play({ args: "red" }).waitFor("finish"); 619 | await animate.play({ args: "blue" }).waitFor("finish"); 620 | await animate.play({ args: "green" }).waitFor("finish"); 621 | } catch (e) { 622 | // ignore uncaught promise error 623 | } 624 | }, []); 625 | 626 | useEffect(() => { 627 | onClickAll(); 628 | }, []); 629 | 630 | return ( 631 |
632 | 633 | 637 | 638 |
639 | 646 | 653 | 660 | 661 |
662 |
663 | ); 664 | }, 665 | }; 666 | 667 | const WavedRect = ({ i }: { i: number }) => { 668 | const baseTiming: AnimationOptions = { 669 | easing: "ease-in-out", 670 | iterations: Infinity, 671 | direction: "alternate", 672 | delay: i * 98, 673 | }; 674 | const move = useAnimation( 675 | [ 676 | { transform: "translateY(0) scaleX(.8)" }, 677 | { transform: "translateY(95vh) scaleX(1)" }, 678 | ], 679 | { ...baseTiming, duration: 2500 } 680 | ); 681 | const opacity = useAnimation([{ opacity: 1 }, { opacity: 0 }], { 682 | ...baseTiming, 683 | duration: 2000, 684 | }); 685 | const color = useAnimation( 686 | [{ backgroundColor: "rgb(239, 239, 255)" }, { backgroundColor: "#e4c349" }], 687 | { ...baseTiming, duration: 3000 } 688 | ); 689 | 690 | useEffect(() => { 691 | move.play(); 692 | opacity.play(); 693 | color.play(); 694 | }, []); 695 | 696 | return ( 697 |
706 | ); 707 | }; 708 | 709 | export const Wave: StoryObj = { 710 | render: () => { 711 | const [rects] = useState(() => Array.from({ length: 20 }).map((_, i) => i)); 712 | 713 | return ( 714 |
723 | {rects.map((i) => ( 724 | 725 | ))} 726 |
727 | ); 728 | }, 729 | }; 730 | 731 | export const Countdown: StoryObj = { 732 | render: () => { 733 | const [count, setCount] = useState(10); 734 | const countAnimation = useAnimation( 735 | [ 736 | { opacity: 1, transform: "scale(.6)" }, 737 | { opacity: 0.5, transform: "scale(1)" }, 738 | ], 739 | { 740 | duration: 500, 741 | easing: "linear", 742 | delay: 0, 743 | iterations: 1000, 744 | direction: "alternate", 745 | } 746 | ); 747 | const boomAnimation = useAnimation( 748 | [ 749 | { 750 | opacity: 0, 751 | transform: "scale(.01) rotate(0deg)", 752 | color: "white", 753 | offset: 0, 754 | }, 755 | { 756 | opacity: 1, 757 | transform: "scale(6) rotate(360deg)", 758 | color: "orange", 759 | offset: 0.8, 760 | }, 761 | { 762 | opacity: 1, 763 | transform: "scale(1) rotate(720deg)", 764 | color: "white", 765 | offset: 1, 766 | }, 767 | ], 768 | { 769 | duration: 2000, 770 | easing: "ease-out", 771 | delay: 0, 772 | iterations: 1, 773 | } 774 | ); 775 | 776 | useEffect(() => { 777 | countAnimation.play(); 778 | let startCount = count; 779 | 780 | const id = setInterval(() => { 781 | startCount -= 1; 782 | setCount((p) => p - 1); 783 | 784 | if (startCount > 0) { 785 | countAnimation.setPlaybackRate((prev) => Math.min(prev * 1.15, 6)); 786 | } else { 787 | clearInterval(id); 788 | boomAnimation.play(); 789 | } 790 | }, 1000); 791 | 792 | return () => { 793 | clearInterval(id); 794 | }; 795 | }, []); 796 | return ( 797 |
807 | 811 | {count} 812 | 813 |
814 | ); 815 | }, 816 | }; 817 | 818 | // const Block = ({ i, length: n }: { i: number; length: number }) => { 819 | // const timing: AnimationOptions = { 820 | // duration: 250, 821 | // }; 822 | // const one = useAnimation( 823 | // [{ backgroundColor: "#eee" }, { backgroundColor: "steelblue" }], 824 | // { ...timing, endDelay: 1000 } 825 | // ); 826 | // const two = useAnimation( 827 | // [{ backgroundColor: "steelblue" }, { backgroundColor: "orange" }], 828 | // { ...timing, endDelay: 1000 } 829 | // ); 830 | // const three = useAnimation( 831 | // [{ backgroundColor: "orange" }, { backgroundColor: "#eee" }], 832 | // { ...timing, endDelay: n } 833 | // ); 834 | 835 | // useEffect(() => { 836 | // one.cancel(); 837 | // two.cancel(); 838 | // three.cancel(); 839 | // const run = async () => { 840 | // try { 841 | // await one.play().waitFor("finish"); 842 | // one.cancel(); 843 | // await two.play().waitFor("finish"); 844 | // two.cancel(); 845 | // await three.play().waitFor("finish"); 846 | // three.cancel(); 847 | // run(); 848 | // } catch (e) { 849 | // // ignore uncaught promise error 850 | // } 851 | // }; 852 | // setTimeout(run, i + (Math.random() * n) / 4); 853 | // }, []); 854 | 855 | // return ( 856 | //
867 | // ); 868 | // }; 869 | 870 | // export const Chained: StoryObj = { 871 | // render: () => { 872 | // const length = 4002; 873 | // return ( 874 | //
875 | // {Array.from({ length: length }).map((_, i) => ( 876 | // 877 | // ))} 878 | //
879 | // ); 880 | // }, 881 | // }; 882 | -------------------------------------------------------------------------------- /stories/hooks/useAnimationFunction.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useCallback, useEffect, useRef, useState } from "react"; 3 | import { useAnimationFunction } from "../../src"; 4 | 5 | export default { component: useAnimationFunction }; 6 | 7 | export const Text: StoryObj = { 8 | render: () => { 9 | const [enable, setEnable] = useState(true); 10 | const [time, setTime] = useState(0); 11 | const animate = useAnimationFunction( 12 | ({ progress }) => { 13 | setTime(progress); 14 | }, 15 | { 16 | duration: 1000, 17 | easing: "ease-in-out", 18 | iterations: Infinity, 19 | direction: "alternate", 20 | } 21 | ); 22 | 23 | useEffect(() => { 24 | animate.play(); 25 | }, [animate]); 26 | 27 | const onClick = useCallback(() => { 28 | setEnable((p) => !p); 29 | if (enable) { 30 | animate.pause(); 31 | } else { 32 | animate.play(); 33 | } 34 | }, [animate, enable]); 35 | 36 | return ( 37 |
38 | 39 |
{time.toFixed(6)}
40 |
41 | ); 42 | }, 43 | }; 44 | 45 | export const Progress: StoryObj = { 46 | render: () => { 47 | const [value, setValue] = useState(0); 48 | const animate = useAnimationFunction<{ start: number; end: number }>( 49 | ({ progress }, arg) => { 50 | setValue(arg.start * (1 - progress) + progress * arg.end); 51 | }, 52 | { 53 | duration: 600, 54 | easing: "ease-in-out", 55 | } 56 | ); 57 | useEffect(() => { 58 | animate.play({ args: { start: value, end: Math.random() * 100 } }); 59 | }, []); 60 | 61 | return ( 62 | <> 63 |
64 | 65 |
66 |
67 | 72 | 77 | 82 |
83 | 84 | ); 85 | }, 86 | }; 87 | 88 | export const Canvas: StoryObj = { 89 | render: () => { 90 | const ref = useRef(null); 91 | const ctxRef = useRef(null); 92 | const width = 400; 93 | const height = 400; 94 | const animate = useAnimationFunction( 95 | (timing) => { 96 | if (!ref.current) return; 97 | const ctx = 98 | ctxRef.current || (ctxRef.current = ref.current.getContext("2d")!); 99 | ctx.clearRect(0, 0, width, height); 100 | const fillStyle = ["red", "blue", "green"][timing.currentIteration % 3]; 101 | ctx.fillStyle = fillStyle; 102 | ctx.font = ctx.font.replace( 103 | /\d+px/, 104 | `${Math.floor(timing.progress * 50)}px` 105 | ); 106 | ctx.fillText(fillStyle, 40, 80); 107 | ctx.beginPath(); 108 | ctx.arc(width / 2, height / 2, 50 * timing.progress, 0, Math.PI * 2); 109 | ctx.fill(); 110 | }, 111 | { 112 | easing: "ease-in-out", 113 | duration: 1000, 114 | iterations: Infinity, 115 | } 116 | ); 117 | 118 | useEffect(() => { 119 | animate.play(); 120 | }, [animate]); 121 | 122 | return ; 123 | }, 124 | }; 125 | -------------------------------------------------------------------------------- /stories/hooks/useScrollTimeline.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect } from "react"; 3 | import { useAnimation, useScrollTimeline } from "../../src"; 4 | 5 | export default { component: useScrollTimeline }; 6 | 7 | export const Document: StoryObj = { 8 | render: () => { 9 | const size = 100; 10 | const height = 2000; 11 | 12 | const animate = useAnimation( 13 | [{ transform: `translate(0px, ${height - size}px) rotate(3600deg)` }], 14 | { 15 | duration: 500, 16 | easing: "ease-in", 17 | timeline: useScrollTimeline(), 18 | } 19 | ); 20 | useEffect(() => { 21 | animate.play(); 22 | }, []); 23 | 24 | return ( 25 |
31 | Please scroll down! 32 |
43 |
44 | ); 45 | }, 46 | }; 47 | 48 | export const Overflow: StoryObj = { 49 | render: () => { 50 | const size = 100; 51 | const height = 2000; 52 | 53 | const timeline = useScrollTimeline(); 54 | const animate = useAnimation( 55 | [{ transform: `translate(0px, ${height - size}px) rotate(3600deg)` }], 56 | { 57 | duration: 500, 58 | easing: "ease-in", 59 | timeline, 60 | } 61 | ); 62 | useEffect(() => { 63 | animate.play(); 64 | }, []); 65 | 66 | return ( 67 |
73 |
82 |
88 | Please scroll down! 89 |
100 |
101 |
102 |
103 | ); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /stories/hooks/useTransitionAnimation.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import { 4 | TransitionGroup, 5 | AnimationOptions, 6 | useTransitionAnimation, 7 | } from "../../src"; 8 | 9 | export default { component: useTransitionAnimation }; 10 | 11 | const usePrevious = (value: T) => { 12 | const prev = useRef(value); 13 | useEffect(() => { 14 | prev.current = value; 15 | }, [value]); 16 | return prev.current; 17 | }; 18 | 19 | const Text = ({ children }: { children: string }) => { 20 | const prev = usePrevious(children); 21 | 22 | const timing = { duration: 800, easing: "ease-out" }; 23 | const animate = useTransitionAnimation({ 24 | enter: [ 25 | [ 26 | { transform: "translateY(-20px)", opacity: 0.2 }, 27 | { transform: "translateY(0px)", opacity: 1 }, 28 | ], 29 | timing, 30 | ], 31 | exit: [ 32 | [ 33 | { transform: "translateY(0px)", opacity: 1 }, 34 | { transform: "translateY(20px)", opacity: 0.2 }, 35 | ], 36 | timing, 37 | ], 38 | update: [ 39 | [{ transform: "rotateX(360deg)" }, { transform: "rotateX(0deg)" }], 40 | children !== prev ? timing : undefined, 41 | ], 42 | }); 43 | 44 | return ( 45 | 49 | {children} 50 | 51 | ); 52 | }; 53 | 54 | export const Input: StoryObj = { 55 | render: () => { 56 | const [value, setValue] = useState("Animation"); 57 | return ( 58 |
59 |
60 | setValue(e.target.value)} /> 61 |
62 |
63 | 64 | {value.split("").map((t, i) => ( 65 | {t} 66 | ))} 67 | 68 |
69 |
70 | ); 71 | }, 72 | }; 73 | 74 | const shuffle = (array: T[]): T[] => { 75 | for (let i = array.length - 1; i >= 0; i--) { 76 | const j = Math.floor(Math.random() * (i + 1)); 77 | [array[i], array[j]] = [array[j], array[i]]; 78 | } 79 | return array; 80 | }; 81 | 82 | const SvgText = ({ children, i }: { children: string; i: number }) => { 83 | const x = i * 20; 84 | const prevX = usePrevious(x); 85 | const timing = { duration: 800, easing: "ease-in-out" }; 86 | const transition = useTransitionAnimation({ 87 | update: [ 88 | (prev) => [ 89 | { fill: prev.fill, transform: `translateX(${prevX - x}px)` }, 90 | { fill: "#333", transform: `translateX(0px)` }, 91 | ], 92 | timing, 93 | ], 94 | enter: [ 95 | [ 96 | { fill: "green", fillOpacity: "0", transform: "translateY(-20px)" }, 97 | { fill: "green", fillOpacity: "1", transform: "translateY(0px)" }, 98 | ], 99 | timing, 100 | ], 101 | exit: [ 102 | { fill: "brown", fillOpacity: "0", transform: "translateY(20px)" }, 103 | timing, 104 | ], 105 | }); 106 | 107 | return ( 108 | 109 | {children} 110 | 111 | ); 112 | }; 113 | 114 | const ALPHABETS = "abcdefghijklmnopqrstuvwxyz".split(""); 115 | 116 | export const Alphabet: StoryObj = { 117 | render: () => { 118 | const [texts, setTexts] = useState(ALPHABETS); 119 | useEffect(() => { 120 | const id = setInterval(() => { 121 | const shuffled = shuffle(ALPHABETS) 122 | .slice(0, Math.floor(Math.random() * 26)) 123 | .sort(); 124 | setTexts(shuffled); 125 | }, 1000); 126 | return () => { 127 | clearInterval(id); 128 | }; 129 | }, []); 130 | 131 | return ( 132 | <> 133 | 134 | 135 | 136 | {texts.map((t, i) => ( 137 | 138 | {t} 139 | 140 | ))} 141 | 142 | 143 | 144 | 145 | 146 | ); 147 | }, 148 | }; 149 | 150 | const ExpandRect = ({ i, length }: { i: number; length: number }) => { 151 | const timing: AnimationOptions = { 152 | easing: "ease-in-out", 153 | direction: "alternate", 154 | duration: 1000, 155 | }; 156 | 157 | const startStyle = (s: CSSStyleDeclaration, defaultScale: number) => { 158 | const transform = s.transform; 159 | const scale = 160 | transform.slice("matrix".length + 1, transform.indexOf(",") - 1) || 161 | defaultScale; 162 | return { 163 | backgroundColor: s.backgroundColor, 164 | transform: `scale(${scale})`, 165 | opacity: s.opacity, 166 | }; 167 | }; 168 | 169 | const transition = useTransitionAnimation({ 170 | enter: [ 171 | (prev) => [ 172 | startStyle(prev, 0), 173 | { backgroundColor: "skyblue", transform: "scale(1)", opacity: 1 }, 174 | ], 175 | { ...timing, delay: i * 100 }, 176 | ], 177 | exit: [ 178 | (prev) => [ 179 | startStyle(prev, 1), 180 | { 181 | backgroundColor: "limegreen", 182 | transform: "scale(0)", 183 | opacity: 0, 184 | }, 185 | ], 186 | { ...timing, delay: (length - i) * 100 }, 187 | ], 188 | }); 189 | 190 | return ( 191 |
203 | ); 204 | }; 205 | 206 | export const Expand: StoryObj = { 207 | render: () => { 208 | const [expanded, setExpanded] = useState(true); 209 | const length = 16; 210 | const [rects] = useState(() => Array.from({ length }).map((_, i) => i)); 211 | 212 | return ( 213 | <> 214 | 220 |
232 | 233 | {expanded 234 | ? rects.map((i) => ) 235 | : []} 236 | 237 |
238 | 239 | ); 240 | }, 241 | }; 242 | -------------------------------------------------------------------------------- /stories/hooks/useViewTimeline.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect } from "react"; 3 | import { useAnimation, useViewTimeline } from "../../src"; 4 | 5 | export default { component: useViewTimeline }; 6 | 7 | export const Document: StoryObj = { 8 | render: () => { 9 | const animate = useAnimation( 10 | [ 11 | { opacity: 0, transform: "scaleX(0)" }, 12 | { opacity: 1, transform: "scaleX(1)" }, 13 | ], 14 | { 15 | duration: 200, 16 | timeline: useViewTimeline(), 17 | } 18 | ); 19 | useEffect(() => { 20 | animate.play(); 21 | }, []); 22 | 23 | return ( 24 |
25 |

Content

26 |

27 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 28 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Risus quis 29 | varius quam quisque id. Et ligula ullamcorper malesuada proin libero 30 | nunc consequat interdum varius. Elit ullamcorper dignissim cras 31 | tincidunt lobortis feugiat vivamus at augue. 32 |

33 |

34 | Dolor sed viverra ipsum nunc aliquet. Sed sed risus pretium quam 35 | vulputate dignissim. Tortor aliquam nulla facilisi cras. A erat nam at 36 | lectus urna duis convallis convallis. Nibh ipsum consequat nisl vel 37 | pretium lectus. Sagittis aliquam malesuada bibendum arcu vitae 38 | elementum. Malesuada bibendum arcu vitae elementum curabitur vitae 39 | nunc sed velit. 40 |

41 |
50 |

51 | Adipiscing enim eu turpis egestas pretium aenean pharetra magna ac. 52 | Arcu cursus vitae congue mauris rhoncus aenean vel. Sit amet cursus 53 | sit amet dictum. Augue neque gravida in fermentum et. Gravida rutrum 54 | quisque non tellus orci ac auctor augue mauris. Risus quis varius quam 55 | quisque id diam vel quam elementum. Nibh praesent tristique magna sit 56 | amet purus gravida quis. Duis ultricies lacus sed turpis tincidunt id 57 | aliquet. In egestas erat imperdiet sed euismod nisi. Eget egestas 58 | purus viverra accumsan in nisl nisi scelerisque. Netus et malesuada 59 | fames ac. 60 |

61 |
62 | ); 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /stories/with-libraries/css-in-js/emotion.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect } from "react"; 3 | import { useAnimation } from "../../../src"; 4 | import { css } from "@emotion/css"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [ 12 | { transform: "rotate(0deg)", borderRadius: "1rem" }, 13 | { transform: "rotate(360deg)", borderRadius: "50%" }, 14 | { transform: "rotate(720deg)", borderRadius: "1rem" }, 15 | ], 16 | { 17 | duration: 1000, 18 | iterations: Infinity, 19 | easing: "ease-in-out", 20 | } 21 | ); 22 | 23 | useEffect(() => { 24 | animate.play(); 25 | }, []); 26 | 27 | return ( 28 |
{ 37 | animate.pause(); 38 | }} 39 | onMouseLeave={() => { 40 | animate.play(); 41 | }} 42 | /> 43 | ); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /stories/with-libraries/css-in-js/linaria.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect } from "react"; 3 | import { useAnimation } from "../../../src"; 4 | import { styled } from '@linaria/react'; 5 | 6 | export default { component: useAnimation }; 7 | 8 | const Comp = styled.div` 9 | border: solid 0.1rem #135569; 10 | height: 6rem; 11 | width: 6rem; 12 | margin: 2rem 0 2rem 2rem; 13 | `; 14 | 15 | export const Default: StoryObj = { 16 | render: () => { 17 | const animate = useAnimation( 18 | [ 19 | { transform: "rotate(0deg)", borderRadius: "1rem" }, 20 | { transform: "rotate(360deg)", borderRadius: "50%" }, 21 | { transform: "rotate(720deg)", borderRadius: "1rem" }, 22 | ], 23 | { 24 | duration: 1000, 25 | iterations: Infinity, 26 | easing: "ease-in-out", 27 | } 28 | ); 29 | 30 | useEffect(() => { 31 | animate.play(); 32 | }, []); 33 | 34 | return ( 35 | { 38 | animate.pause(); 39 | }} 40 | onMouseLeave={() => { 41 | animate.play(); 42 | }} 43 | /> 44 | ); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /stories/with-libraries/css-in-js/styled-components.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect } from "react"; 3 | import { useAnimation } from "../../../src"; 4 | import styled from "styled-components"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | const Comp = styled.div` 9 | border: solid 0.1rem #135569; 10 | height: 6rem; 11 | width: 6rem; 12 | margin: 2rem 0 2rem 2rem; 13 | `; 14 | 15 | export const Default: StoryObj = { 16 | render: () => { 17 | const animate = useAnimation( 18 | [ 19 | { transform: "rotate(0deg)", borderRadius: "1rem" }, 20 | { transform: "rotate(360deg)", borderRadius: "50%" }, 21 | { transform: "rotate(720deg)", borderRadius: "1rem" }, 22 | ], 23 | { 24 | duration: 1000, 25 | iterations: Infinity, 26 | easing: "ease-in-out", 27 | } 28 | ); 29 | 30 | useEffect(() => { 31 | animate.play(); 32 | }, []); 33 | 34 | return ( 35 | { 38 | animate.pause(); 39 | }} 40 | onMouseLeave={() => { 41 | animate.play(); 42 | }} 43 | /> 44 | ); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /stories/with-libraries/css-in-js/vanilla-extract.css.ts: -------------------------------------------------------------------------------- 1 | import { style } from "@vanilla-extract/css"; 2 | 3 | export const myStyle = style({ 4 | border: "solid 0.1rem #135569", 5 | height: "6rem", 6 | width: "6rem", 7 | margin: " 2rem 0 2rem 2rem", 8 | }); 9 | -------------------------------------------------------------------------------- /stories/with-libraries/css-in-js/vanilla-extract.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React, { useEffect } from "react"; 3 | import { useAnimation } from "../../../src"; 4 | import { myStyle } from "./vanilla-extract.css"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [ 12 | { transform: "rotate(0deg)", borderRadius: "1rem" }, 13 | { transform: "rotate(360deg)", borderRadius: "50%" }, 14 | { transform: "rotate(720deg)", borderRadius: "1rem" }, 15 | ], 16 | { 17 | duration: 1000, 18 | iterations: Infinity, 19 | easing: "ease-in-out", 20 | } 21 | ); 22 | 23 | useEffect(() => { 24 | animate.play(); 25 | }, []); 26 | 27 | return ( 28 |
{ 32 | animate.pause(); 33 | }} 34 | onMouseLeave={() => { 35 | animate.play(); 36 | }} 37 | /> 38 | ); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /stories/with-libraries/ui-components/Ant Design.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React from "react"; 3 | import { Button } from "antd"; 4 | import { useAnimation } from "../../../src"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [{ transform: "rotate(0deg)" }, { transform: "rotate(720deg)" }], 12 | { 13 | duration: 1000, 14 | easing: "ease-in-out", 15 | } 16 | ); 17 | 18 | return ( 19 | 27 | ); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /stories/with-libraries/ui-components/Chakra UI.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React from "react"; 3 | import { ChakraProvider, Button } from "@chakra-ui/react"; 4 | import { useAnimation } from "../../../src"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [{ transform: "rotate(0deg)" }, { transform: "rotate(720deg)" }], 12 | { 13 | duration: 1000, 14 | easing: "ease-in-out", 15 | } 16 | ); 17 | 18 | return ( 19 | 20 | 29 | 30 | ); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /stories/with-libraries/ui-components/Fluent UI.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React from "react"; 3 | import { PrimaryButton } from "@fluentui/react"; 4 | import { useAnimation } from "../../../src"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [{ transform: "rotate(0deg)" }, { transform: "rotate(720deg)" }], 12 | { 13 | duration: 1000, 14 | easing: "ease-in-out", 15 | } 16 | ); 17 | 18 | return ( 19 | { 22 | animate.play(); 23 | }} 24 | > 25 | Click Me! 26 | 27 | ); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /stories/with-libraries/ui-components/Material UI.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React from "react"; 3 | import Button from "@mui/material/Button"; 4 | import { useAnimation } from "../../../src"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [{ transform: "rotate(0deg)" }, { transform: "rotate(720deg)" }], 12 | { 13 | duration: 1000, 14 | easing: "ease-in-out", 15 | } 16 | ); 17 | 18 | return ( 19 | 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /stories/with-libraries/ui-components/mantine.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from "@storybook/react"; 2 | import React from "react"; 3 | import { MantineProvider, Button } from "@mantine/core"; 4 | import { useAnimation } from "../../../src"; 5 | 6 | export default { component: useAnimation }; 7 | 8 | export const Default: StoryObj = { 9 | render: () => { 10 | const animate = useAnimation( 11 | [{ transform: "rotate(0deg)" }, { transform: "rotate(720deg)" }], 12 | { 13 | duration: 1000, 14 | easing: "ease-in-out", 15 | } 16 | ); 17 | 18 | return ( 19 | 20 | 28 | 29 | ); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "jsx": "react-jsx", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "strict": true, 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noUncheckedIndexedAccess": true, 19 | "noPropertyAccessFromIndexSignature": true, 20 | "noImplicitReturns": true, 21 | "noImplicitOverride": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "allowUnreachableCode": false, 24 | "allowUnusedLabels": false, 25 | "exactOptionalPropertyTypes": true, 26 | "stripInternal": true 27 | }, 28 | "include": ["src"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./src/index.ts"], 3 | "out": "docs", 4 | "name": "API", 5 | "plugin": ["typedoc-plugin-markdown"], 6 | "readme": "none", 7 | "sort": ["kind"], 8 | "groupOrder": ["Function", "Interface", "TypeAlias"], 9 | "githubPages": false, 10 | "entryDocument": "API.md", 11 | "hideBreadcrumbs": true 12 | } 13 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | root: "src", 6 | environment: "jsdom", 7 | clearMocks: true, 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------