├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── favicon.ico ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── compute │ ├── generation │ │ ├── cloth.ts │ │ ├── index.ts │ │ └── ropes.ts │ ├── input.ts │ └── simulation │ │ ├── constrain.ts │ │ ├── index.ts │ │ ├── lines.ts │ │ ├── step.ts │ │ └── types.ts ├── index.html ├── main.css ├── main.ts └── render │ ├── camera.ts │ ├── geometry.ts │ ├── lines.ts │ ├── points.ts │ └── renderer.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | client: 10 | environment: 11 | name: github-pages 12 | url: ${{ steps.deployment.outputs.page_url }} 13 | permissions: 14 | contents: read 15 | pages: write 16 | packages: read 17 | id-token: write 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: pnpm/action-setup@v2 22 | with: 23 | version: 7 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 18 27 | cache: 'pnpm' 28 | - name: Install 29 | run: pnpm install 30 | - name: Build 31 | run: pnpm build 32 | env: 33 | WEBGPU_ORIGIN_TRIAL: ${{ vars.WEBGPU_ORIGIN_TRIAL }} 34 | - uses: actions/configure-pages@v3 35 | - uses: actions/upload-pages-artifact@v1 36 | with: 37 | path: 'dist' 38 | - id: deployment 39 | uses: actions/deploy-pages@v1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ggsimm.wgsl-literal" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "files.eol": "\n", 4 | "search.exclude": { 5 | "**/.github": true, 6 | "**/.vscode": true, 7 | "**/dist": true, 8 | "**/node_modules": true, 9 | }, 10 | "files.exclude": { 11 | "**/.github": true, 12 | "**/.vscode": true, 13 | "**/dist": true, 14 | "**/node_modules": true, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2023 Daniel Esteban Nombela 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [gpucloth](https://github.com/danielesteban/gpucloth) 2 | == 3 | 4 | ```bash 5 | # clone repo: 6 | git clone https://github.com/danielesteban/gpucloth.git 7 | cd gpucloth 8 | # install dependencies: 9 | pnpm install 10 | # start environment: 11 | pnpm start 12 | # open http://localhost:8080/ in your browser 13 | ``` 14 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielesteban/gpucloth/be0011b0edd3cbb3a44c030ffc30e884ca44f89c/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpucloth", 3 | "scripts": { 4 | "build": "rollup -c rollup.config.js", 5 | "start": "pnpm build -w" 6 | }, 7 | "type": "module", 8 | "dependencies": { 9 | "gl-matrix": "^3.4.3" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-html": "^1.0.2", 13 | "@rollup/plugin-node-resolve": "^15.0.1", 14 | "@rollup/plugin-terser": "^0.4.0", 15 | "@rollup/plugin-typescript": "^11.0.0", 16 | "@webgpu/types": "^0.1.30", 17 | "postcss": "^8.4.21", 18 | "rollup": "^3.20.2", 19 | "rollup-plugin-copy": "^3.4.0", 20 | "rollup-plugin-livereload": "^2.0.5", 21 | "rollup-plugin-postcss": "^4.0.2", 22 | "rollup-plugin-serve": "^2.0.2", 23 | "tslib": "^2.5.0", 24 | "typescript": "^5.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@rollup/plugin-html': ^1.0.2 5 | '@rollup/plugin-node-resolve': ^15.0.1 6 | '@rollup/plugin-terser': ^0.4.0 7 | '@rollup/plugin-typescript': ^11.0.0 8 | '@webgpu/types': ^0.1.30 9 | gl-matrix: ^3.4.3 10 | postcss: ^8.4.21 11 | rollup: ^3.20.2 12 | rollup-plugin-copy: ^3.4.0 13 | rollup-plugin-livereload: ^2.0.5 14 | rollup-plugin-postcss: ^4.0.2 15 | rollup-plugin-serve: ^2.0.2 16 | tslib: ^2.5.0 17 | typescript: ^5.0.2 18 | 19 | dependencies: 20 | gl-matrix: 3.4.3 21 | 22 | devDependencies: 23 | '@rollup/plugin-html': 1.0.2_rollup@3.20.2 24 | '@rollup/plugin-node-resolve': 15.0.1_rollup@3.20.2 25 | '@rollup/plugin-terser': 0.4.0_rollup@3.20.2 26 | '@rollup/plugin-typescript': 11.0.0_b72j35qsjh4lu3mgicjyqmc4ve 27 | '@webgpu/types': 0.1.30 28 | postcss: 8.4.21 29 | rollup: 3.20.2 30 | rollup-plugin-copy: 3.4.0 31 | rollup-plugin-livereload: 2.0.5 32 | rollup-plugin-postcss: 4.0.2_postcss@8.4.21 33 | rollup-plugin-serve: 2.0.2 34 | tslib: 2.5.0 35 | typescript: 5.0.2 36 | 37 | packages: 38 | 39 | /@jridgewell/gen-mapping/0.3.2: 40 | resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} 41 | engines: {node: '>=6.0.0'} 42 | dependencies: 43 | '@jridgewell/set-array': 1.1.2 44 | '@jridgewell/sourcemap-codec': 1.4.14 45 | '@jridgewell/trace-mapping': 0.3.17 46 | dev: true 47 | 48 | /@jridgewell/resolve-uri/3.1.0: 49 | resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} 50 | engines: {node: '>=6.0.0'} 51 | dev: true 52 | 53 | /@jridgewell/set-array/1.1.2: 54 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 55 | engines: {node: '>=6.0.0'} 56 | dev: true 57 | 58 | /@jridgewell/source-map/0.3.2: 59 | resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} 60 | dependencies: 61 | '@jridgewell/gen-mapping': 0.3.2 62 | '@jridgewell/trace-mapping': 0.3.17 63 | dev: true 64 | 65 | /@jridgewell/sourcemap-codec/1.4.14: 66 | resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} 67 | dev: true 68 | 69 | /@jridgewell/trace-mapping/0.3.17: 70 | resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} 71 | dependencies: 72 | '@jridgewell/resolve-uri': 3.1.0 73 | '@jridgewell/sourcemap-codec': 1.4.14 74 | dev: true 75 | 76 | /@nodelib/fs.scandir/2.1.5: 77 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 78 | engines: {node: '>= 8'} 79 | dependencies: 80 | '@nodelib/fs.stat': 2.0.5 81 | run-parallel: 1.2.0 82 | dev: true 83 | 84 | /@nodelib/fs.stat/2.0.5: 85 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 86 | engines: {node: '>= 8'} 87 | dev: true 88 | 89 | /@nodelib/fs.walk/1.2.8: 90 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 91 | engines: {node: '>= 8'} 92 | dependencies: 93 | '@nodelib/fs.scandir': 2.1.5 94 | fastq: 1.15.0 95 | dev: true 96 | 97 | /@rollup/plugin-html/1.0.2_rollup@3.20.2: 98 | resolution: {integrity: sha512-jGqb45BPj5kwvb/bq1jIzUDLebsm1xmfnY1rwgTIZyjpsMyMKLuQO27n4z5qv6kZmxqxh+CBRD7d1rjAu85Uzg==} 99 | engines: {node: '>=14.0.0'} 100 | peerDependencies: 101 | rollup: ^1.20.0||^2.0.0||^3.0.0 102 | peerDependenciesMeta: 103 | rollup: 104 | optional: true 105 | dependencies: 106 | rollup: 3.20.2 107 | dev: true 108 | 109 | /@rollup/plugin-node-resolve/15.0.1_rollup@3.20.2: 110 | resolution: {integrity: sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==} 111 | engines: {node: '>=14.0.0'} 112 | peerDependencies: 113 | rollup: ^2.78.0||^3.0.0 114 | peerDependenciesMeta: 115 | rollup: 116 | optional: true 117 | dependencies: 118 | '@rollup/pluginutils': 5.0.2_rollup@3.20.2 119 | '@types/resolve': 1.20.2 120 | deepmerge: 4.3.1 121 | is-builtin-module: 3.2.1 122 | is-module: 1.0.0 123 | resolve: 1.22.1 124 | rollup: 3.20.2 125 | dev: true 126 | 127 | /@rollup/plugin-terser/0.4.0_rollup@3.20.2: 128 | resolution: {integrity: sha512-Ipcf3LPNerey1q9ZMjiaWHlNPEHNU/B5/uh9zXLltfEQ1lVSLLeZSgAtTPWGyw8Ip1guOeq+mDtdOlEj/wNxQw==} 129 | engines: {node: '>=14.0.0'} 130 | peerDependencies: 131 | rollup: ^2.x || ^3.x 132 | peerDependenciesMeta: 133 | rollup: 134 | optional: true 135 | dependencies: 136 | rollup: 3.20.2 137 | serialize-javascript: 6.0.1 138 | smob: 0.0.6 139 | terser: 5.16.8 140 | dev: true 141 | 142 | /@rollup/plugin-typescript/11.0.0_b72j35qsjh4lu3mgicjyqmc4ve: 143 | resolution: {integrity: sha512-goPyCWBiimk1iJgSTgsehFD5OOFHiAknrRJjqFCudcW8JtWiBlK284Xnn4flqMqg6YAjVG/EE+3aVzrL5qNSzQ==} 144 | engines: {node: '>=14.0.0'} 145 | peerDependencies: 146 | rollup: ^2.14.0||^3.0.0 147 | tslib: '*' 148 | typescript: '>=3.7.0' 149 | peerDependenciesMeta: 150 | rollup: 151 | optional: true 152 | tslib: 153 | optional: true 154 | dependencies: 155 | '@rollup/pluginutils': 5.0.2_rollup@3.20.2 156 | resolve: 1.22.1 157 | rollup: 3.20.2 158 | tslib: 2.5.0 159 | typescript: 5.0.2 160 | dev: true 161 | 162 | /@rollup/pluginutils/5.0.2_rollup@3.20.2: 163 | resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} 164 | engines: {node: '>=14.0.0'} 165 | peerDependencies: 166 | rollup: ^1.20.0||^2.0.0||^3.0.0 167 | peerDependenciesMeta: 168 | rollup: 169 | optional: true 170 | dependencies: 171 | '@types/estree': 1.0.0 172 | estree-walker: 2.0.2 173 | picomatch: 2.3.1 174 | rollup: 3.20.2 175 | dev: true 176 | 177 | /@trysound/sax/0.2.0: 178 | resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} 179 | engines: {node: '>=10.13.0'} 180 | dev: true 181 | 182 | /@types/estree/1.0.0: 183 | resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} 184 | dev: true 185 | 186 | /@types/fs-extra/8.1.2: 187 | resolution: {integrity: sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==} 188 | dependencies: 189 | '@types/node': 18.15.9 190 | dev: true 191 | 192 | /@types/glob/7.2.0: 193 | resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} 194 | dependencies: 195 | '@types/minimatch': 5.1.2 196 | '@types/node': 18.15.9 197 | dev: true 198 | 199 | /@types/minimatch/5.1.2: 200 | resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} 201 | dev: true 202 | 203 | /@types/node/18.15.9: 204 | resolution: {integrity: sha512-dUxhiNzBLr6IqlZXz6e/rN2YQXlFgOei/Dxy+e3cyXTJ4txSUbGT2/fmnD6zd/75jDMeW5bDee+YXxlFKHoV0A==} 205 | dev: true 206 | 207 | /@types/resolve/1.20.2: 208 | resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} 209 | dev: true 210 | 211 | /@webgpu/types/0.1.30: 212 | resolution: {integrity: sha512-9AXJSmL3MzY8ZL//JjudA//q+2kBRGhLBFpkdGksWIuxrMy81nFrCzj2Am+mbh8WoU6rXmv7cY5E3rdlyru2Qg==} 213 | dev: true 214 | 215 | /acorn/8.8.2: 216 | resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} 217 | engines: {node: '>=0.4.0'} 218 | hasBin: true 219 | dev: true 220 | 221 | /ansi-styles/4.3.0: 222 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 223 | engines: {node: '>=8'} 224 | dependencies: 225 | color-convert: 2.0.1 226 | dev: true 227 | 228 | /anymatch/3.1.3: 229 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 230 | engines: {node: '>= 8'} 231 | dependencies: 232 | normalize-path: 3.0.0 233 | picomatch: 2.3.1 234 | dev: true 235 | 236 | /array-union/2.1.0: 237 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 238 | engines: {node: '>=8'} 239 | dev: true 240 | 241 | /balanced-match/1.0.2: 242 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 243 | dev: true 244 | 245 | /binary-extensions/2.2.0: 246 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 247 | engines: {node: '>=8'} 248 | dev: true 249 | 250 | /boolbase/1.0.0: 251 | resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} 252 | dev: true 253 | 254 | /brace-expansion/1.1.11: 255 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 256 | dependencies: 257 | balanced-match: 1.0.2 258 | concat-map: 0.0.1 259 | dev: true 260 | 261 | /braces/3.0.2: 262 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 263 | engines: {node: '>=8'} 264 | dependencies: 265 | fill-range: 7.0.1 266 | dev: true 267 | 268 | /browserslist/4.21.5: 269 | resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} 270 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 271 | hasBin: true 272 | dependencies: 273 | caniuse-lite: 1.0.30001469 274 | electron-to-chromium: 1.4.339 275 | node-releases: 2.0.10 276 | update-browserslist-db: 1.0.10_browserslist@4.21.5 277 | dev: true 278 | 279 | /buffer-from/1.1.2: 280 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 281 | dev: true 282 | 283 | /builtin-modules/3.3.0: 284 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 285 | engines: {node: '>=6'} 286 | dev: true 287 | 288 | /caniuse-api/3.0.0: 289 | resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} 290 | dependencies: 291 | browserslist: 4.21.5 292 | caniuse-lite: 1.0.30001469 293 | lodash.memoize: 4.1.2 294 | lodash.uniq: 4.5.0 295 | dev: true 296 | 297 | /caniuse-lite/1.0.30001469: 298 | resolution: {integrity: sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==} 299 | dev: true 300 | 301 | /chalk/4.1.2: 302 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 303 | engines: {node: '>=10'} 304 | dependencies: 305 | ansi-styles: 4.3.0 306 | supports-color: 7.2.0 307 | dev: true 308 | 309 | /chokidar/3.5.3: 310 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 311 | engines: {node: '>= 8.10.0'} 312 | dependencies: 313 | anymatch: 3.1.3 314 | braces: 3.0.2 315 | glob-parent: 5.1.2 316 | is-binary-path: 2.1.0 317 | is-glob: 4.0.3 318 | normalize-path: 3.0.0 319 | readdirp: 3.6.0 320 | optionalDependencies: 321 | fsevents: 2.3.2 322 | dev: true 323 | 324 | /color-convert/2.0.1: 325 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 326 | engines: {node: '>=7.0.0'} 327 | dependencies: 328 | color-name: 1.1.4 329 | dev: true 330 | 331 | /color-name/1.1.4: 332 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 333 | dev: true 334 | 335 | /colord/2.9.3: 336 | resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} 337 | dev: true 338 | 339 | /colorette/1.4.0: 340 | resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} 341 | dev: true 342 | 343 | /commander/2.20.3: 344 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 345 | dev: true 346 | 347 | /commander/7.2.0: 348 | resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} 349 | engines: {node: '>= 10'} 350 | dev: true 351 | 352 | /concat-map/0.0.1: 353 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 354 | dev: true 355 | 356 | /concat-with-sourcemaps/1.1.0: 357 | resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} 358 | dependencies: 359 | source-map: 0.6.1 360 | dev: true 361 | 362 | /css-declaration-sorter/6.4.0_postcss@8.4.21: 363 | resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} 364 | engines: {node: ^10 || ^12 || >=14} 365 | peerDependencies: 366 | postcss: ^8.0.9 367 | dependencies: 368 | postcss: 8.4.21 369 | dev: true 370 | 371 | /css-select/4.3.0: 372 | resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} 373 | dependencies: 374 | boolbase: 1.0.0 375 | css-what: 6.1.0 376 | domhandler: 4.3.1 377 | domutils: 2.8.0 378 | nth-check: 2.1.1 379 | dev: true 380 | 381 | /css-tree/1.1.3: 382 | resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} 383 | engines: {node: '>=8.0.0'} 384 | dependencies: 385 | mdn-data: 2.0.14 386 | source-map: 0.6.1 387 | dev: true 388 | 389 | /css-what/6.1.0: 390 | resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} 391 | engines: {node: '>= 6'} 392 | dev: true 393 | 394 | /cssesc/3.0.0: 395 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 396 | engines: {node: '>=4'} 397 | hasBin: true 398 | dev: true 399 | 400 | /cssnano-preset-default/5.2.14_postcss@8.4.21: 401 | resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} 402 | engines: {node: ^10 || ^12 || >=14.0} 403 | peerDependencies: 404 | postcss: ^8.2.15 405 | dependencies: 406 | css-declaration-sorter: 6.4.0_postcss@8.4.21 407 | cssnano-utils: 3.1.0_postcss@8.4.21 408 | postcss: 8.4.21 409 | postcss-calc: 8.2.4_postcss@8.4.21 410 | postcss-colormin: 5.3.1_postcss@8.4.21 411 | postcss-convert-values: 5.1.3_postcss@8.4.21 412 | postcss-discard-comments: 5.1.2_postcss@8.4.21 413 | postcss-discard-duplicates: 5.1.0_postcss@8.4.21 414 | postcss-discard-empty: 5.1.1_postcss@8.4.21 415 | postcss-discard-overridden: 5.1.0_postcss@8.4.21 416 | postcss-merge-longhand: 5.1.7_postcss@8.4.21 417 | postcss-merge-rules: 5.1.4_postcss@8.4.21 418 | postcss-minify-font-values: 5.1.0_postcss@8.4.21 419 | postcss-minify-gradients: 5.1.1_postcss@8.4.21 420 | postcss-minify-params: 5.1.4_postcss@8.4.21 421 | postcss-minify-selectors: 5.2.1_postcss@8.4.21 422 | postcss-normalize-charset: 5.1.0_postcss@8.4.21 423 | postcss-normalize-display-values: 5.1.0_postcss@8.4.21 424 | postcss-normalize-positions: 5.1.1_postcss@8.4.21 425 | postcss-normalize-repeat-style: 5.1.1_postcss@8.4.21 426 | postcss-normalize-string: 5.1.0_postcss@8.4.21 427 | postcss-normalize-timing-functions: 5.1.0_postcss@8.4.21 428 | postcss-normalize-unicode: 5.1.1_postcss@8.4.21 429 | postcss-normalize-url: 5.1.0_postcss@8.4.21 430 | postcss-normalize-whitespace: 5.1.1_postcss@8.4.21 431 | postcss-ordered-values: 5.1.3_postcss@8.4.21 432 | postcss-reduce-initial: 5.1.2_postcss@8.4.21 433 | postcss-reduce-transforms: 5.1.0_postcss@8.4.21 434 | postcss-svgo: 5.1.0_postcss@8.4.21 435 | postcss-unique-selectors: 5.1.1_postcss@8.4.21 436 | dev: true 437 | 438 | /cssnano-utils/3.1.0_postcss@8.4.21: 439 | resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} 440 | engines: {node: ^10 || ^12 || >=14.0} 441 | peerDependencies: 442 | postcss: ^8.2.15 443 | dependencies: 444 | postcss: 8.4.21 445 | dev: true 446 | 447 | /cssnano/5.1.15_postcss@8.4.21: 448 | resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} 449 | engines: {node: ^10 || ^12 || >=14.0} 450 | peerDependencies: 451 | postcss: ^8.2.15 452 | dependencies: 453 | cssnano-preset-default: 5.2.14_postcss@8.4.21 454 | lilconfig: 2.1.0 455 | postcss: 8.4.21 456 | yaml: 1.10.2 457 | dev: true 458 | 459 | /csso/4.2.0: 460 | resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} 461 | engines: {node: '>=8.0.0'} 462 | dependencies: 463 | css-tree: 1.1.3 464 | dev: true 465 | 466 | /deepmerge/4.3.1: 467 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 468 | engines: {node: '>=0.10.0'} 469 | dev: true 470 | 471 | /dir-glob/3.0.1: 472 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 473 | engines: {node: '>=8'} 474 | dependencies: 475 | path-type: 4.0.0 476 | dev: true 477 | 478 | /dom-serializer/1.4.1: 479 | resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} 480 | dependencies: 481 | domelementtype: 2.3.0 482 | domhandler: 4.3.1 483 | entities: 2.2.0 484 | dev: true 485 | 486 | /domelementtype/2.3.0: 487 | resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 488 | dev: true 489 | 490 | /domhandler/4.3.1: 491 | resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} 492 | engines: {node: '>= 4'} 493 | dependencies: 494 | domelementtype: 2.3.0 495 | dev: true 496 | 497 | /domutils/2.8.0: 498 | resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} 499 | dependencies: 500 | dom-serializer: 1.4.1 501 | domelementtype: 2.3.0 502 | domhandler: 4.3.1 503 | dev: true 504 | 505 | /electron-to-chromium/1.4.339: 506 | resolution: {integrity: sha512-MSXHBJGcbBydq/DQDlpBeUKnJ6C7aTiNCTRpfDV5Iz0sNr/Ng6RJFloq82AAicp/SrmDq4zF6XsKG0B8Xwn1UQ==} 507 | dev: true 508 | 509 | /entities/2.2.0: 510 | resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} 511 | dev: true 512 | 513 | /escalade/3.1.1: 514 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 515 | engines: {node: '>=6'} 516 | dev: true 517 | 518 | /estree-walker/0.6.1: 519 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 520 | dev: true 521 | 522 | /estree-walker/2.0.2: 523 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 524 | dev: true 525 | 526 | /eventemitter3/4.0.7: 527 | resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 528 | dev: true 529 | 530 | /fast-glob/3.2.12: 531 | resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} 532 | engines: {node: '>=8.6.0'} 533 | dependencies: 534 | '@nodelib/fs.stat': 2.0.5 535 | '@nodelib/fs.walk': 1.2.8 536 | glob-parent: 5.1.2 537 | merge2: 1.4.1 538 | micromatch: 4.0.5 539 | dev: true 540 | 541 | /fastq/1.15.0: 542 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} 543 | dependencies: 544 | reusify: 1.0.4 545 | dev: true 546 | 547 | /fill-range/7.0.1: 548 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 549 | engines: {node: '>=8'} 550 | dependencies: 551 | to-regex-range: 5.0.1 552 | dev: true 553 | 554 | /fs-extra/8.1.0: 555 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 556 | engines: {node: '>=6 <7 || >=8'} 557 | dependencies: 558 | graceful-fs: 4.2.11 559 | jsonfile: 4.0.0 560 | universalify: 0.1.2 561 | dev: true 562 | 563 | /fs.realpath/1.0.0: 564 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 565 | dev: true 566 | 567 | /fsevents/2.3.2: 568 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 569 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 570 | os: [darwin] 571 | requiresBuild: true 572 | dev: true 573 | optional: true 574 | 575 | /function-bind/1.1.1: 576 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 577 | dev: true 578 | 579 | /generic-names/4.0.0: 580 | resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} 581 | dependencies: 582 | loader-utils: 3.2.1 583 | dev: true 584 | 585 | /gl-matrix/3.4.3: 586 | resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} 587 | dev: false 588 | 589 | /glob-parent/5.1.2: 590 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 591 | engines: {node: '>= 6'} 592 | dependencies: 593 | is-glob: 4.0.3 594 | dev: true 595 | 596 | /glob/7.2.3: 597 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 598 | dependencies: 599 | fs.realpath: 1.0.0 600 | inflight: 1.0.6 601 | inherits: 2.0.4 602 | minimatch: 3.1.2 603 | once: 1.4.0 604 | path-is-absolute: 1.0.1 605 | dev: true 606 | 607 | /globby/10.0.1: 608 | resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} 609 | engines: {node: '>=8'} 610 | dependencies: 611 | '@types/glob': 7.2.0 612 | array-union: 2.1.0 613 | dir-glob: 3.0.1 614 | fast-glob: 3.2.12 615 | glob: 7.2.3 616 | ignore: 5.2.4 617 | merge2: 1.4.1 618 | slash: 3.0.0 619 | dev: true 620 | 621 | /graceful-fs/4.2.11: 622 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 623 | dev: true 624 | 625 | /has-flag/4.0.0: 626 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 627 | engines: {node: '>=8'} 628 | dev: true 629 | 630 | /has/1.0.3: 631 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 632 | engines: {node: '>= 0.4.0'} 633 | dependencies: 634 | function-bind: 1.1.1 635 | dev: true 636 | 637 | /icss-replace-symbols/1.1.0: 638 | resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} 639 | dev: true 640 | 641 | /icss-utils/5.1.0_postcss@8.4.21: 642 | resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} 643 | engines: {node: ^10 || ^12 || >= 14} 644 | peerDependencies: 645 | postcss: ^8.1.0 646 | dependencies: 647 | postcss: 8.4.21 648 | dev: true 649 | 650 | /ignore/5.2.4: 651 | resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} 652 | engines: {node: '>= 4'} 653 | dev: true 654 | 655 | /import-cwd/3.0.0: 656 | resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} 657 | engines: {node: '>=8'} 658 | dependencies: 659 | import-from: 3.0.0 660 | dev: true 661 | 662 | /import-from/3.0.0: 663 | resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} 664 | engines: {node: '>=8'} 665 | dependencies: 666 | resolve-from: 5.0.0 667 | dev: true 668 | 669 | /inflight/1.0.6: 670 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 671 | dependencies: 672 | once: 1.4.0 673 | wrappy: 1.0.2 674 | dev: true 675 | 676 | /inherits/2.0.4: 677 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 678 | dev: true 679 | 680 | /is-binary-path/2.1.0: 681 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 682 | engines: {node: '>=8'} 683 | dependencies: 684 | binary-extensions: 2.2.0 685 | dev: true 686 | 687 | /is-builtin-module/3.2.1: 688 | resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} 689 | engines: {node: '>=6'} 690 | dependencies: 691 | builtin-modules: 3.3.0 692 | dev: true 693 | 694 | /is-core-module/2.11.0: 695 | resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} 696 | dependencies: 697 | has: 1.0.3 698 | dev: true 699 | 700 | /is-extglob/2.1.1: 701 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 702 | engines: {node: '>=0.10.0'} 703 | dev: true 704 | 705 | /is-glob/4.0.3: 706 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 707 | engines: {node: '>=0.10.0'} 708 | dependencies: 709 | is-extglob: 2.1.1 710 | dev: true 711 | 712 | /is-module/1.0.0: 713 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 714 | dev: true 715 | 716 | /is-number/7.0.0: 717 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 718 | engines: {node: '>=0.12.0'} 719 | dev: true 720 | 721 | /is-plain-object/3.0.1: 722 | resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} 723 | engines: {node: '>=0.10.0'} 724 | dev: true 725 | 726 | /jsonfile/4.0.0: 727 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 728 | optionalDependencies: 729 | graceful-fs: 4.2.11 730 | dev: true 731 | 732 | /lilconfig/2.1.0: 733 | resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 734 | engines: {node: '>=10'} 735 | dev: true 736 | 737 | /livereload-js/3.4.1: 738 | resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==} 739 | dev: true 740 | 741 | /livereload/0.9.3: 742 | resolution: {integrity: sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==} 743 | engines: {node: '>=8.0.0'} 744 | hasBin: true 745 | dependencies: 746 | chokidar: 3.5.3 747 | livereload-js: 3.4.1 748 | opts: 2.0.2 749 | ws: 7.5.9 750 | transitivePeerDependencies: 751 | - bufferutil 752 | - utf-8-validate 753 | dev: true 754 | 755 | /loader-utils/3.2.1: 756 | resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} 757 | engines: {node: '>= 12.13.0'} 758 | dev: true 759 | 760 | /lodash.camelcase/4.3.0: 761 | resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} 762 | dev: true 763 | 764 | /lodash.memoize/4.1.2: 765 | resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} 766 | dev: true 767 | 768 | /lodash.uniq/4.5.0: 769 | resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} 770 | dev: true 771 | 772 | /mdn-data/2.0.14: 773 | resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} 774 | dev: true 775 | 776 | /merge2/1.4.1: 777 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 778 | engines: {node: '>= 8'} 779 | dev: true 780 | 781 | /micromatch/4.0.5: 782 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 783 | engines: {node: '>=8.6'} 784 | dependencies: 785 | braces: 3.0.2 786 | picomatch: 2.3.1 787 | dev: true 788 | 789 | /mime/3.0.0: 790 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 791 | engines: {node: '>=10.0.0'} 792 | hasBin: true 793 | dev: true 794 | 795 | /minimatch/3.1.2: 796 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 797 | dependencies: 798 | brace-expansion: 1.1.11 799 | dev: true 800 | 801 | /nanoid/3.3.4: 802 | resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} 803 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 804 | hasBin: true 805 | dev: true 806 | 807 | /node-releases/2.0.10: 808 | resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} 809 | dev: true 810 | 811 | /normalize-path/3.0.0: 812 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 813 | engines: {node: '>=0.10.0'} 814 | dev: true 815 | 816 | /normalize-url/6.1.0: 817 | resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} 818 | engines: {node: '>=10'} 819 | dev: true 820 | 821 | /nth-check/2.1.1: 822 | resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 823 | dependencies: 824 | boolbase: 1.0.0 825 | dev: true 826 | 827 | /once/1.4.0: 828 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 829 | dependencies: 830 | wrappy: 1.0.2 831 | dev: true 832 | 833 | /opener/1.5.2: 834 | resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} 835 | hasBin: true 836 | dev: true 837 | 838 | /opts/2.0.2: 839 | resolution: {integrity: sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==} 840 | dev: true 841 | 842 | /p-finally/1.0.0: 843 | resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} 844 | engines: {node: '>=4'} 845 | dev: true 846 | 847 | /p-queue/6.6.2: 848 | resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} 849 | engines: {node: '>=8'} 850 | dependencies: 851 | eventemitter3: 4.0.7 852 | p-timeout: 3.2.0 853 | dev: true 854 | 855 | /p-timeout/3.2.0: 856 | resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} 857 | engines: {node: '>=8'} 858 | dependencies: 859 | p-finally: 1.0.0 860 | dev: true 861 | 862 | /path-is-absolute/1.0.1: 863 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 864 | engines: {node: '>=0.10.0'} 865 | dev: true 866 | 867 | /path-parse/1.0.7: 868 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 869 | dev: true 870 | 871 | /path-type/4.0.0: 872 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 873 | engines: {node: '>=8'} 874 | dev: true 875 | 876 | /picocolors/1.0.0: 877 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 878 | dev: true 879 | 880 | /picomatch/2.3.1: 881 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 882 | engines: {node: '>=8.6'} 883 | dev: true 884 | 885 | /pify/5.0.0: 886 | resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} 887 | engines: {node: '>=10'} 888 | dev: true 889 | 890 | /postcss-calc/8.2.4_postcss@8.4.21: 891 | resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} 892 | peerDependencies: 893 | postcss: ^8.2.2 894 | dependencies: 895 | postcss: 8.4.21 896 | postcss-selector-parser: 6.0.11 897 | postcss-value-parser: 4.2.0 898 | dev: true 899 | 900 | /postcss-colormin/5.3.1_postcss@8.4.21: 901 | resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} 902 | engines: {node: ^10 || ^12 || >=14.0} 903 | peerDependencies: 904 | postcss: ^8.2.15 905 | dependencies: 906 | browserslist: 4.21.5 907 | caniuse-api: 3.0.0 908 | colord: 2.9.3 909 | postcss: 8.4.21 910 | postcss-value-parser: 4.2.0 911 | dev: true 912 | 913 | /postcss-convert-values/5.1.3_postcss@8.4.21: 914 | resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} 915 | engines: {node: ^10 || ^12 || >=14.0} 916 | peerDependencies: 917 | postcss: ^8.2.15 918 | dependencies: 919 | browserslist: 4.21.5 920 | postcss: 8.4.21 921 | postcss-value-parser: 4.2.0 922 | dev: true 923 | 924 | /postcss-discard-comments/5.1.2_postcss@8.4.21: 925 | resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} 926 | engines: {node: ^10 || ^12 || >=14.0} 927 | peerDependencies: 928 | postcss: ^8.2.15 929 | dependencies: 930 | postcss: 8.4.21 931 | dev: true 932 | 933 | /postcss-discard-duplicates/5.1.0_postcss@8.4.21: 934 | resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} 935 | engines: {node: ^10 || ^12 || >=14.0} 936 | peerDependencies: 937 | postcss: ^8.2.15 938 | dependencies: 939 | postcss: 8.4.21 940 | dev: true 941 | 942 | /postcss-discard-empty/5.1.1_postcss@8.4.21: 943 | resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} 944 | engines: {node: ^10 || ^12 || >=14.0} 945 | peerDependencies: 946 | postcss: ^8.2.15 947 | dependencies: 948 | postcss: 8.4.21 949 | dev: true 950 | 951 | /postcss-discard-overridden/5.1.0_postcss@8.4.21: 952 | resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} 953 | engines: {node: ^10 || ^12 || >=14.0} 954 | peerDependencies: 955 | postcss: ^8.2.15 956 | dependencies: 957 | postcss: 8.4.21 958 | dev: true 959 | 960 | /postcss-load-config/3.1.4_postcss@8.4.21: 961 | resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} 962 | engines: {node: '>= 10'} 963 | peerDependencies: 964 | postcss: '>=8.0.9' 965 | ts-node: '>=9.0.0' 966 | peerDependenciesMeta: 967 | postcss: 968 | optional: true 969 | ts-node: 970 | optional: true 971 | dependencies: 972 | lilconfig: 2.1.0 973 | postcss: 8.4.21 974 | yaml: 1.10.2 975 | dev: true 976 | 977 | /postcss-merge-longhand/5.1.7_postcss@8.4.21: 978 | resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} 979 | engines: {node: ^10 || ^12 || >=14.0} 980 | peerDependencies: 981 | postcss: ^8.2.15 982 | dependencies: 983 | postcss: 8.4.21 984 | postcss-value-parser: 4.2.0 985 | stylehacks: 5.1.1_postcss@8.4.21 986 | dev: true 987 | 988 | /postcss-merge-rules/5.1.4_postcss@8.4.21: 989 | resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} 990 | engines: {node: ^10 || ^12 || >=14.0} 991 | peerDependencies: 992 | postcss: ^8.2.15 993 | dependencies: 994 | browserslist: 4.21.5 995 | caniuse-api: 3.0.0 996 | cssnano-utils: 3.1.0_postcss@8.4.21 997 | postcss: 8.4.21 998 | postcss-selector-parser: 6.0.11 999 | dev: true 1000 | 1001 | /postcss-minify-font-values/5.1.0_postcss@8.4.21: 1002 | resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} 1003 | engines: {node: ^10 || ^12 || >=14.0} 1004 | peerDependencies: 1005 | postcss: ^8.2.15 1006 | dependencies: 1007 | postcss: 8.4.21 1008 | postcss-value-parser: 4.2.0 1009 | dev: true 1010 | 1011 | /postcss-minify-gradients/5.1.1_postcss@8.4.21: 1012 | resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} 1013 | engines: {node: ^10 || ^12 || >=14.0} 1014 | peerDependencies: 1015 | postcss: ^8.2.15 1016 | dependencies: 1017 | colord: 2.9.3 1018 | cssnano-utils: 3.1.0_postcss@8.4.21 1019 | postcss: 8.4.21 1020 | postcss-value-parser: 4.2.0 1021 | dev: true 1022 | 1023 | /postcss-minify-params/5.1.4_postcss@8.4.21: 1024 | resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} 1025 | engines: {node: ^10 || ^12 || >=14.0} 1026 | peerDependencies: 1027 | postcss: ^8.2.15 1028 | dependencies: 1029 | browserslist: 4.21.5 1030 | cssnano-utils: 3.1.0_postcss@8.4.21 1031 | postcss: 8.4.21 1032 | postcss-value-parser: 4.2.0 1033 | dev: true 1034 | 1035 | /postcss-minify-selectors/5.2.1_postcss@8.4.21: 1036 | resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} 1037 | engines: {node: ^10 || ^12 || >=14.0} 1038 | peerDependencies: 1039 | postcss: ^8.2.15 1040 | dependencies: 1041 | postcss: 8.4.21 1042 | postcss-selector-parser: 6.0.11 1043 | dev: true 1044 | 1045 | /postcss-modules-extract-imports/3.0.0_postcss@8.4.21: 1046 | resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} 1047 | engines: {node: ^10 || ^12 || >= 14} 1048 | peerDependencies: 1049 | postcss: ^8.1.0 1050 | dependencies: 1051 | postcss: 8.4.21 1052 | dev: true 1053 | 1054 | /postcss-modules-local-by-default/4.0.0_postcss@8.4.21: 1055 | resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} 1056 | engines: {node: ^10 || ^12 || >= 14} 1057 | peerDependencies: 1058 | postcss: ^8.1.0 1059 | dependencies: 1060 | icss-utils: 5.1.0_postcss@8.4.21 1061 | postcss: 8.4.21 1062 | postcss-selector-parser: 6.0.11 1063 | postcss-value-parser: 4.2.0 1064 | dev: true 1065 | 1066 | /postcss-modules-scope/3.0.0_postcss@8.4.21: 1067 | resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} 1068 | engines: {node: ^10 || ^12 || >= 14} 1069 | peerDependencies: 1070 | postcss: ^8.1.0 1071 | dependencies: 1072 | postcss: 8.4.21 1073 | postcss-selector-parser: 6.0.11 1074 | dev: true 1075 | 1076 | /postcss-modules-values/4.0.0_postcss@8.4.21: 1077 | resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} 1078 | engines: {node: ^10 || ^12 || >= 14} 1079 | peerDependencies: 1080 | postcss: ^8.1.0 1081 | dependencies: 1082 | icss-utils: 5.1.0_postcss@8.4.21 1083 | postcss: 8.4.21 1084 | dev: true 1085 | 1086 | /postcss-modules/4.3.1_postcss@8.4.21: 1087 | resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} 1088 | peerDependencies: 1089 | postcss: ^8.0.0 1090 | dependencies: 1091 | generic-names: 4.0.0 1092 | icss-replace-symbols: 1.1.0 1093 | lodash.camelcase: 4.3.0 1094 | postcss: 8.4.21 1095 | postcss-modules-extract-imports: 3.0.0_postcss@8.4.21 1096 | postcss-modules-local-by-default: 4.0.0_postcss@8.4.21 1097 | postcss-modules-scope: 3.0.0_postcss@8.4.21 1098 | postcss-modules-values: 4.0.0_postcss@8.4.21 1099 | string-hash: 1.1.3 1100 | dev: true 1101 | 1102 | /postcss-normalize-charset/5.1.0_postcss@8.4.21: 1103 | resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} 1104 | engines: {node: ^10 || ^12 || >=14.0} 1105 | peerDependencies: 1106 | postcss: ^8.2.15 1107 | dependencies: 1108 | postcss: 8.4.21 1109 | dev: true 1110 | 1111 | /postcss-normalize-display-values/5.1.0_postcss@8.4.21: 1112 | resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} 1113 | engines: {node: ^10 || ^12 || >=14.0} 1114 | peerDependencies: 1115 | postcss: ^8.2.15 1116 | dependencies: 1117 | postcss: 8.4.21 1118 | postcss-value-parser: 4.2.0 1119 | dev: true 1120 | 1121 | /postcss-normalize-positions/5.1.1_postcss@8.4.21: 1122 | resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} 1123 | engines: {node: ^10 || ^12 || >=14.0} 1124 | peerDependencies: 1125 | postcss: ^8.2.15 1126 | dependencies: 1127 | postcss: 8.4.21 1128 | postcss-value-parser: 4.2.0 1129 | dev: true 1130 | 1131 | /postcss-normalize-repeat-style/5.1.1_postcss@8.4.21: 1132 | resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} 1133 | engines: {node: ^10 || ^12 || >=14.0} 1134 | peerDependencies: 1135 | postcss: ^8.2.15 1136 | dependencies: 1137 | postcss: 8.4.21 1138 | postcss-value-parser: 4.2.0 1139 | dev: true 1140 | 1141 | /postcss-normalize-string/5.1.0_postcss@8.4.21: 1142 | resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} 1143 | engines: {node: ^10 || ^12 || >=14.0} 1144 | peerDependencies: 1145 | postcss: ^8.2.15 1146 | dependencies: 1147 | postcss: 8.4.21 1148 | postcss-value-parser: 4.2.0 1149 | dev: true 1150 | 1151 | /postcss-normalize-timing-functions/5.1.0_postcss@8.4.21: 1152 | resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} 1153 | engines: {node: ^10 || ^12 || >=14.0} 1154 | peerDependencies: 1155 | postcss: ^8.2.15 1156 | dependencies: 1157 | postcss: 8.4.21 1158 | postcss-value-parser: 4.2.0 1159 | dev: true 1160 | 1161 | /postcss-normalize-unicode/5.1.1_postcss@8.4.21: 1162 | resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} 1163 | engines: {node: ^10 || ^12 || >=14.0} 1164 | peerDependencies: 1165 | postcss: ^8.2.15 1166 | dependencies: 1167 | browserslist: 4.21.5 1168 | postcss: 8.4.21 1169 | postcss-value-parser: 4.2.0 1170 | dev: true 1171 | 1172 | /postcss-normalize-url/5.1.0_postcss@8.4.21: 1173 | resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} 1174 | engines: {node: ^10 || ^12 || >=14.0} 1175 | peerDependencies: 1176 | postcss: ^8.2.15 1177 | dependencies: 1178 | normalize-url: 6.1.0 1179 | postcss: 8.4.21 1180 | postcss-value-parser: 4.2.0 1181 | dev: true 1182 | 1183 | /postcss-normalize-whitespace/5.1.1_postcss@8.4.21: 1184 | resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} 1185 | engines: {node: ^10 || ^12 || >=14.0} 1186 | peerDependencies: 1187 | postcss: ^8.2.15 1188 | dependencies: 1189 | postcss: 8.4.21 1190 | postcss-value-parser: 4.2.0 1191 | dev: true 1192 | 1193 | /postcss-ordered-values/5.1.3_postcss@8.4.21: 1194 | resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} 1195 | engines: {node: ^10 || ^12 || >=14.0} 1196 | peerDependencies: 1197 | postcss: ^8.2.15 1198 | dependencies: 1199 | cssnano-utils: 3.1.0_postcss@8.4.21 1200 | postcss: 8.4.21 1201 | postcss-value-parser: 4.2.0 1202 | dev: true 1203 | 1204 | /postcss-reduce-initial/5.1.2_postcss@8.4.21: 1205 | resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} 1206 | engines: {node: ^10 || ^12 || >=14.0} 1207 | peerDependencies: 1208 | postcss: ^8.2.15 1209 | dependencies: 1210 | browserslist: 4.21.5 1211 | caniuse-api: 3.0.0 1212 | postcss: 8.4.21 1213 | dev: true 1214 | 1215 | /postcss-reduce-transforms/5.1.0_postcss@8.4.21: 1216 | resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} 1217 | engines: {node: ^10 || ^12 || >=14.0} 1218 | peerDependencies: 1219 | postcss: ^8.2.15 1220 | dependencies: 1221 | postcss: 8.4.21 1222 | postcss-value-parser: 4.2.0 1223 | dev: true 1224 | 1225 | /postcss-selector-parser/6.0.11: 1226 | resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} 1227 | engines: {node: '>=4'} 1228 | dependencies: 1229 | cssesc: 3.0.0 1230 | util-deprecate: 1.0.2 1231 | dev: true 1232 | 1233 | /postcss-svgo/5.1.0_postcss@8.4.21: 1234 | resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} 1235 | engines: {node: ^10 || ^12 || >=14.0} 1236 | peerDependencies: 1237 | postcss: ^8.2.15 1238 | dependencies: 1239 | postcss: 8.4.21 1240 | postcss-value-parser: 4.2.0 1241 | svgo: 2.8.0 1242 | dev: true 1243 | 1244 | /postcss-unique-selectors/5.1.1_postcss@8.4.21: 1245 | resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} 1246 | engines: {node: ^10 || ^12 || >=14.0} 1247 | peerDependencies: 1248 | postcss: ^8.2.15 1249 | dependencies: 1250 | postcss: 8.4.21 1251 | postcss-selector-parser: 6.0.11 1252 | dev: true 1253 | 1254 | /postcss-value-parser/4.2.0: 1255 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 1256 | dev: true 1257 | 1258 | /postcss/8.4.21: 1259 | resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} 1260 | engines: {node: ^10 || ^12 || >=14} 1261 | dependencies: 1262 | nanoid: 3.3.4 1263 | picocolors: 1.0.0 1264 | source-map-js: 1.0.2 1265 | dev: true 1266 | 1267 | /promise.series/0.2.0: 1268 | resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} 1269 | engines: {node: '>=0.12'} 1270 | dev: true 1271 | 1272 | /queue-microtask/1.2.3: 1273 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1274 | dev: true 1275 | 1276 | /randombytes/2.1.0: 1277 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 1278 | dependencies: 1279 | safe-buffer: 5.2.1 1280 | dev: true 1281 | 1282 | /readdirp/3.6.0: 1283 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1284 | engines: {node: '>=8.10.0'} 1285 | dependencies: 1286 | picomatch: 2.3.1 1287 | dev: true 1288 | 1289 | /resolve-from/5.0.0: 1290 | resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 1291 | engines: {node: '>=8'} 1292 | dev: true 1293 | 1294 | /resolve/1.22.1: 1295 | resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} 1296 | hasBin: true 1297 | dependencies: 1298 | is-core-module: 2.11.0 1299 | path-parse: 1.0.7 1300 | supports-preserve-symlinks-flag: 1.0.0 1301 | dev: true 1302 | 1303 | /reusify/1.0.4: 1304 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 1305 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1306 | dev: true 1307 | 1308 | /rollup-plugin-copy/3.4.0: 1309 | resolution: {integrity: sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==} 1310 | engines: {node: '>=8.3'} 1311 | dependencies: 1312 | '@types/fs-extra': 8.1.2 1313 | colorette: 1.4.0 1314 | fs-extra: 8.1.0 1315 | globby: 10.0.1 1316 | is-plain-object: 3.0.1 1317 | dev: true 1318 | 1319 | /rollup-plugin-livereload/2.0.5: 1320 | resolution: {integrity: sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==} 1321 | engines: {node: '>=8.3'} 1322 | dependencies: 1323 | livereload: 0.9.3 1324 | transitivePeerDependencies: 1325 | - bufferutil 1326 | - utf-8-validate 1327 | dev: true 1328 | 1329 | /rollup-plugin-postcss/4.0.2_postcss@8.4.21: 1330 | resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} 1331 | engines: {node: '>=10'} 1332 | peerDependencies: 1333 | postcss: 8.x 1334 | dependencies: 1335 | chalk: 4.1.2 1336 | concat-with-sourcemaps: 1.1.0 1337 | cssnano: 5.1.15_postcss@8.4.21 1338 | import-cwd: 3.0.0 1339 | p-queue: 6.6.2 1340 | pify: 5.0.0 1341 | postcss: 8.4.21 1342 | postcss-load-config: 3.1.4_postcss@8.4.21 1343 | postcss-modules: 4.3.1_postcss@8.4.21 1344 | promise.series: 0.2.0 1345 | resolve: 1.22.1 1346 | rollup-pluginutils: 2.8.2 1347 | safe-identifier: 0.4.2 1348 | style-inject: 0.3.0 1349 | transitivePeerDependencies: 1350 | - ts-node 1351 | dev: true 1352 | 1353 | /rollup-plugin-serve/2.0.2: 1354 | resolution: {integrity: sha512-ALqyTbPhlf7FZ5RzlbDvMYvbKuCHWginJkTo6dMsbgji/a78IbsXox+pC83HENdkTRz8OXrTj+aShp3+3ratpg==} 1355 | dependencies: 1356 | mime: 3.0.0 1357 | opener: 1.5.2 1358 | dev: true 1359 | 1360 | /rollup-pluginutils/2.8.2: 1361 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 1362 | dependencies: 1363 | estree-walker: 0.6.1 1364 | dev: true 1365 | 1366 | /rollup/3.20.2: 1367 | resolution: {integrity: sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==} 1368 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 1369 | hasBin: true 1370 | optionalDependencies: 1371 | fsevents: 2.3.2 1372 | dev: true 1373 | 1374 | /run-parallel/1.2.0: 1375 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1376 | dependencies: 1377 | queue-microtask: 1.2.3 1378 | dev: true 1379 | 1380 | /safe-buffer/5.2.1: 1381 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1382 | dev: true 1383 | 1384 | /safe-identifier/0.4.2: 1385 | resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} 1386 | dev: true 1387 | 1388 | /serialize-javascript/6.0.1: 1389 | resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} 1390 | dependencies: 1391 | randombytes: 2.1.0 1392 | dev: true 1393 | 1394 | /slash/3.0.0: 1395 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 1396 | engines: {node: '>=8'} 1397 | dev: true 1398 | 1399 | /smob/0.0.6: 1400 | resolution: {integrity: sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==} 1401 | dev: true 1402 | 1403 | /source-map-js/1.0.2: 1404 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 1405 | engines: {node: '>=0.10.0'} 1406 | dev: true 1407 | 1408 | /source-map-support/0.5.21: 1409 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1410 | dependencies: 1411 | buffer-from: 1.1.2 1412 | source-map: 0.6.1 1413 | dev: true 1414 | 1415 | /source-map/0.6.1: 1416 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1417 | engines: {node: '>=0.10.0'} 1418 | dev: true 1419 | 1420 | /stable/0.1.8: 1421 | resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} 1422 | deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' 1423 | dev: true 1424 | 1425 | /string-hash/1.1.3: 1426 | resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} 1427 | dev: true 1428 | 1429 | /style-inject/0.3.0: 1430 | resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} 1431 | dev: true 1432 | 1433 | /stylehacks/5.1.1_postcss@8.4.21: 1434 | resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} 1435 | engines: {node: ^10 || ^12 || >=14.0} 1436 | peerDependencies: 1437 | postcss: ^8.2.15 1438 | dependencies: 1439 | browserslist: 4.21.5 1440 | postcss: 8.4.21 1441 | postcss-selector-parser: 6.0.11 1442 | dev: true 1443 | 1444 | /supports-color/7.2.0: 1445 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1446 | engines: {node: '>=8'} 1447 | dependencies: 1448 | has-flag: 4.0.0 1449 | dev: true 1450 | 1451 | /supports-preserve-symlinks-flag/1.0.0: 1452 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 1453 | engines: {node: '>= 0.4'} 1454 | dev: true 1455 | 1456 | /svgo/2.8.0: 1457 | resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} 1458 | engines: {node: '>=10.13.0'} 1459 | hasBin: true 1460 | dependencies: 1461 | '@trysound/sax': 0.2.0 1462 | commander: 7.2.0 1463 | css-select: 4.3.0 1464 | css-tree: 1.1.3 1465 | csso: 4.2.0 1466 | picocolors: 1.0.0 1467 | stable: 0.1.8 1468 | dev: true 1469 | 1470 | /terser/5.16.8: 1471 | resolution: {integrity: sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==} 1472 | engines: {node: '>=10'} 1473 | hasBin: true 1474 | dependencies: 1475 | '@jridgewell/source-map': 0.3.2 1476 | acorn: 8.8.2 1477 | commander: 2.20.3 1478 | source-map-support: 0.5.21 1479 | dev: true 1480 | 1481 | /to-regex-range/5.0.1: 1482 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1483 | engines: {node: '>=8.0'} 1484 | dependencies: 1485 | is-number: 7.0.0 1486 | dev: true 1487 | 1488 | /tslib/2.5.0: 1489 | resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} 1490 | dev: true 1491 | 1492 | /typescript/5.0.2: 1493 | resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} 1494 | engines: {node: '>=12.20'} 1495 | hasBin: true 1496 | dev: true 1497 | 1498 | /universalify/0.1.2: 1499 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 1500 | engines: {node: '>= 4.0.0'} 1501 | dev: true 1502 | 1503 | /update-browserslist-db/1.0.10_browserslist@4.21.5: 1504 | resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} 1505 | hasBin: true 1506 | peerDependencies: 1507 | browserslist: '>= 4.21.0' 1508 | dependencies: 1509 | browserslist: 4.21.5 1510 | escalade: 3.1.1 1511 | picocolors: 1.0.0 1512 | dev: true 1513 | 1514 | /util-deprecate/1.0.2: 1515 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1516 | dev: true 1517 | 1518 | /wrappy/1.0.2: 1519 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1520 | dev: true 1521 | 1522 | /ws/7.5.9: 1523 | resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} 1524 | engines: {node: '>=8.3.0'} 1525 | peerDependencies: 1526 | bufferutil: ^4.0.1 1527 | utf-8-validate: ^5.0.2 1528 | peerDependenciesMeta: 1529 | bufferutil: 1530 | optional: true 1531 | utf-8-validate: 1532 | optional: true 1533 | dev: true 1534 | 1535 | /yaml/1.10.2: 1536 | resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} 1537 | engines: {node: '>= 6'} 1538 | dev: true 1539 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import copy from 'rollup-plugin-copy'; 5 | import html from '@rollup/plugin-html'; 6 | import livereload from 'rollup-plugin-livereload'; 7 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 8 | import postcss from 'rollup-plugin-postcss'; 9 | import serve from 'rollup-plugin-serve'; 10 | import terser from '@rollup/plugin-terser'; 11 | import typescript from '@rollup/plugin-typescript'; 12 | 13 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 14 | const production = !process.env.ROLLUP_WATCH; 15 | const outputPath = path.resolve(__dirname, 'dist'); 16 | 17 | // Trial for WebGPU 18 | // https://developer.chrome.com/origintrials/#/view_trial/118219490218475521 19 | const originTrial = process.env.WEBGPU_ORIGIN_TRIAL || ( 20 | 'AvMV7+QuKgPxuDvjlFx3+twwSmQTXtOiBWJxkIz/C0SdqdDbaYdk6fYULy2nZgs6uu0+ymOmQnAoJDI5JKFfNAoAAABJeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJmZWF0dXJlIjoiV2ViR1BVIiwiZXhwaXJ5IjoxNjkxNzExOTk5fQ==' 21 | ); 22 | 23 | export default { 24 | input: path.join(__dirname, 'src', 'main.ts'), 25 | output: { 26 | dir: outputPath, 27 | format: 'iife', 28 | sourcemap: !production, 29 | }, 30 | plugins: [ 31 | nodeResolve({ extensions: ['.js', '.ts'] }), 32 | typescript({ sourceMap: !production, inlineSources: !production }), 33 | postcss({ extract: true, minimize: production }), 34 | html({ 35 | template: ({ files }) => ( 36 | fs.readFileSync(path.join(__dirname, 'src', 'index.html'), 'utf8') 37 | .replace('__WEBGPU_ORIGIN_TRIAL__', originTrial) 38 | .replace( 39 | '', 40 | (files.css || []) 41 | .map(({ fileName }) => ``) 42 | ) 43 | .replace( 44 | '', 45 | (files.js || []) 46 | .map(({ fileName }) => ``) 47 | ) 48 | .replace(/( |\n)/g, '') 49 | ), 50 | }), 51 | copy({ 52 | copyOnce: true, 53 | targets: [ 54 | { src: 'favicon.ico', dest: 'dist' }, 55 | ], 56 | }), 57 | ...(production ? [ 58 | terser({ format: { comments: false } }), 59 | ] : [ 60 | serve({ 61 | contentBase: outputPath, 62 | port: 8080, 63 | }), 64 | livereload({ 65 | watch: outputPath, 66 | }), 67 | ]), 68 | ], 69 | watch: { clearScreen: false }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/compute/generation/cloth.ts: -------------------------------------------------------------------------------- 1 | import { Joint, JointBuffer, Point, PointBuffers } from '../simulation/types'; 2 | 3 | export default (large: boolean = false, tension: boolean = false) => { 4 | const width = large ? 65 : 33; 5 | const height = large ? 65 : 33; 6 | const gap = 4; 7 | const points: Point[] = []; 8 | const joints: Joint[] = []; 9 | for (let i = 0, y = 0; y < height; y++) { 10 | for (let x = 0; x < width; x++, i++) { 11 | points.push({ 12 | locked: tension ? ( 13 | ((y === 0 || y === height - 1) && (x % 8 === 0)) 14 | || ((x === 0 || x === width - 1) && (y % 8 === 0)) 15 | ) : ( 16 | y === height - 1 && (x % 8 === 0) 17 | ), 18 | position: { 19 | x: (x - width * 0.5 + 0.5) * gap * 1.125 + gap * (Math.random() - 0.25) * 0.125, 20 | y: (y - height * 0.5 + 0.5) * gap + gap * (Math.random() - 0.5) * 0.125 + height * gap * 0.2, 21 | }, 22 | size: 1.5 + Math.random() * 0.5, 23 | uv: { 24 | x: (x + 0.5) / width, 25 | y: (y + 0.5) / height, 26 | }, 27 | }); 28 | if (x < width - 1) { 29 | joints.push({ 30 | enabled: true, 31 | a: i, 32 | b: i + 1, 33 | length: 0, 34 | }); 35 | } 36 | if (y > 0) { 37 | joints.push({ 38 | enabled: true, 39 | a: i, 40 | b: i - width, 41 | length: 0, 42 | }); 43 | } 44 | } 45 | } 46 | joints.forEach((joint) => { 47 | const a = points[joint.a].position; 48 | const b = points[joint.b].position; 49 | joint.length = Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); 50 | }); 51 | if (tension || large) { 52 | points.forEach(({ position }) => { 53 | if (tension) position.x *= 1.25; 54 | position.y = (position.y - height * gap * 0.2) * 1.4; 55 | }); 56 | } 57 | return { 58 | ...PointBuffers(points), 59 | joints: JointBuffer(joints), 60 | numJoints: joints.length, 61 | numPoints: points.length, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/compute/generation/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Cloth } from './cloth'; 2 | export { default as Ropes } from './ropes'; 3 | -------------------------------------------------------------------------------- /src/compute/generation/ropes.ts: -------------------------------------------------------------------------------- 1 | import { Joint, JointBuffer, Point, PointBuffers } from '../simulation/types'; 2 | 3 | export default () => { 4 | const points: Point[] = []; 5 | const joints: Joint[] = []; 6 | const length = 33; 7 | for (let j = 0, i = 0; j < 2; j++) { 8 | let o = i; 9 | let x = 64 * (j === 0 ? -1 : 1); 10 | for (i = 0; i < length; i++) { 11 | points.push({ 12 | locked: i === length - 1, 13 | position: { 14 | x, 15 | y: i * 4 - 45, 16 | }, 17 | size: 1.5 + Math.random() * 0.5, 18 | uv: { 19 | x: x / length, 20 | y: (i + 0.5) / length, 21 | }, 22 | }); 23 | if (i >= 3 && i < length - 1) { 24 | joints.push({ 25 | enabled: true, 26 | a: o + i, 27 | b: o + i + 1, 28 | length: 4, 29 | }); 30 | } 31 | } 32 | points[o + 2].position.x -= 4; 33 | points[o + 1].position.x += 4; 34 | [ 35 | [3, 2], 36 | [3, 1], 37 | [2, 1], 38 | [2, 0], 39 | [1, 0], 40 | ].forEach(([a, b]) => joints.push({ 41 | enabled: true, 42 | a: o + a, 43 | b: o + b, 44 | length: 8, 45 | })); 46 | } 47 | return { 48 | ...PointBuffers(points), 49 | joints: JointBuffer(joints), 50 | numJoints: joints.length, 51 | numPoints: points.length, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/compute/input.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix'; 2 | import Camera from '../render/camera'; 3 | 4 | class Input { 5 | private hotkeys: Record void> = {}; 6 | private readonly pointer: { 7 | id: number; 8 | button: number; 9 | normalized: vec2; 10 | position: vec2; 11 | }; 12 | 13 | constructor(target: HTMLCanvasElement) { 14 | this.pointer = { 15 | id: -1, 16 | button: 0, 17 | normalized: vec2.fromValues(-1, -1), 18 | position: vec2.create(), 19 | }; 20 | window.addEventListener('keydown', this.onKeyDown.bind(this)); 21 | target.addEventListener('pointerdown', this.onPointerDown.bind(this)); 22 | window.addEventListener('pointermove', this.onPointerMove.bind(this)); 23 | target.addEventListener('pointerup', this.onPointerUp.bind(this)); 24 | 25 | { 26 | const canvas = document.createElement('canvas'); 27 | const ctx = canvas.getContext('2d'); 28 | if (!ctx) { 29 | throw new Error("Couldn't get 2d context"); 30 | } 31 | canvas.width = 20; 32 | canvas.height = 20; 33 | ctx.lineWidth = 5; 34 | ctx.strokeStyle = '#111'; 35 | ctx.arc(canvas.width * 0.5, canvas.height * 0.5, 6, 0, Math.PI * 2); 36 | ctx.stroke(); 37 | ctx.lineWidth = 3; 38 | ctx.strokeStyle = '#eee'; 39 | ctx.stroke(); 40 | canvas.toBlob((blob) => { 41 | if (blob) { 42 | document.body.style.cursor = `url(${URL.createObjectURL(blob)}) 10 10, default`; 43 | } 44 | }); 45 | } 46 | } 47 | 48 | getPointer(camera: Camera) { 49 | const { pointer } = this; 50 | vec2.transformMat4( 51 | pointer.position, 52 | pointer.normalized, 53 | camera.getMatrixInverse() 54 | ); 55 | return pointer; 56 | } 57 | 58 | setHotkeys(hotkeys: Record void>) { 59 | this.hotkeys = hotkeys; 60 | } 61 | 62 | private onKeyDown({ key, repeat, target }: KeyboardEvent) { 63 | const { hotkeys } = this; 64 | const handler = hotkeys[key.toLowerCase()]; 65 | if ( 66 | handler 67 | && !repeat 68 | && !['input', 'textarea', 'select'].includes( 69 | (target as HTMLElement).tagName.toLowerCase() 70 | ) 71 | ) { 72 | handler(); 73 | } 74 | } 75 | 76 | private onPointerDown({ buttons, pointerId, target }: PointerEvent) { 77 | (target as HTMLCanvasElement).setPointerCapture(pointerId); 78 | const { pointer } = this; 79 | if (pointer.id !== -1) { 80 | return; 81 | } 82 | pointer.id = pointerId; 83 | pointer.button = buttons; 84 | } 85 | 86 | private onPointerMove({ pointerId, clientX, clientY }: PointerEvent) { 87 | const { pointer } = this; 88 | if (pointer.id !== -1 && pointer.id !== pointerId) { 89 | return; 90 | } 91 | vec2.set( 92 | pointer.normalized, 93 | (clientX / window.innerWidth) * 2 - 1, 94 | -(clientY / window.innerHeight) * 2 + 1 95 | ); 96 | } 97 | 98 | private onPointerUp({ pointerId, target }: PointerEvent) { 99 | (target as HTMLCanvasElement).releasePointerCapture(pointerId); 100 | const { pointer } = this; 101 | if (pointer.id !== pointerId) { 102 | return; 103 | } 104 | pointer.id = -1; 105 | pointer.button = 0; 106 | } 107 | } 108 | 109 | export default Input; 110 | -------------------------------------------------------------------------------- /src/compute/simulation/constrain.ts: -------------------------------------------------------------------------------- 1 | import { Data, Joint, Line } from './types'; 2 | 3 | const Compute = (numIterations: number, numPoints: number, numJoints: number) => /* wgsl */` 4 | ${Data} 5 | ${Joint} 6 | ${Line()} 7 | 8 | @group(0) @binding(0) var data: array; 9 | @group(0) @binding(1) var joints: array; 10 | @group(0) @binding(2) var lines: Lines; 11 | @group(1) @binding(0) var points: array, ${numPoints}>; 12 | 13 | @compute @workgroup_size(1) 14 | fn main() { 15 | lines.instanceCount = 0; 16 | for (var j: u32 = 0; j < ${numIterations}; j++) { 17 | for (var i: u32 = 0; i < ${numJoints}; i++) { 18 | var joint = joints[i]; 19 | if (joint.enabled == 0) { 20 | continue; 21 | } 22 | var origin = (points[joint.a] + points[joint.b]) * 0.5; 23 | var edge = normalize(points[joint.a] - points[joint.b]) * joint.length * 0.5; 24 | if (data[joint.a].locked == 0) { 25 | points[joint.a] = origin + edge; 26 | } 27 | if (data[joint.b].locked == 0) { 28 | points[joint.b] = origin - edge; 29 | } 30 | } 31 | } 32 | } 33 | `; 34 | 35 | class ConstrainSimulation { 36 | private readonly bindings: { 37 | data: GPUBindGroup, 38 | points: GPUBindGroup[], 39 | }; 40 | private readonly pipeline: GPUComputePipeline; 41 | 42 | constructor( 43 | device: GPUDevice, 44 | data: GPUBuffer, 45 | joints: GPUBuffer, 46 | numJoints: number, 47 | lines: GPUBuffer, 48 | points: GPUBuffer[], 49 | numPoints: number 50 | ) { 51 | this.pipeline = device.createComputePipeline({ 52 | layout: 'auto', 53 | compute: { 54 | entryPoint: 'main', 55 | module: device.createShaderModule({ 56 | code: Compute(4, numPoints, numJoints), 57 | }), 58 | }, 59 | }); 60 | this.bindings = { 61 | data: device.createBindGroup({ 62 | layout: this.pipeline.getBindGroupLayout(0), 63 | entries: [ 64 | { 65 | binding: 0, 66 | resource: { buffer: data }, 67 | }, 68 | { 69 | binding: 1, 70 | resource: { buffer: joints }, 71 | }, 72 | { 73 | binding: 2, 74 | resource: { buffer: lines }, 75 | }, 76 | ], 77 | }), 78 | points: points.map((buffer) => device.createBindGroup({ 79 | layout: this.pipeline.getBindGroupLayout(1), 80 | entries: [ 81 | { 82 | binding: 0, 83 | resource: { buffer }, 84 | }, 85 | ], 86 | })), 87 | }; 88 | } 89 | 90 | compute(pass: GPUComputePassEncoder, step: number) { 91 | const { bindings, pipeline } = this; 92 | pass.setPipeline(pipeline); 93 | pass.setBindGroup(0, bindings.data); 94 | pass.setBindGroup(1, bindings.points[step]); 95 | pass.dispatchWorkgroups(1); 96 | } 97 | } 98 | 99 | export default ConstrainSimulation; 100 | -------------------------------------------------------------------------------- /src/compute/simulation/index.ts: -------------------------------------------------------------------------------- 1 | import ConstrainSimulation from './constrain'; 2 | import ComputeLines from './lines'; 3 | import StepSimulation from './step'; 4 | import { LineBuffer, UniformsBuffer } from './types'; 5 | 6 | class Simulation { 7 | private buffers?: { 8 | data: GPUBuffer; 9 | joints: GPUBuffer; 10 | lines: GPUBuffer; 11 | points: GPUBuffer[]; 12 | }; 13 | private count: number = 0; 14 | private device: GPUDevice; 15 | private initial?: { 16 | joints: ArrayBuffer; 17 | points: ArrayBuffer; 18 | }; 19 | private pipelines?: { 20 | constraint: ConstrainSimulation, 21 | lines: ComputeLines, 22 | step: StepSimulation, 23 | }; 24 | private step: number = 0; 25 | private readonly uniforms: UniformsBuffer; 26 | 27 | constructor(device: GPUDevice) { 28 | this.device = device; 29 | this.uniforms = new UniformsBuffer(device); 30 | } 31 | 32 | compute( 33 | command: GPUCommandEncoder, 34 | delta: number, 35 | pointer: { button: number; position: [number, number] | Float32Array; }, 36 | radius: number 37 | ) { 38 | const { buffers, pipelines, step, uniforms } = this; 39 | 40 | if (!buffers || !pipelines) { 41 | return; 42 | } 43 | 44 | uniforms.delta = delta; 45 | uniforms.button = pointer.button; 46 | uniforms.pointer = pointer.position; 47 | uniforms.radius = radius; 48 | uniforms.update(); 49 | 50 | const pass = command.beginComputePass(); 51 | pipelines.step.compute(pass, step); 52 | this.step = (this.step + 1) % 2; 53 | pipelines.constraint.compute(pass, this.step); 54 | pipelines.lines.compute(pass, this.step); 55 | pass.end(); 56 | } 57 | 58 | getBuffers() { 59 | const { buffers, count, step } = this; 60 | if (!buffers) { 61 | throw new Error("Simulation is not loaded"); 62 | } 63 | return { 64 | count, 65 | data: buffers.data, 66 | lines: buffers.lines, 67 | points: buffers.points[step], 68 | }; 69 | } 70 | 71 | load( 72 | { data, joints, numJoints, points, numPoints }: { 73 | data: ArrayBuffer; 74 | joints: ArrayBuffer; 75 | numJoints: number; 76 | points: ArrayBuffer; 77 | numPoints: number; 78 | } 79 | ) { 80 | const { device } = this; 81 | const createBuffer = (data: ArrayBuffer, usage: number) => { 82 | const buffer = device.createBuffer({ 83 | mappedAtCreation: true, 84 | size: data.byteLength, 85 | usage, 86 | }); 87 | new Uint32Array(buffer.getMappedRange()).set(new Uint32Array(data)); 88 | buffer.unmap(); 89 | return buffer; 90 | }; 91 | 92 | if (this.buffers) { 93 | this.buffers.data.destroy(); 94 | this.buffers.joints.destroy(); 95 | this.buffers.lines.destroy(); 96 | this.buffers.points.forEach((buffer) => buffer.destroy()); 97 | } 98 | this.buffers = { 99 | data: createBuffer( 100 | data, 101 | GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX 102 | ), 103 | joints: createBuffer( 104 | joints, 105 | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE 106 | ), 107 | lines: LineBuffer(device, numJoints), 108 | points: Array.from({ length: 2 }, () => createBuffer( 109 | points, 110 | GPUBufferUsage.COPY_DST 111 | | GPUBufferUsage.STORAGE 112 | | GPUBufferUsage.VERTEX 113 | )), 114 | }; 115 | this.count = numPoints; 116 | this.initial = { joints, points }; 117 | this.pipelines = { 118 | constraint: new ConstrainSimulation( 119 | device, 120 | this.buffers.data, 121 | this.buffers.joints, 122 | numJoints, 123 | this.buffers.lines, 124 | this.buffers.points, 125 | numPoints 126 | ), 127 | lines: new ComputeLines( 128 | device, 129 | this.buffers.joints, 130 | numJoints, 131 | this.buffers.lines, 132 | this.buffers.points, 133 | numPoints, 134 | this.uniforms.getBuffer() 135 | ), 136 | step: new StepSimulation( 137 | device, 138 | this.buffers.data, 139 | this.buffers.points, 140 | numPoints, 141 | this.uniforms.getBuffer() 142 | ), 143 | }; 144 | } 145 | 146 | reset() { 147 | const { buffers, device, initial } = this; 148 | if (!buffers || !initial) { 149 | return; 150 | } 151 | device.queue.writeBuffer(buffers.joints, 0, initial.joints); 152 | buffers.points.forEach((buffer) => ( 153 | device.queue.writeBuffer(buffer, 0, initial.points) 154 | )); 155 | } 156 | } 157 | 158 | export default Simulation; 159 | -------------------------------------------------------------------------------- /src/compute/simulation/lines.ts: -------------------------------------------------------------------------------- 1 | import { Joint, Line, Uniforms } from './types'; 2 | 3 | const Compute = (numPoints: number, numJoints: number) => /* wgsl */` 4 | ${Joint} 5 | ${Line(true)} 6 | ${Uniforms} 7 | 8 | @group(0) @binding(0) var joints: array; 9 | @group(0) @binding(1) var lines: Lines; 10 | @group(0) @binding(2) var uniforms: Uniforms; 11 | @group(1) @binding(0) var points: array, ${numPoints}>; 12 | 13 | fn sdSegment(p: vec2, a: vec2, b: vec2) -> f32 { 14 | var pa: vec2 = p-a; 15 | var ba: vec2 = b-a; 16 | var h: f32 = clamp(dot(pa,ba)/dot(ba,ba), 0.0, 1.0); 17 | return length(pa - ba*h); 18 | } 19 | 20 | @compute @workgroup_size(256) 21 | fn main(@builtin(global_invocation_id) id: vec3) { 22 | let index: u32 = id.x; 23 | if (index >= ${numJoints}) { 24 | return; 25 | } 26 | var joint = joints[index]; 27 | if (joint.enabled == 0) { 28 | return; 29 | } 30 | if (uniforms.button == 2) { 31 | if ( 32 | sdSegment(uniforms.pointer, points[joint.a], points[joint.b]) <= uniforms.radius * 0.25 33 | ) { 34 | joints[index].enabled = 0; 35 | return; 36 | } 37 | } 38 | 39 | var origin = (points[joint.a] + points[joint.b]) * 0.5; 40 | var line = points[joint.a] - points[joint.b]; 41 | var direction = normalize(line); 42 | var rotation = atan2(direction.x, direction.y); 43 | var size = length(line); 44 | 45 | var instance = atomicAdd(&lines.instanceCount, 1); 46 | lines.data[instance].position = origin; 47 | lines.data[instance].rotation = rotation; 48 | lines.data[instance].size = size; 49 | } 50 | `; 51 | 52 | class ComputeLines { 53 | private readonly bindings: { 54 | data: GPUBindGroup, 55 | points: GPUBindGroup[], 56 | }; 57 | private readonly pipeline: GPUComputePipeline; 58 | private readonly workgroups: number; 59 | 60 | constructor( 61 | device: GPUDevice, 62 | joints: GPUBuffer, 63 | numJoints: number, 64 | lines: GPUBuffer, 65 | points: GPUBuffer[], 66 | numPoints: number, 67 | uniforms: GPUBuffer 68 | ) { 69 | this.pipeline = device.createComputePipeline({ 70 | layout: 'auto', 71 | compute: { 72 | entryPoint: 'main', 73 | module: device.createShaderModule({ 74 | code: Compute(numPoints, numJoints), 75 | }), 76 | }, 77 | }); 78 | this.bindings = { 79 | data: device.createBindGroup({ 80 | layout: this.pipeline.getBindGroupLayout(0), 81 | entries: [ 82 | { 83 | binding: 0, 84 | resource: { buffer: joints }, 85 | }, 86 | { 87 | binding: 1, 88 | resource: { buffer: lines }, 89 | }, 90 | { 91 | binding: 2, 92 | resource: { buffer: uniforms }, 93 | }, 94 | ], 95 | }), 96 | points: points.map((buffer) => device.createBindGroup({ 97 | layout: this.pipeline.getBindGroupLayout(1), 98 | entries: [ 99 | { 100 | binding: 0, 101 | resource: { buffer }, 102 | }, 103 | ], 104 | })), 105 | }; 106 | this.workgroups = Math.ceil(numJoints / 256); 107 | } 108 | 109 | compute(pass: GPUComputePassEncoder, step: number) { 110 | const { bindings, pipeline, workgroups } = this; 111 | pass.setPipeline(pipeline); 112 | pass.setBindGroup(0, bindings.data); 113 | pass.setBindGroup(1, bindings.points[step]); 114 | pass.dispatchWorkgroups(workgroups); 115 | } 116 | } 117 | 118 | export default ComputeLines; 119 | -------------------------------------------------------------------------------- /src/compute/simulation/step.ts: -------------------------------------------------------------------------------- 1 | import { Data, Uniforms } from './types'; 2 | 3 | const Compute = (numPoints: number) => /* wgsl */` 4 | ${Data} 5 | ${Uniforms} 6 | 7 | @group(0) @binding(0) var data: array; 8 | @group(0) @binding(1) var uniforms: Uniforms; 9 | @group(1) @binding(0) var input: array, ${numPoints}>; 10 | @group(1) @binding(1) var output: array, ${numPoints}>; 11 | 12 | @compute @workgroup_size(256) 13 | fn main(@builtin(global_invocation_id) id: vec3) { 14 | let index: u32 = id.x; 15 | if (index >= ${numPoints}) { 16 | return; 17 | } 18 | var point = input[index]; 19 | if (data[index].locked == 0) { 20 | point += point - output[index]; 21 | point += vec2(0, -8) * uniforms.delta; 22 | if (uniforms.button != 2) { 23 | var d = point - uniforms.pointer; 24 | if (length(d) < min(uniforms.radius * 4, 24)) { 25 | point += d * uniforms.radius * uniforms.delta; 26 | } 27 | } 28 | } 29 | output[index] = point; 30 | } 31 | `; 32 | 33 | class StepSimulation { 34 | private readonly bindings: { 35 | data: GPUBindGroup, 36 | points: GPUBindGroup[], 37 | }; 38 | private readonly pipeline: GPUComputePipeline; 39 | private readonly workgroups: number; 40 | 41 | constructor( 42 | device: GPUDevice, 43 | data: GPUBuffer, 44 | points: GPUBuffer[], 45 | numPoints: number, 46 | uniforms: GPUBuffer 47 | ) { 48 | this.pipeline = device.createComputePipeline({ 49 | layout: 'auto', 50 | compute: { 51 | entryPoint: 'main', 52 | module: device.createShaderModule({ 53 | code: Compute(numPoints), 54 | }), 55 | }, 56 | }); 57 | this.bindings = { 58 | data: device.createBindGroup({ 59 | layout: this.pipeline.getBindGroupLayout(0), 60 | entries: [ 61 | { 62 | binding: 0, 63 | resource: { buffer: data }, 64 | }, 65 | { 66 | binding: 1, 67 | resource: { buffer: uniforms }, 68 | }, 69 | ], 70 | }), 71 | points: points.map((buffer, i) => device.createBindGroup({ 72 | layout: this.pipeline.getBindGroupLayout(1), 73 | entries: [ 74 | { 75 | binding: 0, 76 | resource: { buffer }, 77 | }, 78 | { 79 | binding: 1, 80 | resource: { buffer: points[(i + 1) % 2] }, 81 | }, 82 | ], 83 | })), 84 | }; 85 | this.workgroups = Math.ceil(numPoints / 256); 86 | } 87 | 88 | compute(pass: GPUComputePassEncoder, step: number) { 89 | const { bindings, pipeline, workgroups } = this; 90 | pass.setPipeline(pipeline); 91 | pass.setBindGroup(0, bindings.data); 92 | pass.setBindGroup(1, bindings.points[step]); 93 | pass.dispatchWorkgroups(workgroups); 94 | } 95 | } 96 | 97 | export default StepSimulation; 98 | -------------------------------------------------------------------------------- /src/compute/simulation/types.ts: -------------------------------------------------------------------------------- 1 | export const Data = /* wgsl */` 2 | struct Data { 3 | locked: u32, 4 | size: f32, 5 | uv: vec2, 6 | } 7 | `; 8 | 9 | export type Point = { 10 | locked: boolean; 11 | position: { x: number; y: number; }; 12 | size: number; 13 | uv: { x: number, y: number }; 14 | }; 15 | 16 | export const PointBuffers = (values: Point[]) => { 17 | const data = new ArrayBuffer(values.length * 16); 18 | const points = new ArrayBuffer(values.length * 8); 19 | values.forEach(({ locked, position, size, uv }, i) => { 20 | const o = i * 16; 21 | new Uint32Array(data, o, 1)[0] = locked ? 1 : 0; 22 | new Float32Array(data, o + 4, 1)[0] = size; 23 | new Float32Array(data, o + 8, 2).set([uv.x, uv.y]); 24 | new Float32Array(points, i * 8, 2).set([position.x, position.y]); 25 | }); 26 | return { data, points }; 27 | }; 28 | 29 | export const Joint = /* wgsl */` 30 | struct Joint { 31 | enabled: u32, 32 | a: u32, 33 | b: u32, 34 | length: f32, 35 | } 36 | `; 37 | 38 | export type Joint = { 39 | enabled: boolean; 40 | a: number; 41 | b: number; 42 | length: number; 43 | }; 44 | 45 | export const JointBuffer = (data: Joint[]) => { 46 | const buffer = new ArrayBuffer(data.length * 16); 47 | data.forEach(({ enabled, a, b, length }, i) => { 48 | const o = i * 16; 49 | new Uint32Array(buffer, o, 1)[0] = enabled ? 1 : 0; 50 | new Uint32Array(buffer, o + 4, 1)[0] = a; 51 | new Uint32Array(buffer, o + 8, 1)[0] = b; 52 | new Float32Array(buffer, o + 12, 1)[0] = length; 53 | }); 54 | return buffer; 55 | }; 56 | 57 | export const Line = (atomicCount: boolean = false) => /* wgsl */` 58 | struct Line { 59 | position: vec2, 60 | rotation: f32, 61 | size: f32, 62 | } 63 | struct Lines { 64 | vertexCount: u32, 65 | instanceCount: ${atomicCount ? 'atomic' : 'u32'}, 66 | firstVertex: u32, 67 | firstInstance: u32, 68 | data: array, 69 | } 70 | `; 71 | 72 | export const LineBuffer = (device: GPUDevice, numJoints: number) => { 73 | const buffer = device.createBuffer({ 74 | mappedAtCreation: true, 75 | size: 16 + numJoints * 16, 76 | usage: ( 77 | GPUBufferUsage.COPY_DST 78 | | GPUBufferUsage.INDIRECT 79 | | GPUBufferUsage.STORAGE 80 | | GPUBufferUsage.VERTEX 81 | ), 82 | }); 83 | new Uint32Array(buffer.getMappedRange(0, 4)).set(new Uint32Array([6])); 84 | buffer.unmap(); 85 | return buffer; 86 | }; 87 | 88 | export const Uniforms = /* wgsl */` 89 | struct Uniforms { 90 | button: u32, 91 | delta: f32, 92 | pointer: vec2, 93 | radius: f32, 94 | } 95 | `; 96 | 97 | export class UniformsBuffer { 98 | private readonly buffers: { 99 | cpu: ArrayBuffer, 100 | gpu: GPUBuffer, 101 | }; 102 | private readonly device: GPUDevice; 103 | 104 | constructor(device: GPUDevice) { 105 | const buffer = new ArrayBuffer(24); 106 | this.buffers = { 107 | cpu: buffer, 108 | gpu: device.createBuffer({ 109 | size: buffer.byteLength, 110 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, 111 | }), 112 | }; 113 | this.device = device; 114 | } 115 | 116 | getBuffer() { 117 | return this.buffers.gpu; 118 | } 119 | 120 | set button(value: number) { 121 | new Uint32Array(this.buffers.cpu, 0, 1)[0] = value; 122 | } 123 | 124 | set delta(value: number) { 125 | new Float32Array(this.buffers.cpu, 4, 1)[0] = value; 126 | } 127 | 128 | set pointer(value: [number, number] | Float32Array) { 129 | new Float32Array(this.buffers.cpu, 8, 2).set(value); 130 | } 131 | 132 | set radius(value: number) { 133 | new Float32Array(this.buffers.cpu, 16, 1)[0] = value; 134 | } 135 | 136 | update() { 137 | this.device.queue.writeBuffer(this.buffers.gpu, 0, this.buffers.cpu); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gpucloth 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
Loading...
17 |
18 |
19 | Drag & drop an image
20 | to set it as the texture 21 |
22 | Right Click: Rip & Tear
23 | ESC: Reset simulation
24 | 1-5: Swap generation
25 | ?: Toggle this help
26 |
27 |
28 | gpucloth - view source
29 | dani@gatunes © 2023 30 |
31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-size: 16px; 3 | width: 100vw; 4 | height: 100%; 5 | } 6 | 7 | a { 8 | color: inherit; 9 | } 10 | 11 | canvas { 12 | vertical-align: middle; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | background: #000; 18 | color: #eee; 19 | cursor: default; 20 | user-select: none; 21 | overflow: hidden; 22 | font-family: 'Roboto Condensed', monospace; 23 | font-size: 0.75rem; 24 | line-height: 1.125rem; 25 | touch-action: none; 26 | -webkit-touch-callout: none; 27 | -webkit-text-size-adjust: none; 28 | overflow: hidden; 29 | } 30 | 31 | body, #app { 32 | width: 100vw; 33 | height: 100%; 34 | } 35 | 36 | #loading, #support { 37 | position: absolute; 38 | bottom: 50%; 39 | left: 50%; 40 | transform: translate(-50%, -50%); 41 | text-align: center; 42 | } 43 | 44 | #help, #info { 45 | position: absolute; 46 | bottom: 1rem; 47 | } 48 | 49 | #help { 50 | left: 1rem; 51 | } 52 | 53 | #help > div { 54 | color: #bbb; 55 | } 56 | 57 | #help > span { 58 | color: #999; 59 | } 60 | 61 | #info { 62 | right: 1rem; 63 | text-align: right; 64 | } 65 | 66 | #help.hidden, #loading.hidden, #support.hidden { 67 | display: none; 68 | } 69 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './main.css'; 2 | import Camera from './render/camera'; 3 | import Input from './compute/input'; 4 | import Lines from './render/lines'; 5 | import Points from './render/points'; 6 | import Renderer from './render/renderer'; 7 | import Simulation from './compute/simulation'; 8 | import { Cloth, Ropes } from './compute/generation'; 9 | 10 | const Main = (device: GPUDevice) => { 11 | const dom = document.getElementById('app'); 12 | if (!dom) { 13 | throw new Error("Couldn't get app DOM node"); 14 | } 15 | const camera = new Camera(device); 16 | const renderer = new Renderer(camera, device); 17 | const input = new Input(renderer.getCanvas()); 18 | const simulation = new Simulation(device); 19 | 20 | dom.appendChild(renderer.getCanvas()); 21 | renderer.setAnimationLoop((command, delta) => ( 22 | simulation.compute(command, delta, input.getPointer(camera), camera.getZoom() * 0.02) 23 | )); 24 | renderer.setSize(window.innerWidth, window.innerHeight); 25 | simulation.load(Cloth()); 26 | 27 | const lines = new Lines(camera, device, renderer.getFormat(), renderer.getSamples(), simulation); 28 | renderer.add(lines); 29 | const points = new Points(camera, device, renderer.getFormat(), renderer.getSamples(), simulation); 30 | renderer.add(points); 31 | 32 | input.setHotkeys({ 33 | 1: () => simulation.load(Cloth()), 34 | 2: () => simulation.load(Cloth(false, true)), 35 | 3: () => simulation.load(Ropes()), 36 | 4: () => simulation.load(Cloth(true, false)), 37 | 5: () => simulation.load(Cloth(true, true)), 38 | escape: () => simulation.reset(), 39 | '?': () => document.getElementById('help')?.classList.toggle('hidden'), 40 | }); 41 | window.addEventListener('drop', (e) => { 42 | e.preventDefault(); 43 | const [file] = e.dataTransfer?.files || []; 44 | if (file && file.type.indexOf('image/') === 0) { 45 | points.setTexture(file); 46 | } 47 | }); 48 | window.addEventListener('resize', () => ( 49 | renderer.setSize(window.innerWidth, window.innerHeight) 50 | )); 51 | window.addEventListener('wheel', ({ deltaY }) => ( 52 | camera.setZoom(Math.min(Math.max(camera.getZoom() * (1 + deltaY * 0.001), 200), 400)) 53 | )); 54 | }; 55 | 56 | const GPU = async () => { 57 | if (!navigator.gpu) { 58 | throw new Error("Couldn't load WebGPU"); 59 | } 60 | const adapter = await navigator.gpu.requestAdapter(); 61 | if (!adapter) { 62 | throw new Error("Couldn't load WebGPU adapter"); 63 | } 64 | const device = await adapter.requestDevice(); 65 | if (!device) { 66 | throw new Error("Couldn't load WebGPU device"); 67 | } 68 | return device; 69 | }; 70 | 71 | const prevent = (e: DragEvent | MouseEvent | TouchEvent) => e.preventDefault(); 72 | window.addEventListener('contextmenu', prevent); 73 | window.addEventListener('dragenter', prevent); 74 | window.addEventListener('dragover', prevent); 75 | window.addEventListener('touchstart', prevent); 76 | 77 | GPU() 78 | .then(Main) 79 | .catch((e) => { 80 | document.getElementById('error')!.innerText = e.message; 81 | document.getElementById('support')!.classList.remove('hidden'); 82 | }) 83 | .finally(() => document.getElementById('loading')!.classList.add('hidden')); 84 | -------------------------------------------------------------------------------- /src/render/camera.ts: -------------------------------------------------------------------------------- 1 | import { mat4, vec2 } from 'gl-matrix'; 2 | 3 | class Camera { 4 | private readonly device: GPUDevice; 5 | private readonly buffer: GPUBuffer; 6 | private aspect: number; 7 | private near: number; 8 | private far: number; 9 | private zoom: number; 10 | private readonly matrix: mat4; 11 | private readonly matrixInverse: mat4; 12 | private readonly position: vec2; 13 | 14 | constructor(device: GPUDevice) { 15 | this.aspect = 1; 16 | this.near = -100; 17 | this.far = 100; 18 | this.zoom = 200; 19 | this.position = vec2.create(); 20 | 21 | this.matrix = mat4.create(); 22 | this.matrixInverse = mat4.create(); 23 | 24 | this.device = device; 25 | this.buffer = device.createBuffer({ 26 | size: (this.matrix as Float32Array).byteLength, 27 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, 28 | }); 29 | } 30 | 31 | getBuffer() { 32 | return this.buffer; 33 | } 34 | 35 | getMatrixInverse() { 36 | return this.matrixInverse; 37 | } 38 | 39 | getZoom() { 40 | return this.zoom; 41 | } 42 | 43 | setAspect(aspect: number) { 44 | this.aspect = aspect; 45 | this.update(); 46 | } 47 | 48 | setZoom(zoom: number) { 49 | this.zoom = zoom; 50 | this.update(); 51 | } 52 | 53 | private update() { 54 | const { 55 | device, buffer, 56 | matrix, matrixInverse, 57 | aspect, near, far, zoom, position, 58 | } = this; 59 | const x = zoom * aspect * 0.5; 60 | const y = zoom * 0.5; 61 | mat4.ortho( 62 | matrix, 63 | position[0] - x, position[0] + x, 64 | position[1] - y, position[1] + y, 65 | near, far 66 | ); 67 | mat4.invert(matrixInverse, matrix); 68 | device.queue.writeBuffer(buffer, 0, matrix as Float32Array); 69 | } 70 | } 71 | 72 | export default Camera; 73 | -------------------------------------------------------------------------------- /src/render/geometry.ts: -------------------------------------------------------------------------------- 1 | export const Plane = (device: GPUDevice, width: number = 1, height: number = 1) => { 2 | const buffer = device.createBuffer({ 3 | mappedAtCreation: true, 4 | size: 24 * Float32Array.BYTES_PER_ELEMENT, 5 | usage: GPUBufferUsage.VERTEX, 6 | }); 7 | new Float32Array(buffer.getMappedRange()).set([ 8 | width * -0.5, height * 0.5, 0, 1, 9 | width * 0.5, height * 0.5, 1, 1, 10 | width * 0.5, height * -0.5, 1, 0, 11 | width * 0.5, height * -0.5, 1, 0, 12 | width * -0.5, height * -0.5, 0, 0, 13 | width * -0.5, height * 0.5, 0, 1, 14 | ]); 15 | buffer.unmap(); 16 | return buffer; 17 | }; 18 | -------------------------------------------------------------------------------- /src/render/lines.ts: -------------------------------------------------------------------------------- 1 | import Camera from './camera'; 2 | import { Plane } from './geometry'; 3 | import Simulation from '../compute/simulation'; 4 | 5 | const Vertex = /* wgsl */` 6 | struct VertexInput { 7 | @location(0) position: vec2, 8 | @location(1) uv: vec2, 9 | @location(2) iposition: vec2, 10 | @location(3) irotation: f32, 11 | @location(4) isize: f32, 12 | } 13 | struct VertexOutput { 14 | @builtin(position) position: vec4, 15 | } 16 | 17 | @group(0) @binding(0) var camera: mat4x4; 18 | 19 | fn rotate(rad: f32) -> mat2x2 { 20 | var c: f32 = cos(rad); 21 | var s: f32 = sin(rad); 22 | return mat2x2(c, s, -s, c); 23 | } 24 | 25 | @vertex 26 | fn main(vertex: VertexInput) -> VertexOutput { 27 | var out: VertexOutput; 28 | out.position = camera * vec4(vertex.position * vec2(1, vertex.isize) * rotate(vertex.irotation) + vertex.iposition, 0, 1); 29 | return out; 30 | } 31 | `; 32 | 33 | const Fragment = /* wgsl */` 34 | @fragment 35 | fn main() -> @location(0) vec4 { 36 | return vec4(vec3(0.125), 1); 37 | } 38 | `; 39 | 40 | class Lines { 41 | private readonly bindings: GPUBindGroup; 42 | private readonly geometry: GPUBuffer; 43 | private readonly pipeline: GPURenderPipeline; 44 | private readonly simulation: Simulation; 45 | 46 | constructor( 47 | camera: Camera, 48 | device: GPUDevice, 49 | format: GPUTextureFormat, 50 | samples: number, 51 | simulation: Simulation, 52 | ) { 53 | this.geometry = Plane(device); 54 | this.pipeline = device.createRenderPipeline({ 55 | layout: 'auto', 56 | vertex: { 57 | buffers: [ 58 | { 59 | arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, 60 | attributes: [ 61 | { 62 | shaderLocation: 0, 63 | offset: 0, 64 | format: 'float32x2', 65 | }, 66 | { 67 | shaderLocation: 1, 68 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 69 | format: 'float32x2', 70 | }, 71 | ], 72 | }, 73 | { 74 | arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, 75 | stepMode: 'instance', 76 | attributes: [ 77 | { 78 | shaderLocation: 2, 79 | offset: 0, 80 | format: 'float32x2', 81 | }, 82 | { 83 | shaderLocation: 3, 84 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 85 | format: 'float32', 86 | }, 87 | { 88 | shaderLocation: 4, 89 | offset: 3 * Float32Array.BYTES_PER_ELEMENT, 90 | format: 'float32', 91 | }, 92 | ], 93 | }, 94 | ], 95 | entryPoint: 'main', 96 | module: device.createShaderModule({ 97 | code: Vertex, 98 | }), 99 | }, 100 | fragment: { 101 | entryPoint: 'main', 102 | module: device.createShaderModule({ 103 | code: Fragment, 104 | }), 105 | targets: [{ format }], 106 | }, 107 | primitive: { 108 | topology: 'triangle-list', 109 | }, 110 | multisample: { 111 | count: samples, 112 | }, 113 | }); 114 | this.bindings = device.createBindGroup({ 115 | layout: this.pipeline.getBindGroupLayout(0), 116 | entries: [ 117 | { 118 | binding: 0, 119 | resource: { buffer: camera.getBuffer() }, 120 | }, 121 | ], 122 | }); 123 | this.simulation = simulation; 124 | } 125 | 126 | render(pass: GPURenderPassEncoder) { 127 | const { bindings, geometry, pipeline, simulation } = this; 128 | const { lines } = simulation.getBuffers(); 129 | pass.setPipeline(pipeline); 130 | pass.setBindGroup(0, bindings); 131 | pass.setVertexBuffer(0, geometry); 132 | pass.setVertexBuffer(1, lines, 16); 133 | pass.drawIndirect(lines, 0); 134 | } 135 | } 136 | 137 | export default Lines; 138 | -------------------------------------------------------------------------------- /src/render/points.ts: -------------------------------------------------------------------------------- 1 | import Camera from './camera'; 2 | import { Plane } from './geometry'; 3 | import Simulation from '../compute/simulation'; 4 | 5 | const Vertex = /* wgsl */` 6 | struct VertexInput { 7 | @location(0) position: vec2, 8 | @location(1) uv: vec2, 9 | @location(2) iposition: vec2, 10 | @location(3) isize: f32, 11 | @location(4) iuv: vec2, 12 | } 13 | struct VertexOutput { 14 | @builtin(position) position: vec4, 15 | @location(0) size: f32, 16 | @location(1) uv: vec2, 17 | @location(2) uv2: vec2, 18 | } 19 | 20 | @group(0) @binding(0) var camera: mat4x4; 21 | 22 | @vertex 23 | fn main(vertex: VertexInput) -> VertexOutput { 24 | var out: VertexOutput; 25 | out.position = camera * vec4(vertex.position * vertex.isize + vertex.iposition, 0, 1); 26 | out.size = vertex.isize; 27 | out.uv = (vertex.uv - 0.5) * 2; 28 | out.uv2 = vertex.iuv; 29 | return out; 30 | } 31 | `; 32 | 33 | const Fragment = /* wgsl */` 34 | struct FragmentInput { 35 | @location(0) size: f32, 36 | @location(1) uv: vec2, 37 | @location(2) uv2: vec2, 38 | } 39 | 40 | @group(0) @binding(1) var texture: texture_2d; 41 | @group(0) @binding(2) var textureSampler: sampler; 42 | 43 | fn linearTosRGB(linear: vec3) -> vec3 { 44 | if (all(linear <= vec3(0.0031308))) { 45 | return linear * 12.92; 46 | } 47 | return (pow(abs(linear), vec3(1.0/2.4)) * 1.055) - vec3(0.055); 48 | } 49 | 50 | @fragment 51 | fn main(fragment: FragmentInput) -> @location(0) vec4 { 52 | let l = min(length(fragment.uv), 1); 53 | var uv = fragment.uv2 + (fragment.uv / fragment.size / 33); 54 | return vec4(linearTosRGB( 55 | textureSample(texture, textureSampler, uv).xyz + smoothstep(0.5, 1, l) * 0.1 56 | ), 1 - smoothstep(0.8, 1, l)); 57 | } 58 | `; 59 | 60 | class Points { 61 | private readonly bindings: GPUBindGroup; 62 | private readonly device: GPUDevice; 63 | private readonly geometry: GPUBuffer; 64 | private readonly pipeline: GPURenderPipeline; 65 | private readonly simulation: Simulation; 66 | private readonly texture: GPUTexture; 67 | 68 | constructor( 69 | camera: Camera, 70 | device: GPUDevice, 71 | format: GPUTextureFormat, 72 | samples: number, 73 | simulation: Simulation, 74 | ) { 75 | this.device = device; 76 | this.geometry = Plane(device, 2, 2); 77 | this.pipeline = device.createRenderPipeline({ 78 | layout: 'auto', 79 | vertex: { 80 | buffers: [ 81 | { 82 | arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, 83 | attributes: [ 84 | { 85 | shaderLocation: 0, 86 | offset: 0, 87 | format: 'float32x2', 88 | }, 89 | { 90 | shaderLocation: 1, 91 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 92 | format: 'float32x2', 93 | }, 94 | ], 95 | }, 96 | { 97 | arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, 98 | stepMode: 'instance', 99 | attributes: [ 100 | { 101 | shaderLocation: 2, 102 | offset: 0, 103 | format: 'float32x2', 104 | }, 105 | ], 106 | }, 107 | { 108 | arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, 109 | stepMode: 'instance', 110 | attributes: [ 111 | { 112 | shaderLocation: 3, 113 | offset: 1 * Float32Array.BYTES_PER_ELEMENT, 114 | format: 'float32', 115 | }, 116 | { 117 | shaderLocation: 4, 118 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 119 | format: 'float32x2', 120 | }, 121 | ], 122 | }, 123 | ], 124 | entryPoint: 'main', 125 | module: device.createShaderModule({ 126 | code: Vertex, 127 | }), 128 | }, 129 | fragment: { 130 | entryPoint: 'main', 131 | module: device.createShaderModule({ 132 | code: Fragment, 133 | }), 134 | targets: [{ 135 | format, 136 | blend: { 137 | color: { 138 | srcFactor: 'src-alpha', 139 | dstFactor: 'one-minus-src-alpha', 140 | operation: 'add', 141 | }, 142 | alpha: { 143 | srcFactor: 'src-alpha', 144 | dstFactor: 'one-minus-src-alpha', 145 | operation: 'add', 146 | }, 147 | }, 148 | }], 149 | }, 150 | primitive: { 151 | topology: 'triangle-list', 152 | }, 153 | multisample: { 154 | count: samples, 155 | }, 156 | }); 157 | this.texture = device.createTexture({ 158 | dimension: '2d', 159 | format: 'rgba8unorm-srgb', 160 | size: [512, 512], 161 | usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 162 | }); 163 | this.bindings = device.createBindGroup({ 164 | layout: this.pipeline.getBindGroupLayout(0), 165 | entries: [ 166 | { 167 | binding: 0, 168 | resource: { buffer: camera.getBuffer() }, 169 | }, 170 | { 171 | binding: 1, 172 | resource: this.texture.createView(), 173 | }, 174 | { 175 | binding: 2, 176 | resource: device.createSampler({ minFilter: 'linear', magFilter: 'linear' }), 177 | }, 178 | ], 179 | }); 180 | this.simulation = simulation; 181 | this.generateDefaultTexture(); 182 | } 183 | 184 | render(pass: GPURenderPassEncoder) { 185 | const { bindings, geometry, pipeline, simulation } = this; 186 | const { count, data, points } = simulation.getBuffers(); 187 | pass.setPipeline(pipeline); 188 | pass.setBindGroup(0, bindings); 189 | pass.setVertexBuffer(0, geometry); 190 | pass.setVertexBuffer(1, points); 191 | pass.setVertexBuffer(2, data); 192 | pass.draw(6, count, 0, 0); 193 | } 194 | 195 | setTexture(file: Blob) { 196 | const image = new Image(); 197 | image.addEventListener('load', () => { 198 | const canvas = document.createElement('canvas'); 199 | const ctx = canvas.getContext('2d'); 200 | if (!ctx) { 201 | throw new Error("Couldn't get 2d context"); 202 | } 203 | let x = 0; 204 | let y = 0; 205 | let w = canvas.width = 512; 206 | let h = canvas.height = 512; 207 | if (image.width / image.height > w / h) { 208 | w = image.width * canvas.height / image.height; 209 | x = (canvas.width - w) * 0.5; 210 | } else { 211 | h = image.height * canvas.width / image.width; 212 | y = (canvas.height - h) * 0.5; 213 | } 214 | ctx.imageSmoothingEnabled = true; 215 | ctx.imageSmoothingQuality = 'high'; 216 | ctx.drawImage(image, 0, 0, image.width, image.height, x, y, w, h); 217 | this.updateTexture(canvas); 218 | }); 219 | image.src = URL.createObjectURL(file); 220 | } 221 | 222 | private generateDefaultTexture() { 223 | const canvas = document.createElement('canvas'); 224 | const ctx = canvas.getContext('2d'); 225 | if (!ctx) { 226 | throw new Error("Couldn't get 2d context"); 227 | } 228 | canvas.width = canvas.height = 512; 229 | for (let i = 0; i < 256; i++) { 230 | ctx.fillStyle = `hsl(${360 * Math.random()},${20 + 40 * Math.random()}%,${20 + 40 * Math.random()}%)`; 231 | ctx.beginPath(); 232 | ctx.arc(canvas.width * Math.random(), canvas.height * Math.random(), 16 + Math.random() * 64, 0, Math.PI * 2); 233 | ctx.fill(); 234 | } 235 | this.updateTexture(canvas); 236 | } 237 | 238 | private async updateTexture(canvas: HTMLCanvasElement) { 239 | const { device, texture } = this; 240 | const source = await createImageBitmap(canvas) 241 | device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, [512, 512]); 242 | } 243 | } 244 | 245 | export default Points; 246 | -------------------------------------------------------------------------------- /src/render/renderer.ts: -------------------------------------------------------------------------------- 1 | import Camera from './camera'; 2 | 3 | class Renderer { 4 | private readonly animation: { 5 | clock: number; 6 | loop: (command: GPUCommandEncoder, delta: number, time: number) => void; 7 | request: number; 8 | }; 9 | private readonly camera: Camera; 10 | private readonly canvas: HTMLCanvasElement; 11 | private readonly context: GPUCanvasContext; 12 | private readonly descriptor: GPURenderPassDescriptor; 13 | private readonly device: GPUDevice; 14 | private readonly format: GPUTextureFormat; 15 | private readonly objects: { render: (pass: GPURenderPassEncoder) => void }[]; 16 | private readonly samples: number = 4; 17 | private target: GPUTexture = undefined as unknown as GPUTexture; 18 | 19 | constructor(camera: Camera, device: GPUDevice) { 20 | this.camera = camera; 21 | this.canvas = document.createElement('canvas'); 22 | const context = this.canvas.getContext('webgpu'); 23 | if (!context) { 24 | throw new Error("Couldn't get GPUCanvasContext"); 25 | } 26 | this.context = context; 27 | this.format = navigator.gpu.getPreferredCanvasFormat(); 28 | this.context.configure({ alphaMode: 'opaque', device, format: this.format }); 29 | this.descriptor = { 30 | colorAttachments: [ 31 | { 32 | clearValue: { r: 0, g: 0, b: 0, a: 1 }, 33 | loadOp: 'clear', 34 | storeOp: 'store', 35 | view: undefined as unknown as GPUTextureView, 36 | }, 37 | ], 38 | }; 39 | this.device = device; 40 | this.objects = []; 41 | 42 | this.animate = this.animate.bind(this); 43 | this.animation = { 44 | clock: performance.now() / 1000, 45 | loop: () => {}, 46 | request: requestAnimationFrame(this.animate), 47 | }; 48 | this.visibilitychange = this.visibilitychange.bind(this); 49 | document.addEventListener('visibilitychange', this.visibilitychange); 50 | } 51 | 52 | add(object: { render: (pass: GPURenderPassEncoder) => void }) { 53 | this.objects.push(object); 54 | } 55 | 56 | getCanvas() { 57 | return this.canvas; 58 | } 59 | 60 | getFormat() { 61 | return this.format; 62 | } 63 | 64 | getSamples() { 65 | return this.samples; 66 | } 67 | 68 | setAnimationLoop(loop: (command: GPUCommandEncoder, delta: number, time: number) => void) { 69 | this.animation.loop = loop; 70 | } 71 | 72 | setSize(width: number, height: number) { 73 | const { 74 | camera, 75 | canvas, 76 | descriptor: { colorAttachments: [color] }, 77 | device, 78 | format, 79 | samples, 80 | target, 81 | } = this; 82 | const pixelRatio = window.devicePixelRatio || 1; 83 | const size = [Math.floor(width * pixelRatio), Math.floor(height * pixelRatio)]; 84 | canvas.width = size[0]; 85 | canvas.height = size[1]; 86 | canvas.style.width = `${width}px`; 87 | canvas.style.height = `${height}px`; 88 | camera.setAspect(width / height); 89 | if (target) { 90 | target.destroy(); 91 | } 92 | this.target = device.createTexture({ 93 | format, 94 | sampleCount: samples, 95 | size, 96 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 97 | }); 98 | color!.view = this.target.createView(); 99 | } 100 | 101 | private animate() { 102 | const { animation, device } = this; 103 | const time = performance.now() / 1000; 104 | const delta = Math.min(time - animation.clock, 0.1); 105 | animation.clock = time; 106 | animation.request = requestAnimationFrame(this.animate); 107 | 108 | const command = device.createCommandEncoder(); 109 | animation.loop(command, delta, time); 110 | this.render(command); 111 | device.queue.submit([command.finish()]); 112 | } 113 | 114 | private render(command: GPUCommandEncoder) { 115 | const { 116 | context, 117 | descriptor, 118 | objects, 119 | } = this; 120 | const { colorAttachments: [color] } = descriptor; 121 | color!.resolveTarget = context.getCurrentTexture().createView(); 122 | const pass = command.beginRenderPass(descriptor); 123 | objects.forEach((object) => object.render(pass)); 124 | pass.end(); 125 | } 126 | 127 | private visibilitychange() { 128 | const { animation } = this; 129 | cancelAnimationFrame(animation.request); 130 | if (document.visibilityState === 'visible') { 131 | animation.clock = performance.now() / 1000; 132 | animation.request = requestAnimationFrame(this.animate); 133 | } 134 | } 135 | } 136 | 137 | export default Renderer; 138 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitReturns": true, 10 | "noImplicitOverride": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "strict": true, 14 | "target": "esnext", 15 | "typeRoots": [ 16 | "./node_modules/@webgpu/types", 17 | "./node_modules/@types" 18 | ] 19 | }, 20 | "exclude": ["node_modules", "dist"], 21 | "include": ["src"] 22 | } 23 | --------------------------------------------------------------------------------