3d / client /src /scene /Scene.tsx
hologramicon's picture
Upload 19 files
1b5663e verified
import React, { useEffect, useRef, useState } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default function Scene(): JSX.Element {
const mountRef = useRef<HTMLDivElement|null>(null)
const [ready,setReady] = useState(false)
useEffect(()=>{
const container = mountRef.current!
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000)
camera.position.set(0,0,20)
const renderer = new THREE.WebGLRenderer({ antialias:true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
container.appendChild(renderer.domElement)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
const ambient = new THREE.AmbientLight(0xffffff,0.7); scene.add(ambient)
const dir = new THREE.DirectionalLight(0xffffff,0.6); dir.position.set(5,10,7.5); scene.add(dir)
const group = new THREE.Group(); scene.add(group)
// comet particles system
const trails: {mesh:THREE.Mesh, life:number}[] = []
function addComet(word:string){
const geom = new THREE.SphereGeometry(0.12,8,8)
const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(`hsl(${Math.random()*360},80%,60%)`), emissive:0x111111 })
const m = new THREE.Mesh(geom, mat)
m.position.set((Math.random()-0.5)*30, (Math.random()-0.5)*20, (Math.random()-0.5)*30)
(m.userData as any).vel = new THREE.Vector3((Math.random()-0.5)*0.6, (Math.random()-0.5)*0.6, (Math.random()-0.5)*0.6)
(m.userData as any).word = word
group.add(m)
trails.push({mesh:m, life:120})
}
// text labels as sprites
const spriteMaterial = new THREE.SpriteMaterial({ color: 0xffffff })
function addLabel(text:string, pos:THREE.Vector3){
const canvas = document.createElement('canvas')
canvas.width = 256; canvas.height = 64
const ctx = canvas.getContext('2d')!
ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.font = '30px sans-serif'
ctx.fillText(text, 8,40)
const tex = new THREE.CanvasTexture(canvas)
const mat = new THREE.SpriteMaterial({ map:tex, transparent:true })
const sp = new THREE.Sprite(mat)
sp.scale.set(4,1,1); sp.position.copy(pos)
(sp.userData as any).isLabel = true
group.add(sp)
}
// generate initial galaxy nodes from demo algorithm
function spawnGalaxy(topic:string, origin:THREE.Vector3){
const mainCount = Math.max(3, Math.min(12, Math.floor(Math.random()*10)))
for(let i=0;i<mainCount;i++){
const theta = Math.random()*Math.PI*2
const r = 6 + Math.random()*6
const x = origin.x + Math.cos(theta)*r
const y = origin.y + (Math.random()-0.5)*4
const z = origin.z + Math.sin(theta)*r
const node = new THREE.Mesh(new THREE.SphereGeometry(0.25,10,10), new THREE.MeshStandardMaterial({ color: new THREE.Color(`hsl(${(i/mainCount)*360},80%,60%)`) }))
node.position.set(x,y,z)
group.add(node)
addLabel('n'+i, node.position.clone().add(new THREE.Vector3(0.6,0.6,0)))
// create comets passing near node
if(Math.random()<0.6){
addComet('⭐')
}
}
}
// add a root
const root = new THREE.Mesh(new THREE.SphereGeometry(0.5,16,16), new THREE.MeshStandardMaterial({ color: 0x66ffcc }))
root.position.set(0,0,0); group.add(root); addLabel('ROOT', root.position.clone().add(new THREE.Vector3(0,1,0)))
spawnGalaxy('example', new THREE.Vector3(0,0,0))
// comet update and trail rendering (simple)
const trailGroup = new THREE.Group(); scene.add(trailGroup)
function animate(){
requestAnimationFrame(animate)
controls.update()
// update comets
for(let i=trails.length-1;i>=0;i--){
const t = trails[i]
t.mesh.position.add((t.mesh.userData as any).vel)
t.life -= 1
// spawn small fading dots to simulate trail
const dot = new THREE.Mesh(new THREE.SphereGeometry(0.04,6,6), new THREE.MeshBasicMaterial({ color: (t.mesh.material as any).color, transparent:true, opacity:0.6 }))
dot.position.copy(t.mesh.position)
trailGroup.add(dot)
// fade dots
setTimeout(()=>{ try{ trailGroup.remove(dot); dot.geometry.dispose(); (dot.material as any).dispose() }catch(e){} }, 900)
if(t.life<=0){
try{ group.remove(t.mesh); t.mesh.geometry.dispose(); (t.mesh.material as any).dispose() }catch(e){}
trails.splice(i,1)
}
}
renderer.render(scene, camera)
}
animate()
const onResize = ()=>{ camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight) }
window.addEventListener('resize', onResize)
setReady(true)
return ()=>{
window.removeEventListener('resize', onResize)
try{ container.removeChild(renderer.domElement) }catch(e){}
}
},[])
return <div style={{width:'100vw',height:'100vh',position:'relative'}} ref={mountRef}>
<div style={{position:'absolute',left:20,top:20,zIndex:3,background:'rgba(10,10,10,0.6)',padding:12,borderRadius:8}}>
<input id="topic" placeholder="hashtag or topic" style={{width:300}} />
</div>
</div>
}