import * as THREE from 'three' type StarfieldOptions = { count: number halfWidth: number halfHeight: number } const NEAR_DEPTH = -2 const FAR_DEPTH = -40 const palette = [0xffffff, 0xbae6fd, 0xf5d0fe, 0xfde68a].map( (hex) => new THREE.Color(hex), ) export class Starfield { readonly points: THREE.Points private readonly positions: Float32Array private readonly speeds: Float32Array private readonly halfWidth: number private readonly minY: number private readonly maxY: number constructor({ count, halfWidth, halfHeight }: StarfieldOptions) { // Deeper stars are visible across a much wider area than the play field. this.halfWidth = halfWidth * 3 this.minY = -halfHeight * 2.2 this.maxY = halfHeight * 2.6 this.positions = new Float32Array(count * 3) this.speeds = new Float32Array(count) const colors = new Float32Array(count * 3) for (let i = 0; i < count; i++) { const depth = THREE.MathUtils.randFloat(FAR_DEPTH, NEAR_DEPTH) const nearness = (depth - FAR_DEPTH) / (NEAR_DEPTH - FAR_DEPTH) this.positions[i * 3] = THREE.MathUtils.randFloatSpread(this.halfWidth * 2) this.positions[i * 3 + 1] = THREE.MathUtils.randFloat(this.minY, this.maxY) this.positions[i * 3 + 2] = depth this.speeds[i] = 2 + nearness * 14 const color = palette[Math.floor(Math.random() * palette.length)]! const brightness = 0.4 + nearness * 0.6 colors[i * 3] = color.r * brightness colors[i * 3 + 1] = color.g * brightness colors[i * 3 + 2] = color.b * brightness } const geometry = new THREE.BufferGeometry() geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3)) geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) this.points = new THREE.Points( geometry, new THREE.PointsMaterial({ size: 0.24, vertexColors: true, transparent: true, depthWrite: false, }), ) } update(dt: number) { const range = this.maxY - this.minY for (let i = 0; i < this.speeds.length; i++) { const yIndex = i * 3 + 1 this.positions[yIndex]! -= this.speeds[i]! * dt if (this.positions[yIndex]! < this.minY) { this.positions[yIndex]! += range this.positions[i * 3] = THREE.MathUtils.randFloatSpread(this.halfWidth * 2) } } this.points.geometry.attributes.position!.needsUpdate = true } }