Created
March 22, 2025 08:44
-
-
Save lukevanin/f7bf87ee42c97964e74c4df42f8c709b to your computer and use it in GitHub Desktop.
Vibe coded particle system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as THREE from 'three'; | |
import { World } from './world'; | |
/** | |
* Particle configuration class | |
*/ | |
export class ParticleConfig { | |
name: string; | |
textures: string[]; | |
lifetime: number; | |
birthrate: number; | |
size?: number[]; | |
color?: number[]; | |
opacity?: number[]; | |
velocity?: number[]; | |
spread?: number; | |
worldSpace?: boolean; | |
animateOpacity?: boolean; | |
animateScale?: boolean; | |
animatePosition?: boolean; | |
numberOfParticles?: number; | |
planeSize?: number[]; | |
blendMode?: string; | |
constructor(data: any) { | |
this.name = data.name || 'default'; | |
this.textures = data.textures || []; | |
this.lifetime = data.lifetime || 1.0; | |
this.birthrate = data.birthrate || 10; | |
this.size = data.size || [0.1, 0.1]; | |
this.color = data.color || [255, 255, 255]; | |
this.opacity = data.opacity || [1.0, 0.0]; | |
this.velocity = data.velocity || [0.1, 0.1]; | |
this.spread = data.spread || 30; | |
this.worldSpace = data.worldSpace || false; | |
this.animateOpacity = data.animateOpacity !== undefined ? data.animateOpacity : true; | |
this.animateScale = data.animateScale !== undefined ? data.animateScale : true; | |
this.animatePosition = data.animatePosition !== undefined ? data.animatePosition : true; | |
this.numberOfParticles = data.numberOfParticles || 100; // Default max particle count | |
this.planeSize = data.planeSize || [0.5, 0.5]; // Default plane size (width, height) | |
this.blendMode = data.blendMode || 'normal'; // Default blend mode | |
} | |
} | |
/** | |
* Particle class representing a single particle instance | |
*/ | |
export class Particle { | |
private mesh: THREE.Mesh; | |
private startTime: number; | |
private lifetime: number; | |
private initialSize: number; | |
private finalSize: number; | |
private initialOpacity: number; | |
private finalOpacity: number; | |
private velocity: THREE.Vector3; | |
private animateOpacity: boolean; | |
private animateScale: boolean; | |
private animatePosition: boolean; | |
private textureLoader: THREE.TextureLoader; | |
constructor( | |
position: THREE.Vector3, | |
lifetime: number, | |
size: number[], | |
color: number[], | |
opacity: number[], | |
velocity: THREE.Vector3, | |
options?: { | |
animateOpacity?: boolean; | |
animateScale?: boolean; | |
animatePosition?: boolean; | |
texturePath?: string; | |
planeSize?: number[]; | |
blendMode?: string; | |
} | |
) { | |
this.textureLoader = new THREE.TextureLoader(); | |
// Default options | |
const defaultOptions = options || {}; | |
const planeSize = defaultOptions.planeSize || [0.5, 0.5]; | |
// Create a plane for the particle | |
const geometry = new THREE.PlaneGeometry(planeSize[0], planeSize[1]); | |
// Create material - if texture path is provided, use it; otherwise use color | |
let material: THREE.Material; | |
if (defaultOptions.texturePath) { | |
// Create textured material | |
material = new THREE.MeshBasicMaterial({ | |
map: this.textureLoader.load(defaultOptions.texturePath), | |
transparent: true, | |
opacity: opacity[0], | |
depthWrite: false, | |
side: THREE.DoubleSide | |
}); | |
} else { | |
// Create colored material | |
material = new THREE.MeshBasicMaterial({ | |
color: new THREE.Color( | |
color[0] / 255, | |
color[1] / 255, | |
color[2] / 255 | |
), | |
transparent: true, | |
opacity: opacity[0], | |
depthWrite: false, | |
side: THREE.DoubleSide | |
}); | |
} | |
// Apply blend mode if specified | |
const blendMode = defaultOptions.blendMode || 'normal'; | |
this.applyBlendMode(material as THREE.MeshBasicMaterial, blendMode); | |
this.mesh = new THREE.Mesh(geometry, material); | |
this.mesh.position.copy(position); | |
this.mesh.scale.set(size[0], size[0], size[0]); // Initial size | |
// Make the particle face the camera | |
// this.mesh.rotation.set(-Math.PI / 2, 0, 0); // Initially face up (will be updated to face camera) | |
this.startTime = performance.now() / 1000; // Current time in seconds | |
this.lifetime = lifetime; | |
this.initialSize = size[0]; | |
this.finalSize = size[1]; | |
this.initialOpacity = opacity[0]; | |
this.finalOpacity = opacity[1]; | |
this.velocity = velocity.clone(); | |
// Set animation flags with defaults | |
this.animateOpacity = defaultOptions.animateOpacity !== undefined ? defaultOptions.animateOpacity : true; | |
this.animateScale = defaultOptions.animateScale !== undefined ? defaultOptions.animateScale : true; | |
this.animatePosition = defaultOptions.animatePosition !== undefined ? defaultOptions.animatePosition : true; | |
} | |
/** | |
* Configure an existing particle with new parameters | |
* @param position New position for the particle | |
* @param lifetime New lifetime | |
* @param size New size range [initial, final] | |
* @param color New color | |
* @param opacity New opacity range [initial, final] | |
* @param velocity New velocity | |
* @param options Animation options | |
*/ | |
public configure( | |
position: THREE.Vector3, | |
lifetime: number, | |
size: number[], | |
color: number[], | |
opacity: number[], | |
velocity: THREE.Vector3, | |
options?: { | |
animateOpacity?: boolean; | |
animateScale?: boolean; | |
animatePosition?: boolean; | |
texturePath?: string; | |
planeSize?: number[]; | |
blendMode?: string; | |
} | |
): void { | |
// Update mesh position | |
this.mesh.position.copy(position); | |
// Handle texture or color update | |
const material = this.mesh.material as THREE.MeshBasicMaterial; | |
if (options?.texturePath) { | |
// Update texture if provided | |
material.map = this.textureLoader.load(options.texturePath); | |
} else { | |
// Update color if no texture | |
material.color.setRGB( | |
color[0] / 255, | |
color[1] / 255, | |
color[2] / 255 | |
); | |
} | |
// If plane size changed, update geometry | |
if (options?.planeSize && | |
((this.mesh.geometry as THREE.PlaneGeometry).parameters.width !== options.planeSize[0] || | |
(this.mesh.geometry as THREE.PlaneGeometry).parameters.height !== options.planeSize[1])) { | |
// Dispose old geometry | |
this.mesh.geometry.dispose(); | |
// Create new geometry with updated size | |
this.mesh.geometry = new THREE.PlaneGeometry(options.planeSize[0], options.planeSize[1]); | |
} | |
// Apply blend mode if specified | |
if (options?.blendMode) { | |
this.applyBlendMode(material, options.blendMode); | |
} | |
// Update material opacity | |
material.opacity = opacity[0]; | |
// Update mesh scale | |
this.mesh.scale.set(size[0], size[0], size[0]); | |
// Reset rotation to face up (will be updated to face camera) | |
this.mesh.rotation.set(-Math.PI / 2, 0, 0); | |
// Update particle properties | |
this.startTime = performance.now() / 1000; | |
this.lifetime = lifetime; | |
this.initialSize = size[0]; | |
this.finalSize = size[1]; | |
this.initialOpacity = opacity[0]; | |
this.finalOpacity = opacity[1]; | |
this.velocity = velocity.clone(); | |
// Update animation flags | |
if (options) { | |
this.animateOpacity = options.animateOpacity !== undefined ? options.animateOpacity : this.animateOpacity; | |
this.animateScale = options.animateScale !== undefined ? options.animateScale : this.animateScale; | |
this.animatePosition = options.animatePosition !== undefined ? options.animatePosition : this.animatePosition; | |
} | |
} | |
/** | |
* Get the mesh of this particle | |
*/ | |
getMesh(): THREE.Mesh { | |
return this.mesh; | |
} | |
/** | |
* Update the particle | |
* @param currentTime Current time in seconds | |
* @returns True if particle is still alive, false if it should be removed | |
*/ | |
update(currentTime: number): boolean { | |
const age = currentTime - this.startTime; | |
// Check if particle has exceeded its lifetime | |
if (age >= this.lifetime) { | |
return false; | |
} | |
// Calculate life progress (0 to 1) | |
const lifeProgress = age / this.lifetime; | |
// Update position based on velocity if animatePosition is true | |
if (this.animatePosition) { | |
this.mesh.position.add(this.velocity.clone().multiplyScalar(0.016)); // Assuming 60fps | |
} | |
// Update size (linear interpolation) if animateScale is true | |
if (this.animateScale) { | |
const currentSize = this.initialSize + (this.finalSize - this.initialSize) * lifeProgress; | |
this.mesh.scale.set(currentSize, currentSize, currentSize); | |
} | |
// Update opacity (linear interpolation) if animateOpacity is true | |
if (this.animateOpacity) { | |
const material = this.mesh.material as THREE.MeshBasicMaterial; | |
material.opacity = this.initialOpacity + (this.finalOpacity - this.initialOpacity) * lifeProgress; | |
} | |
// Always make the particle face the camera by using billboard technique | |
// This will be called in the emitter's update method | |
return true; | |
} | |
/** | |
* Make the particle face the camera (billboard technique) | |
* @param camera The camera to face | |
*/ | |
public faceCamera(camera: THREE.Camera): void { | |
// this.mesh.lookAt(camera.position); | |
const cameraPosition = camera.position.clone(); | |
// Project camera position onto the XZ plane (ignore Y for horizontal facing) | |
// const target = new THREE.Vector3(cameraPosition.x, this.mesh.position.y, cameraPosition.z); | |
// const target = new THREE.Vector3(this.mesh.position.x, cameraPosition.y, cameraPosition.z); | |
// Make the billboard look at the camera, but only rotate around Y-axis | |
this.mesh.lookAt(cameraPosition); | |
} | |
/** | |
* Dispose of the particle resources | |
*/ | |
dispose(): void { | |
if (this.mesh.parent) { | |
this.mesh.parent.remove(this.mesh); | |
} | |
(this.mesh.geometry as THREE.BufferGeometry).dispose(); | |
const material = this.mesh.material as THREE.MeshBasicMaterial; | |
if (material.map) { | |
material.map.dispose(); | |
} | |
material.dispose(); | |
} | |
/** | |
* Applies a blend mode to a material | |
* @param material The material to apply the blend mode to | |
* @param blendMode The blend mode to apply: 'normal', 'add', 'multiply', 'screen', etc. | |
*/ | |
private applyBlendMode(material: THREE.MeshBasicMaterial, blendMode: string): void { | |
// TODO: Only update the material if the blend mode has changed | |
// Set up blending mode | |
switch (blendMode.toLowerCase()) { | |
case 'add': | |
case 'additive': | |
material.blending = THREE.AdditiveBlending; | |
break; | |
case 'multiply': | |
material.blending = THREE.MultiplyBlending; | |
break; | |
case 'screen': | |
material.blending = THREE.CustomBlending; | |
material.blendSrc = THREE.OneFactor; | |
material.blendDst = THREE.OneMinusSrcColorFactor; | |
break; | |
case 'normal': | |
default: | |
material.blending = THREE.NormalBlending; | |
break; | |
} | |
} | |
} | |
/** | |
* Particle emitter class for creating visual effects | |
*/ | |
export class ParticleEmitter { | |
private attachmentTarget: THREE.Object3D; | |
private attachmentOffset: THREE.Vector3; | |
private debugMesh: THREE.Mesh; | |
private isAttached: boolean = false; | |
private isEmitting: boolean = false; | |
private config: ParticleConfig | null = null; | |
private activeParticles: Particle[] = []; | |
private inactiveParticles: Particle[] = []; | |
private lastEmitTime: number = 0; | |
private clock: THREE.Clock; | |
private world: World | null = null; | |
/** | |
* Create a particle emitter | |
* @param target THREE.js node where particles will be emitted | |
* @param offset Relative offset on the node where the emitter will be attached | |
* @param config Optional particle configuration | |
* @param world Reference to the world | |
*/ | |
constructor(target: THREE.Object3D, offset: THREE.Vector3, config?: ParticleConfig, world?: World | null) { | |
this.attachmentTarget = target; | |
this.attachmentOffset = offset.clone(); | |
this.config = config || null; | |
this.world = world || null; | |
this.clock = new THREE.Clock(); | |
// Create debug visualization (magenta sphere with radius 0.2) | |
const geometry = new THREE.SphereGeometry(0.05, 16, 16); | |
const material = new THREE.MeshBasicMaterial({ color: 0xff00ff }); | |
this.debugMesh = new THREE.Mesh(geometry, material); | |
// Attach to target | |
this.attach(); | |
// Initialize the particle pool if we have a config | |
if (this.config) { | |
this.initializeParticlePool(); | |
} | |
} | |
/** | |
* Set the particle configuration | |
* @param config Particle configuration to use | |
*/ | |
public setConfig(config: ParticleConfig): void { | |
this.config = config; | |
// Clear any existing particles | |
this.clearParticles(); | |
// Initialize the particle pool with the new config | |
this.initializeParticlePool(); | |
} | |
/** | |
* Initialize the particle pool with inactive particles | |
*/ | |
private initializeParticlePool(): void { | |
if (!this.config) return; | |
const maxParticles = this.config.numberOfParticles || 100; | |
// Clear existing pools | |
this.clearParticles(); | |
// Create particles and add them to the inactive pool | |
for (let i = 0; i < maxParticles; i++) { | |
const particle = this.createParticle(); | |
// Hide the particle initially | |
particle.getMesh().visible = false; | |
// Add to inactive pool | |
this.inactiveParticles.push(particle); | |
} | |
console.log(`Initialized particle pool with ${maxParticles} particles for emitter ${this.config.name}`); | |
} | |
/** | |
* Create a new particle without configuring it | |
* @returns A new particle instance | |
*/ | |
private createParticle(): Particle { | |
// Create with dummy values - will be configured when activated | |
const position = new THREE.Vector3(); | |
const lifetime = 1.0; | |
const size = [0.1, 0.1]; | |
const color = [255, 255, 255]; | |
const opacity = [1.0, 0.0]; | |
const velocity = new THREE.Vector3(); | |
// Get plane size from config if available | |
const planeSize = this.config?.planeSize || [0.5, 0.5]; | |
// Select a random texture if textures are available in config | |
let texturePath: string | undefined = undefined; | |
if (this.config?.textures && this.config.textures.length > 0) { | |
const randomIndex = Math.floor(Math.random() * this.config.textures.length); | |
texturePath = this.config.textures[randomIndex]; | |
} | |
// Get blend mode from config | |
const blendMode = this.config?.blendMode || 'normal'; | |
return new Particle( | |
position, | |
lifetime, | |
size, | |
color, | |
opacity, | |
velocity, | |
{ | |
planeSize: planeSize, | |
texturePath: texturePath, | |
blendMode: blendMode | |
} | |
); | |
} | |
/** | |
* Clear all particles from both active and inactive pools | |
*/ | |
private clearParticles(): void { | |
// Dispose active particles | |
for (const particle of this.activeParticles) { | |
particle.dispose(); | |
} | |
// Dispose inactive particles | |
for (const particle of this.inactiveParticles) { | |
particle.dispose(); | |
} | |
// Clear arrays | |
this.activeParticles = []; | |
this.inactiveParticles = []; | |
} | |
/** | |
* Get the current particle configuration | |
* @returns The current particle configuration or null if not set | |
*/ | |
public getConfig(): ParticleConfig | null { | |
return this.config; | |
} | |
/** | |
* Attach the emitter to its target | |
*/ | |
public attach(): void { | |
if (!this.isAttached && this.attachmentTarget) { | |
// this.attachmentTarget.add(this.debugMesh); | |
// this.debugMesh.position.copy(this.attachmentOffset); | |
this.isAttached = true; | |
} | |
} | |
/** | |
* Detach the emitter from its target | |
*/ | |
public detach(): void { | |
if (this.isAttached && this.attachmentTarget) { | |
this.attachmentTarget.remove(this.debugMesh); | |
this.isAttached = false; | |
} | |
} | |
/** | |
* Change the attachment target | |
* @param newTarget New THREE.js node to attach to | |
* @param newOffset Optional new offset position | |
*/ | |
public setTarget(newTarget: THREE.Object3D, newOffset?: THREE.Vector3): void { | |
// Detach from current target | |
this.detach(); | |
// Update target and offset | |
this.attachmentTarget = newTarget; | |
if (newOffset) { | |
this.attachmentOffset = newOffset.clone(); | |
} | |
// Attach to new target | |
this.attach(); | |
} | |
/** | |
* Start emitting particles | |
*/ | |
public start(): void { | |
this.isEmitting = true; | |
this.debugMesh.visible = true; | |
this.lastEmitTime = this.clock.getElapsedTime(); | |
} | |
/** | |
* Stop emitting particles | |
*/ | |
public stop(): void { | |
this.isEmitting = false; | |
this.debugMesh.visible = false; | |
} | |
/** | |
* Check if the emitter is currently emitting | |
* @returns True if emitting, false otherwise | |
*/ | |
public getIsEmitting(): boolean { | |
return this.isEmitting; | |
} | |
/** | |
* Update emitter position and emit particles | |
*/ | |
public update(): void { | |
// Skip if no config | |
if (!this.config) { | |
return; | |
} | |
// Update existing active particles | |
this.updateParticles(); | |
// Skip emission if not emitting | |
if (!this.isEmitting) { | |
return; | |
} | |
// Check if it's time to emit new particles | |
const currentTime = this.clock.getElapsedTime(); | |
const timeSinceLastEmit = currentTime - this.lastEmitTime; | |
const emitInterval = 1.0 / this.config.birthrate; | |
if (timeSinceLastEmit >= emitInterval) { | |
this.emitParticle(); | |
this.lastEmitTime = currentTime; | |
} | |
} | |
/** | |
* Emit a new particle from the pool | |
*/ | |
private emitParticle(): void { | |
if (!this.config || !this.isAttached || !this.attachmentTarget) { | |
return; | |
} | |
// Get a particle from the pool | |
let particle: Particle; | |
if (this.inactiveParticles.length > 0) { | |
// Take from inactive pool | |
particle = this.inactiveParticles.pop()!; | |
} else if (this.activeParticles.length > 0) { | |
// No inactive particles available, recycle the oldest active one | |
particle = this.activeParticles.shift()!; | |
// If it's in a scene, remove it first | |
const mesh = particle.getMesh(); | |
if (mesh.parent) { | |
mesh.parent.remove(mesh); | |
} | |
} else { | |
// This shouldn't happen with proper initialization | |
console.warn('No particles available in pool'); | |
return; | |
} | |
// Get world position of attachment point | |
const worldPosition = new THREE.Vector3(); | |
const targetWorldPosition = new THREE.Vector3(); | |
const targetWorldQuaternion = new THREE.Quaternion(); | |
// Get world position of attachment target | |
this.attachmentTarget.getWorldPosition(targetWorldPosition); | |
this.attachmentTarget.getWorldQuaternion(targetWorldQuaternion); | |
// Add offset in local space, then transform to world space | |
const offsetWorldMatrix = new THREE.Matrix4().makeTranslation( | |
this.attachmentOffset.x, | |
this.attachmentOffset.y, | |
this.attachmentOffset.z | |
); | |
// Create rotation matrix from quaternion | |
const rotationMatrix = new THREE.Matrix4().makeRotationFromQuaternion(targetWorldQuaternion); | |
// Apply rotation to offset matrix | |
const attachmentWorldMatrix = offsetWorldMatrix.multiply(rotationMatrix); | |
// Combine with the attachment target's matrix | |
const combinedMatrix = new THREE.Matrix4().copy(this.attachmentTarget.matrixWorld).multiply(attachmentWorldMatrix); | |
// Extract the position from the combined matrix | |
worldPosition.setFromMatrixPosition(combinedMatrix); | |
// Generate random size within configured range | |
const sizeRange = this.config.size || [0.1, 0.1]; | |
const size = sizeRange[0]; | |
// Get color from config | |
const color = this.config.color || [255, 255, 255]; | |
// Get opacity from config | |
const opacity = this.config.opacity || [1.0, 0.0]; | |
// Calculate random velocity based on config | |
const velocityRange = this.config.velocity || [0.1, 0.1]; | |
const speed = velocityRange[0] + Math.random() * (velocityRange[1] - velocityRange[0]); | |
// Calculate direction based on spread | |
const spread = this.config.spread || 0; | |
const angle = (Math.random() - 0.5) * spread * (Math.PI / 180); | |
// Calculate velocity direction (forward with random spread) | |
const forward = new THREE.Vector3(0, 0, -1); | |
forward.applyQuaternion(this.attachmentTarget.getWorldQuaternion(new THREE.Quaternion())); | |
// Apply spread angle | |
const spreadMatrix = new THREE.Matrix4().makeRotationY(angle); | |
forward.applyMatrix4(spreadMatrix); | |
// Create velocity vector | |
const velocity = forward.multiplyScalar(speed); | |
// Select a random texture if textures are available | |
let texturePath: string | undefined = undefined; | |
if (this.config.textures && this.config.textures.length > 0) { | |
const randomIndex = Math.floor(Math.random() * this.config.textures.length); | |
texturePath = this.config.textures[randomIndex]; | |
} | |
// Animation options | |
const animationOptions = { | |
animateOpacity: this.config.animateOpacity, | |
animateScale: this.config.animateScale, | |
animatePosition: this.config.animatePosition, | |
texturePath: texturePath, | |
planeSize: this.config.planeSize, | |
blendMode: this.config.blendMode | |
}; | |
// Configure the particle | |
particle.configure( | |
worldPosition, | |
this.config.lifetime, | |
[size, this.config.size ? this.config.size[1] : size], | |
color, | |
opacity, | |
velocity, | |
animationOptions | |
); | |
// Make particle visible | |
particle.getMesh().visible = true; | |
// Add particle to scene based on particle space configuration | |
if (this.config.worldSpace && this.world) { | |
// World space: Add to world scene | |
this.world.getScene().add(particle.getMesh()); | |
} else { | |
// Local space: Add to attachment target | |
this.attachmentTarget.add(particle.getMesh()); | |
// For local particles, reset to local position | |
particle.getMesh().position.copy(this.attachmentOffset); | |
} | |
// Store the particle in the active list | |
this.activeParticles.push(particle); | |
} | |
/** | |
* Update existing particles | |
*/ | |
private updateParticles(): void { | |
if (!this.world) return; | |
const currentTime = performance.now() / 1000; | |
const camera = this.world.getCamera(); | |
// Process particles in reverse to safely remove during iteration | |
for (let i = this.activeParticles.length - 1; i >= 0; i--) { | |
const particle = this.activeParticles[i]; | |
const isAlive = particle.update(currentTime); | |
// Make the particle face the camera (billboard technique) | |
if (camera) { | |
particle.faceCamera(camera); | |
} | |
if (!isAlive) { | |
// Remove from active list | |
this.activeParticles.splice(i, 1); | |
// Hide the particle | |
particle.getMesh().visible = false; | |
// Remove from scene if it has a parent | |
const mesh = particle.getMesh(); | |
if (mesh.parent) { | |
mesh.parent.remove(mesh); | |
} | |
// Add to inactive list | |
this.inactiveParticles.push(particle); | |
} | |
} | |
} | |
/** | |
* Set the world reference | |
* @param world Reference to the world | |
*/ | |
public setWorld(world: World | null): void { | |
this.world = world; | |
} | |
/** | |
* Clean up resources when emitter is no longer needed | |
*/ | |
public dispose(): void { | |
// Stop emitting | |
this.stop(); | |
// Detach from target | |
this.detach(); | |
// Clear all particles | |
this.clearParticles(); | |
// Dispose debug mesh | |
if (this.debugMesh.geometry) { | |
this.debugMesh.geometry.dispose(); | |
} | |
if (this.debugMesh.material) { | |
if (Array.isArray(this.debugMesh.material)) { | |
for (const material of this.debugMesh.material) { | |
material.dispose(); | |
} | |
} else { | |
this.debugMesh.material.dispose(); | |
} | |
} | |
} | |
} | |
/** | |
* System for managing particle emitters | |
*/ | |
export class ParticleSystem { | |
private emitters: ParticleEmitter[] = []; | |
private configs: Map<string, ParticleConfig> = new Map(); | |
private world: World | null = null; | |
constructor(world?: World | null) { | |
this.world = world || null; | |
} | |
/** | |
* Load particle configurations from JSON | |
* @param data JSON data with particle configurations | |
*/ | |
public loadConfigurations(data: any): void { | |
if (!data || !data.particles || !Array.isArray(data.particles)) { | |
console.error('Invalid particle configuration data'); | |
return; | |
} | |
// Clear existing configurations | |
this.configs.clear(); | |
// Load new configurations | |
for (const particleData of data.particles) { | |
if (particleData.name) { | |
const config = new ParticleConfig(particleData); | |
this.configs.set(config.name, config); | |
console.log(`Loaded particle configuration: ${config.name}`); | |
} | |
} | |
console.log(`Loaded ${this.configs.size} particle configurations`); | |
} | |
/** | |
* Get a particle configuration by name | |
* @param name Name of the configuration to get | |
* @returns The particle configuration or null if not found | |
*/ | |
public getConfig(name: string): ParticleConfig | null { | |
return this.configs.get(name) || null; | |
} | |
/** | |
* Create a particle emitter attached to a given THREE.js node | |
* @param target THREE.js node where particles will be emitted | |
* @param offset Relative offset on the node where the emitter will be attached | |
* @param configName Optional name of the particle configuration to use | |
* @returns The created emitter | |
*/ | |
public createEmitter(target: THREE.Object3D, offset: THREE.Vector3, configName?: string): ParticleEmitter { | |
let config: ParticleConfig | undefined = undefined; | |
if (configName) { | |
const foundConfig = this.getConfig(configName); | |
if (foundConfig) { | |
config = foundConfig; | |
} | |
} | |
const emitter = new ParticleEmitter(target, offset, config, this.world); | |
this.emitters.push(emitter); | |
return emitter; | |
} | |
/** | |
* Update all emitters | |
*/ | |
public update(): void { | |
for (const emitter of this.emitters) { | |
emitter.update(); | |
} | |
} | |
/** | |
* Set the world reference | |
* @param world Reference to the world | |
*/ | |
public setWorld(world: World): void { | |
this.world = world; | |
// Update world reference for all existing emitters | |
for (const emitter of this.emitters) { | |
emitter.setWorld(world); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment