import { Entity, EntityNames } from "."; import { BoundingBox, Component, ComponentNames, Life, Renderable, } from "../components"; import { Coord2D, Dimension2D, DrawArgs } from "../interfaces"; import { colors } from "../utils"; import { normalRandom } from "../utils/random"; export interface ParticleSpawnOptions { spawnerDimensions: Dimension2D; center: Coord2D; spawnerShape: "circle" | "rectangle"; particleShape: "circle" | "rectangle"; particleCount: number; particleMeanLife: number; particleLifeVariance: number; particleMeanSize: number; particleSizeVariance: number; particleMeanSpeed: number; particleSpeedVariance: number; particleColors: Array; } const DEFAULT_PARTICLE_SPAWN_OPTIONS: ParticleSpawnOptions = { spawnerDimensions: { width: 0, height: 0 }, center: { x: 0, y: 0 }, spawnerShape: "circle", particleShape: "circle", particleCount: 50, particleMeanLife: 200, particleLifeVariance: 50, particleMeanSize: 12, particleSizeVariance: 1, particleMeanSpeed: 2, particleSpeedVariance: 1, particleColors: [colors.gray, colors.aqua, colors.lightAqua], }; interface Particle { position: Coord2D; velocity: Coord2D; dimension: Dimension2D; color: string; life: number; shape: "circle" | "rectangle"; } class ParticleRenderer extends Component implements Renderable { private particles: Array; private onDeath?: () => void; constructor(particles: Array = [], onDeath?: () => void) { super(ComponentNames.Sprite); this.particles = particles; this.onDeath = onDeath; } public update(dt: number) { this.particles = this.particles.filter((particle) => { particle.position.x += particle.velocity.x * dt; particle.position.y += particle.velocity.y * dt; particle.life -= dt; return particle.life > 0; }); if (this.particles.length === 0 && this.onDeath) { this.onDeath(); } } public draw(ctx: CanvasRenderingContext2D, _drawArgs: DrawArgs) { for (const particle of this.particles) { ctx.fillStyle = particle.color; if (particle.shape === "circle") { ctx.beginPath(); ctx.ellipse( particle.position.x, particle.position.y, particle.dimension.width / 2, particle.dimension.height / 2, 0, 0, Math.PI * 2, ); ctx.fill(); } else { ctx.fillRect( particle.position.x - particle.dimension.width / 2, particle.position.y - particle.dimension.height / 2, particle.dimension.width, particle.dimension.height, ); } } } } export class Particles extends Entity { constructor(options: Partial) { super(EntityNames.Particles); const spawnOptions = { ...DEFAULT_PARTICLE_SPAWN_OPTIONS, ...options, }; const particles = Array(options.particleCount) .fill(0) .map(() => Particles.spawnParticle(spawnOptions)); this.addComponent(new Life(true)); this.addComponent( new ParticleRenderer(particles, () => { const life = this.getComponent(ComponentNames.Life); life.alive = false; this.addComponent(life); }), ); this.addComponent( new BoundingBox( { x: 0, y: 0, }, { width: spawnOptions.spawnerDimensions.width, height: spawnOptions.spawnerDimensions.height, }, 0, ), ); } static spawnParticle(options: ParticleSpawnOptions) { const angle = Math.random() * Math.PI * 2; const speed = normalRandom( options.particleMeanSpeed, options.particleSpeedVariance, ); const life = normalRandom( options.particleMeanLife, options.particleLifeVariance, ); const size = normalRandom( options.particleMeanSize, options.particleSizeVariance, ); const color = options.particleColors[ Math.floor(Math.random() * options.particleColors.length) ]; const position = { x: options.center.x + Math.cos(angle) * options.spawnerDimensions.width, y: options.center.y + Math.sin(angle) * options.spawnerDimensions.height, }; if (options.spawnerShape === "rectangle") { // determine a random position on the edge of the spawner based on the angle const halfWidth = options.spawnerDimensions.width / 2; const halfHeight = options.spawnerDimensions.height / 2; if (angle < Math.PI / 4 || angle > (Math.PI * 7) / 4) { position.x += halfWidth; position.y += Math.tan(angle) * halfWidth; } else if (angle < (Math.PI * 3) / 4) { position.y += halfHeight; position.x += halfHeight / Math.tan(angle); } else if (angle < (Math.PI * 5) / 4) { position.x -= halfWidth; position.y -= Math.tan(angle) * halfWidth; } else { position.y -= halfHeight; position.x -= halfHeight / Math.tan(angle); } } return { position, velocity: { x: Math.cos(angle) * speed, y: Math.sin(angle) * speed, }, color, life, dimension: { width: size, height: size }, shape: options.particleShape, }; } }