[READ-ONLY] Mirror of https://github.com/flo-bit/bluesky-particles. flo-bit.dev/bluesky-visualizers/particles
0

Configure Feed

Select the types of activity you want to include in your feed.

switch to pixi, add icons, toggle buttons

Florian (Nov 18, 2024, 2:27 AM +0100) 595dd67a 567d7d84

+372 -320
+103 -37
index.html
··· 73 73 </head> 74 74 75 75 <body class="bg-black"> 76 - <div class="absolute top-2 left-2"> 77 - <span 78 - class="inline-flex items-center rounded-full bg-orange-400/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-orange-400 ring-1 ring-inset ring-orange-400/30" 79 - >new post</span 76 + <div class="absolute bottom-2 right-2 z-10 bg-black/90 p-2 rounded-xl"> 77 + <button 78 + id="like-button" 79 + class="inline-flex gap-1 items-center rounded-full bg-rose-500/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-rose-400 ring-1 ring-inset ring-rose-400/30" 80 80 > 81 - <span 82 - class="inline-flex items-center rounded-full bg-pink-400/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-pink-400 ring-1 ring-inset ring-pink-400/30" 83 - >new like</span 81 + <svg 82 + xmlns="http://www.w3.org/2000/svg" 83 + viewBox="0 0 24 24" 84 + fill="currentColor" 85 + class="size-5" 86 + > 87 + <path 88 + d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" 89 + /> 90 + </svg> 91 + 92 + new like 93 + </button> 94 + <button 95 + id="follow-button" 96 + class="inline-flex gap-1 items-center rounded-full bg-green-500/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-green-400 ring-1 ring-inset ring-green-400/30" 84 97 > 85 - <span 86 - class="inline-flex items-center rounded-full bg-red-400/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-red-400 ring-1 ring-inset ring-red-400/30" 87 - >new follow</span 98 + <svg 99 + xmlns="http://www.w3.org/2000/svg" 100 + viewBox="0 0 24 24" 101 + fill="currentColor" 102 + class="size-5" 103 + > 104 + <path 105 + d="M5.25 6.375a4.125 4.125 0 1 1 8.25 0 4.125 4.125 0 0 1-8.25 0ZM2.25 19.125a7.125 7.125 0 0 1 14.25 0v.003l-.001.119a.75.75 0 0 1-.363.63 13.067 13.067 0 0 1-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 0 1-.364-.63l-.001-.122ZM18.75 7.5a.75.75 0 0 0-1.5 0v2.25H15a.75.75 0 0 0 0 1.5h2.25v2.25a.75.75 0 0 0 1.5 0v-2.25H21a.75.75 0 0 0 0-1.5h-2.25V7.5Z" 106 + /> 107 + </svg> 108 + 109 + new follow 110 + </button> 111 + <button 112 + id="post-button" 113 + class="inline-flex gap-1 items-center rounded-full bg-amber-500/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-amber-400 ring-1 ring-inset ring-amber-400/30" 88 114 > 89 - <span 90 - class="inline-flex items-center rounded-full bg-zinc-400/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-zinc-400 ring-1 ring-inset ring-zinc-400/30" 91 - >new user</span 115 + <svg 116 + xmlns="http://www.w3.org/2000/svg" 117 + viewBox="0 0 24 24" 118 + fill="currentColor" 119 + class="size-5" 120 + > 121 + <path 122 + fill-rule="evenodd" 123 + d="M12 2.25c-2.429 0-4.817.178-7.152.521C2.87 3.061 1.5 4.795 1.5 6.741v6.018c0 1.946 1.37 3.68 3.348 3.97.877.129 1.761.234 2.652.316V21a.75.75 0 0 0 1.28.53l4.184-4.183a.39.39 0 0 1 .266-.112c2.006-.05 3.982-.22 5.922-.506 1.978-.29 3.348-2.023 3.348-3.97V6.741c0-1.947-1.37-3.68-3.348-3.97A49.145 49.145 0 0 0 12 2.25ZM8.25 8.625a1.125 1.125 0 1 0 0 2.25 1.125 1.125 0 0 0 0-2.25Zm2.625 1.125a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Zm4.875-1.125a1.125 1.125 0 1 0 0 2.25 1.125 1.125 0 0 0 0-2.25Z" 124 + clip-rule="evenodd" 125 + /> 126 + </svg> 127 + 128 + new post 129 + </button> 130 + <button 131 + id="user-button" 132 + class="inline-flex gap-1 items-center rounded-full bg-sky-500/10 backdrop-blur-sm px-2.5 py-1 text-xs font-medium text-sky-400 ring-1 ring-inset ring-sky-400/30" 92 133 > 93 - </div> 134 + <svg 135 + fill="currentColor" 136 + xmlns="http://www.w3.org/2000/svg" 137 + viewBox="-40 -40 680 620" 138 + version="1.1" 139 + class="size-5" 140 + > 141 + <path 142 + d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" 143 + ></path> 144 + </svg> 94 145 95 - <canvas id="root"></canvas> 146 + new user 147 + </button> 148 + </div> 96 149 97 - <a 98 - class="absolute bottom-2 left-3 text-cyan-400 text-sm font-semibold hover:text-cyan-300 transition-colors duration-100" 99 - href="https://flo-bit.dev" 100 - target="_blank" 101 - >made by flo-bit</a 150 + <div 151 + class="absolute text-white top-2 left-3 bg-black px-2 py-1 rounded-lg z-10 font-semibold" 102 152 > 153 + live bluesky activity 154 + </div> 103 155 104 - <a 105 - class="absolute top-2 right-3 text-zinc-300 hover:text-zinc-200 transition-colors duration-100" 106 - href="https://github.com/flo-bit/bluesky-particles" 107 - target="_blank" 156 + <div 157 + class="flex gap-2 items-center absolute top-2 right-3 text-white bg-black/70 px-2 py-1 rounded-lg text-sm font-semibold transition-colors duration-100 z-10" 108 158 > 109 - <span class="sr-only">GitHub Code</span> 110 - <svg 111 - fill="currentColor" 112 - viewBox="0 0 24 24" 113 - aria-hidden="true" 114 - class="w-8 h-8" 159 + <div> 160 + made by 161 + <a 162 + href="https://flo-bit.dev" 163 + class="text-cyan-200 hover:text-cyan-400 transition-colors duration-100" 164 + target="_blank" 165 + >flo-bit</a 166 + > 167 + </div> 168 + 169 + <a 170 + class="text-cyan-200 hover:text-cyan-400 transition-colors duration-100 z-10" 171 + href="https://github.com/flo-bit/bluesky-particles" 172 + target="_blank" 115 173 > 116 - <path 117 - fill-rule="evenodd" 118 - d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" 119 - clip-rule="evenodd" 120 - /> 121 - </svg> 122 - </a> 174 + <span class="sr-only">GitHub Code</span> 175 + <svg 176 + fill="currentColor" 177 + viewBox="0 0 24 24" 178 + aria-hidden="true" 179 + class="size-6" 180 + > 181 + <path 182 + fill-rule="evenodd" 183 + d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" 184 + clip-rule="evenodd" 185 + /> 186 + </svg> 187 + </a> 188 + </div> 123 189 124 190 <script src="src/script.ts" type="module"></script> 125 191 </body>
+71 -83
package-lock.json
··· 6 6 "": { 7 7 "name": "bluesky-particles", 8 8 "dependencies": { 9 - "@types/three": "^0.168.0", 10 - "alea": "^1.0.1", 9 + "pixi.js": "^8.5.2", 10 + "postprocessing": "^6.36.4", 11 11 "prettier": "^3.3.3", 12 - "simplex-noise": "^4.0.3", 13 - "three": "^0.166.1", 14 - "uber-noise": "^0.2.1", 15 - "vite": "^5.4.3", 16 - "ws": "^8.18.0" 12 + "vite": "^5.4.3" 17 13 }, 18 14 "devDependencies": { 19 15 "autoprefixer": "^10.4.20", ··· 478 474 "node": ">= 8" 479 475 } 480 476 }, 477 + "node_modules/@pixi/colord": { 478 + "version": "2.9.6", 479 + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", 480 + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" 481 + }, 481 482 "node_modules/@pkgjs/parseargs": { 482 483 "version": "0.11.0", 483 484 "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", ··· 680 681 "win32" 681 682 ] 682 683 }, 683 - "node_modules/@tweenjs/tween.js": { 684 - "version": "23.1.3", 685 - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", 686 - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==" 684 + "node_modules/@types/css-font-loading-module": { 685 + "version": "0.0.12", 686 + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", 687 + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==" 688 + }, 689 + "node_modules/@types/earcut": { 690 + "version": "2.1.4", 691 + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", 692 + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==" 687 693 }, 688 694 "node_modules/@types/estree": { 689 695 "version": "1.0.5", 690 696 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", 691 697 "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" 692 698 }, 693 - "node_modules/@types/stats.js": { 694 - "version": "0.17.3", 695 - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", 696 - "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==" 697 - }, 698 - "node_modules/@types/three": { 699 - "version": "0.168.0", 700 - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.168.0.tgz", 701 - "integrity": "sha512-qAGLGzbaYgkkonOBfwOr+TZpOskPfFjrDAj801WQSVkUz0/D9zwir4vhruJ/CC/GteywzR9pqeVVfs5th/2oKw==", 702 - "dependencies": { 703 - "@tweenjs/tween.js": "~23.1.3", 704 - "@types/stats.js": "*", 705 - "@types/webxr": "*", 706 - "@webgpu/types": "*", 707 - "fflate": "~0.8.2", 708 - "meshoptimizer": "~0.18.1" 709 - } 710 - }, 711 - "node_modules/@types/webxr": { 712 - "version": "0.5.20", 713 - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.20.tgz", 714 - "integrity": "sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==" 715 - }, 716 699 "node_modules/@webgpu/types": { 717 700 "version": "0.1.44", 718 701 "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", 719 702 "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==" 720 703 }, 721 - "node_modules/alea": { 722 - "version": "1.0.1", 723 - "resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz", 724 - "integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==" 704 + "node_modules/@xmldom/xmldom": { 705 + "version": "0.8.10", 706 + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", 707 + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", 708 + "engines": { 709 + "node": ">=10.0.0" 710 + } 725 711 }, 726 712 "node_modules/ansi-regex": { 727 713 "version": "6.1.0", ··· 1010 996 "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", 1011 997 "dev": true 1012 998 }, 999 + "node_modules/earcut": { 1000 + "version": "2.2.4", 1001 + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", 1002 + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" 1003 + }, 1013 1004 "node_modules/eastasianwidth": { 1014 1005 "version": "0.2.0", 1015 1006 "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", ··· 1074 1065 "node": ">=6" 1075 1066 } 1076 1067 }, 1068 + "node_modules/eventemitter3": { 1069 + "version": "5.0.1", 1070 + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", 1071 + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" 1072 + }, 1077 1073 "node_modules/fast-glob": { 1078 1074 "version": "3.3.2", 1079 1075 "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", ··· 1110 1106 "dependencies": { 1111 1107 "reusify": "^1.0.4" 1112 1108 } 1113 - }, 1114 - "node_modules/fflate": { 1115 - "version": "0.8.2", 1116 - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", 1117 - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" 1118 1109 }, 1119 1110 "node_modules/fill-range": { 1120 1111 "version": "7.1.1", ··· 1295 1286 "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1296 1287 "dev": true 1297 1288 }, 1289 + "node_modules/ismobilejs": { 1290 + "version": "1.1.1", 1291 + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", 1292 + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" 1293 + }, 1298 1294 "node_modules/jackspeak": { 1299 1295 "version": "3.4.3", 1300 1296 "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", ··· 1348 1344 "engines": { 1349 1345 "node": ">= 8" 1350 1346 } 1351 - }, 1352 - "node_modules/meshoptimizer": { 1353 - "version": "0.18.1", 1354 - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", 1355 - "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==" 1356 1347 }, 1357 1348 "node_modules/micromatch": { 1358 1349 "version": "4.0.8", ··· 1467 1458 "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", 1468 1459 "dev": true 1469 1460 }, 1461 + "node_modules/parse-svg-path": { 1462 + "version": "0.1.2", 1463 + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", 1464 + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==" 1465 + }, 1470 1466 "node_modules/path-key": { 1471 1467 "version": "3.1.1", 1472 1468 "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", ··· 1533 1529 "node": ">= 6" 1534 1530 } 1535 1531 }, 1532 + "node_modules/pixi.js": { 1533 + "version": "8.5.2", 1534 + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.5.2.tgz", 1535 + "integrity": "sha512-TOt9g8ifOj4R9DN9ST1M8t2nvnuhr5oWL5YW9ywFLbnOVgFMDcEz+Xek5Mo8Xr64D+QU3qre3IFgreBlsHxTNw==", 1536 + "dependencies": { 1537 + "@pixi/colord": "^2.9.6", 1538 + "@types/css-font-loading-module": "^0.0.12", 1539 + "@types/earcut": "^2.1.4", 1540 + "@webgpu/types": "^0.1.40", 1541 + "@xmldom/xmldom": "^0.8.10", 1542 + "earcut": "^2.2.4", 1543 + "eventemitter3": "^5.0.1", 1544 + "ismobilejs": "^1.1.1", 1545 + "parse-svg-path": "^0.1.2" 1546 + } 1547 + }, 1536 1548 "node_modules/postcss": { 1537 1549 "version": "8.4.47", 1538 1550 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", ··· 1686 1698 "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 1687 1699 "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 1688 1700 "dev": true 1701 + }, 1702 + "node_modules/postprocessing": { 1703 + "version": "6.36.4", 1704 + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.36.4.tgz", 1705 + "integrity": "sha512-3fAyBGuLNR7Rg/q+f2SNlsVCI5mDrymvxhOif3tPKEej8M38z4TvwzNZm+RNpRY2tACE7qQUNE5IHyMcqgvlAg==", 1706 + "peerDependencies": { 1707 + "three": ">= 0.157.0 < 0.171.0" 1708 + } 1689 1709 }, 1690 1710 "node_modules/prettier": { 1691 1711 "version": "3.3.3", ··· 1859 1879 "url": "https://github.com/sponsors/isaacs" 1860 1880 } 1861 1881 }, 1862 - "node_modules/simplex-noise": { 1863 - "version": "4.0.3", 1864 - "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz", 1865 - "integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==" 1866 - }, 1867 1882 "node_modules/source-map-js": { 1868 1883 "version": "1.2.1", 1869 1884 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", ··· 2062 2077 }, 2063 2078 "node_modules/three": { 2064 2079 "version": "0.166.1", 2065 - "license": "MIT" 2080 + "resolved": "https://registry.npmjs.org/three/-/three-0.166.1.tgz", 2081 + "integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==", 2082 + "peer": true 2066 2083 }, 2067 2084 "node_modules/to-regex-range": { 2068 2085 "version": "5.0.1", ··· 2082 2099 "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", 2083 2100 "dev": true 2084 2101 }, 2085 - "node_modules/uber-noise": { 2086 - "version": "0.2.1", 2087 - "resolved": "https://registry.npmjs.org/uber-noise/-/uber-noise-0.2.1.tgz", 2088 - "integrity": "sha512-0TfMzwduiMJ18BQjeeioBqdQHcBU1Ns+HbnXhA3F6NPdP0vzjNzk/MFaFIN3f2MIe2MdS+WBgIojcynPrTyvlQ==", 2089 - "dependencies": { 2090 - "alea": "^1.0.1", 2091 - "simplex-noise": "^4.0.3" 2092 - } 2093 - }, 2094 2102 "node_modules/update-browserslist-db": { 2095 2103 "version": "1.1.0", 2096 2104 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", ··· 2289 2297 }, 2290 2298 "engines": { 2291 2299 "node": ">=8" 2292 - } 2293 - }, 2294 - "node_modules/ws": { 2295 - "version": "8.18.0", 2296 - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 2297 - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 2298 - "engines": { 2299 - "node": ">=10.0.0" 2300 - }, 2301 - "peerDependencies": { 2302 - "bufferutil": "^4.0.1", 2303 - "utf-8-validate": ">=5.0.2" 2304 - }, 2305 - "peerDependenciesMeta": { 2306 - "bufferutil": { 2307 - "optional": true 2308 - }, 2309 - "utf-8-validate": { 2310 - "optional": true 2311 - } 2312 2300 } 2313 2301 }, 2314 2302 "node_modules/yaml": {
+3 -7
package.json
··· 7 7 "preview": "vite preview" 8 8 }, 9 9 "dependencies": { 10 - "@types/three": "^0.168.0", 11 - "alea": "^1.0.1", 10 + "pixi.js": "^8.5.2", 11 + "postprocessing": "^6.36.4", 12 12 "prettier": "^3.3.3", 13 - "simplex-noise": "^4.0.3", 14 - "three": "^0.166.1", 15 - "uber-noise": "^0.2.1", 16 - "vite": "^5.4.3", 17 - "ws": "^8.18.0" 13 + "vite": "^5.4.3" 18 14 }, 19 15 "devDependencies": { 20 16 "autoprefixer": "^10.4.20",
public/follow.png

This is a binary file and will not be displayed.

public/follow2.png

This is a binary file and will not be displayed.

public/heart.png

This is a binary file and will not be displayed.

public/heart2.png

This is a binary file and will not be displayed.

public/post.png

This is a binary file and will not be displayed.

public/post2.png

This is a binary file and will not be displayed.

public/user.png

This is a binary file and will not be displayed.

public/user2.png

This is a binary file and will not be displayed.

+85 -149
src/particles.ts
··· 1 - import * as THREE from "three"; 1 + import * as PIXI from "pixi.js"; 2 + 3 + interface ParticleSprite extends PIXI.Sprite { 4 + speedX: number; 5 + speedY: number; 6 + age: number; 7 + maxAge: number; 8 + initialSize: number; 9 + } 2 10 3 - export type StarsOptions = { 4 - particleCount: number; 5 - minimumDistance: number; 6 - maximumDistance: number; 11 + export type ParticleOptions = { 12 + x: number; 13 + y: number; 14 + size: number; 15 + color: PIXI.ColorSource; 16 + alpha: number; 17 + speedX: number; 18 + speedY: number; 19 + maxAge: number; 7 20 }; 8 21 9 - const SPEED = 0.001; 22 + export default class ParticleSystem { 23 + private app: PIXI.Application; 24 + private maxParticles: number; 25 + public container: PIXI.Container; 26 + private particles: ParticleSprite[]; 27 + private particlePool: ParticleSprite[]; 28 + private baseTexture: PIXI.Texture; 10 29 11 - export class Particles extends THREE.Group { 12 - private particleCount: number = 50000; 30 + private loaded = false; 13 31 14 - private minimumDistance: number = 10; 15 - private maximumDistance: number = 20; 16 - 17 - private positions: Float32Array; 18 - private colors: Float32Array; 19 - private velocities: Float32Array; 20 - private alive: Float32Array; 21 - 22 - private geometry: THREE.BufferGeometry; 23 - private material: THREE.PointsMaterial; 24 - private particles: THREE.Points; 25 - 26 - private spawnPost: number = 0; 27 - private spawnLike: number = 0; 28 - private spawnFollow: number = 0; 29 - private spawnNewUser: number = 0; 30 - 31 - constructor(opts: Partial<StarsOptions> = {}) { 32 - super(); 33 - 34 - this.particleCount = opts.particleCount ?? this.particleCount; 35 - this.minimumDistance = opts.minimumDistance ?? this.minimumDistance; 36 - this.maximumDistance = opts.maximumDistance ?? this.maximumDistance; 37 - 38 - this.positions = new Float32Array(this.particleCount * 3); 39 - this.colors = new Float32Array(this.particleCount * 3); 40 - this.velocities = new Float32Array(this.particleCount * 3); 41 - this.alive = new Float32Array(this.particleCount); 42 - this.setupParticles(); 32 + constructor(texture: string, maxParticles: number = 10000) { 33 + this.maxParticles = maxParticles; 34 + this.container = new PIXI.Container(); 35 + this.container.zIndex = 10; 36 + this.particles = []; 37 + this.particlePool = []; 38 + this.loadTexture(texture); 43 39 } 44 40 45 - private setupParticles() { 46 - for (let i = 0; i < this.particleCount; i++) { 47 - this.resetParticle(i, false); 48 - } 49 - 50 - this.geometry = new THREE.BufferGeometry(); 51 - this.geometry.setAttribute( 52 - "position", 53 - new THREE.BufferAttribute(this.positions, 3), 54 - ); 55 - this.geometry.setAttribute( 56 - "color", 57 - new THREE.BufferAttribute(this.colors, 3), 58 - ); 59 - 60 - this.material = new THREE.PointsMaterial({ 61 - size: 0.04, 62 - vertexColors: true, 63 - }); 64 - 65 - this.particles = new THREE.Points(this.geometry, this.material); 66 - 67 - this.add(this.particles); 41 + async loadTexture(url: string) { 42 + const texture = await PIXI.Assets.load(url); 43 + this.baseTexture = texture; 44 + this.loaded = true; 68 45 } 69 46 70 - spawn(color: number) { 71 - if (color === 1) { 72 - this.spawnPost += 1; 73 - } else if (color === 2) { 74 - this.spawnLike += 1; 75 - } else if (color === 3) { 76 - this.spawnFollow += 1; 47 + spawnParticle(opts: Partial<ParticleOptions>): ParticleSprite | null { 48 + if (!this.loaded) return null; 49 + let particle: ParticleSprite; 50 + if (this.particlePool.length > 0) { 51 + particle = this.particlePool.pop()!; 52 + } else if (this.particles.length < this.maxParticles) { 53 + particle = new PIXI.Sprite(this.baseTexture) as ParticleSprite; 54 + particle.anchor.set(0.5, 0.5); 55 + this.container.addChild(particle); 77 56 } else { 78 - this.spawnNewUser += 1; 57 + return null; 79 58 } 80 - } 81 59 82 - update(delta: number) { 83 - for (let i = 0; i < this.particleCount; i++) { 84 - if (this.alive[i] <= 0) { 85 - if (this.spawnPost > 0) { 86 - this.resetParticle(i, true, 1); 87 - this.spawnPost--; 88 - } else if (this.spawnLike > 0) { 89 - this.resetParticle(i, true, 2); 90 - this.spawnLike--; 91 - } else if (this.spawnFollow > 0) { 92 - this.resetParticle(i, true, 3); 93 - this.spawnFollow--; 94 - } else if (this.spawnNewUser > 0) { 95 - this.resetParticle(i, true, 4); 96 - this.spawnNewUser--; 97 - } 98 - } 60 + particle.rotation = (Math.random() - 0.5) * 0.5; 61 + particle.visible = true; 62 + particle.x = opts.x ?? 0; 63 + particle.y = opts.y ?? 0; 64 + particle.width = particle.height = opts.size ?? 1; 65 + particle.tint = opts.color ?? 0xffffff; 66 + particle.alpha = opts.alpha ?? 1; 67 + particle.speedX = opts.speedX ?? 0; 68 + particle.speedY = opts.speedY ?? 0; 69 + particle.age = 0; 70 + particle.maxAge = opts.maxAge ?? 1; 71 + particle.initialSize = opts.size ?? 1; 99 72 100 - const index = i * 3; 101 - this.positions[index] += this.velocities[index] * delta * SPEED; 102 - this.positions[index + 1] += this.velocities[index + 1] * delta * SPEED; 103 - this.positions[index + 2] += this.velocities[index + 2] * delta * SPEED; 73 + this.particles.push(particle); 104 74 105 - this.alive[i] -= delta; 75 + return particle; 76 + } 106 77 107 - if (this.alive[i] <= 0) { 108 - this.resetParticle(i, false); 109 - } 110 - } 78 + removeParticle(particle: ParticleSprite): void { 79 + const index = this.particles.indexOf(particle); 80 + if (index !== -1) { 81 + particle.visible = false; 111 82 112 - if ( 113 - this.spawnPost > 0 || 114 - this.spawnLike > 0 || 115 - this.spawnFollow > 0 || 116 - this.spawnNewUser > 0 117 - ) { 118 - console.log("not enough particles"); 83 + this.particles.splice(index, 1); 84 + this.particlePool.push(particle); 119 85 } 120 - 121 - this.geometry.attributes.position.needsUpdate = true; 122 - // Need to mark colors as needing update too 123 - this.geometry.attributes.color.needsUpdate = true; 124 86 } 125 87 126 - private resetParticle(i: number, alive: boolean = true, color: number = 1) { 127 - const index = i * 3; 88 + update(deltaTime: number): void { 89 + for (let i = this.particles.length - 1; i >= 0; i--) { 90 + const particle = this.particles[i]; 91 + particle.x += particle.speedX * deltaTime; 92 + particle.y += particle.speedY * deltaTime; 93 + particle.age += deltaTime; 128 94 129 - let size = 10; 130 - let angle = Math.random() * Math.PI * 2; 131 - this.positions[index] = Math.cos(angle) * size; 132 - this.positions[index + 1] = Math.sin(angle) * size; 133 - this.positions[index + 2] = -10 + Math.random() * 9; 134 - 135 - // posts are orange 136 - if (color === 1) { 137 - this.colors[index] = 0.97; 138 - this.colors[index + 1] = 0.45; 139 - this.colors[index + 2] = 0.08; 140 - } else if (color === 2) { 141 - // likes are pink 142 - this.colors[index] = 0.85; 143 - this.colors[index + 1] = 0.15; 144 - this.colors[index + 2] = 0.46; 145 - } else if (color === 3) { 146 - // follows are cyan 147 - this.colors[index] = 0.86; 148 - this.colors[index + 1] = 0.14; 149 - this.colors[index + 2] = 0.14; 150 - } else { 151 - this.colors[index] = 1; 152 - this.colors[index + 1] = 1; 153 - this.colors[index + 2] = 1; 154 - } 95 + // Shrink the particle 96 + let lifeRatio = 1 - particle.age / particle.maxAge; 97 + lifeRatio = Math.min(Math.max(lifeRatio, 0), 1); 98 + particle.width = particle.height = particle.initialSize * lifeRatio; 155 99 156 - if (!alive) { 157 - this.colors[index] = 0; 158 - this.colors[index + 1] = 0; 159 - this.colors[index + 2] = 0; 100 + // Remove particle if it's too old or off-screen 101 + if ( 102 + particle.age >= particle.maxAge || 103 + particle.initialSize * lifeRatio < 4 104 + ) { 105 + this.removeParticle(particle); 106 + } 160 107 } 161 - 162 - let speed = Math.pow(Math.random(), 4) * 0.5 + 1; 163 - let move = 0.4; 164 - 165 - this.velocities[index] = 166 - (-Math.cos(angle) + Math.random() * move - move / 2) * speed; 167 - this.velocities[index + 1] = 168 - (-Math.sin(angle) + Math.random() * move - move / 2) * speed; 169 - this.velocities[index + 2] = Math.random() * move - move / 2; 170 - 171 - this.alive[i] = alive ? 5000 : 0; 172 108 } 173 109 }
+110 -44
src/script.ts
··· 1 - import * as THREE from "three"; 2 - import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 - import { Particles } from "./particles"; 4 - 5 - const url = 6 - "wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.like&wantedCollections=app.bsky.graph.follow"; 1 + import * as PIXI from "pixi.js"; 2 + import ParticleSystem from "./particles"; 7 3 8 - const width = window.innerWidth, 9 - height = window.innerHeight; 4 + let app = new PIXI.Application(); 10 5 11 - const canvas = document.getElementById("root"); 6 + let w = window.innerWidth, 7 + h = window.innerHeight; 12 8 13 - if (!canvas) { 14 - throw new Error("Canvas not found"); 15 - } 16 - const scene = new THREE.Scene(); 17 - const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 30); 18 - camera.position.set(0, 0, 2.5); 19 - 20 - const renderer = new THREE.WebGLRenderer({ 9 + // set to full screen 10 + await app.init({ 11 + width: w, 12 + height: h, 21 13 antialias: true, 22 - canvas, 23 - alpha: true, 24 14 }); 25 15 26 - // basic tone mapping 27 - renderer.toneMapping = THREE.LinearToneMapping; 28 - renderer.shadowMap.enabled = true; 16 + document.body.appendChild(app.canvas); 29 17 30 - const _ = new OrbitControls(camera, renderer.domElement); 18 + // set canvas to absolute position and full size 19 + app.canvas.style.position = "absolute"; 20 + app.canvas.style.width = "100%"; 21 + app.canvas.style.height = "100%"; 31 22 32 - const particles = new Particles(); 33 - scene.add(particles); 23 + let heartParticles = new ParticleSystem("heart2.png"); 24 + let postParticles = new ParticleSystem("post.png"); 25 + let followParticles = new ParticleSystem("follow.png"); 26 + let userParticles = new ParticleSystem("user.png"); 34 27 35 - let total = 0; 36 - let last = 0; 28 + app.stage.addChild(followParticles.container); 29 + app.stage.addChild(heartParticles.container); 30 + app.stage.addChild(postParticles.container); 31 + app.stage.addChild(userParticles.container); 37 32 38 - renderer.setAnimationLoop((current) => { 39 - renderer.render(scene, camera); 33 + let totalTime = 0; 40 34 41 - particles.update(current - last); 35 + function spawnParticle(type: "heart" | "post" | "follow" | "user") { 36 + let particleSystem; 37 + if (type === "heart") { 38 + particleSystem = heartParticles; 39 + } else if (type === "post") { 40 + particleSystem = postParticles; 41 + } else if (type === "follow") { 42 + particleSystem = followParticles; 43 + } else if (type === "user") { 44 + particleSystem = userParticles; 45 + } 46 + let top = true; 47 + particleSystem.spawnParticle({ 48 + x: Math.random() * w, 49 + y: top ? -50 : h + 50, 50 + size: (Math.pow(Math.random(), 4) * 40 + 10) * (type === "user" ? 1.5 : 1), 51 + maxAge: Math.random() * 10 + 4, 52 + speedY: Math.random() * 150 * (top ? 1 : -1) + 50, 53 + speedX: Math.random() * 10 - 5, 54 + }); 55 + } 42 56 43 - last = current; 57 + const visible = { 58 + heart: true, 59 + post: true, 60 + follow: true, 61 + user: true, 62 + }; 44 63 45 - //particles.spawn(Math.floor(Math.random() * 4) + 1); 64 + app.ticker.add((ticker) => { 65 + // get ellapsed time 66 + const deltaTime = ticker.deltaMS * 0.001; 67 + totalTime += deltaTime; 68 + 69 + heartParticles.update(deltaTime); 70 + postParticles.update(deltaTime); 71 + followParticles.update(deltaTime); 72 + userParticles.update(deltaTime); 73 + }); 74 + 75 + window.addEventListener("resize", () => { 76 + w = window.innerWidth; 77 + h = window.innerHeight; 78 + app.renderer.resize(w, h); 79 + }); 80 + 81 + // post-button 82 + document.getElementById("post-button")?.addEventListener("click", () => { 83 + visible.post = !visible.post; 84 + if (visible.post) { 85 + postParticles.container.visible = true; 86 + } else { 87 + postParticles.container.visible = false; 88 + } 89 + }); 90 + 91 + // like-button 92 + document.getElementById("like-button")?.addEventListener("click", () => { 93 + visible.heart = !visible.heart; 94 + if (visible.heart) { 95 + heartParticles.container.visible = true; 96 + } else { 97 + heartParticles.container.visible = false; 98 + } 46 99 }); 47 100 48 - function updateSize() { 49 - renderer.setSize(window.innerWidth, window.innerHeight); 50 - renderer.setPixelRatio(window.devicePixelRatio); 51 - camera.aspect = window.innerWidth / window.innerHeight; 52 - camera.updateProjectionMatrix(); 53 - } 101 + // follow-button 102 + document.getElementById("follow-button")?.addEventListener("click", () => { 103 + visible.follow = !visible.follow; 104 + if (visible.follow) { 105 + followParticles.container.visible = true; 106 + } else { 107 + followParticles.container.visible = false; 108 + } 109 + }); 54 110 55 - updateSize(); 56 - window.addEventListener("resize", updateSize); 111 + // user-button 112 + document.getElementById("user-button")?.addEventListener("click", () => { 113 + visible.user = !visible.user; 114 + if (visible.user) { 115 + userParticles.container.visible = true; 116 + } else { 117 + userParticles.container.visible = false; 118 + } 119 + }); 120 + 121 + const url = 122 + "wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.like&wantedCollections=app.bsky.graph.follow"; 57 123 58 124 // WebSocket logic 59 125 const ws = new WebSocket(url); ··· 69 135 json.commit.collection === "app.bsky.feed.post" && 70 136 json.commit.operation === "create" 71 137 ) { 72 - particles.spawn(1); 138 + spawnParticle("post"); 73 139 } else if ( 74 140 json.commit.collection === "app.bsky.feed.like" && 75 141 json.commit.operation === "create" 76 142 ) { 77 - particles.spawn(2); 143 + spawnParticle("heart"); 78 144 } else if ( 79 145 json.commit.collection === "app.bsky.graph.follow" && 80 146 json.commit.operation === "create" 81 147 ) { 82 - particles.spawn(3); 148 + spawnParticle("follow"); 83 149 } 84 150 } else if (json.kind === "account" && json.account.active) { 85 - particles.spawn(4); 151 + spawnParticle("user"); 86 152 } 87 153 }; 88 154