├── .gitignore ├── LICENSE ├── README.md ├── demo ├── .gitignore ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── src │ ├── App.tsx │ └── index.tsx ├── tsconfig.json └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── spring ├── .gitignore ├── package.json ├── rollup.config.js ├── src │ ├── AnimatedArray.ts │ ├── AnimatedObject.ts │ ├── AnimatedString.ts │ ├── AnimatedStyle.ts │ ├── Animation.ts │ ├── AnimationConfig.ts │ ├── AnimationResult.ts │ ├── Controller.ts │ ├── FrameLoop.ts │ ├── FrameValue.ts │ ├── Interpolation.ts │ ├── SpringPhase.ts │ ├── SpringRef.ts │ ├── SpringValue.ts │ ├── animated.ts │ ├── applyAnimatedValues.ts │ ├── colors.ts │ ├── context.ts │ ├── createHost.ts │ ├── createInterpolator.ts │ ├── fluids.ts │ ├── globals.ts │ ├── index.ts │ ├── normalizeColor.ts │ ├── primitives.ts │ ├── rafz.ts │ ├── runAsync.ts │ ├── scheduleProps.ts │ ├── solid │ │ ├── createSpring.ts │ │ └── createSprings.ts │ ├── utils.ts │ └── withAnimated.tsx └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 M. Bagher Abiat 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 | 2 |

solid-spring

3 |

A port of react-spring, for SolidJS

4 | 5 | `solid-spring` is a spring-physics first animation library for SolidJS based on react-spring/core. 6 | 7 | > This an experimental project that was made to be submitted in hack.solidjs.com, feel free to create github ticket 8 | 9 | The API looks like this: 10 | 11 | ```jsx 12 | const styles = createSpring({ 13 | from: { 14 | opacity: 0 15 | }, 16 | to: { 17 | opacity: 1 18 | } 19 | }) 20 | 21 | 22 | ``` 23 | 24 | The API is similar to what we have in react-spring, with small differences to make the library compatible with SolidJS 25 | 26 | ## Preview 27 | Click on the below gifs for exploring the code of each preview (ported from Poimandres examples). 28 | 29 | 30 |

31 | 32 | 33 |

34 | 35 | ## Install 36 | 37 | ```shell 38 | npm install solid-spring 39 | ``` 40 | ## Examples 41 | 42 | [Hello (opacity animation)](https://codesandbox.io/s/hello-qe3eq5?file=/index.tsx) 43 |
44 | [Svg (animating svg elements)](https://codesandbox.io/s/svg-omnp4c?file=/index.tsx) 45 |
46 | [Numbers (non style use case)](https://codesandbox.io/s/numbers-kbc57h?file=/index.tsx) 47 | 48 | ## API 49 | 50 | ### `createSpring` 51 | > Turns values into animated-values. 52 | 53 | ```jsx 54 | import { createSpring, animated } from "solid-spring"; 55 | 56 | function ChainExample() { 57 | const styles = createSpring({ 58 | loop: true, 59 | to: [ 60 | { opacity: 1, color: '#ffaaee' }, 61 | { opacity: 0, color: 'rgb(14,26,19)' }, 62 | ], 63 | from: { opacity: 0, color: 'red' }, 64 | }) 65 | 66 | return I will fade in and out 67 | } 68 | ``` 69 | `createSpring` also takes a function in case you want to pass a reactive value as a style! 70 | ```jsx 71 | const [disabled, setDisabled] = createSignal(false) 72 | 73 | const styles = createSpring(() => ({ 74 | pause: disabled(), 75 | })) 76 | ``` 77 | ### `createSprings` 78 | > Creates multiple springs, each with its own config. Use it for static lists, etc. 79 | 80 | Similar to `useSprings` in react-spring, It takes number or a function that returns a number (for reactivity) as the first argument, and a list of springs or a function that returns a spring as the second argument. 81 | 82 | ```jsx 83 | function createSprings( 84 | lengthFn: number | (() => number), 85 | props: Props[] & CreateSpringsProps>[] 86 | ): Accessor>[]> & { 87 | ref: SpringRefType>; 88 | }; 89 | 90 | function createSprings( 91 | lengthFn: number | (() => number), 92 | props: (i: number, ctrl: Controller) => Props 93 | ): Accessor>[]> & { 94 | ref: SpringRefType>; 95 | }; 96 | ``` 97 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. 4 | 5 | This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template. 6 | 7 | ```bash 8 | $ npm install # or pnpm install or yarn install 9 | ``` 10 | 11 | ### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) 12 | 13 | ## Available Scripts 14 | 15 | In the project directory, you can run: 16 | 17 | ### `npm dev` or `npm start` 18 | 19 | Runs the app in the development mode.
20 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 21 | 22 | The page will reload if you make edits.
23 | 24 | ### `npm run build` 25 | 26 | Builds the app for production to the `dist` folder.
27 | It correctly bundles Solid in production mode and optimizes the build for the best performance. 28 | 29 | The build is minified and the filenames include the hashes.
30 | Your app is ready to be deployed! 31 | 32 | ## Deployment 33 | 34 | You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) 35 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-template-solid", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "vite", 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview" 10 | }, 11 | "license": "MIT", 12 | "devDependencies": { 13 | "typescript": "^4.6.3", 14 | "vite": "^2.8.6", 15 | "vite-plugin-solid": "^2.2.6" 16 | }, 17 | "dependencies": { 18 | "@react-spring/animated": "^9.4.4", 19 | "@react-spring/core": "^9.4.4", 20 | "solid-js": "^1.3.13", 21 | "solid-spring": "workspace:^0.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | '@react-spring/animated': ^9.4.4 5 | '@react-spring/core': ^9.4.4 6 | solid-js: ^1.3.13 7 | typescript: ^4.6.3 8 | vite: ^2.8.6 9 | vite-plugin-solid: ^2.2.6 10 | 11 | dependencies: 12 | '@react-spring/animated': 9.4.4 13 | '@react-spring/core': 9.4.4 14 | solid-js: 1.3.13 15 | 16 | devDependencies: 17 | typescript: 4.6.3 18 | vite: 2.8.6 19 | vite-plugin-solid: 2.2.6 20 | 21 | packages: 22 | 23 | /@ampproject/remapping/2.1.2: 24 | resolution: {integrity: sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==} 25 | engines: {node: '>=6.0.0'} 26 | dependencies: 27 | '@jridgewell/trace-mapping': 0.3.4 28 | dev: true 29 | 30 | /@babel/code-frame/7.16.7: 31 | resolution: {integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==} 32 | engines: {node: '>=6.9.0'} 33 | dependencies: 34 | '@babel/highlight': 7.16.10 35 | dev: true 36 | 37 | /@babel/compat-data/7.17.7: 38 | resolution: {integrity: sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==} 39 | engines: {node: '>=6.9.0'} 40 | dev: true 41 | 42 | /@babel/core/7.17.8: 43 | resolution: {integrity: sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==} 44 | engines: {node: '>=6.9.0'} 45 | dependencies: 46 | '@ampproject/remapping': 2.1.2 47 | '@babel/code-frame': 7.16.7 48 | '@babel/generator': 7.17.7 49 | '@babel/helper-compilation-targets': 7.17.7_@babel+core@7.17.8 50 | '@babel/helper-module-transforms': 7.17.7 51 | '@babel/helpers': 7.17.8 52 | '@babel/parser': 7.17.8 53 | '@babel/template': 7.16.7 54 | '@babel/traverse': 7.17.3 55 | '@babel/types': 7.17.0 56 | convert-source-map: 1.8.0 57 | debug: 4.3.4 58 | gensync: 1.0.0-beta.2 59 | json5: 2.2.1 60 | semver: 6.3.0 61 | transitivePeerDependencies: 62 | - supports-color 63 | dev: true 64 | 65 | /@babel/generator/7.17.7: 66 | resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} 67 | engines: {node: '>=6.9.0'} 68 | dependencies: 69 | '@babel/types': 7.17.0 70 | jsesc: 2.5.2 71 | source-map: 0.5.7 72 | dev: true 73 | 74 | /@babel/helper-annotate-as-pure/7.16.7: 75 | resolution: {integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==} 76 | engines: {node: '>=6.9.0'} 77 | dependencies: 78 | '@babel/types': 7.17.0 79 | dev: true 80 | 81 | /@babel/helper-compilation-targets/7.17.7_@babel+core@7.17.8: 82 | resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} 83 | engines: {node: '>=6.9.0'} 84 | peerDependencies: 85 | '@babel/core': ^7.0.0 86 | dependencies: 87 | '@babel/compat-data': 7.17.7 88 | '@babel/core': 7.17.8 89 | '@babel/helper-validator-option': 7.16.7 90 | browserslist: 4.20.2 91 | semver: 6.3.0 92 | dev: true 93 | 94 | /@babel/helper-create-class-features-plugin/7.17.6_@babel+core@7.17.8: 95 | resolution: {integrity: sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==} 96 | engines: {node: '>=6.9.0'} 97 | peerDependencies: 98 | '@babel/core': ^7.0.0 99 | dependencies: 100 | '@babel/core': 7.17.8 101 | '@babel/helper-annotate-as-pure': 7.16.7 102 | '@babel/helper-environment-visitor': 7.16.7 103 | '@babel/helper-function-name': 7.16.7 104 | '@babel/helper-member-expression-to-functions': 7.17.7 105 | '@babel/helper-optimise-call-expression': 7.16.7 106 | '@babel/helper-replace-supers': 7.16.7 107 | '@babel/helper-split-export-declaration': 7.16.7 108 | transitivePeerDependencies: 109 | - supports-color 110 | dev: true 111 | 112 | /@babel/helper-environment-visitor/7.16.7: 113 | resolution: {integrity: sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==} 114 | engines: {node: '>=6.9.0'} 115 | dependencies: 116 | '@babel/types': 7.17.0 117 | dev: true 118 | 119 | /@babel/helper-function-name/7.16.7: 120 | resolution: {integrity: sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==} 121 | engines: {node: '>=6.9.0'} 122 | dependencies: 123 | '@babel/helper-get-function-arity': 7.16.7 124 | '@babel/template': 7.16.7 125 | '@babel/types': 7.17.0 126 | dev: true 127 | 128 | /@babel/helper-get-function-arity/7.16.7: 129 | resolution: {integrity: sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==} 130 | engines: {node: '>=6.9.0'} 131 | dependencies: 132 | '@babel/types': 7.17.0 133 | dev: true 134 | 135 | /@babel/helper-hoist-variables/7.16.7: 136 | resolution: {integrity: sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==} 137 | engines: {node: '>=6.9.0'} 138 | dependencies: 139 | '@babel/types': 7.17.0 140 | dev: true 141 | 142 | /@babel/helper-member-expression-to-functions/7.17.7: 143 | resolution: {integrity: sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==} 144 | engines: {node: '>=6.9.0'} 145 | dependencies: 146 | '@babel/types': 7.17.0 147 | dev: true 148 | 149 | /@babel/helper-module-imports/7.16.0: 150 | resolution: {integrity: sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==} 151 | engines: {node: '>=6.9.0'} 152 | dependencies: 153 | '@babel/types': 7.17.0 154 | dev: true 155 | 156 | /@babel/helper-module-imports/7.16.7: 157 | resolution: {integrity: sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==} 158 | engines: {node: '>=6.9.0'} 159 | dependencies: 160 | '@babel/types': 7.17.0 161 | dev: true 162 | 163 | /@babel/helper-module-transforms/7.17.7: 164 | resolution: {integrity: sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==} 165 | engines: {node: '>=6.9.0'} 166 | dependencies: 167 | '@babel/helper-environment-visitor': 7.16.7 168 | '@babel/helper-module-imports': 7.16.7 169 | '@babel/helper-simple-access': 7.17.7 170 | '@babel/helper-split-export-declaration': 7.16.7 171 | '@babel/helper-validator-identifier': 7.16.7 172 | '@babel/template': 7.16.7 173 | '@babel/traverse': 7.17.3 174 | '@babel/types': 7.17.0 175 | transitivePeerDependencies: 176 | - supports-color 177 | dev: true 178 | 179 | /@babel/helper-optimise-call-expression/7.16.7: 180 | resolution: {integrity: sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==} 181 | engines: {node: '>=6.9.0'} 182 | dependencies: 183 | '@babel/types': 7.17.0 184 | dev: true 185 | 186 | /@babel/helper-plugin-utils/7.16.7: 187 | resolution: {integrity: sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==} 188 | engines: {node: '>=6.9.0'} 189 | dev: true 190 | 191 | /@babel/helper-replace-supers/7.16.7: 192 | resolution: {integrity: sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==} 193 | engines: {node: '>=6.9.0'} 194 | dependencies: 195 | '@babel/helper-environment-visitor': 7.16.7 196 | '@babel/helper-member-expression-to-functions': 7.17.7 197 | '@babel/helper-optimise-call-expression': 7.16.7 198 | '@babel/traverse': 7.17.3 199 | '@babel/types': 7.17.0 200 | transitivePeerDependencies: 201 | - supports-color 202 | dev: true 203 | 204 | /@babel/helper-simple-access/7.17.7: 205 | resolution: {integrity: sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==} 206 | engines: {node: '>=6.9.0'} 207 | dependencies: 208 | '@babel/types': 7.17.0 209 | dev: true 210 | 211 | /@babel/helper-split-export-declaration/7.16.7: 212 | resolution: {integrity: sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==} 213 | engines: {node: '>=6.9.0'} 214 | dependencies: 215 | '@babel/types': 7.17.0 216 | dev: true 217 | 218 | /@babel/helper-validator-identifier/7.16.7: 219 | resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} 220 | engines: {node: '>=6.9.0'} 221 | dev: true 222 | 223 | /@babel/helper-validator-option/7.16.7: 224 | resolution: {integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==} 225 | engines: {node: '>=6.9.0'} 226 | dev: true 227 | 228 | /@babel/helpers/7.17.8: 229 | resolution: {integrity: sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==} 230 | engines: {node: '>=6.9.0'} 231 | dependencies: 232 | '@babel/template': 7.16.7 233 | '@babel/traverse': 7.17.3 234 | '@babel/types': 7.17.0 235 | transitivePeerDependencies: 236 | - supports-color 237 | dev: true 238 | 239 | /@babel/highlight/7.16.10: 240 | resolution: {integrity: sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==} 241 | engines: {node: '>=6.9.0'} 242 | dependencies: 243 | '@babel/helper-validator-identifier': 7.16.7 244 | chalk: 2.4.2 245 | js-tokens: 4.0.0 246 | dev: true 247 | 248 | /@babel/parser/7.17.8: 249 | resolution: {integrity: sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==} 250 | engines: {node: '>=6.0.0'} 251 | hasBin: true 252 | dev: true 253 | 254 | /@babel/plugin-syntax-jsx/7.16.7_@babel+core@7.17.8: 255 | resolution: {integrity: sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==} 256 | engines: {node: '>=6.9.0'} 257 | peerDependencies: 258 | '@babel/core': ^7.0.0-0 259 | dependencies: 260 | '@babel/core': 7.17.8 261 | '@babel/helper-plugin-utils': 7.16.7 262 | dev: true 263 | 264 | /@babel/plugin-syntax-typescript/7.16.7_@babel+core@7.17.8: 265 | resolution: {integrity: sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==} 266 | engines: {node: '>=6.9.0'} 267 | peerDependencies: 268 | '@babel/core': ^7.0.0-0 269 | dependencies: 270 | '@babel/core': 7.17.8 271 | '@babel/helper-plugin-utils': 7.16.7 272 | dev: true 273 | 274 | /@babel/plugin-transform-typescript/7.16.8_@babel+core@7.17.8: 275 | resolution: {integrity: sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==} 276 | engines: {node: '>=6.9.0'} 277 | peerDependencies: 278 | '@babel/core': ^7.0.0-0 279 | dependencies: 280 | '@babel/core': 7.17.8 281 | '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.17.8 282 | '@babel/helper-plugin-utils': 7.16.7 283 | '@babel/plugin-syntax-typescript': 7.16.7_@babel+core@7.17.8 284 | transitivePeerDependencies: 285 | - supports-color 286 | dev: true 287 | 288 | /@babel/preset-typescript/7.16.7_@babel+core@7.17.8: 289 | resolution: {integrity: sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==} 290 | engines: {node: '>=6.9.0'} 291 | peerDependencies: 292 | '@babel/core': ^7.0.0-0 293 | dependencies: 294 | '@babel/core': 7.17.8 295 | '@babel/helper-plugin-utils': 7.16.7 296 | '@babel/helper-validator-option': 7.16.7 297 | '@babel/plugin-transform-typescript': 7.16.8_@babel+core@7.17.8 298 | transitivePeerDependencies: 299 | - supports-color 300 | dev: true 301 | 302 | /@babel/template/7.16.7: 303 | resolution: {integrity: sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==} 304 | engines: {node: '>=6.9.0'} 305 | dependencies: 306 | '@babel/code-frame': 7.16.7 307 | '@babel/parser': 7.17.8 308 | '@babel/types': 7.17.0 309 | dev: true 310 | 311 | /@babel/traverse/7.17.3: 312 | resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==} 313 | engines: {node: '>=6.9.0'} 314 | dependencies: 315 | '@babel/code-frame': 7.16.7 316 | '@babel/generator': 7.17.7 317 | '@babel/helper-environment-visitor': 7.16.7 318 | '@babel/helper-function-name': 7.16.7 319 | '@babel/helper-hoist-variables': 7.16.7 320 | '@babel/helper-split-export-declaration': 7.16.7 321 | '@babel/parser': 7.17.8 322 | '@babel/types': 7.17.0 323 | debug: 4.3.4 324 | globals: 11.12.0 325 | transitivePeerDependencies: 326 | - supports-color 327 | dev: true 328 | 329 | /@babel/types/7.17.0: 330 | resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} 331 | engines: {node: '>=6.9.0'} 332 | dependencies: 333 | '@babel/helper-validator-identifier': 7.16.7 334 | to-fast-properties: 2.0.0 335 | dev: true 336 | 337 | /@jridgewell/resolve-uri/3.0.5: 338 | resolution: {integrity: sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==} 339 | engines: {node: '>=6.0.0'} 340 | dev: true 341 | 342 | /@jridgewell/sourcemap-codec/1.4.11: 343 | resolution: {integrity: sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==} 344 | dev: true 345 | 346 | /@jridgewell/trace-mapping/0.3.4: 347 | resolution: {integrity: sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==} 348 | dependencies: 349 | '@jridgewell/resolve-uri': 3.0.5 350 | '@jridgewell/sourcemap-codec': 1.4.11 351 | dev: true 352 | 353 | /@react-spring/animated/9.4.4: 354 | resolution: {integrity: sha512-e9xnuBaUTD+NolKikUmrGWjX8AVCPyj1GcEgjgq9E+0sXKv46UY7cm2EmB6mUDTxWIDVKebARY++xT4nGDraBQ==} 355 | peerDependencies: 356 | react: ^16.8.0 || ^17.0.0 357 | dependencies: 358 | '@react-spring/shared': 9.4.4 359 | '@react-spring/types': 9.4.4 360 | dev: false 361 | 362 | /@react-spring/core/9.4.4: 363 | resolution: {integrity: sha512-llgb0ljFyjMB0JhWsaFHOi9XFT8n1jBMVs1IFY2ipIBerWIRWrgUmIpakLPHTa4c4jwqTaDSwX90s2a0iN7dxQ==} 364 | peerDependencies: 365 | react: ^16.8.0 || ^17.0.0 366 | dependencies: 367 | '@react-spring/animated': 9.4.4 368 | '@react-spring/rafz': 9.4.4 369 | '@react-spring/shared': 9.4.4 370 | '@react-spring/types': 9.4.4 371 | dev: false 372 | 373 | /@react-spring/rafz/9.4.4: 374 | resolution: {integrity: sha512-5ki/sQ06Mdf8AuFstSt5zbNNicRT4LZogiJttDAww1ozhuvemafNWEHxhzcULgCPCDu2s7HsroaISV7+GQWrhw==} 375 | dev: false 376 | 377 | /@react-spring/shared/9.4.4: 378 | resolution: {integrity: sha512-ySVgScDZlhm/+Iy2smY9i/DDrShArY0j6zjTS/Re1lasKnhq8qigoGiAxe8xMPJNlCaj3uczCqHy3TY9bKRtfQ==} 379 | peerDependencies: 380 | react: ^16.8.0 || ^17.0.0 381 | dependencies: 382 | '@react-spring/rafz': 9.4.4 383 | '@react-spring/types': 9.4.4 384 | dev: false 385 | 386 | /@react-spring/types/9.4.4: 387 | resolution: {integrity: sha512-KpxKt/D//q/t/6FBcde/RE36LKp8PpWu7kFEMLwpzMGl9RpcexunmYOQJWwmJWtkQjgE1YRr7DzBMryz6La1cQ==} 388 | dev: false 389 | 390 | /ansi-styles/3.2.1: 391 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 392 | engines: {node: '>=4'} 393 | dependencies: 394 | color-convert: 1.9.3 395 | dev: true 396 | 397 | /babel-plugin-jsx-dom-expressions/0.32.11_@babel+core@7.17.8: 398 | resolution: {integrity: sha512-hytqY33SGW6B3obSLt8K5X510UwtNkTktCCWgwba+QOOV0CowDFiqeL+0ru895FLacFaYANHFTu1y76dg3GVtw==} 399 | dependencies: 400 | '@babel/helper-module-imports': 7.16.0 401 | '@babel/plugin-syntax-jsx': 7.16.7_@babel+core@7.17.8 402 | '@babel/types': 7.17.0 403 | html-entities: 2.3.2 404 | transitivePeerDependencies: 405 | - '@babel/core' 406 | dev: true 407 | 408 | /babel-preset-solid/1.3.13_@babel+core@7.17.8: 409 | resolution: {integrity: sha512-MZnmsceI9yiHlwwFCSALTJhadk2eea/+2UP4ec4jkPZFR+XRKTLoIwRkrBh7uLtvHF+3lHGyUaXtZukOmmUwhA==} 410 | dependencies: 411 | babel-plugin-jsx-dom-expressions: 0.32.11_@babel+core@7.17.8 412 | transitivePeerDependencies: 413 | - '@babel/core' 414 | dev: true 415 | 416 | /browserslist/4.20.2: 417 | resolution: {integrity: sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==} 418 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 419 | hasBin: true 420 | dependencies: 421 | caniuse-lite: 1.0.30001320 422 | electron-to-chromium: 1.4.93 423 | escalade: 3.1.1 424 | node-releases: 2.0.2 425 | picocolors: 1.0.0 426 | dev: true 427 | 428 | /caniuse-lite/1.0.30001320: 429 | resolution: {integrity: sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==} 430 | dev: true 431 | 432 | /chalk/2.4.2: 433 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 434 | engines: {node: '>=4'} 435 | dependencies: 436 | ansi-styles: 3.2.1 437 | escape-string-regexp: 1.0.5 438 | supports-color: 5.5.0 439 | dev: true 440 | 441 | /color-convert/1.9.3: 442 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 443 | dependencies: 444 | color-name: 1.1.3 445 | dev: true 446 | 447 | /color-name/1.1.3: 448 | resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} 449 | dev: true 450 | 451 | /convert-source-map/1.8.0: 452 | resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} 453 | dependencies: 454 | safe-buffer: 5.1.2 455 | dev: true 456 | 457 | /debug/4.3.4: 458 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 459 | engines: {node: '>=6.0'} 460 | peerDependencies: 461 | supports-color: '*' 462 | peerDependenciesMeta: 463 | supports-color: 464 | optional: true 465 | dependencies: 466 | ms: 2.1.2 467 | dev: true 468 | 469 | /electron-to-chromium/1.4.93: 470 | resolution: {integrity: sha512-ywq9Pc5Gwwpv7NG767CtoU8xF3aAUQJjH9//Wy3MBCg4w5JSLbJUq2L8IsCdzPMjvSgxuue9WcVaTOyyxCL0aQ==} 471 | dev: true 472 | 473 | /esbuild-android-64/0.14.27: 474 | resolution: {integrity: sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ==} 475 | engines: {node: '>=12'} 476 | cpu: [x64] 477 | os: [android] 478 | requiresBuild: true 479 | dev: true 480 | optional: true 481 | 482 | /esbuild-android-arm64/0.14.27: 483 | resolution: {integrity: sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ==} 484 | engines: {node: '>=12'} 485 | cpu: [arm64] 486 | os: [android] 487 | requiresBuild: true 488 | dev: true 489 | optional: true 490 | 491 | /esbuild-darwin-64/0.14.27: 492 | resolution: {integrity: sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g==} 493 | engines: {node: '>=12'} 494 | cpu: [x64] 495 | os: [darwin] 496 | requiresBuild: true 497 | dev: true 498 | optional: true 499 | 500 | /esbuild-darwin-arm64/0.14.27: 501 | resolution: {integrity: sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ==} 502 | engines: {node: '>=12'} 503 | cpu: [arm64] 504 | os: [darwin] 505 | requiresBuild: true 506 | dev: true 507 | optional: true 508 | 509 | /esbuild-freebsd-64/0.14.27: 510 | resolution: {integrity: sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA==} 511 | engines: {node: '>=12'} 512 | cpu: [x64] 513 | os: [freebsd] 514 | requiresBuild: true 515 | dev: true 516 | optional: true 517 | 518 | /esbuild-freebsd-arm64/0.14.27: 519 | resolution: {integrity: sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA==} 520 | engines: {node: '>=12'} 521 | cpu: [arm64] 522 | os: [freebsd] 523 | requiresBuild: true 524 | dev: true 525 | optional: true 526 | 527 | /esbuild-linux-32/0.14.27: 528 | resolution: {integrity: sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw==} 529 | engines: {node: '>=12'} 530 | cpu: [ia32] 531 | os: [linux] 532 | requiresBuild: true 533 | dev: true 534 | optional: true 535 | 536 | /esbuild-linux-64/0.14.27: 537 | resolution: {integrity: sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg==} 538 | engines: {node: '>=12'} 539 | cpu: [x64] 540 | os: [linux] 541 | requiresBuild: true 542 | dev: true 543 | optional: true 544 | 545 | /esbuild-linux-arm/0.14.27: 546 | resolution: {integrity: sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw==} 547 | engines: {node: '>=12'} 548 | cpu: [arm] 549 | os: [linux] 550 | requiresBuild: true 551 | dev: true 552 | optional: true 553 | 554 | /esbuild-linux-arm64/0.14.27: 555 | resolution: {integrity: sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ==} 556 | engines: {node: '>=12'} 557 | cpu: [arm64] 558 | os: [linux] 559 | requiresBuild: true 560 | dev: true 561 | optional: true 562 | 563 | /esbuild-linux-mips64le/0.14.27: 564 | resolution: {integrity: sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A==} 565 | engines: {node: '>=12'} 566 | cpu: [mips64el] 567 | os: [linux] 568 | requiresBuild: true 569 | dev: true 570 | optional: true 571 | 572 | /esbuild-linux-ppc64le/0.14.27: 573 | resolution: {integrity: sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA==} 574 | engines: {node: '>=12'} 575 | cpu: [ppc64] 576 | os: [linux] 577 | requiresBuild: true 578 | dev: true 579 | optional: true 580 | 581 | /esbuild-linux-riscv64/0.14.27: 582 | resolution: {integrity: sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg==} 583 | engines: {node: '>=12'} 584 | cpu: [riscv64] 585 | os: [linux] 586 | requiresBuild: true 587 | dev: true 588 | optional: true 589 | 590 | /esbuild-linux-s390x/0.14.27: 591 | resolution: {integrity: sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg==} 592 | engines: {node: '>=12'} 593 | cpu: [s390x] 594 | os: [linux] 595 | requiresBuild: true 596 | dev: true 597 | optional: true 598 | 599 | /esbuild-netbsd-64/0.14.27: 600 | resolution: {integrity: sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q==} 601 | engines: {node: '>=12'} 602 | cpu: [x64] 603 | os: [netbsd] 604 | requiresBuild: true 605 | dev: true 606 | optional: true 607 | 608 | /esbuild-openbsd-64/0.14.27: 609 | resolution: {integrity: sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw==} 610 | engines: {node: '>=12'} 611 | cpu: [x64] 612 | os: [openbsd] 613 | requiresBuild: true 614 | dev: true 615 | optional: true 616 | 617 | /esbuild-sunos-64/0.14.27: 618 | resolution: {integrity: sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg==} 619 | engines: {node: '>=12'} 620 | cpu: [x64] 621 | os: [sunos] 622 | requiresBuild: true 623 | dev: true 624 | optional: true 625 | 626 | /esbuild-windows-32/0.14.27: 627 | resolution: {integrity: sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw==} 628 | engines: {node: '>=12'} 629 | cpu: [ia32] 630 | os: [win32] 631 | requiresBuild: true 632 | dev: true 633 | optional: true 634 | 635 | /esbuild-windows-64/0.14.27: 636 | resolution: {integrity: sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg==} 637 | engines: {node: '>=12'} 638 | cpu: [x64] 639 | os: [win32] 640 | requiresBuild: true 641 | dev: true 642 | optional: true 643 | 644 | /esbuild-windows-arm64/0.14.27: 645 | resolution: {integrity: sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg==} 646 | engines: {node: '>=12'} 647 | cpu: [arm64] 648 | os: [win32] 649 | requiresBuild: true 650 | dev: true 651 | optional: true 652 | 653 | /esbuild/0.14.27: 654 | resolution: {integrity: sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==} 655 | engines: {node: '>=12'} 656 | hasBin: true 657 | requiresBuild: true 658 | optionalDependencies: 659 | esbuild-android-64: 0.14.27 660 | esbuild-android-arm64: 0.14.27 661 | esbuild-darwin-64: 0.14.27 662 | esbuild-darwin-arm64: 0.14.27 663 | esbuild-freebsd-64: 0.14.27 664 | esbuild-freebsd-arm64: 0.14.27 665 | esbuild-linux-32: 0.14.27 666 | esbuild-linux-64: 0.14.27 667 | esbuild-linux-arm: 0.14.27 668 | esbuild-linux-arm64: 0.14.27 669 | esbuild-linux-mips64le: 0.14.27 670 | esbuild-linux-ppc64le: 0.14.27 671 | esbuild-linux-riscv64: 0.14.27 672 | esbuild-linux-s390x: 0.14.27 673 | esbuild-netbsd-64: 0.14.27 674 | esbuild-openbsd-64: 0.14.27 675 | esbuild-sunos-64: 0.14.27 676 | esbuild-windows-32: 0.14.27 677 | esbuild-windows-64: 0.14.27 678 | esbuild-windows-arm64: 0.14.27 679 | dev: true 680 | 681 | /escalade/3.1.1: 682 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 683 | engines: {node: '>=6'} 684 | dev: true 685 | 686 | /escape-string-regexp/1.0.5: 687 | resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} 688 | engines: {node: '>=0.8.0'} 689 | dev: true 690 | 691 | /fsevents/2.3.2: 692 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 693 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 694 | os: [darwin] 695 | requiresBuild: true 696 | dev: true 697 | optional: true 698 | 699 | /function-bind/1.1.1: 700 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 701 | dev: true 702 | 703 | /gensync/1.0.0-beta.2: 704 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 705 | engines: {node: '>=6.9.0'} 706 | dev: true 707 | 708 | /globals/11.12.0: 709 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 710 | engines: {node: '>=4'} 711 | dev: true 712 | 713 | /has-flag/3.0.0: 714 | resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} 715 | engines: {node: '>=4'} 716 | dev: true 717 | 718 | /has/1.0.3: 719 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 720 | engines: {node: '>= 0.4.0'} 721 | dependencies: 722 | function-bind: 1.1.1 723 | dev: true 724 | 725 | /html-entities/2.3.2: 726 | resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==} 727 | dev: true 728 | 729 | /is-core-module/2.8.1: 730 | resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==} 731 | dependencies: 732 | has: 1.0.3 733 | dev: true 734 | 735 | /is-what/4.1.7: 736 | resolution: {integrity: sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==} 737 | engines: {node: '>=12.13'} 738 | dev: true 739 | 740 | /js-tokens/4.0.0: 741 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 742 | dev: true 743 | 744 | /jsesc/2.5.2: 745 | resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} 746 | engines: {node: '>=4'} 747 | hasBin: true 748 | dev: true 749 | 750 | /json5/2.2.1: 751 | resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} 752 | engines: {node: '>=6'} 753 | hasBin: true 754 | dev: true 755 | 756 | /merge-anything/5.0.2: 757 | resolution: {integrity: sha512-POPQBWkBC0vxdgzRJ2Mkj4+2NTKbvkHo93ih+jGDhNMLzIw+rYKjO7949hOQM2X7DxMHH1uoUkwWFLIzImw7gA==} 758 | engines: {node: '>=12.13'} 759 | dependencies: 760 | is-what: 4.1.7 761 | ts-toolbelt: 9.6.0 762 | dev: true 763 | 764 | /ms/2.1.2: 765 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 766 | dev: true 767 | 768 | /nanoid/3.3.1: 769 | resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} 770 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 771 | hasBin: true 772 | dev: true 773 | 774 | /node-releases/2.0.2: 775 | resolution: {integrity: sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==} 776 | dev: true 777 | 778 | /path-parse/1.0.7: 779 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 780 | dev: true 781 | 782 | /picocolors/1.0.0: 783 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 784 | dev: true 785 | 786 | /postcss/8.4.12: 787 | resolution: {integrity: sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==} 788 | engines: {node: ^10 || ^12 || >=14} 789 | dependencies: 790 | nanoid: 3.3.1 791 | picocolors: 1.0.0 792 | source-map-js: 1.0.2 793 | dev: true 794 | 795 | /resolve/1.22.0: 796 | resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} 797 | hasBin: true 798 | dependencies: 799 | is-core-module: 2.8.1 800 | path-parse: 1.0.7 801 | supports-preserve-symlinks-flag: 1.0.0 802 | dev: true 803 | 804 | /rollup/2.70.1: 805 | resolution: {integrity: sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==} 806 | engines: {node: '>=10.0.0'} 807 | hasBin: true 808 | optionalDependencies: 809 | fsevents: 2.3.2 810 | dev: true 811 | 812 | /safe-buffer/5.1.2: 813 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 814 | dev: true 815 | 816 | /semver/6.3.0: 817 | resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} 818 | hasBin: true 819 | dev: true 820 | 821 | /solid-js/1.3.13: 822 | resolution: {integrity: sha512-1EBEIW9u2yqT5QNjFdvz/tMAoKsDdaRA2Jbgykd2Dt13Ia0D4mV+BFvPkOaseSyu7DsMKS23+ZZofV8BVKmpuQ==} 823 | 824 | /solid-refresh/0.4.0_solid-js@1.3.13: 825 | resolution: {integrity: sha512-5XCUz845n/sHPzKK2i2G2EeV61tAmzv6SqzqhXcPaYhrgzVy7nKTQaBpKK8InKrriq9Z2JFF/mguIU00t/73xw==} 826 | peerDependencies: 827 | solid-js: ^1.3.0 828 | dependencies: 829 | '@babel/generator': 7.17.7 830 | '@babel/helper-module-imports': 7.16.7 831 | '@babel/types': 7.17.0 832 | solid-js: 1.3.13 833 | dev: true 834 | 835 | /source-map-js/1.0.2: 836 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 837 | engines: {node: '>=0.10.0'} 838 | dev: true 839 | 840 | /source-map/0.5.7: 841 | resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} 842 | engines: {node: '>=0.10.0'} 843 | dev: true 844 | 845 | /supports-color/5.5.0: 846 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 847 | engines: {node: '>=4'} 848 | dependencies: 849 | has-flag: 3.0.0 850 | dev: true 851 | 852 | /supports-preserve-symlinks-flag/1.0.0: 853 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 854 | engines: {node: '>= 0.4'} 855 | dev: true 856 | 857 | /to-fast-properties/2.0.0: 858 | resolution: {integrity: sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=} 859 | engines: {node: '>=4'} 860 | dev: true 861 | 862 | /ts-toolbelt/9.6.0: 863 | resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} 864 | dev: true 865 | 866 | /typescript/4.6.3: 867 | resolution: {integrity: sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==} 868 | engines: {node: '>=4.2.0'} 869 | hasBin: true 870 | dev: true 871 | 872 | /vite-plugin-solid/2.2.6: 873 | resolution: {integrity: sha512-J1RnmqkZZJSNYDW7vZj0giKKHLWGr9tS/gxR70WDSTYfhyXrgukbZdIfSEFbtrsg8ZiQ2t2zXcvkWoeefenqKw==} 874 | dependencies: 875 | '@babel/core': 7.17.8 876 | '@babel/preset-typescript': 7.16.7_@babel+core@7.17.8 877 | babel-preset-solid: 1.3.13_@babel+core@7.17.8 878 | merge-anything: 5.0.2 879 | solid-js: 1.3.13 880 | solid-refresh: 0.4.0_solid-js@1.3.13 881 | vite: 2.8.6 882 | transitivePeerDependencies: 883 | - less 884 | - sass 885 | - stylus 886 | - supports-color 887 | dev: true 888 | 889 | /vite/2.8.6: 890 | resolution: {integrity: sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==} 891 | engines: {node: '>=12.2.0'} 892 | hasBin: true 893 | peerDependencies: 894 | less: '*' 895 | sass: '*' 896 | stylus: '*' 897 | peerDependenciesMeta: 898 | less: 899 | optional: true 900 | sass: 901 | optional: true 902 | stylus: 903 | optional: true 904 | dependencies: 905 | esbuild: 0.14.27 906 | postcss: 8.4.12 907 | resolve: 1.22.0 908 | rollup: 2.70.1 909 | optionalDependencies: 910 | fsevents: 2.3.2 911 | dev: true 912 | -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | createSignal, 4 | } from "solid-js"; 5 | import { createSpring, animated, config } from "solid-spring"; 6 | 7 | function ChainExample() { 8 | const [flip, set] = createSignal(false); 9 | 10 | const styles = createSpring(() => { 11 | return { 12 | to: { opacity: 1 }, 13 | from: { opacity: 0 }, 14 | reset: true, 15 | reverse: flip(), 16 | delay: 200, 17 | config: config.molasses, 18 | onRest: () => { 19 | set(!flip()); 20 | }, 21 | }; 22 | }); 23 | 24 | return hello; 25 | } 26 | 27 | const App: Component = () => { 28 | return ; 29 | }; 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from 'solid-js/web'; 3 | 4 | import App from './App'; 5 | 6 | render(() => , document.getElementById('root') as HTMLElement); 7 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "jsx": "preserve", 10 | "jsxImportSource": "solid-js", 11 | "types": ["vite/client"], 12 | "noEmit": true, 13 | "isolatedModules": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | export default defineConfig({ 5 | plugins: [solidPlugin()], 6 | build: { 7 | target: 'esnext', 8 | polyfillDynamicImport: false, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@monorepo/solid-spring", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/aslemammad/solid-spring.git" 7 | }, 8 | "license": "MIT", 9 | "bugs": { 10 | "url": "https://github.com/aslemammad/solid-spring/issues" 11 | }, 12 | "homepage": "https://github.com/aslemammad/solid-spring#readme", 13 | "description": "Like react-spring, but for SolidJS", 14 | "info": "solid-spring is a spring-physics first animation library for SolidJS based on react-spring/core", 15 | "contributors": [], 16 | "scripts": {}, 17 | "keywords": [ 18 | "best_ecosystem", 19 | "solidhack", 20 | "react-spring", 21 | "solid" 22 | ], 23 | "author": "M. Bagher Abiat", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@rollup/plugin-alias": "^3.1.9", 27 | "@rollup/plugin-commonjs": "^21.0.2", 28 | "@rollup/plugin-json": "^4.1.0", 29 | "@rollup/plugin-node-resolve": "^13.1.3", 30 | "esbuild": "^0.14.27", 31 | "rollup-plugin-esbuild": "^4.8.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ./* 3 | -------------------------------------------------------------------------------- /spring/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | -------------------------------------------------------------------------------- /spring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-spring", 3 | "version": "0.0.7", 4 | "type": "module", 5 | "description": "Like react-spring, but for SolidJS", 6 | "info": "solid-spring is a spring-physics first animation library for SolidJS based on react-spring/core", 7 | "contributors": [], 8 | "keywords": [ 9 | "best_ecosystem", 10 | "solidhack", 11 | "react-spring", 12 | "solid" 13 | ], 14 | "scripts": { 15 | "build": "rimraf dist && rollup -c", 16 | "dev": "rollup -c --watch src", 17 | "typecheck": "tsc --noEmit", 18 | "test": "pnpm vitest" 19 | }, 20 | "main": "./dist/index.js", 21 | "module": "./dist/index.js", 22 | "types": "./dist/index.d.ts", 23 | "exports": { 24 | ".": { 25 | "import": "./dist/index.js", 26 | "types": "./dist/index.d.ts" 27 | } 28 | }, 29 | "files": [ 30 | "dist", 31 | "bin", 32 | "*.d.ts" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/aslemammad/solid-spring.git" 37 | }, 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/aslemammad/solid-spring/issues" 41 | }, 42 | "homepage": "https://github.com/aslemammad/solid-spring#readme", 43 | "devDependencies": { 44 | "rollup-plugin-dts": "^4.2.0", 45 | "solid-js": "^1.3.13" 46 | }, 47 | "peerDependencies": { 48 | "solid-js": "^1.3.13" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /spring/rollup.config.js: -------------------------------------------------------------------------------- 1 | import esbuild from "rollup-plugin-esbuild"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import json from "@rollup/plugin-json"; 5 | import alias from "@rollup/plugin-alias"; 6 | import dts from 'rollup-plugin-dts' 7 | import pkg from "./package.json"; 8 | 9 | const entry = ["src/index.ts"]; 10 | 11 | const external = [ 12 | ...Object.keys(pkg.dependencies || []), 13 | ...Object.keys(pkg.peerDependencies || []), 14 | "worker_threads", 15 | "esbuild", 16 | "solid-js", 17 | "solid-js/web", 18 | "fs/promises", 19 | ]; 20 | 21 | export default [ 22 | { 23 | input: entry, 24 | output: { 25 | dir: "dist", 26 | format: "esm", 27 | sourcemap: true, 28 | }, 29 | external, 30 | plugins: [ 31 | alias({ 32 | entries: [{ find: /^node:(.+)$/, replacement: "$1" }], 33 | }), 34 | resolve({ 35 | preferBuiltins: true, 36 | }), 37 | json(), 38 | commonjs(), 39 | esbuild({ 40 | define: { 41 | "import.meta.vitest": 'false', 42 | }, 43 | target: "node14", 44 | }), 45 | ], 46 | }, 47 | { 48 | input: [ 49 | 'src/index.ts', 50 | ], 51 | output: { 52 | file: 'dist/index.d.ts', 53 | format: 'esm', 54 | }, 55 | external, 56 | plugins: [ 57 | dts(), 58 | ], 59 | }, 60 | ]; 61 | -------------------------------------------------------------------------------- /spring/src/AnimatedArray.ts: -------------------------------------------------------------------------------- 1 | import { AnimatedValue } from "./animated" 2 | import { AnimatedObject } from "./AnimatedObject" 3 | import { AnimatedString } from "./AnimatedString" 4 | import { isAnimatedString } from "./utils" 5 | 6 | type Value = number | string 7 | type Source = AnimatedValue[] 8 | 9 | /** An array of animated nodes */ 10 | export class AnimatedArray< 11 | T extends ReadonlyArray = Value[] 12 | > extends AnimatedObject { 13 | protected declare source: Source 14 | constructor(source: T) { 15 | super(source) 16 | } 17 | 18 | /** @internal */ 19 | static create>(source: T) { 20 | return new AnimatedArray(source) 21 | } 22 | 23 | getValue(): T { 24 | return this.source.map(node => node.getValue()) as any 25 | } 26 | 27 | setValue(source: T) { 28 | const payload = this.getPayload() 29 | // Reuse the payload when lengths are equal. 30 | if (source.length == payload.length) { 31 | return payload.map((node, i) => node.setValue(source[i])).some(Boolean) 32 | } 33 | // Remake the payload when length changes. 34 | super.setValue(source.map(makeAnimated)) 35 | return true 36 | } 37 | } 38 | 39 | function makeAnimated(value: any) { 40 | const nodeType = isAnimatedString(value) ? AnimatedString : AnimatedValue 41 | return nodeType.create(value) 42 | } 43 | -------------------------------------------------------------------------------- /spring/src/AnimatedObject.ts: -------------------------------------------------------------------------------- 1 | import { Animated, AnimatedValue, getPayload, isAnimated } from "./animated" 2 | import { TreeContext } from "./context" 3 | import { getFluidValue, hasFluidValue } from "./fluids" 4 | import { Lookup, eachProp, each } from "./utils" 5 | 6 | 7 | /** An object containing `Animated` nodes */ 8 | export class AnimatedObject extends Animated { 9 | constructor(protected source: Lookup) { 10 | super() 11 | this.setValue(source) 12 | } 13 | 14 | getValue(animated?: boolean) { 15 | const values: Lookup = {} 16 | eachProp(this.source, (source, key) => { 17 | if (isAnimated(source)) { 18 | values[key] = source.getValue(animated) 19 | } else if (hasFluidValue(source)) { 20 | values[key] = getFluidValue(source) 21 | } else if (!animated) { 22 | values[key] = source 23 | } 24 | }) 25 | return values 26 | } 27 | 28 | /** Replace the raw object data */ 29 | setValue(source: Lookup) { 30 | this.source = source 31 | this.payload = this._makePayload(source) 32 | } 33 | 34 | reset() { 35 | if (this.payload) { 36 | each(this.payload, node => node.reset()) 37 | } 38 | } 39 | 40 | /** Create a payload set. */ 41 | protected _makePayload(source: Lookup) { 42 | if (source) { 43 | const payload = new Set() 44 | eachProp(source, this._addToPayload, payload) 45 | return Array.from(payload) 46 | } 47 | } 48 | 49 | /** Add to a payload set. */ 50 | protected _addToPayload(this: Set, source: any) { 51 | if (TreeContext.dependencies && hasFluidValue(source)) { 52 | TreeContext.dependencies.add(source) 53 | } 54 | const payload = getPayload(source) 55 | if (payload) { 56 | each(payload, node => this.add(node)) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spring/src/AnimatedString.ts: -------------------------------------------------------------------------------- 1 | import { AnimatedValue } from "./animated" 2 | import { createInterpolator } from "./createInterpolator" 3 | import { is } from "./utils" 4 | 5 | type Value = string | number 6 | 7 | export class AnimatedString extends AnimatedValue { 8 | protected declare _value: number 9 | protected _string: string | null = null 10 | protected _toString: (input: number) => string 11 | 12 | constructor(value: string) { 13 | super(0) 14 | this._toString = createInterpolator({ 15 | output: [value, value], 16 | }) 17 | } 18 | 19 | /** @internal */ 20 | static create(value: string) { 21 | return new AnimatedString(value) 22 | } 23 | 24 | getValue() { 25 | let value = this._string 26 | return value == null ? (this._string = this._toString(this._value)) : value 27 | } 28 | 29 | setValue(value: Value) { 30 | if (is.str(value)) { 31 | if (value == this._string) { 32 | return false 33 | } 34 | this._string = value 35 | this._value = 1 36 | } else if (super.setValue(value)) { 37 | this._string = null 38 | } else { 39 | return false 40 | } 41 | return true 42 | } 43 | 44 | reset(goal?: string) { 45 | if (goal) { 46 | this._toString = createInterpolator({ 47 | output: [this.getValue(), goal], 48 | }) 49 | } 50 | this._value = 0 51 | super.reset() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /spring/src/AnimatedStyle.ts: -------------------------------------------------------------------------------- 1 | import { AnimatedObject } from "./AnimatedObject" 2 | import { addFluidObserver, callFluidObservers, FluidEvent, FluidValue, getFluidValue, hasFluidValue, removeFluidObserver } from "./fluids" 3 | import { is,each, Lookup, OneOrMore, eachProp, toArray } from "./utils" 4 | 5 | /** The transform-functions 6 | * (https://developer.mozilla.org/fr/docs/Web/CSS/transform-function) 7 | * that you can pass as keys to your animated component style and that will be 8 | * animated. Perspective has been left out as it would conflict with the 9 | * non-transform perspective style. 10 | */ 11 | const domTransforms = /^(matrix|translate|scale|rotate|skew)/ 12 | 13 | // These keys have "px" units by default 14 | const pxTransforms = /^(translate)/ 15 | 16 | // These keys have "deg" units by default 17 | const degTransforms = /^(rotate|skew)/ 18 | 19 | type Value = number | string 20 | 21 | /** Add a unit to the value when the value is unit-less (eg: a number) */ 22 | const addUnit = (value: Value, unit: string): string | 0 => 23 | is.num(value) && value !== 0 ? value + unit : value 24 | 25 | /** 26 | * Checks if the input value matches the identity value. 27 | * 28 | * isValueIdentity(0, 0) // => true 29 | * isValueIdentity('0px', 0) // => true 30 | * isValueIdentity([0, '0px', 0], 0) // => true 31 | */ 32 | const isValueIdentity = (value: OneOrMore, id: number): boolean => 33 | is.arr(value) 34 | ? value.every(v => isValueIdentity(v, id)) 35 | : is.num(value) 36 | ? value === id 37 | : parseFloat(value) === id 38 | 39 | type Inputs = ReadonlyArray>[] 40 | type Transforms = ((value: any) => [string, boolean])[] 41 | 42 | /** 43 | * This AnimatedStyle will simplify animated components transforms by 44 | * interpolating all transform function passed as keys in the style object 45 | * including shortcuts such as x, y and z for translateX/Y/Z 46 | */ 47 | export class AnimatedStyle extends AnimatedObject { 48 | constructor({ x, y, z, ...style }: Lookup) { 49 | /** 50 | * An array of arrays that contains the values (static or fluid) 51 | * used by each transform function. 52 | */ 53 | const inputs: Inputs = [] 54 | /** 55 | * An array of functions that take a list of values (static or fluid) 56 | * and returns (1) a CSS transform string and (2) a boolean that's true 57 | * when the transform has no effect (eg: an identity transform). 58 | */ 59 | const transforms: Transforms = [] 60 | 61 | // Combine x/y/z into translate3d 62 | if (x || y || z) { 63 | inputs.push([x || 0, y || 0, z || 0]) 64 | transforms.push((xyz: Value[]) => [ 65 | `translate3d(${xyz.map(v => addUnit(v, 'px')).join(',')})`, // prettier-ignore 66 | isValueIdentity(xyz, 0), 67 | ]) 68 | } 69 | 70 | // Pluck any other transform-related props 71 | eachProp(style, (value, key) => { 72 | if (key === 'transform') { 73 | inputs.push([value || '']) 74 | transforms.push((transform: string) => [transform, transform === '']) 75 | } else if (domTransforms.test(key)) { 76 | delete style[key] 77 | if (is.und(value)) return 78 | 79 | const unit = pxTransforms.test(key) 80 | ? 'px' 81 | : degTransforms.test(key) 82 | ? 'deg' 83 | : '' 84 | 85 | inputs.push(toArray(value)) 86 | transforms.push( 87 | key === 'rotate3d' 88 | ? ([x, y, z, deg]: [number, number, number, Value]) => [ 89 | `rotate3d(${x},${y},${z},${addUnit(deg, unit)})`, 90 | isValueIdentity(deg, 0), 91 | ] 92 | : (input: Value[]) => [ 93 | `${key}(${input.map(v => addUnit(v, unit)).join(',')})`, 94 | isValueIdentity(input, key.startsWith('scale') ? 1 : 0), 95 | ] 96 | ) 97 | } 98 | }) 99 | 100 | if (inputs.length) { 101 | style.transform = new FluidTransform(inputs, transforms) 102 | } 103 | 104 | super(style) 105 | } 106 | } 107 | 108 | /** @internal */ 109 | class FluidTransform extends FluidValue { 110 | protected _value: string | null = null 111 | 112 | constructor(readonly inputs: Inputs, readonly transforms: Transforms) { 113 | super() 114 | } 115 | 116 | get() { 117 | return this._value || (this._value = this._get()) 118 | } 119 | 120 | protected _get() { 121 | let transform = '' 122 | let identity = true 123 | each(this.inputs, (input, i) => { 124 | const arg1 = getFluidValue(input[0]) 125 | const [t, id] = this.transforms[i]( 126 | is.arr(arg1) ? arg1 : input.map(getFluidValue) 127 | ) 128 | transform += ' ' + t 129 | identity = identity && id 130 | }) 131 | return identity ? 'none' : transform 132 | } 133 | 134 | // Start observing our inputs once we have an observer. 135 | protected observerAdded(count: number) { 136 | if (count == 1) 137 | each(this.inputs, input => 138 | each( 139 | input, 140 | value => hasFluidValue(value) && addFluidObserver(value, this) 141 | ) 142 | ) 143 | } 144 | 145 | // Stop observing our inputs once we have no observers. 146 | protected observerRemoved(count: number) { 147 | if (count == 0) 148 | each(this.inputs, input => 149 | each( 150 | input, 151 | value => hasFluidValue(value) && removeFluidObserver(value, this) 152 | ) 153 | ) 154 | } 155 | 156 | eventObserved(event: FluidEvent) { 157 | if (event.type == 'change') { 158 | this._value = null 159 | } 160 | callFluidObservers(this, event) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /spring/src/Animation.ts: -------------------------------------------------------------------------------- 1 | import { AnimatedValue } from "./animated" 2 | import { FluidValue } from "./fluids" 3 | import { AnimationConfig, PickEventFns, SpringProps } from "./utils" 4 | 5 | const emptyArray: readonly any[] = [] 6 | 7 | /** An animation being executed by the frameloop */ 8 | export class Animation { 9 | changed = false 10 | values: readonly AnimatedValue[] = emptyArray 11 | toValues: readonly number[] | null = null 12 | fromValues: readonly number[] = emptyArray 13 | 14 | to!: T | FluidValue 15 | from!: T | FluidValue 16 | config = new AnimationConfig() 17 | immediate = false 18 | } 19 | 20 | export interface Animation extends PickEventFns> {} 21 | -------------------------------------------------------------------------------- /spring/src/AnimationConfig.ts: -------------------------------------------------------------------------------- 1 | import { config, EasingFunction, easings, is } from "./utils" 2 | 3 | const defaults: any = { 4 | ...config.default, 5 | mass: 1, 6 | damping: 1, 7 | easing: easings.linear, 8 | clamp: false, 9 | } 10 | 11 | export class AnimationConfig { 12 | /** 13 | * With higher tension, the spring will resist bouncing and try harder to stop at its end value. 14 | * 15 | * When tension is zero, no animation occurs. 16 | */ 17 | tension!: number 18 | 19 | /** 20 | * The damping ratio coefficient, or just the damping ratio when `speed` is defined. 21 | * 22 | * When `speed` is defined, this value should be between 0 and 1. 23 | * 24 | * Higher friction means the spring will slow down faster. 25 | */ 26 | friction!: number 27 | 28 | /** 29 | * The natural frequency (in seconds), which dictates the number of bounces 30 | * per second when no damping exists. 31 | * 32 | * When defined, `tension` is derived from this, and `friction` is derived 33 | * from `tension` and `damping`. 34 | */ 35 | frequency?: number 36 | 37 | /** 38 | * The damping ratio, which dictates how the spring slows down. 39 | * 40 | * Set to `0` to never slow down. Set to `1` to slow down without bouncing. 41 | * Between `0` and `1` is for you to explore. 42 | * 43 | * Only works when `frequency` is defined. 44 | * 45 | * Defaults to 1 46 | */ 47 | damping!: number 48 | 49 | /** 50 | * Higher mass means more friction is required to slow down. 51 | * 52 | * Defaults to 1, which works fine most of the time. 53 | */ 54 | mass!: number 55 | 56 | /** 57 | * The initial velocity of one or more values. 58 | */ 59 | velocity: number | number[] = 0 60 | 61 | /** 62 | * The smallest velocity before the animation is considered "not moving". 63 | * 64 | * When undefined, `precision` is used instead. 65 | */ 66 | restVelocity?: number 67 | 68 | /** 69 | * The smallest distance from a value before that distance is essentially zero. 70 | * 71 | * This helps in deciding when a spring is "at rest". The spring must be within 72 | * this distance from its final value, and its velocity must be lower than this 73 | * value too (unless `restVelocity` is defined). 74 | */ 75 | precision?: number 76 | 77 | /** 78 | * For `duration` animations only. Note: The `duration` is not affected 79 | * by this property. 80 | * 81 | * Defaults to `0`, which means "start from the beginning". 82 | * 83 | * Setting to `1+` makes an immediate animation. 84 | * 85 | * Setting to `0.5` means "start from the middle of the easing function". 86 | * 87 | * Any number `>= 0` and `<= 1` makes sense here. 88 | */ 89 | progress?: number 90 | 91 | /** 92 | * Animation length in number of milliseconds. 93 | */ 94 | duration?: number 95 | 96 | /** 97 | * The animation curve. Only used when `duration` is defined. 98 | * 99 | * Defaults to quadratic ease-in-out. 100 | */ 101 | easing!: EasingFunction 102 | 103 | /** 104 | * Avoid overshooting by ending abruptly at the goal value. 105 | */ 106 | clamp!: boolean 107 | 108 | /** 109 | * When above zero, the spring will bounce instead of overshooting when 110 | * exceeding its goal value. Its velocity is multiplied by `-1 + bounce` 111 | * whenever its current value equals or exceeds its goal. For example, 112 | * setting `bounce` to `0.5` chops the velocity in half on each bounce, 113 | * in addition to any friction. 114 | */ 115 | bounce?: number 116 | 117 | /** 118 | * "Decay animations" decelerate without an explicit goal value. 119 | * Useful for scrolling animations. 120 | * 121 | * Use `true` for the default exponential decay factor (`0.998`). 122 | * 123 | * When a `number` between `0` and `1` is given, a lower number makes the 124 | * animation slow down faster. And setting to `1` would make an unending 125 | * animation. 126 | */ 127 | decay?: boolean | number 128 | 129 | /** 130 | * While animating, round to the nearest multiple of this number. 131 | * The `from` and `to` values are never rounded, as well as any value 132 | * passed to the `set` method of an animated value. 133 | */ 134 | round?: number 135 | 136 | constructor() { 137 | Object.assign(this, defaults) 138 | } 139 | } 140 | 141 | export function mergeConfig( 142 | config: AnimationConfig, 143 | newConfig: Partial, 144 | defaultConfig?: Partial 145 | ): typeof config 146 | 147 | export function mergeConfig( 148 | config: any, 149 | newConfig: object, 150 | defaultConfig?: object 151 | ) { 152 | if (defaultConfig) { 153 | defaultConfig = { ...defaultConfig } 154 | sanitizeConfig(defaultConfig, newConfig) 155 | newConfig = { ...defaultConfig, ...newConfig } 156 | } 157 | 158 | sanitizeConfig(config, newConfig) 159 | Object.assign(config, newConfig) 160 | 161 | for (const key in defaults) { 162 | if (config[key] == null) { 163 | config[key] = defaults[key] 164 | } 165 | } 166 | 167 | let { mass, frequency, damping } = config 168 | if (!is.und(frequency)) { 169 | if (frequency < 0.01) frequency = 0.01 170 | if (damping < 0) damping = 0 171 | config.tension = Math.pow((2 * Math.PI) / frequency, 2) * mass 172 | config.friction = (4 * Math.PI * damping * mass) / frequency 173 | } 174 | 175 | return config 176 | } 177 | 178 | // Prevent a config from accidentally overriding new props. 179 | // This depends on which "config" props take precedence when defined. 180 | function sanitizeConfig( 181 | config: Partial, 182 | props: Partial 183 | ) { 184 | if (!is.und(props.decay)) { 185 | config.duration = undefined 186 | } else { 187 | const isTensionConfig = !is.und(props.tension) || !is.und(props.friction) 188 | if ( 189 | isTensionConfig || 190 | !is.und(props.frequency) || 191 | !is.und(props.damping) || 192 | !is.und(props.mass) 193 | ) { 194 | config.duration = undefined 195 | config.decay = undefined 196 | } 197 | if (isTensionConfig) { 198 | config.frequency = undefined 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /spring/src/AnimationResult.ts: -------------------------------------------------------------------------------- 1 | import { AnimationResult, Readable } from "./utils" 2 | 3 | /** @internal */ 4 | export const getCombinedResult = ( 5 | target: T, 6 | results: AnimationResult[] 7 | ): AnimationResult => 8 | results.length == 1 9 | ? results[0] 10 | : results.some(result => result.cancelled) 11 | ? getCancelledResult(target.get()) 12 | : results.every(result => result.noop) 13 | ? getNoopResult(target.get()) 14 | : getFinishedResult( 15 | target.get(), 16 | results.every(result => result.finished) 17 | ) 18 | 19 | /** No-op results are for updates that never start an animation. */ 20 | export const getNoopResult = (value: any) => ({ 21 | value, 22 | noop: true, 23 | finished: true, 24 | cancelled: false, 25 | }) 26 | 27 | export const getFinishedResult = ( 28 | value: any, 29 | finished: boolean, 30 | cancelled: boolean = false 31 | ) => ({ 32 | value, 33 | finished, 34 | cancelled, 35 | }) 36 | 37 | export const getCancelledResult = (value: any) => ({ 38 | value, 39 | cancelled: true, 40 | finished: false, 41 | }) 42 | -------------------------------------------------------------------------------- /spring/src/Controller.ts: -------------------------------------------------------------------------------- 1 | import { AnimationResult, each, eachProp, ControllerUpdate, Lookup, OnChange, OnRest, OnStart, SpringValues, toArray, UnknownProps, OneOrMore, is, flushCalls, AsyncResult, getDefaultProp, flush, Falsy, noop, ControllerFlushFn } from "./utils" 2 | import { createLoopUpdate, createUpdate, SpringValue, } from './SpringValue' 3 | import { SpringRef } from "./SpringRef" 4 | import { FrameValue } from "./FrameValue" 5 | import { addFluidObserver, FluidObserver } from "./fluids" 6 | import { runAsync, RunAsyncState, stopAsync } from "./runAsync" 7 | import { scheduleProps } from "./scheduleProps" 8 | import { getCancelledResult, getCombinedResult } from "./AnimationResult" 9 | import { raf } from "./rafz" 10 | 11 | /** Events batched by the `Controller` class */ 12 | const BATCHED_EVENTS = ['onStart', 'onChange', 'onRest'] as const 13 | 14 | let nextId = 1 15 | 16 | /** Queue of pending updates for a `Controller` instance. */ 17 | export interface ControllerQueue 18 | extends Array< 19 | ControllerUpdate & { 20 | /** The keys affected by this update. When null, all keys are affected. */ 21 | keys: string[] | null 22 | } 23 | > {} 24 | 25 | export class Controller { 26 | readonly id = nextId++ 27 | 28 | /** The animated values */ 29 | springs: SpringValues = {} as any 30 | 31 | /** The queue of props passed to the `update` method. */ 32 | queue: ControllerQueue = [] 33 | 34 | /** 35 | * The injected ref. When defined, render-based updates are pushed 36 | * onto the `queue` instead of being auto-started. 37 | */ 38 | ref?: SpringRef 39 | 40 | /** Custom handler for flushing update queues */ 41 | protected _flush?: ControllerFlushFn 42 | 43 | /** These props are used by all future spring values */ 44 | protected _initialProps?: Lookup 45 | 46 | /** The counter for tracking `scheduleProps` calls */ 47 | protected _lastAsyncId = 0 48 | 49 | /** The values currently being animated */ 50 | protected _active = new Set() 51 | 52 | /** The values that changed recently */ 53 | protected _changed = new Set() 54 | 55 | /** Equals false when `onStart` listeners can be called */ 56 | protected _started = false 57 | 58 | private _item?: any 59 | 60 | /** State used by the `runAsync` function */ 61 | protected _state: RunAsyncState = { 62 | paused: false, 63 | pauseQueue: new Set(), 64 | resumeQueue: new Set(), 65 | timeouts: new Set(), 66 | } 67 | 68 | /** The event queues that are flushed once per frame maximum */ 69 | protected _events = { 70 | onStart: new Map< 71 | OnStart, Controller, any>, 72 | AnimationResult 73 | >(), 74 | onChange: new Map< 75 | OnChange, Controller, any>, 76 | AnimationResult 77 | >(), 78 | onRest: new Map< 79 | OnRest, Controller, any>, 80 | AnimationResult 81 | >(), 82 | } 83 | 84 | constructor( 85 | props?: ControllerUpdate | null, 86 | flush?: ControllerFlushFn 87 | ) { 88 | this._onFrame = this._onFrame.bind(this) 89 | if (flush) { 90 | this._flush = flush 91 | } 92 | if (props) { 93 | this.start({ default: true, ...props }) 94 | } 95 | } 96 | 97 | /** 98 | * Equals `true` when no spring values are in the frameloop, and 99 | * no async animation is currently active. 100 | */ 101 | get idle() { 102 | return ( 103 | !this._state.asyncTo && 104 | Object.values(this.springs as Lookup).every(spring => { 105 | return spring.idle && !spring.isDelayed && !spring.isPaused 106 | }) 107 | ) 108 | } 109 | 110 | get item() { 111 | return this._item 112 | } 113 | 114 | set item(item) { 115 | this._item = item 116 | } 117 | 118 | /** Get the current values of our springs */ 119 | get(): State & UnknownProps { 120 | const values: any = {} 121 | this.each((spring, key) => (values[key] = spring.get())) 122 | return values 123 | } 124 | 125 | /** Set the current values without animating. */ 126 | set(values: Partial) { 127 | for (const key in values) { 128 | const value = values[key] 129 | if (!is.und(value)) { 130 | this.springs[key].set(value) 131 | } 132 | } 133 | } 134 | 135 | /** Push an update onto the queue of each value. */ 136 | update(props: ControllerUpdate | Falsy) { 137 | if (props) { 138 | this.queue.push(createUpdate(props)) 139 | } 140 | return this 141 | } 142 | 143 | /** 144 | * Start the queued animations for every spring, and resolve the returned 145 | * promise once all queued animations have finished or been cancelled. 146 | * 147 | * When you pass a queue (instead of nothing), that queue is used instead of 148 | * the queued animations added with the `update` method, which are left alone. 149 | */ 150 | start(props?: OneOrMore> | null): AsyncResult { 151 | let { queue } = this as any 152 | if (props) { 153 | queue = toArray(props).map(createUpdate) 154 | } else { 155 | this.queue = [] 156 | } 157 | 158 | if (this._flush) { 159 | return this._flush(this, queue) 160 | } 161 | 162 | prepareKeys(this, queue) 163 | return flushUpdateQueue(this, queue) 164 | } 165 | 166 | /** Stop all animations. */ 167 | stop(): this 168 | /** Stop animations for the given keys. */ 169 | stop(keys: OneOrMore): this 170 | /** Cancel all animations. */ 171 | stop(cancel: boolean): this 172 | /** Cancel animations for the given keys. */ 173 | stop(cancel: boolean, keys: OneOrMore): this 174 | /** Stop some or all animations. */ 175 | stop(keys?: OneOrMore): this 176 | /** Cancel some or all animations. */ 177 | stop(cancel: boolean, keys?: OneOrMore): this 178 | /** @internal */ 179 | stop(arg?: boolean | OneOrMore, keys?: OneOrMore) { 180 | if (arg !== !!arg) { 181 | keys = arg as OneOrMore 182 | } 183 | if (keys) { 184 | const springs = this.springs as Lookup 185 | each(toArray(keys) as string[], key => springs[key].stop(!!arg)) 186 | } else { 187 | stopAsync(this._state, this._lastAsyncId) 188 | this.each(spring => spring.stop(!!arg)) 189 | } 190 | return this 191 | } 192 | 193 | /** Freeze the active animation in time */ 194 | pause(keys?: OneOrMore) { 195 | if (is.und(keys)) { 196 | this.start({ pause: true }) 197 | } else { 198 | const springs = this.springs as Lookup 199 | each(toArray(keys) as string[], key => springs[key].pause()) 200 | } 201 | return this 202 | } 203 | 204 | /** Resume the animation if paused. */ 205 | resume(keys?: OneOrMore) { 206 | if (is.und(keys)) { 207 | this.start({ pause: false }) 208 | } else { 209 | const springs = this.springs as Lookup 210 | each(toArray(keys) as string[], key => springs[key].resume()) 211 | } 212 | return this 213 | } 214 | 215 | /** Call a function once per spring value */ 216 | each(iterator: (spring: SpringValue, key: string) => void) { 217 | eachProp(this.springs, iterator as any) 218 | } 219 | 220 | /** @internal Called at the end of every animation frame */ 221 | protected _onFrame() { 222 | const { onStart, onChange, onRest } = this._events 223 | 224 | const active = this._active.size > 0 225 | const changed = this._changed.size > 0 226 | 227 | if ((active && !this._started) || (changed && !this._started)) { 228 | this._started = true 229 | flush(onStart, ([onStart, result]) => { 230 | result.value = this.get() 231 | onStart(result, this, this._item) 232 | }) 233 | } 234 | 235 | const idle = !active && this._started 236 | const values = changed || (idle && onRest.size) ? this.get() : null 237 | 238 | if (changed && onChange.size) { 239 | flush(onChange, ([onChange, result]) => { 240 | result.value = values 241 | onChange(result, this, this._item) 242 | }) 243 | } 244 | 245 | // The "onRest" queue is only flushed when all springs are idle. 246 | if (idle) { 247 | this._started = false 248 | flush(onRest, ([onRest, result]) => { 249 | result.value = values 250 | onRest(result, this, this._item) 251 | }) 252 | } 253 | } 254 | 255 | /** @internal */ 256 | eventObserved(event: FrameValue.Event) { 257 | if (event.type == 'change') { 258 | this._changed.add(event.parent) 259 | if (!event.idle) { 260 | this._active.add(event.parent) 261 | } 262 | } else if (event.type == 'idle') { 263 | this._active.delete(event.parent) 264 | } 265 | // The `onFrame` handler runs when a parent is changed or idle. 266 | else return 267 | raf.onFrame(this._onFrame) 268 | } 269 | } 270 | 271 | /** 272 | * Warning: Props might be mutated. 273 | */ 274 | export function flushUpdateQueue( 275 | ctrl: Controller, 276 | queue: ControllerQueue 277 | ) { 278 | return Promise.all(queue.map(props => flushUpdate(ctrl, props))).then( 279 | results => getCombinedResult(ctrl, results) 280 | ) 281 | } 282 | 283 | /** 284 | * Warning: Props might be mutated. 285 | * 286 | * Process a single set of props using the given controller. 287 | * 288 | * The returned promise resolves to `true` once the update is 289 | * applied and any animations it starts are finished without being 290 | * stopped or cancelled. 291 | */ 292 | export async function flushUpdate( 293 | ctrl: Controller, 294 | props: ControllerQueue[number], 295 | isLoop?: boolean 296 | ): AsyncResult { 297 | const { keys, to, from, loop, onRest, onResolve } = props 298 | const defaults = is.obj(props.default) && props.default 299 | 300 | // Looping must be handled in this function, or else the values 301 | // would end up looping out-of-sync in many common cases. 302 | if (loop) { 303 | props.loop = false 304 | } 305 | 306 | // Treat false like null, which gets ignored. 307 | if (to === false) props.to = null 308 | if (from === false) props.from = null 309 | 310 | const asyncTo = is.arr(to) || is.fun(to) ? to : undefined 311 | if (asyncTo) { 312 | props.to = undefined 313 | props.onRest = undefined 314 | if (defaults) { 315 | defaults.onRest = undefined 316 | } 317 | } 318 | // For certain events, use batching to prevent multiple calls per frame. 319 | // However, batching is avoided when the `to` prop is async, because any 320 | // event props are used as default props instead. 321 | else { 322 | each(BATCHED_EVENTS, key => { 323 | const handler: any = props[key] 324 | if (is.fun(handler)) { 325 | const queue = ctrl['_events'][key] 326 | props[key] = (({ finished, cancelled }: AnimationResult) => { 327 | const result = queue.get(handler) 328 | if (result) { 329 | if (!finished) result.finished = false 330 | if (cancelled) result.cancelled = true 331 | } else { 332 | // The "value" is set before the "handler" is called. 333 | queue.set(handler, { 334 | value: null, 335 | finished: finished || false, 336 | cancelled: cancelled || false, 337 | }) 338 | } 339 | }) as any 340 | 341 | // Avoid using a batched `handler` as a default prop. 342 | if (defaults) { 343 | defaults[key] = props[key] as any 344 | } 345 | } 346 | }) 347 | } 348 | 349 | const state = ctrl['_state'] 350 | 351 | // Pause/resume the `asyncTo` when `props.pause` is true/false. 352 | if (props.pause === !state.paused) { 353 | state.paused = props.pause 354 | flushCalls(props.pause ? state.pauseQueue : state.resumeQueue) 355 | } 356 | // When a controller is paused, its values are also paused. 357 | else if (state.paused) { 358 | props.pause = true 359 | } 360 | 361 | const promises: AsyncResult[] = (keys || Object.keys(ctrl.springs)).map(key => 362 | ctrl.springs[key]!.start(props as any) 363 | ) 364 | 365 | const cancel = 366 | props.cancel === true || getDefaultProp(props, 'cancel') === true 367 | 368 | if (asyncTo || (cancel && state.asyncId)) { 369 | promises.push( 370 | scheduleProps(++ctrl['_lastAsyncId'], { 371 | props, 372 | state, 373 | actions: { 374 | pause: noop, 375 | resume: noop, 376 | start(props, resolve) { 377 | if (cancel) { 378 | stopAsync(state, ctrl['_lastAsyncId']) 379 | resolve(getCancelledResult(ctrl)) 380 | } else { 381 | props.onRest = onRest 382 | resolve(runAsync(asyncTo!, props, state, ctrl)) 383 | } 384 | }, 385 | }, 386 | }) 387 | ) 388 | } 389 | 390 | // Pause after updating each spring, so they can be resumed separately 391 | // and so their default `pause` and `cancel` props are updated. 392 | if (state.paused) { 393 | // Ensure `this` must be resumed before the returned promise 394 | // is resolved and before starting the next `loop` repetition. 395 | await new Promise(resume => { 396 | state.resumeQueue.add(resume) 397 | }) 398 | } 399 | 400 | const result = getCombinedResult(ctrl, await Promise.all(promises)) 401 | if (loop && result.finished && !(isLoop && result.noop)) { 402 | const nextProps = createLoopUpdate(props, loop, to) 403 | if (nextProps) { 404 | prepareKeys(ctrl, [nextProps]) 405 | return flushUpdate(ctrl, nextProps, true) 406 | } 407 | } 408 | if (onResolve) { 409 | raf.batchedUpdates(() => onResolve(result, ctrl, ctrl.item)) 410 | } 411 | return result 412 | } 413 | 414 | /** 415 | * From an array of updates, get the map of `SpringValue` objects 416 | * by their keys. Springs are created when any update wants to 417 | * animate a new key. 418 | * 419 | * Springs created by `getSprings` are neither cached nor observed 420 | * until they're given to `setSprings`. 421 | */ 422 | export function getSprings( 423 | ctrl: Controller>, 424 | props?: OneOrMore> 425 | ) { 426 | const springs = { ...ctrl.springs } 427 | if (props) { 428 | each(toArray(props), (props: any) => { 429 | if (is.und(props.keys)) { 430 | props = createUpdate(props) 431 | } 432 | if (!is.obj(props.to)) { 433 | // Avoid passing array/function to each spring. 434 | props = { ...props, to: undefined } 435 | } 436 | prepareSprings(springs as any, props, key => { 437 | return createSpring(key) 438 | }) 439 | }) 440 | } 441 | setSprings(ctrl, springs) 442 | return springs 443 | } 444 | 445 | /** 446 | * Tell a controller to manage the given `SpringValue` objects 447 | * whose key is not already in use. 448 | */ 449 | export function setSprings( 450 | ctrl: Controller>, 451 | springs: SpringValues 452 | ) { 453 | eachProp(springs, (spring, key) => { 454 | if (!ctrl.springs[key]) { 455 | ctrl.springs[key] = spring 456 | addFluidObserver(spring, ctrl) 457 | } 458 | }) 459 | } 460 | 461 | function createSpring(key: string, observer?: FluidObserver) { 462 | const spring = new SpringValue() 463 | spring.key = key 464 | if (observer) { 465 | addFluidObserver(spring, observer) 466 | } 467 | return spring 468 | } 469 | 470 | /** 471 | * Ensure spring objects exist for each defined key. 472 | * 473 | * Using the `props`, the `Animated` node of each `SpringValue` may 474 | * be created or updated. 475 | */ 476 | function prepareSprings( 477 | springs: SpringValues, 478 | props: ControllerQueue[number], 479 | create: (key: string) => SpringValue 480 | ) { 481 | if (props.keys) { 482 | each(props.keys, key => { 483 | const spring = springs[key] || (springs[key] = create(key)) 484 | spring['_prepareNode'](props) 485 | }) 486 | } 487 | } 488 | 489 | /** 490 | * Ensure spring objects exist for each defined key, and attach the 491 | * `ctrl` to them for observation. 492 | * 493 | * The queue is expected to contain `createUpdate` results. 494 | */ 495 | function prepareKeys(ctrl: Controller, queue: ControllerQueue[number][]) { 496 | each(queue, props => { 497 | prepareSprings(ctrl.springs, props, key => { 498 | return createSpring(key, ctrl) 499 | }) 500 | }) 501 | } 502 | -------------------------------------------------------------------------------- /spring/src/FrameLoop.ts: -------------------------------------------------------------------------------- 1 | import * as G from './globals' 2 | import { raf } from './rafz' 3 | 4 | export interface OpaqueAnimation { 5 | idle: boolean 6 | priority: number 7 | advance(dt: number): void 8 | } 9 | 10 | // Animations starting on the next frame 11 | const startQueue = new Set() 12 | 13 | // The animations being updated in the current frame, sorted by lowest 14 | // priority first. These two arrays are swapped at the end of each frame. 15 | let currentFrame: OpaqueAnimation[] = [] 16 | let prevFrame: OpaqueAnimation[] = [] 17 | 18 | // The priority of the currently advancing animation. 19 | // To protect against a race condition whenever a frame is being processed, 20 | // where the filtering of `animations` is corrupted with a shifting index, 21 | // causing animations to potentially advance 2x faster than intended. 22 | let priority = 0 23 | 24 | /** 25 | * The frameloop executes its animations in order of lowest priority first. 26 | * Animations are retained until idle. 27 | */ 28 | export const frameLoop = { 29 | get idle() { 30 | return !startQueue.size && !currentFrame.length 31 | }, 32 | 33 | /** Advance the given animation on every frame until idle. */ 34 | start(animation: OpaqueAnimation) { 35 | // An animation can be added while a frame is being processed, 36 | // unless its priority is lower than the animation last updated. 37 | if (priority > animation.priority) { 38 | startQueue.add(animation) 39 | raf.onStart(flushStartQueue) 40 | } else { 41 | startSafely(animation) 42 | raf(advance) 43 | } 44 | }, 45 | 46 | /** Advance all animations by the given time. */ 47 | advance, 48 | 49 | /** Call this when an animation's priority changes. */ 50 | sort(animation: OpaqueAnimation) { 51 | if (priority) { 52 | raf.onFrame(() => frameLoop.sort(animation)) 53 | } else { 54 | const prevIndex = currentFrame.indexOf(animation) 55 | if (~prevIndex) { 56 | currentFrame.splice(prevIndex, 1) 57 | startUnsafely(animation) 58 | } 59 | } 60 | }, 61 | 62 | /** 63 | * Clear all animations. For testing purposes. 64 | * 65 | * ☠️ Never call this from within the frameloop. 66 | */ 67 | clear() { 68 | currentFrame = [] 69 | startQueue.clear() 70 | }, 71 | } 72 | 73 | function flushStartQueue() { 74 | startQueue.forEach(startSafely) 75 | startQueue.clear() 76 | raf(advance) 77 | } 78 | 79 | function startSafely(animation: OpaqueAnimation) { 80 | if (!currentFrame.includes(animation)) startUnsafely(animation) 81 | } 82 | 83 | function startUnsafely(animation: OpaqueAnimation) { 84 | currentFrame.splice( 85 | findIndex(currentFrame, other => other.priority > animation.priority), 86 | 0, 87 | animation 88 | ) 89 | } 90 | 91 | function advance(dt: number) { 92 | const nextFrame = prevFrame 93 | 94 | for (let i = 0; i < currentFrame.length; i++) { 95 | const animation = currentFrame[i] 96 | priority = animation.priority 97 | 98 | // Animations may go idle before advancing. 99 | if (!animation.idle) { 100 | G.willAdvance(animation) 101 | animation.advance(dt) 102 | if (!animation.idle) { 103 | nextFrame.push(animation) 104 | } 105 | } 106 | } 107 | priority = 0 108 | 109 | // Reuse the `currentFrame` array to avoid garbage collection. 110 | prevFrame = currentFrame 111 | prevFrame.length = 0 112 | 113 | // Set `currentFrame` for next frame, so the `start` function 114 | // adds new animations to the proper array. 115 | currentFrame = nextFrame 116 | 117 | return currentFrame.length > 0 118 | } 119 | 120 | /** Like `Array.prototype.findIndex` but returns `arr.length` instead of `-1` */ 121 | function findIndex(arr: T[], test: (value: T) => boolean) { 122 | const index = arr.findIndex(test) 123 | return index < 0 ? arr.length : index 124 | } 125 | -------------------------------------------------------------------------------- /spring/src/FrameValue.ts: -------------------------------------------------------------------------------- 1 | import * as G from "./globals"; 2 | import { getAnimated } from "./animated"; 3 | import { FluidValue, callFluidObservers } from "./fluids"; 4 | import { Interpolation, InterpolatorArgs } from "./Interpolation"; 5 | import { frameLoop } from "./FrameLoop"; 6 | 7 | export const isFrameValue = (value: any): value is FrameValue => 8 | value instanceof FrameValue; 9 | 10 | let nextId = 1; 11 | 12 | /** 13 | * A kind of `FluidValue` that manages an `AnimatedValue` node. 14 | * 15 | * Its underlying value can be accessed and even observed. 16 | */ 17 | export abstract class FrameValue extends FluidValue< 18 | T, 19 | FrameValue.Event 20 | > { 21 | readonly id = nextId++; 22 | 23 | abstract key?: string; 24 | abstract get idle(): boolean; 25 | 26 | protected _priority = 0; 27 | 28 | get priority() { 29 | return this._priority; 30 | } 31 | set priority(priority: number) { 32 | if (this._priority != priority) { 33 | this._priority = priority; 34 | this._onPriorityChange(priority); 35 | } 36 | } 37 | 38 | /** Get the current value */ 39 | get(): T { 40 | const node = getAnimated(this); 41 | return node && node.getValue(); 42 | } 43 | 44 | /** Create a spring that maps our value to another value */ 45 | to(...args: InterpolatorArgs) { 46 | return G.to(this, args) as Interpolation; 47 | } 48 | 49 | /** @deprecated Use the `to` method instead. */ 50 | interpolate(...args: InterpolatorArgs) { 51 | return G.to(this, args) as Interpolation; 52 | } 53 | 54 | toJSON() { 55 | return this.get(); 56 | } 57 | 58 | protected observerAdded(count: number) { 59 | if (count == 1) this._attach(); 60 | } 61 | 62 | protected observerRemoved(count: number) { 63 | if (count == 0) this._detach(); 64 | } 65 | 66 | /** @internal */ 67 | abstract advance(dt: number): void; 68 | 69 | /** @internal */ 70 | abstract eventObserved(_event: FrameValue.Event): void; 71 | 72 | /** Called when the first child is added. */ 73 | protected _attach() {} 74 | 75 | /** Called when the last child is removed. */ 76 | protected _detach() {} 77 | 78 | /** Tell our children about our new value */ 79 | protected _onChange(value: T, idle = false) { 80 | callFluidObservers(this, { 81 | type: "change", 82 | parent: this, 83 | value, 84 | idle, 85 | }); 86 | } 87 | 88 | /** Tell our children about our new priority */ 89 | protected _onPriorityChange(priority: number) { 90 | if (!this.idle) { 91 | frameLoop.sort(this); 92 | } 93 | callFluidObservers(this, { 94 | type: "priority", 95 | parent: this, 96 | priority, 97 | }); 98 | } 99 | } 100 | 101 | export declare namespace FrameValue { 102 | /** A parent changed its value */ 103 | interface ChangeEvent { 104 | parent: FrameValue; 105 | type: "change"; 106 | value: T; 107 | idle: boolean; 108 | } 109 | 110 | /** A parent changed its priority */ 111 | interface PriorityEvent { 112 | parent: FrameValue; 113 | type: "priority"; 114 | priority: number; 115 | } 116 | 117 | /** A parent is done animating */ 118 | interface IdleEvent { 119 | parent: FrameValue; 120 | type: "idle"; 121 | } 122 | 123 | /** Events sent to children of `FrameValue` objects */ 124 | export type Event = ChangeEvent | PriorityEvent | IdleEvent; 125 | } 126 | -------------------------------------------------------------------------------- /spring/src/Interpolation.ts: -------------------------------------------------------------------------------- 1 | import * as G from './globals' 2 | import { getAnimated, getPayload, setAnimated } from "./animated" 3 | import { createInterpolator } from "./createInterpolator" 4 | import { addFluidObserver, callFluidObservers, FluidValue, getFluidValue, hasFluidValue, removeFluidObserver } from "./fluids" 5 | import { frameLoop } from "./FrameLoop" 6 | import { FrameValue, isFrameValue } from "./FrameValue" 7 | import { raf } from "./rafz" 8 | import { Any, toArray, each, getAnimatedType, isEqual, is } from "./utils" 9 | 10 | export type Animatable = T extends number 11 | ? number 12 | : T extends string 13 | ? string 14 | : T extends ReadonlyArray 15 | ? Array extends T // When true, T is not a tuple 16 | ? ReadonlyArray 17 | : { [P in keyof T]: Animatable } 18 | : never 19 | 20 | /** Ensure each type of `T` is an array */ 21 | export type Arrify = [T, T] extends [infer T, infer DT] 22 | ? DT extends ReadonlyArray 23 | ? Array extends DT 24 | ? ReadonlyArray ? U : T> 25 | : DT 26 | : ReadonlyArray ? U : T> 27 | : never 28 | 29 | export type ExtrapolateType = 'identity' | 'clamp' | 'extend' 30 | 31 | /** Better type errors for overloads with generic types */ 32 | export type Constrain = [T] extends [Any] ? U : [T] extends [U] ? T : U 33 | 34 | export type EasingFunction = (t: number) => number 35 | 36 | export type InterpolatorConfig = { 37 | /** 38 | * What happens when the spring goes below its target value. 39 | * 40 | * - `extend` continues the interpolation past the target value 41 | * - `clamp` limits the interpolation at the max value 42 | * - `identity` sets the value to the interpolation input as soon as it hits the boundary 43 | * 44 | * @default 'extend' 45 | */ 46 | extrapolateLeft?: ExtrapolateType 47 | 48 | /** 49 | * What happens when the spring exceeds its target value. 50 | * 51 | * - `extend` continues the interpolation past the target value 52 | * - `clamp` limits the interpolation at the max value 53 | * - `identity` sets the value to the interpolation input as soon as it hits the boundary 54 | * 55 | * @default 'extend' 56 | */ 57 | extrapolateRight?: ExtrapolateType 58 | 59 | /** 60 | * What happens when the spring exceeds its target value. 61 | * Shortcut to set `extrapolateLeft` and `extrapolateRight`. 62 | * 63 | * - `extend` continues the interpolation past the target value 64 | * - `clamp` limits the interpolation at the max value 65 | * - `identity` sets the value to the interpolation input as soon as it hits the boundary 66 | * 67 | * @default 'extend' 68 | */ 69 | extrapolate?: ExtrapolateType 70 | 71 | /** 72 | * Input ranges mapping the interpolation to the output values. 73 | * 74 | * @example 75 | * 76 | * range: [0, 0.5, 1], output: ['yellow', 'orange', 'red'] 77 | * 78 | * @default [0,1] 79 | */ 80 | range?: readonly number[] 81 | 82 | /** 83 | * Output values from the interpolation function. Should match the length of the `range` array. 84 | */ 85 | output: readonly Constrain[] 86 | 87 | /** 88 | * Transformation to apply to the value before interpolation. 89 | */ 90 | map?: (value: number) => number 91 | 92 | /** 93 | * Custom easing to apply in interpolator. 94 | */ 95 | easing?: EasingFunction 96 | } 97 | 98 | export type InterpolatorArgs = 99 | | [InterpolatorFn, Out>] 100 | | [InterpolatorConfig] 101 | | [ 102 | readonly number[], 103 | readonly Constrain[], 104 | (ExtrapolateType | undefined)? 105 | ] 106 | 107 | export type InterpolatorFn = (...inputs: Arrify) => Out 108 | /** 109 | * An `Interpolation` is a memoized value that's computed whenever one of its 110 | * `FluidValue` dependencies has its value changed. 111 | * 112 | * Other `FrameValue` objects can depend on this. For example, passing an 113 | * `Interpolation` as the `to` prop of a `useSpring` call will trigger an 114 | * animation toward the memoized value. 115 | */ 116 | export class Interpolation extends FrameValue { 117 | /** Useful for debugging. */ 118 | key?: string 119 | 120 | /** Equals false when in the frameloop */ 121 | idle = true 122 | 123 | /** The function that maps inputs values to output */ 124 | readonly calc: InterpolatorFn 125 | 126 | /** The inputs which are currently animating */ 127 | protected _active = new Set() 128 | 129 | constructor( 130 | /** The source of input values */ 131 | readonly source: unknown, 132 | args: InterpolatorArgs 133 | ) { 134 | super() 135 | this.calc = createInterpolator(...args) 136 | 137 | const value = this._get() 138 | const nodeType = getAnimatedType(value) 139 | 140 | // Assume the computed value never changes type. 141 | setAnimated(this, nodeType.create(value)) 142 | } 143 | 144 | advance(_dt?: number) { 145 | const value = this._get() 146 | const oldValue = this.get() 147 | if (!isEqual(value, oldValue)) { 148 | getAnimated(this)!.setValue(value) 149 | this._onChange(value, this.idle) 150 | } 151 | // Become idle when all parents are idle or paused. 152 | if (!this.idle && checkIdle(this._active)) { 153 | becomeIdle(this) 154 | } 155 | } 156 | 157 | protected _get() { 158 | const inputs: Arrify = is.arr(this.source) 159 | ? this.source.map(getFluidValue) 160 | : (toArray(getFluidValue(this.source)) as any) 161 | 162 | return this.calc(...inputs) 163 | } 164 | 165 | protected _start() { 166 | if (this.idle && !checkIdle(this._active)) { 167 | this.idle = false 168 | 169 | each(getPayload(this)!, node => { 170 | node.done = false 171 | }) 172 | 173 | if (G.skipAnimation) { 174 | raf.batchedUpdates(() => this.advance()) 175 | becomeIdle(this) 176 | } else { 177 | frameLoop.start(this) 178 | } 179 | } 180 | } 181 | 182 | // Observe our sources only when we're observed. 183 | protected _attach() { 184 | let priority = 1 185 | each(toArray(this.source), source => { 186 | if (hasFluidValue(source)) { 187 | addFluidObserver(source, this) 188 | } 189 | if (isFrameValue(source)) { 190 | if (!source.idle) { 191 | this._active.add(source) 192 | } 193 | priority = Math.max(priority, source.priority + 1) 194 | } 195 | }) 196 | this.priority = priority 197 | this._start() 198 | } 199 | 200 | // Stop observing our sources once we have no observers. 201 | protected _detach() { 202 | each(toArray(this.source), source => { 203 | if (hasFluidValue(source)) { 204 | removeFluidObserver(source, this) 205 | } 206 | }) 207 | this._active.clear() 208 | becomeIdle(this) 209 | } 210 | 211 | /** @internal */ 212 | eventObserved(event: FrameValue.Event) { 213 | // Update our value when an idle parent is changed, 214 | // and enter the frameloop when a parent is resumed. 215 | if (event.type == 'change') { 216 | if (event.idle) { 217 | this.advance() 218 | } else { 219 | this._active.add(event.parent) 220 | this._start() 221 | } 222 | } 223 | // Once all parents are idle, the `advance` method runs one more time, 224 | // so we should avoid updating the `idle` status here. 225 | else if (event.type == 'idle') { 226 | this._active.delete(event.parent) 227 | } 228 | // Ensure our priority is greater than all parents, which means 229 | // our value won't be updated until our parents have updated. 230 | else if (event.type == 'priority') { 231 | this.priority = toArray(this.source).reduce( 232 | (highest: number, parent) => 233 | Math.max(highest, (isFrameValue(parent) ? parent.priority : 0) + 1), 234 | 0 235 | ) 236 | } 237 | } 238 | } 239 | 240 | export interface InterpolatorFactory { 241 | (interpolator: InterpolatorFn): typeof interpolator; 242 | 243 | (config: InterpolatorConfig): (input: number) => Animatable 244 | 245 | ( 246 | range: readonly number[], 247 | output: readonly Constrain[], 248 | extrapolate?: ExtrapolateType 249 | ): (input: number) => Animatable 250 | 251 | (...args: InterpolatorArgs): InterpolatorFn 252 | } 253 | 254 | 255 | /** Returns true for an idle source. */ 256 | function isIdle(source: any) { 257 | return source.idle !== false 258 | } 259 | 260 | /** Return true if all values in the given set are idle or paused. */ 261 | function checkIdle(active: Set) { 262 | // Parents can be active even when paused, so the `.every` check 263 | // removes us from the frameloop if all active parents are paused. 264 | return !active.size || Array.from(active).every(isIdle) 265 | } 266 | 267 | /** Become idle if not already idle. */ 268 | function becomeIdle(self: Interpolation) { 269 | if (!self.idle) { 270 | self.idle = true 271 | 272 | each(getPayload(self)!, node => { 273 | node.done = true 274 | }) 275 | 276 | callFluidObservers(self, { 277 | type: 'idle', 278 | parent: self, 279 | }) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /spring/src/SpringPhase.ts: -------------------------------------------------------------------------------- 1 | /** The property symbol of the current animation phase. */ 2 | const $P = Symbol.for('SpringPhase') 3 | 4 | const HAS_ANIMATED = 1 5 | const IS_ANIMATING = 2 6 | const IS_PAUSED = 4 7 | 8 | /** Returns true if the `target` has ever animated. */ 9 | export const hasAnimated = (target: any) => (target[$P] & HAS_ANIMATED) > 0 10 | 11 | /** Returns true if the `target` is animating (even if paused). */ 12 | export const isAnimating = (target: any) => (target[$P] & IS_ANIMATING) > 0 13 | 14 | /** Returns true if the `target` is paused (even if idle). */ 15 | export const isPaused = (target: any) => (target[$P] & IS_PAUSED) > 0 16 | 17 | /** Set the active bit of the `target` phase. */ 18 | export const setActiveBit = (target: any, active: boolean) => 19 | active 20 | ? (target[$P] |= IS_ANIMATING | HAS_ANIMATED) 21 | : (target[$P] &= ~IS_ANIMATING) 22 | 23 | export const setPausedBit = (target: any, paused: boolean) => 24 | paused ? (target[$P] |= IS_PAUSED) : (target[$P] &= ~IS_PAUSED) 25 | -------------------------------------------------------------------------------- /spring/src/SpringRef.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "./Controller" 2 | import { AsyncResult, ControllerUpdate, Falsy, is, Lookup, OneOrMore, each } from "./utils" 3 | 4 | export interface ControllerUpdateFn { 5 | (i: number, ctrl: Controller): ControllerUpdate | Falsy 6 | } 7 | 8 | export interface SpringRef { 9 | (props?: ControllerUpdate | ControllerUpdateFn): AsyncResult< 10 | Controller 11 | >[] 12 | current: Controller[] 13 | 14 | /** Add a controller to this ref */ 15 | add(ctrl: Controller): void 16 | 17 | /** Remove a controller from this ref */ 18 | delete(ctrl: Controller): void 19 | 20 | /** Pause all animations. */ 21 | pause(): this 22 | /** Pause animations for the given keys. */ 23 | pause(keys: OneOrMore): this 24 | /** Pause some or all animations. */ 25 | pause(keys?: OneOrMore): this 26 | 27 | /** Resume all animations. */ 28 | resume(): this 29 | /** Resume animations for the given keys. */ 30 | resume(keys: OneOrMore): this 31 | /** Resume some or all animations. */ 32 | resume(keys?: OneOrMore): this 33 | 34 | /** Update the state of each controller without animating. */ 35 | set(values: Partial): void 36 | 37 | /** Start the queued animations of each controller. */ 38 | start(): AsyncResult>[] 39 | /** Update every controller with the same props. */ 40 | start(props: ControllerUpdate): AsyncResult>[] 41 | /** Update controllers based on their state. */ 42 | start(props: ControllerUpdateFn): AsyncResult>[] 43 | /** Start animating each controller. */ 44 | start( 45 | props?: ControllerUpdate | ControllerUpdateFn 46 | ): AsyncResult>[] 47 | 48 | /** Stop all animations. */ 49 | stop(): this 50 | /** Stop animations for the given keys. */ 51 | stop(keys: OneOrMore): this 52 | /** Cancel all animations. */ 53 | stop(cancel: boolean): this 54 | /** Cancel animations for the given keys. */ 55 | stop(cancel: boolean, keys: OneOrMore): this 56 | /** Stop some or all animations. */ 57 | stop(keys?: OneOrMore): this 58 | /** Cancel some or all animations. */ 59 | stop(cancel: boolean, keys?: OneOrMore): this 60 | 61 | /** Add the same props to each controller's update queue. */ 62 | update(props: ControllerUpdate): this 63 | /** Generate separate props for each controller's update queue. */ 64 | update(props: ControllerUpdateFn): this 65 | /** Add props to each controller's update queue. */ 66 | update(props: ControllerUpdate | ControllerUpdateFn): this 67 | 68 | _getProps( 69 | arg: ControllerUpdate | ControllerUpdateFn, 70 | ctrl: Controller, 71 | index: number 72 | ): ControllerUpdate | Falsy 73 | } 74 | 75 | export const SpringRef = < 76 | State extends Lookup = Lookup 77 | >(): SpringRef => { 78 | const current: Controller[] = [] 79 | 80 | const SpringRef: SpringRef = function (props) { 81 | const results: AsyncResult[] = [] 82 | 83 | each(current, (ctrl, i) => { 84 | if (is.und(props)) { 85 | results.push(ctrl.start()) 86 | } else { 87 | const update = _getProps(props, ctrl, i) 88 | if (update) { 89 | results.push(ctrl.start(update)) 90 | } 91 | } 92 | }) 93 | 94 | return results 95 | } 96 | 97 | SpringRef.current = current 98 | 99 | /** Add a controller to this ref */ 100 | SpringRef.add = function (ctrl: Controller) { 101 | if (!current.includes(ctrl)) { 102 | current.push(ctrl) 103 | } 104 | } 105 | 106 | /** Remove a controller from this ref */ 107 | SpringRef.delete = function (ctrl: Controller) { 108 | const i = current.indexOf(ctrl) 109 | if (~i) current.splice(i, 1) 110 | } 111 | 112 | /** Pause all animations. */ 113 | SpringRef.pause = function () { 114 | each(current, ctrl => ctrl.pause(...arguments)) 115 | return this 116 | } 117 | 118 | /** Resume all animations. */ 119 | SpringRef.resume = function () { 120 | each(current, ctrl => ctrl.resume(...arguments)) 121 | return this 122 | } 123 | 124 | /** Update the state of each controller without animating. */ 125 | SpringRef.set = function (values: Partial) { 126 | each(current, ctrl => ctrl.set(values)) 127 | } 128 | 129 | SpringRef.start = function (props?: object | ControllerUpdateFn) { 130 | const results: AsyncResult[] = [] 131 | 132 | each(current, (ctrl, i) => { 133 | if (is.und(props)) { 134 | results.push(ctrl.start()) 135 | } else { 136 | const update = this._getProps(props, ctrl, i) 137 | if (update) { 138 | results.push(ctrl.start(update)) 139 | } 140 | } 141 | }) 142 | 143 | return results 144 | } 145 | 146 | /** Stop all animations. */ 147 | SpringRef.stop = function () { 148 | each(current, ctrl => ctrl.stop(...arguments)) 149 | return this 150 | } 151 | 152 | SpringRef.update = function (props: object | ControllerUpdateFn) { 153 | each(current, (ctrl, i) => ctrl.update(this._getProps(props, ctrl, i))) 154 | return this 155 | } 156 | 157 | /** Overridden by `useTrail` to manipulate props */ 158 | const _getProps = function ( 159 | arg: ControllerUpdate | ControllerUpdateFn, 160 | ctrl: Controller, 161 | index: number 162 | ): ControllerUpdate | Falsy { 163 | return is.fun(arg) ? arg(index, ctrl) : arg 164 | } 165 | 166 | SpringRef._getProps = _getProps 167 | 168 | return SpringRef 169 | } 170 | -------------------------------------------------------------------------------- /spring/src/SpringValue.ts: -------------------------------------------------------------------------------- 1 | import * as G from './globals' 2 | import {scheduleProps} from './scheduleProps' 3 | import { Animated, AnimatedValue, getAnimated, getPayload, setAnimated } from "./animated"; 4 | import { AnimatedString } from "./AnimatedString"; 5 | import { Animation } from "./Animation"; 6 | import { getCancelledResult, getCombinedResult, getFinishedResult, getNoopResult } from "./AnimationResult"; 7 | import { addFluidObserver, callFluidObservers, FluidValue, getFluidObservers, getFluidValue, hasFluidValue, removeFluidObserver } from "./fluids"; 8 | import { FrameValue, isFrameValue } from "./FrameValue"; 9 | import { raf } from "./rafz"; 10 | import { runAsync, RunAsyncProps, RunAsyncState, stopAsync } from "./runAsync"; 11 | import { hasAnimated, isAnimating, isPaused, setActiveBit, setPausedBit } from "./SpringPhase"; 12 | import { 13 | EventKey, 14 | Lookup, 15 | SpringProps, 16 | SpringUpdate, 17 | eachProp, 18 | getDefaultProp, 19 | resolveProp, 20 | computeGoal, 21 | isEqual, 22 | callProp, 23 | inferTo, 24 | getDefaultProps, 25 | is, 26 | isAsyncTo, 27 | PickEventFns, 28 | AnimationResolver, 29 | VelocityProp, 30 | toArray, 31 | AsyncResult, 32 | flushCalls, 33 | each, 34 | matchProp, 35 | isAnimatedString, 36 | AnimationRange, 37 | getAnimatedType 38 | } from "./utils"; 39 | import { mergeConfig } from './AnimationConfig'; 40 | import { frameLoop } from './FrameLoop'; 41 | 42 | declare const console: any 43 | 44 | interface DefaultSpringProps 45 | extends Pick, 'pause' | 'cancel' | 'immediate' | 'config'>, 46 | PickEventFns> {} 47 | 48 | /** 49 | * Only numbers, strings, and arrays of numbers/strings are supported. 50 | * Non-animatable strings are also supported. 51 | */ 52 | export class SpringValue extends FrameValue { 53 | /** The property name used when `to` or `from` is an object. Useful when debugging too. */ 54 | key?: string; 55 | 56 | /** The animation state */ 57 | animation = new Animation(); 58 | 59 | /** The queue of pending props */ 60 | queue?: SpringUpdate[]; 61 | 62 | /** Some props have customizable default values */ 63 | defaultProps: DefaultSpringProps = {}; 64 | 65 | /** The state for `runAsync` calls */ 66 | protected _state: RunAsyncState> = { 67 | paused: false, 68 | delayed: false, 69 | pauseQueue: new Set(), 70 | resumeQueue: new Set(), 71 | timeouts: new Set(), 72 | }; 73 | 74 | /** The promise resolvers of pending `start` calls */ 75 | protected _pendingCalls = new Set>(); 76 | 77 | /** The counter for tracking `scheduleProps` calls */ 78 | protected _lastCallId = 0; 79 | 80 | /** The last `scheduleProps` call that changed the `to` prop */ 81 | protected _lastToId = 0; 82 | 83 | protected _memoizedDuration = 0; 84 | 85 | constructor(from: Exclude, props?: SpringUpdate); 86 | constructor(props?: SpringUpdate); 87 | constructor(arg1?: any, arg2?: any) { 88 | super(); 89 | if (!is.und(arg1) || !is.und(arg2)) { 90 | const props = is.obj(arg1) ? { ...arg1 } : { ...arg2, from: arg1 }; 91 | if (is.und(props.default)) { 92 | props.default = true; 93 | } 94 | this.start(props); 95 | } 96 | } 97 | 98 | /** Equals true when not advancing on each frame. */ 99 | get idle() { 100 | return !(isAnimating(this) || this._state.asyncTo) || isPaused(this); 101 | } 102 | 103 | get goal() { 104 | return getFluidValue(this.animation.to) as T; 105 | } 106 | 107 | get velocity(): VelocityProp { 108 | const node = getAnimated(this)!; 109 | return ( 110 | node instanceof AnimatedValue 111 | ? node.lastVelocity || 0 112 | : node.getPayload().map((node) => node.lastVelocity || 0) 113 | ) as any; 114 | } 115 | 116 | /** 117 | * When true, this value has been animated at least once. 118 | */ 119 | get hasAnimated() { 120 | return hasAnimated(this); 121 | } 122 | 123 | /** 124 | * When true, this value has an unfinished animation, 125 | * which is either active or paused. 126 | */ 127 | get isAnimating() { 128 | return isAnimating(this); 129 | } 130 | 131 | /** 132 | * When true, all current and future animations are paused. 133 | */ 134 | get isPaused() { 135 | return isPaused(this); 136 | } 137 | 138 | /** 139 | * 140 | * 141 | */ 142 | get isDelayed() { 143 | return this._state.delayed; 144 | } 145 | 146 | /** Advance the current animation by a number of milliseconds */ 147 | advance(dt: number) { 148 | let idle = true; 149 | let changed = false; 150 | 151 | const anim = this.animation; 152 | let { config, toValues } = anim; 153 | 154 | const payload = getPayload(anim.to); 155 | if (!payload && hasFluidValue(anim.to)) { 156 | toValues = toArray(getFluidValue(anim.to)) as any; 157 | } 158 | 159 | anim.values.forEach((node, i) => { 160 | if (node.done) return; 161 | 162 | const to = 163 | // Animated strings always go from 0 to 1. 164 | node.constructor == AnimatedString 165 | ? 1 166 | : payload 167 | ? payload[i].lastPosition 168 | : toValues![i]; 169 | 170 | let finished = anim.immediate; 171 | let position = to; 172 | 173 | if (!finished) { 174 | position = node.lastPosition; 175 | 176 | // Loose springs never move. 177 | if (config.tension <= 0) { 178 | node.done = true; 179 | return; 180 | } 181 | 182 | let elapsed = (node.elapsedTime += dt); 183 | const from = anim.fromValues[i]; 184 | 185 | const v0 = 186 | node.v0 != null 187 | ? node.v0 188 | : (node.v0 = is.arr(config.velocity) 189 | ? config.velocity[i] 190 | : config.velocity); 191 | 192 | let velocity: number; 193 | 194 | // Duration easing 195 | if (!is.und(config.duration)) { 196 | let p = 1; 197 | if (config.duration > 0) { 198 | /** 199 | * Here we check if the duration has changed in the config 200 | * and if so update the elapsed time to the percentage 201 | * of completition so there is no jank in the animation 202 | * https://github.com/pmndrs/react-spring/issues/1163 203 | */ 204 | if (this._memoizedDuration !== config.duration) { 205 | // update the memoized version to the new duration 206 | this._memoizedDuration = config.duration; 207 | 208 | // if the value has started animating we need to update it 209 | if (node.durationProgress > 0) { 210 | // set elapsed time to be the same percentage of progress as the previous duration 211 | node.elapsedTime = config.duration * node.durationProgress; 212 | // add the delta so the below updates work as expected 213 | elapsed = node.elapsedTime += dt; 214 | } 215 | } 216 | 217 | // calculate the new progress 218 | p = (config.progress || 0) + elapsed / this._memoizedDuration; 219 | // p is clamped between 0-1 220 | p = p > 1 ? 1 : p < 0 ? 0 : p; 221 | // store our new progress 222 | node.durationProgress = p; 223 | } 224 | 225 | position = from + config.easing(p) * (to - from); 226 | velocity = (position - node.lastPosition) / dt; 227 | 228 | finished = p == 1; 229 | } 230 | 231 | // Decay easing 232 | else if (config.decay) { 233 | const decay = config.decay === true ? 0.998 : config.decay; 234 | const e = Math.exp(-(1 - decay) * elapsed); 235 | 236 | position = from + (v0 / (1 - decay)) * (1 - e); 237 | finished = Math.abs(node.lastPosition - position) < 0.1; 238 | 239 | // derivative of position 240 | velocity = v0 * e; 241 | } 242 | 243 | // Spring easing 244 | else { 245 | velocity = node.lastVelocity == null ? v0 : node.lastVelocity; 246 | 247 | /** The smallest distance from a value before being treated like said value. */ 248 | /** 249 | * TODO: make this value ~0.0001 by default in next breaking change 250 | * for more info see – https://github.com/pmndrs/react-spring/issues/1389 251 | */ 252 | const precision = 253 | config.precision || 254 | (from == to ? 0.005 : Math.min(1, Math.abs(to - from) * 0.001)); 255 | 256 | /** The velocity at which movement is essentially none */ 257 | const restVelocity = config.restVelocity || precision / 10; 258 | 259 | // Bouncing is opt-in (not to be confused with overshooting) 260 | const bounceFactor = config.clamp ? 0 : config.bounce!; 261 | const canBounce = !is.und(bounceFactor); 262 | 263 | /** When `true`, the value is increasing over time */ 264 | const isGrowing = from == to ? node.v0 > 0 : from < to; 265 | 266 | /** When `true`, the velocity is considered moving */ 267 | let isMoving!: boolean; 268 | 269 | /** When `true`, the velocity is being deflected or clamped */ 270 | let isBouncing = false; 271 | 272 | const step = 1; // 1ms 273 | const numSteps = Math.ceil(dt / step); 274 | for (let n = 0; n < numSteps; ++n) { 275 | isMoving = Math.abs(velocity) > restVelocity; 276 | 277 | if (!isMoving) { 278 | finished = Math.abs(to - position) <= precision; 279 | if (finished) { 280 | break; 281 | } 282 | } 283 | 284 | if (canBounce) { 285 | isBouncing = position == to || position > to == isGrowing; 286 | 287 | // Invert the velocity with a magnitude, or clamp it. 288 | if (isBouncing) { 289 | velocity = -velocity * bounceFactor; 290 | position = to; 291 | } 292 | } 293 | 294 | const springForce = -config.tension * 0.000001 * (position - to); 295 | const dampingForce = -config.friction * 0.001 * velocity; 296 | const acceleration = (springForce + dampingForce) / config.mass; // pt/ms^2 297 | 298 | velocity = velocity + acceleration * step; // pt/ms 299 | position = position + velocity * step; 300 | } 301 | } 302 | 303 | node.lastVelocity = velocity; 304 | 305 | if (Number.isNaN(position)) { 306 | console.warn(`Got NaN while animating:`, this); 307 | finished = true; 308 | } 309 | } 310 | 311 | // Parent springs must finish before their children can. 312 | if (payload && !payload[i].done) { 313 | finished = false; 314 | } 315 | 316 | if (finished) { 317 | node.done = true; 318 | } else { 319 | idle = false; 320 | } 321 | 322 | if (node.setValue(position, config.round)) { 323 | changed = true; 324 | } 325 | }); 326 | 327 | const node = getAnimated(this)!; 328 | /** 329 | * Get the node's current value, this will be different 330 | * to anim.to when config.decay is true 331 | */ 332 | const currVal = node.getValue(); 333 | if (idle) { 334 | // get our final fluid val from the anim.to 335 | const finalVal = getFluidValue(anim.to); 336 | /** 337 | * check if they're not equal, or if they're 338 | * change and if there's no config.decay set 339 | */ 340 | if ((currVal !== finalVal || changed) && !config.decay) { 341 | // set the value to anim.to 342 | node.setValue(finalVal); 343 | this._onChange(finalVal); 344 | } else if (changed && config.decay) { 345 | /** 346 | * if it's changed but there is a config.decay, 347 | * just call _onChange with currrent value 348 | */ 349 | this._onChange(currVal); 350 | } 351 | // call stop because the spring has stopped. 352 | this._stop(); 353 | } else if (changed) { 354 | /** 355 | * if the spring has changed, but is not idle, 356 | * just call the _onChange handler 357 | */ 358 | this._onChange(currVal); 359 | } 360 | } 361 | 362 | /** Set the current value, while stopping the current animation */ 363 | set(value: T | FluidValue) { 364 | raf.batchedUpdates(() => { 365 | this._stop(); 366 | 367 | // These override the current value and goal value that may have 368 | // been updated by `onRest` handlers in the `_stop` call above. 369 | this._focus(value); 370 | this._set(value); 371 | }); 372 | return this; 373 | } 374 | 375 | /** 376 | * Freeze the active animation in time, as well as any updates merged 377 | * before `resume` is called. 378 | */ 379 | pause() { 380 | this._update({ pause: true }); 381 | } 382 | 383 | /** Resume the animation if paused. */ 384 | resume() { 385 | this._update({ pause: false }); 386 | } 387 | 388 | /** Skip to the end of the current animation. */ 389 | finish() { 390 | if (isAnimating(this)) { 391 | const { to, config } = this.animation; 392 | raf.batchedUpdates(() => { 393 | // Ensure the "onStart" and "onRest" props are called. 394 | this._onStart(); 395 | 396 | // Jump to the goal value, except for decay animations 397 | // which have an undefined goal value. 398 | if (!config.decay) { 399 | this._set(to, false); 400 | } 401 | 402 | this._stop(); 403 | }); 404 | } 405 | return this; 406 | } 407 | 408 | /** Push props into the pending queue. */ 409 | update(props: SpringUpdate) { 410 | const queue = this.queue || (this.queue = []); 411 | queue.push(props); 412 | return this; 413 | } 414 | 415 | /** 416 | * Update this value's animation using the queue of pending props, 417 | * and unpause the current animation (if one is frozen). 418 | * 419 | * When arguments are passed, a new animation is created, and the 420 | * queued animations are left alone. 421 | */ 422 | start(): AsyncResult; 423 | 424 | start(props: SpringUpdate): AsyncResult; 425 | 426 | start(to: T, props?: SpringProps): AsyncResult; 427 | 428 | start(to?: T | SpringUpdate, arg2?: SpringProps) { 429 | let queue: SpringUpdate[]; 430 | if (!is.und(to)) { 431 | queue = [is.obj(to) ? to : { ...arg2, to }]; 432 | } else { 433 | queue = this.queue || []; 434 | this.queue = []; 435 | } 436 | 437 | return Promise.all( 438 | queue.map((props) => { 439 | const up = this._update(props); 440 | return up; 441 | }) 442 | ).then((results) => getCombinedResult(this, results)); 443 | } 444 | 445 | /** 446 | * Stop the current animation, and cancel any delayed updates. 447 | * 448 | * Pass `true` to call `onRest` with `cancelled: true`. 449 | */ 450 | stop(cancel?: boolean) { 451 | const { to } = this.animation; 452 | 453 | // The current value becomes the goal value. 454 | this._focus(this.get()); 455 | 456 | stopAsync(this._state, cancel && this._lastCallId); 457 | raf.batchedUpdates(() => this._stop(to, cancel)); 458 | 459 | return this; 460 | } 461 | 462 | /** Restart the animation. */ 463 | reset() { 464 | this._update({ reset: true }); 465 | } 466 | 467 | /** @internal */ 468 | eventObserved(event: FrameValue.Event) { 469 | if (event.type == "change") { 470 | this._start(); 471 | } else if (event.type == "priority") { 472 | this.priority = event.priority + 1; 473 | } 474 | } 475 | 476 | /** 477 | * Parse the `to` and `from` range from the given `props` object. 478 | * 479 | * This also ensures the initial value is available to animated components 480 | * during the render phase. 481 | */ 482 | protected _prepareNode(props: { 483 | to?: any; 484 | from?: any; 485 | reverse?: boolean; 486 | default?: any; 487 | }) { 488 | const key = this.key || ""; 489 | 490 | let { to, from } = props; 491 | 492 | to = is.obj(to) ? to[key] : to; 493 | if (to == null || isAsyncTo(to)) { 494 | to = undefined; 495 | } 496 | 497 | from = is.obj(from) ? from[key] : from; 498 | if (from == null) { 499 | from = undefined; 500 | } 501 | 502 | // Create the range now to avoid "reverse" logic. 503 | const range = { to, from }; 504 | 505 | // Before ever animating, this method ensures an `Animated` node 506 | // exists and keeps its value in sync with the "from" prop. 507 | if (!hasAnimated(this)) { 508 | if (props.reverse) [to, from] = [from, to]; 509 | 510 | from = getFluidValue(from); 511 | if (!is.und(from)) { 512 | this._set(from); 513 | } 514 | // Use the "to" value if our node is undefined. 515 | else if (!getAnimated(this)) { 516 | this._set(to); 517 | } 518 | } 519 | 520 | return range; 521 | } 522 | 523 | /** Every update is processed by this method before merging. */ 524 | protected _update( 525 | { ...props }: SpringProps, 526 | isLoop?: boolean 527 | ): AsyncResult> { 528 | const { key, defaultProps } = this; 529 | 530 | // Update the default props immediately. 531 | if (props.default) 532 | Object.assign( 533 | defaultProps, 534 | getDefaultProps(props, (value, prop) => 535 | /^on/.test(prop) ? resolveProp(value, key) : value 536 | ) 537 | ); 538 | 539 | mergeActiveFn(this, props, "onProps"); 540 | sendEvent(this, "onProps", props, this); 541 | 542 | // Ensure the initial value can be accessed by animated components. 543 | const range = this._prepareNode(props); 544 | 545 | if (Object.isFrozen(this)) { 546 | throw Error( 547 | "Cannot animate a `SpringValue` object that is frozen. " + 548 | "Did you forget to pass your component to `animated(...)` before animating its props?" 549 | ); 550 | } 551 | 552 | const state = this._state; 553 | 554 | return scheduleProps(++this._lastCallId, { 555 | key, 556 | props, 557 | defaultProps, 558 | state, 559 | actions: { 560 | pause: () => { 561 | if (!isPaused(this)) { 562 | setPausedBit(this, true); 563 | flushCalls(state.pauseQueue); 564 | sendEvent( 565 | this, 566 | "onPause", 567 | getFinishedResult(this, checkFinished(this, this.animation.to)), 568 | this 569 | ); 570 | } 571 | }, 572 | resume: () => { 573 | if (isPaused(this)) { 574 | setPausedBit(this, false); 575 | if (isAnimating(this)) { 576 | this._resume(); 577 | } 578 | flushCalls(state.resumeQueue); 579 | sendEvent( 580 | this, 581 | "onResume", 582 | getFinishedResult(this, checkFinished(this, this.animation.to)), 583 | this 584 | ); 585 | } 586 | }, 587 | start: this._merge.bind(this, range), 588 | }, 589 | }).then((result) => { 590 | if (props.loop && result.finished && !(isLoop && result.noop)) { 591 | const nextProps = createLoopUpdate(props); 592 | if (nextProps) { 593 | return this._update(nextProps, true); 594 | } 595 | } 596 | return result; 597 | }); 598 | } 599 | 600 | /** Merge props into the current animation */ 601 | protected _merge( 602 | range: AnimationRange, 603 | props: RunAsyncProps>, 604 | resolve: AnimationResolver> 605 | ): void { 606 | // The "cancel" prop cancels all pending delays and it forces the 607 | // active animation to stop where it is. 608 | if (props.cancel) { 609 | this.stop(true); 610 | return resolve(getCancelledResult(this)); 611 | } 612 | 613 | /** The "to" prop is defined. */ 614 | const hasToProp = !is.und(range.to); 615 | 616 | /** The "from" prop is defined. */ 617 | const hasFromProp = !is.und(range.from); 618 | 619 | // Avoid merging other props if implicitly prevented, except 620 | // when both the "to" and "from" props are undefined. 621 | if (hasToProp || hasFromProp) { 622 | if (props.callId > this._lastToId) { 623 | this._lastToId = props.callId; 624 | } else { 625 | return resolve(getCancelledResult(this)); 626 | } 627 | } 628 | 629 | const { key, defaultProps, animation: anim } = this; 630 | const { to: prevTo, from: prevFrom } = anim; 631 | let { to = prevTo, from = prevFrom } = range; 632 | 633 | // Focus the "from" value if changing without a "to" value. 634 | // For default updates, do this only if no "to" value exists. 635 | if (hasFromProp && !hasToProp && (!props.default || is.und(to))) { 636 | to = from; 637 | } 638 | 639 | // Flip the current range if "reverse" is true. 640 | if (props.reverse) [to, from] = [from, to]; 641 | 642 | /** The "from" value is changing. */ 643 | const hasFromChanged = !isEqual(from, prevFrom); 644 | 645 | if (hasFromChanged) { 646 | anim.from = from; 647 | } 648 | 649 | // Coerce "from" into a static value. 650 | from = getFluidValue(from); 651 | 652 | /** The "to" value is changing. */ 653 | const hasToChanged = !isEqual(to, prevTo); 654 | 655 | if (hasToChanged) { 656 | this._focus(to); 657 | } 658 | 659 | /** The "to" prop is async. */ 660 | const hasAsyncTo = isAsyncTo(props.to); 661 | 662 | const { config } = anim; 663 | const { decay, velocity } = config; 664 | 665 | // Reset to default velocity when goal values are defined. 666 | if (hasToProp || hasFromProp) { 667 | config.velocity = 0; 668 | } 669 | 670 | // The "runAsync" function treats the "config" prop as a default, 671 | // so we must avoid merging it when the "to" prop is async. 672 | if (props.config && !hasAsyncTo) { 673 | mergeConfig( 674 | config, 675 | callProp(props.config, key!), 676 | // Avoid calling the same "config" prop twice. 677 | props.config !== defaultProps.config 678 | ? callProp(defaultProps.config, key!) 679 | : void 0 680 | ); 681 | } 682 | 683 | // This instance might not have its Animated node yet. For example, 684 | // the constructor can be given props without a "to" or "from" value. 685 | let node = getAnimated(this); 686 | if (!node || is.und(to)) { 687 | return resolve(getFinishedResult(this, true)); 688 | } 689 | 690 | /** When true, start at the "from" value. */ 691 | const reset = 692 | // When `reset` is undefined, the `from` prop implies `reset: true`, 693 | // except for declarative updates. When `reset` is defined, there 694 | // must exist a value to animate from. 695 | is.und(props.reset) 696 | ? hasFromProp && !props.default 697 | : !is.und(from) && matchProp(props.reset, key); 698 | 699 | // The current value, where the animation starts from. 700 | const value = reset ? (from as T) : this.get(); 701 | 702 | // The animation ends at this value, unless "to" is fluid. 703 | const goal = computeGoal(to); 704 | 705 | // Only specific types can be animated to/from. 706 | const isAnimatable = is.num(goal) || is.arr(goal) || isAnimatedString(goal); 707 | 708 | // When true, the value changes instantly on the next frame. 709 | const immediate = 710 | !hasAsyncTo && 711 | (!isAnimatable || 712 | matchProp(defaultProps.immediate || props.immediate, key)); 713 | 714 | if (hasToChanged) { 715 | const nodeType = getAnimatedType(to); 716 | if (nodeType !== node.constructor) { 717 | if (immediate) { 718 | node = this._set(goal)!; 719 | } else 720 | throw Error( 721 | `Cannot animate between ${node.constructor.name} and ${nodeType.name}, as the "to" prop suggests` 722 | ); 723 | } 724 | } 725 | 726 | // The type of Animated node for the goal value. 727 | const goalType = node.constructor; 728 | 729 | // When the goal value is fluid, we don't know if its value 730 | // will change before the next animation frame, so it always 731 | // starts the animation to be safe. 732 | let started = hasFluidValue(to); 733 | let finished = false; 734 | 735 | if (!started) { 736 | // When true, the current value has probably changed. 737 | const hasValueChanged = reset || (!hasAnimated(this) && hasFromChanged); 738 | 739 | // When the "to" value or current value are changed, 740 | // start animating if not already finished. 741 | if (hasToChanged || hasValueChanged) { 742 | finished = isEqual(computeGoal(value), goal); 743 | started = !finished; 744 | } 745 | 746 | // Changing "decay" or "velocity" starts the animation. 747 | if ( 748 | (!isEqual(anim.immediate, immediate) && !immediate) || 749 | !isEqual(config.decay, decay) || 750 | !isEqual(config.velocity, velocity) 751 | ) { 752 | started = true; 753 | } 754 | } 755 | 756 | // Was the goal value set to the current value while animating? 757 | if (finished && isAnimating(this)) { 758 | // If the first frame has passed, allow the animation to 759 | // overshoot instead of stopping abruptly. 760 | if (anim.changed && !reset) { 761 | started = true; 762 | } 763 | // Stop the animation before its first frame. 764 | else if (!started) { 765 | this._stop(prevTo); 766 | } 767 | } 768 | 769 | if (!hasAsyncTo) { 770 | // Make sure our "toValues" are updated even if our previous 771 | // "to" prop is a fluid value whose current value is also ours. 772 | if (started || hasFluidValue(prevTo)) { 773 | anim.values = node.getPayload(); 774 | anim.toValues = hasFluidValue(to) 775 | ? null 776 | : goalType == AnimatedString 777 | ? [1] 778 | : toArray(goal); 779 | } 780 | 781 | if (anim.immediate != immediate) { 782 | anim.immediate = immediate; 783 | 784 | // Ensure the immediate goal is used as from value. 785 | if (!immediate && !reset) { 786 | this._set(prevTo); 787 | } 788 | } 789 | 790 | if (started) { 791 | const { onRest } = anim; 792 | 793 | // Set the active handlers when an animation starts. 794 | each(ACTIVE_EVENTS, (type) => mergeActiveFn(this, props, type)); 795 | 796 | const result = getFinishedResult(this, checkFinished(this, prevTo)); 797 | flushCalls(this._pendingCalls, result); 798 | this._pendingCalls.add(resolve); 799 | 800 | if (anim.changed) 801 | raf.batchedUpdates(() => { 802 | // Ensure `onStart` can be called after a reset. 803 | anim.changed = !reset; 804 | 805 | // Call the active `onRest` handler from the interrupted animation. 806 | onRest?.(result, this); 807 | 808 | // Notify the default `onRest` of the reset, but wait for the 809 | // first frame to pass before sending an `onStart` event. 810 | if (reset) { 811 | callProp(defaultProps.onRest, result); 812 | } 813 | // Call the active `onStart` handler here since the first frame 814 | // has already passed, which means this is a goal update and not 815 | // an entirely new animation. 816 | else { 817 | anim.onStart?.(result, this); 818 | } 819 | }); 820 | } 821 | } 822 | 823 | if (reset) { 824 | this._set(value); 825 | } 826 | 827 | if (hasAsyncTo) { 828 | resolve(runAsync(props.to, props, this._state, this)); 829 | } 830 | 831 | // Start an animation 832 | else if (started) { 833 | this._start(); 834 | } 835 | 836 | // Postpone promise resolution until the animation is finished, 837 | // so that no-op updates still resolve at the expected time. 838 | else if (isAnimating(this) && !hasToChanged) { 839 | this._pendingCalls.add(resolve); 840 | } 841 | 842 | // Resolve our promise immediately. 843 | else { 844 | resolve(getNoopResult(value)); 845 | } 846 | } 847 | 848 | /** Update the `animation.to` value, which might be a `FluidValue` */ 849 | protected _focus(value: T | FluidValue) { 850 | const anim = this.animation; 851 | if (value !== anim.to) { 852 | if (getFluidObservers(this)) { 853 | this._detach(); 854 | } 855 | anim.to = value; 856 | if (getFluidObservers(this)) { 857 | this._attach(); 858 | } 859 | } 860 | } 861 | 862 | protected _attach() { 863 | let priority = 0; 864 | 865 | const { to } = this.animation; 866 | if (hasFluidValue(to)) { 867 | addFluidObserver(to, this); 868 | if (isFrameValue(to)) { 869 | priority = to.priority + 1; 870 | } 871 | } 872 | 873 | this.priority = priority; 874 | } 875 | 876 | protected _detach() { 877 | const { to } = this.animation; 878 | if (hasFluidValue(to)) { 879 | removeFluidObserver(to, this); 880 | } 881 | } 882 | 883 | /** 884 | * Update the current value from outside the frameloop, 885 | * and return the `Animated` node. 886 | */ 887 | protected _set(arg: T | FluidValue, idle = true): Animated | undefined { 888 | const value = getFluidValue(arg); 889 | if (!is.und(value)) { 890 | const oldNode = getAnimated(this); 891 | if (!oldNode || !isEqual(value, oldNode.getValue())) { 892 | // Create a new node or update the existing node. 893 | const nodeType = getAnimatedType(value); 894 | if (!oldNode || oldNode.constructor != nodeType) { 895 | setAnimated(this, nodeType.create(value)); 896 | } else { 897 | oldNode.setValue(value); 898 | } 899 | // Never emit a "change" event for the initial value. 900 | if (oldNode) { 901 | raf.batchedUpdates(() => { 902 | this._onChange(value, idle); 903 | }); 904 | } 905 | } 906 | } 907 | return getAnimated(this); 908 | } 909 | 910 | protected _onStart() { 911 | const anim = this.animation; 912 | if (!anim.changed) { 913 | anim.changed = true; 914 | sendEvent( 915 | this, 916 | "onStart", 917 | getFinishedResult(this, checkFinished(this, anim.to)), 918 | this 919 | ); 920 | } 921 | } 922 | 923 | protected _onChange(value: T, idle?: boolean) { 924 | if (!idle) { 925 | this._onStart(); 926 | callProp(this.animation.onChange, value, this); 927 | } 928 | callProp(this.defaultProps.onChange, value, this); 929 | super._onChange(value, idle); 930 | } 931 | 932 | // This method resets the animation state (even if already animating) to 933 | // ensure the latest from/to range is used, and it also ensures this spring 934 | // is added to the frameloop. 935 | protected _start() { 936 | const anim = this.animation; 937 | 938 | // Reset the state of each Animated node. 939 | getAnimated(this)!.reset(getFluidValue(anim.to)); 940 | 941 | // Use the current values as the from values. 942 | if (!anim.immediate) { 943 | anim.fromValues = anim.values.map((node) => node.lastPosition); 944 | } 945 | 946 | if (!isAnimating(this)) { 947 | setActiveBit(this, true); 948 | if (!isPaused(this)) { 949 | this._resume(); 950 | } 951 | } 952 | } 953 | 954 | protected _resume() { 955 | // The "skipAnimation" global avoids the frameloop. 956 | if (G.skipAnimation) { 957 | this.finish(); 958 | } else { 959 | frameLoop.start(this); 960 | } 961 | } 962 | 963 | /** 964 | * Exit the frameloop and notify `onRest` listeners. 965 | * 966 | * Always wrap `_stop` calls with `batchedUpdates`. 967 | */ 968 | protected _stop(goal?: any, cancel?: boolean) { 969 | if (isAnimating(this)) { 970 | setActiveBit(this, false); 971 | 972 | const anim = this.animation; 973 | each(anim.values, (node) => { 974 | node.done = true; 975 | }); 976 | 977 | // These active handlers must be reset to undefined or else 978 | // they could be called while idle. But keep them defined 979 | // when the goal value is dynamic. 980 | if (anim.toValues) { 981 | anim.onChange = anim.onPause = anim.onResume = undefined; 982 | } 983 | 984 | callFluidObservers(this, { 985 | type: "idle", 986 | parent: this, 987 | }); 988 | 989 | const result = cancel 990 | ? getCancelledResult(this.get()) 991 | : getFinishedResult(this.get(), checkFinished(this, goal ?? anim.to)); 992 | 993 | flushCalls(this._pendingCalls, result); 994 | if (anim.changed) { 995 | anim.changed = false; 996 | sendEvent(this, "onRest", result, this); 997 | } 998 | } 999 | } 1000 | } 1001 | 1002 | /** Returns true when the current value and goal value are equal. */ 1003 | function checkFinished(target: SpringValue, to: T | FluidValue) { 1004 | const goal = computeGoal(to); 1005 | const value = computeGoal(target.get()); 1006 | return isEqual(value, goal); 1007 | } 1008 | 1009 | export function createLoopUpdate( 1010 | props: T & { loop?: any; to?: any; from?: any; reverse?: any }, 1011 | loop = props.loop, 1012 | to = props.to 1013 | ): T | undefined { 1014 | let loopRet = callProp(loop); 1015 | if (loopRet) { 1016 | const overrides = loopRet !== true && inferTo(loopRet); 1017 | const reverse = (overrides || props).reverse; 1018 | const reset = !overrides || overrides.reset; 1019 | return createUpdate({ 1020 | ...props, 1021 | loop, 1022 | 1023 | // Avoid updating default props when looping. 1024 | default: false, 1025 | 1026 | // Never loop the `pause` prop. 1027 | pause: undefined, 1028 | 1029 | // For the "reverse" prop to loop as expected, the "to" prop 1030 | // must be undefined. The "reverse" prop is ignored when the 1031 | // "to" prop is an array or function. 1032 | to: !reverse || isAsyncTo(to) ? to : undefined, 1033 | 1034 | // Ignore the "from" prop except on reset. 1035 | from: reset ? props.from : undefined, 1036 | reset, 1037 | 1038 | // The "loop" prop can return a "useSpring" props object to 1039 | // override any of the original props. 1040 | ...overrides, 1041 | }); 1042 | } 1043 | } 1044 | 1045 | /** 1046 | * Return a new object based on the given `props`. 1047 | * 1048 | * - All non-reserved props are moved into the `to` prop object. 1049 | * - The `keys` prop is set to an array of affected keys, 1050 | * or `null` if all keys are affected. 1051 | */ 1052 | export function createUpdate(props: any) { 1053 | const { to, from } = (props = inferTo(props)); 1054 | 1055 | // Collect the keys affected by this update. 1056 | const keys = new Set(); 1057 | 1058 | if (is.obj(to)) findDefined(to, keys); 1059 | if (is.obj(from)) findDefined(from, keys); 1060 | 1061 | // The "keys" prop helps in applying updates to affected keys only. 1062 | props.keys = keys.size ? Array.from(keys) : null; 1063 | 1064 | return props; 1065 | } 1066 | 1067 | /** 1068 | * A modified version of `createUpdate` meant for declarative APIs. 1069 | */ 1070 | export function declareUpdate(props: any) { 1071 | const update = createUpdate(props); 1072 | if (is.und(update.default)) { 1073 | update.default = getDefaultProps(update); 1074 | } 1075 | return update; 1076 | } 1077 | 1078 | /** Find keys with defined values */ 1079 | function findDefined(values: Lookup, keys: Set) { 1080 | eachProp(values, (value, key) => value != null && keys.add(key as any)); 1081 | } 1082 | 1083 | /** Event props with "active handler" support */ 1084 | const ACTIVE_EVENTS = [ 1085 | "onStart", 1086 | "onRest", 1087 | "onChange", 1088 | "onPause", 1089 | "onResume", 1090 | ] as const; 1091 | 1092 | function mergeActiveFn( 1093 | target: SpringValue, 1094 | props: SpringProps, 1095 | type: P 1096 | ) { 1097 | target.animation[type] = 1098 | props[type] !== getDefaultProp(props, type) 1099 | ? resolveProp(props[type], target.key) 1100 | : undefined; 1101 | } 1102 | 1103 | type EventArgs = Parameters< 1104 | Extract[P], Function> 1105 | >; 1106 | 1107 | /** Call the active handler first, then the default handler. */ 1108 | function sendEvent( 1109 | target: SpringValue, 1110 | type: P, 1111 | ...args: EventArgs 1112 | ) { 1113 | target.animation[type]?.(...(args as [any, any])); 1114 | target.defaultProps[type]?.(...(args as [any, any])); 1115 | } 1116 | -------------------------------------------------------------------------------- /spring/src/animated.ts: -------------------------------------------------------------------------------- 1 | import { defineHidden, is } from "./utils"; 2 | 3 | const $node: any = Symbol.for("Animated:node"); 4 | 5 | export const isAnimated = (value: any): value is Animated => 6 | !!value && value[$node] === value; 7 | 8 | /** Get the owner's `Animated` node. */ 9 | export const getAnimated = (owner: any): Animated | undefined => 10 | owner && owner[$node]; 11 | 12 | /** Set the owner's `Animated` node. */ 13 | export const setAnimated = (owner: any, node: Animated) => 14 | defineHidden(owner, $node, node); 15 | 16 | /** Get every `AnimatedValue` in the owner's `Animated` node. */ 17 | export const getPayload = (owner: any): AnimatedValue[] | undefined => 18 | owner && owner[$node] && owner[$node].getPayload(); 19 | 20 | export abstract class Animated { 21 | /** The cache of animated values */ 22 | protected payload?: Payload; 23 | 24 | constructor() { 25 | // This makes "isAnimated" return true. 26 | setAnimated(this, this); 27 | } 28 | 29 | /** Get the current value. Pass `true` for only animated values. */ 30 | abstract getValue(animated?: boolean): T; 31 | 32 | /** Set the current value. Returns `true` if the value changed. */ 33 | abstract setValue(value: T): boolean | void; 34 | 35 | /** Reset any animation state. */ 36 | abstract reset(goal?: T): void; 37 | 38 | /** Get every `AnimatedValue` used by this node. */ 39 | getPayload(): Payload { 40 | return this.payload || []; 41 | } 42 | } 43 | 44 | export type Payload = readonly AnimatedValue[]; 45 | /** An animated number or a native attribute value */ 46 | export class AnimatedValue extends Animated { 47 | done = true; 48 | elapsedTime!: number; 49 | lastPosition!: number; 50 | lastVelocity?: number | null; 51 | v0?: number | null; 52 | durationProgress = 0; 53 | 54 | constructor(protected _value: T) { 55 | super(); 56 | if (is.num(this._value)) { 57 | this.lastPosition = this._value; 58 | } 59 | } 60 | 61 | /** @internal */ 62 | static create(value: any) { 63 | return new AnimatedValue(value); 64 | } 65 | 66 | getPayload(): Payload { 67 | return [this]; 68 | } 69 | 70 | getValue() { 71 | return this._value; 72 | } 73 | 74 | setValue(value: T, step?: number) { 75 | if (is.num(value)) { 76 | this.lastPosition = value; 77 | if (step) { 78 | value = (Math.round(value / step) * step) as any; 79 | if (this.done) { 80 | this.lastPosition = value as any; 81 | } 82 | } 83 | } 84 | if (this._value === value) { 85 | return false; 86 | } 87 | this._value = value; 88 | return true; 89 | } 90 | 91 | reset() { 92 | const { done } = this; 93 | this.done = false; 94 | if (is.num(this._value)) { 95 | this.elapsedTime = 0; 96 | this.durationProgress = 0; 97 | this.lastPosition = this._value; 98 | if (done) this.lastVelocity = null; 99 | this.v0 = null; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /spring/src/applyAnimatedValues.ts: -------------------------------------------------------------------------------- 1 | import { Lookup } from "./utils"; 2 | 3 | const isCustomPropRE = /^--/; 4 | 5 | type Value = string | number | boolean | null; 6 | 7 | function dangerousStyleValue(name: string, value: Value) { 8 | if (value == null || typeof value === "boolean" || value === "") return ""; 9 | if ( 10 | typeof value === "number" && 11 | value !== 0 && 12 | !isCustomPropRE.test(name) && 13 | !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) 14 | ) 15 | return value + "px"; 16 | // Presumes implicit 'px' suffix for unitless numbers 17 | return ("" + value).trim(); 18 | } 19 | 20 | const attributeCache: Lookup = {}; 21 | 22 | type Instance = HTMLDivElement & { style?: Lookup }; 23 | 24 | export function applyAnimatedValues(instance: Instance, props: Lookup) { 25 | if (!instance.nodeType || !instance.setAttribute) { 26 | return false; 27 | } 28 | 29 | const isFilterElement = 30 | instance.nodeName === "filter" || 31 | (instance.parentNode && instance.parentNode.nodeName === "filter"); 32 | 33 | const { style, children, scrollTop, scrollLeft, ...attributes } = props!; 34 | 35 | const values = Object.values(attributes); 36 | const names = Object.keys(attributes).map((name) => 37 | isFilterElement || instance.hasAttribute(name) 38 | ? name 39 | : attributeCache[name] || 40 | (attributeCache[name] = name.replace( 41 | /([A-Z])/g, 42 | // Attributes are written in dash case 43 | (n) => "-" + n.toLowerCase() 44 | )) 45 | ); 46 | 47 | if (children !== void 0) { 48 | instance.textContent = children; 49 | } 50 | 51 | // Apply CSS styles 52 | for (let name in style) { 53 | if (style.hasOwnProperty(name)) { 54 | const value = dangerousStyleValue(name, style[name]); 55 | if (isCustomPropRE.test(name)) { 56 | instance.style.setProperty(name, value); 57 | } else { 58 | instance.style[name] = value; 59 | } 60 | } 61 | } 62 | 63 | // Apply DOM attributes 64 | names.forEach((name, i) => { 65 | instance.setAttribute(name, values[i]); 66 | }); 67 | 68 | if (scrollTop !== void 0) { 69 | instance.scrollTop = scrollTop; 70 | } 71 | if (scrollLeft !== void 0) { 72 | instance.scrollLeft = scrollLeft; 73 | } 74 | } 75 | 76 | let isUnitlessNumber: { [key: string]: true } = { 77 | animationIterationCount: true, 78 | borderImageOutset: true, 79 | borderImageSlice: true, 80 | borderImageWidth: true, 81 | boxFlex: true, 82 | boxFlexGroup: true, 83 | boxOrdinalGroup: true, 84 | columnCount: true, 85 | columns: true, 86 | flex: true, 87 | flexGrow: true, 88 | flexPositive: true, 89 | flexShrink: true, 90 | flexNegative: true, 91 | flexOrder: true, 92 | gridRow: true, 93 | gridRowEnd: true, 94 | gridRowSpan: true, 95 | gridRowStart: true, 96 | gridColumn: true, 97 | gridColumnEnd: true, 98 | gridColumnSpan: true, 99 | gridColumnStart: true, 100 | fontWeight: true, 101 | lineClamp: true, 102 | lineHeight: true, 103 | opacity: true, 104 | order: true, 105 | orphans: true, 106 | tabSize: true, 107 | widows: true, 108 | zIndex: true, 109 | zoom: true, 110 | // SVG-related properties 111 | fillOpacity: true, 112 | floodOpacity: true, 113 | stopOpacity: true, 114 | strokeDasharray: true, 115 | strokeDashoffset: true, 116 | strokeMiterlimit: true, 117 | strokeOpacity: true, 118 | strokeWidth: true, 119 | }; 120 | 121 | const prefixKey = (prefix: string, key: string) => 122 | prefix + key.charAt(0).toUpperCase() + key.substring(1); 123 | const prefixes = ["Webkit", "Ms", "Moz", "O"]; 124 | 125 | isUnitlessNumber = Object.keys(isUnitlessNumber).reduce((acc, prop) => { 126 | prefixes.forEach((prefix) => (acc[prefixKey(prefix, prop)] = acc[prop])); 127 | return acc; 128 | }, isUnitlessNumber); 129 | -------------------------------------------------------------------------------- /spring/src/colors.ts: -------------------------------------------------------------------------------- 1 | export type ColorName = keyof typeof colors 2 | 3 | // http://www.w3.org/TR/css3-color/#svg-color 4 | export const colors = { 5 | transparent: 0x00000000, 6 | aliceblue: 0xf0f8ffff, 7 | antiquewhite: 0xfaebd7ff, 8 | aqua: 0x00ffffff, 9 | aquamarine: 0x7fffd4ff, 10 | azure: 0xf0ffffff, 11 | beige: 0xf5f5dcff, 12 | bisque: 0xffe4c4ff, 13 | black: 0x000000ff, 14 | blanchedalmond: 0xffebcdff, 15 | blue: 0x0000ffff, 16 | blueviolet: 0x8a2be2ff, 17 | brown: 0xa52a2aff, 18 | burlywood: 0xdeb887ff, 19 | burntsienna: 0xea7e5dff, 20 | cadetblue: 0x5f9ea0ff, 21 | chartreuse: 0x7fff00ff, 22 | chocolate: 0xd2691eff, 23 | coral: 0xff7f50ff, 24 | cornflowerblue: 0x6495edff, 25 | cornsilk: 0xfff8dcff, 26 | crimson: 0xdc143cff, 27 | cyan: 0x00ffffff, 28 | darkblue: 0x00008bff, 29 | darkcyan: 0x008b8bff, 30 | darkgoldenrod: 0xb8860bff, 31 | darkgray: 0xa9a9a9ff, 32 | darkgreen: 0x006400ff, 33 | darkgrey: 0xa9a9a9ff, 34 | darkkhaki: 0xbdb76bff, 35 | darkmagenta: 0x8b008bff, 36 | darkolivegreen: 0x556b2fff, 37 | darkorange: 0xff8c00ff, 38 | darkorchid: 0x9932ccff, 39 | darkred: 0x8b0000ff, 40 | darksalmon: 0xe9967aff, 41 | darkseagreen: 0x8fbc8fff, 42 | darkslateblue: 0x483d8bff, 43 | darkslategray: 0x2f4f4fff, 44 | darkslategrey: 0x2f4f4fff, 45 | darkturquoise: 0x00ced1ff, 46 | darkviolet: 0x9400d3ff, 47 | deeppink: 0xff1493ff, 48 | deepskyblue: 0x00bfffff, 49 | dimgray: 0x696969ff, 50 | dimgrey: 0x696969ff, 51 | dodgerblue: 0x1e90ffff, 52 | firebrick: 0xb22222ff, 53 | floralwhite: 0xfffaf0ff, 54 | forestgreen: 0x228b22ff, 55 | fuchsia: 0xff00ffff, 56 | gainsboro: 0xdcdcdcff, 57 | ghostwhite: 0xf8f8ffff, 58 | gold: 0xffd700ff, 59 | goldenrod: 0xdaa520ff, 60 | gray: 0x808080ff, 61 | green: 0x008000ff, 62 | greenyellow: 0xadff2fff, 63 | grey: 0x808080ff, 64 | honeydew: 0xf0fff0ff, 65 | hotpink: 0xff69b4ff, 66 | indianred: 0xcd5c5cff, 67 | indigo: 0x4b0082ff, 68 | ivory: 0xfffff0ff, 69 | khaki: 0xf0e68cff, 70 | lavender: 0xe6e6faff, 71 | lavenderblush: 0xfff0f5ff, 72 | lawngreen: 0x7cfc00ff, 73 | lemonchiffon: 0xfffacdff, 74 | lightblue: 0xadd8e6ff, 75 | lightcoral: 0xf08080ff, 76 | lightcyan: 0xe0ffffff, 77 | lightgoldenrodyellow: 0xfafad2ff, 78 | lightgray: 0xd3d3d3ff, 79 | lightgreen: 0x90ee90ff, 80 | lightgrey: 0xd3d3d3ff, 81 | lightpink: 0xffb6c1ff, 82 | lightsalmon: 0xffa07aff, 83 | lightseagreen: 0x20b2aaff, 84 | lightskyblue: 0x87cefaff, 85 | lightslategray: 0x778899ff, 86 | lightslategrey: 0x778899ff, 87 | lightsteelblue: 0xb0c4deff, 88 | lightyellow: 0xffffe0ff, 89 | lime: 0x00ff00ff, 90 | limegreen: 0x32cd32ff, 91 | linen: 0xfaf0e6ff, 92 | magenta: 0xff00ffff, 93 | maroon: 0x800000ff, 94 | mediumaquamarine: 0x66cdaaff, 95 | mediumblue: 0x0000cdff, 96 | mediumorchid: 0xba55d3ff, 97 | mediumpurple: 0x9370dbff, 98 | mediumseagreen: 0x3cb371ff, 99 | mediumslateblue: 0x7b68eeff, 100 | mediumspringgreen: 0x00fa9aff, 101 | mediumturquoise: 0x48d1ccff, 102 | mediumvioletred: 0xc71585ff, 103 | midnightblue: 0x191970ff, 104 | mintcream: 0xf5fffaff, 105 | mistyrose: 0xffe4e1ff, 106 | moccasin: 0xffe4b5ff, 107 | navajowhite: 0xffdeadff, 108 | navy: 0x000080ff, 109 | oldlace: 0xfdf5e6ff, 110 | olive: 0x808000ff, 111 | olivedrab: 0x6b8e23ff, 112 | orange: 0xffa500ff, 113 | orangered: 0xff4500ff, 114 | orchid: 0xda70d6ff, 115 | palegoldenrod: 0xeee8aaff, 116 | palegreen: 0x98fb98ff, 117 | paleturquoise: 0xafeeeeff, 118 | palevioletred: 0xdb7093ff, 119 | papayawhip: 0xffefd5ff, 120 | peachpuff: 0xffdab9ff, 121 | peru: 0xcd853fff, 122 | pink: 0xffc0cbff, 123 | plum: 0xdda0ddff, 124 | powderblue: 0xb0e0e6ff, 125 | purple: 0x800080ff, 126 | rebeccapurple: 0x663399ff, 127 | red: 0xff0000ff, 128 | rosybrown: 0xbc8f8fff, 129 | royalblue: 0x4169e1ff, 130 | saddlebrown: 0x8b4513ff, 131 | salmon: 0xfa8072ff, 132 | sandybrown: 0xf4a460ff, 133 | seagreen: 0x2e8b57ff, 134 | seashell: 0xfff5eeff, 135 | sienna: 0xa0522dff, 136 | silver: 0xc0c0c0ff, 137 | skyblue: 0x87ceebff, 138 | slateblue: 0x6a5acdff, 139 | slategray: 0x708090ff, 140 | slategrey: 0x708090ff, 141 | snow: 0xfffafaff, 142 | springgreen: 0x00ff7fff, 143 | steelblue: 0x4682b4ff, 144 | tan: 0xd2b48cff, 145 | teal: 0x008080ff, 146 | thistle: 0xd8bfd8ff, 147 | tomato: 0xff6347ff, 148 | turquoise: 0x40e0d0ff, 149 | violet: 0xee82eeff, 150 | wheat: 0xf5deb3ff, 151 | white: 0xffffffff, 152 | whitesmoke: 0xf5f5f5ff, 153 | yellow: 0xffff00ff, 154 | yellowgreen: 0x9acd32ff, 155 | } 156 | -------------------------------------------------------------------------------- /spring/src/context.ts: -------------------------------------------------------------------------------- 1 | import { FluidValue } from "./fluids" 2 | 3 | export type TreeContext = { 4 | /** 5 | * Any animated values found when updating the payload of an `AnimatedObject` 6 | * are also added to this `Set` to be observed by an animated component. 7 | */ 8 | dependencies: Set | null 9 | } 10 | 11 | export const TreeContext: TreeContext = { dependencies: null } 12 | -------------------------------------------------------------------------------- /spring/src/createHost.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | 3 | import type { JSX } from "solid-js"; 4 | import { Animated } from "./animated"; 5 | import { AnimatedObject as AnimatedObjectClass } from "./AnimatedObject"; 6 | import { FluidProps, FluidValue } from "./fluids"; 7 | import { is, Lookup, eachProp, Merge, NonObject } from "./utils"; 8 | import { AnimatableComponent, withAnimated } from "./withAnimated"; 9 | 10 | export interface HostConfig { 11 | /** Provide custom logic for native updates */ 12 | applyAnimatedValues: (node: any, props: Lookup) => boolean | void; 13 | /** Wrap the `style` prop with an animated node */ 14 | createAnimatedStyle: (style: Lookup) => Animated; 15 | /** Intercept props before they're passed to an animated component */ 16 | getComponentProps: (props: Lookup) => typeof props; 17 | } 18 | type Angle = number | string; 19 | type Length = number | string; 20 | 21 | type TransformProps = { 22 | transform?: string; 23 | x?: Length; 24 | y?: Length; 25 | z?: Length; 26 | translate?: Length | readonly [Length, Length]; 27 | translateX?: Length; 28 | translateY?: Length; 29 | translateZ?: Length; 30 | translate3d?: readonly [Length, Length, Length]; 31 | rotate?: Angle; 32 | rotateX?: Angle; 33 | rotateY?: Angle; 34 | rotateZ?: Angle; 35 | rotate3d?: readonly [number, number, number, Angle]; 36 | // Note: "string" is not really supported by "scale", but this lets us 37 | // spread React.CSSProperties into an animated style object. 38 | scale?: number | readonly [number, number] | string; 39 | scaleX?: number; 40 | scaleY?: number; 41 | scaleZ?: number; 42 | scale3d?: readonly [number, number, number]; 43 | skew?: Angle | readonly [Angle, Angle]; 44 | skewX?: Angle; 45 | skewY?: Angle; 46 | matrix?: readonly [number, number, number, number, number, number]; 47 | matrix3d?: readonly [ 48 | number, // a1 49 | number, 50 | number, 51 | number, 52 | number, // a2 53 | number, 54 | number, 55 | number, 56 | number, // a3 57 | number, 58 | number, 59 | number, 60 | number, // a4 61 | number, 62 | number, 63 | number 64 | ]; 65 | }; 66 | 67 | type CSSProperties = JSX.IntrinsicElements["a"]["style"]; 68 | type StyleProps = Merge; 69 | 70 | // A stub type that gets replaced by @react-spring/web and others. 71 | export type WithAnimated = ((Component: any) => any) & 72 | { 73 | // (Component: AnimatableComponent): any; 74 | [K in keyof JSX.IntrinsicElements]: ( 75 | props: AnimatedProps< 76 | Merge 77 | > & 78 | FluidProps<{ 79 | scrollTop?: number; 80 | scrollLeft?: number; 81 | }> 82 | ) => JSX.Element; 83 | }; 84 | /** The props of an `animated()` component */ 85 | export type AnimatedProps = { 86 | [P in keyof Props]: P extends "ref" | "key" 87 | ? Props[P] 88 | : AnimatedProp; 89 | }; 90 | // The animated prop value of a React element 91 | type AnimatedProp = [T, T] extends [infer T, infer DT] 92 | ? [DT] extends [never] 93 | ? never 94 | : DT extends void 95 | ? undefined 96 | : DT extends string | number 97 | ? DT | AnimatedLeaf 98 | : DT extends object 99 | ? [ValidStyleProps
] extends [never] 100 | ? DT extends ReadonlyArray 101 | ? AnimatedStyles
102 | : DT 103 | : AnimatedStyle 104 | : DT | AnimatedLeaf 105 | : never; 106 | 107 | // An animated object of style attributes 108 | type AnimatedStyle = [T, T] extends [infer T, infer DT] 109 | ? DT extends void 110 | ? undefined 111 | : [DT] extends [never] 112 | ? never 113 | : DT extends string | number 114 | ? DT | AnimatedLeaf 115 | : DT extends object 116 | ? AnimatedObject
117 | : DT | AnimatedLeaf 118 | : never; 119 | 120 | type AnimatedObject = 121 | | { [P in keyof T]: AnimatedStyle } 122 | | (T extends ReadonlyArray 123 | ? FluidValue> 124 | : never); 125 | 126 | // An animated array of style objects 127 | type AnimatedStyles> = { 128 | [P in keyof T]: [T[P]] extends [infer DT] 129 | ? DT extends object 130 | ? [ValidStyleProps
] extends [never] 131 | ? DT extends ReadonlyArray 132 | ? AnimatedStyles
133 | : DT 134 | : { [P in keyof DT]: AnimatedProp } 135 | : DT 136 | : never; 137 | }; 138 | // An animated primitive (or an array of them) 139 | type AnimatedLeaf = NonObject extends infer U 140 | ? [U] extends [never] 141 | ? never 142 | : FluidValue 143 | : never; 144 | 145 | type StylePropKeys = keyof StyleProps; 146 | 147 | type ValidStyleProps = { 148 | [P in keyof T & StylePropKeys]: T[P] extends StyleProps[P] ? P : never; 149 | }[keyof T & StylePropKeys]; 150 | 151 | // For storing the animated version on the original component 152 | const cacheKey = Symbol.for("AnimatedComponent"); 153 | 154 | export const createHost = ( 155 | components: AnimatableComponent[] | { [key: string]: AnimatableComponent }, 156 | { 157 | applyAnimatedValues = () => false, 158 | createAnimatedStyle = (style) => new AnimatedObjectClass(style), 159 | getComponentProps = (props) => props, 160 | }: Partial = {} 161 | ) => { 162 | const hostConfig: HostConfig = { 163 | applyAnimatedValues, 164 | createAnimatedStyle, 165 | getComponentProps, 166 | }; 167 | 168 | const animated: any = (Component: any): any => { 169 | if (is.str(Component)) { 170 | Component = 171 | animated[Component] || 172 | (animated[Component] = withAnimated(Component, hostConfig)); 173 | } else { 174 | Component = 175 | Component[cacheKey] || 176 | (Component[cacheKey] = withAnimated(Component, hostConfig)); 177 | } 178 | 179 | return Component; 180 | }; 181 | 182 | eachProp(components, (Component, key) => { 183 | if (is.arr(components)) { 184 | key = Component; 185 | } 186 | animated[key] = animated(Component); 187 | }); 188 | 189 | return { 190 | animated, 191 | } as { animated: WithAnimated }; 192 | }; 193 | -------------------------------------------------------------------------------- /spring/src/createInterpolator.ts: -------------------------------------------------------------------------------- 1 | import * as G from './globals' 2 | import { ExtrapolateType, Animatable, InterpolatorConfig, InterpolatorFactory, InterpolatorFn } from "./Interpolation" 3 | import { EasingFunction, is } from "./utils" 4 | 5 | export const createInterpolator: InterpolatorFactory = ( 6 | range: readonly number[] | InterpolatorFn | InterpolatorConfig, 7 | output?: readonly Animatable[], 8 | extrapolate?: ExtrapolateType 9 | ) => { 10 | if (is.fun(range)) { 11 | return range 12 | } 13 | 14 | if (is.arr(range)) { 15 | return createInterpolator({ 16 | range, 17 | output: output!, 18 | extrapolate, 19 | }) 20 | } 21 | 22 | if (is.str(range.output[0])) { 23 | return G.createStringInterpolator(range as any) as any 24 | } 25 | 26 | const config = range as InterpolatorConfig 27 | const outputRange = config.output 28 | const inputRange = config.range || [0, 1] 29 | 30 | const extrapolateLeft = 31 | config.extrapolateLeft || config.extrapolate || 'extend' 32 | const extrapolateRight = 33 | config.extrapolateRight || config.extrapolate || 'extend' 34 | const easing = config.easing || (t => t) 35 | 36 | return (input: number) => { 37 | const range = findRange(input, inputRange) 38 | return interpolate( 39 | input, 40 | inputRange[range], 41 | inputRange[range + 1], 42 | outputRange[range], 43 | outputRange[range + 1], 44 | easing, 45 | extrapolateLeft, 46 | extrapolateRight, 47 | config.map 48 | ) 49 | } 50 | } 51 | 52 | function interpolate( 53 | input: number, 54 | inputMin: number, 55 | inputMax: number, 56 | outputMin: number, 57 | outputMax: number, 58 | easing: EasingFunction, 59 | extrapolateLeft: ExtrapolateType, 60 | extrapolateRight: ExtrapolateType, 61 | map?: (x: number) => number 62 | ) { 63 | let result = map ? map(input) : input 64 | // Extrapolate 65 | if (result < inputMin) { 66 | if (extrapolateLeft === 'identity') return result 67 | else if (extrapolateLeft === 'clamp') result = inputMin 68 | } 69 | if (result > inputMax) { 70 | if (extrapolateRight === 'identity') return result 71 | else if (extrapolateRight === 'clamp') result = inputMax 72 | } 73 | if (outputMin === outputMax) return outputMin 74 | if (inputMin === inputMax) return input <= inputMin ? outputMin : outputMax 75 | // Input Range 76 | if (inputMin === -Infinity) result = -result 77 | else if (inputMax === Infinity) result = result - inputMin 78 | else result = (result - inputMin) / (inputMax - inputMin) 79 | // Easing 80 | result = easing(result) 81 | // Output Range 82 | if (outputMin === -Infinity) result = -result 83 | else if (outputMax === Infinity) result = result + outputMin 84 | else result = result * (outputMax - outputMin) + outputMin 85 | return result 86 | } 87 | 88 | function findRange(input: number, inputRange: readonly number[]) { 89 | for (var i = 1; i < inputRange.length - 1; ++i) 90 | if (inputRange[i] >= input) break 91 | return i - 1 92 | } 93 | -------------------------------------------------------------------------------- /spring/src/fluids.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) Alec Larson 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | /** 24 | * Extend this class for automatic TypeScript support when passing this 25 | * value to `fluids`-compatible libraries. 26 | */ 27 | const $get = Symbol.for("FluidValue.get"); 28 | const $observers = Symbol.for("FluidValue.observers"); 29 | 30 | /** Returns true if `arg` can be observed. */ 31 | export const hasFluidValue = (arg: any): arg is FluidValue => Boolean(arg && arg[$get]) 32 | 33 | /** An event sent to `FluidObserver` objects. */ 34 | export interface FluidEvent { 35 | type: string; 36 | parent: FluidValue; 37 | } 38 | 39 | /** Add the `FluidValue` type to every property. */ 40 | export type FluidProps = T extends object 41 | ? { [P in keyof T]: T[P] | FluidValue> } 42 | : unknown; 43 | 44 | const setHidden = (target: any, key: any, value: any) => 45 | Object.defineProperty(target, key, { 46 | value, 47 | writable: true, 48 | configurable: true, 49 | }); 50 | 51 | /** Define the getter called by `getFluidValue`. */ 52 | const setFluidGetter = (target: object, get: () => any) => 53 | setHidden(target, $get, get); 54 | 55 | /** An observer of `FluidValue` objects. */ 56 | export type FluidObserver = 57 | | { eventObserved(event: E): void } 58 | | { (event: E): void }; 59 | 60 | export abstract class FluidValue = any> { 61 | // @ts-ignore 62 | private [$get]: () => T; 63 | // @ts-ignore 64 | private [$observers]?: Set>; 65 | 66 | constructor(get?: () => T) { 67 | if (!get && !(get = this.get)) { 68 | throw Error("Unknown getter"); 69 | } 70 | setFluidGetter(this, get); 71 | } 72 | 73 | /** Get the current value. */ 74 | protected get?(): T; 75 | /** Called after an observer is added. */ 76 | protected observerAdded?(count: number, observer: FluidObserver): void; 77 | /** Called after an observer is removed. */ 78 | protected observerRemoved?(count: number, observer: FluidObserver): void; 79 | } 80 | 81 | /** 82 | * Get the current value. 83 | * If `arg` is not observable, `arg` is returned. 84 | */ 85 | export const getFluidValue: GetFluidValue = (arg: any) => 86 | arg && arg[$get] ? arg[$get]() : arg; 87 | 88 | type GetFluidValue = { 89 | (target: T | FluidValue): Exclude | U; 90 | }; 91 | 92 | /** Send an event to an observer. */ 93 | export function callFluidObserver( 94 | observer: FluidObserver, 95 | event: E 96 | ): void; 97 | 98 | export function callFluidObserver(observer: any, event: FluidEvent) { 99 | if (observer.eventObserved) { 100 | observer.eventObserved(event); 101 | } else { 102 | observer(event); 103 | } 104 | } 105 | 106 | /** Send an event to all observers. */ 107 | export function callFluidObservers( 108 | target: FluidValue, 109 | event: E 110 | ): void; 111 | 112 | export function callFluidObservers(target: object, event: FluidEvent): void; 113 | 114 | export function callFluidObservers(target: any, event: FluidEvent) { 115 | let observers: Set = target[$observers]; 116 | if (observers) { 117 | observers.forEach((observer) => { 118 | callFluidObserver(observer, event); 119 | }); 120 | } 121 | } 122 | type GetFluidObservers = { 123 | (target: FluidValue): ReadonlySet< 124 | FluidObserver 125 | > | null 126 | (target: object): ReadonlySet | null 127 | } 128 | 129 | 130 | /** Observe a `fluids`-compatible object. */ 131 | export function addFluidObserver( 132 | target: FluidValue, 133 | observer: FluidObserver 134 | ): typeof observer 135 | 136 | export function addFluidObserver( 137 | target: object, 138 | observer: FluidObserver 139 | ): typeof observer 140 | 141 | export function addFluidObserver(target: any, observer: FluidObserver) { 142 | if (target[$get]) { 143 | let observers: Set = target[$observers] 144 | if (!observers) { 145 | setHidden(target, $observers, (observers = new Set())) 146 | } 147 | if (!observers.has(observer)) { 148 | observers.add(observer) 149 | if (target.observerAdded) { 150 | target.observerAdded(observers.size, observer) 151 | } 152 | } 153 | } 154 | return observer 155 | } 156 | 157 | /** Stop observing a `fluids`-compatible object. */ 158 | export function removeFluidObserver( 159 | target: FluidValue, 160 | observer: FluidObserver 161 | ): void 162 | 163 | export function removeFluidObserver( 164 | target: object, 165 | observer: FluidObserver 166 | ): void 167 | 168 | export function removeFluidObserver(target: any, observer: FluidObserver) { 169 | let observers: Set = target[$observers] 170 | if (observers && observers.has(observer)) { 171 | const count = observers.size - 1 172 | if (count) { 173 | observers.delete(observer) 174 | } else { 175 | target[$observers] = null 176 | } 177 | if (target.observerRemoved) { 178 | target.observerRemoved(count, observer) 179 | } 180 | } 181 | } 182 | 183 | 184 | /** Get the current observer set. Never mutate it directly! */ 185 | export const getFluidObservers: GetFluidObservers = (target: any) => 186 | target[$observers] || null 187 | 188 | -------------------------------------------------------------------------------- /spring/src/globals.ts: -------------------------------------------------------------------------------- 1 | import { createInterpolator } from "./createInterpolator"; 2 | import { FluidValue, getFluidValue } from "./fluids"; 3 | import type { OpaqueAnimation } from "./FrameLoop"; 4 | import { Interpolation, InterpolatorArgs, InterpolatorConfig } from "./Interpolation"; 5 | import { normalizeColor } from "./normalizeColor"; 6 | import { raf } from "./rafz"; 7 | import { cssVariableRegex, isSSR, noop, OneOrMore, Rafz } from "./utils"; 8 | import {colors as _colors} from './colors' 9 | 10 | // Covers color names (transparent, blue, etc.) 11 | let namedColorRegex: RegExp; 12 | // Covers rgb, rgba, hsl, hsla 13 | // Taken from https://gist.github.com/olmokramer/82ccce673f86db7cda5e 14 | export const colorRegex = 15 | /(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))/gi; 16 | 17 | export const numberRegex = /[+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; 18 | 19 | // Gets numbers with units when specified 20 | export const unitRegex = new RegExp(`(${numberRegex.source})(%|[a-z]+)`, "i"); 21 | 22 | // get values of rgba string 23 | export const rgbaRegex = 24 | /rgba\(([0-9\.-]+), ([0-9\.-]+), ([0-9\.-]+), ([0-9\.-]+)\)/gi; 25 | 26 | // rgba requires that the r,g,b are integers.... so we want to round them, 27 | // but we *dont* want to round the opacity (4th column). 28 | const rgbaRound = (_: any, p1: number, p2: number, p3: number, p4: number) => 29 | `rgba(${Math.round(p1)}, ${Math.round(p2)}, ${Math.round(p3)}, ${p4})`; 30 | 31 | // 32 | // Required 33 | // 34 | 35 | /** 36 | * Supports string shapes by extracting numbers so new values can be computed, 37 | * and recombines those values into new strings of the same shape. Supports 38 | * things like: 39 | * 40 | * "rgba(123, 42, 99, 0.36)" // colors 41 | * "-45deg" // values with units 42 | * "0 2px 2px 0px rgba(0, 0, 0, 0.12)" // CSS box-shadows 43 | * "rotate(0deg) translate(2px, 3px)" // CSS transforms 44 | */ 45 | export let createStringInterpolator = ( 46 | config: InterpolatorConfig 47 | ) => { 48 | if (!namedColorRegex) 49 | namedColorRegex = colors 50 | ? // match color names, ignore partial matches 51 | new RegExp(`(${Object.keys(colors).join("|")})(?!\\w)`, "g") 52 | : // never match 53 | /^\b$/; 54 | 55 | // Convert colors to rgba(...) 56 | const output = config.output.map((value) => { 57 | return getFluidValue(value) 58 | .replace(cssVariableRegex, variableToRgba) 59 | .replace(colorRegex, colorToRgba) 60 | .replace(namedColorRegex, colorToRgba); 61 | }); 62 | 63 | // Convert ["1px 2px", "0px 0px"] into [[1, 2], [0, 0]] 64 | const keyframes = output.map((value) => 65 | value.match(numberRegex)!.map(Number) 66 | ); 67 | 68 | // Convert ["1px 2px", "0px 0px"] into [[1, 0], [2, 0]] 69 | const outputRanges = keyframes[0].map((_, i) => 70 | keyframes.map((values) => { 71 | if (!(i in values)) { 72 | throw Error('The arity of each "output" value must be equal'); 73 | } 74 | return values[i]; 75 | }) 76 | ); 77 | 78 | // Create an interpolator for each animated number 79 | const interpolators = outputRanges.map((output) => 80 | createInterpolator({ ...config, output }) 81 | ); 82 | 83 | // Use the first `output` as a template for each call 84 | return (input: number) => { 85 | // Convert numbers to units if available (allows for ["0", "100%"]) 86 | const missingUnit = 87 | !unitRegex.test(output[0]) && 88 | output.find((value) => unitRegex.test(value))?.replace(numberRegex, ""); 89 | 90 | let i = 0; 91 | return output[0] 92 | .replace( 93 | numberRegex, 94 | () => `${interpolators[i++](input)}${missingUnit || ""}` 95 | ) 96 | .replace(rgbaRegex, rgbaRound); 97 | }; 98 | }; 99 | 100 | // 101 | // Optional 102 | // 103 | 104 | export let to = ( 105 | source: OneOrMore, 106 | args: InterpolatorArgs 107 | ) => { 108 | return new Interpolation(source, args) 109 | } 110 | 111 | export let colors = _colors as { [key: string]: number } | null; 112 | 113 | export let skipAnimation = false as boolean; 114 | 115 | export let willAdvance: (animation: OpaqueAnimation) => void = noop; 116 | 117 | // 118 | // Configuration 119 | // 120 | 121 | export interface AnimatedGlobals { 122 | /** Returns a new `Interpolation` object */ 123 | to?: typeof to; 124 | /** Used to measure frame length. Read more [here](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now) */ 125 | now?: typeof raf.now; 126 | /** Provide custom color names for interpolation */ 127 | colors?: typeof colors; 128 | /** Make all animations instant and skip the frameloop entirely */ 129 | skipAnimation?: typeof skipAnimation; 130 | /** Provide custom logic for string interpolation */ 131 | createStringInterpolator?: typeof createStringInterpolator; 132 | /** Schedule a function to run on the next frame */ 133 | requestAnimationFrame?: (cb: () => void) => void; 134 | /** Event props are called with `batchedUpdates` to reduce extraneous renders */ 135 | batchedUpdates?: typeof raf.batchedUpdates; 136 | /** @internal Exposed for testing purposes */ 137 | willAdvance?: typeof willAdvance; 138 | /** sets the global frameLoop setting for the global raf instance */ 139 | frameLoop?: Rafz["frameLoop"]; 140 | } 141 | 142 | export const assign = (globals: AnimatedGlobals) => { 143 | if (globals.to) to = globals.to; 144 | if (globals.now) raf.now = globals.now; 145 | if (globals.colors !== undefined) colors = globals.colors; 146 | if (globals.skipAnimation != null) skipAnimation = globals.skipAnimation; 147 | if (globals.createStringInterpolator) 148 | createStringInterpolator = globals.createStringInterpolator; 149 | if (globals.requestAnimationFrame) raf.use(globals.requestAnimationFrame); 150 | if (globals.batchedUpdates) raf.batchedUpdates = globals.batchedUpdates; 151 | if (globals.willAdvance) willAdvance = globals.willAdvance; 152 | if (globals.frameLoop) raf.frameLoop = globals.frameLoop; 153 | }; 154 | 155 | export const variableToRgba = (input: string): string => { 156 | const [token, fallback] = parseCSSVariable(input); 157 | 158 | if (!token || isSSR()) { 159 | return input; 160 | } 161 | 162 | const value = window 163 | .getComputedStyle(document.documentElement) 164 | .getPropertyValue(token); 165 | 166 | if (value) { 167 | /** 168 | * We have a valid variable returned 169 | * trim and return 170 | */ 171 | return value.trim(); 172 | } else if (fallback && fallback.startsWith("--")) { 173 | /** 174 | * fallback is something like --my-variable 175 | * so we try get property value 176 | */ 177 | const value = window 178 | .getComputedStyle(document.documentElement) 179 | .getPropertyValue(fallback); 180 | 181 | /** 182 | * if it exists, return else nothing was found so just return the input 183 | */ 184 | if (value) { 185 | return value; 186 | } else { 187 | return input; 188 | } 189 | } else if (fallback && cssVariableRegex.test(fallback)) { 190 | /** 191 | * We have a fallback and it's another CSS variable 192 | */ 193 | return variableToRgba(fallback); 194 | } else if (fallback) { 195 | /** 196 | * We have a fallback and it's not a CSS variable 197 | */ 198 | return fallback; 199 | } 200 | 201 | /** 202 | * Nothing worked so just return the input 203 | * like our other FluidValue replace functions do 204 | */ 205 | return input; 206 | }; 207 | 208 | const parseCSSVariable = (current: string) => { 209 | const match = cssVariableRegex.exec(current); 210 | if (!match) return [,]; 211 | 212 | const [, token, fallback] = match; 213 | return [token, fallback]; 214 | }; 215 | 216 | export function colorToRgba(input: string) { 217 | let int32Color = normalizeColor(input); 218 | if (int32Color === null) return input; 219 | int32Color = int32Color || 0; 220 | let r = (int32Color & 0xff000000) >>> 24; 221 | let g = (int32Color & 0x00ff0000) >>> 16; 222 | let b = (int32Color & 0x0000ff00) >>> 8; 223 | let a = (int32Color & 0x000000ff) / 255; 224 | return `rgba(${r}, ${g}, ${b}, ${a})`; 225 | } 226 | -------------------------------------------------------------------------------- /spring/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./solid/createSprings"; 2 | export * from "./solid/createSpring"; 3 | export { config } from "./utils"; 4 | 5 | import { AnimatedStyle } from "./AnimatedStyle"; 6 | import { applyAnimatedValues } from "./applyAnimatedValues"; 7 | import { createHost, WithAnimated } from "./createHost"; 8 | import { Interpolation } from "./Interpolation"; 9 | import { primitives } from "./primitives"; 10 | 11 | const host = createHost(primitives, { 12 | applyAnimatedValues, 13 | createAnimatedStyle: (style) => new AnimatedStyle(style), 14 | getComponentProps: ({ scrollTop, scrollLeft, ...props }: any) => props, 15 | }); 16 | 17 | export const animated = host.animated as WithAnimated; 18 | export const to = (source: any, ...args: [any]) => 19 | new Interpolation(source, args); 20 | export { animated as a }; 21 | -------------------------------------------------------------------------------- /spring/src/normalizeColor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/react-community/normalize-css-color 3 | 4 | BSD 3-Clause License 5 | 6 | Copyright (c) 2016, React Community 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the copyright holder nor the names of its 20 | contributors may be used to endorse or promote products derived from 21 | this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | import * as G from './globals' 36 | 37 | // const INTEGER = '[-+]?\\d+'; 38 | const NUMBER = '[-+]?\\d*\\.?\\d+' 39 | const PERCENTAGE = NUMBER + '%' 40 | 41 | function call(...parts: string[]) { 42 | return '\\(\\s*(' + parts.join(')\\s*,\\s*(') + ')\\s*\\)' 43 | } 44 | 45 | export const rgb = new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)) 46 | export const rgba = new RegExp('rgba' + call(NUMBER, NUMBER, NUMBER, NUMBER)) 47 | export const hsl = new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)) 48 | export const hsla = new RegExp( 49 | 'hsla' + call(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) 50 | ) 51 | export const hex3 = /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/ 52 | export const hex4 = /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/ 53 | export const hex6 = /^#([0-9a-fA-F]{6})$/ 54 | export const hex8 = /^#([0-9a-fA-F]{8})$/ 55 | 56 | export function normalizeColor(color: number | string) { 57 | let match 58 | 59 | if (typeof color === 'number') { 60 | return color >>> 0 === color && color >= 0 && color <= 0xffffffff 61 | ? color 62 | : null 63 | } 64 | 65 | // Ordered based on occurrences on Facebook codebase 66 | if ((match = hex6.exec(color))) 67 | return parseInt(match[1] + 'ff', 16) >>> 0 68 | 69 | if (G.colors && G.colors[color] !== undefined) { 70 | return G.colors[color] 71 | } 72 | 73 | if ((match = rgb.exec(color))) { 74 | return ( 75 | ((parse255(match[1]) << 24) | // r 76 | (parse255(match[2]) << 16) | // g 77 | (parse255(match[3]) << 8) | // b 78 | 0x000000ff) >>> // a 79 | 0 80 | ) 81 | } 82 | 83 | if ((match = rgba.exec(color))) { 84 | return ( 85 | ((parse255(match[1]) << 24) | // r 86 | (parse255(match[2]) << 16) | // g 87 | (parse255(match[3]) << 8) | // b 88 | parse1(match[4])) >>> // a 89 | 0 90 | ) 91 | } 92 | 93 | if ((match = hex3.exec(color))) { 94 | return ( 95 | parseInt( 96 | match[1] + 97 | match[1] + // r 98 | match[2] + 99 | match[2] + // g 100 | match[3] + 101 | match[3] + // b 102 | 'ff', // a 103 | 16 104 | ) >>> 0 105 | ) 106 | } 107 | 108 | // https://drafts.csswg.org/css-color-4/#hex-notation 109 | if ((match = hex8.exec(color))) return parseInt(match[1], 16) >>> 0 110 | 111 | if ((match = hex4.exec(color))) { 112 | return ( 113 | parseInt( 114 | match[1] + 115 | match[1] + // r 116 | match[2] + 117 | match[2] + // g 118 | match[3] + 119 | match[3] + // b 120 | match[4] + 121 | match[4], // a 122 | 16 123 | ) >>> 0 124 | ) 125 | } 126 | 127 | if ((match = hsl.exec(color))) { 128 | return ( 129 | (hslToRgb( 130 | parse360(match[1]), // h 131 | parsePercentage(match[2]), // s 132 | parsePercentage(match[3]) // l 133 | ) | 134 | 0x000000ff) >>> // a 135 | 0 136 | ) 137 | } 138 | 139 | if ((match = hsla.exec(color))) { 140 | return ( 141 | (hslToRgb( 142 | parse360(match[1]), // h 143 | parsePercentage(match[2]), // s 144 | parsePercentage(match[3]) // l 145 | ) | 146 | parse1(match[4])) >>> // a 147 | 0 148 | ) 149 | } 150 | return null 151 | } 152 | 153 | function hue2rgb(p: number, q: number, t: number) { 154 | if (t < 0) t += 1 155 | if (t > 1) t -= 1 156 | if (t < 1 / 6) return p + (q - p) * 6 * t 157 | if (t < 1 / 2) return q 158 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 159 | return p 160 | } 161 | 162 | function hslToRgb(h: number, s: number, l: number) { 163 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s 164 | const p = 2 * l - q 165 | const r = hue2rgb(p, q, h + 1 / 3) 166 | const g = hue2rgb(p, q, h) 167 | const b = hue2rgb(p, q, h - 1 / 3) 168 | return ( 169 | (Math.round(r * 255) << 24) | 170 | (Math.round(g * 255) << 16) | 171 | (Math.round(b * 255) << 8) 172 | ) 173 | } 174 | 175 | function parse255(str: string) { 176 | const int = parseInt(str, 10) 177 | if (int < 0) return 0 178 | if (int > 255) return 255 179 | return int 180 | } 181 | 182 | function parse360(str: string) { 183 | const int = parseFloat(str) 184 | return (((int % 360) + 360) % 360) / 360 185 | } 186 | 187 | function parse1(str: string) { 188 | const num = parseFloat(str) 189 | if (num < 0) return 0 190 | if (num > 1) return 255 191 | return Math.round(num * 255) 192 | } 193 | 194 | function parsePercentage(str: string) { 195 | // parseFloat conveniently ignores the final % 196 | const int = parseFloat(str) 197 | if (int < 0) return 0 198 | if (int > 100) return 1 199 | return int / 100 200 | } 201 | -------------------------------------------------------------------------------- /spring/src/primitives.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { JSX } from "solid-js/types/jsx"; 3 | export type Primitives = keyof JSX.IntrinsicElements; 4 | 5 | export const primitives: Primitives[] = [ 6 | "a", 7 | "abbr", 8 | "address", 9 | "area", 10 | "article", 11 | "aside", 12 | "audio", 13 | "b", 14 | "base", 15 | "bdi", 16 | "bdo", 17 | "big", 18 | "blockquote", 19 | "body", 20 | "br", 21 | "button", 22 | "canvas", 23 | "caption", 24 | "cite", 25 | "code", 26 | "col", 27 | "colgroup", 28 | "data", 29 | "datalist", 30 | "dd", 31 | "del", 32 | "details", 33 | "dfn", 34 | "dialog", 35 | "div", 36 | "dl", 37 | "dt", 38 | "em", 39 | "embed", 40 | "fieldset", 41 | "figcaption", 42 | "figure", 43 | "footer", 44 | "form", 45 | "h1", 46 | "h2", 47 | "h3", 48 | "h4", 49 | "h5", 50 | "h6", 51 | "head", 52 | "header", 53 | "hgroup", 54 | "hr", 55 | "html", 56 | "i", 57 | "iframe", 58 | "img", 59 | "input", 60 | "ins", 61 | "kbd", 62 | "keygen", 63 | "label", 64 | "legend", 65 | "li", 66 | "link", 67 | "main", 68 | "map", 69 | "mark", 70 | "menu", 71 | "menuitem", 72 | "meta", 73 | "meter", 74 | "nav", 75 | "noscript", 76 | "object", 77 | "ol", 78 | "optgroup", 79 | "option", 80 | "output", 81 | "p", 82 | "param", 83 | "picture", 84 | "pre", 85 | "progress", 86 | "q", 87 | "rp", 88 | "rt", 89 | "ruby", 90 | "s", 91 | "samp", 92 | "script", 93 | "section", 94 | "select", 95 | "small", 96 | "source", 97 | "span", 98 | "strong", 99 | "style", 100 | "sub", 101 | "summary", 102 | "sup", 103 | "table", 104 | "tbody", 105 | "td", 106 | "textarea", 107 | "tfoot", 108 | "th", 109 | "thead", 110 | "time", 111 | "title", 112 | "tr", 113 | "track", 114 | "u", 115 | "ul", 116 | "var", 117 | "video", 118 | "wbr", 119 | // SVG 120 | "circle", 121 | "clipPath", 122 | "defs", 123 | "ellipse", 124 | "foreignObject", 125 | "g", 126 | "image", 127 | "line", 128 | "linearGradient", 129 | "mask", 130 | "path", 131 | "pattern", 132 | "polygon", 133 | "polyline", 134 | "radialGradient", 135 | "rect", 136 | "stop", 137 | "svg", 138 | "text", 139 | "tspan", 140 | ]; 141 | -------------------------------------------------------------------------------- /spring/src/rafz.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FrameFn, 3 | FrameUpdateFn, 4 | NativeRaf, 5 | Rafz, 6 | Timeout, 7 | Throttled, 8 | } from './utils' 9 | 10 | export type { FrameFn, FrameUpdateFn, Timeout, Throttled, Rafz } 11 | 12 | let updateQueue = makeQueue() 13 | 14 | /** 15 | * Schedule an update for next frame. 16 | * Your function can return `true` to repeat next frame. 17 | */ 18 | export const raf: Rafz = fn => schedule(fn, updateQueue) 19 | 20 | let writeQueue = makeQueue() 21 | raf.write = fn => schedule(fn, writeQueue) 22 | 23 | let onStartQueue = makeQueue() 24 | raf.onStart = fn => schedule(fn, onStartQueue) 25 | 26 | let onFrameQueue = makeQueue() 27 | raf.onFrame = fn => schedule(fn, onFrameQueue) 28 | 29 | let onFinishQueue = makeQueue() 30 | raf.onFinish = fn => schedule(fn, onFinishQueue) 31 | 32 | let timeouts: Timeout[] = [] 33 | raf.setTimeout = (handler, ms) => { 34 | let time = raf.now() + ms 35 | let cancel = () => { 36 | let i = timeouts.findIndex(t => t.cancel == cancel) 37 | if (~i) timeouts.splice(i, 1) 38 | pendingCount -= ~i ? 1 : 0 39 | } 40 | 41 | let timeout: Timeout = { time, handler, cancel } 42 | timeouts.splice(findTimeout(time), 0, timeout) 43 | pendingCount += 1 44 | 45 | start() 46 | return timeout 47 | } 48 | 49 | /** Find the index where the given time is not greater. */ 50 | let findTimeout = (time: number) => 51 | ~(~timeouts.findIndex(t => t.time > time) || ~timeouts.length) 52 | 53 | raf.cancel = fn => { 54 | onStartQueue.delete(fn) 55 | onFrameQueue.delete(fn) 56 | updateQueue.delete(fn) 57 | writeQueue.delete(fn) 58 | onFinishQueue.delete(fn) 59 | } 60 | 61 | raf.sync = fn => { 62 | sync = true 63 | raf.batchedUpdates(fn) 64 | sync = false 65 | } 66 | 67 | raf.throttle = fn => { 68 | let lastArgs: any 69 | function queuedFn() { 70 | try { 71 | fn(...lastArgs) 72 | } finally { 73 | lastArgs = null 74 | } 75 | } 76 | function throttled(...args: any) { 77 | lastArgs = args 78 | raf.onStart(queuedFn) 79 | } 80 | throttled.handler = fn 81 | throttled.cancel = () => { 82 | onStartQueue.delete(queuedFn) 83 | lastArgs = null 84 | } 85 | return throttled as any 86 | } 87 | 88 | let nativeRaf = 89 | typeof window != 'undefined' 90 | ? (window.requestAnimationFrame as NativeRaf) 91 | : () => {} 92 | 93 | raf.use = impl => (nativeRaf = impl) 94 | raf.now = typeof performance != 'undefined' ? () => performance.now() : Date.now 95 | raf.batchedUpdates = fn => fn() 96 | raf.catch = console.error 97 | 98 | raf.frameLoop = 'always' 99 | 100 | raf.advance = () => { 101 | if (raf.frameLoop !== 'demand') { 102 | console.warn( 103 | 'Cannot call the manual advancement of rafz whilst frameLoop is not set as demand' 104 | ) 105 | } else { 106 | update() 107 | } 108 | } 109 | 110 | /** The most recent timestamp. */ 111 | let ts = -1 112 | 113 | /** The number of pending tasks */ 114 | let pendingCount = 0 115 | 116 | /** When true, scheduling is disabled. */ 117 | let sync = false 118 | 119 | function schedule(fn: T, queue: Queue) { 120 | if (sync) { 121 | queue.delete(fn) 122 | fn(0) 123 | } else { 124 | queue.add(fn) 125 | start() 126 | } 127 | } 128 | 129 | function start() { 130 | if (ts < 0) { 131 | ts = 0 132 | if (raf.frameLoop !== 'demand') { 133 | nativeRaf(loop) 134 | } 135 | } 136 | } 137 | 138 | function stop() { 139 | ts = -1 140 | } 141 | 142 | function loop() { 143 | if (~ts) { 144 | nativeRaf(loop) 145 | raf.batchedUpdates(update) 146 | } 147 | } 148 | 149 | function update() { 150 | let prevTs = ts 151 | ts = raf.now() 152 | 153 | // Flush timeouts whose time is up. 154 | let count = findTimeout(ts) 155 | if (count) { 156 | eachSafely(timeouts.splice(0, count), t => t.handler()) 157 | pendingCount -= count 158 | } 159 | 160 | onStartQueue.flush() 161 | updateQueue.flush(prevTs ? Math.min(64, ts - prevTs) : 16.667) 162 | onFrameQueue.flush() 163 | writeQueue.flush() 164 | onFinishQueue.flush() 165 | 166 | if (!pendingCount) { 167 | stop() 168 | } 169 | } 170 | 171 | interface Queue { 172 | add: (fn: T) => void 173 | delete: (fn: T) => boolean 174 | flush: (arg?: any) => void 175 | } 176 | 177 | function makeQueue(): Queue { 178 | let next = new Set() 179 | let current = next 180 | return { 181 | add(fn) { 182 | pendingCount += current == next && !next.has(fn) ? 1 : 0 183 | next.add(fn) 184 | }, 185 | delete(fn) { 186 | pendingCount -= current == next && next.has(fn) ? 1 : 0 187 | return next.delete(fn) 188 | }, 189 | flush(arg) { 190 | if (current.size) { 191 | next = new Set() 192 | pendingCount -= current.size 193 | eachSafely(current, fn => fn(arg) && next.add(fn)) 194 | pendingCount += next.size 195 | current = next 196 | } 197 | }, 198 | } 199 | } 200 | 201 | interface Eachable { 202 | forEach(cb: (value: T) => void): void 203 | } 204 | 205 | function eachSafely(values: Eachable, each: (value: T) => void) { 206 | values.forEach(value => { 207 | try { 208 | each(value) 209 | } catch (e) { 210 | raf.catch(e as Error) 211 | } 212 | }) 213 | } 214 | 215 | /** Tree-shakable state for testing purposes */ 216 | export const __raf = { 217 | /** The number of pending tasks */ 218 | count(): number { 219 | return pendingCount 220 | }, 221 | /** Whether there's a raf update loop running */ 222 | isRunning(): boolean { 223 | return ts >= 0 224 | }, 225 | /** Clear internal state. Never call from update loop! */ 226 | clear() { 227 | ts = -1 228 | timeouts = [] 229 | onStartQueue = makeQueue() 230 | updateQueue = makeQueue() 231 | onFrameQueue = makeQueue() 232 | writeQueue = makeQueue() 233 | onFinishQueue = makeQueue() 234 | pendingCount = 0 235 | }, 236 | } 237 | -------------------------------------------------------------------------------- /spring/src/runAsync.ts: -------------------------------------------------------------------------------- 1 | import { getCancelledResult, getFinishedResult } from "./AnimationResult"; 2 | import { Controller } from "./Controller"; 3 | import * as G from "./globals"; 4 | import { raf } from "./rafz"; 5 | import { SpringValue } from "./SpringValue"; 6 | import { 7 | AsyncResult, 8 | is, 9 | flush, 10 | getDefaultProps, 11 | SpringChain, 12 | SpringToFn, 13 | Timeout, 14 | eachProp, 15 | AnimationResult, 16 | Falsy, 17 | AnimationTarget, 18 | ControllerUpdate, 19 | SpringUpdate, 20 | Lookup, 21 | Readable, 22 | } from "./utils"; 23 | 24 | type AsyncTo = SpringChain | SpringToFn; 25 | 26 | /** @internal */ 27 | export type InferState = T extends Controller 28 | ? State 29 | : T extends SpringValue 30 | ? U 31 | : unknown; 32 | 33 | /** @internal */ 34 | export type InferProps = T extends Controller 35 | ? ControllerUpdate 36 | : T extends SpringValue 37 | ? SpringUpdate 38 | : Lookup; 39 | 40 | /** @internal */ 41 | export type RunAsyncProps = InferProps & { 42 | callId: number; 43 | parentId?: number; 44 | cancel: boolean; 45 | to?: any; 46 | }; 47 | 48 | /** @internal */ 49 | export interface RunAsyncState { 50 | paused: boolean; 51 | pauseQueue: Set<() => void>; 52 | resumeQueue: Set<() => void>; 53 | timeouts: Set; 54 | delayed?: boolean; 55 | asyncId?: number; 56 | asyncTo?: AsyncTo>; 57 | promise?: AsyncResult; 58 | cancelId?: number; 59 | } 60 | 61 | /** 62 | * Start an async chain or an async script. 63 | * 64 | * Always call `runAsync` in the action callback of a `scheduleProps` call. 65 | * 66 | * The `T` parameter can be a set of animated values (as an object type) 67 | * or a primitive type for a single animated value. 68 | */ 69 | export function runAsync( 70 | to: AsyncTo>, 71 | props: RunAsyncProps, 72 | state: RunAsyncState, 73 | target: T 74 | ): AsyncResult { 75 | const { callId, parentId, onRest } = props; 76 | const { asyncTo: prevTo, promise: prevPromise } = state; 77 | 78 | if (!parentId && to === prevTo && !props.reset) { 79 | return prevPromise!; 80 | } 81 | 82 | return (state.promise = (async () => { 83 | state.asyncId = callId; 84 | state.asyncTo = to; 85 | 86 | // The default props of any `animate` calls. 87 | const defaultProps = getDefaultProps>(props, (value, key) => 88 | // The `onRest` prop is only called when the `runAsync` promise is resolved. 89 | key === "onRest" ? undefined : value 90 | ); 91 | 92 | let preventBail!: () => void; 93 | let bail: (error: any) => void; 94 | 95 | // This promise is rejected when the animation is interrupted. 96 | const bailPromise = new Promise( 97 | (resolve, reject) => ((preventBail = resolve), (bail = reject)) 98 | ); 99 | 100 | const bailIfEnded = (bailSignal: BailSignal) => { 101 | const bailResult = 102 | // The `cancel` prop or `stop` method was used. 103 | (callId <= (state.cancelId || 0) && getCancelledResult(target)) || 104 | // The async `to` prop was replaced. 105 | (callId !== state.asyncId && getFinishedResult(target, false)); 106 | 107 | if (bailResult) { 108 | bailSignal.result = bailResult; 109 | 110 | // Reject the `bailPromise` to ensure the `runAsync` promise 111 | // is not relying on the caller to rethrow the error for us. 112 | bail(bailSignal); 113 | throw bailSignal; 114 | } 115 | }; 116 | 117 | const animate: any = (arg1: any, arg2?: any) => { 118 | // Create the bail signal outside the returned promise, 119 | // so the generated stack trace is relevant. 120 | const bailSignal = new BailSignal(); 121 | const skipAnimationSignal = new SkipAniamtionSignal(); 122 | 123 | return (async () => { 124 | if (G.skipAnimation) { 125 | /** 126 | * We need to stop animations if `skipAnimation` 127 | * is set in the Globals 128 | * 129 | */ 130 | stopAsync(state); 131 | 132 | // create the rejection error that's handled gracefully 133 | skipAnimationSignal.result = getFinishedResult(target, false); 134 | bail(skipAnimationSignal); 135 | throw skipAnimationSignal; 136 | } 137 | 138 | bailIfEnded(bailSignal); 139 | 140 | const props: any = is.obj(arg1) ? { ...arg1 } : { ...arg2, to: arg1 }; 141 | props.parentId = callId; 142 | 143 | eachProp(defaultProps, (value, key) => { 144 | if (is.und(props[key])) { 145 | props[key] = value; 146 | } 147 | }); 148 | 149 | const result = await target.start(props); 150 | bailIfEnded(bailSignal); 151 | 152 | if (state.paused) { 153 | await new Promise((resume) => { 154 | state.resumeQueue.add(resume); 155 | }); 156 | } 157 | 158 | return result; 159 | })(); 160 | }; 161 | 162 | let result!: AnimationResult; 163 | 164 | if (G.skipAnimation) { 165 | /** 166 | * We need to stop animations if `skipAnimation` 167 | * is set in the Globals 168 | */ 169 | stopAsync(state); 170 | return getFinishedResult(target, false); 171 | } 172 | 173 | try { 174 | let animating!: Promise; 175 | 176 | // Async sequence 177 | if (is.arr(to)) { 178 | animating = (async (queue: any[]) => { 179 | for (const props of queue) { 180 | await animate(props); 181 | } 182 | })(to); 183 | } 184 | 185 | // Async script 186 | else { 187 | animating = Promise.resolve(to(animate, target.stop.bind(target))); 188 | } 189 | 190 | await Promise.all([animating.then(preventBail), bailPromise]); 191 | result = getFinishedResult(target.get(), true, false); 192 | 193 | // Bail handling 194 | } catch (err) { 195 | if (err instanceof BailSignal) { 196 | result = err.result; 197 | } else if (err instanceof SkipAniamtionSignal) { 198 | result = err.result; 199 | } else { 200 | throw err; 201 | } 202 | 203 | // Reset the async state. 204 | } finally { 205 | if (callId == state.asyncId) { 206 | state.asyncId = parentId; 207 | state.asyncTo = parentId ? prevTo : undefined; 208 | state.promise = parentId ? prevPromise : undefined; 209 | } 210 | } 211 | 212 | if (is.fun(onRest)) { 213 | raf.batchedUpdates(() => { 214 | onRest(result, target, target.item); 215 | }); 216 | } 217 | 218 | return result; 219 | })()); 220 | } 221 | 222 | /** Stop the current `runAsync` call with `finished: false` (or with `cancelled: true` when `cancelId` is defined) */ 223 | export function stopAsync(state: RunAsyncState, cancelId?: number | Falsy) { 224 | flush(state.timeouts, (t) => t.cancel()); 225 | state.pauseQueue.clear(); 226 | state.resumeQueue.clear(); 227 | state.asyncId = state.asyncTo = state.promise = undefined; 228 | if (cancelId) state.cancelId = cancelId; 229 | } 230 | 231 | /** This error is thrown to signal an interrupted async animation. */ 232 | export class BailSignal extends Error { 233 | result!: AnimationResult; 234 | constructor() { 235 | super( 236 | "An async animation has been interrupted. You see this error because you " + 237 | "forgot to use `await` or `.catch(...)` on its returned promise." 238 | ); 239 | } 240 | } 241 | 242 | export class SkipAniamtionSignal extends Error { 243 | result!: AnimationResult; 244 | 245 | constructor() { 246 | super("SkipAnimationSignal"); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /spring/src/scheduleProps.ts: -------------------------------------------------------------------------------- 1 | import * as G from './globals' 2 | import { raf } from "./rafz"; 3 | import { InferProps, InferState, RunAsyncProps, RunAsyncState } from "./runAsync"; 4 | import { AnimationResolver, AnimationTarget, AsyncResult, callProp, is, matchProp, MatchProp, Timeout } from "./utils"; 5 | 6 | // The `scheduleProps` function only handles these defaults. 7 | type DefaultProps = { cancel?: MatchProp; pause?: MatchProp } 8 | 9 | interface ScheduledProps { 10 | key?: string 11 | props: InferProps 12 | defaultProps?: DefaultProps> 13 | state: RunAsyncState 14 | actions: { 15 | pause: () => void 16 | resume: () => void 17 | start: (props: RunAsyncProps, resolve: AnimationResolver) => void 18 | } 19 | } 20 | 21 | /** 22 | * This function sets a timeout if both the `delay` prop exists and 23 | * the `cancel` prop is not `true`. 24 | * 25 | * The `actions.start` function must handle the `cancel` prop itself, 26 | * but the `pause` prop is taken care of. 27 | */ 28 | export function scheduleProps( 29 | callId: number, 30 | { key, props, defaultProps, state, actions }: ScheduledProps 31 | ): AsyncResult { 32 | return new Promise((resolve, reject) => { 33 | let delay: number 34 | let timeout: Timeout 35 | 36 | let cancel = matchProp(props.cancel ?? defaultProps?.cancel, key) 37 | if (cancel) { 38 | onStart() 39 | } else { 40 | // The `pause` prop updates the paused flag. 41 | if (!is.und(props.pause)) { 42 | state.paused = matchProp(props.pause, key) 43 | } 44 | // The default `pause` takes precedence when true, 45 | // which allows `SpringContext` to work as expected. 46 | let pause = defaultProps?.pause 47 | if (pause !== true) { 48 | pause = state.paused || matchProp(pause, key) 49 | } 50 | 51 | delay = callProp(props.delay || 0, key) 52 | if (pause) { 53 | state.resumeQueue.add(onResume) 54 | actions.pause() 55 | } else { 56 | actions.resume() 57 | onResume() 58 | } 59 | } 60 | 61 | function onPause() { 62 | state.resumeQueue.add(onResume) 63 | state.timeouts.delete(timeout) 64 | timeout.cancel() 65 | // Cache the remaining delay. 66 | delay = timeout.time - raf.now() 67 | } 68 | 69 | function onResume() { 70 | if (delay > 0 && !G.skipAnimation) { 71 | state.delayed = true 72 | timeout = raf.setTimeout(onStart, delay) 73 | state.pauseQueue.add(onPause) 74 | state.timeouts.add(timeout) 75 | } else { 76 | onStart() 77 | } 78 | } 79 | 80 | function onStart() { 81 | if (state.delayed) { 82 | state.delayed = false 83 | } 84 | 85 | state.pauseQueue.delete(onPause) 86 | state.timeouts.delete(timeout) 87 | 88 | // Maybe cancelled during its delay. 89 | if (callId <= (state.cancelId || 0)) { 90 | cancel = true 91 | } 92 | 93 | try { 94 | actions.start({ ...props, callId, cancel }, resolve) 95 | } catch (err) { 96 | reject(err) 97 | } 98 | } 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /spring/src/solid/createSpring.ts: -------------------------------------------------------------------------------- 1 | import { Accessor, createEffect, createMemo } from "solid-js"; 2 | import { SpringRef } from "../SpringRef"; 3 | import type { SpringRef as SpringRefType } from "../SpringRef"; 4 | import { 5 | ControllerUpdate, 6 | is, 7 | PickAnimated, 8 | Remap, 9 | SpringValues, 10 | Valid, 11 | } from "../utils"; 12 | import { createSprings } from "./createSprings"; 13 | 14 | /** 15 | * The props that `useSpring` recognizes. 16 | */ 17 | export type CreateSpringProps = unknown & 18 | PickAnimated extends infer State 19 | ? Remap< 20 | ControllerUpdate & { 21 | /** 22 | * Used to access the imperative API. 23 | * 24 | * When defined, the render animation won't auto-start. 25 | */ 26 | ref?: SpringRef; 27 | } 28 | > 29 | : never; 30 | 31 | export function createSpring( 32 | props: () => 33 | | (Props & Valid>) 34 | | CreateSpringProps 35 | ): Accessor>> & { 36 | ref: SpringRefType>; 37 | }; 38 | 39 | export function createSpring( 40 | props: (Props & Valid>) | CreateSpringProps 41 | ): Accessor>> & { 42 | ref: SpringRefType>; 43 | }; 44 | 45 | export function createSpring(props: any): any { 46 | const fn: Accessor = createMemo(is.fun(props) ? props : () => props); 47 | 48 | const springsFn = createSprings(1, fn); 49 | const springMemo = createMemo(() => { 50 | const [value] = springsFn(); 51 | return value 52 | }); 53 | 54 | return springMemo; 55 | } 56 | -------------------------------------------------------------------------------- /spring/src/solid/createSprings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ControllerFlushFn, 3 | ControllerUpdate, 4 | detachRefs, 5 | is, 6 | Lookup, 7 | PickAnimated, 8 | replaceRef, 9 | SpringValues, 10 | } from "../utils"; 11 | import { SpringRef } from "../SpringRef"; 12 | import type { SpringRef as SpringRefType } from "../SpringRef"; 13 | import { each } from "../utils"; 14 | import { 15 | Controller, 16 | flushUpdateQueue, 17 | getSprings, 18 | setSprings, 19 | } from "../Controller"; 20 | import { 21 | Accessor, 22 | createEffect, 23 | createMemo, 24 | createRenderEffect, 25 | createSignal, 26 | onCleanup, 27 | } from "solid-js"; 28 | import { declareUpdate } from "../SpringValue"; 29 | 30 | export type CreateSpringsProps = unknown & 31 | ControllerUpdate & { 32 | ref?: SpringRefType; 33 | }; 34 | 35 | export function createSprings( 36 | lengthFn: number | (() => number), 37 | props: Props[] & CreateSpringsProps>[] 38 | ): Accessor>[]> & { 39 | ref: SpringRefType>; 40 | }; 41 | 42 | export function createSprings( 43 | lengthFn: number | (() => number), 44 | props: (i: number, ctrl: Controller) => Props 45 | ): Accessor>[]> & { 46 | ref: SpringRefType>; 47 | }; 48 | 49 | export function createSprings( 50 | lengthFn: any, 51 | props: any[] | ((i: number, ctrl: Controller) => any) 52 | ): Accessor>[]> & { 53 | ref: SpringRefType>; 54 | } { 55 | const _lengthFn = lengthFn 56 | lengthFn = is.fun(lengthFn) ? lengthFn : () => _lengthFn as number; 57 | const propsFn = is.fun(props) ? props : undefined; 58 | const ref = SpringRef(); 59 | 60 | interface State { 61 | // The controllers used for applying updates. 62 | ctrls: Controller[]; 63 | // The queue of changes to make on commit. 64 | queue: Array<() => void>; 65 | // The flush function used by controllers. 66 | flush: ControllerFlushFn; 67 | } 68 | let layoutId = 0; 69 | 70 | const state: State = { 71 | ctrls: [], 72 | queue: [], 73 | flush(ctrl, updates) { 74 | const springs = getSprings(ctrl, updates); 75 | 76 | // Flushing is postponed until the component's commit phase 77 | // if a spring was created since the last commit. 78 | const canFlushSync = 79 | layoutId > 0 && 80 | !state.queue.length && 81 | !Object.keys(springs).some((key) => !ctrl.springs[key]); 82 | 83 | return canFlushSync 84 | ? flushUpdateQueue(ctrl, updates) 85 | : new Promise((resolve) => { 86 | setSprings(ctrl, springs); 87 | state.queue.push(() => { 88 | resolve(flushUpdateQueue(ctrl, updates)); 89 | }); 90 | // forceUpdate() 91 | }); 92 | }, 93 | }; 94 | 95 | const ctrls = [...state.ctrls]; 96 | 97 | const updates: any[] = []; 98 | // Create new controllers when "length" increases, and destroy 99 | // the affected controllers when "length" decreases. 100 | createEffect(() => { 101 | const length = lengthFn(); 102 | // Clean up any unused controllers 103 | each(ctrls.slice(length, prevLength), (ctrl) => { 104 | detachRefs(ctrl, ref); 105 | ctrl.stop(true); 106 | }); 107 | ctrls.length = length; 108 | 109 | declareUpdates(prevLength, length); 110 | }); 111 | 112 | // Cache old controllers to dispose in the commit phase. 113 | const prevLength = lengthFn() || 0; 114 | const [update, setUpdate] = createSignal(Symbol()) 115 | 116 | // Update existing controllers when "deps" are changed. 117 | createRenderEffect(() => { 118 | const length = lengthFn(); 119 | declareUpdates(0, Math.min(prevLength, length)); 120 | }); 121 | 122 | 123 | /** Fill the `updates` array with declarative updates for the given index range. */ 124 | function declareUpdates(startIndex: number, endIndex: number) { 125 | for (let i = startIndex; i < endIndex; i++) { 126 | const ctrl = ctrls[i] || (ctrls[i] = new Controller(null, state.flush)); 127 | 128 | const update: CreateSpringsProps = propsFn 129 | ? propsFn(i, ctrl) 130 | : (props as any)[i]; 131 | 132 | if (update) { 133 | updates[i] = declareUpdate(update); 134 | } 135 | } 136 | setUpdate(Symbol()) 137 | } 138 | 139 | // New springs are created during render so users can pass them to 140 | // their animated components, but new springs aren't cached until the 141 | // commit phase (see the `useLayoutEffect` callback below). 142 | const springs = ctrls.map((ctrl, i) => getSprings(ctrl, updates[i])); 143 | 144 | createRenderEffect(() => { 145 | update() 146 | 147 | layoutId++; 148 | 149 | // Replace the cached controllers. 150 | state.ctrls = ctrls; 151 | 152 | // Flush the commit queue. 153 | const { queue } = state; 154 | if (queue.length) { 155 | state.queue = []; 156 | each(queue, (cb) => cb()); 157 | } 158 | 159 | // Update existing controllers. 160 | each(ctrls, (ctrl, i) => { 161 | // Attach the controller to the local ref. 162 | ref.add(ctrl); 163 | 164 | // Update the default props. 165 | /* if (hasContext) { 166 | ctrl.start({ default: context }) 167 | } */ 168 | 169 | // Apply updates created during render. 170 | const update = updates[i]; 171 | if (update) { 172 | // Update the injected ref if needed. 173 | replaceRef(ctrl, update.ref); 174 | 175 | // When an injected ref exists, the update is postponed 176 | // until the ref has its `start` method called. 177 | if (ctrl.ref) { 178 | ctrl.queue.push(update); 179 | } else { 180 | ctrl.start(update); 181 | } 182 | } 183 | }); 184 | }); 185 | 186 | onCleanup(() => { 187 | each(state.ctrls, (ctrl) => ctrl.stop(true)); 188 | }); 189 | 190 | const value: Accessor>[]> & { 191 | ref: SpringRefType>; 192 | } = createMemo(() => springs.map((x) => ({ ...x }))) as any; 193 | 194 | value.ref = ref as any; 195 | 196 | return value; 197 | } 198 | -------------------------------------------------------------------------------- /spring/src/withAnimated.tsx: -------------------------------------------------------------------------------- 1 | import { Dynamic } from "solid-js/web"; 2 | import { 3 | children, 4 | createComponent, 5 | createRenderEffect, 6 | onCleanup, 7 | } from "solid-js"; 8 | import { AnimatedObject } from "./AnimatedObject"; 9 | import { TreeContext } from "./context"; 10 | import { HostConfig } from "./createHost"; 11 | import { 12 | addFluidObserver, 13 | FluidEvent, 14 | FluidValue, 15 | removeFluidObserver, 16 | } from "./fluids"; 17 | import { each } from "./utils"; 18 | import { raf } from "./rafz"; 19 | 20 | export type AnimatableComponent = string; 21 | 22 | // Shout out to @Otonashi & @Alex Lohr: https://discord.com/channels/722131463138705510/817960620736380928/961505601039523880 23 | export const withAnimated = (Component: string, host: HostConfig) => { 24 | return (props: any) => { 25 | const c = children(() => 26 | createComponent(Dynamic, { component: Component, ...props }) 27 | ); 28 | const instanceRef: Element = c() as any; 29 | const [_props, deps] = getAnimatedState(props, host); 30 | 31 | const callback = () => { 32 | const didUpdate = instanceRef 33 | ? host.applyAnimatedValues(instanceRef, _props.getValue(true)) 34 | : false; 35 | 36 | // Re-render the component when native updates fail. 37 | if (didUpdate === false) { 38 | // forceUpdate() 39 | } 40 | }; 41 | 42 | const observer = new PropsObserver(callback, deps); 43 | 44 | createRenderEffect(() => { 45 | // Observe the latest dependencies. 46 | each(deps, (dep) => addFluidObserver(dep, observer)); 47 | 48 | // if (lastObserver) { 49 | // each(observer.deps, (dep) => removeFluidObserver(dep, observer)); 50 | // raf.cancel(observer.update); 51 | // } 52 | }); 53 | callback(); 54 | onCleanup(() => { 55 | each(observer.deps, (dep) => removeFluidObserver(dep, observer)); 56 | }); 57 | 58 | return c; 59 | }; 60 | }; 61 | 62 | class PropsObserver { 63 | constructor(readonly update: () => void, readonly deps: Set) {} 64 | eventObserved(event: FluidEvent) { 65 | if (event.type == "change") { 66 | raf.write(this.update); 67 | } 68 | } 69 | } 70 | 71 | type AnimatedState = [props: AnimatedObject, dependencies: Set]; 72 | 73 | function getAnimatedState(props: any, host: HostConfig): AnimatedState { 74 | const dependencies = new Set(); 75 | TreeContext.dependencies = dependencies; 76 | 77 | // Search the style for dependencies. 78 | if (props.style) 79 | props = { 80 | ...props, 81 | style: host.createAnimatedStyle(props.style), 82 | }; 83 | 84 | // Search the props for dependencies. 85 | props = new AnimatedObject(props); 86 | 87 | TreeContext.dependencies = null; 88 | return [props, dependencies]; 89 | } 90 | -------------------------------------------------------------------------------- /spring/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": ["./dist"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["esnext", "dom"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | "declaration": true, 15 | "inlineSourceMap": true, 16 | "jsx": "preserve" 17 | }, 18 | "exclude": [ 19 | "**/dist/**", 20 | "./packages/vitest/dist/**", 21 | "./packages/ui/client/**", 22 | "./examples/**/*.*", 23 | "./bench/**" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------