diff options
author | Elizabeth Hunt <elizabeth.hunt@simponic.xyz> | 2024-03-06 14:35:04 -0700 |
---|---|---|
committer | Elizabeth Hunt <elizabeth.hunt@simponic.xyz> | 2024-03-06 14:35:04 -0700 |
commit | 823620b2a6ebb7ece619991e47a37ad46542b69f (patch) | |
tree | 82a1501c5f707a1bcbc6c28bd6d0f5731cc9f618 /src/engine/entities/Particles.ts | |
parent | ce06fa7c29ba4e3d6137f7aa74fbfe45af0e8b73 (diff) | |
download | the-abstraction-engine-823620b2a6ebb7ece619991e47a37ad46542b69f.tar.gz the-abstraction-engine-823620b2a6ebb7ece619991e47a37ad46542b69f.zip |
add particles
Diffstat (limited to 'src/engine/entities/Particles.ts')
-rw-r--r-- | src/engine/entities/Particles.ts | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/src/engine/entities/Particles.ts b/src/engine/entities/Particles.ts new file mode 100644 index 0000000..34b475c --- /dev/null +++ b/src/engine/entities/Particles.ts @@ -0,0 +1,194 @@ +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<string>; +} + +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<Particle>; + private onDeath?: () => void; + + constructor(particles: Array<Particle> = [], 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<ParticleSpawnOptions>) { + 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<Life>(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, + }; + } +} |