Compare commits

..

97 Commits

Author SHA1 Message Date
neru f1ab2b692d style: change text 2026-06-02 04:43:09 -03:00
neru 673aabce50 style: move patch to its own file 2026-06-02 04:42:16 -03:00
neru b3a5712c85 feat: make movement jitter, add phase effects 2026-06-02 04:42:07 -03:00
neru 4691a9fbf4 feat: add flicker anim 2026-06-02 04:30:03 -03:00
neru 0fca4db440 style: change size and alignment 2026-06-02 04:13:54 -03:00
neru 930139d1df feat: add unicode glitches 2026-06-02 04:13:29 -03:00
neru 4120e5ec72 feat: move href logic to state mgr 2026-06-02 03:57:42 -03:00
neru fd314cf2ec feat: make finale text infinite 2026-06-02 03:57:31 -03:00
neru e3ab974988 style: reduce fov 2026-06-02 03:45:04 -03:00
neru e4a0c57e79 feat: add TEST_MODE 2026-06-02 03:44:58 -03:00
neru ebda4b281e feat: add steel texture for doors 2026-06-02 03:29:17 -03:00
neru 8dcc888d5c feat: add psx style vertex snapping and affine distortion 2026-06-02 03:29:07 -03:00
neru b7e61b4240 feat: lerp flashlight pos 2026-06-02 02:50:11 -03:00
neru 23c39a71a6 feat: add footsteps 2026-06-02 02:50:04 -03:00
neru a0ee50703c feat: add better ambience sound 2026-06-02 02:44:44 -03:00
neru 8c4080f10c feat: add finale text 2026-06-01 22:02:22 -03:00
neru b9eeed848b feat: add door knocks 2026-06-01 22:02:12 -03:00
neru d506071ce2 feat: overhaul grass 2026-06-01 17:31:13 -03:00
neru cad47f07bd feat: randomize grass height 2026-06-01 17:02:05 -03:00
neru 10543bba89 fix: make grass rotation less strong 2026-06-01 17:00:33 -03:00
neru aeee2158ba feat/fix: misc shader changes (fix normals, lighting, uv) 2026-06-01 16:56:33 -03:00
neru 569a4f29fb fix: make fog fit hdr 2026-06-01 16:50:55 -03:00
neru 9a67a800fa feat: add hover anim 2026-06-01 16:49:04 -03:00
neru 719a75d393 feat: change cube lighting to standard mat 2026-06-01 16:49:00 -03:00
neru f583cfdc57 style: formatting 2026-06-01 16:47:44 -03:00
neru 5665804b8f fix: disable AA (already handled by SMAA) 2026-06-01 16:47:37 -03:00
neru 67bf6325fa fix: misc visual changes 2026-06-01 16:47:24 -03:00
neru 6d7651dec9 feat: implement acceleration, prevent bob from offsetting root pos 2026-06-01 15:50:46 -03:00
neru ee2eb45527 fix: preload creature tex 2026-06-01 15:42:25 -03:00
neru 079986ebec fix: move sounds outside suspense 2026-06-01 15:42:17 -03:00
neru a0b416c412 fix: remove dbg loop counts 2026-06-01 15:39:36 -03:00
neru c582d6b745 fix?: ensure canvas position and block selection 2026-06-01 15:38:26 -03:00
neru beff5e3265 feat: rely on state mgr instead of hardcoded loop count 2026-06-01 15:38:04 -03:00
neru c04d8536c0 fix: issue with AmbientSound not updating correctly 2026-06-01 15:37:46 -03:00
neru cb15cc3d95 feat: add ambient2, add keys to sounds 2026-06-01 15:37:23 -03:00
neru 0d72d49d7b fix: move ambient light to page root 2026-06-01 15:37:08 -03:00
neru d66c898f23 feat: move state updates to page root 2026-06-01 15:36:21 -03:00
neru 566a684bfa fix?: try to fix camera snapping to rnd angles 2026-06-01 15:35:56 -03:00
neru df81fc1ee0 style: adjust flashlight params 2026-06-01 15:08:10 -03:00
neru dd5e8a2ae2 feat: add CREATURE_SPEED and FLASHLIGHT_INTENSITY_BASE 2026-06-01 15:08:03 -03:00
neru 20b6a559fd fix: add separate rust texture for walls to stop stretching 2026-06-01 15:07:01 -03:00
neru eec01440f9 feat: it speaks 2026-05-31 23:41:32 -03:00
neru 339c660bcb feat: he is here. 2026-05-31 23:24:14 -03:00
neru fe686b0071 fix: audio handling 2026-05-31 22:03:52 -03:00
neru 092faa9449 feat: more fear 2026-05-31 21:58:13 -03:00
neru a163a12483 feat: add rust 2026-05-31 21:57:43 -03:00
neru 30498e4faa feat: downscale textures 2026-05-31 21:57:37 -03:00
neru df44640ecb fix: declare type for keyboard events 2026-05-31 20:35:14 -03:00
neru 5de127449a feat: add fear 2026-05-31 20:26:03 -03:00
neru 7a1c28cd19 style: remove title from opengraph 2026-05-31 14:46:17 -03:00
neru 606dde5122 fix: remove duplicate env 2026-05-31 14:45:05 -03:00
neru 59e5a8c0c0 style: restrict max dist to 6 2026-05-31 14:41:55 -03:00
neru a40ee3854b feat: add ambient sound 2026-05-31 14:40:49 -03:00
neru 18246eab22 feat: add AmbientSound component 2026-05-31 14:40:36 -03:00
neru a6a6cdd168 give functions names 2026-05-31 14:36:34 -03:00
neru d9869a469e style: increase saturation 2026-05-31 14:29:36 -03:00
neru 0db7c08168 style: misc terrain generation params 2026-05-31 14:29:16 -03:00
neru 5e8ebf9491 style: change terrain colours again 2026-05-31 14:27:13 -03:00
neru 4d28cf8bcb feat: add LUT 2026-05-31 14:26:25 -03:00
neru f7b3153b92 fix: more performant grass config 2026-05-31 14:24:43 -03:00
neru 458037829f feat: add seal. 2026-05-31 14:20:38 -03:00
neru d778d82302 fix: use new resolveDiscordAsset args 2026-05-31 11:58:29 -03:00
neru 30fad6e2e3 fix: make width look better on low width 2026-05-31 11:56:29 -03:00
neru 6f01cc1793 fix: stop tags from breaking on low width 2026-05-31 11:55:55 -03:00
neru 144127952a style: move image to /img/ folder 2026-05-31 11:52:42 -03:00
neru 6209236041 feat: host cursor locally 2026-05-31 11:52:14 -03:00
neru cf70296b04 feat: close modal on esc 2026-05-31 11:49:50 -03:00
neru 105518282f fix?: only poll presence if tab is visible 2026-05-31 11:47:54 -03:00
neru 7791f6a684 styles: misc changes 2026-05-31 11:44:06 -03:00
neru 79c47a3632 fix: make marquee loop infinitely 2026-05-31 11:43:58 -03:00
neru c03449526a feat: add discord cdn support for app images 2026-05-31 11:39:06 -03:00
neru cd630f0372 fix: make discord button an actual button and not <a/> 2026-05-31 11:38:50 -03:00
neru 1ee1461d51 fix: make <a/> links open in new tab 2026-05-31 11:30:28 -03:00
neru cf13e2a2bf fix: remove repeated variables 2026-05-31 11:29:21 -03:00
neru cc7ab4ad9f feat: add spotify status 2026-05-30 19:49:43 -03:00
neru 9a2d73a6cb feat: add discord box 2026-05-30 18:24:03 -03:00
neru ebe0842759 feat: add discord status class 2026-05-30 18:20:22 -03:00
neru cc0cf9e055 fix: remove link to git 2026-05-28 07:11:09 -03:00
neru 4c57e8f24f style: make hover effect more visible 2026-05-28 07:10:54 -03:00
neru 68a8d5f255 fix: move themeColor to viewport, remove desc 2026-05-25 21:14:11 -03:00
neru a9beb53a91 feat: add metadata 2026-05-25 21:11:04 -03:00
neru c0fd9e370c feat: add prefix and suffix 2026-05-25 21:04:07 -03:00
neru fd0177f167 feat: remove luma invite, add projects 2026-05-25 21:04:02 -03:00
neru b97767dba8 feat: update style again 2026-05-25 21:01:13 -03:00
neru d9b0975a61 style: change bg color 2026-05-17 06:09:23 -03:00
neru 0d8d95aa4b style: add b to dumb !!! very important 2026-05-07 10:29:41 -03:00
neru 5c5f3f1861 fix: wrong default state for modal 2026-05-07 10:28:20 -03:00
neru 086a65913b style: change wording 2026-05-07 10:27:58 -03:00
neru ecf4c76b15 feat: add discord infomodal 2026-05-07 10:27:52 -03:00
neru d1a28fd777 style: make sparkle color more intense 2026-05-07 10:02:11 -03:00
neru 4ad8c3f3db style: change invite btn text 2026-05-07 10:01:33 -03:00
neru 47855d7941 style: make header way smaller 2026-05-07 10:01:09 -03:00
neru b865cd0d99 style: remove .rip from header 2026-05-07 10:00:43 -03:00
neru dac9906971 feat: invert sparkles 2026-05-07 06:43:17 -03:00
neru 082dd811b7 feat: add new site 2026-05-07 06:09:32 -03:00
neru 559c9b6562 chore: update packages 2026-05-07 06:09:28 -03:00
neru 54d0345bff chore: remove old stuff 2026-05-07 06:09:24 -03:00
55 changed files with 3050 additions and 775 deletions
+77 -39
View File
@@ -77,7 +77,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1073,6 +1072,7 @@
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -2002,7 +2002,6 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.2.tgz",
"integrity": "sha512-H4B4+FDNHpvIb4FmphH4ubxOfX5bxmfOw0+3pkQwR9u9wFiyMS7wUDkNn0m4RqQuiLWeia9jfN1eBvtyAVGEog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.32.0",
@@ -2405,6 +2404,7 @@
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@@ -2416,6 +2416,7 @@
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
@@ -2463,7 +2464,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2474,7 +2474,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2499,7 +2498,6 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
@@ -2561,7 +2559,6 @@
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.1",
"@typescript-eslint/types": "8.50.1",
@@ -3079,6 +3076,7 @@
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "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",
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2",
@@ -3111,6 +3112,7 @@
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "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",
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1",
@@ -3130,6 +3133,7 @@
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
@@ -3143,6 +3147,7 @@
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -3153,6 +3158,7 @@
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@@ -3162,7 +3168,8 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1",
@@ -3170,6 +3177,7 @@
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
@@ -3187,6 +3195,7 @@
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
@@ -3201,6 +3210,7 @@
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
@@ -3214,6 +3224,7 @@
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-api-error": "1.13.2",
@@ -3229,6 +3240,7 @@
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@xtuc/long": "4.2.2"
@@ -3245,14 +3257,16 @@
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true,
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true,
"license": "Apache-2.0"
"license": "Apache-2.0",
"peer": true
},
"node_modules/acorn": {
"version": "8.15.0",
@@ -3260,7 +3274,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3274,6 +3287,7 @@
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
},
@@ -3297,7 +3311,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -3315,6 +3328,7 @@
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^8.0.0"
},
@@ -3333,6 +3347,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -3349,7 +3364,8 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
@@ -3631,7 +3647,6 @@
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.26.0"
}
@@ -3735,7 +3750,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3779,7 +3793,8 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/call-bind": {
"version": "1.0.8",
@@ -3897,6 +3912,7 @@
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.0"
}
@@ -3938,7 +3954,8 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -4336,7 +4353,8 @@
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
@@ -4427,7 +4445,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4613,7 +4630,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4853,6 +4869,7 @@
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.8.x"
}
@@ -4944,7 +4961,8 @@
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/fastq": {
"version": "1.20.1",
@@ -5221,7 +5239,8 @@
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/globals": {
"version": "14.0.0",
@@ -5986,6 +6005,7 @@
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
@@ -6001,6 +6021,7 @@
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"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",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -6470,6 +6492,7 @@
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.11.5"
},
@@ -6573,7 +6596,8 @@
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/merge-value": {
"version": "1.0.0",
@@ -6635,6 +6659,7 @@
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -6645,6 +6670,7 @@
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -6751,7 +6777,8 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/next": {
"version": "16.1.1",
@@ -7133,7 +7160,6 @@
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.2.tgz",
"integrity": "sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==",
"license": "Zlib",
"peer": true,
"peerDependencies": {
"three": ">= 0.157.0 < 0.183.0"
}
@@ -7227,6 +7253,7 @@
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -7257,7 +7284,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7277,7 +7303,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -7512,7 +7537,8 @@
"url": "https://feross.org/support"
}
],
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
@@ -7590,6 +7616,7 @@
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -7834,6 +7861,7 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7853,6 +7881,7 @@
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -8151,6 +8180,7 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -8170,6 +8200,7 @@
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
@@ -8223,6 +8254,7 @@
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"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",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "4.3.3",
@@ -8243,6 +8276,7 @@
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
@@ -8261,8 +8295,7 @@
"version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
@@ -8337,7 +8370,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -8567,7 +8599,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8732,6 +8763,7 @@
"integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -8757,6 +8789,7 @@
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -8806,6 +8839,7 @@
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
}
@@ -8834,6 +8868,7 @@
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
@@ -8847,6 +8882,7 @@
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -8861,6 +8897,7 @@
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=4.0"
}
@@ -8870,7 +8907,8 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "4.3.3",
@@ -8878,6 +8916,7 @@
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
@@ -9032,7 +9071,6 @@
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

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

Binary file not shown.
Binary file not shown.
+76
View File
@@ -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;
}
+262
View File
@@ -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>
);
}
+10
View File
@@ -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;
}
+120
View File
@@ -0,0 +1,120 @@
'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 { FEAR_SETTINGS, 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';
import FinaleText from './scene-components/finale-text';
function PostProcessing() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
return (<EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 10} />
<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: 55, far: 100 }}
>
<FearStateUpdater />
<ListenerCreator />
<color attach="background" args={['#050505']} />
{FEAR_SETTINGS.TEST_MODE ? <ambientLight intensity={2} /> : <ambientLight intensity={0.0225} />}
{FEAR_SETTINGS.TEST_MODE ? null : <fogExp2 attach='fog' args={[0x050505, 0.035]} />}
{FEAR_SETTINGS.TEST_MODE ? null : < PostProcessing />}
<Suspense fallback={null}>
<Hallway />
<TheCreature />
<Player />
</Suspense>
<AmbientSound
key="ambient-1"
url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 0.5}
/>
<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>
<FinaleText />
</>)
}
@@ -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
}
+196
View File
@@ -0,0 +1,196 @@
import { useTexture, PositionalAudio } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three";
import { FEAR_SETTINGS, fearState } from "../state";
import { ShaderPatch } from "../shader-patch";
useTexture.preload('fear/img/creature.png');
export default function TheCreature() {
const baseTexture = useTexture('fear/img/creature.png');
const texture = useMemo(() => {
const t = baseTexture.clone();
t.needsUpdate = true;
return t;
}, [baseTexture]);
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);
const movePhase = useRef<'frozen' | 'lurching'>('frozen');
const phaseTimer = useRef<number>(1.5);
const glitchCooldown = useRef<number>(0);
const isGlitchSpiking = useRef<boolean>(false);
const flickerCooldown = useRef<number>(0);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setFinaleTriggered(fearState.finaleTriggered);
if (!fearState.finaleTriggered) {
setIsSpawned(false);
setHasTriggered(false);
globalDistance.current = 32;
audioPlaying.current = false;
movePhase.current = 'frozen';
phaseTimer.current = 1.5;
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;
movePhase.current = 'frozen';
phaseTimer.current = 1.0 + Math.random() * 1.5;
}
if (!hasTriggered) {
if (globalDistance.current < 40)
setHasTriggered(true);
}
if (hasTriggered) {
phaseTimer.current -= delta;
if (phaseTimer.current <= 0) {
if (movePhase.current === 'frozen') {
movePhase.current = 'lurching';
phaseTimer.current = 0.05 + Math.random() * 0.2;
} else {
movePhase.current = 'frozen';
const proximityFactor = Math.max(0.05, globalDistance.current / 32);
phaseTimer.current = (0.2 + Math.random() * 1.0) * proximityFactor;
}
}
if (movePhase.current === 'lurching') {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * 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);
if (!hasTriggered) return;
const proximity = 1 - Math.max(0, Math.min(1, globalDistance.current / 32));
const jitterX = 1.0 + (Math.random() - 0.5) * 0.04 * proximity;
let jitterY = 1.0 + (Math.random() - 0.5) * 0.06 * proximity;
glitchCooldown.current -= delta;
if (glitchCooldown.current <= 0) {
if (Math.random() < 0.25 + proximity * 0.35) {
isGlitchSpiking.current = true;
glitchCooldown.current = 0.03 + Math.random() * 0.08;
} else {
isGlitchSpiking.current = false;
glitchCooldown.current = 0.08 + Math.random() * 0.4 * (1 - proximity * 0.7);
}
}
if (isGlitchSpiking.current) {
const spike = 0.15 + Math.random() * 0.35;
jitterY += Math.random() > 0.5 ? spike : -spike * 0.6;
}
creature.scale.set(jitterX, jitterY, 1.0);
flickerCooldown.current -= delta;
if (flickerCooldown.current <= 0) {
if (creature.visible && Math.random() < 0.12 + proximity * 0.08) {
creature.visible = false;
flickerCooldown.current = 0.02 + Math.random() * 0.05;
} else {
creature.visible = true;
flickerCooldown.current = 0.05 + Math.random() * 0.3 * (1 - proximity * 0.5);
}
}
texture.offset.set(
(Math.random() - 0.5) * 0.025 * proximity,
(Math.random() - 0.5) * 0.025 * proximity
);
if (proximity > 0.2) {
creature.position.x += (Math.random() - 0.5) * 0.12 * proximity;
creature.position.y += (Math.random() - 0.5) * 0.06 * proximity;
}
});
return (
<mesh
ref={meshRef}
visible={finaleTriggered}
>
<planeGeometry args={[3.0, 4.8]} />
<meshStandardMaterial
map={texture}
transparent={true}
depthWrite={false}
side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive="#ffffff"
emissiveMap={texture}
emissiveIntensity={0.15}
/>
{finaleTriggered && (
<PositionalAudio
url="fear/snd/riser.mp3"
ref={audioRef}
distance={25}
loop={false}
autoplay={false}
/>
)}
</mesh>
);
}
@@ -0,0 +1,84 @@
@font-face {
font-family: 'VCR';
src: url('/fear/fonts/vcr.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
.finale-container {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0vh;
display: grid;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
/* filter: invert(100%); */
backdrop-filter: brightness(100%);
grid-auto-rows: 5vh;
/* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content);
user-select: none;
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
}
@keyframes invertFlicker {
0%,
43%,
45%,
88%,
92% {
filter: invert(0%) contrast(100%) brightness(100%);
backdrop-filter: brightness(100%) hue-rotate(0deg);
}
44%,
46%,
89%,
93%,
100% {
filter: invert(100%) contrast(300%) brightness(150%);
backdrop-filter: brightness(30%) hue-rotate(180deg) saturate(500%);
}
}
.finale-text {
font-family: 'VCR', sans-serif;
font-variant-numeric: tabular-nums;
letter-spacing: 0.1em;
height: 0px;
width: 100%;
color: rgb(255, 255, 255);
font-size: 8vh;
text-align: center;
white-space: nowrap;
}
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900;
background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 4px);
}
@@ -0,0 +1,56 @@
import { JSX, useEffect, useState } from "react"
import { fearState } from "../state"
import './finale-text.css';
const BLOCKS = [
"▀", "▂", "▃", "▄", "▅", "▆", "▇",
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗",
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟"
];
export default function FinaleText() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [elements, setElements] = useState<JSX.Element[]>([]);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught)
});
return () => unsubscribe();
}, []);
useEffect(() => {
if (!wasCaught)
return;
const interval = setInterval(() => {
if (Math.random() > 0.9) return;
const baseText = "bwaaaaaaaaa";
const corrupted = baseText
.split("")
.map((char) => (Math.random() > 0.98 ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] : char))
.join("");
setElements((prev) => [...prev.slice(-30),
<span className="finale-text" key={crypto.randomUUID()}>
{corrupted}
</span>
]);
}, 10);
return () => clearInterval(interval);
}, [wasCaught]);
if (!wasCaught) return null;
return (<>
<div className="finale-container">
{elements}
</div>
<div className="scanlines" />
</>)
}
+401
View File
@@ -0,0 +1,401 @@
import { useEffect, useRef, useState } from "react";
import { FEAR_SETTINGS, fearState } from "../state";
import { useTexture, PositionalAudio } from "@react-three/drei";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { ShaderPatch } from "../shader-patch";
interface DoorProps {
position: [number, number, number];
rotation: [number, number, number];
}
function Door({ position, rotation }: DoorProps) {
const [soundUrl, setSoundUrl] = useState<string | null>(null);
const currentSound = useRef<string | null>(null);
const steelTex = useTexture('fear/img/steel.png');
useEffect(() => {
const interval = setInterval(() => {
if (Math.random() < 0.02) {
const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3";
setSoundUrl(chosenSound);
currentSound.current = chosenSound;
}
}, 5000);
return () => clearInterval(interval);
}, []);
const handleAudioEnded = () => {
setSoundUrl(null);
currentSound.current = null;
};
return (
<group position={position} rotation={rotation}>
{/* frame */}
<mesh position={[0, 2, -0.1]}>
<boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#8d8d8d" onBeforeCompile={ShaderPatch} />
</mesh>
{/* panel */}
<mesh position={[0, 1.95, -0.0]}>
<boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#4e4a4a" onBeforeCompile={ShaderPatch} />
</mesh>
{/* handle */}
<mesh position={[0.75, 1.8, .085]}>
<boxGeometry args={[0.3, 0.08, 0.1]} />
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} />
</mesh>
{soundUrl && (
<PositionalAudio
url={soundUrl}
distance={25}
loop={false}
autoplay={true}
onEnded={handleAudioEnded}
/>
)}
</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}
onBeforeCompile={ShaderPatch}
/>
</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, 4, 10]} />
<meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
map={floorTex}
onBeforeCompile={ShaderPatch}
/>
</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, 4, 10]} />
<meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
map={floorTex}
onBeforeCompile={ShaderPatch}
/>
</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, 10, 4]} />
<meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
map={wallTex}
onBeforeCompile={ShaderPatch}
/>
</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, 10, 4]} />
<meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
map={wallTex}
onBeforeCompile={ShaderPatch}
/>
</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}
onBeforeCompile={ShaderPatch}
/>
</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}
onBeforeCompile={ShaderPatch}
/>
</mesh>
);
})}
</group>
</group>
))}
</>
);
}
+194
View File
@@ -0,0 +1,194 @@
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);
const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false);
useEffect(() => {
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z);
footstepAudio.current = Array.from({ length: 6 }, (_, i) => {
const audio = new Audio(`fear/snd/footstep${i + 1}.mp3`);
audio.volume = 0.4;
return audio;
});
}, []);
const playRandomFootstep = () => {
if (footstepAudio.current.length === 0) return;
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length);
const audio = footstepAudio.current[randomIndex];
audio.currentTime = 0;
audio.play().catch((err) => {
console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err);
});
};
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 sinWave = Math.sin(movementCounter.current);
const moveBobY = sinWave * 0.06 * bobIntensity.current;
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
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.lerp(camera.position, 7 * dt);
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}
/>
</>
);
}
+57
View File
@@ -0,0 +1,57 @@
export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
float precisionModifier = 200.0;
gl_Position.xy /= gl_Position.w;
gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier;
gl_Position.xy *= gl_Position.w;
vDepth = gl_Position.w;
#ifdef USE_MAP
vAffineUv = vMapUv * gl_Position.w;
#endif
`
);
shader.fragmentShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
#ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
vec2 warpDiff = flatAffineUV - vMapUv;
float warpDist = length(warpDiff);
float maxDistortion = 0.25;
float falloff = maxDistortion / (maxDistortion + warpDist);
vec2 finalUV = vMapUv + (warpDiff * falloff);
vec4 texelColor = texture2D( map, finalUV );
diffuseColor *= texelColor;
#endif
`
);
}
+74
View File
@@ -0,0 +1,74 @@
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,
EVENT_FINALE_DURATION: 1,
TEST_MODE: false
};
const listeners = new Set<() => void>();
export const fearState = {
loopCount: 0,
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
isRustActive: false,
finaleTriggered: false,
wasCaught: false,
finaleProgression: 0,
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;
}
if (this.wasCaught) {
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) {
this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
} else {
window.location.href = '/';
}
}
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();
}
};
+17 -2
View File
@@ -1,11 +1,26 @@
import type { Metadata } from 'next';
import type { Metadata, Viewport } from 'next';
import './globals.css';
export const metadata: Metadata = {
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({
children
}: Readonly<{
+46
View File
@@ -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);
}
}
+173
View File
@@ -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
}
+312
View File
@@ -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>
);
}
+28
View File
@@ -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);
float gx = worldPos.x;
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;
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
float taperFactor = pow(uv.y, 4.0);
float taper = 1.0 - taperFactor * 0.6;
// taper (fade)
float taperFactor = uv.y * uv.y * uv.y;
float taper = 1.0 - taperFactor * 0.85;
transformed.x *= taper;
transformed.z *= taper;
// curve
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;
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
transformed.x += curveAmount * curveDir.x * 0.5;
transformed.z += curveAmount * curveDir.y * 0.5;
transformed.x += curveAmount * curveDir.x * 0.4;
transformed.z += curveAmount * curveDir.y * 0.4;
// sway
float swayAmount = (totalWind + spring) * uv.y * uv.y;
transformed.x += swayAmount * windDir.x;
transformed.z += swayAmount * windDir.y;
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;
+279
View File
@@ -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>
);
}
+296 -29
View File
@@ -1,46 +1,313 @@
.canvas {
width: 100vw !important;
height: 100vh !important;
:root {
--bg: #fff;
--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;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
z-index: 4444;
transition: opacity 1s ease-in-out;
pointer-events: none;
opacity: 1;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(3px);
display: flex;
justify-content: center;
align-items: center;
will-change: opacity;
z-index: 1000;
}
.loader.hidden {
opacity: 0;
.modal-content {
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 {
width: 50vw;
height: auto;
display: block;
animation: spin 3s ease-in-out infinite;
will-change: transform;
.modal-header {
color: var(--text-title);
font-size: 1rem;
font-style: italic;
margin-bottom: 12px;
border-bottom: 1px dashed var(--accent);
padding-bottom: 5px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
.modal-body p {
font-size: 0.85rem;
margin: 6px 0;
color: var(--text-main);
}
.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;
}
+101 -3
View File
@@ -1,8 +1,106 @@
'use client';
import { useEffect, useState } from 'react';
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() {
return <SealHome />;
}
return (
<Content />
)
}
-684
View File
@@ -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>
</>
);
}
-13
View File
@@ -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;