2024-03-06 14:35:04 -07:00

195 lines
5.2 KiB
TypeScript

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,
};
}
}