Skip to content

src/starfield.ts

75 lines · 2.4 KB · TypeScript
Raw
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
  }
}