├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── license.md ├── package-lock.json ├── package.json ├── src ├── application │ ├── Application.ts │ ├── Camera.ts │ ├── Renderer.ts │ ├── Sound.ts │ ├── controls │ │ ├── MouseControl.ts │ │ └── mobile-control │ │ │ ├── MobileControl.ts │ │ │ └── mobileControl.css │ ├── experience │ │ ├── Experience.ts │ │ ├── active-elements │ │ │ ├── ActiveElements.ts │ │ │ └── elements │ │ │ │ ├── ActiveElement.ts │ │ │ │ ├── Github.ts │ │ │ │ ├── Linkedin.ts │ │ │ │ └── Mail.ts │ │ ├── dust │ │ │ ├── Dust.ts │ │ │ ├── dustFragmentShader.glsl │ │ │ └── dustVertexShader.glsl │ │ ├── environment │ │ │ └── Environment.ts │ │ ├── map │ │ │ └── Map.ts │ │ ├── obstacle │ │ │ ├── Obstackle.ts │ │ │ ├── Plank.ts │ │ │ └── PlanksObstacle.ts │ │ └── submarine │ │ │ ├── Bubbles.ts │ │ │ ├── Reflector.ts │ │ │ ├── Submarine.ts │ │ │ └── bubble-emiter │ │ │ ├── BubbleEmiter.ts │ │ │ ├── bubbleFragmentShader.glsl │ │ │ └── bubbleVertexShader.glsl │ ├── physic │ │ ├── PhysicApi.ts │ │ ├── types.ts │ │ └── worker │ │ │ ├── Worker.ts │ │ │ ├── tsconfig.json │ │ │ └── types.ts │ ├── resources │ │ ├── Resources.ts │ │ ├── sources.ts │ │ └── sources.types.ts │ └── utils │ │ ├── Device.ts │ │ ├── Sizes.ts │ │ └── Time.ts ├── index.html ├── main.css └── main.ts ├── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ ├── LICENSE │ └── optimer_bold.typeface.json ├── images │ ├── github-mark.svg │ ├── mouse.png │ └── social.png ├── libs │ └── draco │ │ ├── README.md │ │ ├── draco_decoder.js │ │ ├── draco_decoder.wasm │ │ ├── draco_encoder.js │ │ ├── draco_wasm_wrapper.js │ │ └── gltf │ │ ├── draco_decoder.js │ │ ├── draco_decoder.wasm │ │ ├── draco_encoder.js │ │ └── draco_wasm_wrapper.js ├── models │ ├── character.glb │ ├── map.glb │ ├── plank_1.glb │ └── submarine.glb ├── site.webmanifest ├── sounds │ ├── bigimpact.mp3 │ ├── engine.mp3 │ ├── impactmetal.mp3 │ ├── impactwave.mp3 │ ├── music.mp3 │ └── powerload.mp3 └── textures │ ├── beam.png │ ├── beam2.png │ ├── enter.png │ ├── flashlight.png │ └── light_01.png ├── tsconfig.json └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | dist 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | 25 | # Dependency directory 26 | node_modules 27 | bower_components 28 | 29 | # Editors 30 | .idea 31 | *.iml 32 | 33 | # OS metadata 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Ignore built ts files 38 | dist/**/* 39 | 40 | # ignore yarn.lock 41 | yarn.lock -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | static 2 | tsconfig.json 3 | vite.config.js 4 | **/*.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Echoes from Below - Portfolio 2 | 3 | Experience heavily inspired by [INSIDE](https://playdead.com/games/inside/) game by [Playdead](https://playdead.com) 4 | 5 | --- 6 | -- Click here to visit the website-- 7 | 8 | --- 9 | 10 | ## Technical details 11 | - 3D engine: Three.js 12 | - 3D models: Blender 13 | - Physics: Cannon-es 14 | - Animation: Tween.js 15 | - Textures: Pixelmator Pro 16 | 17 | ## Setup 18 | ```bash 19 | # Install dependencies 20 | npm install 21 | 22 | # Run development server 23 | npm run dev 24 | 25 | # Build for production 26 | npm run build 27 | ``` 28 | --- 29 |  30 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paweł Bród 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "portfolio", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@tweenjs/tween.js": "^21.0.0", 13 | "cannon-es": "^0.20.0", 14 | "dat.gui": "^0.7.9", 15 | "eventemitter3": "^5.0.1", 16 | "howler": "^2.2.4", 17 | "stats.js": "^0.17.0", 18 | "three": "^0.175.0", 19 | "vite": "^5.0.2" 20 | }, 21 | "devDependencies": { 22 | "@types/dat.gui": "^0.7.12", 23 | "@types/howler": "^2.2.11", 24 | "@types/three": "^0.158.3", 25 | "prettier": "3.1.1", 26 | "vite-plugin-glsl": "^1.2.0" 27 | } 28 | }, 29 | "node_modules/@esbuild/android-arm": { 30 | "version": "0.19.7", 31 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", 32 | "integrity": "sha512-YGSPnndkcLo4PmVl2tKatEn+0mlVMr3yEpOOT0BeMria87PhvoJb5dg5f5Ft9fbCVgtAz4pWMzZVgSEGpDAlww==", 33 | "cpu": [ 34 | "arm" 35 | ], 36 | "optional": true, 37 | "os": [ 38 | "android" 39 | ], 40 | "engines": { 41 | "node": ">=12" 42 | } 43 | }, 44 | "node_modules/@esbuild/android-arm64": { 45 | "version": "0.19.7", 46 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.7.tgz", 47 | "integrity": "sha512-YEDcw5IT7hW3sFKZBkCAQaOCJQLONVcD4bOyTXMZz5fr66pTHnAet46XAtbXAkJRfIn2YVhdC6R9g4xa27jQ1w==", 48 | "cpu": [ 49 | "arm64" 50 | ], 51 | "optional": true, 52 | "os": [ 53 | "android" 54 | ], 55 | "engines": { 56 | "node": ">=12" 57 | } 58 | }, 59 | "node_modules/@esbuild/android-x64": { 60 | "version": "0.19.7", 61 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.7.tgz", 62 | "integrity": "sha512-jhINx8DEjz68cChFvM72YzrqfwJuFbfvSxZAk4bebpngGfNNRm+zRl4rtT9oAX6N9b6gBcFaJHFew5Blf6CvUw==", 63 | "cpu": [ 64 | "x64" 65 | ], 66 | "optional": true, 67 | "os": [ 68 | "android" 69 | ], 70 | "engines": { 71 | "node": ">=12" 72 | } 73 | }, 74 | "node_modules/@esbuild/darwin-arm64": { 75 | "version": "0.19.7", 76 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.7.tgz", 77 | "integrity": "sha512-dr81gbmWN//3ZnBIm6YNCl4p3pjnabg1/ZVOgz2fJoUO1a3mq9WQ/1iuEluMs7mCL+Zwv7AY5e3g1hjXqQZ9Iw==", 78 | "cpu": [ 79 | "arm64" 80 | ], 81 | "optional": true, 82 | "os": [ 83 | "darwin" 84 | ], 85 | "engines": { 86 | "node": ">=12" 87 | } 88 | }, 89 | "node_modules/@esbuild/darwin-x64": { 90 | "version": "0.19.7", 91 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.7.tgz", 92 | "integrity": "sha512-Lc0q5HouGlzQEwLkgEKnWcSazqr9l9OdV2HhVasWJzLKeOt0PLhHaUHuzb8s/UIya38DJDoUm74GToZ6Wc7NGQ==", 93 | "cpu": [ 94 | "x64" 95 | ], 96 | "optional": true, 97 | "os": [ 98 | "darwin" 99 | ], 100 | "engines": { 101 | "node": ">=12" 102 | } 103 | }, 104 | "node_modules/@esbuild/freebsd-arm64": { 105 | "version": "0.19.7", 106 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.7.tgz", 107 | "integrity": "sha512-+y2YsUr0CxDFF7GWiegWjGtTUF6gac2zFasfFkRJPkMAuMy9O7+2EH550VlqVdpEEchWMynkdhC9ZjtnMiHImQ==", 108 | "cpu": [ 109 | "arm64" 110 | ], 111 | "optional": true, 112 | "os": [ 113 | "freebsd" 114 | ], 115 | "engines": { 116 | "node": ">=12" 117 | } 118 | }, 119 | "node_modules/@esbuild/freebsd-x64": { 120 | "version": "0.19.7", 121 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.7.tgz", 122 | "integrity": "sha512-CdXOxIbIzPJmJhrpmJTLx+o35NoiKBIgOvmvT+jeSadYiWJn0vFKsl+0bSG/5lwjNHoIDEyMYc/GAPR9jxusTA==", 123 | "cpu": [ 124 | "x64" 125 | ], 126 | "optional": true, 127 | "os": [ 128 | "freebsd" 129 | ], 130 | "engines": { 131 | "node": ">=12" 132 | } 133 | }, 134 | "node_modules/@esbuild/linux-arm": { 135 | "version": "0.19.7", 136 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.7.tgz", 137 | "integrity": "sha512-Y+SCmWxsJOdQtjcBxoacn/pGW9HDZpwsoof0ttL+2vGcHokFlfqV666JpfLCSP2xLxFpF1lj7T3Ox3sr95YXww==", 138 | "cpu": [ 139 | "arm" 140 | ], 141 | "optional": true, 142 | "os": [ 143 | "linux" 144 | ], 145 | "engines": { 146 | "node": ">=12" 147 | } 148 | }, 149 | "node_modules/@esbuild/linux-arm64": { 150 | "version": "0.19.7", 151 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.7.tgz", 152 | "integrity": "sha512-inHqdOVCkUhHNvuQPT1oCB7cWz9qQ/Cz46xmVe0b7UXcuIJU3166aqSunsqkgSGMtUCWOZw3+KMwI6otINuC9g==", 153 | "cpu": [ 154 | "arm64" 155 | ], 156 | "optional": true, 157 | "os": [ 158 | "linux" 159 | ], 160 | "engines": { 161 | "node": ">=12" 162 | } 163 | }, 164 | "node_modules/@esbuild/linux-ia32": { 165 | "version": "0.19.7", 166 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.7.tgz", 167 | "integrity": "sha512-2BbiL7nLS5ZO96bxTQkdO0euGZIUQEUXMTrqLxKUmk/Y5pmrWU84f+CMJpM8+EHaBPfFSPnomEaQiG/+Gmh61g==", 168 | "cpu": [ 169 | "ia32" 170 | ], 171 | "optional": true, 172 | "os": [ 173 | "linux" 174 | ], 175 | "engines": { 176 | "node": ">=12" 177 | } 178 | }, 179 | "node_modules/@esbuild/linux-loong64": { 180 | "version": "0.19.7", 181 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.7.tgz", 182 | "integrity": "sha512-BVFQla72KXv3yyTFCQXF7MORvpTo4uTA8FVFgmwVrqbB/4DsBFWilUm1i2Oq6zN36DOZKSVUTb16jbjedhfSHw==", 183 | "cpu": [ 184 | "loong64" 185 | ], 186 | "optional": true, 187 | "os": [ 188 | "linux" 189 | ], 190 | "engines": { 191 | "node": ">=12" 192 | } 193 | }, 194 | "node_modules/@esbuild/linux-mips64el": { 195 | "version": "0.19.7", 196 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.7.tgz", 197 | "integrity": "sha512-DzAYckIaK+pS31Q/rGpvUKu7M+5/t+jI+cdleDgUwbU7KdG2eC3SUbZHlo6Q4P1CfVKZ1lUERRFP8+q0ob9i2w==", 198 | "cpu": [ 199 | "mips64el" 200 | ], 201 | "optional": true, 202 | "os": [ 203 | "linux" 204 | ], 205 | "engines": { 206 | "node": ">=12" 207 | } 208 | }, 209 | "node_modules/@esbuild/linux-ppc64": { 210 | "version": "0.19.7", 211 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.7.tgz", 212 | "integrity": "sha512-JQ1p0SmUteNdUaaiRtyS59GkkfTW0Edo+e0O2sihnY4FoZLz5glpWUQEKMSzMhA430ctkylkS7+vn8ziuhUugQ==", 213 | "cpu": [ 214 | "ppc64" 215 | ], 216 | "optional": true, 217 | "os": [ 218 | "linux" 219 | ], 220 | "engines": { 221 | "node": ">=12" 222 | } 223 | }, 224 | "node_modules/@esbuild/linux-riscv64": { 225 | "version": "0.19.7", 226 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.7.tgz", 227 | "integrity": "sha512-xGwVJ7eGhkprY/nB7L7MXysHduqjpzUl40+XoYDGC4UPLbnG+gsyS1wQPJ9lFPcxYAaDXbdRXd1ACs9AE9lxuw==", 228 | "cpu": [ 229 | "riscv64" 230 | ], 231 | "optional": true, 232 | "os": [ 233 | "linux" 234 | ], 235 | "engines": { 236 | "node": ">=12" 237 | } 238 | }, 239 | "node_modules/@esbuild/linux-s390x": { 240 | "version": "0.19.7", 241 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.7.tgz", 242 | "integrity": "sha512-U8Rhki5PVU0L0nvk+E8FjkV8r4Lh4hVEb9duR6Zl21eIEYEwXz8RScj4LZWA2i3V70V4UHVgiqMpszXvG0Yqhg==", 243 | "cpu": [ 244 | "s390x" 245 | ], 246 | "optional": true, 247 | "os": [ 248 | "linux" 249 | ], 250 | "engines": { 251 | "node": ">=12" 252 | } 253 | }, 254 | "node_modules/@esbuild/linux-x64": { 255 | "version": "0.19.7", 256 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.7.tgz", 257 | "integrity": "sha512-ZYZopyLhm4mcoZXjFt25itRlocKlcazDVkB4AhioiL9hOWhDldU9n38g62fhOI4Pth6vp+Mrd5rFKxD0/S+7aQ==", 258 | "cpu": [ 259 | "x64" 260 | ], 261 | "optional": true, 262 | "os": [ 263 | "linux" 264 | ], 265 | "engines": { 266 | "node": ">=12" 267 | } 268 | }, 269 | "node_modules/@esbuild/netbsd-x64": { 270 | "version": "0.19.7", 271 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.7.tgz", 272 | "integrity": "sha512-/yfjlsYmT1O3cum3J6cmGG16Fd5tqKMcg5D+sBYLaOQExheAJhqr8xOAEIuLo8JYkevmjM5zFD9rVs3VBcsjtQ==", 273 | "cpu": [ 274 | "x64" 275 | ], 276 | "optional": true, 277 | "os": [ 278 | "netbsd" 279 | ], 280 | "engines": { 281 | "node": ">=12" 282 | } 283 | }, 284 | "node_modules/@esbuild/openbsd-x64": { 285 | "version": "0.19.7", 286 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.7.tgz", 287 | "integrity": "sha512-MYDFyV0EW1cTP46IgUJ38OnEY5TaXxjoDmwiTXPjezahQgZd+j3T55Ht8/Q9YXBM0+T9HJygrSRGV5QNF/YVDQ==", 288 | "cpu": [ 289 | "x64" 290 | ], 291 | "optional": true, 292 | "os": [ 293 | "openbsd" 294 | ], 295 | "engines": { 296 | "node": ">=12" 297 | } 298 | }, 299 | "node_modules/@esbuild/sunos-x64": { 300 | "version": "0.19.7", 301 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.7.tgz", 302 | "integrity": "sha512-JcPvgzf2NN/y6X3UUSqP6jSS06V0DZAV/8q0PjsZyGSXsIGcG110XsdmuWiHM+pno7/mJF6fjH5/vhUz/vA9fw==", 303 | "cpu": [ 304 | "x64" 305 | ], 306 | "optional": true, 307 | "os": [ 308 | "sunos" 309 | ], 310 | "engines": { 311 | "node": ">=12" 312 | } 313 | }, 314 | "node_modules/@esbuild/win32-arm64": { 315 | "version": "0.19.7", 316 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.7.tgz", 317 | "integrity": "sha512-ZA0KSYti5w5toax5FpmfcAgu3ZNJxYSRm0AW/Dao5up0YV1hDVof1NvwLomjEN+3/GMtaWDI+CIyJOMTRSTdMw==", 318 | "cpu": [ 319 | "arm64" 320 | ], 321 | "optional": true, 322 | "os": [ 323 | "win32" 324 | ], 325 | "engines": { 326 | "node": ">=12" 327 | } 328 | }, 329 | "node_modules/@esbuild/win32-ia32": { 330 | "version": "0.19.7", 331 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.7.tgz", 332 | "integrity": "sha512-CTOnijBKc5Jpk6/W9hQMMvJnsSYRYgveN6O75DTACCY18RA2nqka8dTZR+x/JqXCRiKk84+5+bRKXUSbbwsS0A==", 333 | "cpu": [ 334 | "ia32" 335 | ], 336 | "optional": true, 337 | "os": [ 338 | "win32" 339 | ], 340 | "engines": { 341 | "node": ">=12" 342 | } 343 | }, 344 | "node_modules/@esbuild/win32-x64": { 345 | "version": "0.19.7", 346 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.7.tgz", 347 | "integrity": "sha512-gRaP2sk6hc98N734luX4VpF318l3w+ofrtTu9j5L8EQXF+FzQKV6alCOHMVoJJHvVK/mGbwBXfOL1HETQu9IGQ==", 348 | "cpu": [ 349 | "x64" 350 | ], 351 | "optional": true, 352 | "os": [ 353 | "win32" 354 | ], 355 | "engines": { 356 | "node": ">=12" 357 | } 358 | }, 359 | "node_modules/@rollup/pluginutils": { 360 | "version": "5.0.5", 361 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", 362 | "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", 363 | "dev": true, 364 | "dependencies": { 365 | "@types/estree": "^1.0.0", 366 | "estree-walker": "^2.0.2", 367 | "picomatch": "^2.3.1" 368 | }, 369 | "engines": { 370 | "node": ">=14.0.0" 371 | }, 372 | "peerDependencies": { 373 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 374 | }, 375 | "peerDependenciesMeta": { 376 | "rollup": { 377 | "optional": true 378 | } 379 | } 380 | }, 381 | "node_modules/@rollup/rollup-android-arm-eabi": { 382 | "version": "4.5.1", 383 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.1.tgz", 384 | "integrity": "sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==", 385 | "cpu": [ 386 | "arm" 387 | ], 388 | "optional": true, 389 | "os": [ 390 | "android" 391 | ] 392 | }, 393 | "node_modules/@rollup/rollup-android-arm64": { 394 | "version": "4.5.1", 395 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.1.tgz", 396 | "integrity": "sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw==", 397 | "cpu": [ 398 | "arm64" 399 | ], 400 | "optional": true, 401 | "os": [ 402 | "android" 403 | ] 404 | }, 405 | "node_modules/@rollup/rollup-darwin-arm64": { 406 | "version": "4.5.1", 407 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.1.tgz", 408 | "integrity": "sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw==", 409 | "cpu": [ 410 | "arm64" 411 | ], 412 | "optional": true, 413 | "os": [ 414 | "darwin" 415 | ] 416 | }, 417 | "node_modules/@rollup/rollup-darwin-x64": { 418 | "version": "4.5.1", 419 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.1.tgz", 420 | "integrity": "sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw==", 421 | "cpu": [ 422 | "x64" 423 | ], 424 | "optional": true, 425 | "os": [ 426 | "darwin" 427 | ] 428 | }, 429 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 430 | "version": "4.5.1", 431 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.1.tgz", 432 | "integrity": "sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A==", 433 | "cpu": [ 434 | "arm" 435 | ], 436 | "optional": true, 437 | "os": [ 438 | "linux" 439 | ] 440 | }, 441 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 442 | "version": "4.5.1", 443 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.1.tgz", 444 | "integrity": "sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww==", 445 | "cpu": [ 446 | "arm64" 447 | ], 448 | "optional": true, 449 | "os": [ 450 | "linux" 451 | ] 452 | }, 453 | "node_modules/@rollup/rollup-linux-arm64-musl": { 454 | "version": "4.5.1", 455 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.1.tgz", 456 | "integrity": "sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw==", 457 | "cpu": [ 458 | "arm64" 459 | ], 460 | "optional": true, 461 | "os": [ 462 | "linux" 463 | ] 464 | }, 465 | "node_modules/@rollup/rollup-linux-x64-gnu": { 466 | "version": "4.5.1", 467 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.1.tgz", 468 | "integrity": "sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw==", 469 | "cpu": [ 470 | "x64" 471 | ], 472 | "optional": true, 473 | "os": [ 474 | "linux" 475 | ] 476 | }, 477 | "node_modules/@rollup/rollup-linux-x64-musl": { 478 | "version": "4.5.1", 479 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.1.tgz", 480 | "integrity": "sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ==", 481 | "cpu": [ 482 | "x64" 483 | ], 484 | "optional": true, 485 | "os": [ 486 | "linux" 487 | ] 488 | }, 489 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 490 | "version": "4.5.1", 491 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.1.tgz", 492 | "integrity": "sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ==", 493 | "cpu": [ 494 | "arm64" 495 | ], 496 | "optional": true, 497 | "os": [ 498 | "win32" 499 | ] 500 | }, 501 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 502 | "version": "4.5.1", 503 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.1.tgz", 504 | "integrity": "sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg==", 505 | "cpu": [ 506 | "ia32" 507 | ], 508 | "optional": true, 509 | "os": [ 510 | "win32" 511 | ] 512 | }, 513 | "node_modules/@rollup/rollup-win32-x64-msvc": { 514 | "version": "4.5.1", 515 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.1.tgz", 516 | "integrity": "sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q==", 517 | "cpu": [ 518 | "x64" 519 | ], 520 | "optional": true, 521 | "os": [ 522 | "win32" 523 | ] 524 | }, 525 | "node_modules/@tweenjs/tween.js": { 526 | "version": "21.0.0", 527 | "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz", 528 | "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA==" 529 | }, 530 | "node_modules/@types/dat.gui": { 531 | "version": "0.7.12", 532 | "resolved": "https://registry.npmjs.org/@types/dat.gui/-/dat.gui-0.7.12.tgz", 533 | "integrity": "sha512-el5dYeQZu2r6YW6Ft4rGtjr/dLe/uzXESMoie5UM6/weVShB1V8IRpXtTKrczd4qe7044fTKZS2l8d6EBFOkoA==", 534 | "dev": true 535 | }, 536 | "node_modules/@types/estree": { 537 | "version": "1.0.5", 538 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", 539 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", 540 | "dev": true 541 | }, 542 | "node_modules/@types/howler": { 543 | "version": "2.2.11", 544 | "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.11.tgz", 545 | "integrity": "sha512-7aBoUL6RbSIrqKnpEgfa1wSNUBK06mn08siP2QI0zYk7MXfEJAaORc4tohamQYqCqVESoDyRWSdQn2BOKWj2Qw==", 546 | "dev": true 547 | }, 548 | "node_modules/@types/stats.js": { 549 | "version": "0.17.3", 550 | "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", 551 | "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", 552 | "dev": true 553 | }, 554 | "node_modules/@types/three": { 555 | "version": "0.158.3", 556 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.158.3.tgz", 557 | "integrity": "sha512-6Qs1rUvLSbkJ4hlIe6/rdwIf61j1x2UKvGJg7s8KjswYsz1C1qDTs6voVXXB8kYaI0hgklgZgbZUupfL1l9xdA==", 558 | "dev": true, 559 | "dependencies": { 560 | "@types/stats.js": "*", 561 | "@types/webxr": "*", 562 | "fflate": "~0.6.10", 563 | "meshoptimizer": "~0.18.1" 564 | } 565 | }, 566 | "node_modules/@types/webxr": { 567 | "version": "0.5.9", 568 | "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.9.tgz", 569 | "integrity": "sha512-Oe31JXBe25ptNQeoOBqYAN8Em1AXm/WJxh4GgNFEMAv9yoa/xyMAyBw1YY6OgksNBPHJAWmRlsbSgxsLiAWM3Q==", 570 | "dev": true 571 | }, 572 | "node_modules/cannon-es": { 573 | "version": "0.20.0", 574 | "resolved": "https://registry.npmjs.org/cannon-es/-/cannon-es-0.20.0.tgz", 575 | "integrity": "sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==" 576 | }, 577 | "node_modules/dat.gui": { 578 | "version": "0.7.9", 579 | "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", 580 | "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==" 581 | }, 582 | "node_modules/esbuild": { 583 | "version": "0.19.7", 584 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.7.tgz", 585 | "integrity": "sha512-6brbTZVqxhqgbpqBR5MzErImcpA0SQdoKOkcWK/U30HtQxnokIpG3TX2r0IJqbFUzqLjhU/zC1S5ndgakObVCQ==", 586 | "hasInstallScript": true, 587 | "bin": { 588 | "esbuild": "bin/esbuild" 589 | }, 590 | "engines": { 591 | "node": ">=12" 592 | }, 593 | "optionalDependencies": { 594 | "@esbuild/android-arm": "0.19.7", 595 | "@esbuild/android-arm64": "0.19.7", 596 | "@esbuild/android-x64": "0.19.7", 597 | "@esbuild/darwin-arm64": "0.19.7", 598 | "@esbuild/darwin-x64": "0.19.7", 599 | "@esbuild/freebsd-arm64": "0.19.7", 600 | "@esbuild/freebsd-x64": "0.19.7", 601 | "@esbuild/linux-arm": "0.19.7", 602 | "@esbuild/linux-arm64": "0.19.7", 603 | "@esbuild/linux-ia32": "0.19.7", 604 | "@esbuild/linux-loong64": "0.19.7", 605 | "@esbuild/linux-mips64el": "0.19.7", 606 | "@esbuild/linux-ppc64": "0.19.7", 607 | "@esbuild/linux-riscv64": "0.19.7", 608 | "@esbuild/linux-s390x": "0.19.7", 609 | "@esbuild/linux-x64": "0.19.7", 610 | "@esbuild/netbsd-x64": "0.19.7", 611 | "@esbuild/openbsd-x64": "0.19.7", 612 | "@esbuild/sunos-x64": "0.19.7", 613 | "@esbuild/win32-arm64": "0.19.7", 614 | "@esbuild/win32-ia32": "0.19.7", 615 | "@esbuild/win32-x64": "0.19.7" 616 | } 617 | }, 618 | "node_modules/estree-walker": { 619 | "version": "2.0.2", 620 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 621 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 622 | "dev": true 623 | }, 624 | "node_modules/eventemitter3": { 625 | "version": "5.0.1", 626 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", 627 | "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" 628 | }, 629 | "node_modules/fflate": { 630 | "version": "0.6.10", 631 | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", 632 | "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", 633 | "dev": true 634 | }, 635 | "node_modules/fsevents": { 636 | "version": "2.3.3", 637 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 638 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 639 | "hasInstallScript": true, 640 | "optional": true, 641 | "os": [ 642 | "darwin" 643 | ], 644 | "engines": { 645 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 646 | } 647 | }, 648 | "node_modules/howler": { 649 | "version": "2.2.4", 650 | "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", 651 | "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==" 652 | }, 653 | "node_modules/meshoptimizer": { 654 | "version": "0.18.1", 655 | "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", 656 | "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", 657 | "dev": true 658 | }, 659 | "node_modules/nanoid": { 660 | "version": "3.3.7", 661 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 662 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 663 | "funding": [ 664 | { 665 | "type": "github", 666 | "url": "https://github.com/sponsors/ai" 667 | } 668 | ], 669 | "bin": { 670 | "nanoid": "bin/nanoid.cjs" 671 | }, 672 | "engines": { 673 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 674 | } 675 | }, 676 | "node_modules/picocolors": { 677 | "version": "1.0.0", 678 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 679 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 680 | }, 681 | "node_modules/picomatch": { 682 | "version": "2.3.1", 683 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 684 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 685 | "dev": true, 686 | "engines": { 687 | "node": ">=8.6" 688 | }, 689 | "funding": { 690 | "url": "https://github.com/sponsors/jonschlinkert" 691 | } 692 | }, 693 | "node_modules/postcss": { 694 | "version": "8.4.31", 695 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 696 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 697 | "funding": [ 698 | { 699 | "type": "opencollective", 700 | "url": "https://opencollective.com/postcss/" 701 | }, 702 | { 703 | "type": "tidelift", 704 | "url": "https://tidelift.com/funding/github/npm/postcss" 705 | }, 706 | { 707 | "type": "github", 708 | "url": "https://github.com/sponsors/ai" 709 | } 710 | ], 711 | "dependencies": { 712 | "nanoid": "^3.3.6", 713 | "picocolors": "^1.0.0", 714 | "source-map-js": "^1.0.2" 715 | }, 716 | "engines": { 717 | "node": "^10 || ^12 || >=14" 718 | } 719 | }, 720 | "node_modules/prettier": { 721 | "version": "3.1.1", 722 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", 723 | "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", 724 | "dev": true, 725 | "bin": { 726 | "prettier": "bin/prettier.cjs" 727 | }, 728 | "engines": { 729 | "node": ">=14" 730 | }, 731 | "funding": { 732 | "url": "https://github.com/prettier/prettier?sponsor=1" 733 | } 734 | }, 735 | "node_modules/rollup": { 736 | "version": "4.5.1", 737 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.1.tgz", 738 | "integrity": "sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA==", 739 | "bin": { 740 | "rollup": "dist/bin/rollup" 741 | }, 742 | "engines": { 743 | "node": ">=18.0.0", 744 | "npm": ">=8.0.0" 745 | }, 746 | "optionalDependencies": { 747 | "@rollup/rollup-android-arm-eabi": "4.5.1", 748 | "@rollup/rollup-android-arm64": "4.5.1", 749 | "@rollup/rollup-darwin-arm64": "4.5.1", 750 | "@rollup/rollup-darwin-x64": "4.5.1", 751 | "@rollup/rollup-linux-arm-gnueabihf": "4.5.1", 752 | "@rollup/rollup-linux-arm64-gnu": "4.5.1", 753 | "@rollup/rollup-linux-arm64-musl": "4.5.1", 754 | "@rollup/rollup-linux-x64-gnu": "4.5.1", 755 | "@rollup/rollup-linux-x64-musl": "4.5.1", 756 | "@rollup/rollup-win32-arm64-msvc": "4.5.1", 757 | "@rollup/rollup-win32-ia32-msvc": "4.5.1", 758 | "@rollup/rollup-win32-x64-msvc": "4.5.1", 759 | "fsevents": "~2.3.2" 760 | } 761 | }, 762 | "node_modules/source-map-js": { 763 | "version": "1.0.2", 764 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 765 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 766 | "engines": { 767 | "node": ">=0.10.0" 768 | } 769 | }, 770 | "node_modules/stats.js": { 771 | "version": "0.17.0", 772 | "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", 773 | "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" 774 | }, 775 | "node_modules/three": { 776 | "version": "0.175.0", 777 | "resolved": "https://registry.npmjs.org/three/-/three-0.175.0.tgz", 778 | "integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==", 779 | "license": "MIT" 780 | }, 781 | "node_modules/vite": { 782 | "version": "5.0.2", 783 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.2.tgz", 784 | "integrity": "sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==", 785 | "dependencies": { 786 | "esbuild": "^0.19.3", 787 | "postcss": "^8.4.31", 788 | "rollup": "^4.2.0" 789 | }, 790 | "bin": { 791 | "vite": "bin/vite.js" 792 | }, 793 | "engines": { 794 | "node": "^18.0.0 || >=20.0.0" 795 | }, 796 | "funding": { 797 | "url": "https://github.com/vitejs/vite?sponsor=1" 798 | }, 799 | "optionalDependencies": { 800 | "fsevents": "~2.3.3" 801 | }, 802 | "peerDependencies": { 803 | "@types/node": "^18.0.0 || >=20.0.0", 804 | "less": "*", 805 | "lightningcss": "^1.21.0", 806 | "sass": "*", 807 | "stylus": "*", 808 | "sugarss": "*", 809 | "terser": "^5.4.0" 810 | }, 811 | "peerDependenciesMeta": { 812 | "@types/node": { 813 | "optional": true 814 | }, 815 | "less": { 816 | "optional": true 817 | }, 818 | "lightningcss": { 819 | "optional": true 820 | }, 821 | "sass": { 822 | "optional": true 823 | }, 824 | "stylus": { 825 | "optional": true 826 | }, 827 | "sugarss": { 828 | "optional": true 829 | }, 830 | "terser": { 831 | "optional": true 832 | } 833 | } 834 | }, 835 | "node_modules/vite-plugin-glsl": { 836 | "version": "1.2.0", 837 | "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.2.0.tgz", 838 | "integrity": "sha512-geJc6oI/uaEZzRz6lkOGCXfOJBxu+crzHApPKptSkfh2bByCOHUrG9HJAENoQ09n9g5HQrlTa4vwmE8AG8dgBg==", 839 | "dev": true, 840 | "dependencies": { 841 | "@rollup/pluginutils": "^5.0.2" 842 | }, 843 | "engines": { 844 | "node": ">= 16.15.1", 845 | "npm": ">= 8.11.0" 846 | }, 847 | "peerDependencies": { 848 | "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" 849 | } 850 | } 851 | }, 852 | "dependencies": { 853 | "@esbuild/android-arm": { 854 | "version": "0.19.7", 855 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", 856 | "integrity": "sha512-YGSPnndkcLo4PmVl2tKatEn+0mlVMr3yEpOOT0BeMria87PhvoJb5dg5f5Ft9fbCVgtAz4pWMzZVgSEGpDAlww==", 857 | "optional": true 858 | }, 859 | "@esbuild/android-arm64": { 860 | "version": "0.19.7", 861 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.7.tgz", 862 | "integrity": "sha512-YEDcw5IT7hW3sFKZBkCAQaOCJQLONVcD4bOyTXMZz5fr66pTHnAet46XAtbXAkJRfIn2YVhdC6R9g4xa27jQ1w==", 863 | "optional": true 864 | }, 865 | "@esbuild/android-x64": { 866 | "version": "0.19.7", 867 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.7.tgz", 868 | "integrity": "sha512-jhINx8DEjz68cChFvM72YzrqfwJuFbfvSxZAk4bebpngGfNNRm+zRl4rtT9oAX6N9b6gBcFaJHFew5Blf6CvUw==", 869 | "optional": true 870 | }, 871 | "@esbuild/darwin-arm64": { 872 | "version": "0.19.7", 873 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.7.tgz", 874 | "integrity": "sha512-dr81gbmWN//3ZnBIm6YNCl4p3pjnabg1/ZVOgz2fJoUO1a3mq9WQ/1iuEluMs7mCL+Zwv7AY5e3g1hjXqQZ9Iw==", 875 | "optional": true 876 | }, 877 | "@esbuild/darwin-x64": { 878 | "version": "0.19.7", 879 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.7.tgz", 880 | "integrity": "sha512-Lc0q5HouGlzQEwLkgEKnWcSazqr9l9OdV2HhVasWJzLKeOt0PLhHaUHuzb8s/UIya38DJDoUm74GToZ6Wc7NGQ==", 881 | "optional": true 882 | }, 883 | "@esbuild/freebsd-arm64": { 884 | "version": "0.19.7", 885 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.7.tgz", 886 | "integrity": "sha512-+y2YsUr0CxDFF7GWiegWjGtTUF6gac2zFasfFkRJPkMAuMy9O7+2EH550VlqVdpEEchWMynkdhC9ZjtnMiHImQ==", 887 | "optional": true 888 | }, 889 | "@esbuild/freebsd-x64": { 890 | "version": "0.19.7", 891 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.7.tgz", 892 | "integrity": "sha512-CdXOxIbIzPJmJhrpmJTLx+o35NoiKBIgOvmvT+jeSadYiWJn0vFKsl+0bSG/5lwjNHoIDEyMYc/GAPR9jxusTA==", 893 | "optional": true 894 | }, 895 | "@esbuild/linux-arm": { 896 | "version": "0.19.7", 897 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.7.tgz", 898 | "integrity": "sha512-Y+SCmWxsJOdQtjcBxoacn/pGW9HDZpwsoof0ttL+2vGcHokFlfqV666JpfLCSP2xLxFpF1lj7T3Ox3sr95YXww==", 899 | "optional": true 900 | }, 901 | "@esbuild/linux-arm64": { 902 | "version": "0.19.7", 903 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.7.tgz", 904 | "integrity": "sha512-inHqdOVCkUhHNvuQPT1oCB7cWz9qQ/Cz46xmVe0b7UXcuIJU3166aqSunsqkgSGMtUCWOZw3+KMwI6otINuC9g==", 905 | "optional": true 906 | }, 907 | "@esbuild/linux-ia32": { 908 | "version": "0.19.7", 909 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.7.tgz", 910 | "integrity": "sha512-2BbiL7nLS5ZO96bxTQkdO0euGZIUQEUXMTrqLxKUmk/Y5pmrWU84f+CMJpM8+EHaBPfFSPnomEaQiG/+Gmh61g==", 911 | "optional": true 912 | }, 913 | "@esbuild/linux-loong64": { 914 | "version": "0.19.7", 915 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.7.tgz", 916 | "integrity": "sha512-BVFQla72KXv3yyTFCQXF7MORvpTo4uTA8FVFgmwVrqbB/4DsBFWilUm1i2Oq6zN36DOZKSVUTb16jbjedhfSHw==", 917 | "optional": true 918 | }, 919 | "@esbuild/linux-mips64el": { 920 | "version": "0.19.7", 921 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.7.tgz", 922 | "integrity": "sha512-DzAYckIaK+pS31Q/rGpvUKu7M+5/t+jI+cdleDgUwbU7KdG2eC3SUbZHlo6Q4P1CfVKZ1lUERRFP8+q0ob9i2w==", 923 | "optional": true 924 | }, 925 | "@esbuild/linux-ppc64": { 926 | "version": "0.19.7", 927 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.7.tgz", 928 | "integrity": "sha512-JQ1p0SmUteNdUaaiRtyS59GkkfTW0Edo+e0O2sihnY4FoZLz5glpWUQEKMSzMhA430ctkylkS7+vn8ziuhUugQ==", 929 | "optional": true 930 | }, 931 | "@esbuild/linux-riscv64": { 932 | "version": "0.19.7", 933 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.7.tgz", 934 | "integrity": "sha512-xGwVJ7eGhkprY/nB7L7MXysHduqjpzUl40+XoYDGC4UPLbnG+gsyS1wQPJ9lFPcxYAaDXbdRXd1ACs9AE9lxuw==", 935 | "optional": true 936 | }, 937 | "@esbuild/linux-s390x": { 938 | "version": "0.19.7", 939 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.7.tgz", 940 | "integrity": "sha512-U8Rhki5PVU0L0nvk+E8FjkV8r4Lh4hVEb9duR6Zl21eIEYEwXz8RScj4LZWA2i3V70V4UHVgiqMpszXvG0Yqhg==", 941 | "optional": true 942 | }, 943 | "@esbuild/linux-x64": { 944 | "version": "0.19.7", 945 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.7.tgz", 946 | "integrity": "sha512-ZYZopyLhm4mcoZXjFt25itRlocKlcazDVkB4AhioiL9hOWhDldU9n38g62fhOI4Pth6vp+Mrd5rFKxD0/S+7aQ==", 947 | "optional": true 948 | }, 949 | "@esbuild/netbsd-x64": { 950 | "version": "0.19.7", 951 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.7.tgz", 952 | "integrity": "sha512-/yfjlsYmT1O3cum3J6cmGG16Fd5tqKMcg5D+sBYLaOQExheAJhqr8xOAEIuLo8JYkevmjM5zFD9rVs3VBcsjtQ==", 953 | "optional": true 954 | }, 955 | "@esbuild/openbsd-x64": { 956 | "version": "0.19.7", 957 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.7.tgz", 958 | "integrity": "sha512-MYDFyV0EW1cTP46IgUJ38OnEY5TaXxjoDmwiTXPjezahQgZd+j3T55Ht8/Q9YXBM0+T9HJygrSRGV5QNF/YVDQ==", 959 | "optional": true 960 | }, 961 | "@esbuild/sunos-x64": { 962 | "version": "0.19.7", 963 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.7.tgz", 964 | "integrity": "sha512-JcPvgzf2NN/y6X3UUSqP6jSS06V0DZAV/8q0PjsZyGSXsIGcG110XsdmuWiHM+pno7/mJF6fjH5/vhUz/vA9fw==", 965 | "optional": true 966 | }, 967 | "@esbuild/win32-arm64": { 968 | "version": "0.19.7", 969 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.7.tgz", 970 | "integrity": "sha512-ZA0KSYti5w5toax5FpmfcAgu3ZNJxYSRm0AW/Dao5up0YV1hDVof1NvwLomjEN+3/GMtaWDI+CIyJOMTRSTdMw==", 971 | "optional": true 972 | }, 973 | "@esbuild/win32-ia32": { 974 | "version": "0.19.7", 975 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.7.tgz", 976 | "integrity": "sha512-CTOnijBKc5Jpk6/W9hQMMvJnsSYRYgveN6O75DTACCY18RA2nqka8dTZR+x/JqXCRiKk84+5+bRKXUSbbwsS0A==", 977 | "optional": true 978 | }, 979 | "@esbuild/win32-x64": { 980 | "version": "0.19.7", 981 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.7.tgz", 982 | "integrity": "sha512-gRaP2sk6hc98N734luX4VpF318l3w+ofrtTu9j5L8EQXF+FzQKV6alCOHMVoJJHvVK/mGbwBXfOL1HETQu9IGQ==", 983 | "optional": true 984 | }, 985 | "@rollup/pluginutils": { 986 | "version": "5.0.5", 987 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", 988 | "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", 989 | "dev": true, 990 | "requires": { 991 | "@types/estree": "^1.0.0", 992 | "estree-walker": "^2.0.2", 993 | "picomatch": "^2.3.1" 994 | } 995 | }, 996 | "@rollup/rollup-android-arm-eabi": { 997 | "version": "4.5.1", 998 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.1.tgz", 999 | "integrity": "sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==", 1000 | "optional": true 1001 | }, 1002 | "@rollup/rollup-android-arm64": { 1003 | "version": "4.5.1", 1004 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.1.tgz", 1005 | "integrity": "sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw==", 1006 | "optional": true 1007 | }, 1008 | "@rollup/rollup-darwin-arm64": { 1009 | "version": "4.5.1", 1010 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.1.tgz", 1011 | "integrity": "sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw==", 1012 | "optional": true 1013 | }, 1014 | "@rollup/rollup-darwin-x64": { 1015 | "version": "4.5.1", 1016 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.1.tgz", 1017 | "integrity": "sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw==", 1018 | "optional": true 1019 | }, 1020 | "@rollup/rollup-linux-arm-gnueabihf": { 1021 | "version": "4.5.1", 1022 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.1.tgz", 1023 | "integrity": "sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A==", 1024 | "optional": true 1025 | }, 1026 | "@rollup/rollup-linux-arm64-gnu": { 1027 | "version": "4.5.1", 1028 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.1.tgz", 1029 | "integrity": "sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww==", 1030 | "optional": true 1031 | }, 1032 | "@rollup/rollup-linux-arm64-musl": { 1033 | "version": "4.5.1", 1034 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.1.tgz", 1035 | "integrity": "sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw==", 1036 | "optional": true 1037 | }, 1038 | "@rollup/rollup-linux-x64-gnu": { 1039 | "version": "4.5.1", 1040 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.1.tgz", 1041 | "integrity": "sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw==", 1042 | "optional": true 1043 | }, 1044 | "@rollup/rollup-linux-x64-musl": { 1045 | "version": "4.5.1", 1046 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.1.tgz", 1047 | "integrity": "sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ==", 1048 | "optional": true 1049 | }, 1050 | "@rollup/rollup-win32-arm64-msvc": { 1051 | "version": "4.5.1", 1052 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.1.tgz", 1053 | "integrity": "sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ==", 1054 | "optional": true 1055 | }, 1056 | "@rollup/rollup-win32-ia32-msvc": { 1057 | "version": "4.5.1", 1058 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.1.tgz", 1059 | "integrity": "sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg==", 1060 | "optional": true 1061 | }, 1062 | "@rollup/rollup-win32-x64-msvc": { 1063 | "version": "4.5.1", 1064 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.1.tgz", 1065 | "integrity": "sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q==", 1066 | "optional": true 1067 | }, 1068 | "@tweenjs/tween.js": { 1069 | "version": "21.0.0", 1070 | "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz", 1071 | "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA==" 1072 | }, 1073 | "@types/dat.gui": { 1074 | "version": "0.7.12", 1075 | "resolved": "https://registry.npmjs.org/@types/dat.gui/-/dat.gui-0.7.12.tgz", 1076 | "integrity": "sha512-el5dYeQZu2r6YW6Ft4rGtjr/dLe/uzXESMoie5UM6/weVShB1V8IRpXtTKrczd4qe7044fTKZS2l8d6EBFOkoA==", 1077 | "dev": true 1078 | }, 1079 | "@types/estree": { 1080 | "version": "1.0.5", 1081 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", 1082 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", 1083 | "dev": true 1084 | }, 1085 | "@types/howler": { 1086 | "version": "2.2.11", 1087 | "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.11.tgz", 1088 | "integrity": "sha512-7aBoUL6RbSIrqKnpEgfa1wSNUBK06mn08siP2QI0zYk7MXfEJAaORc4tohamQYqCqVESoDyRWSdQn2BOKWj2Qw==", 1089 | "dev": true 1090 | }, 1091 | "@types/stats.js": { 1092 | "version": "0.17.3", 1093 | "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", 1094 | "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", 1095 | "dev": true 1096 | }, 1097 | "@types/three": { 1098 | "version": "0.158.3", 1099 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.158.3.tgz", 1100 | "integrity": "sha512-6Qs1rUvLSbkJ4hlIe6/rdwIf61j1x2UKvGJg7s8KjswYsz1C1qDTs6voVXXB8kYaI0hgklgZgbZUupfL1l9xdA==", 1101 | "dev": true, 1102 | "requires": { 1103 | "@types/stats.js": "*", 1104 | "@types/webxr": "*", 1105 | "fflate": "~0.6.10", 1106 | "meshoptimizer": "~0.18.1" 1107 | } 1108 | }, 1109 | "@types/webxr": { 1110 | "version": "0.5.9", 1111 | "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.9.tgz", 1112 | "integrity": "sha512-Oe31JXBe25ptNQeoOBqYAN8Em1AXm/WJxh4GgNFEMAv9yoa/xyMAyBw1YY6OgksNBPHJAWmRlsbSgxsLiAWM3Q==", 1113 | "dev": true 1114 | }, 1115 | "cannon-es": { 1116 | "version": "0.20.0", 1117 | "resolved": "https://registry.npmjs.org/cannon-es/-/cannon-es-0.20.0.tgz", 1118 | "integrity": "sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==" 1119 | }, 1120 | "dat.gui": { 1121 | "version": "0.7.9", 1122 | "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", 1123 | "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==" 1124 | }, 1125 | "esbuild": { 1126 | "version": "0.19.7", 1127 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.7.tgz", 1128 | "integrity": "sha512-6brbTZVqxhqgbpqBR5MzErImcpA0SQdoKOkcWK/U30HtQxnokIpG3TX2r0IJqbFUzqLjhU/zC1S5ndgakObVCQ==", 1129 | "requires": { 1130 | "@esbuild/android-arm": "0.19.7", 1131 | "@esbuild/android-arm64": "0.19.7", 1132 | "@esbuild/android-x64": "0.19.7", 1133 | "@esbuild/darwin-arm64": "0.19.7", 1134 | "@esbuild/darwin-x64": "0.19.7", 1135 | "@esbuild/freebsd-arm64": "0.19.7", 1136 | "@esbuild/freebsd-x64": "0.19.7", 1137 | "@esbuild/linux-arm": "0.19.7", 1138 | "@esbuild/linux-arm64": "0.19.7", 1139 | "@esbuild/linux-ia32": "0.19.7", 1140 | "@esbuild/linux-loong64": "0.19.7", 1141 | "@esbuild/linux-mips64el": "0.19.7", 1142 | "@esbuild/linux-ppc64": "0.19.7", 1143 | "@esbuild/linux-riscv64": "0.19.7", 1144 | "@esbuild/linux-s390x": "0.19.7", 1145 | "@esbuild/linux-x64": "0.19.7", 1146 | "@esbuild/netbsd-x64": "0.19.7", 1147 | "@esbuild/openbsd-x64": "0.19.7", 1148 | "@esbuild/sunos-x64": "0.19.7", 1149 | "@esbuild/win32-arm64": "0.19.7", 1150 | "@esbuild/win32-ia32": "0.19.7", 1151 | "@esbuild/win32-x64": "0.19.7" 1152 | } 1153 | }, 1154 | "estree-walker": { 1155 | "version": "2.0.2", 1156 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 1157 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 1158 | "dev": true 1159 | }, 1160 | "eventemitter3": { 1161 | "version": "5.0.1", 1162 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", 1163 | "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" 1164 | }, 1165 | "fflate": { 1166 | "version": "0.6.10", 1167 | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", 1168 | "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", 1169 | "dev": true 1170 | }, 1171 | "fsevents": { 1172 | "version": "2.3.3", 1173 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1174 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1175 | "optional": true 1176 | }, 1177 | "howler": { 1178 | "version": "2.2.4", 1179 | "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", 1180 | "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==" 1181 | }, 1182 | "meshoptimizer": { 1183 | "version": "0.18.1", 1184 | "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", 1185 | "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", 1186 | "dev": true 1187 | }, 1188 | "nanoid": { 1189 | "version": "3.3.7", 1190 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 1191 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" 1192 | }, 1193 | "picocolors": { 1194 | "version": "1.0.0", 1195 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 1196 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 1197 | }, 1198 | "picomatch": { 1199 | "version": "2.3.1", 1200 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1201 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1202 | "dev": true 1203 | }, 1204 | "postcss": { 1205 | "version": "8.4.31", 1206 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 1207 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 1208 | "requires": { 1209 | "nanoid": "^3.3.6", 1210 | "picocolors": "^1.0.0", 1211 | "source-map-js": "^1.0.2" 1212 | } 1213 | }, 1214 | "prettier": { 1215 | "version": "3.1.1", 1216 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", 1217 | "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", 1218 | "dev": true 1219 | }, 1220 | "rollup": { 1221 | "version": "4.5.1", 1222 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.1.tgz", 1223 | "integrity": "sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA==", 1224 | "requires": { 1225 | "@rollup/rollup-android-arm-eabi": "4.5.1", 1226 | "@rollup/rollup-android-arm64": "4.5.1", 1227 | "@rollup/rollup-darwin-arm64": "4.5.1", 1228 | "@rollup/rollup-darwin-x64": "4.5.1", 1229 | "@rollup/rollup-linux-arm-gnueabihf": "4.5.1", 1230 | "@rollup/rollup-linux-arm64-gnu": "4.5.1", 1231 | "@rollup/rollup-linux-arm64-musl": "4.5.1", 1232 | "@rollup/rollup-linux-x64-gnu": "4.5.1", 1233 | "@rollup/rollup-linux-x64-musl": "4.5.1", 1234 | "@rollup/rollup-win32-arm64-msvc": "4.5.1", 1235 | "@rollup/rollup-win32-ia32-msvc": "4.5.1", 1236 | "@rollup/rollup-win32-x64-msvc": "4.5.1", 1237 | "fsevents": "~2.3.2" 1238 | } 1239 | }, 1240 | "source-map-js": { 1241 | "version": "1.0.2", 1242 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 1243 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 1244 | }, 1245 | "stats.js": { 1246 | "version": "0.17.0", 1247 | "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", 1248 | "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" 1249 | }, 1250 | "three": { 1251 | "version": "0.175.0", 1252 | "resolved": "https://registry.npmjs.org/three/-/three-0.175.0.tgz", 1253 | "integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==" 1254 | }, 1255 | "vite": { 1256 | "version": "5.0.2", 1257 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.2.tgz", 1258 | "integrity": "sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==", 1259 | "requires": { 1260 | "esbuild": "^0.19.3", 1261 | "fsevents": "~2.3.3", 1262 | "postcss": "^8.4.31", 1263 | "rollup": "^4.2.0" 1264 | } 1265 | }, 1266 | "vite-plugin-glsl": { 1267 | "version": "1.2.0", 1268 | "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.2.0.tgz", 1269 | "integrity": "sha512-geJc6oI/uaEZzRz6lkOGCXfOJBxu+crzHApPKptSkfh2bByCOHUrG9HJAENoQ09n9g5HQrlTa4vwmE8AG8dgBg==", 1270 | "dev": true, 1271 | "requires": { 1272 | "@rollup/pluginutils": "^5.0.2" 1273 | } 1274 | } 1275 | } 1276 | } 1277 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "pritier": "prettier --write ." 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@tweenjs/tween.js": "^21.0.0", 18 | "cannon-es": "^0.20.0", 19 | "dat.gui": "^0.7.9", 20 | "eventemitter3": "^5.0.1", 21 | "howler": "^2.2.4", 22 | "stats.js": "^0.17.0", 23 | "three": "^0.160.0", 24 | "vite": "^5.0.2" 25 | }, 26 | "devDependencies": { 27 | "@types/dat.gui": "^0.7.12", 28 | "@types/howler": "^2.2.11", 29 | "@types/three": "^0.158.3", 30 | "prettier": "3.1.1", 31 | "vite-plugin-glsl": "^1.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/application/Application.ts: -------------------------------------------------------------------------------- 1 | import { MouseControl } from "./controls/MouseControl"; 2 | import { Camera } from "./Camera"; 3 | import { Renderer } from "./Renderer"; 4 | import { Scene } from "three"; 5 | import { Sizes } from "./utils/Sizes"; 6 | import { Time } from "./utils/Time"; 7 | import * as dat from "dat.gui"; 8 | import Stats from "stats.js"; 9 | import { Sound } from "./Sound"; 10 | 11 | import { Experience } from "./experience/Experience"; 12 | import { MobileControl } from "./controls/mobile-control/MobileControl"; 13 | import Tween from "@tweenjs/tween.js"; 14 | import { PhysicApi } from "./physic/PhysicApi.js"; 15 | 16 | export class Application { 17 | mouseControl: MouseControl; 18 | mobileControl: MobileControl; 19 | scene: Scene; 20 | physicApi: PhysicApi; 21 | sizes: Sizes; 22 | camera: Camera; 23 | renderer: Renderer; 24 | experience: Experience; 25 | time: Time; 26 | sound: Sound; 27 | debug?: dat.GUI; 28 | stats?: Stats; 29 | 30 | constructor() { 31 | if (location.hash === "#debug") { 32 | this.setDebug(); 33 | } 34 | 35 | this.sizes = new Sizes(); 36 | this.time = new Time(); 37 | this.scene = new Scene(); 38 | this.camera = new Camera(this); 39 | this.renderer = new Renderer(this); 40 | this.physicApi = new PhysicApi(); 41 | this.sound = new Sound(this); 42 | this.mouseControl = new MouseControl(this); 43 | this.mobileControl = new MobileControl(this); 44 | this.experience = new Experience(this); 45 | 46 | this.adjustFOV(); 47 | this.time.on("tick", this.update); 48 | this.sizes.on("resize", this.resize); 49 | } 50 | 51 | private adjustFOV = () => { 52 | if (this.sizes.aspectRatio < 1) { 53 | this.camera.instance.fov = 80; 54 | this.camera.instance.updateProjectionMatrix(); 55 | } else { 56 | this.camera.instance.fov = this.camera.defaultFOV; 57 | this.camera.instance.updateProjectionMatrix(); 58 | } 59 | }; 60 | 61 | setDebug() { 62 | this.stats = new Stats(); 63 | this.stats.showPanel(0); 64 | 65 | document.body.appendChild(this.stats.dom); 66 | 67 | this.debug = new dat.GUI({ width: 280 }); 68 | 69 | this.debug.domElement.onmouseenter = () => { 70 | this.mouseControl.disable(); 71 | }; 72 | this.debug.domElement.onmouseleave = () => { 73 | this.mouseControl.enable(); 74 | }; 75 | 76 | this.debug.close(); 77 | } 78 | 79 | start() { 80 | this.time.start(); 81 | this.renderer.renderer.compile(this.scene, this.camera.instance); 82 | } 83 | 84 | experienceStart(enableTouchInterface = false) { 85 | this.experience.start(); 86 | this.sound.init(); 87 | 88 | if (enableTouchInterface) { 89 | this.mobileControl.enable(); 90 | } 91 | } 92 | 93 | resize = () => { 94 | this.camera.resize(); 95 | this.renderer.resize(); 96 | this.adjustFOV(); 97 | }; 98 | 99 | update = () => { 100 | this.stats?.begin(); 101 | Tween.update(); 102 | this.mouseControl.updateRaycaster(); 103 | this.experience.update(); 104 | this.renderer.update(); 105 | this.stats?.end(); 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /src/application/Camera.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "./Application"; 2 | import { Vector3, Frustum, Matrix4, PerspectiveCamera } from "three"; 3 | 4 | export class Camera { 5 | readonly instance: PerspectiveCamera; 6 | readonly defaultFOV: number = 50; 7 | readonly defaultZ: number = 13; 8 | 9 | private frustum = new Frustum(); 10 | private projectionMatrix = new Matrix4(); 11 | constructor(private application: Application) { 12 | this.instance = new PerspectiveCamera( 13 | this.defaultFOV, 14 | this.application.sizes.aspectRatio, 15 | 0.1, 16 | 55, 17 | ); 18 | this.instance.position.z = this.defaultZ; 19 | this.instance.position.y = 3; 20 | this.instance.position.x = 0; 21 | 22 | this.setDebug(); 23 | } 24 | 25 | resize() { 26 | this.instance.aspect = this.application.sizes.aspectRatio; 27 | this.instance.updateProjectionMatrix(); 28 | } 29 | 30 | checkIfInFrustum(point: Vector3) { 31 | this.frustum.setFromProjectionMatrix( 32 | this.projectionMatrix.multiplyMatrices( 33 | this.instance.projectionMatrix, 34 | this.instance.matrixWorldInverse, 35 | ), 36 | ); 37 | return this.frustum.containsPoint(point); 38 | } 39 | 40 | reset() { 41 | this.instance.position.x = 0; 42 | this.instance.position.y = 3; 43 | this.instance.position.z = this.defaultZ; 44 | this.instance.rotation.x = 0; 45 | this.instance.rotation.y = 0; 46 | this.instance.rotation.z = 0; 47 | this.instance.fov = this.defaultFOV; 48 | this.instance.updateProjectionMatrix(); 49 | } 50 | 51 | setDebug() { 52 | if (this.application.debug) { 53 | const folder = this.application.debug.addFolder("Camera"); 54 | 55 | folder.open(); 56 | 57 | folder.add(this.instance.position, "z", 5, 25).name("camera z"); 58 | 59 | folder 60 | .add(this.instance.rotation, "x", -Math.PI, Math.PI) 61 | .name("camera rotation x"); 62 | 63 | folder 64 | .add(this.instance.rotation, "y", -Math.PI, Math.PI) 65 | .name("camera rotation y"); 66 | 67 | folder 68 | .add(this.instance.rotation, "z", -Math.PI, Math.PI) 69 | .name("camera rotation z"); 70 | 71 | folder 72 | .add(this.instance, "fov", 0, 180) 73 | .name("camera fov") 74 | .onChange(() => { 75 | this.instance.updateProjectionMatrix(); 76 | }); 77 | 78 | folder.add({ reset: () => this.reset() }, "reset"); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/application/Renderer.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "./Application"; 2 | import { Device } from "./utils/Device"; 3 | import { PCFSoftShadowMap, SRGBColorSpace, WebGLRenderer } from "three"; 4 | import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; 5 | import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"; 6 | import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass"; 7 | import { FilmPass } from "three/examples/jsm/postprocessing/FilmPass"; 8 | 9 | export class Renderer { 10 | readonly renderer: WebGLRenderer; 11 | private readonly effectComposer: EffectComposer; 12 | 13 | constructor(private application: Application) { 14 | this.renderer = new WebGLRenderer({ 15 | canvas: document.querySelector("canvas#canvas")!, 16 | antialias: false, 17 | depth: true, 18 | alpha: true, 19 | stencil: true, 20 | powerPreference: "high-performance", 21 | }); 22 | 23 | this.renderer.autoClear = false; 24 | this.renderer.outputColorSpace = SRGBColorSpace; 25 | this.renderer.shadowMap.enabled = false; 26 | this.renderer.shadowMap.type = PCFSoftShadowMap; 27 | this.renderer.setSize( 28 | this.application.sizes.width, 29 | this.application.sizes.height, 30 | ); 31 | this.renderer.setPixelRatio(this.application.sizes.allowedPixelRatio); 32 | this.renderer.setClearColor(0x000000, 0); 33 | this.renderer.debug.checkShaderErrors = false; 34 | 35 | this.effectComposer = new EffectComposer(this.renderer); 36 | this.effectComposer.setSize( 37 | this.application.sizes.width, 38 | this.application.sizes.height, 39 | ); 40 | this.effectComposer.setPixelRatio(this.application.sizes.allowedPixelRatio); 41 | 42 | const renderPass = new RenderPass( 43 | this.application.scene, 44 | this.application.camera.instance, 45 | ); 46 | this.effectComposer.addPass(renderPass); 47 | 48 | if (!Device.isAndroid()) { 49 | const filmPass = new FilmPass(0.9); 50 | this.effectComposer.addPass(filmPass); 51 | } 52 | 53 | const outputPass = new OutputPass(); 54 | this.effectComposer.addPass(outputPass); 55 | } 56 | 57 | resize() { 58 | this.renderer.setSize( 59 | this.application.sizes.width, 60 | this.application.sizes.height, 61 | ); 62 | this.renderer.setPixelRatio(this.application.sizes.allowedPixelRatio); 63 | 64 | this.effectComposer.setSize( 65 | this.application.sizes.width, 66 | this.application.sizes.height, 67 | ); 68 | this.effectComposer.setPixelRatio(this.application.sizes.allowedPixelRatio); 69 | } 70 | 71 | update() { 72 | this.effectComposer.render(this.application.time.getDeltaElapsedTime()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/application/Sound.ts: -------------------------------------------------------------------------------- 1 | import { Howler } from "howler"; 2 | import { Application } from "./Application"; 3 | import { resources } from "./resources/Resources"; 4 | 5 | export class Sound { 6 | private isMuted = false; 7 | private audioButton = document.getElementById("audio"); 8 | 9 | constructor(private application: Application) { 10 | this.toggleMute(); 11 | } 12 | 13 | init() { 14 | this.sounds.music.loop(true); 15 | this.sounds.music.volume(0.4); 16 | this.sounds.music.play(); 17 | 18 | this.sounds.engine.loop(true); 19 | this.sounds.engine.volume(0); 20 | this.sounds.engine.play(); 21 | 22 | this.sounds.powerload.loop(true); 23 | this.sounds.powerload.volume(0); 24 | this.sounds.powerload.play(); 25 | 26 | this.sounds.impactwave.loop(false); 27 | this.sounds.impactwave.volume(0.5); 28 | 29 | this.sounds.impactmetal.loop(false); 30 | this.sounds.impactmetal.volume(1); 31 | 32 | this.sounds.bigimpact.loop(false); 33 | this.sounds.bigimpact.volume(0.5); 34 | 35 | this.initMuteButton(); 36 | this.toggleMute(); 37 | } 38 | 39 | sounds = { 40 | music: resources.getAudio("music"), 41 | engine: resources.getAudio("engine"), 42 | impactwave: resources.getAudio("impactwave"), 43 | powerload: resources.getAudio("powerload"), 44 | impactmetal: resources.getAudio("impactmetal"), 45 | bigimpact: resources.getAudio("bigimpact"), 46 | }; 47 | 48 | initMuteButton() { 49 | if (this.audioButton) { 50 | this.audioButton.classList.add("visible"); 51 | this.audioButton.addEventListener("pointerup", () => { 52 | this.toggleMute(); 53 | }); 54 | } 55 | } 56 | 57 | toggleMute() { 58 | this.isMuted = !this.isMuted; 59 | Howler.mute(this.isMuted); 60 | 61 | if (this.audioButton) { 62 | if (this.isMuted) { 63 | this.audioButton.classList.add("muted"); 64 | } else { 65 | this.audioButton.classList.remove("muted"); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/application/controls/MouseControl.ts: -------------------------------------------------------------------------------- 1 | import { Plane, Raycaster, Vector2, Vector3 } from "three"; 2 | import { Application } from "../Application"; 3 | import EventEmitter from "eventemitter3"; 4 | 5 | const MOUSE_RIGHT = 2; 6 | const MOUSE_LEFT = 0; 7 | const MOUSE_MIDDLE = 1; 8 | 9 | export class MouseControl extends EventEmitter { 10 | private readonly screenPosition: Vector2; 11 | private readonly scrollDelta: Vector2; 12 | private readonly raycaster: Raycaster; 13 | private readonly castPosition: Vector3; 14 | private readonly castPlane: Plane; 15 | private readonly percentagePosition: Vector2; 16 | 17 | private isEnabled: boolean = true; 18 | private button: number = -1; 19 | 20 | constructor(private application: Application) { 21 | super(); 22 | this.screenPosition = new Vector2(); 23 | this.scrollDelta = new Vector2(); 24 | this.castPosition = new Vector3(); 25 | this.raycaster = new Raycaster(); 26 | this.percentagePosition = new Vector2(); 27 | this.castPlane = new Plane(new Vector3(0, 0, 1), 0); 28 | 29 | document.addEventListener("mousemove", (event) => { 30 | window.requestAnimationFrame(() => { 31 | this.screenPosition.x = event.clientX; 32 | this.screenPosition.y = event.clientY; 33 | 34 | this.emit("move", event); 35 | }); 36 | }); 37 | 38 | document.addEventListener("mousedown", (event) => { 39 | if (!this.isEnabled) { 40 | return; 41 | } 42 | 43 | this.button = event.button; 44 | 45 | if (this.button === MOUSE_LEFT) { 46 | this.emit("leftDown", event); 47 | } else if (this.button === MOUSE_RIGHT) { 48 | this.emit("rightDown", event); 49 | } else if (this.button === MOUSE_MIDDLE) { 50 | this.emit("middleDown", event); 51 | } 52 | }); 53 | 54 | document.addEventListener("mouseup", (event) => { 55 | if (this.button === MOUSE_LEFT) { 56 | this.emit("leftUp", event); 57 | } else if (this.button === MOUSE_RIGHT) { 58 | this.emit("rightUp", event); 59 | } else if (this.button === MOUSE_MIDDLE) { 60 | this.emit("middleUp", event); 61 | } 62 | 63 | this.button = -1; 64 | }); 65 | 66 | document.addEventListener("contextmenu", (event) => { 67 | event.preventDefault(); 68 | }); 69 | 70 | document.addEventListener( 71 | "wheel", 72 | (event) => { 73 | event.preventDefault(); 74 | 75 | this.scrollDelta.x = event.deltaX; 76 | this.scrollDelta.y = event.deltaY; 77 | 78 | if (event.deltaX == 1 || event.deltaX == -1) { 79 | this.scrollDelta.x = 0; 80 | } 81 | 82 | if (event.deltaY == 1 || event.deltaY == -1) { 83 | this.scrollDelta.y = 0; 84 | } 85 | }, 86 | { passive: false }, 87 | ); 88 | } 89 | 90 | updateRaycaster() { 91 | this.raycaster.setFromCamera( 92 | this.getNDCPosition(), 93 | this.application.camera.instance, 94 | ); 95 | } 96 | 97 | getPercentagePosition(): Vector2 { 98 | this.percentagePosition.set( 99 | this.screenPosition.x / window.innerWidth, 100 | 1 - this.screenPosition.y / window.innerHeight, 101 | ); 102 | return this.percentagePosition; 103 | } 104 | 105 | getNDCPosition(): Vector2 { 106 | return this.getPercentagePosition().multiplyScalar(2).subScalar(1); 107 | } 108 | 109 | getCastedPosition(): Vector3 { 110 | this.raycaster.ray.intersectPlane(this.castPlane, this.castPosition); 111 | return this.castPosition; 112 | } 113 | 114 | isLeftButtonPressed(): boolean { 115 | return this.button === MOUSE_LEFT; 116 | } 117 | 118 | isRightButtonPressed(): boolean { 119 | return this.button === MOUSE_RIGHT; 120 | } 121 | 122 | isMiddleButtonPressed(): boolean { 123 | return this.button === MOUSE_MIDDLE; 124 | } 125 | 126 | getScrollDelta(): Vector2 { 127 | return this.scrollDelta; 128 | } 129 | 130 | enable() { 131 | this.isEnabled = true; 132 | } 133 | 134 | disable() { 135 | this.isEnabled = false; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/application/controls/mobile-control/MobileControl.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "eventemitter3"; 2 | import { Vector2 } from "three"; 3 | import { Application } from "../../Application"; 4 | import "./mobileControl.css"; 5 | 6 | export class MobileControl extends EventEmitter { 7 | private driveButton = document.createElement("button"); 8 | private powerMoveButton = document.createElement("button"); 9 | private enterButton = document.createElement("button"); 10 | private joystick = document.createElement("button"); 11 | private isEnabled = false; 12 | 13 | private isJoystickActive = false; 14 | constructor(private application: Application) { 15 | super(); 16 | this.initDriveButton(); 17 | this.initPowerMoveButton(); 18 | this.initEnterButton(); 19 | this.initJoystick(); 20 | 21 | window.addEventListener( 22 | "touchmove", 23 | (ev) => { 24 | ev.preventDefault(); 25 | }, 26 | { passive: false }, 27 | ); 28 | } 29 | 30 | enable() { 31 | this.showButton(this.driveButton); 32 | this.showButton(this.powerMoveButton); 33 | this.showButton(this.enterButton); 34 | this.showButton(this.joystick); 35 | this.isEnabled = true; 36 | } 37 | 38 | disable() { 39 | if (this.isEnabled) { 40 | this.hideButton(this.driveButton); 41 | this.hideButton(this.powerMoveButton); 42 | this.hideButton(this.enterButton); 43 | this.hideButton(this.joystick); 44 | this.isEnabled = false; 45 | } 46 | } 47 | 48 | hideButton(element: HTMLElement) { 49 | element.style.opacity = "0"; 50 | element.style.pointerEvents = "none"; 51 | } 52 | 53 | showButton(element: HTMLElement) { 54 | element.style.opacity = "1"; 55 | element.style.pointerEvents = "all"; 56 | } 57 | 58 | initDriveButton() { 59 | this.driveButton.innerHTML = "MOVE"; 60 | this.driveButton.classList.add("drive-button", "mobile-button"); 61 | 62 | this.hideButton(this.driveButton); 63 | 64 | this.driveButton.addEventListener( 65 | "touchstart", 66 | (event) => { 67 | event.preventDefault(); 68 | this.driveButton.classList.add("pressed"); 69 | this.emit("start-move"); 70 | }, 71 | { passive: false }, 72 | ); 73 | this.driveButton.addEventListener( 74 | "touchend", 75 | (event) => { 76 | event.preventDefault(); 77 | this.driveButton.classList.remove("pressed"); 78 | this.emit("stop-move"); 79 | }, 80 | { passive: false }, 81 | ); 82 | document.body.appendChild(this.driveButton); 83 | } 84 | 85 | initPowerMoveButton() { 86 | this.powerMoveButton.innerHTML = "POWER"; 87 | this.powerMoveButton.classList.add("power-move-button", "mobile-button"); 88 | 89 | this.hideButton(this.powerMoveButton); 90 | 91 | this.powerMoveButton.addEventListener( 92 | "touchstart", 93 | (event) => { 94 | event.preventDefault(); 95 | this.powerMoveButton.classList.add("pressed"); 96 | this.emit("start-power"); 97 | }, 98 | false, 99 | ); 100 | this.powerMoveButton.addEventListener( 101 | "touchend", 102 | (event) => { 103 | event.preventDefault(); 104 | this.powerMoveButton.classList.remove("pressed"); 105 | this.emit("stop-power"); 106 | }, 107 | false, 108 | ); 109 | document.body.appendChild(this.powerMoveButton); 110 | } 111 | 112 | initEnterButton() { 113 | this.enterButton.innerHTML = "ENTER"; 114 | this.enterButton.classList.add("enter-button", "mobile-button"); 115 | 116 | this.hideButton(this.enterButton); 117 | 118 | this.enterButton.addEventListener( 119 | "touchstart", 120 | (event) => { 121 | event.preventDefault(); 122 | this.enterButton.classList.add("pressed"); 123 | document.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); 124 | }, 125 | false, 126 | ); 127 | this.enterButton.addEventListener( 128 | "touchend", 129 | (event) => { 130 | event.preventDefault(); 131 | this.enterButton.classList.remove("pressed"); 132 | }, 133 | false, 134 | ); 135 | document.body.appendChild(this.enterButton); 136 | } 137 | 138 | private setDirection(x: number, y: number) { 139 | const center = { 140 | x: 20 + 155 / 2, 141 | y: this.application.sizes.height - 155 / 2 - 20, 142 | }; 143 | 144 | const dx = x - center.x; 145 | const dy = y - center.y; 146 | 147 | const vector = new Vector2(dx, dy); 148 | vector.normalize(); 149 | 150 | this.setJoystickStyleProperties(vector.x, vector.y); 151 | 152 | vector.y *= -1; 153 | this.emit("joystick-move", vector); 154 | } 155 | 156 | private setJoystickStyleProperties(x: number, y: number) { 157 | this.joystick.style.setProperty("--vX", x.toString()); 158 | this.joystick.style.setProperty("--vY", y.toString()); 159 | } 160 | 161 | initJoystick() { 162 | this.joystick.classList.add("joystick-button", "mobile-button"); 163 | 164 | this.hideButton(this.joystick); 165 | 166 | this.joystick.addEventListener( 167 | "touchstart", 168 | (event) => { 169 | event.preventDefault(); 170 | this.joystick.classList.add("pressed"); 171 | this.isJoystickActive = true; 172 | 173 | this.setDirection( 174 | event.targetTouches[0].clientX, 175 | event.targetTouches[0].clientY, 176 | ); 177 | }, 178 | false, 179 | ); 180 | 181 | this.joystick.addEventListener("touchmove", (event) => { 182 | event.preventDefault(); 183 | if (this.isJoystickActive) { 184 | const touch = event.targetTouches[0]; 185 | const x = touch.clientX; 186 | const y = touch.clientY; 187 | 188 | this.setDirection(x, y); 189 | } 190 | }); 191 | 192 | this.joystick.addEventListener("touchend", (event) => { 193 | event.preventDefault(); 194 | this.joystick.classList.remove("pressed"); 195 | this.isJoystickActive = false; 196 | }); 197 | 198 | document.body.appendChild(this.joystick); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/application/controls/mobile-control/mobileControl.css: -------------------------------------------------------------------------------- 1 | .mobile-button { 2 | position: fixed; 3 | background: rgba(255, 255, 255, 0.1); 4 | right: 20px; 5 | bottom: 20px; 6 | border: none; 7 | padding: 10px 20px; 8 | border-radius: 100%; 9 | height: 70px; 10 | width: 70px; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | color: white; 15 | transition: 16 | opacity 0.4s ease-in-out, 17 | background 0.2s ease-in-out; 18 | will-change: opacity, background; 19 | font-family: "Poppins", sans-serif; 20 | 21 | &.pressed { 22 | background: rgba(255, 255, 255, 0.5); 23 | } 24 | 25 | &.power-move-button { 26 | bottom: 110px; 27 | } 28 | 29 | &.enter-button { 30 | bottom: 65px; 31 | right: 95px; 32 | } 33 | } 34 | 35 | .joystick-button { 36 | right: auto; 37 | left: 20px; 38 | width: 155px; 39 | height: 155px; 40 | 41 | &.pressed { 42 | background: rgba(255, 255, 255, 0.1); 43 | } 44 | 45 | &.pressed:before { 46 | background: rgba(255, 255, 255, 0.5); 47 | } 48 | 49 | &:before { 50 | content: ""; 51 | position: absolute; 52 | width: 50%; 53 | height: 50%; 54 | border-radius: 50%; 55 | background: rgba(255, 255, 255, 0.1); 56 | transition: background 0.2s ease-in-out; 57 | will-change: background; 58 | transform: translateX(calc(var(--vX) * 50px)) 59 | translateY(calc(var(--vY) * 50px)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/application/experience/Experience.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../Application"; 2 | import { Submarine } from "./submarine/Submarine"; 3 | import { Environment } from "./environment/Environment"; 4 | import { Map } from "./map/Map"; 5 | import { MouseControl } from "../controls/MouseControl"; 6 | import { Camera } from "../Camera"; 7 | import { Dust } from "./dust/Dust"; 8 | import { Obstacle } from "./obstacle/Obstackle"; 9 | import { MobileControl } from "../controls/mobile-control/MobileControl"; 10 | import { Vector2, Vector3 } from "three"; 11 | import { ActiveElements } from "./active-elements/ActiveElements"; 12 | 13 | export const WORLD_GRAVITY = 0; 14 | export class Experience { 15 | private environment: Environment; 16 | private submarine!: Submarine; 17 | private map!: Map; 18 | 19 | private dust!: Dust; 20 | private dust2!: Dust; 21 | private dust3!: Dust; 22 | 23 | private obstacle1!: Obstacle; 24 | private obstacle2!: Obstacle; 25 | 26 | private activeElements!: ActiveElements; 27 | 28 | private mouseControl: MouseControl; 29 | private mobileControl: MobileControl; 30 | private camera: Camera; 31 | 32 | constructor(private application: Application) { 33 | this.setupPhysicsWorld(); 34 | 35 | this.mouseControl = application.mouseControl; 36 | this.mobileControl = application.mobileControl; 37 | this.camera = application.camera; 38 | 39 | this.environment = new Environment(this.application); 40 | 41 | this.setupObstacles(); 42 | this.setupSubmarine(); 43 | this.setupMap(); 44 | this.setupDust(); 45 | this.setupActiveElements(); 46 | 47 | this.camera.instance.position.x = this.submarine.initialPosition.x; 48 | this.camera.instance.position.y = this.submarine.initialPosition.y; 49 | } 50 | 51 | private setupPhysicsWorld() { 52 | this.application.physicApi.init({ 53 | gravity: [0, WORLD_GRAVITY, 0], 54 | allowSleep: true, 55 | broadphase: "SAPBroadphase", 56 | defaultAngularDamping: 0.2, 57 | defaultLinearDamping: 0.3, 58 | defaultLinearFactor: [1, 1, 0], 59 | }); 60 | } 61 | 62 | private setupActiveElements() { 63 | this.activeElements = new ActiveElements( 64 | this.application, 65 | this.submarine.getReflectorRangeFactor, 66 | this.submarine.getDistance2D, 67 | ); 68 | } 69 | 70 | private setupObstacles() { 71 | this.obstacle1 = new Obstacle(this.application, [25.5, 0, 0]); 72 | this.obstacle1.init().then(() => { 73 | this.obstacle1.addInstanceToScene(); 74 | }); 75 | 76 | this.obstacle2 = new Obstacle(this.application, [8.5, -29.8, 0]); 77 | this.obstacle2.init().then(() => { 78 | this.obstacle2.setPosition(8.5, -29.8, 0); 79 | this.obstacle2.addInstanceToScene(); 80 | }); 81 | } 82 | 83 | private setupSubmarine() { 84 | this.submarine = new Submarine(this.application); 85 | this.submarine.addInstanceToScene(); 86 | 87 | //connect obstacles 88 | this.submarine.on("velocityChange", (velocity) => { 89 | if (velocity > 6) { 90 | this.obstacle1.deactivate(); 91 | this.obstacle2.deactivate(); 92 | } else { 93 | this.obstacle1.activate(); 94 | this.obstacle2.activate(); 95 | } 96 | }); 97 | } 98 | 99 | private setupSubmarineControls() { 100 | this.mobileControl.on("start-move", () => { 101 | this.submarine.startEngine(); 102 | }); 103 | 104 | this.mouseControl.on("leftDown", () => { 105 | this.submarine.startEngine(); 106 | }); 107 | 108 | this.mobileControl.on("stop-move", () => { 109 | this.submarine.stopEngine(); 110 | }); 111 | 112 | this.mouseControl.on("leftUp", () => { 113 | this.submarine.stopEngine(); 114 | }); 115 | 116 | this.mobileControl.on("start-power", () => { 117 | this.submarine.startLoadingExtraPower(); 118 | }); 119 | 120 | this.mouseControl.on("rightDown", () => { 121 | this.submarine.startLoadingExtraPower(); 122 | }); 123 | 124 | this.mobileControl.on("stop-power", () => { 125 | this.submarine.firePowerMove(); 126 | }); 127 | 128 | this.mouseControl.on("rightUp", () => { 129 | this.submarine.firePowerMove(); 130 | }); 131 | 132 | this.mouseControl.on("move", () => { 133 | const submarineDirection = this.mouseControl 134 | .getCastedPosition() 135 | .sub(this.submarine.instance.position) 136 | .normalize(); 137 | this.submarine.setDirection(submarineDirection); 138 | }); 139 | 140 | this.mobileControl.on("joystick-move", (direction: Vector2) => { 141 | this.submarine.setDirection(new Vector3(direction.x, direction.y, 0)); 142 | }); 143 | } 144 | 145 | private setupMap() { 146 | this.map = new Map(this.application); 147 | this.map.init().then(() => { 148 | this.map.addInstanceToScene(); 149 | }); 150 | } 151 | 152 | private setupDust() { 153 | this.dust = new Dust(this.application, 30, 15, 7, 450, 2); 154 | this.dust.addInstanceToScene(); 155 | this.dust.setPosition(-2, 7, 2); 156 | 157 | this.dust2 = new Dust(this.application, 11, 21, 7, 150, 2); 158 | this.dust2.addInstanceToScene(); 159 | this.dust2.setPosition(3, -8, 2); 160 | 161 | this.dust3 = new Dust(this.application, 45, 60, 7, 650, 3); 162 | this.dust3.addInstanceToScene(); 163 | this.dust3.setPosition(-10, -29, 2); 164 | } 165 | 166 | private syncCameraWithSubmarine() { 167 | const targetCameraPositionY = this.submarine.instance.position.y; 168 | const targetCameraPositionX = this.submarine.instance.position.x; 169 | this.camera.instance.position.y += 170 | (targetCameraPositionY - this.camera.instance.position.y) * 0.04; 171 | this.camera.instance.position.x += 172 | (targetCameraPositionX - this.camera.instance.position.x) * 0.02; 173 | 174 | if (this.submarine.instance.position.y < -29.5) { 175 | const targetCameraPositionZ = this.camera.defaultZ + 6; 176 | const targetFogDensity = 0.03; 177 | this.camera.instance.position.z += 178 | (targetCameraPositionZ - this.camera.instance.position.z) * 0.04; 179 | this.environment.fogDensity += 180 | (targetFogDensity - this.environment.fogDensity) * 0.02; 181 | } else { 182 | if (this.camera.instance.position.z > this.camera.defaultZ) { 183 | const targetCameraPositionZ = this.camera.defaultZ; 184 | const targetFogDensity = this.environment.defaultFogDensity; 185 | this.camera.instance.position.z += 186 | (targetCameraPositionZ - this.camera.instance.position.z) * 0.04; 187 | this.environment.fogDensity += 188 | (targetFogDensity - this.environment.fogDensity) * 0.02; 189 | } 190 | } 191 | } 192 | 193 | private stepPhysics() { 194 | this.application.physicApi.step( 195 | this.application.time.getDeltaElapsedTime(), 196 | ); 197 | } 198 | 199 | start() { 200 | this.setupSubmarineControls(); 201 | } 202 | 203 | update() { 204 | this.stepPhysics(); 205 | this.submarine.update(); 206 | this.obstacle1.update(); 207 | this.obstacle2.update(); 208 | this.activeElements.update(); 209 | 210 | this.dust.update(); 211 | 212 | if ( 213 | this.submarine.instance.position.y < 0 && 214 | this.submarine.instance.position.y > -29.5 215 | ) { 216 | this.dust2.update(); 217 | } 218 | 219 | if (this.submarine.instance.position.y < -29.5) { 220 | this.dust3.update(); 221 | } 222 | 223 | this.syncCameraWithSubmarine(); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/application/experience/active-elements/ActiveElements.ts: -------------------------------------------------------------------------------- 1 | import { ActiveElement } from "./elements/ActiveElement"; 2 | 3 | import { Object3D } from "three"; 4 | import { Application } from "../../Application"; 5 | import { Github } from "./elements/Github"; 6 | import { Mail } from "./elements/Mail"; 7 | import { Linkedin } from "./elements/Linkedin"; 8 | 9 | export class ActiveElements { 10 | private activeElements: Map = new Map< 11 | Object3D, 12 | ActiveElement 13 | >(); 14 | private activeElementInstances: Object3D[] = []; 15 | private rangeData = { range: 0, distance: 0 }; 16 | private focusedElement: ActiveElement | undefined; 17 | 18 | constructor( 19 | private application: Application, 20 | private getRangeFactor: ( 21 | element: Object3D, 22 | data: { range: number; distance: number }, 23 | ) => { range: number; distance: number }, 24 | private getDistance2d: (element: Object3D) => number, 25 | ) { 26 | const linkedin = new Linkedin(application); 27 | linkedin.addInstanceToScene(); 28 | linkedin.setPosition(8, -3, 0); 29 | 30 | const github = new Github(application); 31 | github.addInstanceToScene(); 32 | github.setPosition(8, -6, 0); 33 | 34 | const mail = new Mail(application); 35 | mail.addInstanceToScene(); 36 | mail.setPosition(8, -9, 0); 37 | 38 | this.activeElements.set(github.instance, github); 39 | this.activeElements.set(linkedin.instance, linkedin); 40 | this.activeElements.set(mail.instance, mail); 41 | 42 | this.activeElementInstances = Array.from(this.activeElements.keys()); 43 | 44 | document.addEventListener("keydown", (event) => { 45 | if (event.key === "Enter" && this.focusedElement) { 46 | const win = window.open(this.focusedElement.link.url, "_blank"); 47 | if (!win) { 48 | alert("Please allow popups for this website"); 49 | } 50 | } 51 | }); 52 | } 53 | 54 | update() { 55 | const threshold = 0.9; 56 | let range = -1; 57 | let focusedElementCandidate: ActiveElement | undefined; 58 | this.activeElementInstances.forEach((instance) => { 59 | const elementRange = this.getRangeFactor(instance, this.rangeData); 60 | const distance2d = this.getDistance2d(instance); 61 | const activeElement = this.activeElements.get(instance); 62 | 63 | if (!activeElement) return; 64 | 65 | const elementInFrustum = this.application.camera.checkIfInFrustum( 66 | activeElement.instance.position, 67 | ); 68 | const cameraPositionOn2Floor = 69 | this.application.camera.instance.position.y < 0; 70 | 71 | if ( 72 | cameraPositionOn2Floor && 73 | elementInFrustum && 74 | elementRange.range > threshold && 75 | elementRange.range > range 76 | ) { 77 | range = elementRange.range; 78 | focusedElementCandidate = activeElement; 79 | } 80 | 81 | if (elementRange.range > threshold) { 82 | if (!activeElement.isActivated) { 83 | activeElement.activate(); 84 | } 85 | } else { 86 | if (activeElement.isActivated) { 87 | activeElement.deactivate(); 88 | } 89 | } 90 | 91 | if (distance2d < 2.5 && activeElement.isVisible) { 92 | activeElement.hide(); 93 | } else if (distance2d > 2.5 && !activeElement.isVisible) { 94 | activeElement.show(); 95 | } 96 | 97 | activeElement.update(); 98 | }); 99 | 100 | if (focusedElementCandidate && !focusedElementCandidate.isFocused) { 101 | if (this.focusedElement !== focusedElementCandidate) { 102 | this.focusedElement?.unfocus(); 103 | } 104 | focusedElementCandidate.focus(); 105 | this.focusedElement = focusedElementCandidate; 106 | } 107 | 108 | if (!focusedElementCandidate && this.focusedElement) { 109 | this.focusedElement.unfocus(); 110 | this.focusedElement = undefined; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/application/experience/active-elements/elements/ActiveElement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IcosahedronGeometry, 3 | Mesh, 4 | MeshBasicMaterial, 5 | MeshLambertMaterial, 6 | Object3D, 7 | PlaneGeometry, 8 | Vector3, 9 | } from "three"; 10 | import { Application } from "../../../Application"; 11 | import { resources } from "../../../resources/Resources"; 12 | import { Easing, Tween } from "@tweenjs/tween.js"; 13 | import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry"; 14 | 15 | const rockGeometry = new IcosahedronGeometry(0.5, 0); 16 | const rockMaterial = new MeshLambertMaterial({ 17 | color: 0xffffff, 18 | fog: true, 19 | transparent: false, 20 | }); 21 | const textMaterial = new MeshBasicMaterial({ 22 | color: 0xffffff, 23 | fog: true, 24 | transparent: false, 25 | }); 26 | const enterGeometry = new PlaneGeometry(1, 1, 1); 27 | const enterMaterial = new MeshBasicMaterial({ side: 2, transparent: true }); 28 | 29 | export abstract class ActiveElement { 30 | instance: Object3D = new Object3D(); 31 | private tween: Tween | undefined; 32 | private textTween: Tween | undefined; 33 | private enterTween: Tween | undefined; 34 | private readonly rock: Mesh; 35 | private readonly text: Mesh; 36 | private readonly enter: Mesh; 37 | 38 | isActivated: boolean = false; 39 | isVisible: boolean = true; 40 | isFocused: boolean = false; 41 | protected constructor( 42 | protected application: Application, 43 | public link: { text: string; url: string }, 44 | ) { 45 | enterMaterial.map = resources.getTexture("enter"); 46 | 47 | this.rock = new Mesh(rockGeometry, rockMaterial); 48 | this.text = this.createText(link.text); 49 | this.enter = this.createEnter(); 50 | this.assembleObjects(); 51 | } 52 | 53 | private assembleObjects() { 54 | this.instance.add(this.text); 55 | this.text.position.y = 0.8; 56 | this.text.scale.set(0, 0, 0); 57 | this.instance.add(this.enter); 58 | this.enter.position.y = -1.2; 59 | this.enter.scale.set(0, 0, 0); 60 | this.instance.add(this.rock); 61 | } 62 | 63 | private createText(text: string) { 64 | const textGeometry = new TextGeometry(text, { 65 | font: resources.getFont("optimerbold"), 66 | size: 0.35, 67 | height: 0, 68 | curveSegments: 6, 69 | bevelEnabled: false, 70 | }); 71 | 72 | textGeometry.center(); 73 | return new Mesh(textGeometry, textMaterial); 74 | } 75 | 76 | private createEnter() { 77 | return new Mesh(enterGeometry, enterMaterial); 78 | } 79 | 80 | private get positionTween() { 81 | if (this.tween) { 82 | return this.tween; 83 | } else { 84 | this.tween = new Tween(this.instance.position); 85 | return this.tween; 86 | } 87 | } 88 | 89 | private get textScaleTween() { 90 | if (this.textTween) { 91 | return this.textTween; 92 | } else { 93 | this.textTween = new Tween(this.text.scale); 94 | return this.textTween; 95 | } 96 | } 97 | 98 | private get enterScaleTween() { 99 | if (this.enterTween) { 100 | return this.enterTween; 101 | } else { 102 | this.enterTween = new Tween(this.enter.scale); 103 | return this.enterTween; 104 | } 105 | } 106 | addInstanceToScene() { 107 | this.application.scene.add(this.instance); 108 | } 109 | 110 | setPosition(x: number, y: number, z: number) { 111 | this.instance.position.set(x, y, z); 112 | } 113 | 114 | setRotation(x: number, y: number, z: number) { 115 | this.instance.rotation.set(x, y, z); 116 | } 117 | 118 | hide() { 119 | this.isVisible = false; 120 | this.positionTween 121 | .stop() 122 | .to({ z: -2 }, 1000) 123 | .easing(Easing.Elastic.Out) 124 | .startFromCurrentValues(); 125 | } 126 | 127 | show() { 128 | this.isVisible = true; 129 | this.positionTween 130 | .stop() 131 | .to({ z: 0 }, 1000) 132 | .easing(Easing.Elastic.Out) 133 | .startFromCurrentValues(); 134 | } 135 | 136 | activate() { 137 | this.isActivated = true; 138 | } 139 | 140 | deactivate() { 141 | this.isActivated = false; 142 | } 143 | 144 | focus() { 145 | this.isFocused = true; 146 | this.textScaleTween 147 | .stop() 148 | .to({ x: 1.0, y: 1.0, z: 1.0 }, 300) 149 | .easing(Easing.Quadratic.Out) 150 | .startFromCurrentValues(); 151 | this.enterScaleTween 152 | .stop() 153 | .to({ x: 1.0, y: 1.0, z: 1.0 }, 300) 154 | .easing(Easing.Quadratic.Out) 155 | .startFromCurrentValues(); 156 | } 157 | 158 | unfocus() { 159 | this.isFocused = false; 160 | this.textScaleTween 161 | .stop() 162 | .to({ x: 0.0, y: 0.0, z: 0.0 }, 300) 163 | .easing(Easing.Quadratic.Out) 164 | .startFromCurrentValues(); 165 | this.enterScaleTween 166 | .stop() 167 | .to({ x: 0.0, y: 0.0, z: 0.0 }, 300) 168 | .easing(Easing.Quadratic.Out) 169 | .startFromCurrentValues(); 170 | } 171 | 172 | update() { 173 | this.text.lookAt(this.application.camera.instance.position); 174 | this.enter.lookAt(this.application.camera.instance.position); 175 | if (this.isActivated) { 176 | this.rock.rotation.y += 0.01; 177 | this.rock.rotation.z += 0.01; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/application/experience/active-elements/elements/Github.ts: -------------------------------------------------------------------------------- 1 | import { ActiveElement } from "./ActiveElement"; 2 | import { Application } from "../../../Application"; 3 | 4 | export class Github extends ActiveElement { 5 | constructor(application: Application) { 6 | super(application, { 7 | text: "GITHUB", 8 | url: "https://github.com/Unruly-Coder", 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/application/experience/active-elements/elements/Linkedin.ts: -------------------------------------------------------------------------------- 1 | import { ActiveElement } from "./ActiveElement"; 2 | 3 | import { Application } from "../../../Application"; 4 | 5 | export class Linkedin extends ActiveElement { 6 | constructor(application: Application) { 7 | super(application, { 8 | text: "LINKEDIN", 9 | url: "https://www.linkedin.com/in/pawelbrod/", 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/application/experience/active-elements/elements/Mail.ts: -------------------------------------------------------------------------------- 1 | import { ActiveElement } from "./ActiveElement"; 2 | import { Application } from "../../../Application"; 3 | 4 | export class Mail extends ActiveElement { 5 | constructor(application: Application) { 6 | super(application, { text: "MAIL", url: "mailto:pawel.brod@gmail.com" }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/application/experience/dust/Dust.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AdditiveBlending, 3 | BufferGeometry, 4 | Float32BufferAttribute, 5 | Points, 6 | ShaderMaterial, 7 | Vector3, 8 | } from "three"; 9 | import dustVertexShader from "./dustVertexShader.glsl"; 10 | import dustFragmentShader from "./dustFragmentShader.glsl"; 11 | import { Application } from "../../Application"; 12 | 13 | export class Dust { 14 | private instance: Points; 15 | private positions: Float32Array; 16 | private particlesMaterial: ShaderMaterial; 17 | constructor( 18 | private application: Application, 19 | width: number, 20 | height: number, 21 | deep: number, 22 | nrOfParticles: number, 23 | size: number = 1, 24 | ) { 25 | const particlesGeometry = new BufferGeometry(); 26 | this.positions = new Float32Array(nrOfParticles * 3); 27 | const colors = new Float32Array(nrOfParticles * 4); 28 | const sizes = new Float32Array(nrOfParticles); 29 | 30 | for (let i = 0; i < nrOfParticles; i++) { 31 | const y = Math.random() * height; 32 | const z = Math.random() * deep; 33 | const x = Math.random() * width; 34 | 35 | const colorIndex = i * 4; 36 | const randomShade = Math.random(); 37 | colors[colorIndex] = (randomShade * 52) / 255; 38 | colors[colorIndex + 1] = (randomShade * 201) / 255; 39 | colors[colorIndex + 2] = (randomShade * 235) / 255; 40 | colors[colorIndex + 3] = 0.5 + Math.random() * 0.5; 41 | 42 | const index = i * 3; 43 | this.positions[index] = x; 44 | this.positions[index + 1] = y * -1; 45 | this.positions[index + 2] = z; 46 | 47 | sizes[i] = 48 | Math.max(1, 1 + Math.random()) * 49 | this.application.sizes.allowedPixelRatio; 50 | } 51 | 52 | particlesGeometry.setAttribute( 53 | "position", 54 | new Float32BufferAttribute(new Float32Array(this.positions), 3), 55 | ); 56 | particlesGeometry.setAttribute( 57 | "color", 58 | new Float32BufferAttribute(colors, 4), 59 | ); 60 | particlesGeometry.setAttribute( 61 | "size", 62 | new Float32BufferAttribute(sizes, 1), 63 | ); 64 | 65 | this.particlesMaterial = new ShaderMaterial({ 66 | uniforms: { 67 | uTime: { value: 0 }, 68 | uVelocity: { value: 0.4 }, 69 | uScale: { value: size * 10.0 }, 70 | uUseLight: { value: true }, 71 | uLightPosition: { value: new Vector3(0, 0, 0) }, 72 | uLightDirection: { value: new Vector3(1, 0, 0) }, 73 | }, 74 | vertexShader: dustVertexShader, 75 | fragmentShader: dustFragmentShader, 76 | transparent: false, 77 | depthWrite: false, 78 | blending: AdditiveBlending, 79 | vertexColors: true, 80 | precision: "lowp", 81 | }); 82 | 83 | this.instance = new Points(particlesGeometry, this.particlesMaterial); 84 | } 85 | 86 | addInstanceToScene() { 87 | this.application.scene.add(this.instance); 88 | } 89 | 90 | setPosition(x: number, y: number, z: number) { 91 | this.instance.position.set(x, y, z); 92 | } 93 | 94 | update() { 95 | this.particlesMaterial.uniforms.uTime.value = 96 | this.application.time.getElapsedTime(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/application/experience/dust/dustFragmentShader.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | 3 | void main() { 4 | vec2 center = vec2(0.5); 5 | float strength = distance(gl_PointCoord, center); 6 | strength = clamp(1.0 - (strength * 2.0), 0.0, 1.0); 7 | 8 | 9 | 10 | vec4 mask = vec4(strength, strength, strength , strength) ; 11 | vec4 finalColor = vColor * mask; 12 | gl_FragColor = finalColor; 13 | #include 14 | } -------------------------------------------------------------------------------- /src/application/experience/dust/dustVertexShader.glsl: -------------------------------------------------------------------------------- 1 | uniform float uTime; 2 | uniform float uVelocity; 3 | uniform float uScale; 4 | 5 | attribute float size; 6 | varying vec4 vColor; 7 | 8 | 9 | void main() 10 | { 11 | float x = sin(position.x + uTime * 0.15); 12 | float y = cos(position.x + uTime * 0.15); 13 | float z = sin(position.x + uTime * 0.15); 14 | vec3 offset = vec3(x, y, z); 15 | 16 | vec3 animatedPosition = position + offset * uVelocity; 17 | 18 | vec4 modelPosition = modelMatrix * vec4(animatedPosition, 1.0); 19 | vec4 viewPosition = viewMatrix * modelPosition; 20 | vec4 projectedPosition = projectionMatrix * viewPosition; 21 | 22 | gl_Position = projectedPosition; 23 | gl_PointSize = size * uScale; 24 | gl_PointSize *= ( 1.0 / - viewPosition.z ); 25 | 26 | vColor = color; 27 | } -------------------------------------------------------------------------------- /src/application/experience/environment/Environment.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../Application"; 2 | import { FogExp2, AmbientLight, HemisphereLight, Color } from "three"; 3 | 4 | export class Environment { 5 | readonly defaultFogDensity: number = 0.05; 6 | private readonly fog: FogExp2; 7 | private readonly ambientLight: AmbientLight; 8 | private readonly hemisphereLight: HemisphereLight; 9 | 10 | private readonly colors = { 11 | background: 0x000000, 12 | fog: 0x394e50, // 0x161F1F, 13 | ambientLight: 0x404040, 14 | hemisphereLight: 0xffffff, 15 | hemisphereGroundLight: 0x153232, 16 | directionalLight: 0xffffff, 17 | }; 18 | constructor(private application: Application) { 19 | this.application.scene.background = new Color(this.colors.background); 20 | this.fog = new FogExp2(this.colors.fog, this.defaultFogDensity); //0.07); 21 | 22 | this.application.scene.fog = this.fog; 23 | 24 | this.ambientLight = new AmbientLight(this.colors.ambientLight, 0); 25 | this.application.scene.add(this.ambientLight); 26 | 27 | this.hemisphereLight = new HemisphereLight( 28 | this.colors.hemisphereLight, 29 | this.colors.hemisphereGroundLight, 30 | 0.05, 31 | ); 32 | this.application.scene.add(this.hemisphereLight); 33 | 34 | this.setDebug(); 35 | } 36 | 37 | get fogDensity() { 38 | return this.fog.density; 39 | } 40 | set fogDensity(density: number) { 41 | this.fog.density = density; 42 | } 43 | 44 | private setDebug() { 45 | if (this.application.debug) { 46 | const folder = this.application.debug.addFolder("Environment"); 47 | 48 | folder.open(); 49 | 50 | folder.add(this.fog, "density", 0, 0.3).name("fog density"); 51 | 52 | folder 53 | .addColor(this.colors, "fog") 54 | .name("fog color") 55 | .onChange(() => { 56 | this.fog.color.set(this.colors.fog); 57 | }); 58 | 59 | folder.add(this.ambientLight, "intensity", 0, 1).name("amb intensity"); 60 | 61 | folder 62 | .addColor(this.colors, "ambientLight") 63 | .name("amb color") 64 | .onChange(() => { 65 | this.ambientLight.color.set(this.colors.ambientLight); 66 | }); 67 | 68 | folder.add(this.hemisphereLight, "intensity", 0, 1).name("hem intensity"); 69 | 70 | folder 71 | .addColor(this.colors, "hemisphereLight") 72 | .name("hem sky color") 73 | .onChange(() => { 74 | this.hemisphereLight.color.set(this.colors.hemisphereLight); 75 | }); 76 | 77 | folder 78 | .addColor(this.colors, "hemisphereGroundLight") 79 | .name("hem ground color") 80 | .onChange(() => { 81 | this.hemisphereLight.groundColor.set( 82 | this.colors.hemisphereGroundLight, 83 | ); 84 | }); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/application/experience/map/Map.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../Application"; 2 | import { resources } from "../../resources/Resources"; 3 | import { Group } from "three"; 4 | 5 | export class Map { 6 | private instance!: Group; 7 | private physicBodyId: number | undefined; 8 | 9 | constructor(private application: Application) { 10 | this.createRoomObject3d(); 11 | } 12 | 13 | async init() { 14 | await this.createRoomPhysicBody(); 15 | this.setupCollisionSound(); 16 | } 17 | 18 | private createRoomObject3d() { 19 | const map = resources.getGltf("map"); 20 | this.instance = map.scene; 21 | } 22 | 23 | private async createRoomPhysicBody() { 24 | const modelOffset = 3 * 0.5; 25 | 26 | this.physicBodyId = await this.application.physicApi.addBody({ 27 | mass: 0, 28 | position: [0, 0, 0], 29 | shapes: [ 30 | { 31 | type: "box", // floor 32 | halfExtents: [12.7, 0.2, 6], 33 | offset: [-modelOffset + 12.1, -0.1, 0], 34 | }, 35 | { 36 | type: "box", // ceiling 37 | halfExtents: [15.5, 0.5, 6], 38 | offset: [-modelOffset + 15, 7.5, 0], 39 | }, 40 | { 41 | type: "box", // wall 42 | halfExtents: [0.5, 3.5 + 4.5, 6], 43 | offset: [-modelOffset - 1, 3.5 - 4.5, 0], 44 | }, 45 | { 46 | type: "box", // wall 47 | halfExtents: [0.5, 3.5 + 4.5, 6], 48 | offset: [-modelOffset + 30, 3.5 - 4.5, 0], 49 | }, 50 | { 51 | type: "box", // floor2Right 52 | halfExtents: [6.4, 7.5, 6], 53 | offset: [-modelOffset + 23, -16.5, 0], 54 | }, 55 | { 56 | type: "box", // floor2Left 57 | halfExtents: [2, 7.5, 6], 58 | offset: [-modelOffset + 1.4, -16.5, 0], 59 | }, 60 | { 61 | type: "box", // floor3 62 | halfExtents: [1.5, 3, 6], 63 | offset: [13.4, -3 - 24, 0], 64 | }, 65 | { 66 | type: "box", // floor3 67 | halfExtents: [1.5, 3, 6], 68 | offset: [3.6, -3 - 24, 0], 69 | }, 70 | { 71 | type: "box", // floor4Right 72 | halfExtents: [8.5, 0.2, 6], 73 | offset: [-2.2, -30.2, 0], 74 | }, 75 | { 76 | type: "box", // floor4Left 77 | halfExtents: [13.5, 0.2, 6], 78 | offset: [24, -30.2, 0], 79 | }, 80 | { 81 | type: "box", // lastWall 82 | halfExtents: [0.5, 30.4, 6], 83 | offset: [-11.3, -60, 0], 84 | }, 85 | { 86 | type: "box", // lastWall 87 | halfExtents: [0.5, 30.4, 6], 88 | offset: [37.9, -60, 0], 89 | }, 90 | { 91 | type: "box", // lastFloor 92 | halfExtents: [24.5, 0.2, 6], 93 | offset: [13, -90.6, 0], 94 | }, 95 | { 96 | type: "box", // ramp 97 | halfExtents: [2.3, 0.6, 0.05], 98 | offset: [25.5, -8.45, -2.4], 99 | }, 100 | { 101 | type: "box", // ramp 102 | halfExtents: [2.3, 0.6, 0.05], 103 | offset: [25.5, -8.45, 2.4], 104 | }, 105 | ], 106 | }); 107 | } 108 | 109 | private setupCollisionSound() { 110 | if (this.physicBodyId === undefined) 111 | throw new Error("physicBodyId is undefined"); 112 | 113 | this.application.physicApi.addListener( 114 | this.physicBodyId, 115 | "collide", 116 | (event) => { 117 | const targetData = this.application.physicApi.getBodyData( 118 | event.targetId, 119 | ); 120 | const impactVelocity = parseFloat( 121 | ((event.contact.impactVelocity * targetData.mass) / 100).toFixed(1), 122 | ); 123 | 124 | if (impactVelocity > 0.1) { 125 | this.application.sound.sounds.impactmetal.volume( 126 | Math.min(impactVelocity * 0.5, 1), 127 | ); 128 | this.application.sound.sounds.impactmetal.play(); 129 | } 130 | }, 131 | ); 132 | } 133 | 134 | addInstanceToScene() { 135 | this.application.scene.add(this.instance); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/application/experience/obstacle/Obstackle.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "three"; 2 | 3 | import { Application } from "../../Application"; 4 | import { PlanksObstacle } from "./PlanksObstacle"; 5 | 6 | export class Obstacle { 7 | private instance: Group = new Group(); 8 | private physicBodyId: number | undefined; 9 | 10 | private isPermanentBroken: boolean = false; 11 | private width: number = 4; 12 | private height: number = 0.1; 13 | private depth: number = 4; 14 | private isInit: boolean = false; 15 | 16 | private planksObstacle: PlanksObstacle; 17 | 18 | constructor( 19 | private application: Application, 20 | private initialPosition?: [x: number, y: number, z: number], 21 | ) { 22 | this.planksObstacle = new PlanksObstacle(application, initialPosition); 23 | } 24 | 25 | async init() { 26 | await this.createPhysicalBody(); 27 | await this.planksObstacle.init(); 28 | this.setupCollisionSound(); 29 | 30 | if (this.physicBodyId === undefined) 31 | throw new Error("physicBodyId is undefined"); 32 | 33 | const bodyData = this.application.physicApi.getBodyData(this.physicBodyId); 34 | 35 | this.application.physicApi.addListener(this.physicBodyId, "collide", () => { 36 | if (this.isPermanentBroken) return; 37 | 38 | if (!bodyData.collisionResponse) { 39 | this.broke(); 40 | } 41 | }); 42 | 43 | this.isInit = true; 44 | } 45 | addInstanceToScene() { 46 | this.application.scene.add(this.instance); 47 | this.planksObstacle.addInstanceToScene(); 48 | } 49 | 50 | private async createPhysicalBody() { 51 | this.physicBodyId = await this.application.physicApi.addBody({ 52 | mass: 0, 53 | position: this.initialPosition, 54 | shapes: [ 55 | { 56 | type: "box", 57 | halfExtents: [this.width / 2, this.height / 2, this.depth / 2], 58 | }, 59 | ], 60 | }); 61 | } 62 | 63 | private setupCollisionSound() { 64 | if (this.physicBodyId === undefined) { 65 | throw new Error( 66 | "physicBodyId is undefined. Collision sound setup failed.", 67 | ); 68 | } 69 | 70 | this.application.physicApi.addListener( 71 | this.physicBodyId, 72 | "collide", 73 | (event) => { 74 | const targetData = this.application.physicApi.getBodyData( 75 | event.targetId, 76 | ); 77 | const impactVelocity = parseFloat( 78 | ((event.contact.impactVelocity * targetData.mass) / 100).toFixed(1), 79 | ); 80 | 81 | if (impactVelocity > 0.1 && this.isActive) { 82 | this.application.sound.sounds.impactmetal.volume( 83 | Math.min(impactVelocity * 0.5, 1), 84 | ); 85 | this.application.sound.sounds.impactmetal.play(); 86 | } 87 | 88 | if (!this.isActive && !this.isPermanentBroken) { 89 | this.application.sound.sounds.bigimpact.play(); 90 | } 91 | }, 92 | ); 93 | } 94 | 95 | setPosition(x: number, y: number, z: number) { 96 | if (this.physicBodyId === undefined) { 97 | throw new Error("physicBodyId is undefined. Cannot set position."); 98 | } 99 | 100 | this.instance.position.x = x; 101 | this.instance.position.y = y; 102 | this.instance.position.z = z; 103 | this.application.physicApi.setBodyPosition(this.physicBodyId, x, y, z); 104 | 105 | this.planksObstacle.setPosition(x, y, z); 106 | } 107 | 108 | get isActive() { 109 | if (this.physicBodyId === undefined) { 110 | throw new Error( 111 | "physicBodyId is undefined. Cannot determine activity status.", 112 | ); 113 | } 114 | 115 | if (this.isPermanentBroken) return false; 116 | const bodyData = this.application.physicApi.getBodyData(this.physicBodyId); 117 | return bodyData.collisionResponse; 118 | } 119 | 120 | deactivate() { 121 | if (this.physicBodyId === undefined) { 122 | throw new Error("physicBodyId is undefined. Cannot deactivate."); 123 | } 124 | 125 | this.application.physicApi.setBodyCollisionResponse( 126 | this.physicBodyId, 127 | false, 128 | ); 129 | } 130 | 131 | activate() { 132 | if (this.physicBodyId === undefined) { 133 | throw new Error( 134 | "Cannot activate due to permanent breakage or undefined physicBodyId.", 135 | ); 136 | } 137 | 138 | if (this.isPermanentBroken) return; 139 | 140 | this.application.physicApi.setBodyCollisionResponse( 141 | this.physicBodyId, 142 | true, 143 | ); 144 | } 145 | 146 | broke() { 147 | if (this.physicBodyId === undefined) { 148 | throw new Error("physicBodyId is undefined. Cannot break."); 149 | } 150 | 151 | this.isPermanentBroken = true; 152 | this.application.physicApi.setBodyCollisionResponse( 153 | this.physicBodyId, 154 | false, 155 | ); 156 | this.planksObstacle.applyForceToPlanks(); 157 | } 158 | 159 | update() { 160 | if (!this.isInit) return; 161 | 162 | this.planksObstacle.update(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/application/experience/obstacle/Plank.ts: -------------------------------------------------------------------------------- 1 | import { Box3, Object3D, Vector3 } from "three"; 2 | import * as CANNON from "cannon-es"; 3 | import { Application } from "../../Application"; 4 | 5 | export class Plank { 6 | private readonly bodyObject3D: Object3D; 7 | private bodyPhysicalId: number | undefined; 8 | private force: CANNON.Vec3 = new CANNON.Vec3(0, -0.3, 0); 9 | private isForceApplied: boolean = false; 10 | private isInit: boolean = false; 11 | 12 | constructor( 13 | private application: Application, 14 | private model: Object3D, 15 | private initialPosition?: [x: number, y: number, z: number], 16 | ) { 17 | this.bodyObject3D = model; 18 | } 19 | 20 | async init() { 21 | await this.createPhysicalBody(); 22 | this.isInit = true; 23 | } 24 | 25 | setPosition(x: number, y: number, z: number) { 26 | if (this.bodyPhysicalId === undefined) return; 27 | this.model.position.set(x, y, z); 28 | this.application.physicApi.setBodyPosition(this.bodyPhysicalId, x, y, z); 29 | } 30 | 31 | addInstanceToScene() { 32 | this.application.scene.add(this.bodyObject3D); 33 | } 34 | 35 | update() { 36 | if (this.bodyPhysicalId === undefined) return; 37 | 38 | if (this.isForceApplied) { 39 | this.application.physicApi.applyForce({ 40 | id: this.bodyPhysicalId, 41 | force: [this.force.x, this.force.y, this.force.z], 42 | }); 43 | } 44 | 45 | const bodyData = this.application.physicApi.getBodyData( 46 | this.bodyPhysicalId, 47 | ); 48 | this.bodyObject3D.position.copy(bodyData.position); 49 | this.bodyObject3D.quaternion.copy(bodyData.quaternion); 50 | } 51 | 52 | applyForce() { 53 | this.isForceApplied = true; 54 | } 55 | 56 | private async createPhysicalBody() { 57 | const boundingBox = new Box3().setFromObject(this.model); 58 | const sizeVector = new Vector3(); 59 | boundingBox.getSize(sizeVector); 60 | 61 | this.bodyPhysicalId = await this.application.physicApi.addBody({ 62 | mass: 1, 63 | allowSleep: true, 64 | sleepSpeedLimit: 0.1, 65 | position: this.initialPosition, 66 | shapes: [ 67 | { 68 | type: "box", 69 | halfExtents: [sizeVector.x / 2, sizeVector.y / 2, sizeVector.z / 2], 70 | }, 71 | ], 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/application/experience/obstacle/PlanksObstacle.ts: -------------------------------------------------------------------------------- 1 | import { Plank } from "./Plank"; 2 | import { Application } from "../../Application"; 3 | import { resources } from "../../resources/Resources"; 4 | 5 | export class PlanksObstacle { 6 | private planks: Plank[] = []; 7 | private planksData: { x: number; y: number; z: number }[] = []; 8 | private isInit: boolean = false; 9 | 10 | constructor( 11 | private application: Application, 12 | private initialPosition?: [x: number, y: number, z: number], 13 | ) { 14 | this.createPlanks(); 15 | } 16 | 17 | async init() { 18 | await Promise.all(this.planks.map((plank) => plank.init())); 19 | this.isInit = true; 20 | } 21 | 22 | private createPlanks() { 23 | const meshes = resources.getGltf("plank_1").scene.clone().children; 24 | meshes.forEach((mesh, i) => { 25 | const initPosition = { 26 | x: mesh.position.x, 27 | y: mesh.position.y - 0.2, 28 | z: mesh.position.z, 29 | }; 30 | this.planksData.push(initPosition); 31 | 32 | const initialGlobalPosition: [number, number, number] | undefined = this 33 | .initialPosition 34 | ? [ 35 | this.initialPosition[0] + initPosition.x, 36 | this.initialPosition[1] + initPosition.y, 37 | this.initialPosition[2] + initPosition.z, 38 | ] 39 | : undefined; 40 | const plank = new Plank(this.application, mesh, initialGlobalPosition); 41 | this.planks.push(plank); 42 | }); 43 | } 44 | 45 | setPosition(x: number, y: number, z: number) { 46 | this.planks.forEach((plank, i) => { 47 | plank.setPosition( 48 | x + this.planksData[i].x, 49 | y + this.planksData[i].y, 50 | z + this.planksData[i].z, 51 | ); 52 | }); 53 | } 54 | 55 | addInstanceToScene() { 56 | this.planks.forEach((plank) => { 57 | plank.addInstanceToScene(); 58 | }); 59 | } 60 | 61 | applyForceToPlanks() { 62 | this.planks.forEach((plank) => { 63 | plank.applyForce(); 64 | }); 65 | } 66 | 67 | update() { 68 | if (!this.isInit) return; 69 | 70 | this.planks.forEach((plank) => { 71 | plank.update(); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/application/experience/submarine/Bubbles.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "three"; 2 | import { Submarine } from "./Submarine"; 3 | import { BubbleEmitter } from "./bubble-emiter/BubbleEmiter"; 4 | import { Application } from "../../Application"; 5 | 6 | export class Bubbles { 7 | readonly instance: Group = new Group(); 8 | 9 | private bubbleEmitters: BubbleEmitter[] = []; 10 | private isBubbling: boolean = false; 11 | 12 | constructor( 13 | private application: Application, 14 | private submarine: Submarine, 15 | ) { 16 | this.crateBubbleEmitters(); 17 | } 18 | private crateBubbleEmitters() { 19 | const sides: string[] = ["left", "right"]; 20 | const verticalAngles: number[] = [0.25, 0.5, 0.75]; 21 | const horizontalAngles: number[] = [0.25, 0.75]; 22 | 23 | sides.forEach((side) => { 24 | verticalAngles.forEach((verticalAngle) => { 25 | horizontalAngles.forEach((horizontalAngle) => { 26 | const bubbleEmitter = new BubbleEmitter(this.application); 27 | bubbleEmitter.instance.position.setFromSphericalCoords( 28 | this.submarine.submarineRadius + 0.25, 29 | Math.PI * verticalAngle, 30 | Math.PI * 31 | (side === "right" ? horizontalAngle : horizontalAngle + 1), 32 | ); 33 | bubbleEmitter.instance.lookAt(0, 0, 0); 34 | this.bubbleEmitters.push(bubbleEmitter); 35 | 36 | this.instance.add(bubbleEmitter.instance); 37 | bubbleEmitter.initBubbles(); 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | startBubbling() { 44 | this.isBubbling = true; 45 | } 46 | 47 | stopBubbling() { 48 | this.isBubbling = false; 49 | } 50 | 51 | update() { 52 | this.bubbleEmitters.forEach((bubbleEmitter) => { 53 | if (this.isBubbling) { 54 | if ( 55 | this.submarine.direction.dot(bubbleEmitter.getEmitterDirection()) > 56 | 0.05 57 | ) { 58 | bubbleEmitter.startEmitting(); 59 | } else { 60 | bubbleEmitter.stopEmitting(); 61 | } 62 | } else { 63 | bubbleEmitter.stopEmitting(); 64 | } 65 | bubbleEmitter.update(); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/application/experience/submarine/Reflector.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../../Application"; 2 | import { resources } from "../../resources/Resources"; 3 | import { 4 | Group, 5 | Vector3, 6 | SpotLight, 7 | Mesh, 8 | MeshBasicMaterial, 9 | SphereGeometry, 10 | MeshLambertMaterial, 11 | BoxGeometry, 12 | CylinderGeometry, 13 | } from "three"; 14 | 15 | export class Reflector { 16 | instance: Group = new Group(); 17 | private lampInstance: Group = new Group(); 18 | private direction: Vector3 = new Vector3(0, -1, 0); 19 | private lampWorldDirection: Vector3 = new Vector3(0, -1, 0); 20 | private lampWorldPosition: Vector3 = new Vector3(0, 0, 0); 21 | private lightedObjectPosition: Vector3 = new Vector3(0, 0, 0); 22 | private colors = { spotlightColor: 0xa8efff }; 23 | private spotLight!: SpotLight; 24 | private cone!: Mesh; 25 | private coneMaterial!: MeshBasicMaterial; 26 | private coneMaterial2!: MeshBasicMaterial; 27 | private lightPower: number = 1; 28 | private readonly lightMaxIntensity: number = 250; 29 | 30 | private lastTargetLightBeamRotation = 0; 31 | 32 | constructor( 33 | private application: Application, 34 | private offsetY: number = 0, 35 | ) { 36 | this.createReflector(); 37 | this.setDebug(); 38 | } 39 | 40 | createReflector() { 41 | const lampGeometry = new SphereGeometry( 42 | 0.15, 43 | 8, 44 | 8, 45 | Math.PI, 46 | Math.PI * 2, 47 | 0, 48 | Math.PI * 0.5, 49 | ); 50 | const lampMaterial = new MeshLambertMaterial({ 51 | color: "black", 52 | }); 53 | const lampMesh = new Mesh(lampGeometry, lampMaterial); 54 | 55 | const handle = new Group(); 56 | const handlerGeometry = new BoxGeometry(0.1, 0.3, 0.1); 57 | const handlerGeometry2 = new BoxGeometry(0.3, 0.07, 0.07); 58 | const handleMesh = new Mesh(handlerGeometry, lampMaterial); 59 | const handleMesh2 = new Mesh(handlerGeometry2, lampMaterial); 60 | handleMesh.position.y = 0.11; 61 | handleMesh.position.x = -0.4; 62 | handleMesh.rotation.z = Math.PI / 4; 63 | 64 | handleMesh2.position.x = -0.17; 65 | handle.add(handleMesh); 66 | handle.add(handleMesh2); 67 | 68 | const coneHeight = 15; 69 | const angle = Math.PI / 4; 70 | const coneGeometry = new CylinderGeometry( 71 | 0.1, 72 | coneHeight * Math.tan(angle) * 0.55, 73 | coneHeight, 74 | 64, 75 | 1, 76 | true, 77 | Math.PI / 2, 78 | Math.PI, 79 | ); 80 | 81 | const coneMaterial = new MeshBasicMaterial({ 82 | color: this.colors.spotlightColor, 83 | transparent: true, 84 | opacity: 0.09, 85 | fog: true, 86 | map: resources.getTexture("lightRay"), 87 | alphaMap: resources.getTexture("lightRay"), 88 | }); 89 | 90 | const coneGeometry2 = new CylinderGeometry( 91 | 0.1, 92 | coneHeight * Math.tan(angle) * 0.85, 93 | coneHeight, 94 | 64, 95 | 1, 96 | true, 97 | Math.PI / 2, 98 | Math.PI, 99 | ); 100 | 101 | const coneMaterial2 = new MeshBasicMaterial({ 102 | color: this.colors.spotlightColor, 103 | transparent: true, 104 | opacity: 0.03, 105 | fog: true, 106 | map: resources.getTexture("lightRay2"), 107 | alphaMap: resources.getTexture("lightRay2"), 108 | }); 109 | 110 | const cone = new Mesh(coneGeometry, coneMaterial); 111 | cone.position.y = (-1 * coneHeight) / 2; 112 | cone.rotation.y = Math.PI; 113 | 114 | const cone2 = new Mesh(coneGeometry2, coneMaterial2); 115 | 116 | cone.add(cone2); 117 | this.coneMaterial = coneMaterial; 118 | this.coneMaterial2 = coneMaterial2; 119 | 120 | const spotLight = new SpotLight(this.colors.spotlightColor, this.lightMaxIntensity, 30); //0x7eeefc 121 | spotLight.penumbra = 1; 122 | 123 | spotLight.position.y = 0; 124 | spotLight.position.z = 0; 125 | spotLight.position.x = 0; 126 | 127 | spotLight.target.position.y = -2; 128 | spotLight.target.position.z = 0; 129 | spotLight.target.position.x = 0; 130 | 131 | spotLight.angle = angle; 132 | spotLight.map = resources.getTexture("flashlightLight"); 133 | 134 | this.spotLight = spotLight; 135 | 136 | this.lampInstance.add(spotLight); 137 | this.lampInstance.add(spotLight.target); 138 | this.lampInstance.add(cone); 139 | this.lampInstance.add(lampMesh); 140 | 141 | this.instance.add(handle); 142 | this.instance.add(this.lampInstance); 143 | this.instance.position.y = this.offsetY; 144 | 145 | this.cone = cone; 146 | } 147 | 148 | private adjustLampDirection() { 149 | const bottomThreshold = -Math.PI / 3; 150 | const topThreshold = Math.PI / 5; 151 | 152 | const directionAngle = Math.atan2( 153 | this.direction.y, 154 | Math.abs(this.direction.x), 155 | ); 156 | 157 | if (directionAngle > bottomThreshold && directionAngle < topThreshold) { 158 | this.lampWorldDirection.x = Math.cos(directionAngle); 159 | this.lampWorldDirection.y = Math.sin(directionAngle); 160 | } 161 | if (this.direction.x <= 0 && this.lampWorldDirection.x > 0) { 162 | this.lampWorldDirection.x *= -1; 163 | } else if (this.direction.x > 0 && this.lampWorldDirection.x < 0) { 164 | this.lampWorldDirection.x *= -1; 165 | } 166 | 167 | this.lampWorldDirection.normalize(); 168 | } 169 | 170 | private setDebug() { 171 | if (this.application.debug) { 172 | const folder = this.application.debug.addFolder("Reflector"); 173 | 174 | folder.open(); 175 | 176 | folder.add(this.spotLight, "intensity", 0, 20000).name("light intensity"); 177 | 178 | folder 179 | .addColor(this.colors, "spotlightColor") 180 | .name("light color") 181 | .onChange(() => { 182 | this.spotLight.color.set(this.colors.spotlightColor); 183 | this.coneMaterial.color.set(this.colors.spotlightColor); 184 | }); 185 | } 186 | } 187 | 188 | setDirection(direction: THREE.Vector3) { 189 | this.direction.copy(direction); 190 | } 191 | 192 | getRangeFactor( 193 | objectPosition: THREE.Object3D, 194 | dataObject: { range: number; distance: number }, 195 | ) { 196 | const lampWorldPosition = this.instance.getWorldPosition( 197 | this.lampWorldPosition, 198 | ); 199 | const lampToObject = objectPosition 200 | .getWorldPosition(this.lightedObjectPosition) 201 | .sub(lampWorldPosition); 202 | 203 | const distance = lampToObject.length(); 204 | lampToObject.normalize(); 205 | 206 | dataObject.distance = distance; 207 | dataObject.range = lampToObject.dot(this.lampWorldDirection); 208 | return dataObject; 209 | } 210 | 211 | private adjustLampRotation() { 212 | const threshold = 0.001; 213 | const lampRadius = Math.abs(this.offsetY); 214 | 215 | this.lampInstance.rotation.z = 216 | Math.min( 217 | Math.max( 218 | -Math.PI / 3, 219 | Math.atan2(this.direction.y, Math.abs(this.direction.x)), 220 | ), 221 | Math.PI / 5, 222 | ) + 223 | Math.PI / 2; 224 | 225 | const targetY = this.direction.x > 0 ? 0 : Math.PI; 226 | const rotationYDifference = Math.abs(targetY - this.instance.rotation.y); 227 | if (rotationYDifference < threshold) { 228 | this.instance.rotation.y = targetY; 229 | } else { 230 | this.instance.rotation.y += (targetY - this.instance.rotation.y) * 0.08; 231 | } 232 | 233 | const angle = Math.atan2(this.direction.y, this.direction.x); 234 | const horizontalAngle = Math.abs( 235 | Math.atan2(this.direction.y, this.direction.x), 236 | ); 237 | 238 | const lampOnTopSide = angle > 0; 239 | const lampOnRightSide = horizontalAngle < Math.PI / 2; 240 | 241 | const rightSideX = Math.sin(Math.PI * 0.25) * lampRadius; 242 | const leftSideX = Math.sin(Math.PI * 0.75) * -lampRadius; 243 | 244 | const targetX = lampOnRightSide ? rightSideX : leftSideX; 245 | this.instance.position.x += (targetX - this.instance.position.x) * 0.08; 246 | 247 | const difference = Math.abs(this.instance.position.x - targetX); 248 | const percentage = difference / (rightSideX - leftSideX); 249 | 250 | this.instance.position.z = Math.sin(Math.PI * percentage) * lampRadius * -1; 251 | 252 | const targetBeamRotation = lampOnRightSide 253 | ? Math.PI 254 | : lampOnTopSide 255 | ? Math.PI * 2 256 | : 0; 257 | const rotationDifference = Math.abs( 258 | targetBeamRotation - this.cone.rotation.y, 259 | ); 260 | 261 | if (rotationDifference < 0.001) { 262 | this.cone.rotation.y = targetBeamRotation; 263 | } else { 264 | this.cone.rotation.y += 265 | (targetBeamRotation - this.cone.rotation.y) * 0.08; 266 | } 267 | 268 | // Handle special cases for 360-degree beam light rotation 269 | if ( 270 | this.lastTargetLightBeamRotation == 0 && 271 | targetBeamRotation == Math.PI * 2 272 | ) { 273 | this.cone.rotation.y = 2 * Math.PI; 274 | } else if ( 275 | this.lastTargetLightBeamRotation == Math.PI * 2 && 276 | targetBeamRotation == 0 277 | ) { 278 | this.cone.rotation.y = 0; 279 | } 280 | 281 | this.lastTargetLightBeamRotation = targetBeamRotation; 282 | 283 | this.cone.rotation.y += 284 | Math.sin(this.application.time.getElapsedTime() * 0.3) * 0.009; 285 | } 286 | 287 | private adjustLightPower() { 288 | this.spotLight.intensity = this.lightPower * this.lightMaxIntensity 289 | } 290 | 291 | setLightPower(power: number) { 292 | this.lightPower = power; 293 | //change cone opacity based on power 294 | this.coneMaterial.opacity = power * 0.09; 295 | this.coneMaterial2.opacity = power * 0.03; 296 | } 297 | 298 | update() { 299 | this.adjustLampRotation(); 300 | this.adjustLampDirection(); 301 | this.adjustLightPower() 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/application/experience/submarine/Submarine.ts: -------------------------------------------------------------------------------- 1 | import { Vector3, Object3D, Quaternion, ArrowHelper } from "three"; 2 | import { Application } from "../../Application"; 3 | import { Reflector } from "./Reflector"; 4 | import { Bubbles } from "./Bubbles"; 5 | import EventEmitter from "eventemitter3"; 6 | import { resources } from "../../resources/Resources"; 7 | 8 | export class Submarine extends EventEmitter { 9 | instance!: Object3D; 10 | physicBodyId: number | undefined; 11 | physicBodyData: { 12 | position: Vector3; 13 | quaternion: Quaternion; 14 | velocity: Vector3; 15 | } = { 16 | position: new Vector3(), 17 | quaternion: new Quaternion(), 18 | velocity: new Vector3(), 19 | }; 20 | 21 | readonly submarineRadius: number = 1; 22 | readonly mass: number = 20; 23 | readonly direction: Vector3 = new Vector3(1, 0, 0).normalize(); 24 | 25 | private readonly maxExtraPower = 190; 26 | private extraPower: number = 0; 27 | private isExtraPowerLoading: boolean = false; 28 | 29 | private forceStrength: number = 0; 30 | private force: Vector3 = new Vector3(0, 0, 0); 31 | 32 | private directionArrow!: ArrowHelper; 33 | private submarine!: Object3D; 34 | private bench!: Object3D; 35 | private reflector!: Reflector; 36 | private bubbles!: Bubbles; 37 | 38 | private lastVelocity: number = 0; 39 | readonly initialPosition: Vector3 = new Vector3(4, 3, 0); 40 | 41 | constructor(private application: Application) { 42 | super(); 43 | this.createSubmarineObject3d(); 44 | this.createSubmarinePhysicBody(); 45 | this.createBubbles(); 46 | this.setDirection(this.direction); 47 | } 48 | 49 | private createSubmarineObject3d() { 50 | this.directionArrow = new ArrowHelper( 51 | this.direction, 52 | new Vector3(0, 0, 0), 53 | 2, 54 | 0xff0000, 55 | ); 56 | this.submarine = resources.getGltf("submarine").scene; 57 | this.bench = resources.getGltf("character").scene; 58 | this.bench.position.y = -0.3; 59 | 60 | this.reflector = new Reflector(this.application, -1.02); 61 | 62 | this.submarine.add(this.bench); 63 | this.submarine.add(this.reflector.instance); 64 | 65 | this.submarine.position.set( 66 | this.initialPosition.x, 67 | this.initialPosition.y, 68 | this.initialPosition.z, 69 | ); 70 | 71 | this.instance = this.submarine; 72 | } 73 | 74 | private async createSubmarinePhysicBody() { 75 | this.physicBodyId = await this.application.physicApi.addBody({ 76 | mass: this.mass, 77 | position: [ 78 | this.initialPosition.x, 79 | this.initialPosition.y, 80 | this.initialPosition.z, 81 | ], 82 | shapes: [ 83 | { 84 | type: "sphere", 85 | radius: this.submarineRadius + 0.1, 86 | }, 87 | ], 88 | }); 89 | } 90 | 91 | private createBubbles() { 92 | this.bubbles = new Bubbles(this.application, this); 93 | this.submarine.add(this.bubbles.instance); 94 | } 95 | 96 | private adjustForce() { 97 | this.force.set(this.direction.x, this.direction.y, this.direction.z); 98 | this.force = this.force.multiplyScalar(this.forceStrength); 99 | } 100 | 101 | getReflectorRangeFactor = ( 102 | object: THREE.Object3D, 103 | dataObject: { range: number; distance: number }, 104 | ) => { 105 | return this.reflector.getRangeFactor(object, dataObject); 106 | }; 107 | 108 | getDistance2D = (object: THREE.Object3D) => { 109 | const x1 = this.instance.position.x; 110 | const y1 = this.instance.position.y; 111 | const x2 = object.position.x; 112 | const y2 = object.position.y; 113 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 114 | }; 115 | 116 | private syncData() { 117 | //not sure why but this is needed to make the submarine move smoothly 118 | //cannon js is not precise enough for some reason; 119 | 120 | if (this.physicBodyId === undefined) return; 121 | 122 | const physicData = this.application.physicApi.getBodyData( 123 | this.physicBodyId, 124 | ); 125 | 126 | this.physicBodyData.position.copy(physicData.position); 127 | this.physicBodyData.quaternion.copy(physicData.quaternion); 128 | this.physicBodyData.velocity.copy(physicData.velocity); 129 | 130 | const targetX = physicData.position.x; 131 | const targetY = physicData.position.y; 132 | const targetZ = physicData.position.z; 133 | 134 | this.instance.position.x += (targetX - this.instance.position.x) * 0.35; 135 | this.instance.position.y += (targetY - this.instance.position.y) * 0.35; 136 | this.instance.position.z += (targetZ - this.instance.position.z) * 0.35; 137 | 138 | const forceLength = this.force.length(); 139 | const minAngle = Math.PI * 0.08; 140 | const maxAngle = -1 * minAngle; 141 | let targetRotationZ = 0; 142 | 143 | if (forceLength > 0) { 144 | const angle = Math.atan2(this.direction.y, this.direction.x); 145 | if ( 146 | (angle > 0 && angle < Math.PI * 0.5) || 147 | (angle > -Math.PI && angle < -Math.PI * 0.5) 148 | ) { 149 | targetRotationZ = minAngle; 150 | } else { 151 | targetRotationZ = maxAngle; 152 | } 153 | } 154 | 155 | if (targetRotationZ === 0 && Math.abs(this.submarine.rotation.z) < 0.01) { 156 | this.submarine.rotation.z = 0; 157 | } else { 158 | const delta = targetRotationZ - this.submarine.rotation.z; 159 | const deltaTime = this.application.time.getDeltaElapsedTime(); 160 | this.submarine.rotation.z += delta * Math.min(deltaTime, 0.1); 161 | } 162 | } 163 | 164 | private adjustBenchRotation() { 165 | this.bench.rotation.z = Math.min( 166 | Math.max( 167 | -Math.PI / 5, 168 | Math.atan2(this.direction.y, Math.abs(this.direction.x)), 169 | ), 170 | Math.PI / 5, 171 | ); 172 | const targetX = this.direction.x > 0 ? 0 : Math.PI; 173 | this.bench.rotation.y += (targetX - this.bench.rotation.y) * 0.08; 174 | } 175 | 176 | setDirection(direction: Vector3) { 177 | this.direction.copy(direction); 178 | this.reflector.setDirection(direction); 179 | 180 | this.adjustForce(); 181 | this.directionArrow.setDirection(this.direction); 182 | } 183 | 184 | startEngine() { 185 | this.forceStrength = 25; 186 | this.adjustForce(); 187 | this.bubbles.startBubbling(); 188 | 189 | this.application.sound.sounds.engine.fade( 190 | this.application.sound.sounds.engine.volume(), 191 | 1, 192 | 1000, 193 | ); 194 | } 195 | 196 | stopEngine() { 197 | this.forceStrength = 0; 198 | this.adjustForce(); 199 | this.bubbles.stopBubbling(); 200 | this.application.sound.sounds.engine.fade( 201 | this.application.sound.sounds.engine.volume(), 202 | 0, 203 | 1000, 204 | ); 205 | } 206 | 207 | private loadExtraPower() { 208 | if (this.physicBodyId === undefined) return; 209 | 210 | if (this.isExtraPowerLoading && this.extraPower < this.maxExtraPower) { 211 | this.extraPower += this.application.time.getDeltaElapsedTime() * 100; 212 | } 213 | 214 | this.application.physicApi.applyImpulse({ 215 | id: this.physicBodyId, 216 | impulse: [ 217 | (Math.sin(this.application.time.getElapsedTime() * 31) * 218 | this.extraPower) / 219 | 10, 220 | (Math.cos(this.application.time.getElapsedTime() * 29) * 221 | this.extraPower) / 222 | 10, 223 | 0, 224 | ], 225 | }); 226 | } 227 | 228 | startLoadingExtraPower() { 229 | this.isExtraPowerLoading = true; 230 | 231 | const powerloadSound = this.application.sound.sounds.powerload; 232 | powerloadSound.fade(powerloadSound.volume(), 0.1, 1500); 233 | } 234 | 235 | firePowerMove() { 236 | if (this.physicBodyId === undefined) return; 237 | 238 | this.application.physicApi.applyImpulse({ 239 | id: this.physicBodyId, 240 | impulse: [ 241 | this.direction.x * this.extraPower, 242 | this.direction.y * this.extraPower, 243 | this.direction.z * this.extraPower, 244 | ], 245 | }); 246 | 247 | const powerloadSound = this.application.sound.sounds.powerload; 248 | powerloadSound.fade(powerloadSound.volume(), 0, 100); 249 | 250 | const impactwaveSound = this.application.sound.sounds.impactwave; 251 | 252 | const volume = Math.max( 253 | Math.min( 254 | parseFloat((this.extraPower / this.maxExtraPower).toFixed(1)), 255 | 1, 256 | ) * 0.7, 257 | 0.1, 258 | ); 259 | impactwaveSound.volume(volume); 260 | impactwaveSound.play(); 261 | 262 | this.extraPower = 0; 263 | this.isExtraPowerLoading = false; 264 | } 265 | 266 | addInstanceToScene() { 267 | this.application.scene.add(this.instance); 268 | } 269 | 270 | getLightPower() { 271 | if(!this.isExtraPowerLoading) { 272 | return 1; 273 | } 274 | 275 | const powerPercent = this.extraPower / this.maxExtraPower; 276 | let outputPower = Math.max(1 - (powerPercent), 0.3); 277 | 278 | const timeWindow = 0.05; 279 | const baseInterval = 0.5; 280 | 281 | // Calculate the adjustment interval based on powerPercent 282 | const adjustmentInterval = baseInterval - (0.25 * powerPercent); 283 | 284 | const elapsedTime = this.application.time.getElapsedTime(); 285 | if(elapsedTime % adjustmentInterval < timeWindow) { 286 | outputPower *= Math.random() * 0.5; 287 | } 288 | 289 | 290 | return outputPower 291 | } 292 | 293 | update() { 294 | this.syncData(); 295 | 296 | if (this.isExtraPowerLoading) { 297 | this.loadExtraPower(); 298 | } 299 | 300 | if (this.forceStrength > 0 && this.physicBodyId !== undefined) { 301 | this.application.physicApi.applyLocalForce({ 302 | id: this.physicBodyId, 303 | force: [this.force.x, this.force.y, this.force.z], 304 | }); 305 | } 306 | 307 | const currentVelocity = this.physicBodyData.velocity.length(); 308 | if (this.lastVelocity !== currentVelocity) { 309 | this.emit("velocityChange", currentVelocity); 310 | this.lastVelocity = this.physicBodyData.velocity.length(); 311 | } 312 | 313 | this.adjustBenchRotation(); 314 | this.reflector.update(); 315 | this.reflector.setLightPower(this.getLightPower()); 316 | this.bubbles.update(); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/application/experience/submarine/bubble-emiter/BubbleEmiter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Object3D, 3 | Vector3, 4 | Quaternion, 5 | BufferGeometry, 6 | Points, 7 | BufferAttribute, 8 | AdditiveBlending, 9 | ShaderMaterial, 10 | } from "three"; 11 | import { resources } from "../../../resources/Resources"; 12 | 13 | import bubbleVertexShader from "./bubbleVertexShader.glsl"; 14 | import bubbleFragmentShader from "./bubbleFragmentShader.glsl"; 15 | 16 | import { Application } from "../../../Application"; 17 | 18 | export class BubbleEmitter { 19 | instance: Object3D = new Object3D(); 20 | 21 | private readonly worldPosition = new Vector3(); 22 | 23 | private readonly nrOfBubbles = 60; 24 | private readonly maxLifeTime = 0.46; 25 | private readonly maxEmitPerStep = 1; 26 | private readonly maxEmitInterval = 0.0001; 27 | 28 | private bubbles?: Points; 29 | private isEmitting: boolean = false; 30 | private lastEmitTime: number = 0; 31 | private bubblesAge: Float32Array = new Float32Array(this.nrOfBubbles).fill(0); 32 | private bubblesRandom: Float32Array = new Float32Array(this.nrOfBubbles).fill( 33 | 0, 34 | ); 35 | 36 | private material?: ShaderMaterial; 37 | 38 | private worldQuaternion = new Quaternion(); 39 | private emitterDirectionVector = new Vector3(0, 0, 1); 40 | constructor(private application: Application) {} 41 | 42 | private getEmitterPosition() { 43 | return this.instance.getWorldPosition(this.worldPosition); 44 | } 45 | 46 | initBubbles() { 47 | const geometry = new BufferGeometry(); 48 | const positions = new Float32Array(this.nrOfBubbles * 3); 49 | const colors = new Float32Array(this.nrOfBubbles * 4); 50 | const sizes = new Float32Array(this.nrOfBubbles); 51 | const rotations = new Float32Array(this.nrOfBubbles); 52 | 53 | const emitterPosition = this.getEmitterPosition(); 54 | 55 | for (let i = 0; i < this.nrOfBubbles; i++) { 56 | const positionIndex = i * 3; 57 | const colorIndex = i * 4; 58 | 59 | this.bubblesRandom[i] = Math.random(); 60 | 61 | positions[positionIndex] = emitterPosition.x; 62 | positions[positionIndex + 1] = emitterPosition.y; 63 | positions[positionIndex + 2] = emitterPosition.z; 64 | 65 | colors[colorIndex] = this.bubblesRandom[i] * 0.7; 66 | colors[colorIndex + 1] = this.bubblesRandom[i] * 0.7; 67 | colors[colorIndex + 2] = this.bubblesRandom[i]; 68 | colors[colorIndex + 3] = 0; 69 | 70 | sizes[i] = this.bubblesRandom[i] * 1.5; 71 | rotations[i] = this.bubblesRandom[i] * Math.PI * 2; 72 | } 73 | 74 | this.material = new ShaderMaterial({ 75 | uniforms: { 76 | pointTexture: { value: resources.getTexture("engineBubbles") }, 77 | uTime: { value: 0 }, 78 | }, 79 | vertexShader: bubbleVertexShader, 80 | fragmentShader: bubbleFragmentShader, 81 | blending: AdditiveBlending, 82 | depthWrite: false, 83 | transparent: true, 84 | precision: "lowp", 85 | }); 86 | 87 | this.bubbles = new Points(geometry, this.material); 88 | this.bubbles.frustumCulled = false; 89 | this.bubbles.geometry.setAttribute( 90 | "position", 91 | new BufferAttribute(positions, 3), 92 | ); 93 | this.bubbles.geometry.setAttribute("color", new BufferAttribute(colors, 4)); 94 | this.bubbles.geometry.setAttribute("size", new BufferAttribute(sizes, 1)); 95 | this.bubbles.geometry.setAttribute( 96 | "rotation", 97 | new BufferAttribute(rotations, 1), 98 | ); 99 | 100 | this.application.scene.add(this.bubbles); 101 | } 102 | 103 | startEmitting() { 104 | this.isEmitting = true; 105 | } 106 | 107 | stopEmitting() { 108 | this.isEmitting = false; 109 | } 110 | 111 | getEmitterDirection() { 112 | this.emitterDirectionVector.set(0, 0, 1); 113 | this.instance.getWorldQuaternion(this.worldQuaternion); 114 | this.emitterDirectionVector.applyQuaternion(this.worldQuaternion); 115 | return this.emitterDirectionVector; 116 | } 117 | 118 | private emitBubble(bubbleIndex: number, emitterPosition: Vector3) { 119 | if (!this.bubbles) return; 120 | 121 | this.bubblesAge[bubbleIndex] = 0; 122 | this.lastEmitTime = this.application.time.getElapsedTime(); 123 | 124 | const positionIndex = bubbleIndex * 3; 125 | 126 | this.bubbles.geometry.attributes.position.array[positionIndex] = 127 | emitterPosition.x; 128 | this.bubbles.geometry.attributes.position.array[positionIndex + 1] = 129 | emitterPosition.y; 130 | this.bubbles.geometry.attributes.position.array[positionIndex + 2] = 131 | emitterPosition.z; 132 | } 133 | 134 | update() { 135 | if (this.bubbles && this.material) { 136 | this.material.uniforms.uTime.value = 137 | this.application.time.getElapsedTime(); 138 | const emitterPosition = this.getEmitterPosition(); 139 | const emitterDirection = this.getEmitterDirection(); 140 | 141 | for (let i = 0; i < this.nrOfBubbles; i++) { 142 | let emitted = 0; 143 | 144 | const elapsedTime = this.application.time.getElapsedTime(); 145 | const deltaTime = this.application.time.getDeltaElapsedTime(); 146 | const particleAge = this.bubblesAge[i]; 147 | const deltaLastEmit = elapsedTime - this.lastEmitTime; 148 | const isBubbleInactive = particleAge >= this.maxLifeTime; 149 | const emmitReachedLimit = emitted >= this.maxEmitPerStep; 150 | const intervalReached = deltaLastEmit > this.maxEmitInterval; 151 | const isFirstEmitted = this.lastEmitTime === 0; 152 | 153 | const canEmit = 154 | isBubbleInactive && 155 | this.isEmitting && 156 | !emmitReachedLimit && 157 | (isFirstEmitted || intervalReached); 158 | 159 | if (canEmit) { 160 | this.emitBubble(i, emitterPosition); 161 | emitted++; 162 | } 163 | 164 | if (this.bubbles.geometry.attributes.color.array[i * 4] === 0) { 165 | return; 166 | } 167 | 168 | if (this.lastEmitTime === 0) { 169 | this.bubbles.geometry.attributes.color.setW(i, 0); 170 | this.bubbles.geometry.attributes.color.needsUpdate = true; 171 | } else { 172 | const positionIndex = i * 3; 173 | 174 | const speed = deltaTime * this.bubblesRandom[i] * 3; 175 | const v = Math.sin(this.bubblesRandom[i] * elapsedTime) * 0.1; 176 | const v2 = Math.cos(this.bubblesRandom[i] * elapsedTime) * 0.3; 177 | 178 | this.bubbles.geometry.attributes.position.array[positionIndex] -= 179 | (v + emitterDirection.x) * speed; 180 | this.bubbles.geometry.attributes.position.array[positionIndex + 1] -= 181 | (v2 + emitterDirection.y) * speed; 182 | this.bubbles.geometry.attributes.position.array[positionIndex + 2] -= 183 | (v + emitterDirection.z) * speed; 184 | this.bubbles.geometry.attributes.position.needsUpdate = true; 185 | 186 | const lifeFactor = Math.max( 187 | 0, 188 | Math.min( 189 | 1, 190 | this.maxLifeTime - this.bubblesAge[i] / this.maxLifeTime, 191 | ), 192 | ); 193 | if (this.isEmitting) { 194 | this.bubbles.geometry.attributes.color.setW(i, lifeFactor * 0.15); 195 | this.bubbles.geometry.attributes.color.needsUpdate = true; 196 | } else { 197 | this.bubbles.geometry.attributes.color.array[i * 4 + 3] -= 198 | deltaTime * 0.2; 199 | this.bubbles.geometry.attributes.color.needsUpdate = true; 200 | } 201 | 202 | this.bubbles.geometry.attributes.size.array[i] = 203 | this.bubblesRandom[i] * 204 | 3.5 * 205 | Math.pow(1 - lifeFactor, 2) * 206 | Math.sin(this.bubblesRandom[i]); 207 | this.bubbles.geometry.attributes.size.needsUpdate = true; 208 | } 209 | 210 | this.bubblesAge[i] += deltaTime; 211 | if (this.bubblesAge[i] > this.maxLifeTime) { 212 | this.bubblesAge[i] = this.maxLifeTime; 213 | } 214 | } 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/application/experience/submarine/bubble-emiter/bubbleFragmentShader.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D pointTexture; 2 | 3 | varying vec4 vColor; 4 | varying vec2 vRotation; 5 | 6 | void main() { 7 | vec2 coords = (gl_PointCoord - 0.5) * mat2(vRotation.x, vRotation.y, -vRotation.y, vRotation.x) + 0.5; 8 | coords = gl_PointCoord; 9 | 10 | if(vColor.a < 0.001) discard; 11 | 12 | gl_FragColor = vColor; 13 | gl_FragColor = gl_FragColor * texture2D( pointTexture, coords ); 14 | 15 | } -------------------------------------------------------------------------------- /src/application/experience/submarine/bubble-emiter/bubbleVertexShader.glsl: -------------------------------------------------------------------------------- 1 | uniform float uTime; 2 | 3 | attribute float size; 4 | attribute vec4 color; 5 | attribute float rotation; 6 | 7 | varying vec4 vColor; 8 | varying vec2 vRotation; 9 | 10 | void main() { 11 | 12 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 13 | vec4 viewPosition = viewMatrix * modelPosition; 14 | vec4 projectedPosition = projectionMatrix * viewPosition; 15 | 16 | gl_Position = projectedPosition; 17 | gl_PointSize = size * ( 200.0 / -viewPosition.z ); 18 | 19 | vRotation = vec2(cos(rotation + uTime/5.0), sin(rotation * uTime/5.0)); 20 | vColor = color; 21 | 22 | } -------------------------------------------------------------------------------- /src/application/physic/PhysicApi.ts: -------------------------------------------------------------------------------- 1 | import { MessageMap } from "./types"; 2 | import { WorkerCollideEvent, WorkerMessage } from "./worker/types.js"; 3 | import { Quaternion, Vector3 } from "three"; 4 | 5 | const TRANSFER_BUFFER_SIZE = 100; 6 | const QUATERNION_SIZE = 4; 7 | const VECTOR_SIZE = 3; 8 | const BODY_SIZE = 1 + VECTOR_SIZE * 2 + QUATERNION_SIZE; //id + position + velocity + quaternion 9 | 10 | export class PhysicApi { 11 | private worker: Worker; 12 | private bodies: Map< 13 | number, 14 | { 15 | collisionResponse: boolean; 16 | mass: number; 17 | position: Vector3; 18 | quaternion: Quaternion; 19 | velocity: Vector3; 20 | } 21 | > = new Map(); 22 | private transferBuffer: Float32Array = new Float32Array( 23 | 1 + TRANSFER_BUFFER_SIZE * BODY_SIZE, 24 | ); 25 | 26 | private listeners: Map< 27 | number, 28 | Map<"collide", Array<(data: WorkerCollideEvent) => void>> 29 | > = new Map(); 30 | constructor() { 31 | this.worker = new Worker(new URL("./worker/Worker.ts", import.meta.url), { 32 | type: "module", 33 | }); 34 | 35 | this.worker.onmessage = ({ data }: MessageEvent) => { 36 | switch (data.operation) { 37 | case "step": 38 | this.transferBuffer = data.payload; 39 | 40 | for (let i = 0; i < data.payload[0]; i++) { 41 | const id = data.payload[i * BODY_SIZE + 1]; 42 | const body = this.bodies.get(id); 43 | if (!body) { 44 | throw new Error(`Body with uuid ${id} not found`); 45 | } 46 | 47 | const { position, quaternion, velocity } = body; 48 | 49 | position.set( 50 | data.payload[i * BODY_SIZE + 2], 51 | data.payload[i * BODY_SIZE + 3], 52 | data.payload[i * BODY_SIZE + 4], 53 | ); 54 | quaternion.set( 55 | data.payload[i * BODY_SIZE + 5], 56 | data.payload[i * BODY_SIZE + 6], 57 | data.payload[i * BODY_SIZE + 7], 58 | data.payload[i * BODY_SIZE + 8], 59 | ); 60 | velocity.set( 61 | data.payload[i * BODY_SIZE + 9], 62 | data.payload[i * BODY_SIZE + 10], 63 | data.payload[i * BODY_SIZE + 11], 64 | ); 65 | } 66 | break; 67 | case "collide": 68 | const listeners = this.listeners.get(data.payload.bodyId); 69 | if (!listeners) return; 70 | const callback = listeners.get("collide"); 71 | if (!callback) return; 72 | callback.forEach((callback) => callback(data.payload)); 73 | break; 74 | } 75 | }; 76 | } 77 | 78 | init(payload: MessageMap["init"]["payload"]) { 79 | this.worker.postMessage({ operation: "init", payload }); 80 | } 81 | 82 | step(payload: number) { 83 | if (this.transferBuffer.buffer.byteLength === 0) return; 84 | this.worker.postMessage( 85 | { 86 | operation: "step", 87 | payload: { deltaTime: payload, transferBuffer: this.transferBuffer }, 88 | }, 89 | [this.transferBuffer.buffer], 90 | ); 91 | } 92 | 93 | async addBody(payload: MessageMap["addBody"]["payload"]) { 94 | return new Promise((resolve) => { 95 | const responseListener = ({ data }: MessageEvent) => { 96 | if (data.operation === "addBody") { 97 | if (this.bodies.get(data.payload)) return; 98 | 99 | this.bodies.set(data.payload, { 100 | mass: payload.mass, 101 | collisionResponse: true, 102 | position: new Vector3(position[0], position[1], position[2]), 103 | quaternion: new Quaternion( 104 | quaternion[0], 105 | quaternion[1], 106 | quaternion[2], 107 | quaternion[3], 108 | ), 109 | velocity: new Vector3(0, 0, 0), 110 | }); 111 | 112 | resolve(data.payload); 113 | this.worker.removeEventListener("message", responseListener); 114 | } 115 | }; 116 | 117 | this.worker.addEventListener("message", responseListener); 118 | 119 | const quaternion = payload.quaternion ?? [0, 0, 0, 1]; 120 | const position = payload.position ?? [0, 0, 0]; 121 | 122 | this.worker.postMessage({ operation: "addBody", payload }); 123 | }); 124 | } 125 | 126 | applyImpulse(payload: MessageMap["applyImpulse"]["payload"]) { 127 | this.worker.postMessage({ operation: "applyImpulse", payload }); 128 | } 129 | 130 | applyForce(payload: MessageMap["applyForce"]["payload"]) { 131 | this.worker.postMessage({ operation: "applyForce", payload }); 132 | } 133 | 134 | applyLocalForce(payload: MessageMap["applyLocalForce"]["payload"]) { 135 | this.worker.postMessage({ operation: "applyLocalForce", payload }); 136 | } 137 | 138 | applyLocalImpulse(payload: MessageMap["applyLocalImpulse"]["payload"]) { 139 | this.worker.postMessage({ operation: "applyLocalImpulse", payload }); 140 | } 141 | 142 | setBodyPosition(id: number, x: number, y: number, z: number) { 143 | const body = this.getBodyData(id); 144 | body.position.set(x, y, z); 145 | 146 | this.worker.postMessage({ 147 | operation: "setBodyPosition", 148 | payload: { id, position: [x, y, z] }, 149 | }); 150 | } 151 | 152 | setBodyCollisionResponse(id: number, isCollisionResponse: boolean) { 153 | const body = this.getBodyData(id); 154 | body.collisionResponse = isCollisionResponse; 155 | this.worker.postMessage({ 156 | operation: "setBodyCollisionResponse", 157 | payload: { id, isCollisionResponse }, 158 | }); 159 | } 160 | 161 | getBodyData(id: number) { 162 | const data = this.bodies.get(id); 163 | if (!data) { 164 | throw new Error(`Body with id ${id} not found`); 165 | } 166 | 167 | return data; 168 | } 169 | 170 | addListener( 171 | id: number, 172 | event: "collide", 173 | callback: (data: WorkerCollideEvent) => void, 174 | ) { 175 | const listeners = this.listeners.get(id) ?? new Map(); 176 | listeners.set(event, [...(listeners.get(event) ?? []), callback]); 177 | this.listeners.set(id, listeners); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/application/physic/types.ts: -------------------------------------------------------------------------------- 1 | export type Triplet = [x: number, y: number, z: number]; 2 | export type Quadruplet = [x: number, y: number, z: number, w: number]; 3 | type MessageShape = { operation: T; payload: K }; 4 | export type WithId = { id: number } & T; 5 | 6 | type ShapeType = "box" | "sphere"; 7 | 8 | interface ShapeBase { 9 | type: ShapeType; 10 | offset?: Triplet; 11 | orientation?: Quadruplet; 12 | } 13 | 14 | interface BoxShape extends ShapeBase { 15 | type: "box"; 16 | halfExtents: Triplet; 17 | } 18 | 19 | interface SphereShape extends ShapeBase { 20 | type: "sphere"; 21 | radius: number; 22 | } 23 | 24 | type Shape = BoxShape | SphereShape; 25 | 26 | type InitData = { 27 | gravity: Triplet; 28 | broadphase: "SAPBroadphase" | "NaiveBroadphase"; 29 | allowSleep: boolean; 30 | defaultLinearDamping?: number; 31 | defaultAngularDamping?: number; 32 | defaultLinearFactor?: Triplet; 33 | }; 34 | 35 | type AddBodyData = { 36 | position?: Triplet; 37 | quaternion?: Quadruplet; 38 | shapes: Shape[]; 39 | mass: number; 40 | allowSleep?: boolean; 41 | sleepSpeedLimit?: number; 42 | linearDamping?: number; 43 | angularDamping?: number; 44 | linearFactor?: Triplet; 45 | }; 46 | 47 | type StepData = { 48 | deltaTime: number; 49 | transferBuffer: Float32Array; 50 | }; 51 | 52 | export type MessageMap = { 53 | init: MessageShape<"init", InitData>; 54 | step: MessageShape<"step", StepData>; 55 | addBody: MessageShape<"addBody", AddBodyData>; 56 | applyImpulse: MessageShape<"applyImpulse", WithId<{ impulse: Triplet }>>; 57 | applyForce: MessageShape<"applyForce", WithId<{ force: Triplet }>>; 58 | applyLocalForce: MessageShape<"applyLocalForce", WithId<{ force: Triplet }>>; 59 | applyLocalImpulse: MessageShape< 60 | "applyLocalImpulse", 61 | WithId<{ impulse: Triplet }> 62 | >; 63 | setBodyPosition: MessageShape< 64 | "setBodyPosition", 65 | WithId<{ position: Triplet }> 66 | >; 67 | setBodyCollisionResponse: MessageShape< 68 | "setBodyCollisionResponse", 69 | WithId<{ isCollisionResponse: boolean }> 70 | >; 71 | }; 72 | 73 | export type ApiMessage = MessageMap[keyof MessageMap]; 74 | -------------------------------------------------------------------------------- /src/application/physic/worker/Worker.ts: -------------------------------------------------------------------------------- 1 | import { ApiMessage, MessageMap } from "./../types"; 2 | import * as CANNON from "cannon-es"; 3 | import { CannonCollideEvent } from "./types.js"; 4 | 5 | declare var self: DedicatedWorkerGlobalScope; 6 | 7 | const physicWorld = new CANNON.World(); 8 | let defaultLinearDamping = 0; 9 | let defaultAngularDamping = 0; 10 | let defaultLinearFactor = [1, 1, 1]; 11 | 12 | const idBodyMap: Map = new Map(); 13 | 14 | self.onmessage = ({ data }: MessageEvent) => { 15 | switch (data.operation) { 16 | case "init": 17 | init(data.payload); 18 | break; 19 | case "step": 20 | step(data.payload); 21 | break; 22 | case "addBody": 23 | addBody(data.payload); 24 | break; 25 | case "applyImpulse": 26 | applyImpulse(data.payload); 27 | break; 28 | case "applyForce": 29 | applyForce(data.payload); 30 | break; 31 | case "applyLocalForce": 32 | applyLocalForce(data.payload); 33 | break; 34 | case "applyLocalImpulse": 35 | applyLocalImpulse(data.payload); 36 | break; 37 | case "setBodyPosition": 38 | setBodyPosition(data.payload); 39 | break; 40 | case "setBodyCollisionResponse": 41 | setBodyCollisionResponse(data.payload); 42 | break; 43 | } 44 | }; 45 | 46 | function init(data: MessageMap["init"]["payload"]) { 47 | physicWorld.gravity.set(data.gravity[0], data.gravity[1], data.gravity[2]); 48 | physicWorld.allowSleep = data.allowSleep; 49 | 50 | if (data.broadphase === "SAPBroadphase") { 51 | const broadphase = new CANNON.SAPBroadphase(physicWorld); 52 | broadphase.autoDetectAxis(); 53 | physicWorld.broadphase = broadphase; 54 | } 55 | 56 | defaultLinearDamping = data.defaultLinearDamping ?? defaultLinearDamping; 57 | defaultAngularDamping = data.defaultAngularDamping ?? defaultAngularDamping; 58 | defaultLinearFactor = data.defaultLinearFactor ?? defaultLinearFactor; 59 | } 60 | 61 | function getBody(id: number) { 62 | const body = idBodyMap.get(id); 63 | if (!body) { 64 | throw new Error(`Body with uuid ${id} not found`); 65 | } 66 | 67 | return body; 68 | } 69 | 70 | function addBody(data: MessageMap["addBody"]["payload"]) { 71 | const body = new CANNON.Body({ 72 | mass: data.mass, 73 | allowSleep: data.allowSleep, 74 | sleepSpeedLimit: data.sleepSpeedLimit, 75 | position: 76 | data.position === undefined 77 | ? undefined 78 | : new CANNON.Vec3(data.position[0], data.position[1], data.position[2]), 79 | quaternion: 80 | data.quaternion === undefined 81 | ? undefined 82 | : new CANNON.Quaternion( 83 | data.quaternion[0], 84 | data.quaternion[1], 85 | data.quaternion[2], 86 | data.quaternion[3], 87 | ), 88 | }); 89 | 90 | data.shapes.forEach((shapeData) => { 91 | switch (shapeData.type) { 92 | case "box": { 93 | const shape = new CANNON.Box( 94 | new CANNON.Vec3( 95 | shapeData.halfExtents[0], 96 | shapeData.halfExtents[1], 97 | shapeData.halfExtents[2], 98 | ), 99 | ); 100 | const offset = new CANNON.Vec3( 101 | shapeData.offset ? shapeData.offset[0] : 0, 102 | shapeData.offset ? shapeData.offset[1] : 0, 103 | shapeData.offset ? shapeData.offset[2] : 0, 104 | ); 105 | const orientation = new CANNON.Quaternion( 106 | shapeData.orientation ? shapeData.orientation[0] : 0, 107 | shapeData.orientation ? shapeData.orientation[1] : 0, 108 | shapeData.orientation ? shapeData.orientation[2] : 0, 109 | shapeData.orientation ? shapeData.orientation[3] : 1, 110 | ); 111 | body.addShape(shape, offset, orientation); 112 | break; 113 | } 114 | case "sphere": { 115 | const shape = new CANNON.Sphere(shapeData.radius); 116 | const offset = new CANNON.Vec3( 117 | shapeData.offset ? shapeData.offset[0] : 0, 118 | shapeData.offset ? shapeData.offset[1] : 0, 119 | shapeData.offset ? shapeData.offset[2] : 0, 120 | ); 121 | const orientation = new CANNON.Quaternion( 122 | shapeData.orientation ? shapeData.orientation[0] : 0, 123 | shapeData.orientation ? shapeData.orientation[1] : 0, 124 | shapeData.orientation ? shapeData.orientation[2] : 0, 125 | shapeData.orientation ? shapeData.orientation[3] : 1, 126 | ); 127 | body.addShape(shape, offset, orientation); 128 | break; 129 | } 130 | } 131 | }); 132 | 133 | body.linearDamping = data.linearDamping ?? defaultLinearDamping; 134 | body.angularDamping = data.angularDamping ?? defaultAngularDamping; 135 | 136 | if (data.linearFactor) { 137 | body.linearFactor.set( 138 | data.linearFactor[0], 139 | data.linearFactor[1], 140 | data.linearFactor[2], 141 | ); 142 | } else { 143 | body.linearFactor.set( 144 | defaultLinearFactor[0], 145 | defaultLinearFactor[1], 146 | defaultLinearFactor[2], 147 | ); 148 | } 149 | 150 | body.addEventListener( 151 | "collide", 152 | ({ body, target, contact }: CannonCollideEvent) => { 153 | const { ni, ri, rj, bi, bj, id } = contact; 154 | const contactPoint = bi.position.vadd(ri); 155 | const contactNormal = bi === body ? ni : ni.scale(-1); 156 | 157 | self.postMessage({ 158 | operation: "collide", 159 | payload: { 160 | bodyId: body.id, 161 | targetId: target.id, 162 | collisionFilters: { 163 | bodyFilterGroup: body.collisionFilterGroup, 164 | bodyFilterMask: body.collisionFilterMask, 165 | targetFilterGroup: target.collisionFilterGroup, 166 | targetFilterMask: target.collisionFilterMask, 167 | }, 168 | contact: { 169 | bi: bi.id, 170 | bj: bj.id, 171 | contactNormal: contactNormal.toArray(), // Normal of the contact, relative to the colliding body 172 | contactPoint: contactPoint.toArray(), // World position of the contact 173 | id, 174 | impactVelocity: contact.getImpactVelocityAlongNormal(), 175 | ni: ni.toArray(), 176 | ri: ri.toArray(), 177 | rj: rj.toArray(), 178 | }, 179 | }, 180 | }); 181 | }, 182 | ); 183 | 184 | physicWorld.addBody(body); 185 | idBodyMap.set(body.id, body); 186 | 187 | self.postMessage({ operation: "addBody", payload: body.id }); 188 | } 189 | 190 | function applyImpulse(data: MessageMap["applyImpulse"]["payload"]) { 191 | const body = getBody(data.id); 192 | body.applyImpulse( 193 | new CANNON.Vec3(data.impulse[0], data.impulse[1], data.impulse[2]), 194 | ); 195 | } 196 | 197 | function applyForce(data: MessageMap["applyForce"]["payload"]) { 198 | const body = getBody(data.id); 199 | body.applyForce(new CANNON.Vec3(data.force[0], data.force[1], data.force[2])); 200 | } 201 | 202 | function applyLocalForce(data: MessageMap["applyLocalForce"]["payload"]) { 203 | const body = getBody(data.id); 204 | body.applyLocalForce( 205 | new CANNON.Vec3(data.force[0], data.force[1], data.force[2]), 206 | ); 207 | } 208 | 209 | function applyLocalImpulse(data: MessageMap["applyLocalImpulse"]["payload"]) { 210 | const body = getBody(data.id); 211 | body.applyLocalImpulse( 212 | new CANNON.Vec3(data.impulse[0], data.impulse[1], data.impulse[2]), 213 | ); 214 | } 215 | 216 | function setBodyPosition(data: MessageMap["setBodyPosition"]["payload"]) { 217 | const body = getBody(data.id); 218 | body.position.set(data.position[0], data.position[1], data.position[2]); 219 | } 220 | 221 | function setBodyCollisionResponse( 222 | data: MessageMap["setBodyCollisionResponse"]["payload"], 223 | ) { 224 | const body = getBody(data.id); 225 | body.collisionResponse = data.isCollisionResponse; 226 | } 227 | 228 | function step(data: MessageMap["step"]["payload"]) { 229 | physicWorld.step(1 / 60, data.deltaTime, 3); 230 | const { transferBuffer } = data; 231 | const nrOfData = 11; 232 | 233 | transferBuffer[0] = physicWorld.bodies.length; 234 | 235 | physicWorld.bodies.forEach((body, index) => { 236 | transferBuffer[index * nrOfData + 1] = body.id; 237 | transferBuffer[index * nrOfData + 2] = body.position.x; 238 | transferBuffer[index * nrOfData + 3] = body.position.y; 239 | transferBuffer[index * nrOfData + 4] = body.position.z; 240 | transferBuffer[index * nrOfData + 5] = body.quaternion.x; 241 | transferBuffer[index * nrOfData + 6] = body.quaternion.y; 242 | transferBuffer[index * nrOfData + 7] = body.quaternion.z; 243 | transferBuffer[index * nrOfData + 8] = body.quaternion.w; 244 | transferBuffer[index * nrOfData + 9] = body.velocity.x; 245 | transferBuffer[index * nrOfData + 10] = body.velocity.y; 246 | transferBuffer[index * nrOfData + 11] = body.velocity.z; 247 | }); 248 | 249 | self.postMessage( 250 | { 251 | operation: "step", 252 | payload: transferBuffer, 253 | }, 254 | [transferBuffer.buffer], 255 | ); 256 | } 257 | -------------------------------------------------------------------------------- /src/application/physic/worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["esnext", "webworker"], 5 | } 6 | } -------------------------------------------------------------------------------- /src/application/physic/worker/types.ts: -------------------------------------------------------------------------------- 1 | import { Body, ContactEquation } from "cannon-es"; 2 | 3 | type MessageShape = { operation: T; payload: K }; 4 | 5 | export type MessageWorkerMap = { 6 | step: MessageShape<"step", Float32Array>; 7 | addBody: MessageShape<"addBody", number>; 8 | collide: MessageShape<"collide", WorkerCollideEvent>; 9 | }; 10 | 11 | export interface CannonCollideEvent { 12 | body: Body; 13 | contact: ContactEquation; 14 | target: Body; 15 | type: "collide"; 16 | } 17 | 18 | export interface WorkerCollideEvent { 19 | bodyId: number; 20 | targetId: number; 21 | collisionFilters: { 22 | bodyFilterGroup: number; 23 | bodyFilterMask: number; 24 | targetFilterGroup: number; 25 | targetFilterMask: number; 26 | }; 27 | contact: { 28 | bi: string; 29 | bj: string; 30 | /** Normal of the contact, relative to the colliding body */ 31 | contactNormal: number[]; 32 | /** Contact point in world space */ 33 | contactPoint: number[]; 34 | id: number; 35 | impactVelocity: number; 36 | ni: number[]; 37 | ri: number[]; 38 | rj: number[]; 39 | }; 40 | } 41 | 42 | export type WorkerMessage = MessageWorkerMap[keyof MessageWorkerMap]; 43 | -------------------------------------------------------------------------------- /src/application/resources/Resources.ts: -------------------------------------------------------------------------------- 1 | import { textures, gltfs, sounds, fonts } from "./sources"; 2 | import { TextureLoader, Texture } from "three"; 3 | import { GLTFLoader, GLTF } from "three/examples/jsm/loaders/GLTFLoader"; 4 | import { FontLoader, Font } from "three/examples/jsm/loaders/FontLoader"; 5 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; 6 | import { Howl } from "howler"; 7 | import { TextureSource } from "./sources.types"; 8 | import EventEmitter from "eventemitter3"; 9 | 10 | /** 11 | * This class is responsible for loading all the resources 12 | * and making them available to the rest of the application. 13 | * 14 | * It is an EventEmitter, so you can listen to the 'loaded' event 15 | * to know when all resources are loaded. 16 | * 17 | * Example: 18 | * 19 | * const resources = new Resources(); 20 | * resources.on('loaded', () => { 21 | * // do something with the resources 22 | * const texture = resources.getTexture('flashlightLight'); 23 | * const gltf = resources.getGltf('submarine'); 24 | * const audio = resources.getAudio('submarineSound'); 25 | * 26 | * // ... 27 | * 28 | * }); 29 | */ 30 | class Resources extends EventEmitter { 31 | private readonly loaders: { 32 | textureLoader: TextureLoader; 33 | gltfLoader: GLTFLoader; 34 | fontLoader: FontLoader; 35 | }; 36 | 37 | private textureItems: Record = {} as Record< 38 | keyof typeof textures, 39 | Texture 40 | >; 41 | private gltfItems: Record = {} as Record< 42 | keyof typeof gltfs, 43 | GLTF 44 | >; 45 | private audioItems: Record = {} as Record< 46 | keyof typeof sounds, 47 | Howl 48 | >; 49 | private fontItems: Record = {} as Record< 50 | keyof typeof fonts, 51 | Font 52 | >; 53 | 54 | private readonly nrToLoad: number; 55 | private nrLoaded: number; 56 | 57 | constructor() { 58 | super(); 59 | const dracoLoader = new DRACOLoader(); 60 | dracoLoader.setDecoderPath("/libs/draco/"); 61 | dracoLoader.preload(); 62 | 63 | const gtlfLoader = new GLTFLoader(); 64 | gtlfLoader.setDRACOLoader(dracoLoader); 65 | 66 | this.loaders = { 67 | textureLoader: new TextureLoader(), 68 | gltfLoader: gtlfLoader, 69 | fontLoader: new FontLoader(), 70 | }; 71 | 72 | this.nrToLoad = 73 | Object.keys(textures).length + 74 | Object.keys(gltfs).length + 75 | Object.keys(sounds).length + 76 | Object.keys(fonts).length; 77 | this.nrLoaded = 0; 78 | 79 | this.load(); 80 | } 81 | 82 | private load() { 83 | const prefix = ""; 84 | 85 | const textureKeys = Object.keys(textures) as Array; 86 | 87 | textureKeys.forEach((key) => { 88 | const source: TextureSource = textures[key]; 89 | 90 | this.loaders.textureLoader.load( 91 | prefix + source.url, 92 | (texture) => { 93 | this.textureItems[key] = texture; 94 | this.incrementLoaded(); 95 | }, 96 | undefined, 97 | (error) => { 98 | console.log(error, key); 99 | }, 100 | ); 101 | }); 102 | 103 | const gltfKeys = Object.keys(gltfs) as Array; 104 | gltfKeys.forEach((key) => { 105 | const source = gltfs[key]; 106 | this.loaders.gltfLoader.load( 107 | prefix + source.url, 108 | (gltf) => { 109 | this.gltfItems[key] = gltf; 110 | this.incrementLoaded(); 111 | }, 112 | undefined, 113 | (error) => { 114 | console.log(error, key); 115 | }, 116 | ); 117 | }); 118 | 119 | const soundKeys = Object.keys(sounds) as Array; 120 | soundKeys.forEach((key) => { 121 | const source = sounds[key]; 122 | this.audioItems[key] = new Howl({ 123 | src: [prefix + source.url], 124 | preload: true, 125 | 126 | onload: () => { 127 | this.incrementLoaded(); 128 | }, 129 | onloaderror: (id, error) => { 130 | console.log(error, key); 131 | }, 132 | }); 133 | }); 134 | 135 | const fontKeys = Object.keys(fonts) as Array; 136 | fontKeys.forEach((key) => { 137 | const source = fonts[key]; 138 | this.loaders.fontLoader.load( 139 | prefix + source.url, 140 | (font) => { 141 | this.fontItems[key] = font; 142 | this.incrementLoaded(); 143 | }, 144 | undefined, 145 | (error) => { 146 | console.log(error, key); 147 | }, 148 | ); 149 | }); 150 | } 151 | 152 | private incrementLoaded() { 153 | this.nrLoaded++; 154 | 155 | this.emit("progress", this.percentLoaded); 156 | 157 | if (this.nrLoaded === this.nrToLoad) { 158 | this.emit("loaded"); 159 | } 160 | } 161 | 162 | getTexture(key: T): Texture { 163 | return this.textureItems[key]; 164 | } 165 | 166 | getGltf(key: T): GLTF { 167 | return this.gltfItems[key]; 168 | } 169 | 170 | getAudio(key: T): Howl { 171 | return this.audioItems[key]; 172 | } 173 | 174 | getFont(key: T): Font { 175 | return this.fontItems[key]; 176 | } 177 | 178 | get percentLoaded() { 179 | return this.nrLoaded / this.nrToLoad; 180 | } 181 | } 182 | 183 | export const resources = new Resources(); 184 | -------------------------------------------------------------------------------- /src/application/resources/sources.ts: -------------------------------------------------------------------------------- 1 | import { GltfSource, SoundSource, FontSource } from "./sources.types"; 2 | export const textures = { 3 | flashlightLight: { 4 | name: "flashlightLight", 5 | type: "texture", 6 | url: "/textures/flashlight.png", 7 | }, 8 | lightRay: { 9 | name: "lightRay", 10 | type: "texture", 11 | url: "/textures/beam.png", 12 | }, 13 | lightRay2: { 14 | name: "lightRay2", 15 | type: "texture", 16 | url: "/textures/beam2.png", 17 | }, 18 | engineBubbles: { 19 | name: "engineBubbles", 20 | type: "texture", 21 | url: "/textures/light_01.png", 22 | }, 23 | enter: { 24 | name: "enter", 25 | type: "texture", 26 | url: "/textures/enter.png", 27 | }, 28 | } as const; 29 | 30 | export const gltfs: Record = { 31 | map: { 32 | name: "map", 33 | type: "gltf", 34 | url: "/models/map.glb", 35 | }, 36 | plank_1: { 37 | name: "plank_1", 38 | type: "gltf", 39 | url: "/models/plank_1.glb", 40 | }, 41 | submarine: { 42 | name: "submarine", 43 | type: "gltf", 44 | url: "/models/submarine.glb", 45 | }, 46 | character: { 47 | name: "character", 48 | type: "gltf", 49 | url: "/models/character.glb", 50 | }, 51 | }; 52 | 53 | export const sounds: Record = { 54 | music: { 55 | name: "music", 56 | type: "sound", 57 | url: "/sounds/music.mp3", 58 | }, 59 | engine: { 60 | name: "engine", 61 | type: "sound", 62 | url: "/sounds/engine.mp3", 63 | }, 64 | impactwave: { 65 | name: "impactwave", 66 | type: "sound", 67 | url: "/sounds/impactwave.mp3", 68 | }, 69 | 70 | powerload: { 71 | name: "powerload", 72 | type: "sound", 73 | url: "/sounds/powerload.mp3", 74 | }, 75 | impactmetal: { 76 | name: "impactmetal", 77 | type: "sound", 78 | url: "/sounds/impactmetal.mp3", 79 | }, 80 | bigimpact: { 81 | name: "bigimpact", 82 | type: "sound", 83 | url: "/sounds/bigimpact.mp3", 84 | }, 85 | }; 86 | 87 | export const fonts: Record = { 88 | optimerbold: { 89 | name: "optimerbold", 90 | type: "font", 91 | url: "/fonts/optimer_bold.typeface.json", 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /src/application/resources/sources.types.ts: -------------------------------------------------------------------------------- 1 | export type SourceType = "texture" | "sound" | "gltf" | "font"; 2 | 3 | interface BaseSource { 4 | name: string; 5 | type: SourceType; 6 | } 7 | 8 | export interface TextureSource extends BaseSource { 9 | type: "texture"; 10 | url: string; 11 | } 12 | 13 | export interface SoundSource extends BaseSource { 14 | type: "sound"; 15 | url: string; 16 | } 17 | 18 | export interface GltfSource extends BaseSource { 19 | type: "gltf"; 20 | url: string; 21 | } 22 | 23 | export interface FontSource extends BaseSource { 24 | type: "font"; 25 | url: string; 26 | } 27 | 28 | export type Source = TextureSource | SoundSource | GltfSource | FontSource; 29 | -------------------------------------------------------------------------------- /src/application/utils/Device.ts: -------------------------------------------------------------------------------- 1 | const ua = navigator.userAgent.toLowerCase(); 2 | export class Device { 3 | public static isAndroid(): boolean { 4 | return ua.indexOf("android") > -1; 5 | } 6 | 7 | public static isMobile(): boolean { 8 | return ua.indexOf("mobile") > -1; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/application/utils/Sizes.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "eventemitter3"; 2 | 3 | /** 4 | * This clas is responsible for keeping track of the window size 5 | * and emitting a 'resize' event when the window is resized. 6 | * 7 | * Example: 8 | * 9 | * const sizes = new Sizes(); 10 | * sizes.on('resize', () => { 11 | * // do something with the new sizes 12 | * const width = sizes.width; 13 | * const height = sizes.height; 14 | * const aspectRatio = sizes.aspectRatio; 15 | * 16 | * // ... 17 | * 18 | * }); 19 | */ 20 | export class Sizes extends EventEmitter { 21 | private _width: number; 22 | private _height: number; 23 | private _pixelRatio: number; 24 | 25 | constructor() { 26 | super(); 27 | this._width = window.innerWidth; 28 | this._height = window.innerHeight; 29 | this._pixelRatio = window.devicePixelRatio; 30 | 31 | window.addEventListener("resize", () => { 32 | this._width = window.innerWidth; 33 | this._height = window.innerHeight; 34 | this.emit("resize"); 35 | }); 36 | } 37 | 38 | get width() { 39 | return this._width; 40 | } 41 | 42 | get height() { 43 | return this._height; 44 | } 45 | 46 | get widthHalf() { 47 | return this._width / 2; 48 | } 49 | 50 | get heightHalf() { 51 | return this._height / 2; 52 | } 53 | 54 | get aspectRatio() { 55 | return this._width / this._height; 56 | } 57 | 58 | private get pixelRatio() { 59 | return this._pixelRatio; 60 | } 61 | 62 | get allowedPixelRatio() { 63 | return Math.min(this.pixelRatio, 1.7); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/application/utils/Time.ts: -------------------------------------------------------------------------------- 1 | import { Clock } from "three"; 2 | import EventEmitter from "eventemitter3"; 3 | 4 | export class Time extends EventEmitter { 5 | private clock: Clock; 6 | private lastElapsedTime: number = 0; 7 | private deltaElapsedTime: number = 0; 8 | 9 | constructor() { 10 | super(); 11 | this.clock = new Clock(); 12 | } 13 | 14 | start() { 15 | this.tick(); 16 | } 17 | 18 | getDeltaElapsedTime() { 19 | return this.deltaElapsedTime; 20 | } 21 | 22 | getElapsedTime() { 23 | return this.clock.getElapsedTime(); 24 | } 25 | 26 | tick = () => { 27 | const elapsedTime = this.clock.getElapsedTime(); 28 | this.deltaElapsedTime = elapsedTime - this.lastElapsedTime; 29 | this.emit("tick"); 30 | this.lastElapsedTime = elapsedTime; 31 | requestAnimationFrame(this.tick); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | Paweł Bród - Software Engineer 31 | 35 | 36 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Paweł Bród - personal portfolio 60 | 61 | Experience heavily inspired by 62 | INSIDE 65 | game by Playdead 66 | 67 | 68 | echoes from below 69 | Explore 70 | 71 | 72 | 73 | 74 | MOVE 75 | POWER 76 | 77 | 78 | 85 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-touch-callout: none; /* Safari */ 3 | -webkit-user-select: none; /* Chrome */ 4 | -moz-user-select: none; /* Firefox */ 5 | -ms-user-select: none; /* Internet Explorer/Edge */ 6 | user-select: none; 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | color: white; 12 | font-weight: 500; 13 | } 14 | 15 | body, 16 | html { 17 | margin: 0; 18 | padding: 0; 19 | background: black; 20 | color: white; 21 | font-family: "Poppins", sans-serif; 22 | -webkit-font-smoothing: antialiased; 23 | overscroll-behavior-y: none; 24 | } 25 | 26 | canvas { 27 | top: 0; 28 | left: 0; 29 | position: fixed; 30 | z-index: -1; 31 | filter: blur(35px); 32 | 33 | .exploration-started & { 34 | filter: blur(0); 35 | transition: filter 2s linear; 36 | } 37 | } 38 | 39 | .resources-loaded #dialog { 40 | & .content { 41 | background: rgba(0, 0, 0, 0); 42 | transition: background 3s ease-in-out; 43 | } 44 | 45 | & .title { 46 | transition: 47 | transform 3s ease-out, 48 | opacity 3s ease-out; 49 | transform: translateY(0); 50 | opacity: 1; 51 | } 52 | 53 | & .description { 54 | transition: opacity 3s ease-in-out; 55 | opacity: 1; 56 | } 57 | 58 | & #button { 59 | transition: 3s ease-in-out; 60 | transition-delay: 3s; 61 | opacity: 1; 62 | } 63 | } 64 | 65 | .exploration-started { 66 | & #github-link { 67 | opacity: 1 !important; 68 | pointer-events: all; 69 | } 70 | } 71 | 72 | #dialog { 73 | color: white; 74 | background: transparent; 75 | padding: 0; 76 | border: none; 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | width: 100dvw; 81 | height: 100dvh; 82 | font-weight: 400; 83 | overflow: hidden; 84 | 85 | .exploration-started & { 86 | opacity: 0; 87 | filter: blur(35px); 88 | transition: 3s linear; 89 | transition-property: filter, opacity; 90 | } 91 | 92 | & .content { 93 | display: flex; 94 | flex-direction: column; 95 | justify-content: center; 96 | align-items: center; 97 | background: rgba(0, 0, 0, 1); 98 | width: 100vw; 99 | height: 100vh; 100 | } 101 | 102 | & .description { 103 | position: absolute; 104 | bottom: 1rem; 105 | font-size: min(0.7rem, 2.8vw); 106 | color: rgba(255, 255, 255, 0.5); 107 | opacity: 0; 108 | will-change: opacity; 109 | } 110 | 111 | & .title { 112 | font-family: "Poiret One", sans-serif; 113 | font-weight: 400; 114 | line-height: 4rem; 115 | font-size: min(4rem, 11vw); 116 | padding: 1rem 0; 117 | margin: 0; 118 | transform: translateY(-25%); 119 | 120 | opacity: 0; 121 | will-change: transform, opacity; 122 | } 123 | 124 | & .loader_bar { 125 | width: 100%; 126 | height: 1px; 127 | 128 | &:before { 129 | content: ""; 130 | display: block; 131 | width: calc(100% * var(--progress)); 132 | opacity: calc(var(--progress) * 0.2); 133 | height: 1px; 134 | background: rgba(255, 255, 255, 0.5); 135 | transition: 1s ease-in-out; 136 | } 137 | } 138 | 139 | & #button { 140 | border: none; 141 | background: transparent; 142 | color: white; 143 | font-family: "Poppins", sans-serif; 144 | text-transform: uppercase; 145 | font-weight: 400; 146 | font-size: 1rem; 147 | padding: 0.09rem 2rem; 148 | cursor: pointer; 149 | opacity: 0; 150 | } 151 | 152 | & .credit { 153 | font-size: min(0.7rem, 2.8vw); 154 | 155 | color: rgba(255, 255, 255, 0.5); 156 | 157 | text-align: center; 158 | } 159 | } 160 | 161 | #github-link { 162 | position: absolute; 163 | top: 20px; 164 | right: 20px; 165 | width: 30px; 166 | height: auto; 167 | opacity: 0; 168 | transition: opacity 1s ease-in-out; 169 | pointer-events: none; 170 | 171 | & img { 172 | width: 100%; 173 | height: 100%; 174 | } 175 | } 176 | 177 | #mouse-helper { 178 | width: 50px; 179 | height: 50px; 180 | position: absolute; 181 | left: 0; 182 | right: 0; 183 | margin: auto; 184 | bottom: 25px; 185 | opacity: 0; 186 | transition: opacity 1s ease-in-out; 187 | 188 | & .left-click { 189 | position: absolute; 190 | left: -60px; 191 | top: -15px; 192 | width: 85px; 193 | border-bottom: 1px solid white; 194 | } 195 | 196 | & .right-click { 197 | position: absolute; 198 | top: -15px; 199 | width: 85px; 200 | right: -60px; 201 | border-bottom: 1px solid white; 202 | text-align: right; 203 | } 204 | & img { 205 | width: 50px; 206 | height: 50px; 207 | } 208 | } 209 | 210 | #audio { 211 | --speed: 1.2s; 212 | width: 30px; 213 | height: 30px; 214 | display: flex; 215 | align-items: center; 216 | justify-content: center; 217 | position: absolute; 218 | gap: 1px; 219 | top: 20px; 220 | left: 20px; 221 | cursor: pointer; 222 | background: none; 223 | border: none; 224 | z-index: 1; 225 | opacity: 0; 226 | transition: opacity 0.4s ease-in-out; 227 | 228 | &.visible { 229 | opacity: 1 !important; 230 | } 231 | 232 | &.muted .bar { 233 | animation-play-state: paused; 234 | } 235 | 236 | & .bar { 237 | width: 3px; 238 | background: white; 239 | height: 20px; 240 | 241 | animation-name: music-bar-anim; 242 | animation-iteration-count: infinite; 243 | animation-duration: 1.3s; 244 | } 245 | 246 | & .bar:nth-child(1) { 247 | animation-duration: calc(var(--speed) + 0.3s); 248 | animation-delay: 0.2s; 249 | } 250 | 251 | & .bar:nth-child(2) { 252 | animation-duration: calc(var(--speed) + 0.8s); 253 | animation-delay: 0.1s; 254 | } 255 | 256 | & .bar:nth-child(3) { 257 | animation-duration: calc(var(--speed) + 1.2s); 258 | animation-delay: 0.3s; 259 | } 260 | 261 | & .bar:nth-child(4) { 262 | animation-duration: calc(var(--speed) + 1s); 263 | animation-delay: 0.15s; 264 | } 265 | 266 | & .bar:nth-child(5) { 267 | animation-duration: calc(var(--speed) + 0.5s); 268 | animation-delay: 0.25s; 269 | } 270 | } 271 | 272 | @keyframes music-bar-anim { 273 | 0% { 274 | transform: scale3d(1, 0.3, 1); 275 | opacity: 0.5; 276 | } 277 | 50% { 278 | transform: scale3d(1, 1, 1); 279 | opacity: 1; 280 | } 281 | 100% { 282 | transform: scale3d(1, 0.3, 1); 283 | opacity: 0.5; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "./main.css"; 2 | import { Application } from "./application/Application"; 3 | import { resources } from "./application/resources/Resources"; 4 | 5 | const exploreButton = document.getElementById("button"); 6 | const mouseHelper = document.getElementById("mouse-helper"); 7 | const dialog = document.getElementById("dialog"); 8 | 9 | setProgress(0); 10 | 11 | function setProgress(progress: number) { 12 | document.body.style.setProperty("--progress", progress.toString()); 13 | } 14 | 15 | function addBodyClass(className: string) { 16 | document.body.classList.add(className); 17 | } 18 | function explorationStartHandler( 19 | application: Application, 20 | enableTouchInterface = false, 21 | ) { 22 | addBodyClass("exploration-started"); 23 | 24 | if (dialog) { 25 | setTimeout(() => { 26 | dialog.style.setProperty("display", "none"); 27 | }, 3000); 28 | } 29 | application.experienceStart(enableTouchInterface); 30 | } 31 | 32 | function resourcesLoadedHandler() { 33 | const application = new Application(); 34 | application.start(); 35 | addBodyClass("resources-loaded"); 36 | 37 | exploreButton?.addEventListener("pointerup", (event) => { 38 | if (event.pointerType === "mouse") { 39 | explorationStartHandler(application); 40 | mouseHelper?.style.setProperty("opacity", "1"); 41 | } else { 42 | explorationStartHandler(application, true); 43 | } 44 | }); 45 | } 46 | 47 | resources.on("loaded", resourcesLoadedHandler); 48 | resources.on("progress", setProgress); 49 | -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/favicon.ico -------------------------------------------------------------------------------- /static/fonts/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright @ 2004 by MAGENTA Ltd. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright and this permission notice shall be included in all copies of one or more of the Font Software typefaces. 6 | 7 | The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing the word "MgOpen", or if the modifications are accepted for inclusion in the Font Software itself by the each appointed Administrator. 8 | 9 | This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "MgOpen" name. 10 | 11 | The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. 12 | 13 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL MAGENTA OR PERSONS OR BODIES IN CHARGE OF ADMINISTRATION AND MAINTENANCE OF THE FONT SOFTWARE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 14 | -------------------------------------------------------------------------------- /static/images/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/images/mouse.png -------------------------------------------------------------------------------- /static/images/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/images/social.png -------------------------------------------------------------------------------- /static/libs/draco/README.md: -------------------------------------------------------------------------------- 1 | # Draco 3D Data Compression 2 | 3 | Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics. 4 | 5 | [Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco) 6 | 7 | ## Contents 8 | 9 | This folder contains three utilities: 10 | 11 | * `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser. 12 | * `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices. 13 | * `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder. 14 | 15 | Each file is provided in two variations: 16 | 17 | * **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco). 18 | * **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension). 19 | 20 | Either variation may be used with `THREE.DRACOLoader`: 21 | 22 | ```js 23 | var dracoLoader = new THREE.DRACOLoader(); 24 | dracoLoader.setDecoderPath('path/to/decoders/'); 25 | dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support. 26 | ``` 27 | 28 | Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder). 29 | 30 | ## License 31 | 32 | [Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE) 33 | -------------------------------------------------------------------------------- /static/libs/draco/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/libs/draco/draco_decoder.wasm -------------------------------------------------------------------------------- /static/libs/draco/gltf/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/libs/draco/gltf/draco_decoder.wasm -------------------------------------------------------------------------------- /static/models/character.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/models/character.glb -------------------------------------------------------------------------------- /static/models/map.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/models/map.glb -------------------------------------------------------------------------------- /static/models/plank_1.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/models/plank_1.glb -------------------------------------------------------------------------------- /static/models/submarine.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/models/submarine.glb -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#000000","background_color":"#000000","display":"standalone"} -------------------------------------------------------------------------------- /static/sounds/bigimpact.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/sounds/bigimpact.mp3 -------------------------------------------------------------------------------- /static/sounds/engine.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/sounds/engine.mp3 -------------------------------------------------------------------------------- /static/sounds/impactmetal.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/sounds/impactmetal.mp3 -------------------------------------------------------------------------------- /static/sounds/impactwave.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/sounds/impactwave.mp3 -------------------------------------------------------------------------------- /static/sounds/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/sounds/music.mp3 -------------------------------------------------------------------------------- /static/sounds/powerload.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/sounds/powerload.mp3 -------------------------------------------------------------------------------- /static/textures/beam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/textures/beam.png -------------------------------------------------------------------------------- /static/textures/beam2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/textures/beam2.png -------------------------------------------------------------------------------- /static/textures/enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/textures/enter.png -------------------------------------------------------------------------------- /static/textures/flashlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/textures/flashlight.png -------------------------------------------------------------------------------- /static/textures/light_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unruly-Coder/portfolio/cd06ee7d8a4eb05fc8e84a8227028757a1ee5283/static/textures/light_01.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ES2020", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["vite-plugin-glsl/ext"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import glsl from 'vite-plugin-glsl'; 2 | 3 | export default { 4 | root: 'src/', 5 | publicDir: '../static/', 6 | base: './', 7 | plugins: [glsl()], 8 | server: 9 | { 10 | host: true, // Open to local network and display URL 11 | open: !('SANDBOX_URL' in process.env || 'CODESANDBOX_HOST' in process.env) // Open if it's not a CodeSandbox 12 | }, 13 | build: 14 | { 15 | outDir: '../dist', // Output in the dist/ folder 16 | emptyOutDir: true, // Empty the folder first 17 | sourcemap: false, // Add sourcemap 18 | }, 19 | } --------------------------------------------------------------------------------
Paweł Bród - personal portfolio
61 | Experience heavily inspired by 62 | INSIDE 65 | game by Playdead 66 |
MOVE
POWER