195 lines
5.2 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|