[READ-ONLY] Mirror of https://github.com/flo-bit/bluesky-visualizers. visualizing bluesky live data in different ways flo-bit.dev/bluesky-visualizers/
bluesky svelte visualizations
0

Configure Feed

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

Merge pull request #7 from flo-bit/main

butterflies

authored by

Florian and committed by
GitHub
(Nov 20, 2024, 10:08 PM +0100) 6293de8a ba1d72be

+451 -50
static/butterflies.webp

This is a binary file and will not be displayed.

+51 -50
src/lib/Credit.svelte
··· 4 4 export let showAll: boolean = true; 5 5 </script> 6 6 7 - <div class="absolute top-2 left-3 right-3 z-10 flex justify-between gap-4"> 8 - <div class="rounded-lg bg-gray-950/70 px-2 py-1 font-semibold backdrop-blur-sm text-white text-sm"> 9 - bluesky visualizers 10 - {#if name} 11 - - {name} 12 - {/if} 13 - 14 - {#if showAll} 15 - <a class="ml-2 text-sm text-cyan-400" href="/bluesky-visualizers">see all</a> 16 - {/if} 17 - </div> 18 - 19 - <div 20 - class="flex items-center gap-2 rounded-lg bg-gray-950/70 backdrop-blur-sm px-2 py-1 text-sm font-semibold text-white transition-colors duration-100" 21 - > 22 - <div> 23 - made by 24 - <a 25 - href="https://flo-bit.dev" 26 - class="text-cyan-400 transition-colors duration-100 hover:text-cyan-200" 27 - target="_blank">flo-bit</a 28 - > 7 + <div class="absolute left-3 right-3 top-2 z-10 flex justify-between items-center gap-4"> 8 + <div 9 + class="rounded-lg bg-gray-950/70 px-2 py-1 text-xs font-semibold text-white backdrop-blur-sm" 10 + > 11 + {#if showAll} 12 + <a class="ml-2 text-xs sm:text-md text-cyan-400" href="/bluesky-visualizers">see all visualizations</a> 13 + {/if} 29 14 </div> 30 15 31 - <a 32 - class="z-10 text-cyan-400 transition-colors duration-100 hover:text-cyan-200" 33 - href="https://github.com/flo-bit/bluesky-visualizers" 34 - target="_blank" 16 + <div 17 + class="flex items-center gap-2 rounded-lg bg-gray-950/70 px-2 py-1 text-sm font-semibold text-white backdrop-blur-sm transition-colors duration-100" 35 18 > 36 - <span class="sr-only">GitHub Code</span> 37 - <svg fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" class="size-6"> 38 - <path 39 - fill-rule="evenodd" 40 - 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" 41 - clip-rule="evenodd" 42 - /> 43 - </svg> 44 - </a> 19 + <div class="text-xs sm:text-md"> 20 + made by 21 + <a 22 + href="https://flo-bit.dev" 23 + class="text-cyan-400 transition-colors duration-100 hover:text-cyan-200" 24 + target="_blank">flo-bit</a 25 + > 26 + </div> 45 27 46 - <a href="https://bsky.app/profile/flo-bit.dev" target="_blank" class="text-cyan-400 transition-colors duration-100 hover:text-cyan-200"> 47 - <span class="sr-only">follow me on bluesky</span> 48 - <svg 49 - fill="currentColor" 50 - xmlns="http://www.w3.org/2000/svg" 51 - viewBox="-40 -40 680 620" 52 - version="1.1" 53 - class="size-6" 54 - > 55 - <path 56 - 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" 57 - ></path> 58 - </svg> 59 - </a> 28 + <a 29 + class="z-10 text-cyan-400 transition-colors duration-100 hover:text-cyan-200" 30 + href="https://github.com/flo-bit/bluesky-visualizers" 31 + target="_blank" 32 + > 33 + <span class="sr-only">GitHub Code</span> 34 + <svg fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" class="size-6"> 35 + <path 36 + fill-rule="evenodd" 37 + 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" 38 + clip-rule="evenodd" 39 + /> 40 + </svg> 41 + </a> 42 + 43 + <a 44 + href="https://bsky.app/profile/flo-bit.dev" 45 + target="_blank" 46 + class="text-cyan-400 transition-colors duration-100 hover:text-cyan-200" 47 + > 48 + <span class="sr-only">follow me on bluesky</span> 49 + <svg 50 + fill="currentColor" 51 + xmlns="http://www.w3.org/2000/svg" 52 + viewBox="-40 -40 680 620" 53 + version="1.1" 54 + class="size-6" 55 + > 56 + <path 57 + 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" 58 + ></path> 59 + </svg> 60 + </a> 61 + </div> 60 62 </div> 61 - </div>
+21
src/routes/+page.svelte
··· 9 9 <div class="sm:flex sm:items-baseline sm:justify-between"> 10 10 <h2 class="text-2xl font-bold tracking-tight text-gray-50">Bluesky Visualizers</h2> 11 11 </div> 12 + <div class="group relative mt-6 aspect-[3/1] overflow-hidden rounded-lg border border-gray-900"> 13 + <img 14 + src="/bluesky-visualizers/butterflies.webp" 15 + alt="" 16 + class="absolute size-full object-cover transition-opacity duration-75 group-hover:opacity-75" 17 + /> 18 + <div 19 + aria-hidden="true" 20 + class="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-80" 21 + ></div> 22 + <div class="absolute inset-0 flex items-end p-6"> 23 + <div> 24 + <h3 class="font-semibold text-white"> 25 + <a href="/bluesky-visualizers/butterflies"> 26 + <span class="absolute inset-0"></span> 27 + Butterflies 28 + </a> 29 + </h3> 30 + </div> 31 + </div> 32 + </div> 12 33 13 34 <div class="mt-6 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:grid-rows-2 sm:gap-x-6 lg:gap-8"> 14 35 <div class="flex flex-col gap-8 sm:row-span-2">
+15
src/routes/butterflies/+page.svelte
··· 1 + <script> 2 + import { Canvas } from '@threlte/core'; 3 + import Scene from './Scene.svelte'; 4 + import Credit from '$lib/Credit.svelte'; 5 + </script> 6 + 7 + <Credit name="planet" /> 8 + 9 + <div class="h-screen w-screen bg-black"> 10 + <Canvas> 11 + <Scene /> 12 + </Canvas> 13 + </div> 14 + 15 + <div class="absolute bottom-1 left-1 right-1 text-center text-white font-semibold text-sm px-2">every butterfly is one new user</div>
+175
src/routes/butterflies/Butterfly.svelte
··· 1 + <script lang="ts"> 2 + import { T, useTask } from '@threlte/core'; 3 + import { onMount } from 'svelte'; 4 + import * as THREE from 'three'; 5 + import { Butterfly } from './butterfly'; 6 + 7 + let geometry: THREE.BufferGeometry; 8 + 9 + let instancedMesh: THREE.InstancedMesh; 10 + 11 + let mesh: THREE.Mesh; 12 + 13 + let numButterflies = 10000; 14 + 15 + let butterflies: Butterfly[] = []; 16 + 17 + export let count = 0; 18 + 19 + function createButterfly(texture: THREE.Texture) { 20 + geometry = new THREE.PlaneGeometry(2, 2, 2, 2).toNonIndexed(); 21 + 22 + const positions = geometry.getAttribute('position'); 23 + console.log(positions); 24 + const morphPositions = new Float32Array(positions.count * 3); 25 + const morphNormals = new Float32Array(positions.count * 3); 26 + 27 + geometry.rotateX(-Math.PI / 2 + 0.5); 28 + let scale = 1; 29 + geometry.scale(scale, scale, scale); 30 + 31 + let v = new THREE.Vector3(); 32 + for (let i = 0; i < positions.count; i++) { 33 + v.fromBufferAttribute(positions, i); 34 + morphPositions[i * 3] = v.x * (scale - Math.abs(v.x) < 0.01 ? 0.1 : 1); 35 + morphPositions[i * 3 + 1] = v.y; 36 + morphPositions[i * 3 + 2] = v.z + (scale - Math.abs(v.x) < 0.01 ? scale : 0); 37 + } 38 + 39 + // calculate morph normals 40 + for (let i = 0; i < positions.count; i += 3) { 41 + let a = new THREE.Vector3( 42 + morphPositions[i * 3], 43 + morphPositions[i * 3 + 1], 44 + morphPositions[i * 3 + 2] 45 + ); 46 + let b = new THREE.Vector3( 47 + morphPositions[i * 3 + 3], 48 + morphPositions[i * 3 + 4], 49 + morphPositions[i * 3 + 5] 50 + ); 51 + let c = new THREE.Vector3( 52 + morphPositions[i * 3 + 6], 53 + morphPositions[i * 3 + 7], 54 + morphPositions[i * 3 + 8] 55 + ); 56 + 57 + let normal = new THREE.Vector3(); 58 + normal.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); 59 + 60 + morphNormals[i * 3] = normal.x; 61 + morphNormals[i * 3 + 1] = normal.y; 62 + morphNormals[i * 3 + 2] = normal.z; 63 + 64 + morphNormals[i * 3 + 3] = normal.x; 65 + morphNormals[i * 3 + 4] = normal.y; 66 + morphNormals[i * 3 + 5] = normal.z; 67 + 68 + morphNormals[i * 3 + 6] = normal.x; 69 + morphNormals[i * 3 + 7] = normal.y; 70 + morphNormals[i * 3 + 8] = normal.z; 71 + } 72 + 73 + geometry.morphAttributes.position = [new THREE.Float32BufferAttribute(morphPositions, 3)]; 74 + geometry.morphAttributes.normal = [new THREE.Float32BufferAttribute(morphNormals, 3)]; 75 + 76 + mesh = new THREE.Mesh( 77 + geometry, 78 + new THREE.MeshStandardMaterial({ transparent: true, side: THREE.DoubleSide }) 79 + ); 80 + mesh.morphTargetInfluences = [1]; 81 + 82 + let material = new THREE.MeshStandardMaterial({ 83 + transparent: true, 84 + side: THREE.DoubleSide, 85 + map: texture, 86 + alphaTest: 0.9 87 + }); 88 + 89 + instancedMesh = new THREE.InstancedMesh(geometry, material, numButterflies); 90 + const color = new THREE.Color(); 91 + // set random positions 92 + const edge = 20; 93 + let start = new THREE.Vector3(1, 0, 0); 94 + for (let i = 0; i < numButterflies; i++) { 95 + let left = Math.random() < 0.5; 96 + let position = new THREE.Vector3().randomDirection(); 97 + position.y = 0; 98 + position.setLength(edge); 99 + position.y = Math.random() * 10 - 5; 100 + 101 + position.x += Math.random() * 2 - 1; 102 + position.z += Math.random() * 2 - 1; 103 + 104 + let scale = 0.1 + Math.random() * 0.3; 105 + 106 + mesh.scale.set(scale, scale, scale); 107 + 108 + mesh.updateMatrix(); 109 + 110 + instancedMesh.setMatrixAt(i, mesh.matrix); 111 + 112 + color.setHSL(Math.random(), 1, 0.5); 113 + 114 + let direction = position.clone(); 115 + direction.y = 0; 116 + direction.normalize(); 117 + direction.multiplyScalar(-1); 118 + 119 + let butterfly = new Butterfly(color, position, direction); 120 + butterflies.push(butterfly); 121 + butterfly.update(0); 122 + 123 + mesh.morphTargetInfluences = [butterfly.morph]; 124 + instancedMesh.setMorphAt(i, mesh); 125 + 126 + instancedMesh.setColorAt(i, butterfly.color); 127 + } 128 + instancedMesh.count = count; 129 + 130 + if (instancedMesh.morphTexture) { 131 + instancedMesh.morphTexture.needsUpdate = true; 132 + } 133 + } 134 + 135 + $: if (count && instancedMesh) { 136 + instancedMesh.count = Math.min(count, numButterflies); 137 + } 138 + 139 + onMount(async () => { 140 + const loader = new THREE.TextureLoader(); 141 + 142 + loader.load('/bluesky-visualizers/planet/butterfly.png', (texture) => { 143 + console.log('mask loaded', texture); 144 + createButterfly(texture); 145 + }); 146 + }); 147 + 148 + useTask((delta) => { 149 + if (!instancedMesh) return; 150 + 151 + let quaternion = new THREE.Quaternion(); 152 + let initialUp = new THREE.Vector3(0, 0, -1); 153 + for (let i = 0; i < instancedMesh.count; i++) { 154 + butterflies[i].update(delta); 155 + 156 + mesh.morphTargetInfluences = [butterflies[i].morph]; 157 + 158 + quaternion.setFromUnitVectors(initialUp, butterflies[i].direction); 159 + 160 + mesh.quaternion.copy(quaternion); 161 + mesh.position.copy(butterflies[i].position); 162 + mesh.updateMatrix(); 163 + 164 + instancedMesh.setMorphAt(i, mesh); 165 + instancedMesh.setMatrixAt(i, mesh.matrix); 166 + } 167 + 168 + if (instancedMesh.morphTexture) { 169 + instancedMesh.morphTexture.needsUpdate = true; 170 + } 171 + instancedMesh.instanceMatrix.needsUpdate = true; 172 + }); 173 + </script> 174 + 175 + <T is={instancedMesh} />
+39
src/routes/butterflies/CustomRenderer.svelte
··· 1 + <script lang="ts"> 2 + import { useTask, useThrelte } from '@threlte/core'; 3 + import { EffectComposer, EffectPass, RenderPass, DepthOfFieldEffect, BloomEffect } from 'postprocessing'; 4 + import { onMount } from 'svelte'; 5 + 6 + const { scene, renderer, camera, size, autoRender, renderStage } = useThrelte(); 7 + 8 + const composer = new EffectComposer(renderer); 9 + const setupEffectComposer = (camera: THREE.Camera) => { 10 + composer.removeAllPasses(); 11 + composer.addPass(new RenderPass(scene, camera)); 12 + 13 + const bloomEffect = new BloomEffect({ 14 + kernelSize: 3, 15 + intensity: 2, 16 + luminanceThreshold: 0.001 17 + }); 18 + 19 + // composer.addPass(new EffectPass(camera, dofEffect)); 20 + composer.addPass(new EffectPass(camera, bloomEffect)); 21 + }; 22 + 23 + $: setupEffectComposer($camera); 24 + $: composer.setSize($size.width, $size.height); 25 + 26 + onMount(() => { 27 + let before = autoRender.current; 28 + autoRender.set(false); 29 + return () => { 30 + autoRender.set(before); 31 + }; 32 + }); 33 + useTask( 34 + (delta) => { 35 + composer.render(delta); 36 + }, 37 + { stage: renderStage, autoInvalidate: false } 38 + ); 39 + </script>
+65
src/routes/butterflies/Scene.svelte
··· 1 + <script> 2 + import { T, useThrelte } from '@threlte/core'; 3 + import { OrbitControls } from '@threlte/extras'; 4 + import Butterfly from './Butterfly.svelte'; 5 + import { onMount } from 'svelte'; 6 + import CustomRenderer from './CustomRenderer.svelte'; 7 + import * as THREE from 'three'; 8 + 9 + let userCount = 10; 10 + 11 + let { scene } = useThrelte(); 12 + 13 + onMount(() => { 14 + scene.fog = new THREE.Fog(0, 10, 20); 15 + 16 + const url = 17 + 'wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.actor.profile'; 18 + 19 + // WebSocket logic 20 + const ws = new WebSocket(url); 21 + ws.onopen = () => { 22 + console.log('Connected to BlueSky WebSocket'); 23 + }; 24 + 25 + ws.onmessage = (event) => { 26 + const json = JSON.parse(event.data); 27 + 28 + if (json.kind !== 'commit') return; 29 + 30 + if ( 31 + json.commit.collection === 'app.bsky.actor.profile' && 32 + json.commit.operation === 'create' 33 + ) { 34 + userCount++; 35 + } 36 + }; 37 + 38 + return () => { 39 + ws.close(); 40 + }; 41 + }); 42 + </script> 43 + 44 + <T.PerspectiveCamera makeDefault position={[1, 0, 0]} far={100}> 45 + <OrbitControls 46 + enableZoom={false} 47 + enablePan={false}></OrbitControls> 48 + <!-- <OrbitControls 49 + autoRotate 50 + autoRotateSpeed={0.2} 51 + target={[0, 21, 0]} 52 + enableZoom={false} 53 + enablePan={false} 54 + ></OrbitControls> --> 55 + </T.PerspectiveCamera> 56 + 57 + <Butterfly count={userCount} /> 58 + 59 + <T.DirectionalLight position={[0, 0, -10]} intensity={2} /> 60 + 61 + <T.AmbientLight intensity={1} /> 62 + 63 + <!-- <Environment files={"/bluesky-visualizers/planet/sky2.hdr"} isBackground /> --> 64 + 65 + <CustomRenderer />
+85
src/routes/butterflies/butterfly.ts
··· 1 + import * as THREE from 'three'; 2 + 3 + import UberNoise from 'uber-noise'; 4 + 5 + export class Butterfly { 6 + color: THREE.Color; 7 + position: THREE.Vector3; 8 + direction: THREE.Vector3; 9 + 10 + morphTime: number = 0; 11 + morph: number = 0; 12 + height: number = 0; 13 + 14 + speed: number = 1; 15 + 16 + scale: number; 17 + 18 + rotation: number = 0; 19 + 20 + static readonly defaultDirection = new THREE.Vector3(0, 0, 1); 21 + 22 + noise: UberNoise; 23 + 24 + constructor(color: THREE.Color, position: THREE.Vector3, direction: THREE.Vector3) { 25 + this.color = color; 26 + this.position = position; 27 + 28 + this.morphTime = Math.random(); 29 + this.direction = direction; 30 + 31 + this.scale = Math.random() * 0.4 + 0.4; 32 + this.speed = Math.random() + 1; 33 + this.height = Math.random(); 34 + 35 + this.rotation = -Butterfly.defaultDirection.angleTo(direction); 36 + 37 + this.noise = new UberNoise({ min: -0.04, max: 0.04, scale: 1 }); 38 + } 39 + 40 + update(deltaTime: number) { 41 + this.rotation = Butterfly.defaultDirection.angleTo(this.direction); 42 + 43 + this.morphTime += deltaTime; 44 + 45 + this.morph = Math.sin(this.morphTime * 10) * 0.5 + 0.5; 46 + 47 + this.position.x += this.direction.x * deltaTime * this.speed; 48 + this.position.y += 49 + this.direction.y * deltaTime * this.speed + 50 + Math.sin(this.morphTime * 5) * 0.02 * this.speed * 0.5; 51 + this.position.z += this.direction.z * deltaTime * this.speed; 52 + 53 + const length = this.position.length(); 54 + 55 + if (length > 0.3) { 56 + this.direction.x += 57 + this.noise.get(this.position.x, this.position.y, this.position.z) * 58 + Math.max(0, 1 - length / 30); 59 + this.direction.z += 60 + this.noise.get(this.position.x, this.position.y, this.position.z) * 61 + Math.max(0, 1 - length / 30); 62 + } 63 + 64 + this.direction.normalize(); 65 + 66 + if (length > 30) { 67 + this.reset(); 68 + } 69 + } 70 + 71 + reset() { 72 + this.position = new THREE.Vector3().randomDirection(); 73 + this.position.y = 0; 74 + this.position.setLength(20); 75 + this.position.y = Math.random() * 20 - 10; 76 + 77 + this.position.x += Math.random() * 2 - 1; 78 + this.position.z += Math.random() * 2 - 1; 79 + 80 + this.direction = this.position.clone(); 81 + this.direction.y = 0; 82 + this.direction.normalize(); 83 + this.direction.multiplyScalar(-1); 84 + } 85 + }