import * as THREE from 'three' export interface SceneObjects { renderer: THREE.WebGLRenderer scene: THREE.Scene camera: THREE.PerspectiveCamera starsFar: THREE.Points starsMid: THREE.Points starsClose: THREE.Points dust: THREE.Points nebulaGroup: THREE.Group light1: THREE.PointLight light2: THREE.PointLight } function createStarLayer( count: number, spread: number, size: number, opacity: number ): THREE.Points { const geo = new THREE.BufferGeometry() const pos = new Float32Array(count * 3) const col = new Float32Array(count * 3) for (let i = 0; i < count; i++) { const i3 = i * 3 pos[i3] = (Math.random() - 0.5) * spread pos[i3 + 1] = (Math.random() - 0.5) * spread pos[i3 + 2] = (Math.random() - 0.5) * spread // Dark particles on white background const shade = Math.random() * 0.08 col[i3] = shade; col[i3+1] = shade; col[i3+2] = shade + 0.02 } geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)) geo.setAttribute('color', new THREE.BufferAttribute(col, 3)) const mat = new THREE.PointsMaterial({ size, vertexColors: true, transparent: true, opacity, sizeAttenuation: true, depthWrite: false, }) return new THREE.Points(geo, mat) } export function createScene(canvas: HTMLCanvasElement): SceneObjects { const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.setClearColor(0x000000, 0) // transparent, CSS handles bg const scene = new THREE.Scene() scene.fog = new THREE.FogExp2(0xf5f5f8, 0.00025) const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 5000) camera.position.set(0, 0, 600) // Stars (dark particles) const starsFar = createStarLayer(6000, 4000, 1.0, 0.3) const starsMid = createStarLayer(3000, 2500, 1.8, 0.45) const starsClose = createStarLayer(1000, 1500, 2.5, 0.6) scene.add(starsFar, starsMid, starsClose) // Dust (dark) const dustGeo = new THREE.BufferGeometry() const dustCount = 2000 const dustPos = new Float32Array(dustCount * 3) for (let i = 0; i < dustCount; i++) { dustPos[i*3] = (Math.random() - 0.5) * 3000 dustPos[i*3+1] = (Math.random() - 0.5) * 3000 dustPos[i*3+2] = (Math.random() - 0.5) * 3000 } dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3)) const dust = new THREE.Points(dustGeo, new THREE.PointsMaterial({ size: 0.5, color: 0x222233, transparent: true, opacity: 0.3, depthWrite: false, sizeAttenuation: true, })) scene.add(dust) const nebulaGroup = new THREE.Group() // Lights scene.add(new THREE.AmbientLight(0xffffff, 0.6)) const light1 = new THREE.PointLight(0xccddee, 1.5, 1200) light1.position.set(200, 200, 200) scene.add(light1) const light2 = new THREE.PointLight(0xbbbbdd, 1.0, 1000) light2.position.set(-300, -100, 100) scene.add(light2) return { renderer, scene, camera, starsFar, starsMid, starsClose, dust, nebulaGroup, light1, light2, } } export function updateScene(objs: SceneObjects, time: number, _dt: number, smoothX: number, smoothY: number) { const { camera, starsFar, starsMid, starsClose, dust, nebulaGroup, light1, light2, } = objs // Camera camera.position.x = smoothX * 150 camera.position.y = -smoothY * 100 camera.position.z = 600 + Math.sin(time * 0.08) * 30 camera.lookAt(smoothX * 30, -smoothY * 20, -150) // Star parallax starsFar.rotation.y = time * 0.008 + smoothX * 0.015 starsFar.rotation.x = time * 0.004 + smoothY * 0.015 starsMid.rotation.y = time * 0.015 + smoothX * 0.03 starsMid.rotation.x = time * 0.008 + smoothY * 0.03 starsClose.rotation.y = time * 0.025 + smoothX * 0.05 starsClose.rotation.x = time * 0.012 + smoothY * 0.05 // Dust dust.rotation.y = time * 0.005 + smoothX * 0.01 dust.rotation.x = time * 0.003 + smoothY * 0.01 // Lights light1.position.x = 200 + Math.sin(time * 0.25) * 120 light2.position.y = -100 + Math.cos(time * 0.3) * 100 }