/** * ParticleField — twinkling stars overhead, fade in as difficulty rises */ import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, Points, PointsMaterial, } from 'three'; function starTexture() { const c = document.createElement('canvas'); c.width = c.height = 64; const ctx = c.getContext('2d'); const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32); g.addColorStop(0, 'rgba(255,255,255,1)'); g.addColorStop(0.35, 'rgba(255,255,255,0.85)'); g.addColorStop(0.7, 'rgba(255,255,255,0.15)'); g.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(32, 32, 32, 0, Math.PI * 2); ctx.fill(); const t = new CanvasTexture(c); t.needsUpdate = true; return t; } export class ParticleField { constructor(scene) { this.scene = scene; const N = 700; const geom = new BufferGeometry(); const pos = new Float32Array(N * 3); for (let i = 0; i < N; i++) { pos[i * 3] = (Math.random() * 2 - 1) * 90; pos[i * 3 + 1] = 12 + Math.random() * 45; pos[i * 3 + 2] = -20 - Math.random() * 55; } geom.setAttribute('position', new BufferAttribute(pos, 3)); this.material = new PointsMaterial({ size: 0.6, color: 0xffffff, map: starTexture(), transparent: true, opacity: 0, blending: AdditiveBlending, depthWrite: false, }); this.points = new Points(geom, this.material); this.scene.add(this.points); this.t = 0; } applyPalette(p) { this.targetOpacity = p.starOpacity; } update(dt) { this.t += dt; this.points.rotation.y += 0.005 * dt; if (this.targetOpacity !== undefined) { this.material.opacity += (this.targetOpacity - this.material.opacity) * 0.04; } // gentle twinkle this.material.size = 0.55 + Math.sin(this.t * 2) * 0.07; } }