Compare commits
80 Commits
seal-3d
...
d506071ce2
| Author | SHA1 | Date | |
|---|---|---|---|
| d506071ce2 | |||
| cad47f07bd | |||
| 10543bba89 | |||
| aeee2158ba | |||
| 569a4f29fb | |||
| 9a67a800fa | |||
| 719a75d393 | |||
| f583cfdc57 | |||
| 5665804b8f | |||
| 67bf6325fa | |||
| 6d7651dec9 | |||
| ee2eb45527 | |||
| 079986ebec | |||
| a0b416c412 | |||
| c582d6b745 | |||
| beff5e3265 | |||
| c04d8536c0 | |||
| cb15cc3d95 | |||
| 0d72d49d7b | |||
| d66c898f23 | |||
| 566a684bfa | |||
| df81fc1ee0 | |||
| dd5e8a2ae2 | |||
| 20b6a559fd | |||
| eec01440f9 | |||
| 339c660bcb | |||
| fe686b0071 | |||
| 092faa9449 | |||
| a163a12483 | |||
| 30498e4faa | |||
| df44640ecb | |||
| 5de127449a | |||
| 7a1c28cd19 | |||
| 606dde5122 | |||
| 59e5a8c0c0 | |||
| a40ee3854b | |||
| 18246eab22 | |||
| a6a6cdd168 | |||
| d9869a469e | |||
| 0db7c08168 | |||
| 5e8ebf9491 | |||
| 4d28cf8bcb | |||
| f7b3153b92 | |||
| 458037829f | |||
| d778d82302 | |||
| 30fad6e2e3 | |||
| 6f01cc1793 | |||
| 144127952a | |||
| 6209236041 | |||
| cf70296b04 | |||
| 105518282f | |||
| 7791f6a684 | |||
| 79c47a3632 | |||
| c03449526a | |||
| cd630f0372 | |||
| 1ee1461d51 | |||
| cf13e2a2bf | |||
| cc7ab4ad9f | |||
| 9a2d73a6cb | |||
| ebe0842759 | |||
| cc0cf9e055 | |||
| 4c57e8f24f | |||
| 68a8d5f255 | |||
| a9beb53a91 | |||
| c0fd9e370c | |||
| fd0177f167 | |||
| b97767dba8 | |||
| d9b0975a61 | |||
| 0d8d95aa4b | |||
| 5c5f3f1861 | |||
| 086a65913b | |||
| ecf4c76b15 | |||
| d1a28fd777 | |||
| 4ad8c3f3db | |||
| 47855d7941 | |||
| b865cd0d99 | |||
| dac9906971 | |||
| 082dd811b7 | |||
| 559c9b6562 | |||
| 54d0345bff |
@@ -77,7 +77,6 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -1073,6 +1072,7 @@
|
|||||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25"
|
"@jridgewell/trace-mapping": "^0.3.25"
|
||||||
@@ -2002,7 +2002,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.2.tgz",
|
||||||
"integrity": "sha512-H4B4+FDNHpvIb4FmphH4ubxOfX5bxmfOw0+3pkQwR9u9wFiyMS7wUDkNn0m4RqQuiLWeia9jfN1eBvtyAVGEog==",
|
"integrity": "sha512-H4B4+FDNHpvIb4FmphH4ubxOfX5bxmfOw0+3pkQwR9u9wFiyMS7wUDkNn0m4RqQuiLWeia9jfN1eBvtyAVGEog==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.17.8",
|
"@babel/runtime": "^7.17.8",
|
||||||
"@types/react-reconciler": "^0.32.0",
|
"@types/react-reconciler": "^0.32.0",
|
||||||
@@ -2405,6 +2404,7 @@
|
|||||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "*",
|
"@types/estree": "*",
|
||||||
"@types/json-schema": "*"
|
"@types/json-schema": "*"
|
||||||
@@ -2416,6 +2416,7 @@
|
|||||||
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint": "*",
|
"@types/eslint": "*",
|
||||||
"@types/estree": "*"
|
"@types/estree": "*"
|
||||||
@@ -2463,7 +2464,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2474,7 +2474,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -2499,7 +2498,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
|
||||||
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
|
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
"@tweenjs/tween.js": "~23.1.3",
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
@@ -2561,7 +2559,6 @@
|
|||||||
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
|
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.50.1",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.50.1",
|
||||||
@@ -3079,6 +3076,7 @@
|
|||||||
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
|
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/helper-numbers": "1.13.2",
|
"@webassemblyjs/helper-numbers": "1.13.2",
|
||||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
|
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
|
||||||
@@ -3089,21 +3087,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
|
||||||
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
|
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/helper-api-error": {
|
"node_modules/@webassemblyjs/helper-api-error": {
|
||||||
"version": "1.13.2",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
|
||||||
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
|
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/helper-buffer": {
|
"node_modules/@webassemblyjs/helper-buffer": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
|
||||||
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
|
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/helper-numbers": {
|
"node_modules/@webassemblyjs/helper-numbers": {
|
||||||
"version": "1.13.2",
|
"version": "1.13.2",
|
||||||
@@ -3111,6 +3112,7 @@
|
|||||||
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
|
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
|
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
|
||||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||||
@@ -3122,7 +3124,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
|
||||||
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
|
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
@@ -3130,6 +3133,7 @@
|
|||||||
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
|
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/ast": "1.14.1",
|
"@webassemblyjs/ast": "1.14.1",
|
||||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||||
@@ -3143,6 +3147,7 @@
|
|||||||
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
|
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtuc/ieee754": "^1.2.0"
|
"@xtuc/ieee754": "^1.2.0"
|
||||||
}
|
}
|
||||||
@@ -3153,6 +3158,7 @@
|
|||||||
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
|
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
@@ -3162,7 +3168,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
|
||||||
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
|
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/wasm-edit": {
|
"node_modules/@webassemblyjs/wasm-edit": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
@@ -3170,6 +3177,7 @@
|
|||||||
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
|
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/ast": "1.14.1",
|
"@webassemblyjs/ast": "1.14.1",
|
||||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||||
@@ -3187,6 +3195,7 @@
|
|||||||
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
|
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/ast": "1.14.1",
|
"@webassemblyjs/ast": "1.14.1",
|
||||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||||
@@ -3201,6 +3210,7 @@
|
|||||||
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
|
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/ast": "1.14.1",
|
"@webassemblyjs/ast": "1.14.1",
|
||||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||||
@@ -3214,6 +3224,7 @@
|
|||||||
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
|
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/ast": "1.14.1",
|
"@webassemblyjs/ast": "1.14.1",
|
||||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||||
@@ -3229,6 +3240,7 @@
|
|||||||
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
|
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@webassemblyjs/ast": "1.14.1",
|
"@webassemblyjs/ast": "1.14.1",
|
||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
@@ -3245,14 +3257,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@xtuc/long": {
|
"node_modules/@xtuc/long": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
@@ -3260,7 +3274,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3274,6 +3287,7 @@
|
|||||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
},
|
},
|
||||||
@@ -3297,7 +3311,6 @@
|
|||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -3315,6 +3328,7 @@
|
|||||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.0.0"
|
"ajv": "^8.0.0"
|
||||||
},
|
},
|
||||||
@@ -3333,6 +3347,7 @@
|
|||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -3349,7 +3364,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/ajv-keywords": {
|
"node_modules/ajv-keywords": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
@@ -3631,7 +3647,6 @@
|
|||||||
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.0"
|
"@babel/types": "^7.26.0"
|
||||||
}
|
}
|
||||||
@@ -3735,7 +3750,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3779,7 +3793,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
@@ -3897,6 +3912,7 @@
|
|||||||
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
|
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
}
|
}
|
||||||
@@ -3938,7 +3954,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@@ -4336,7 +4353,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -4427,7 +4445,6 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -4613,7 +4630,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -4853,6 +4869,7 @@
|
|||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
@@ -4944,7 +4961,8 @@
|
|||||||
"url": "https://opencollective.com/fastify"
|
"url": "https://opencollective.com/fastify"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
@@ -5221,7 +5239,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
@@ -5986,6 +6005,7 @@
|
|||||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"merge-stream": "^2.0.0",
|
"merge-stream": "^2.0.0",
|
||||||
@@ -6001,6 +6021,7 @@
|
|||||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -6065,7 +6086,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
@@ -6470,6 +6492,7 @@
|
|||||||
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
|
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.11.5"
|
"node": ">=6.11.5"
|
||||||
},
|
},
|
||||||
@@ -6573,7 +6596,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/merge-value": {
|
"node_modules/merge-value": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -6635,6 +6659,7 @@
|
|||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -6645,6 +6670,7 @@
|
|||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
},
|
},
|
||||||
@@ -6751,7 +6777,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.1.1",
|
"version": "16.1.1",
|
||||||
@@ -7133,7 +7160,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.2.tgz",
|
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.2.tgz",
|
||||||
"integrity": "sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==",
|
"integrity": "sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==",
|
||||||
"license": "Zlib",
|
"license": "Zlib",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"three": ">= 0.157.0 < 0.183.0"
|
"three": ">= 0.157.0 < 0.183.0"
|
||||||
}
|
}
|
||||||
@@ -7227,6 +7253,7 @@
|
|||||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
@@ -7257,7 +7284,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7277,7 +7303,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -7512,7 +7537,8 @@
|
|||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/safe-push-apply": {
|
"node_modules/safe-push-apply": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -7590,6 +7616,7 @@
|
|||||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -7834,6 +7861,7 @@
|
|||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7853,6 +7881,7 @@
|
|||||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
@@ -8151,6 +8180,7 @@
|
|||||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
@@ -8170,6 +8200,7 @@
|
|||||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jest-worker": "^27.4.5",
|
"jest-worker": "^27.4.5",
|
||||||
@@ -8223,6 +8254,7 @@
|
|||||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3"
|
"fast-deep-equal": "^3.1.3"
|
||||||
},
|
},
|
||||||
@@ -8235,7 +8267,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
|
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
@@ -8243,6 +8276,7 @@
|
|||||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
"ajv": "^8.9.0",
|
"ajv": "^8.9.0",
|
||||||
@@ -8261,8 +8295,7 @@
|
|||||||
"version": "0.182.0",
|
"version": "0.182.0",
|
||||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/three-mesh-bvh": {
|
"node_modules/three-mesh-bvh": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
@@ -8337,7 +8370,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8567,7 +8599,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -8732,6 +8763,7 @@
|
|||||||
"integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==",
|
"integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"graceful-fs": "^4.1.2"
|
"graceful-fs": "^4.1.2"
|
||||||
@@ -8757,6 +8789,7 @@
|
|||||||
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
@@ -8806,6 +8839,7 @@
|
|||||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
@@ -8834,6 +8868,7 @@
|
|||||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3"
|
"fast-deep-equal": "^3.1.3"
|
||||||
},
|
},
|
||||||
@@ -8847,6 +8882,7 @@
|
|||||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esrecurse": "^4.3.0",
|
"esrecurse": "^4.3.0",
|
||||||
"estraverse": "^4.1.1"
|
"estraverse": "^4.1.1"
|
||||||
@@ -8861,6 +8897,7 @@
|
|||||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
@@ -8870,7 +8907,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/schema-utils": {
|
"node_modules/webpack/node_modules/schema-utils": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
@@ -8878,6 +8916,7 @@
|
|||||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
"ajv": "^8.9.0",
|
"ajv": "^8.9.0",
|
||||||
@@ -9032,7 +9071,6 @@
|
|||||||
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
@@ -0,0 +1,76 @@
|
|||||||
|
.discord-status-compact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
box-shadow: 2px 2px 0px var(--pink-accent);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 2px #fffdfd;
|
||||||
|
}
|
||||||
|
.status-dot.online { background-color: #a7f3d0; border: 1px solid #34d399; }
|
||||||
|
.status-dot.idle { background-color: #fef08a; border: 1px solid #facc15; }
|
||||||
|
.status-dot.dnd { background-color: #fecdd3; border: 1px solid #fb7185; }
|
||||||
|
.status-dot.offline { background-color: var(--text-dim); border: 1px solid var(--text-main); }
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text em {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--text-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bubble-inline {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
max-width: 220px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 1px 1px 0px var(--pink-accent);
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import './discordstatus.css';
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface LanyardResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: LanyardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanyardData {
|
||||||
|
discord_status: 'online' | 'idle' | 'dnd' | 'offline';
|
||||||
|
activities: DiscordActivity[];
|
||||||
|
discord_user: DiscordUser;
|
||||||
|
active_on_discord_web: boolean;
|
||||||
|
active_on_discord_desktop: boolean;
|
||||||
|
active_on_discord_mobile: boolean;
|
||||||
|
active_on_discord_embedded: boolean;
|
||||||
|
active_on_discord_vr: boolean;
|
||||||
|
listening_to_spotify: boolean;
|
||||||
|
spotify: SpotifyData | null;
|
||||||
|
kv: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpotifyData {
|
||||||
|
album: string;
|
||||||
|
album_art_url: string;
|
||||||
|
artist: string;
|
||||||
|
song: string;
|
||||||
|
track_id: string;
|
||||||
|
timestamps: {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiscordUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
discriminator: string;
|
||||||
|
global_name: string;
|
||||||
|
display_name: string;
|
||||||
|
avatar: string;
|
||||||
|
bot: boolean;
|
||||||
|
public_flags: number;
|
||||||
|
avatar_decoration_data: null | any;
|
||||||
|
collectibles?: {
|
||||||
|
nameplate?: {
|
||||||
|
asset: string;
|
||||||
|
expires_at: string | null;
|
||||||
|
label: string;
|
||||||
|
palette: string;
|
||||||
|
sku_id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
display_name_styles?: {
|
||||||
|
colors: any[];
|
||||||
|
effect_id: number;
|
||||||
|
font_id: number;
|
||||||
|
};
|
||||||
|
primary_guild?: {
|
||||||
|
badge: null | any;
|
||||||
|
identity_enabled: boolean;
|
||||||
|
identity_guild_id: null | string;
|
||||||
|
tag: null | string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiscordActivity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
session_id?: string;
|
||||||
|
created_at: number;
|
||||||
|
state?: string;
|
||||||
|
details?: string;
|
||||||
|
timestamps?: {
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
assets?: {
|
||||||
|
large_image?: string;
|
||||||
|
large_text?: string;
|
||||||
|
small_image?: string;
|
||||||
|
small_text?: string;
|
||||||
|
};
|
||||||
|
emoji?: {
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
animated?: boolean;
|
||||||
|
};
|
||||||
|
application_id?: string;
|
||||||
|
flags?: number;
|
||||||
|
platform?: string;
|
||||||
|
sync_id?: string;
|
||||||
|
content_classification?: {
|
||||||
|
data: null | any;
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDiscordAsset(applicationId: string | undefined, image: string | undefined): string {
|
||||||
|
if (!image) return "";
|
||||||
|
|
||||||
|
if (image.startsWith("mp:external/")) {
|
||||||
|
const httpsIndex = image.indexOf("/https/");
|
||||||
|
if (httpsIndex !== -1) {
|
||||||
|
return `https://${image.slice(httpsIndex + "/https/".length)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.startsWith("spotify:"))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
|
||||||
|
if (applicationId && image)
|
||||||
|
return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`;
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscordStatusParams {
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<LanyardData['discord_status'], string> = {
|
||||||
|
online: 'online',
|
||||||
|
idle: 'away',
|
||||||
|
dnd: 'busy',
|
||||||
|
offline: 'offline'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DiscordStatus({ userId }: DiscordStatusParams) {
|
||||||
|
const [presence, setPresence] = useState<LanyardData | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
async function fetchRichPresence() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.lanyard.rest/v1/users/${userId}`);
|
||||||
|
const json: LanyardResponse = await response.json();
|
||||||
|
if (json.success)
|
||||||
|
setPresence(json.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch Lanyard presence:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPolling = () => {
|
||||||
|
if (!interval) {
|
||||||
|
fetchRichPresence();
|
||||||
|
interval = setInterval(fetchRichPresence, 30000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible')
|
||||||
|
startPolling();
|
||||||
|
else
|
||||||
|
stopPolling();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.visibilityState === 'visible')
|
||||||
|
startPolling();
|
||||||
|
else
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
stopPolling();
|
||||||
|
};
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>loading status...</p>;
|
||||||
|
|
||||||
|
if (!presence)
|
||||||
|
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>offline</p>;
|
||||||
|
|
||||||
|
const customActivity = presence.activities.find(act => act.id === "custom");
|
||||||
|
const customStatusText = customActivity
|
||||||
|
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const gameActivity = presence.activities
|
||||||
|
.filter(act => act.type === 0)
|
||||||
|
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined;
|
||||||
|
|
||||||
|
const isListeningToSpotify = presence.listening_to_spotify && presence.spotify;
|
||||||
|
|
||||||
|
let primaryActivity = null;
|
||||||
|
let activityText = "";
|
||||||
|
let activityImage = "";
|
||||||
|
|
||||||
|
if (gameActivity) {
|
||||||
|
primaryActivity = gameActivity;
|
||||||
|
activityText = `playing: ${gameActivity.name.toLowerCase()}`;
|
||||||
|
|
||||||
|
if (gameActivity.details)
|
||||||
|
activityText += ` • ${gameActivity.details}`;
|
||||||
|
|
||||||
|
if (gameActivity.assets) {
|
||||||
|
const targetImage = gameActivity.assets.small_image || gameActivity.assets.large_image;
|
||||||
|
activityImage = resolveDiscordAsset(gameActivity.application_id, targetImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isListeningToSpotify && presence.spotify) {
|
||||||
|
primaryActivity = { name: "Spotify" } as DiscordActivity;
|
||||||
|
activityText = `listening to: ${presence.spotify.song} • ${presence.spotify.artist}`;
|
||||||
|
activityImage = presence.spotify.album_art_url || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="discord-status-compact">
|
||||||
|
<div className="avatar-container">
|
||||||
|
<img
|
||||||
|
src={`https://api.lanyard.rest/${userId}.png`}
|
||||||
|
alt="Discord avatar"
|
||||||
|
className="discord-avatar"
|
||||||
|
/>
|
||||||
|
<span className={`status-dot ${presence.discord_status}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-details">
|
||||||
|
<span className="status-text">
|
||||||
|
{primaryActivity ? (
|
||||||
|
<>{activityText}</>
|
||||||
|
) : (
|
||||||
|
<>currently: <em>{STATUS_LABELS[presence.discord_status]}</em></>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{customStatusText && (
|
||||||
|
<span className="status-bubble-inline">
|
||||||
|
{customStatusText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activityImage && primaryActivity && (
|
||||||
|
<img
|
||||||
|
src={activityImage}
|
||||||
|
alt={primaryActivity.name}
|
||||||
|
className="game-icon"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.canvas {
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import './page.css';
|
||||||
|
|
||||||
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
||||||
|
import { Suspense, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { AmbientSound } from './scene-components/ambient-sound';
|
||||||
|
|
||||||
|
import { fearState } from './state';
|
||||||
|
|
||||||
|
import TheCreature from './scene-components/creature';
|
||||||
|
import Player from './scene-components/player';
|
||||||
|
import Hallway from './scene-components/hallway';
|
||||||
|
|
||||||
|
import { AudioListener } from 'three';
|
||||||
|
|
||||||
|
function PostProcessing() {
|
||||||
|
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
|
setWasCaught(fearState.wasCaught);
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (<EffectComposer>
|
||||||
|
<Pixelation granularity={wasCaught ? 18 : 12} />
|
||||||
|
<Vignette />
|
||||||
|
<Noise opacity={wasCaught ? 0.01 : 0.005} />
|
||||||
|
<BrightnessContrast
|
||||||
|
brightness={-0.01}
|
||||||
|
contrast={0.05}
|
||||||
|
/>
|
||||||
|
<HueSaturation saturation={wasCaught ? 1 : 0} />
|
||||||
|
</EffectComposer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListenerCreator() {
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = new AudioListener();
|
||||||
|
camera.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
camera.remove(listener);
|
||||||
|
};
|
||||||
|
}, [camera]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FearStateUpdater() {
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
fearState.update(delta);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Fear() {
|
||||||
|
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
|
||||||
|
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
|
setIsRustActive(fearState.isRustActive);
|
||||||
|
setWasCaught(fearState.wasCaught)
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Canvas
|
||||||
|
shadows
|
||||||
|
gl={{ antialias: true }}
|
||||||
|
className='canvas'
|
||||||
|
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
|
||||||
|
>
|
||||||
|
<FearStateUpdater />
|
||||||
|
|
||||||
|
<ListenerCreator />
|
||||||
|
|
||||||
|
<color attach="background" args={['#050505']} />
|
||||||
|
|
||||||
|
<ambientLight intensity={0.0225} />
|
||||||
|
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
||||||
|
<PostProcessing />
|
||||||
|
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Hallway />
|
||||||
|
<TheCreature />
|
||||||
|
<Player />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
key="ambient-1"
|
||||||
|
url='fear/snd/ambience.mp3'
|
||||||
|
volume={isRustActive ? 0 : 1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
key="ambient-2"
|
||||||
|
url='fear/snd/ambience2.mp3'
|
||||||
|
volume={isRustActive ? 1 : 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{wasCaught ? <AmbientSound
|
||||||
|
key="ambient-glitch"
|
||||||
|
url='fear/snd/glitch.mp3'
|
||||||
|
volume={1}
|
||||||
|
/> : null}
|
||||||
|
</Canvas>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
interface AmbientSoundProps {
|
||||||
|
url: string
|
||||||
|
volume?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
|
const targetVolumeRef = useRef<number>(volume)
|
||||||
|
|
||||||
|
targetVolumeRef.current = volume
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = new Audio(url)
|
||||||
|
audio.loop = true
|
||||||
|
audio.volume = 0
|
||||||
|
audioRef.current = audio
|
||||||
|
|
||||||
|
let componentsMounted = true
|
||||||
|
|
||||||
|
const attemptPlay = () => {
|
||||||
|
if (!audioRef.current || !componentsMounted) return
|
||||||
|
|
||||||
|
audio.volume = targetVolumeRef.current
|
||||||
|
|
||||||
|
if (audio.volume > 0 && audio.paused) {
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn('Autoplay management holding clip playback execution.', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptPlay()
|
||||||
|
|
||||||
|
window.addEventListener('click', attemptPlay)
|
||||||
|
window.addEventListener('keydown', attemptPlay)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
componentsMounted = false
|
||||||
|
window.removeEventListener('click', attemptPlay)
|
||||||
|
window.removeEventListener('keydown', attemptPlay)
|
||||||
|
audio.pause()
|
||||||
|
audio.src = ''
|
||||||
|
audioRef.current = null
|
||||||
|
}
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current
|
||||||
|
if (!audio) return
|
||||||
|
|
||||||
|
if (volume === 0) {
|
||||||
|
if (!audio.paused) audio.pause()
|
||||||
|
} else {
|
||||||
|
audio.volume = volume
|
||||||
|
if (audio.paused) {
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [volume])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { useTexture, PositionalAudio } from "@react-three/drei";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
|
|
||||||
|
useTexture.preload('fear/img/creature.png');
|
||||||
|
|
||||||
|
export default function TheCreature() {
|
||||||
|
const texture = useTexture('fear/img/creature.png');
|
||||||
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
|
const audioRef = useRef<THREE.PositionalAudio>(null);
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
const [hasTriggered, setHasTriggered] = useState(false);
|
||||||
|
const [isSpawned, setIsSpawned] = useState(false);
|
||||||
|
|
||||||
|
const globalDistance = useRef<number>(32);
|
||||||
|
const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered);
|
||||||
|
|
||||||
|
const audioPlaying = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
|
setFinaleTriggered(fearState.finaleTriggered);
|
||||||
|
|
||||||
|
if (!fearState.finaleTriggered) {
|
||||||
|
setIsSpawned(false);
|
||||||
|
setHasTriggered(false);
|
||||||
|
globalDistance.current = 32;
|
||||||
|
audioPlaying.current = false;
|
||||||
|
|
||||||
|
if (audioRef.current && audioRef.current.isPlaying)
|
||||||
|
audioRef.current.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
if (!fearState.finaleTriggered) return;
|
||||||
|
|
||||||
|
const creature = meshRef.current;
|
||||||
|
if (!creature) return;
|
||||||
|
|
||||||
|
if (!isSpawned) {
|
||||||
|
setIsSpawned(true);
|
||||||
|
globalDistance.current = 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTriggered) {
|
||||||
|
if (globalDistance.current < 40) {
|
||||||
|
setHasTriggered(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTriggered) {
|
||||||
|
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * delta;
|
||||||
|
|
||||||
|
if (audioRef.current && !audioPlaying.current) {
|
||||||
|
audioPlaying.current = true;
|
||||||
|
if (audioRef.current.context.state === 'suspended')
|
||||||
|
audioRef.current.context.resume();
|
||||||
|
audioRef.current.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22;
|
||||||
|
camera.position.x += (Math.random() - 0.5) * shakeIntensity;
|
||||||
|
camera.position.y += (Math.random() - 0.5) * shakeIntensity;
|
||||||
|
|
||||||
|
if (globalDistance.current <= 0.1) {
|
||||||
|
window.location.href = '/';
|
||||||
|
fearState.registerCaught();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwardVector = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(forwardVector);
|
||||||
|
const lookDirZ = forwardVector.z < 0 ? -1 : 1;
|
||||||
|
|
||||||
|
const calculatedZ = camera.position.z + (lookDirZ * globalDistance.current);
|
||||||
|
|
||||||
|
creature.position.set(0, 1.6, calculatedZ);
|
||||||
|
creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
ref={meshRef}
|
||||||
|
visible={finaleTriggered}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[3.0, 4.8]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={texture}
|
||||||
|
transparent={true}
|
||||||
|
depthWrite={false}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{finaleTriggered && (
|
||||||
|
<PositionalAudio
|
||||||
|
url="fear/snd/riser.mp3"
|
||||||
|
ref={audioRef}
|
||||||
|
distance={25}
|
||||||
|
loop={false}
|
||||||
|
autoplay={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
|
import { useTexture } from "@react-three/drei";
|
||||||
|
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
|
||||||
|
interface DoorProps {
|
||||||
|
position: [number, number, number];
|
||||||
|
rotation: [number, number, number];
|
||||||
|
}
|
||||||
|
function Door({ position, rotation }: DoorProps) {
|
||||||
|
return (
|
||||||
|
<group position={position} rotation={rotation}>
|
||||||
|
<mesh position={[0, 2, -0.14]}>
|
||||||
|
<boxGeometry args={[2.4, 4.0, 0.2]} />
|
||||||
|
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0, 1.95, -0.08]}>
|
||||||
|
<boxGeometry args={[2.1, 3.8, 0.1]} />
|
||||||
|
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0.9, 1.8, 0.08]}>
|
||||||
|
<boxGeometry args={[0.08, 0.08, 0.15]} />
|
||||||
|
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Hallway() {
|
||||||
|
const [width, setWidth] = useState(fearState.currentWidth);
|
||||||
|
const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([
|
||||||
|
'fear/img/concrete-floor.png',
|
||||||
|
'fear/img/concrete-wall.png',
|
||||||
|
'fear/img/rust.png',
|
||||||
|
'fear/img/rust.png'
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
[floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => {
|
||||||
|
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
tex.minFilter = tex.magFilter = THREE.NearestFilter;
|
||||||
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
});
|
||||||
|
}, [floorTex, wallTex, rustWallTex, rustFloorTex]);
|
||||||
|
|
||||||
|
|
||||||
|
const segmentPool = [0, 1, 2, 3, 4];
|
||||||
|
const segmentCount = segmentPool.length;
|
||||||
|
|
||||||
|
const lightRefs = useRef<(THREE.PointLight | null)[]>([]);
|
||||||
|
const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]);
|
||||||
|
|
||||||
|
const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal');
|
||||||
|
const stateEndTime = useRef<number>(0);
|
||||||
|
const nextEventTime = useRef<number>(5);
|
||||||
|
|
||||||
|
const segmentsRef = useRef<THREE.Group[]>([]);
|
||||||
|
const wallMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
||||||
|
const floorMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
||||||
|
const pipeMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
||||||
|
const bracketMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
||||||
|
|
||||||
|
wallMaterialsRef.current = [];
|
||||||
|
floorMaterialsRef.current = [];
|
||||||
|
pipeMaterialsRef.current = [];
|
||||||
|
bracketMaterialsRef.current = [];
|
||||||
|
|
||||||
|
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
|
setWidth(fearState.currentWidth);
|
||||||
|
setIsRustActive(fearState.isRustActive);
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
/*
|
||||||
|
lights
|
||||||
|
*/
|
||||||
|
let intensity1 = 0.85 + Math.sin(time * 2) * 0.03;
|
||||||
|
if (time > nextEventTime.current && lightState.current === 'normal') {
|
||||||
|
lightState.current = 'flickering';
|
||||||
|
stateEndTime.current = time + 1.5 + Math.random() * 2;
|
||||||
|
}
|
||||||
|
if (lightState.current === 'flickering') {
|
||||||
|
if (time > stateEndTime.current) {
|
||||||
|
if (Math.random() > 0.4) {
|
||||||
|
lightState.current = 'dead';
|
||||||
|
stateEndTime.current = time + 1.0 + Math.random() * 2.5;
|
||||||
|
} else {
|
||||||
|
lightState.current = 'normal';
|
||||||
|
nextEventTime.current = time + 10 + Math.random() * 20;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3;
|
||||||
|
intensity1 = 0.5 + baseWave;
|
||||||
|
if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2) intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lightState.current === 'dead') {
|
||||||
|
if (time > stateEndTime.current) {
|
||||||
|
lightState.current = 'normal';
|
||||||
|
nextEventTime.current = time + 12 + Math.random() * 15;
|
||||||
|
} else {
|
||||||
|
intensity1 = Math.random() > 0.98 ? 0.08 : 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
objects
|
||||||
|
*/
|
||||||
|
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
||||||
|
const playerSegmentZ = Math.floor(state.camera.position.z / length);
|
||||||
|
|
||||||
|
const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
|
floorTex.repeat.set(horizontalTexRepeat, 10);
|
||||||
|
wallTex.repeat.set(10, 1);
|
||||||
|
rustWallTex.repeat.set(10, 1);
|
||||||
|
rustFloorTex.repeat.set(horizontalTexRepeat, 10);
|
||||||
|
|
||||||
|
floorTex.needsUpdate = true;
|
||||||
|
wallTex.needsUpdate = true;
|
||||||
|
rustWallTex.needsUpdate = true;
|
||||||
|
rustFloorTex.needsUpdate = true;
|
||||||
|
|
||||||
|
let closestPoolIndex = 0;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
segmentsRef.current.forEach((segGroup, poolIndex) => {
|
||||||
|
if (!segGroup) return;
|
||||||
|
|
||||||
|
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
|
||||||
|
segGroup.position.z = segmentZIndex * length;
|
||||||
|
|
||||||
|
const distance = Math.abs(segGroup.position.z - state.camera.position.z);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestPoolIndex = poolIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftWallGroup = segGroup.getObjectByName("left-wall-group");
|
||||||
|
if (leftWallGroup) leftWallGroup.position.x = -width / 2;
|
||||||
|
|
||||||
|
const rightWallGroup = segGroup.getObjectByName("right-wall-group");
|
||||||
|
if (rightWallGroup) rightWallGroup.position.x = width / 2;
|
||||||
|
|
||||||
|
const floorMesh = segGroup.getObjectByName("floor-mesh");
|
||||||
|
if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
|
|
||||||
|
const ceilingMesh = segGroup.getObjectByName("ceiling-mesh");
|
||||||
|
if (ceilingMesh) ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const pipe = segGroup.getObjectByName(`pipe-${i}`);
|
||||||
|
if (pipe) pipe.position.x = -width / 2 + 0.4 + (i * 0.20);
|
||||||
|
}
|
||||||
|
const bracketGroup = segGroup.getObjectByName("brackets-group");
|
||||||
|
if (bracketGroup) {
|
||||||
|
bracketGroup.children.forEach(b => {
|
||||||
|
b.position.x = -width / 2 + 0.6;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
dyn light
|
||||||
|
*/
|
||||||
|
segmentPool.forEach((poolIndex) => {
|
||||||
|
const light = lightRefs.current[poolIndex];
|
||||||
|
const mat = matRefs.current[poolIndex];
|
||||||
|
|
||||||
|
if (poolIndex === closestPoolIndex) {
|
||||||
|
if (light) light.intensity = intensity1 * 1.2;
|
||||||
|
if (mat) {
|
||||||
|
mat.emissiveIntensity = intensity1 * 2.5;
|
||||||
|
if (lightState.current !== 'normal') mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7));
|
||||||
|
else mat.emissive.setHex(0xa8a1a1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (light) light.intensity = 0.9;
|
||||||
|
if (mat) {
|
||||||
|
mat.emissiveIntensity = 0.8;
|
||||||
|
mat.emissive.setHex(0xa8a1a1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
materials
|
||||||
|
*/
|
||||||
|
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, targetRustTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => {
|
||||||
|
materials.forEach(mat => {
|
||||||
|
if (!mat) return;
|
||||||
|
const targetTex = isRustActive ? targetRustTex : defaultTex;
|
||||||
|
if (mat.map !== targetTex) {
|
||||||
|
mat.map = targetTex;
|
||||||
|
mat.needsUpdate = true;
|
||||||
|
}
|
||||||
|
mat.color.set(isRustActive ? activeColor : defaultColor);
|
||||||
|
mat.roughness = isRustActive ? activeRough : defaultRough;
|
||||||
|
mat.metalness = isRustActive ? activeMetal : defaultMetal;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMaterials(wallMaterialsRef.current, wallTex, rustWallTex, "#c5c0be", "#ffffff", 0.95, 0.7, 0.05, 0.1);
|
||||||
|
updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#ffffff", 0.95, 0.8, 0.05, 0.2);
|
||||||
|
|
||||||
|
pipeMaterialsRef.current.forEach(mat => {
|
||||||
|
if (!mat) return;
|
||||||
|
mat.color.set(isRustActive ? "#3d1b0f" : "#a5aca8");
|
||||||
|
mat.roughness = isRustActive ? 0.95 : 0.0;
|
||||||
|
mat.metalness = isRustActive ? 0.05 : 0.4;
|
||||||
|
});
|
||||||
|
|
||||||
|
bracketMaterialsRef.current.forEach(mat => {
|
||||||
|
if (!mat) return;
|
||||||
|
mat.color.set(isRustActive ? "#1b0b05" : "#a5aca8");
|
||||||
|
mat.roughness = isRustActive ? 0.95 : 0.0;
|
||||||
|
mat.metalness = isRustActive ? 0.05 : 0.4;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{segmentPool.map((poolIndex) => (
|
||||||
|
<group
|
||||||
|
key={poolIndex}
|
||||||
|
ref={(el) => { if (el) segmentsRef.current[poolIndex] = el; }}
|
||||||
|
position={[0, 0, 0]}
|
||||||
|
>
|
||||||
|
{/* lights */}
|
||||||
|
<group position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1, -FEAR_SETTINGS.HALLWAY_LENGTH / 4]}>
|
||||||
|
<pointLight
|
||||||
|
ref={(el) => { lightRefs.current[poolIndex] = el; }}
|
||||||
|
intensity={0.9}
|
||||||
|
distance={15}
|
||||||
|
color="#a8a1a1"
|
||||||
|
/>
|
||||||
|
<mesh position={[0, 0.09, 0]}>
|
||||||
|
<boxGeometry args={[0.3, 0.01, 0.3]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { matRefs.current[poolIndex] = el; }}
|
||||||
|
color="#111111"
|
||||||
|
emissive="#a8a1a1"
|
||||||
|
emissiveIntensity={0.8}
|
||||||
|
roughness={0.9}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* floor */}
|
||||||
|
<mesh
|
||||||
|
name="floor-mesh"
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
||||||
|
map={floorTex}
|
||||||
|
roughness={0.8}
|
||||||
|
metalness={0.2}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* ceiling */}
|
||||||
|
<mesh
|
||||||
|
name="ceiling-mesh"
|
||||||
|
rotation={[Math.PI / 2, 0, 0]}
|
||||||
|
position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
||||||
|
map={floorTex}
|
||||||
|
roughness={0.8}
|
||||||
|
metalness={0.2}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* left wall */}
|
||||||
|
<group name="left-wall-group">
|
||||||
|
<mesh rotation={[0, Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
||||||
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
||||||
|
map={wallTex}
|
||||||
|
roughness={0.7}
|
||||||
|
metalness={0.1}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
{!isRustActive && (
|
||||||
|
<>
|
||||||
|
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.25]} rotation={[0, Math.PI / 2, 0]} />
|
||||||
|
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.85]} rotation={[0, Math.PI / 2, 0]} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* right wall */}
|
||||||
|
<group name="right-wall-group">
|
||||||
|
<mesh rotation={[0, -Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
||||||
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
||||||
|
map={wallTex}
|
||||||
|
roughness={0.7}
|
||||||
|
metalness={0.1}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
{!isRustActive && (
|
||||||
|
<Door position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]} rotation={[0, -Math.PI / 2, 0]} />
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* pipes */}
|
||||||
|
{Array.from({ length: 3 }).map((_, idx) => (
|
||||||
|
<mesh
|
||||||
|
key={idx}
|
||||||
|
name={`pipe-${idx}`}
|
||||||
|
rotation={[Math.PI / 2, 0, 0]}
|
||||||
|
position={[-FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.4 + (idx * 0.20), FEAR_SETTINGS.HALLWAY_HEIGHT - 0.2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
||||||
|
>
|
||||||
|
<cylinderGeometry args={[0.06, 0.06, FEAR_SETTINGS.HALLWAY_LENGTH, 4]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => el && pipeMaterialsRef.current.push(el)}
|
||||||
|
color="#a5aca8"
|
||||||
|
roughness={0.0}
|
||||||
|
metalness={0.4}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* brackets */}
|
||||||
|
<group name="brackets-group">
|
||||||
|
{Array.from({ length: 5 }).map((_, idx) => {
|
||||||
|
const zOffset = -(idx * 8 + 4);
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
key={`bracket-${idx}`}
|
||||||
|
position={[-FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.6, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.15, zOffset]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[0.7, 0.3, 0.15]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => el && bracketMaterialsRef.current.push(el)}
|
||||||
|
color="#a5aca8"
|
||||||
|
roughness={0.0}
|
||||||
|
metalness={0.4}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
|
import { PointerLockControls } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
const forward = new THREE.Vector3();
|
||||||
|
const side = new THREE.Vector3();
|
||||||
|
const viewDirection = new THREE.Vector3();
|
||||||
|
const targetDest = new THREE.Vector3();
|
||||||
|
|
||||||
|
const playerRoot = new THREE.Vector3(0, FEAR_SETTINGS.PLAYER_HEIGHT, 0);
|
||||||
|
const targetVelocity = new THREE.Vector3();
|
||||||
|
const currentVelocity = new THREE.Vector3();
|
||||||
|
|
||||||
|
function usePlayerControls() {
|
||||||
|
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true;
|
||||||
|
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true;
|
||||||
|
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true;
|
||||||
|
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false;
|
||||||
|
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false;
|
||||||
|
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false;
|
||||||
|
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return keys.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Player() {
|
||||||
|
const { camera } = useThree();
|
||||||
|
const controls = usePlayerControls();
|
||||||
|
|
||||||
|
const flashlightRef = useRef<THREE.SpotLight>(null);
|
||||||
|
const movementCounter = useRef<number>(0);
|
||||||
|
const bobIntensity = useRef<number>(0);
|
||||||
|
|
||||||
|
const confirmedSegment = useRef<number>(0);
|
||||||
|
const hasTriggeredThisSegment = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
const dt = Math.min(delta, 0.1);
|
||||||
|
|
||||||
|
camera.getWorldDirection(forward);
|
||||||
|
forward.y = 0;
|
||||||
|
forward.normalize();
|
||||||
|
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
|
||||||
|
|
||||||
|
const moveForward = Number(controls.Forward) - Number(controls.Backward);
|
||||||
|
const moveSide = Number(controls.Right) - Number(controls.Left);
|
||||||
|
|
||||||
|
targetVelocity.set(0, 0, 0);
|
||||||
|
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
|
||||||
|
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
|
||||||
|
|
||||||
|
if (targetVelocity.lengthSq() > 0)
|
||||||
|
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
|
||||||
|
|
||||||
|
currentVelocity.lerp(targetVelocity, 10 * dt);
|
||||||
|
|
||||||
|
playerRoot.x += currentVelocity.x * dt;
|
||||||
|
playerRoot.z += currentVelocity.z * dt;
|
||||||
|
|
||||||
|
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
|
||||||
|
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
|
||||||
|
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
|
||||||
|
|
||||||
|
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
||||||
|
|
||||||
|
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
|
||||||
|
|
||||||
|
if (isMoving)
|
||||||
|
movementCounter.current += dt * 12;
|
||||||
|
|
||||||
|
const moveBobY = Math.sin(movementCounter.current) * 0.06 * bobIntensity.current;
|
||||||
|
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
|
||||||
|
|
||||||
|
const breatheTime = state.clock.elapsedTime * 1.8;
|
||||||
|
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
|
||||||
|
|
||||||
|
camera.position.copy(playerRoot);
|
||||||
|
camera.position.y += moveBobY + breatheBobY;
|
||||||
|
camera.position.addScaledVector(side, moveBobX);
|
||||||
|
|
||||||
|
if (flashlightRef.current) {
|
||||||
|
flashlightRef.current.position.copy(camera.position);
|
||||||
|
camera.getWorldDirection(viewDirection);
|
||||||
|
|
||||||
|
targetDest
|
||||||
|
.copy(camera.position)
|
||||||
|
.addScaledVector(viewDirection, 10);
|
||||||
|
|
||||||
|
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
|
||||||
|
flashlightRef.current.target.updateMatrixWorld();
|
||||||
|
|
||||||
|
flashlightRef.current.intensity =
|
||||||
|
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
||||||
|
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
||||||
|
const absoluteZ = -playerRoot.z;
|
||||||
|
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
||||||
|
const progressZ = ((absoluteZ % length) + length) % length / length;
|
||||||
|
|
||||||
|
if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) {
|
||||||
|
if (!hasTriggeredThisSegment.current) {
|
||||||
|
fearState.registerLoop('forward');
|
||||||
|
hasTriggeredThisSegment.current = true;
|
||||||
|
}
|
||||||
|
confirmedSegment.current = rawSegmentIndex;
|
||||||
|
}
|
||||||
|
else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) {
|
||||||
|
if (!hasTriggeredThisSegment.current) {
|
||||||
|
fearState.registerLoop('backward');
|
||||||
|
hasTriggeredThisSegment.current = true;
|
||||||
|
}
|
||||||
|
confirmedSegment.current = rawSegmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65) {
|
||||||
|
hasTriggeredThisSegment.current = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PointerLockControls />
|
||||||
|
<spotLight
|
||||||
|
ref={flashlightRef}
|
||||||
|
distance={25}
|
||||||
|
angle={0.35}
|
||||||
|
penumbra={0.8}
|
||||||
|
intensity={0}
|
||||||
|
color="#fffaed"
|
||||||
|
decay={2}
|
||||||
|
castShadow
|
||||||
|
shadow-bias={-0.001}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export const FEAR_SETTINGS = {
|
||||||
|
HALLWAY_LENGTH: 40,
|
||||||
|
HALLWAY_WIDTH: 6,
|
||||||
|
HALLWAY_HEIGHT: 5,
|
||||||
|
PLAYER_HEIGHT: 3,
|
||||||
|
PLAYER_SPEED: 4,
|
||||||
|
FLASHLIGHT_INTENSITY_BASE: 8,
|
||||||
|
WALL_BUFFER: 0.6,
|
||||||
|
CREATURE_SPEED: 8,
|
||||||
|
|
||||||
|
EVENT_NARROW_LOOP_COUNT: 2,
|
||||||
|
EVENT_RUST_LOOP_COUNT: 4,
|
||||||
|
EVENT_FINALE_LOOP_COUNT: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
export const fearState = {
|
||||||
|
loopCount: 0,
|
||||||
|
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
|
||||||
|
isRustActive: false,
|
||||||
|
finaleTriggered: false,
|
||||||
|
wasCaught: false,
|
||||||
|
|
||||||
|
subscribe(listener: () => void) {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => { listeners.delete(listener); };
|
||||||
|
},
|
||||||
|
|
||||||
|
emit() {
|
||||||
|
listeners.forEach((listener) => listener());
|
||||||
|
},
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
|
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
||||||
|
|
||||||
|
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
|
||||||
|
this.currentWidth = newWidth;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
registerLoop(direction: 'forward' | 'backward') {
|
||||||
|
this.loopCount += 1;
|
||||||
|
|
||||||
|
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
|
||||||
|
this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
|
||||||
|
|
||||||
|
this.emit();
|
||||||
|
},
|
||||||
|
|
||||||
|
registerCaught() {
|
||||||
|
this.wasCaught = true;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: '⛧',
|
title: '⛧',
|
||||||
description: ''
|
// description: '',
|
||||||
|
openGraph: {
|
||||||
|
// title: '⛧',
|
||||||
|
// description: '',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://neru.rip/img/ok.jpg',
|
||||||
|
width: 734,
|
||||||
|
height: 1104,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
themeColor: '#fbcfe8',
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children
|
children
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.canvas {
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: black;
|
||||||
|
z-index: 4444;
|
||||||
|
transition: opacity 1s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
will-change: opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.niko-spin {
|
||||||
|
width: 50vw;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
animation: spin 3s ease-in-out infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import './page.css';
|
||||||
|
|
||||||
|
import { Environment, OrbitControls, useProgress } from "@react-three/drei";
|
||||||
|
import { Canvas, useLoader } from '@react-three/fiber';
|
||||||
|
import { Bloom, BrightnessContrast, DepthOfField, EffectComposer, HueSaturation, LUT, Noise, SMAA, SSAO, Vignette } from '@react-three/postprocessing';
|
||||||
|
import { useLayoutEffect, useState } from "react";
|
||||||
|
import { folder, useControls, Leva } from 'leva';
|
||||||
|
import SealCube from './scene-components/sealcube';
|
||||||
|
import Terrain from './scene-components/terrain';
|
||||||
|
import { LUTCubeLoader } from 'three/examples/jsm/Addons.js';
|
||||||
|
import { AmbientSound } from './scene-components/ambient-sound';
|
||||||
|
|
||||||
|
function Loader() {
|
||||||
|
const { progress, active } = useProgress();
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!active && progress === 100) {
|
||||||
|
const timeout = setTimeout(() => setVisible(false), 500);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [progress, active]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`loader ${!visible ? 'loader hidden' : ''}`}>
|
||||||
|
<picture>
|
||||||
|
<img src='niko/img/niko.jpg' className='niko-spin' />
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Scene() {
|
||||||
|
const {
|
||||||
|
terrainDryColor,
|
||||||
|
terrainLushColor,
|
||||||
|
chunks,
|
||||||
|
chunkSize,
|
||||||
|
resolution,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
grassDryColor,
|
||||||
|
grassLushColor,
|
||||||
|
grassCount,
|
||||||
|
grassSize,
|
||||||
|
grassLOD,
|
||||||
|
grassBlades,
|
||||||
|
grassSegments,
|
||||||
|
grassLODStart,
|
||||||
|
grassLODExponent
|
||||||
|
} = useControls('Environment', {
|
||||||
|
Terrain: folder({
|
||||||
|
terrainDryColor: '#232a0c',
|
||||||
|
terrainLushColor: '#142a14',
|
||||||
|
chunks: { value: 16, min: 4, max: 24, step: 2 },
|
||||||
|
chunkSize: { value: 10.0, min: 5.0, max: 40.0, step: 1.0 },
|
||||||
|
resolution: { value: 8.0, min: 4.0, max: 30.0, step: 1.0 },
|
||||||
|
hillScale: { value: 0.15, min: 0.01, max: 0.5, step: 0.01 },
|
||||||
|
hillHeight: { value: 4.0, min: 0.0, max: 20.0, step: 0.5 },
|
||||||
|
detailScale: { value: 1.0, min: 0.1, max: 5.0, step: 0.1 },
|
||||||
|
detailHeight: { value: 0.3, min: 0.0, max: 2.0, step: 0.05 },
|
||||||
|
}),
|
||||||
|
Grass: folder({
|
||||||
|
grassDryColor: '#495a17',
|
||||||
|
grassLushColor: '#255825',
|
||||||
|
grassCount: { value: 8000, min: 1000, max: 30000, step: 500 },
|
||||||
|
grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 },
|
||||||
|
grassLOD: { value: 60, min: 10, max: 200, step: 5 },
|
||||||
|
grassBlades: { value: 3, min: 1, max: 5, step: 1 },
|
||||||
|
grassSegments: { value: 4, min: 1, max: 5, step: 1 },
|
||||||
|
grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 },
|
||||||
|
grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 },
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Environment
|
||||||
|
files={'niko/hdr/sky.hdr'}
|
||||||
|
environmentIntensity={0.85}
|
||||||
|
background
|
||||||
|
/>
|
||||||
|
|
||||||
|
<fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} />
|
||||||
|
|
||||||
|
<ambientLight intensity={0.5} />
|
||||||
|
<directionalLight
|
||||||
|
position={[15, 25, 15]}
|
||||||
|
intensity={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Terrain
|
||||||
|
chunks={chunks}
|
||||||
|
chunkSize={chunkSize}
|
||||||
|
resolution={resolution}
|
||||||
|
scale={1}
|
||||||
|
hillScale={hillScale}
|
||||||
|
hillHeight={hillHeight}
|
||||||
|
detailScale={detailScale}
|
||||||
|
detailHeight={detailHeight}
|
||||||
|
grassCount={grassCount}
|
||||||
|
grassSize={grassSize}
|
||||||
|
grassLOD={grassLOD}
|
||||||
|
terrainDryColor={terrainDryColor}
|
||||||
|
terrainLushColor={terrainLushColor}
|
||||||
|
grassDryColor={grassDryColor}
|
||||||
|
grassLushColor={grassLushColor}
|
||||||
|
grassBlades={grassBlades}
|
||||||
|
grassSegments={grassSegments}
|
||||||
|
grassLODStart={grassLODStart}
|
||||||
|
grassLODExponent={grassLODExponent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SealCube />
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LutEffect() {
|
||||||
|
const lutTexture = useLoader(LUTCubeLoader, 'niko/lut/Landscape6.cube');
|
||||||
|
return <LUT lut={lutTexture.texture3D} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostProcessing() {
|
||||||
|
return (<EffectComposer>
|
||||||
|
<DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} />
|
||||||
|
<Vignette />
|
||||||
|
<Noise opacity={0.05} />
|
||||||
|
<Bloom
|
||||||
|
intensity={0.8}
|
||||||
|
luminanceThreshold={0.4}
|
||||||
|
luminanceSmoothing={0.5}
|
||||||
|
/>
|
||||||
|
<SMAA />
|
||||||
|
<HueSaturation saturation={0.3} />
|
||||||
|
<BrightnessContrast brightness={0.05} contrast={-0.1} />
|
||||||
|
<LutEffect />
|
||||||
|
</EffectComposer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Seal() {
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Leva hidden={isProduction} />
|
||||||
|
|
||||||
|
<Loader />
|
||||||
|
<Canvas
|
||||||
|
shadows
|
||||||
|
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
|
||||||
|
gl={{ antialias: false, powerPreference: "high-performance" }}
|
||||||
|
className='canvas'
|
||||||
|
>
|
||||||
|
<AmbientSound url="niko/snd/wind.mp3" volume={0.4} />
|
||||||
|
<AmbientSound url="niko/snd/birds.mp3" volume={0.1} />
|
||||||
|
|
||||||
|
<Scene />
|
||||||
|
<PostProcessing />
|
||||||
|
|
||||||
|
<OrbitControls
|
||||||
|
target={[0, 3, 0]}
|
||||||
|
enablePan={false}
|
||||||
|
makeDefault
|
||||||
|
minDistance={2}
|
||||||
|
maxDistance={6}
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
interface AmbientSoundProps {
|
||||||
|
url: string
|
||||||
|
volume?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = new Audio(url)
|
||||||
|
audio.loop = true
|
||||||
|
audio.volume = volume
|
||||||
|
audioRef.current = audio
|
||||||
|
|
||||||
|
const startAudio = () => {
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn('Autoplay blocked. Waiting for user interaction.', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
startAudio()
|
||||||
|
|
||||||
|
window.addEventListener('click', startAudio, { once: true })
|
||||||
|
window.addEventListener('keydown', startAudio, { once: true })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', startAudio)
|
||||||
|
window.removeEventListener('keydown', startAudio)
|
||||||
|
audio.pause()
|
||||||
|
audioRef.current = null
|
||||||
|
}
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.volume = volume
|
||||||
|
}
|
||||||
|
}, [volume])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { useFrame, useLoader } from "@react-three/fiber";
|
||||||
|
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||||
|
import { BufferAttribute, BufferGeometry, Color, DoubleSide, InstancedMesh, MeshStandardMaterial, Object3D, TextureLoader } from "three";
|
||||||
|
import { getTerrainHeight, Shader } from "./helpers";
|
||||||
|
|
||||||
|
import grassVert from './shaders/grass.vert';
|
||||||
|
import grassFrag from './shaders/grass.frag';
|
||||||
|
|
||||||
|
interface GrassProps {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
size: number;
|
||||||
|
count: number;
|
||||||
|
grassSize: number;
|
||||||
|
scale: number;
|
||||||
|
hillScale: number;
|
||||||
|
hillHeight: number;
|
||||||
|
detailScale: number;
|
||||||
|
detailHeight: number;
|
||||||
|
noise2D: (x: number, y: number) => number;
|
||||||
|
grassLOD: number;
|
||||||
|
dryColor: string;
|
||||||
|
lushColor: string;
|
||||||
|
grassBlades?: number;
|
||||||
|
grassSegments?: number;
|
||||||
|
grassLODStart?: number;
|
||||||
|
grassLODExponent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Grass({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
count,
|
||||||
|
grassSize,
|
||||||
|
scale,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
noise2D,
|
||||||
|
grassLOD,
|
||||||
|
dryColor = '#556b19',
|
||||||
|
lushColor = '#348a34',
|
||||||
|
grassBlades = 3,
|
||||||
|
grassSegments = 4,
|
||||||
|
grassLODStart = 0.5,
|
||||||
|
grassLODExponent = 1.0
|
||||||
|
}: GrassProps) {
|
||||||
|
const meshRef = useRef<InstancedMesh>(null);
|
||||||
|
const dummyRef = useRef<Object3D>(new Object3D());
|
||||||
|
|
||||||
|
const [alphaMap, normalMap] = useLoader(TextureLoader, [
|
||||||
|
'niko/img/grass_alpha.png',
|
||||||
|
'niko/img/grass_normal.png'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const materialRef = useRef<
|
||||||
|
MeshStandardMaterial & { userData: { shader: Shader } }
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (
|
||||||
|
materialRef.current &&
|
||||||
|
materialRef.current.userData &&
|
||||||
|
materialRef.current.userData.shader
|
||||||
|
) {
|
||||||
|
(materialRef.current.userData.shader as Shader).uniforms.uTime.value =
|
||||||
|
state.clock.getElapsedTime();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const geometry = useMemo(() => {
|
||||||
|
const geo = new BufferGeometry();
|
||||||
|
|
||||||
|
const w = 0.5;
|
||||||
|
const h = 2;
|
||||||
|
const segments = grassSegments;
|
||||||
|
const bladesCount = grassBlades;
|
||||||
|
|
||||||
|
const positions: number[] = [];
|
||||||
|
const uvs: number[] = [];
|
||||||
|
const indices: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < bladesCount; i++) {
|
||||||
|
const angle = (Math.PI / bladesCount) * i;
|
||||||
|
const sinA = Math.sin(angle);
|
||||||
|
const cosA = Math.cos(angle);
|
||||||
|
|
||||||
|
const offset = positions.length / 3;
|
||||||
|
|
||||||
|
for (let y = 0; y <= segments; y++) {
|
||||||
|
const v = y / segments;
|
||||||
|
const yPos = v * h;
|
||||||
|
|
||||||
|
positions.push(-w * cosA, yPos, -w * sinA);
|
||||||
|
uvs.push(0, v);
|
||||||
|
|
||||||
|
positions.push(w * cosA, yPos, w * sinA);
|
||||||
|
uvs.push(1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = 0; y < segments; y++) {
|
||||||
|
const row1 = offset + y * 2;
|
||||||
|
const row2 = offset + (y + 1) * 2;
|
||||||
|
|
||||||
|
indices.push(row1, row1 + 1, row2);
|
||||||
|
indices.push(row1 + 1, row2 + 1, row2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geo.setAttribute(
|
||||||
|
'position',
|
||||||
|
new BufferAttribute(new Float32Array(positions), 3)
|
||||||
|
);
|
||||||
|
geo.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
|
||||||
|
geo.setIndex(indices);
|
||||||
|
geo.computeVertexNormals();
|
||||||
|
|
||||||
|
return geo;
|
||||||
|
}, [grassBlades, grassSegments]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const dummy = dummyRef.current;
|
||||||
|
|
||||||
|
const worldXBase = x * size;
|
||||||
|
const worldZBase = y * size;
|
||||||
|
|
||||||
|
const color = new Color();
|
||||||
|
const lushColorObj = new Color(lushColor);
|
||||||
|
const dryColorObj = new Color(dryColor);
|
||||||
|
|
||||||
|
let instanceIndex = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const localX = (Math.random() - 0.5) * size;
|
||||||
|
const localZ = (Math.random() - 0.5) * size;
|
||||||
|
|
||||||
|
const globalX = worldXBase + localX;
|
||||||
|
const globalZ = worldZBase + localZ;
|
||||||
|
|
||||||
|
const dist = Math.sqrt(globalX * globalX + globalZ * globalZ);
|
||||||
|
|
||||||
|
const maxDist = grassLOD;
|
||||||
|
|
||||||
|
const falloffStart = maxDist * grassLODStart;
|
||||||
|
const falloffEnd = maxDist;
|
||||||
|
|
||||||
|
let fallofFactor = (dist - falloffStart) / (falloffEnd - falloffStart);
|
||||||
|
fallofFactor = Math.max(0, Math.min(1, fallofFactor));
|
||||||
|
|
||||||
|
const density = Math.pow(1.0 - fallofFactor, grassLODExponent);
|
||||||
|
|
||||||
|
if (Math.random() > density) continue;
|
||||||
|
|
||||||
|
const localY = getTerrainHeight(
|
||||||
|
localX,
|
||||||
|
localZ,
|
||||||
|
worldXBase,
|
||||||
|
worldZBase,
|
||||||
|
scale,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
noise2D
|
||||||
|
);
|
||||||
|
|
||||||
|
dummy.position.set(localX, localY, localZ);
|
||||||
|
|
||||||
|
dummy.rotation.y = Math.random() * Math.PI * 2;
|
||||||
|
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
|
||||||
|
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
|
||||||
|
|
||||||
|
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
|
||||||
|
const t = (noiseVal + 1) / 2;
|
||||||
|
const randomInternal = (Math.random() - 0.5) * 0.2;
|
||||||
|
const finalT = Math.max(0, Math.min(1, t + randomInternal));
|
||||||
|
color.lerpColors(dryColorObj, lushColorObj, finalT);
|
||||||
|
meshRef.current.setColorAt(instanceIndex, color);
|
||||||
|
|
||||||
|
const heightNoise = noise2D(globalX * 0.08, globalZ * 0.08);
|
||||||
|
const macroHeight = (heightNoise + 1.0) * 0.5; // 0..1
|
||||||
|
const microNoise = noise2D(globalX * 0.3, globalZ * 0.3);
|
||||||
|
const microHeight = (microNoise + 1.0) * 0.25; // 0..0.5
|
||||||
|
const perBladeRandom = Math.random() * 0.4;
|
||||||
|
|
||||||
|
const grassWidth = grassSize * (0.7 + Math.random() * 0.5);
|
||||||
|
const grassHeight = grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
|
||||||
|
|
||||||
|
dummy.scale.set(grassWidth, grassHeight, grassWidth);
|
||||||
|
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
|
||||||
|
|
||||||
|
instanceIndex++;
|
||||||
|
}
|
||||||
|
meshRef.current.count = instanceIndex;
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
if (meshRef.current.instanceColor)
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}, [
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
count,
|
||||||
|
grassSize,
|
||||||
|
scale,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
noise2D,
|
||||||
|
grassLOD,
|
||||||
|
dryColor,
|
||||||
|
lushColor,
|
||||||
|
grassLODStart,
|
||||||
|
grassLODExponent
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onBeforeCompile = useMemo(
|
||||||
|
() => (shader: Shader) => {
|
||||||
|
shader.uniforms.uTime = { value: 0 };
|
||||||
|
|
||||||
|
shader.vertexShader = `
|
||||||
|
uniform float uTime;
|
||||||
|
varying vec2 vGrassUv;
|
||||||
|
varying vec3 vWorldPos;
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
float noise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
f = f * f * (3.0 - 2.0 * f);
|
||||||
|
float a = hash(i);
|
||||||
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
float value = 0.0;
|
||||||
|
float amplitude = 0.5;
|
||||||
|
float frequency = 1.0;
|
||||||
|
for(int i = 0; i < 4; i++) {
|
||||||
|
value += amplitude * noise(p * frequency);
|
||||||
|
frequency *= 2.0;
|
||||||
|
amplitude *= 0.5;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
${shader.vertexShader}
|
||||||
|
`;
|
||||||
|
|
||||||
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
|
'#include <begin_vertex>',
|
||||||
|
`
|
||||||
|
#include <begin_vertex>
|
||||||
|
${grassVert}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
shader.fragmentShader = `
|
||||||
|
uniform float uTime;
|
||||||
|
varying vec2 vGrassUv;
|
||||||
|
varying vec3 vWorldPos;
|
||||||
|
${shader.fragmentShader}
|
||||||
|
`;
|
||||||
|
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
'#include <color_fragment>',
|
||||||
|
`
|
||||||
|
#include <color_fragment>
|
||||||
|
${grassFrag}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (materialRef.current) {
|
||||||
|
materialRef.current.userData.shader = shader;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh
|
||||||
|
ref={meshRef}
|
||||||
|
args={[undefined, undefined, count]}
|
||||||
|
position={[x * size, 0, y * size]}
|
||||||
|
geometry={geometry}
|
||||||
|
>
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={materialRef}
|
||||||
|
color='#ffffff'
|
||||||
|
side={DoubleSide}
|
||||||
|
alphaMap={alphaMap}
|
||||||
|
alphaTest={0.5}
|
||||||
|
normalMap={normalMap}
|
||||||
|
roughness={0.8}
|
||||||
|
metalness={0.1}
|
||||||
|
onBeforeCompile={onBeforeCompile}
|
||||||
|
/>
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export function getTerrainHeight(
|
||||||
|
localX: number,
|
||||||
|
localZ: number,
|
||||||
|
worldXBase: number,
|
||||||
|
worldZBase: number,
|
||||||
|
scale: number,
|
||||||
|
hillScale: number,
|
||||||
|
hillHeight: number,
|
||||||
|
detailScale: number,
|
||||||
|
detailHeight: number,
|
||||||
|
noise2D: (x: number, y: number) => number
|
||||||
|
) {
|
||||||
|
const worldX = (worldXBase + localX) * 0.1;
|
||||||
|
const worldZ = (worldZBase + localZ) * 0.1;
|
||||||
|
|
||||||
|
const noiseHill =
|
||||||
|
noise2D(worldX * hillScale, worldZ * hillScale) * hillHeight;
|
||||||
|
const noiseDetail =
|
||||||
|
noise2D(worldX * detailScale, worldZ * detailScale) * detailHeight;
|
||||||
|
|
||||||
|
return (noiseHill + noiseDetail) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shader {
|
||||||
|
uniforms: { [key: string]: { value: unknown } };
|
||||||
|
vertexShader: string;
|
||||||
|
fragmentShader: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useFrame, useLoader } from "@react-three/fiber";
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
import { Mesh, TextureLoader } from "three";
|
||||||
|
|
||||||
|
const SealCube = forwardRef<Mesh>((props, ref) => {
|
||||||
|
const texture = useLoader(TextureLoader, 'niko/img/niko.jpg');
|
||||||
|
const meshRef = useRef<Mesh>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => meshRef.current!, []);
|
||||||
|
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
meshRef.current.rotation.x += delta * 0.5;
|
||||||
|
meshRef.current.rotation.y += delta * 0.5;
|
||||||
|
meshRef.current.position.y = 3 + Math.sin(state.clock.getElapsedTime() * 1) * 0.15;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
|
||||||
|
<boxGeometry args={[0.85, 0.85, 0.85]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
map={texture}
|
||||||
|
roughness={0.4}
|
||||||
|
metalness={0.1}
|
||||||
|
envMapIntensity={1.2}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SealCube.displayName = 'SealCube';
|
||||||
|
|
||||||
|
export default SealCube;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
float ao = smoothstep(0.0, 0.7, vGrassUv.y);
|
||||||
|
ao = mix(0.05, 1.0, pow(ao, 1.6));
|
||||||
|
|
||||||
|
vec3 rootColor = diffuseColor.rgb * 0.15;
|
||||||
|
vec3 midColor = diffuseColor.rgb;
|
||||||
|
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.06, 0.08, 0.0);
|
||||||
|
|
||||||
|
float heightParam = vGrassUv.y;
|
||||||
|
vec3 grassColor;
|
||||||
|
if (heightParam < 0.4) {
|
||||||
|
float t = smoothstep(0.0, 0.4, heightParam);
|
||||||
|
grassColor = mix(rootColor, midColor, t);
|
||||||
|
} else {
|
||||||
|
float t = smoothstep(0.4, 1.0, heightParam);
|
||||||
|
grassColor = mix(midColor, tipColor, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 viewDir = normalize(cameraPosition - vWorldPos);
|
||||||
|
vec3 lightDir = normalize(vec3(15.0, 25.0, 15.0));
|
||||||
|
|
||||||
|
float VdotL = max(0.0, dot(viewDir, -lightDir));
|
||||||
|
float sss = pow(VdotL, 3.0) * smoothstep(0.2, 0.9, vGrassUv.y);
|
||||||
|
|
||||||
|
vec3 sssColor = diffuseColor.rgb * vec3(0.6, 1.0, 0.15) * 1.8;
|
||||||
|
grassColor += sssColor * sss * 2.0;
|
||||||
|
|
||||||
|
float NdotV = 1.0 - max(0.0, dot(normalize(vNormal), viewDir));
|
||||||
|
float rim = pow(NdotV, 3.0) * smoothstep(0.3, 1.0, vGrassUv.y) * 0.15;
|
||||||
|
grassColor += vec3(0.3, 0.5, 0.1) * rim;
|
||||||
|
|
||||||
|
diffuseColor.rgb = grassColor * ao;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
vGrassUv = uv;
|
||||||
|
|
||||||
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
float gx = worldPos.x;
|
float gx = worldPos.x;
|
||||||
float gz = worldPos.z;
|
float gz = worldPos.z;
|
||||||
@@ -19,19 +21,32 @@ float spring = sin(uTime * 2.0 + phase) * 0.06 + sin(uTime * 4.5 + phase * 1.5)
|
|||||||
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
|
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
|
||||||
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
|
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
|
||||||
|
|
||||||
float taperFactor = pow(uv.y, 4.0);
|
// taper (fade)
|
||||||
float taper = 1.0 - taperFactor * 0.6;
|
float taperFactor = uv.y * uv.y * uv.y;
|
||||||
|
float taper = 1.0 - taperFactor * 0.85;
|
||||||
transformed.x *= taper;
|
transformed.x *= taper;
|
||||||
transformed.z *= taper;
|
transformed.z *= taper;
|
||||||
|
|
||||||
|
// curve
|
||||||
float curveVal = fbm(vec2(gx, gz) * 0.5);
|
float curveVal = fbm(vec2(gx, gz) * 0.5);
|
||||||
float curveStrength = 2.0 + curveVal * 2.0;
|
float curveStrength = 1.5 + curveVal * 2.5;
|
||||||
float curveAmount = uv.y * uv.y * curveStrength;
|
float curveAmount = uv.y * uv.y * curveStrength;
|
||||||
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
|
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
|
||||||
transformed.x += curveAmount * curveDir.x * 0.5;
|
transformed.x += curveAmount * curveDir.x * 0.4;
|
||||||
transformed.z += curveAmount * curveDir.y * 0.5;
|
transformed.z += curveAmount * curveDir.y * 0.4;
|
||||||
|
|
||||||
|
// sway
|
||||||
float swayAmount = (totalWind + spring) * uv.y * uv.y;
|
float swayAmount = (totalWind + spring) * uv.y * uv.y;
|
||||||
transformed.x += swayAmount * windDir.x;
|
transformed.x += swayAmount * windDir.x;
|
||||||
transformed.z += swayAmount * windDir.y;
|
transformed.z += swayAmount * windDir.y;
|
||||||
transformed.y -= abs(swayAmount) * 0.2;
|
transformed.y -= abs(swayAmount) * 0.2;
|
||||||
|
|
||||||
|
// normal comp
|
||||||
|
vec2 totalBend = curveDir * curveAmount * 0.4 + windDir * swayAmount;
|
||||||
|
float bendMag = length(totalBend);
|
||||||
|
vec3 bentNormal = normalize(vec3(-totalBend.x * 0.5, 1.0, -totalBend.y * 0.5));
|
||||||
|
|
||||||
|
// normal mix
|
||||||
|
objectNormal = normalize(mix(vec3(0.0, 1.0, 0.0), bentNormal, uv.y));
|
||||||
|
|
||||||
|
vWorldPos = (modelMatrix * instanceMatrix * vec4(transformed, 1.0)).xyz;
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import { useMemo, useRef } from "react";
|
||||||
|
import { BufferAttribute, BufferGeometry, Color, Mesh } from "three";
|
||||||
|
import { getTerrainHeight } from "./helpers";
|
||||||
|
import Grass from "./grass";
|
||||||
|
import { createNoise2D } from "simplex-noise";
|
||||||
|
|
||||||
|
interface TerrainChunkProps {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
size: number;
|
||||||
|
resolution: number;
|
||||||
|
scale: number;
|
||||||
|
hillScale: number;
|
||||||
|
hillHeight: number;
|
||||||
|
detailScale: number;
|
||||||
|
detailHeight: number;
|
||||||
|
noise2D: (x: number, y: number) => number;
|
||||||
|
grassCount: number;
|
||||||
|
grassSize: number;
|
||||||
|
grassLOD: number;
|
||||||
|
terrainDryColor: string;
|
||||||
|
terrainLushColor: string;
|
||||||
|
grassDryColor: string;
|
||||||
|
grassLushColor: string;
|
||||||
|
grassBlades: number;
|
||||||
|
grassSegments: number;
|
||||||
|
grassLODStart: number;
|
||||||
|
grassLODExponent: number;
|
||||||
|
}
|
||||||
|
function TerrainChunk({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
resolution,
|
||||||
|
scale,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
noise2D,
|
||||||
|
grassCount,
|
||||||
|
grassSize,
|
||||||
|
grassLOD,
|
||||||
|
terrainDryColor,
|
||||||
|
terrainLushColor,
|
||||||
|
grassDryColor,
|
||||||
|
grassLushColor,
|
||||||
|
grassBlades,
|
||||||
|
grassSegments,
|
||||||
|
grassLODStart,
|
||||||
|
grassLODExponent
|
||||||
|
}: TerrainChunkProps) {
|
||||||
|
const halfSize = size / 2;
|
||||||
|
const worldXBase = x * size;
|
||||||
|
const worldZBase = y * size;
|
||||||
|
|
||||||
|
const minX = Math.abs(worldXBase) <= halfSize ? 0 : Math.abs(worldXBase) - halfSize;
|
||||||
|
const minZ = Math.abs(worldZBase) <= halfSize ? 0 : Math.abs(worldZBase) - halfSize;
|
||||||
|
const chunkMinDist = Math.sqrt(minX * minX + minZ * minZ);
|
||||||
|
|
||||||
|
const shouldRenderGrass = chunkMinDist < grassLOD;
|
||||||
|
|
||||||
|
const meshRef = useRef<Mesh>(null);
|
||||||
|
|
||||||
|
const geometry = useMemo(() => {
|
||||||
|
const geo = new BufferGeometry();
|
||||||
|
|
||||||
|
const vertices: Array<number> = [];
|
||||||
|
const indices: Array<number> = [];
|
||||||
|
|
||||||
|
const step = size / (resolution - 1);
|
||||||
|
const halfSize = size / 2;
|
||||||
|
const worldXBase = x * size;
|
||||||
|
const worldZBase = y * size;
|
||||||
|
|
||||||
|
/*
|
||||||
|
vtx gen
|
||||||
|
*/
|
||||||
|
for (let iz = 0; iz < resolution; iz++) {
|
||||||
|
for (let ix = 0; ix < resolution; ix++) {
|
||||||
|
const localX = ix * step - halfSize;
|
||||||
|
const localZ = iz * step - halfSize;
|
||||||
|
|
||||||
|
const localY = getTerrainHeight(
|
||||||
|
localX,
|
||||||
|
localZ,
|
||||||
|
worldXBase,
|
||||||
|
worldZBase,
|
||||||
|
scale,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
noise2D
|
||||||
|
);
|
||||||
|
|
||||||
|
vertices.push(localX, localY, localZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
idx gen
|
||||||
|
*/
|
||||||
|
for (let iz = 0; iz < resolution - 1; iz++) {
|
||||||
|
for (let ix = 0; ix < resolution - 1; ix++) {
|
||||||
|
const topLeft = iz * resolution + ix;
|
||||||
|
const topRight = topLeft + 1;
|
||||||
|
const bottomLeft = (iz + 1) * resolution + ix;
|
||||||
|
const bottomRight = bottomLeft + 1;
|
||||||
|
|
||||||
|
indices.push(topLeft, bottomLeft, topRight);
|
||||||
|
indices.push(topRight, bottomLeft, bottomRight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
geo.setAttribute(
|
||||||
|
'position',
|
||||||
|
new BufferAttribute(new Float32Array(vertices), 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
const colors: Array<number> = [];
|
||||||
|
for (let iz = 0; iz < resolution; iz++) {
|
||||||
|
for (let ix = 0; ix < resolution; ix++) {
|
||||||
|
const localX = ix * step - halfSize;
|
||||||
|
const localZ = iz * step - halfSize;
|
||||||
|
const globalX = worldXBase + localX;
|
||||||
|
const globalZ = worldZBase + localZ;
|
||||||
|
const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02);
|
||||||
|
const t = (colorNoise + 1) / 2;
|
||||||
|
|
||||||
|
const dryGreen = new Color(terrainDryColor);
|
||||||
|
const lushGreen = new Color(terrainLushColor);
|
||||||
|
|
||||||
|
const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t;
|
||||||
|
const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t;
|
||||||
|
const b = dryGreen.b + (lushGreen.b - dryGreen.b) * t;
|
||||||
|
|
||||||
|
colors.push(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
geo.setAttribute('color', new BufferAttribute(new Float32Array(colors), 3));
|
||||||
|
|
||||||
|
geo.setIndex(indices);
|
||||||
|
geo.computeVertexNormals();
|
||||||
|
|
||||||
|
return geo;
|
||||||
|
}, [
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
resolution,
|
||||||
|
scale,
|
||||||
|
hillScale,
|
||||||
|
hillHeight,
|
||||||
|
detailScale,
|
||||||
|
detailHeight,
|
||||||
|
noise2D,
|
||||||
|
terrainDryColor,
|
||||||
|
terrainLushColor
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<mesh
|
||||||
|
ref={meshRef}
|
||||||
|
geometry={geometry}
|
||||||
|
position={[x * size, 0, y * size]}
|
||||||
|
>
|
||||||
|
<meshStandardMaterial vertexColors roughness={1.0} metalness={0.0} />
|
||||||
|
</mesh>
|
||||||
|
{shouldRenderGrass && (
|
||||||
|
<Grass
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
size={size}
|
||||||
|
count={grassCount}
|
||||||
|
grassSize={grassSize}
|
||||||
|
scale={scale}
|
||||||
|
hillScale={hillScale}
|
||||||
|
hillHeight={hillHeight}
|
||||||
|
detailScale={detailScale}
|
||||||
|
detailHeight={detailHeight}
|
||||||
|
noise2D={noise2D}
|
||||||
|
grassLOD={grassLOD}
|
||||||
|
dryColor={grassDryColor}
|
||||||
|
lushColor={grassLushColor}
|
||||||
|
grassBlades={grassBlades}
|
||||||
|
grassSegments={grassSegments}
|
||||||
|
grassLODStart={grassLODStart}
|
||||||
|
grassLODExponent={grassLODExponent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TerrainProps {
|
||||||
|
chunks?: number;
|
||||||
|
chunkSize?: number;
|
||||||
|
resolution?: number;
|
||||||
|
scale?: number;
|
||||||
|
hillScale: number;
|
||||||
|
hillHeight: number;
|
||||||
|
detailScale: number;
|
||||||
|
detailHeight: number;
|
||||||
|
grassCount?: number;
|
||||||
|
grassSize?: number;
|
||||||
|
grassLOD?: number;
|
||||||
|
terrainDryColor?: string;
|
||||||
|
terrainLushColor?: string;
|
||||||
|
grassDryColor?: string;
|
||||||
|
grassLushColor?: string;
|
||||||
|
grassBlades?: number;
|
||||||
|
grassSegments?: number;
|
||||||
|
grassLODStart?: number;
|
||||||
|
grassLODExponent?: number;
|
||||||
|
}
|
||||||
|
export default function Terrain({
|
||||||
|
chunks = 5,
|
||||||
|
chunkSize = 10,
|
||||||
|
resolution = 8,
|
||||||
|
scale = 1,
|
||||||
|
hillScale = 0.2,
|
||||||
|
hillHeight = 3,
|
||||||
|
detailScale = 3,
|
||||||
|
detailHeight = 0.1,
|
||||||
|
grassCount = 6000,
|
||||||
|
grassSize = 0.6,
|
||||||
|
grassLOD = 40,
|
||||||
|
terrainDryColor = '#010100',
|
||||||
|
terrainLushColor = '#000800',
|
||||||
|
grassDryColor = '#556b19',
|
||||||
|
grassLushColor = '#348a34',
|
||||||
|
grassBlades = 2,
|
||||||
|
grassSegments = 2,
|
||||||
|
grassLODStart = 0.5,
|
||||||
|
grassLODExponent = 1.0
|
||||||
|
}: TerrainProps) {
|
||||||
|
const noise2D = useMemo(() => createNoise2D(), []);
|
||||||
|
const offset = -Math.floor(chunks / 2);
|
||||||
|
|
||||||
|
const chunkPositions = useMemo(() => {
|
||||||
|
const positions: [number, number][] = [];
|
||||||
|
for (let x = 0; x < chunks; x++)
|
||||||
|
for (let y = 0; y < chunks; y++) positions.push([x + offset, y + offset]);
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}, [chunks, offset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{chunkPositions.map(([x, y], index) => (
|
||||||
|
<TerrainChunk
|
||||||
|
key={`${x}-${y}-${index}`}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
size={chunkSize}
|
||||||
|
resolution={resolution}
|
||||||
|
scale={scale}
|
||||||
|
hillScale={hillScale}
|
||||||
|
hillHeight={hillHeight}
|
||||||
|
detailScale={detailScale}
|
||||||
|
detailHeight={detailHeight}
|
||||||
|
noise2D={noise2D}
|
||||||
|
grassCount={grassCount}
|
||||||
|
grassSize={grassSize}
|
||||||
|
grassLOD={grassLOD}
|
||||||
|
terrainDryColor={terrainDryColor}
|
||||||
|
terrainLushColor={terrainLushColor}
|
||||||
|
grassDryColor={grassDryColor}
|
||||||
|
grassLushColor={grassLushColor}
|
||||||
|
grassBlades={grassBlades}
|
||||||
|
grassSegments={grassSegments}
|
||||||
|
grassLODStart={grassLODStart}
|
||||||
|
grassLODExponent={grassLODExponent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,46 +1,313 @@
|
|||||||
.canvas {
|
:root {
|
||||||
width: 100vw !important;
|
--bg: #fff;
|
||||||
height: 100vh !important;
|
--bg-pattern: #f5eded;
|
||||||
|
|
||||||
|
--surface: #ffffff;
|
||||||
|
--border: #fbcfe8;
|
||||||
|
|
||||||
|
--text-header: #475569;
|
||||||
|
--text-title: #64748b;
|
||||||
|
--text-title-shadow: #cbd5e1;
|
||||||
|
|
||||||
|
--text-main: #64748b;
|
||||||
|
--text-dim: #94a3b8;
|
||||||
|
|
||||||
|
--accent: #d7e6ff;
|
||||||
|
--pink-accent: #ffd6f5;
|
||||||
|
|
||||||
|
--sparkle: #ffffff;
|
||||||
|
--sparkle-glow: rgba(255, 182, 193, 0.8);
|
||||||
|
|
||||||
|
--border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
* {
|
||||||
|
cursor: url('/cur/kuromi.webp') 32 32, auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--text-main);
|
||||||
|
font-family: "Times New Roman", Times, serif;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
background-color: var(--bg);
|
||||||
|
background-image: radial-gradient(var(--bg-pattern) 2px, transparent 2px);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
layout stuffs
|
||||||
|
*/
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-frame {
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 35px 25px 25px 25px;
|
||||||
|
position: relative;
|
||||||
|
max-width: 400px;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
|
||||||
|
border: 1px solid var(--pink-accent);
|
||||||
|
outline: 4px solid #fff;
|
||||||
|
box-shadow: 0 0 0 5px var(--accent), 8px 8px 0px rgba(215, 230, 255, 0.5);
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
animation: float 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decorative-sparkle {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--sparkle);
|
||||||
|
text-shadow: 0 0 20px var(--sparkle-glow), 0 0 15px var(--sparkle-glow), 0 0 10px var(--sparkle-glow), 0 0 5px var(--sparkle-glow);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
cursor: url('/cur/kuromi-hearts.webp') 0 0, auto !important;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decorative-sparkle:hover {
|
||||||
|
transform: scale(1.15) rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box {
|
||||||
|
border: 1px double var(--accent);
|
||||||
|
background: #fffdfd;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 0 10px rgba(255, 214, 245, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box::before {
|
||||||
|
content: "•";
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 6px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
text stuff
|
||||||
|
*/
|
||||||
|
h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--text-header);
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
text-shadow: 2px 2px 0px var(--pink-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--pink-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a {
|
||||||
|
color: var(--text-main);
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-bottom: 1px dashed transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover {
|
||||||
|
color: var(--pink-accent);
|
||||||
|
border-bottom: 1px dashed var(--pink-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin: 0 6px;
|
||||||
|
border-bottom: 1px dashed transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links button:hover {
|
||||||
|
color: var(--pink-accent);
|
||||||
|
border-bottom: 1px dashed var(--pink-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text-title);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 5px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory a {
|
||||||
|
text-decoration: none;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 0.73rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
box-shadow: 2px 2px 0px var(--pink-accent);
|
||||||
|
transition: 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory a::before {
|
||||||
|
content: "‹ "
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory a::after {
|
||||||
|
content: " ›"
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory a:hover {
|
||||||
|
cursor: url('/cur/kuromi-hearts.webp') 0 0, auto !important;
|
||||||
|
color: var(--text-header);
|
||||||
|
box-shadow: 1px 1px 0px var(--pink-accent);
|
||||||
|
background: var(--pink-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
marquee stuff at the bottom
|
||||||
|
*/
|
||||||
|
.marquee {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 6px 0;
|
||||||
|
margin-top: 25px;
|
||||||
|
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
border-top: 1px double var(--pink-accent);
|
||||||
|
border-bottom: 1px double var(--pink-accent);
|
||||||
|
background: linear-gradient(to right, transparent, rgba(255, 214, 245, 0.3), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-track {
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
animation: marquee-loop 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-track span {
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee-loop {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate3d(-50%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
modal
|
||||||
|
*/
|
||||||
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: black;
|
background: rgba(255, 255, 255, 0.4);
|
||||||
z-index: 4444;
|
backdrop-filter: blur(3px);
|
||||||
transition: opacity 1s ease-in-out;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
will-change: opacity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader.hidden {
|
.modal-content {
|
||||||
opacity: 0;
|
background: var(--surface);
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--pink-accent);
|
||||||
|
box-shadow: 4px 4px 0px var(--accent);
|
||||||
|
text-align: center;
|
||||||
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.niko-spin {
|
.modal-header {
|
||||||
width: 50vw;
|
color: var(--text-title);
|
||||||
height: auto;
|
font-size: 1rem;
|
||||||
display: block;
|
font-style: italic;
|
||||||
animation: spin 3s ease-in-out infinite;
|
margin-bottom: 12px;
|
||||||
will-change: transform;
|
border-bottom: 1px dashed var(--accent);
|
||||||
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
.modal-body p {
|
||||||
0% {
|
font-size: 0.85rem;
|
||||||
transform: rotate(0deg);
|
margin: 6px 0;
|
||||||
}
|
color: var(--text-main);
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-style: italic;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
@@ -1,8 +1,106 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import './page.css';
|
import './page.css';
|
||||||
import SealHome from './pages/seal';
|
import { DiscordStatus } from './components/discordstatus';
|
||||||
|
|
||||||
|
const TWITTER_LINK = "https://x.com/neruu444"
|
||||||
|
const DISCORD_USER = "neru444"
|
||||||
|
const DISCORD_ID = "1104474057916809226"
|
||||||
|
const STEAM_LINK = "https://steamcommunity.com/profiles/76561198440714757/"
|
||||||
|
|
||||||
|
function Content() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggleModal = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isOpen)
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const marqueeText = "✧ ꒰ა˵• ﻌ •˵ა꒱ ✧ ฅ^•ﻌ•^ฅ ✧ ᶻ 𝗓 𐰁 /ᐠ. 。 .ᐟ\\ ✧ ฅ/ᐠ. ̫ .ᐟ\\ฅ ✧ ꒰ა≽^•⩊•^≼໒꒱ ✧ ₍˄·͈༝·͈˄₎ ✧ /ᐠ. ⩊ .ᐟ\\ノ ✧ 𓏲ּ ֶָ ࣪ /ᐠ .ᆺ. ᐟ\\ノ ✧";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="main-frame">
|
||||||
|
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}>✧</a>
|
||||||
|
<a href="/fear" className="decorative-sparkle" title="✧" style={{ right: '10px' }}>✧</a>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>neru</h1>
|
||||||
|
<p className="motto">˚₊‧꒰ა 𓂋 ໒꒱ ‧₊˚</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav className="social-links">
|
||||||
|
<a href={TWITTER_LINK} target="_blank" rel="noopener noreferrer">twitter</a> •
|
||||||
|
<button onClick={toggleModal}>discord</button> •
|
||||||
|
<a href={STEAM_LINK} target="_blank" rel="noopener noreferrer">steam</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section className="content-box">
|
||||||
|
<h2 className="title">✧ discord ✧</h2>
|
||||||
|
<DiscordStatus userId={DISCORD_ID} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content-box">
|
||||||
|
<h2 className="title">✧ projects im currently working on ✧</h2>
|
||||||
|
<ul className="directory">
|
||||||
|
<li><a href="https://git.neru.rip/neru/seallib" target="_blank" rel="noopener noreferrer">seallib</a></li>
|
||||||
|
<li><a href="https://git.neru.rip/neru/tinymitm" target="_blank" rel="noopener noreferrer">tinymitm</a></li>
|
||||||
|
<li><a href="https://git.neru.rip/neru/luma" target="_blank" rel="noopener noreferrer">luma</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content-box">
|
||||||
|
<h2 className="title">✧ sites ✧</h2>
|
||||||
|
<ul className="directory">
|
||||||
|
<li><a href="https://git.neru.rip" target="_blank" rel="noopener noreferrer">gitea</a></li>
|
||||||
|
<li><a href="https://zl.neru.rip" target="_blank" rel="noopener noreferrer">zipline</a></li>
|
||||||
|
<li><a href="https://files.neru.rip" target="_blank" rel="noopener noreferrer">files</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content-box">
|
||||||
|
<h2 className="title">✧ dumb stuff ✧</h2>
|
||||||
|
<ul className="directory">
|
||||||
|
<li><a href="discord://-/apps">break discord</a></li>
|
||||||
|
<li><a href="discord://-/channels/@me/">fix discord</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div className="marquee">
|
||||||
|
<div className="marquee-track">
|
||||||
|
<span>{marqueeText}</span>
|
||||||
|
<span>{marqueeText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="modal-overlay" onClick={toggleModal}>
|
||||||
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">✧ discord info ✧</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<p><strong>User:</strong> {DISCORD_USER}</p>
|
||||||
|
<p><strong>ID:</strong> {DISCORD_ID}</p>
|
||||||
|
</div>
|
||||||
|
<button className="modal-close-btn" onClick={toggleModal}>[ close ]</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <SealHome />;
|
return (
|
||||||
}
|
<Content />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,684 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
forwardRef,
|
|
||||||
useImperativeHandle,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Bloom,
|
|
||||||
EffectComposer,
|
|
||||||
Noise,
|
|
||||||
Vignette,
|
|
||||||
DepthOfField,
|
|
||||||
SMAA,
|
|
||||||
HueSaturation,
|
|
||||||
BrightnessContrast,
|
|
||||||
LUT
|
|
||||||
} from '@react-three/postprocessing';
|
|
||||||
|
|
||||||
import { Environment, OrbitControls, useProgress } from '@react-three/drei';
|
|
||||||
import { Canvas, useLoader, useFrame } from '@react-three/fiber';
|
|
||||||
import {
|
|
||||||
BufferAttribute,
|
|
||||||
BufferGeometry,
|
|
||||||
Mesh,
|
|
||||||
Object3D,
|
|
||||||
InstancedMesh,
|
|
||||||
DoubleSide,
|
|
||||||
TextureLoader,
|
|
||||||
Color,
|
|
||||||
MeshStandardMaterial
|
|
||||||
} from 'three';
|
|
||||||
|
|
||||||
import { LUTCubeLoader } from 'three/examples/jsm/loaders/LUTCubeLoader.js';
|
|
||||||
|
|
||||||
import { createNoise2D } from 'simplex-noise';
|
|
||||||
|
|
||||||
import grassVert from './shaders/grass.vert';
|
|
||||||
import grassFrag from './shaders/grass.frag';
|
|
||||||
|
|
||||||
interface Shader {
|
|
||||||
uniforms: { [key: string]: { value: unknown } };
|
|
||||||
vertexShader: string;
|
|
||||||
fragmentShader: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTerrainHeight(
|
|
||||||
localX: number,
|
|
||||||
localZ: number,
|
|
||||||
worldXBase: number,
|
|
||||||
worldZBase: number,
|
|
||||||
scale: number,
|
|
||||||
hillScale: number,
|
|
||||||
hillHeight: number,
|
|
||||||
detailScale: number,
|
|
||||||
detailHeight: number,
|
|
||||||
noise2D: (x: number, y: number) => number
|
|
||||||
) {
|
|
||||||
const worldX = (worldXBase + localX) * 0.1;
|
|
||||||
const worldZ = (worldZBase + localZ) * 0.1;
|
|
||||||
|
|
||||||
const noiseHill =
|
|
||||||
noise2D(worldX * hillScale, worldZ * hillScale) * hillHeight;
|
|
||||||
const noiseDetail =
|
|
||||||
noise2D(worldX * detailScale, worldZ * detailScale) * detailHeight;
|
|
||||||
|
|
||||||
return (noiseHill + noiseDetail) * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GrassProps {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
size: number;
|
|
||||||
count: number;
|
|
||||||
grassSize: number;
|
|
||||||
scale: number;
|
|
||||||
hillScale: number;
|
|
||||||
hillHeight: number;
|
|
||||||
detailScale: number;
|
|
||||||
detailHeight: number;
|
|
||||||
noise2D: (x: number, y: number) => number;
|
|
||||||
grassLOD: number;
|
|
||||||
enableShadows?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Grass({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
count,
|
|
||||||
grassSize,
|
|
||||||
scale,
|
|
||||||
hillScale,
|
|
||||||
hillHeight,
|
|
||||||
detailScale,
|
|
||||||
detailHeight,
|
|
||||||
noise2D,
|
|
||||||
grassLOD
|
|
||||||
}: GrassProps) {
|
|
||||||
const meshRef = useRef<InstancedMesh>(null);
|
|
||||||
const dummyRef = useRef<Object3D>(new Object3D());
|
|
||||||
|
|
||||||
const [alphaMap, normalMap] = useLoader(TextureLoader, [
|
|
||||||
'/img/grass_alpha.png',
|
|
||||||
'/img/grass_normal.png'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const materialRef = useRef<
|
|
||||||
MeshStandardMaterial & { userData: { shader: Shader } }
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
useFrame((state) => {
|
|
||||||
if (
|
|
||||||
materialRef.current &&
|
|
||||||
materialRef.current.userData &&
|
|
||||||
materialRef.current.userData.shader
|
|
||||||
) {
|
|
||||||
(materialRef.current.userData.shader as Shader).uniforms.uTime.value =
|
|
||||||
state.clock.getElapsedTime();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const geometry = useMemo(() => {
|
|
||||||
const geo = new BufferGeometry();
|
|
||||||
|
|
||||||
const w = 0.5;
|
|
||||||
const h = 2;
|
|
||||||
const segments = 4;
|
|
||||||
|
|
||||||
const positions: number[] = [];
|
|
||||||
const uvs: number[] = [];
|
|
||||||
const indices: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const angle = (Math.PI / 3) * i;
|
|
||||||
const sinA = Math.sin(angle);
|
|
||||||
const cosA = Math.cos(angle);
|
|
||||||
|
|
||||||
const offset = positions.length / 3;
|
|
||||||
|
|
||||||
for (let y = 0; y <= segments; y++) {
|
|
||||||
const v = y / segments;
|
|
||||||
const yPos = v * h;
|
|
||||||
|
|
||||||
positions.push(-w * cosA, yPos, -w * sinA);
|
|
||||||
uvs.push(0, v);
|
|
||||||
|
|
||||||
positions.push(w * cosA, yPos, w * sinA);
|
|
||||||
uvs.push(1, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let y = 0; y < segments; y++) {
|
|
||||||
const row1 = offset + y * 2;
|
|
||||||
const row2 = offset + (y + 1) * 2;
|
|
||||||
|
|
||||||
indices.push(row1, row1 + 1, row2);
|
|
||||||
indices.push(row1 + 1, row2 + 1, row2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
geo.setAttribute(
|
|
||||||
'position',
|
|
||||||
new BufferAttribute(new Float32Array(positions), 3)
|
|
||||||
);
|
|
||||||
geo.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
|
|
||||||
geo.setIndex(indices);
|
|
||||||
geo.computeVertexNormals();
|
|
||||||
|
|
||||||
return geo;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!meshRef.current) return;
|
|
||||||
const dummy = dummyRef.current;
|
|
||||||
|
|
||||||
const worldXBase = x * size;
|
|
||||||
const worldZBase = y * size;
|
|
||||||
|
|
||||||
const color = new Color();
|
|
||||||
const lushColor = new Color('#348a34');
|
|
||||||
const dryColor = new Color('#556b19');
|
|
||||||
|
|
||||||
let instanceIndex = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const localX = (Math.random() - 0.5) * size;
|
|
||||||
const localZ = (Math.random() - 0.5) * size;
|
|
||||||
|
|
||||||
const globalX = worldXBase + localX;
|
|
||||||
const globalZ = worldZBase + localZ;
|
|
||||||
|
|
||||||
const dist = Math.sqrt(globalX * globalX + globalZ * globalZ);
|
|
||||||
|
|
||||||
const maxDist = grassLOD;
|
|
||||||
|
|
||||||
const falloffStart = maxDist * 0.1;
|
|
||||||
const falloffEnd = maxDist;
|
|
||||||
|
|
||||||
let fallofFactor = (dist - falloffStart) / (falloffEnd - falloffStart);
|
|
||||||
fallofFactor = Math.max(0, Math.min(1, fallofFactor));
|
|
||||||
|
|
||||||
const density = (1.0 - fallofFactor) * (1.0 - fallofFactor);
|
|
||||||
|
|
||||||
if (Math.random() > density) continue;
|
|
||||||
|
|
||||||
const localY = getTerrainHeight(
|
|
||||||
localX,
|
|
||||||
localZ,
|
|
||||||
worldXBase,
|
|
||||||
worldZBase,
|
|
||||||
scale,
|
|
||||||
hillScale,
|
|
||||||
hillHeight,
|
|
||||||
detailScale,
|
|
||||||
detailHeight,
|
|
||||||
noise2D
|
|
||||||
);
|
|
||||||
|
|
||||||
dummy.position.set(localX, localY, localZ);
|
|
||||||
|
|
||||||
dummy.rotation.y = Math.random() * Math.PI * 2;
|
|
||||||
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
|
|
||||||
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
|
|
||||||
|
|
||||||
const baseScale = grassSize + Math.random() * grassSize * 0.5;
|
|
||||||
const heightMult = 0.5 + Math.random() * 1.0;
|
|
||||||
dummy.scale.set(baseScale, baseScale * heightMult, baseScale);
|
|
||||||
|
|
||||||
dummy.updateMatrix();
|
|
||||||
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
|
|
||||||
|
|
||||||
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
|
|
||||||
|
|
||||||
const t = (noiseVal + 1) / 2;
|
|
||||||
|
|
||||||
const randomInternal = (Math.random() - 0.5) * 0.2;
|
|
||||||
const finalT = Math.max(0, Math.min(1, t + randomInternal));
|
|
||||||
|
|
||||||
color.lerpColors(dryColor, lushColor, finalT);
|
|
||||||
|
|
||||||
meshRef.current.setColorAt(instanceIndex, color);
|
|
||||||
|
|
||||||
instanceIndex++;
|
|
||||||
}
|
|
||||||
meshRef.current.count = instanceIndex;
|
|
||||||
|
|
||||||
meshRef.current.instanceMatrix.needsUpdate = true;
|
|
||||||
if (meshRef.current.instanceColor)
|
|
||||||
meshRef.current.instanceColor.needsUpdate = true;
|
|
||||||
}, [
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
count,
|
|
||||||
grassSize,
|
|
||||||
scale,
|
|
||||||
hillScale,
|
|
||||||
hillHeight,
|
|
||||||
detailScale,
|
|
||||||
detailHeight,
|
|
||||||
noise2D,
|
|
||||||
grassLOD
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onBeforeCompile = useMemo(
|
|
||||||
() => (shader: Shader) => {
|
|
||||||
shader.uniforms.uTime = { value: 0 };
|
|
||||||
|
|
||||||
shader.vertexShader = `
|
|
||||||
uniform float uTime;
|
|
||||||
varying vec2 vGrassUv;
|
|
||||||
|
|
||||||
float hash(vec2 p) {
|
|
||||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
||||||
}
|
|
||||||
|
|
||||||
float noise(vec2 p) {
|
|
||||||
vec2 i = floor(p);
|
|
||||||
vec2 f = fract(p);
|
|
||||||
f = f * f * (3.0 - 2.0 * f);
|
|
||||||
|
|
||||||
float a = hash(i);
|
|
||||||
float b = hash(i + vec2(1.0, 0.0));
|
|
||||||
float c = hash(i + vec2(0.0, 1.0));
|
|
||||||
float d = hash(i + vec2(1.0, 1.0));
|
|
||||||
|
|
||||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
float fbm(vec2 p) {
|
|
||||||
float value = 0.0;
|
|
||||||
float amplitude = 0.5;
|
|
||||||
float frequency = 1.0;
|
|
||||||
|
|
||||||
for(int i = 0; i < 4; i++) {
|
|
||||||
value += amplitude * noise(p * frequency);
|
|
||||||
frequency *= 2.0;
|
|
||||||
amplitude *= 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
${shader.vertexShader}
|
|
||||||
`;
|
|
||||||
shader.vertexShader = shader.vertexShader.replace(
|
|
||||||
'#include <begin_vertex>',
|
|
||||||
`
|
|
||||||
#include <begin_vertex>
|
|
||||||
vGrassUv = uv;
|
|
||||||
${grassVert}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
shader.fragmentShader = `
|
|
||||||
uniform float uTime;
|
|
||||||
varying vec2 vGrassUv;
|
|
||||||
${shader.fragmentShader}
|
|
||||||
`;
|
|
||||||
shader.fragmentShader = shader.fragmentShader.replace(
|
|
||||||
'#include <color_fragment>',
|
|
||||||
`
|
|
||||||
#include <color_fragment>
|
|
||||||
${grassFrag}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (materialRef.current) {
|
|
||||||
materialRef.current.userData.shader = shader;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<instancedMesh
|
|
||||||
ref={meshRef}
|
|
||||||
args={[undefined, undefined, count]}
|
|
||||||
position={[x * size, 0, y * size]}
|
|
||||||
geometry={geometry}
|
|
||||||
>
|
|
||||||
<meshStandardMaterial
|
|
||||||
ref={materialRef}
|
|
||||||
color='#ffffff'
|
|
||||||
side={DoubleSide}
|
|
||||||
alphaMap={alphaMap}
|
|
||||||
alphaTest={0.5}
|
|
||||||
normalMap={normalMap}
|
|
||||||
roughness={0.8}
|
|
||||||
metalness={0.1}
|
|
||||||
onBeforeCompile={onBeforeCompile}
|
|
||||||
/>
|
|
||||||
</instancedMesh>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TerrainChunkProps {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
size: number;
|
|
||||||
resolution: number;
|
|
||||||
scale: number;
|
|
||||||
hillScale: number;
|
|
||||||
hillHeight: number;
|
|
||||||
detailScale: number;
|
|
||||||
detailHeight: number;
|
|
||||||
noise2D: (x: number, y: number) => number;
|
|
||||||
grassCount: number;
|
|
||||||
grassSize: number;
|
|
||||||
grassLOD: number;
|
|
||||||
}
|
|
||||||
function TerrainChunk({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
resolution,
|
|
||||||
scale,
|
|
||||||
hillScale,
|
|
||||||
hillHeight,
|
|
||||||
detailScale,
|
|
||||||
detailHeight,
|
|
||||||
noise2D,
|
|
||||||
grassCount,
|
|
||||||
grassSize,
|
|
||||||
grassLOD
|
|
||||||
}: TerrainChunkProps) {
|
|
||||||
const chunkDist = Math.sqrt((x * size) ** 2 + (y * size) ** 2);
|
|
||||||
const shouldRenderGrass = chunkDist < grassLOD + size;
|
|
||||||
|
|
||||||
const meshRef = useRef<Mesh>(null);
|
|
||||||
|
|
||||||
const geometry = useMemo(() => {
|
|
||||||
const geo = new BufferGeometry();
|
|
||||||
|
|
||||||
const vertices: Array<number> = [];
|
|
||||||
const indices: Array<number> = [];
|
|
||||||
|
|
||||||
const step = size / (resolution - 1);
|
|
||||||
const halfSize = size / 2;
|
|
||||||
const worldXBase = x * size;
|
|
||||||
const worldZBase = y * size;
|
|
||||||
|
|
||||||
/*
|
|
||||||
vtx gen
|
|
||||||
*/
|
|
||||||
for (let iz = 0; iz < resolution; iz++) {
|
|
||||||
for (let ix = 0; ix < resolution; ix++) {
|
|
||||||
const localX = ix * step - halfSize;
|
|
||||||
const localZ = iz * step - halfSize;
|
|
||||||
|
|
||||||
const localY = getTerrainHeight(
|
|
||||||
localX,
|
|
||||||
localZ,
|
|
||||||
worldXBase,
|
|
||||||
worldZBase,
|
|
||||||
scale,
|
|
||||||
hillScale,
|
|
||||||
hillHeight,
|
|
||||||
detailScale,
|
|
||||||
detailHeight,
|
|
||||||
noise2D
|
|
||||||
);
|
|
||||||
|
|
||||||
vertices.push(localX, localY, localZ);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
idx gen
|
|
||||||
*/
|
|
||||||
for (let iz = 0; iz < resolution - 1; iz++) {
|
|
||||||
for (let ix = 0; ix < resolution - 1; ix++) {
|
|
||||||
const topLeft = iz * resolution + ix;
|
|
||||||
const topRight = topLeft + 1;
|
|
||||||
const bottomLeft = (iz + 1) * resolution + ix;
|
|
||||||
const bottomRight = bottomLeft + 1;
|
|
||||||
|
|
||||||
indices.push(topLeft, bottomLeft, topRight);
|
|
||||||
indices.push(topRight, bottomLeft, bottomRight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
geo.setAttribute(
|
|
||||||
'position',
|
|
||||||
new BufferAttribute(new Float32Array(vertices), 3)
|
|
||||||
);
|
|
||||||
|
|
||||||
const colors: Array<number> = [];
|
|
||||||
for (let iz = 0; iz < resolution; iz++) {
|
|
||||||
for (let ix = 0; ix < resolution; ix++) {
|
|
||||||
const localX = ix * step - halfSize;
|
|
||||||
const localZ = iz * step - halfSize;
|
|
||||||
const globalX = worldXBase + localX;
|
|
||||||
const globalZ = worldZBase + localZ;
|
|
||||||
const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02);
|
|
||||||
const t = (colorNoise + 1) / 2;
|
|
||||||
|
|
||||||
const dryGreen = { r: 0.01, g: 0.01, b: 0.0 };
|
|
||||||
const lushGreen = { r: 0.0, g: 0.05, b: 0.0 };
|
|
||||||
|
|
||||||
const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t;
|
|
||||||
const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t;
|
|
||||||
const b = dryGreen.b + (lushGreen.b - dryGreen.b) * t;
|
|
||||||
|
|
||||||
colors.push(r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
geo.setAttribute('color', new BufferAttribute(new Float32Array(colors), 3));
|
|
||||||
|
|
||||||
geo.setIndex(indices);
|
|
||||||
geo.computeVertexNormals();
|
|
||||||
|
|
||||||
return geo;
|
|
||||||
}, [
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
resolution,
|
|
||||||
scale,
|
|
||||||
hillScale,
|
|
||||||
hillHeight,
|
|
||||||
detailScale,
|
|
||||||
detailHeight,
|
|
||||||
noise2D
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
<mesh
|
|
||||||
ref={meshRef}
|
|
||||||
geometry={geometry}
|
|
||||||
position={[x * size, 0, y * size]}
|
|
||||||
>
|
|
||||||
<meshStandardMaterial vertexColors roughness={1.0} metalness={0.0} />
|
|
||||||
</mesh>
|
|
||||||
{shouldRenderGrass && (
|
|
||||||
<Grass
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
size={size}
|
|
||||||
count={grassCount}
|
|
||||||
grassSize={grassSize}
|
|
||||||
scale={scale}
|
|
||||||
hillScale={hillScale}
|
|
||||||
hillHeight={hillHeight}
|
|
||||||
detailScale={detailScale}
|
|
||||||
detailHeight={detailHeight}
|
|
||||||
noise2D={noise2D}
|
|
||||||
grassLOD={grassLOD}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TerrainProps {
|
|
||||||
chunks?: number;
|
|
||||||
chunkSize?: number;
|
|
||||||
resolution?: number;
|
|
||||||
scale?: number;
|
|
||||||
hillScale: number;
|
|
||||||
hillHeight: number;
|
|
||||||
detailScale: number;
|
|
||||||
detailHeight: number;
|
|
||||||
grassCount?: number;
|
|
||||||
grassSize?: number;
|
|
||||||
grassLOD?: number;
|
|
||||||
}
|
|
||||||
function Terrain({
|
|
||||||
chunks = 5,
|
|
||||||
chunkSize = 10,
|
|
||||||
resolution = 8,
|
|
||||||
scale = 1,
|
|
||||||
hillScale = 0.2,
|
|
||||||
hillHeight = 3,
|
|
||||||
detailScale = 3,
|
|
||||||
detailHeight = 0.1,
|
|
||||||
grassCount = 6000,
|
|
||||||
grassSize = 0.6,
|
|
||||||
grassLOD = 40
|
|
||||||
}: TerrainProps) {
|
|
||||||
const noise2D = useMemo(() => createNoise2D(), []);
|
|
||||||
const offset = -Math.floor(chunks / 2);
|
|
||||||
|
|
||||||
const chunkPositions = useMemo(() => {
|
|
||||||
const positions: [number, number][] = [];
|
|
||||||
for (let x = 0; x < chunks; x++)
|
|
||||||
for (let y = 0; y < chunks; y++) positions.push([x + offset, y + offset]);
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}, [chunks, offset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{chunkPositions.map(([x, y], index) => (
|
|
||||||
<TerrainChunk
|
|
||||||
key={`${x}-${y}-${index}`}
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
size={chunkSize}
|
|
||||||
resolution={resolution}
|
|
||||||
scale={scale}
|
|
||||||
hillScale={hillScale}
|
|
||||||
hillHeight={hillHeight}
|
|
||||||
detailScale={detailScale}
|
|
||||||
detailHeight={detailHeight}
|
|
||||||
noise2D={noise2D}
|
|
||||||
grassCount={grassCount}
|
|
||||||
grassSize={grassSize}
|
|
||||||
grassLOD={grassLOD}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SealCube = forwardRef<Mesh>((props, ref) => {
|
|
||||||
const texture = useLoader(TextureLoader, '/img/niko.jpg');
|
|
||||||
const meshRef = useRef<Mesh>(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => meshRef.current!, []);
|
|
||||||
|
|
||||||
useFrame((state, delta) => {
|
|
||||||
if (meshRef.current) {
|
|
||||||
meshRef.current.rotation.x += delta * 0.5;
|
|
||||||
meshRef.current.rotation.y += delta * 0.5;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
|
|
||||||
<boxGeometry args={[0.85, 0.85, 0.85]} />
|
|
||||||
<meshBasicMaterial map={texture} depthWrite={true} />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SealCube.displayName = 'SealCube';
|
|
||||||
|
|
||||||
function Loader() {
|
|
||||||
const { progress, active } = useProgress();
|
|
||||||
const [visible, setVisible] = useState(true);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!active && progress === 100) {
|
|
||||||
const timeout = setTimeout(() => setVisible(false), 500);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, [progress, active]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`loader ${!visible ? 'loader hidden' : ''}`}>
|
|
||||||
<picture>
|
|
||||||
<img alt='niko!!' src='/img/niko.jpg' className='niko-spin' />
|
|
||||||
</picture>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LutEffect() {
|
|
||||||
const lutTexture = useLoader(LUTCubeLoader, '/lut/Landscape6.cube');
|
|
||||||
return <LUT lut={lutTexture.texture3D} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SealHome() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Loader />
|
|
||||||
<Canvas
|
|
||||||
shadows
|
|
||||||
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
|
|
||||||
gl={{ antialias: true }}
|
|
||||||
className='canvas'
|
|
||||||
>
|
|
||||||
<EffectComposer>
|
|
||||||
<DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} />
|
|
||||||
<Vignette />
|
|
||||||
<Noise opacity={0.05} />
|
|
||||||
<Bloom
|
|
||||||
intensity={2}
|
|
||||||
luminanceThreshold={0.5}
|
|
||||||
luminanceSmoothing={0.1}
|
|
||||||
/>
|
|
||||||
<SMAA />
|
|
||||||
<HueSaturation saturation={0.1} />
|
|
||||||
<BrightnessContrast brightness={0.05} contrast={-0.1} />
|
|
||||||
<LutEffect />
|
|
||||||
</EffectComposer>
|
|
||||||
|
|
||||||
<Environment
|
|
||||||
files={'hdr/sky.hdr'}
|
|
||||||
environmentIntensity={1}
|
|
||||||
background
|
|
||||||
/>
|
|
||||||
<fogExp2 attach='fog' args={[0x9a9a9a, 0.01]} />
|
|
||||||
|
|
||||||
<Terrain
|
|
||||||
chunks={16}
|
|
||||||
chunkSize={10.0}
|
|
||||||
resolution={8.0}
|
|
||||||
scale={1}
|
|
||||||
hillScale={0.1}
|
|
||||||
hillHeight={6.0}
|
|
||||||
detailScale={1.0}
|
|
||||||
detailHeight={0.2}
|
|
||||||
grassCount={6000}
|
|
||||||
grassSize={0.6}
|
|
||||||
grassLOD={60}
|
|
||||||
/>
|
|
||||||
<SealCube />
|
|
||||||
|
|
||||||
<OrbitControls
|
|
||||||
target={[0, 3, 0]}
|
|
||||||
enablePan={false}
|
|
||||||
makeDefault
|
|
||||||
minDistance={2}
|
|
||||||
maxDistance={10}
|
|
||||||
/>
|
|
||||||
</Canvas>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
float ao = smoothstep(0.0, 0.4, vGrassUv.y);
|
|
||||||
ao = mix(0.2, 1.0, ao); // Roots are 20% brightness
|
|
||||||
|
|
||||||
vec3 rootColor = diffuseColor.rgb * 0.4;
|
|
||||||
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.1, 0.1, 0.0);
|
|
||||||
|
|
||||||
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y);
|
|
||||||
grassColor *= ao;
|
|
||||||
|
|
||||||
float translucency = pow(vGrassUv.y, 2.0) * 0.5;
|
|
||||||
grassColor += vec3(0.1, 0.2, 0.0) * translucency;
|
|
||||||
|
|
||||||
diffuseColor.rgb = grassColor;
|
|
||||||