/ js / particle.js
particle.js
  1  "use strict";
  2  
  3  import { CONFIG } from './config.js';
  4  
  5  export class Particle {
  6  	constructor(x, y, centerX, centerY) {
  7  		this.x = x;
  8  		this.y = y;
  9  		this.ox = x;
 10  		this.oy = y;
 11  		this.vx = 0;
 12  		this.vy = 0;
 13  
 14  		const dx = x - centerX;
 15  		const dy = y - centerY;
 16  		this.distFromCenter = Math.sqrt(dx * dx + dy * dy);
 17  		this.angle = Math.atan2(dy, dx);
 18  
 19  		this.active = false;
 20  		this.alpha = 0; // Fade alpha for smooth transitions
 21  		this.growthOffset = Math.random() * 0.15;
 22  
 23  		// Organic behavior traits
 24  		this.nervousness = 0.8 + Math.random() * 0.4;
 25  		this.awareness = Math.random() * 40 + 25;
 26  	}
 27  
 28  	update(state) {
 29          const {
 30              renderCursorX, renderCursorY, radiusMultiplier, spiralStrength, outbreaks,
 31              anomalies, isMobile, activeParticleRatio, stations,
 32              buffRadiusMultiplier, buffForceMultiplier, hasPulsatingBuff, pulseStrength,
 33              cursorSpeed, cursorDirX, cursorDirY, now
 34          } = state;
 35  
 36  		if (!this.active) return;
 37  
 38  		const dx = renderCursorX - this.x;
 39  		const dy = renderCursorY - this.y;
 40  		const distSq = dx * dx + dy * dy;
 41  
 42  		const dynamicRadiusSq = CONFIG.REPULSION_RADIUS ** 2 * radiusMultiplier ** 2 * buffRadiusMultiplier ** 2;
 43  
 44  		// Early exit: if particle is far from cursor and all entities, skip expensive calculations
 45  		const maxInteractionRange = 200;
 46  		const maxInteractionRangeSq = maxInteractionRange * maxInteractionRange;
 47  
 48  		let nearSomething = distSq < dynamicRadiusSq * 4;
 49  
 50  		if (!nearSomething && outbreaks.length > 0) {
 51  			for (const o of outbreaks) {
 52  				const odx = this.x - o.x;
 53  				const ody = this.y - o.y;
 54  				if (odx * odx + ody * ody < maxInteractionRangeSq) {
 55  					nearSomething = true;
 56  					break;
 57  				}
 58  			}
 59  		}
 60  
 61  		if (!nearSomething && anomalies.length > 0) {
 62  			for (const a of anomalies) {
 63  				const adx = this.x - a.x;
 64  				const ady = this.y - a.y;
 65  				if (adx * adx + ady * ady < maxInteractionRangeSq) {
 66  					nearSomething = true;
 67  					break;
 68  				}
 69  			}
 70  		}
 71  
 72  		if (!nearSomething) {
 73  			// Far from everything: just apply drag and spring back
 74  			this.vx *= CONFIG.PARTICLE_DRAG;
 75  			this.vy *= CONFIG.PARTICLE_DRAG;
 76  			this.x += this.vx + (this.ox - this.x) * CONFIG.PARTICLE_EASE;
 77  			this.y += this.vy + (this.oy - this.y) * CONFIG.PARTICLE_EASE;
 78  			return;
 79  		}
 80  
 81  		// Anticipation: only calculate if cursor is moving fast enough
 82  		if (cursorSpeed > 1.5) {
 83  			const dynamicRadius = Math.sqrt(dynamicRadiusSq);
 84  			const awarenessRadiusSq = (this.awareness + dynamicRadius) ** 2;
 85  
 86  			if (distSq < awarenessRadiusSq && distSq > dynamicRadiusSq) {
 87  				const distSqrt = Math.sqrt(distSq);
 88  				const toParticleX = -dx / distSqrt;
 89  				const toParticleY = -dy / distSqrt;
 90  				const alignmentDot = cursorDirX * toParticleX + cursorDirY * toParticleY;
 91  
 92  				if (alignmentDot > 0.4) {
 93  					const anticipationForce = alignmentDot * 0.2 * this.nervousness * buffForceMultiplier;
 94  					this.vx -= anticipationForce * cursorDirX;
 95  					this.vy -= anticipationForce * cursorDirY;
 96  				}
 97  			}
 98  		}
 99  
100  		// Main repulsion
101  		if (distSq < dynamicRadiusSq) {
102  			const safeDist = Math.max(distSq, 1);
103  			const baseForce = (-dynamicRadiusSq / safeDist) * CONFIG.REPULSION_STRENGTH * buffForceMultiplier;
104  			const force = baseForce * this.nervousness;
105  
106  			// Use normalized vector instead of angle + trig
107  			const distSqrt = Math.sqrt(distSq);
108  			const normX = dx / distSqrt;
109  			const normY = dy / distSqrt;
110  
111  			this.vx += force * normX;
112  			this.vy += force * normY;
113  
114  			// Spiral: perpendicular to normalized direction
115  			const dynamicRadius = Math.sqrt(dynamicRadiusSq);
116  			const spiralForce = (force * spiralStrength * distSqrt) / dynamicRadius;
117  			this.vx -= spiralForce * normY; // Perpendicular
118  			this.vy += spiralForce * normX;
119  		}
120  
121  		if (hasPulsatingBuff) {
122  			const particleSpacing = isMobile ? CONFIG.SPACING_MOBILE : CONFIG.SPACING_DESKTOP;
123  			const waveDistance = particleSpacing * 4;
124  			const waveRadius = Math.sqrt(dynamicRadiusSq) + waveDistance;
125  			const waveRadiusSq = waveRadius * waveRadius;
126  			const waveThickness = particleSpacing * 6;
127  
128  			// Use squared distance for initial check
129  			const distFromWaveSq = Math.abs(distSq - waveRadiusSq);
130  			const waveThicknessSq = waveThickness * waveThickness;
131  
132  			if (distFromWaveSq < waveThicknessSq) {
133  				const distSqrt = Math.sqrt(distSq);
134  				const normX = dx / distSqrt;
135  				const normY = dy / distSqrt;
136  				const normalizedPulse = (pulseStrength) * 2 - 1;
137  				const proximityFactor = 1.0 - (Math.sqrt(distFromWaveSq) / waveThickness);
138  				const waveForce = normalizedPulse * proximityFactor * 8.0;
139  				this.vx += waveForce * normX;
140  				this.vy += waveForce * normY;
141  			}
142  		}
143  
144  		for (const o of outbreaks) {
145  			const odx = this.x - o.x;
146  			const ody = this.y - o.y;
147  			const oDistSq = odx * odx + ody * ody;
148  
149  			// Simplified organic variation (removed one trig call)
150  			const morphPulse = Math.sin(o.frame * 0.05) * 0.05 + 1.0;
151  			const angleApprox = Math.atan2(ody, odx); // Only one atan2
152  			const tentacleVariation = Math.sin(angleApprox * 3 + o.frame * 0.03) * 0.08;
153  			const organicRadius = o.radius * morphPulse * (1.0 + tentacleVariation);
154  			const currentRadiusSq = organicRadius ** 2;
155  
156  			if (o.frame < 360) {
157  				const edgeThickness = 18;
158  				const innerRadiusSq = Math.max(0, organicRadius - edgeThickness) ** 2;
159  				if (oDistSq < currentRadiusSq && oDistSq > innerRadiusSq) {
160  					const oDist = Math.sqrt(oDistSq);
161  					const edgePos = (oDist - (organicRadius - edgeThickness)) / edgeThickness;
162  					const force = (1.0 - edgePos) * 2.0;
163  					// Use normalized vector
164  					const normX = odx / oDist;
165  					const normY = ody / oDist;
166  					this.vx += force * normX;
167  					this.vy += force * normY;
168  				}
169  			} else {
170  				const pullAge = o.frame - 360;
171  				const initialProgress = Math.min(pullAge / 1800, 1.0);
172  				const continuousGrowth = Math.min(pullAge / 5400, 1.2);
173  				const baseMultiplier = CONFIG.OUTBREAK_PULL_RADIUS_MIN + (CONFIG.OUTBREAK_PULL_RADIUS_MAX - CONFIG.OUTBREAK_PULL_RADIUS_MIN) * initialProgress;
174  				const pullRadiusMultiplier = baseMultiplier + continuousGrowth * 1.67;
175  				const pullRadius = organicRadius * pullRadiusMultiplier;
176  				const pullRadiusSq = pullRadius ** 2;
177  
178  				if (oDistSq < pullRadiusSq) {
179  					const oDist = Math.sqrt(oDistSq);
180  					let pullStrength = Math.min(pullAge / 1200, 1.0) * 1.2;
181  					pullStrength *= 1.0 + continuousGrowth * 0.3;
182  					pullStrength *= 1.0 + (1.0 - activeParticleRatio) * 0.8;
183  					const distanceFactor = 1.0 - Math.min(oDist / pullRadius, 1.0);
184  					const blackHoleForce = pullStrength * (1.0 + distanceFactor * 1.4);
185  					// Use normalized vector
186  					const normX = odx / oDist;
187  					const normY = ody / oDist;
188  					this.vx -= blackHoleForce * normX;
189  					this.vy -= blackHoleForce * normY;
190  				}
191  			}
192  		}
193  
194          // "Asteroid" Station Logic
195  		for (const s of stations) {
196              if (s.isCapturing && s.captureTarget) {
197                  const tdx = s.captureTarget.x - this.x;
198                  const tdy = s.captureTarget.y - this.y;
199                  if (tdx * tdx + tdy * tdy < 150 * 150) {
200                      this.vx += tdx * 0.03;
201                      this.vy += tdy * 0.03;
202                  }
203              } else {
204                  const dx = this.x - s.x;
205                  const dy = this.y - s.y;
206                  const stationDistSq = dx * dx + dy * dy;
207                  const radius = 25 + Math.sin(this.angle * 5) * 5; // Irregular shape
208                  const radiusSq = radius * radius;
209  
210                  if (stationDistSq < radiusSq) {
211                      this.vx += (s.x - this.x) * 0.1;
212                      this.vy += (s.y - this.y) * 0.1;
213                  }
214              }
215          }
216  
217  		        for (const a of anomalies) {
218  					const adx = this.x - a.x;
219  					const ady = this.y - a.y;
220  					const aDistSq = adx * adx + ady * ady;
221  					const vortexRadius = a.vortexRadius || CONFIG.ANOMALY_VORTEX_RADIUS;
222  					const vortexRadiusSq = vortexRadius * vortexRadius;
223  
224  					if (aDistSq < vortexRadiusSq) {
225  						const aDist = Math.sqrt(aDistSq);
226  						const falloff = 1.0 - aDist / vortexRadius;
227  						let vortexStrength = (a.vortexStrength || CONFIG.ANOMALY_VORTEX_STRENGTH) * falloff;
228  
229  						if (a.vortexBoostEndTime && now < a.vortexBoostEndTime) {
230  							vortexStrength *= 1.5;
231  						}
232  
233  						// Optimize synergy check: use squared distance to avoid sqrt
234  						const synergyRangeSq = CONFIG.SYNERGY_RANGE * CONFIG.SYNERGY_RANGE;
235  						for (const v of outbreaks) {
236  							const vdx = v.x - a.x;
237  							const vdy = v.y - a.y;
238  							if (vdx * vdx + vdy * vdy < synergyRangeSq) {
239  								vortexStrength *= v.maxed ? CONFIG.SYNERGY_VORTEX_BOOST * 1.4 : CONFIG.SYNERGY_VORTEX_BOOST;
240  								break;
241  							}
242  						}
243  
244  						// Use normalized vector instead of angle + trig
245  						const normX = adx / aDist;
246  						const normY = ady / aDist;
247  						this.vx -= (vortexStrength * 0.3) * normX;
248  						this.vy -= (vortexStrength * 0.3) * normY;
249  
250  						// Perpendicular (tangential) force
251  						this.vx -= (vortexStrength * 1.2) * normY; // Perpendicular
252  						this.vy += (vortexStrength * 1.2) * normX;
253  					}
254  				}
255  		this.vx *= CONFIG.PARTICLE_DRAG;
256  		this.vy *= CONFIG.PARTICLE_DRAG;
257  		this.x += this.vx + (this.ox - this.x) * CONFIG.PARTICLE_EASE;
258  		this.y += this.vy + (this.oy - this.y) * CONFIG.PARTICLE_EASE;
259  	}
260  }