summaryrefslogtreecommitdiff
path: root/src/engine/entities/Particles.ts
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-03-06 14:35:04 -0700
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-03-06 14:35:04 -0700
commit823620b2a6ebb7ece619991e47a37ad46542b69f (patch)
tree82a1501c5f707a1bcbc6c28bd6d0f5731cc9f618 /src/engine/entities/Particles.ts
parentce06fa7c29ba4e3d6137f7aa74fbfe45af0e8b73 (diff)
downloadthe-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.ts194
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,
+ };
+ }
+}