import { Container, Sprite, Texture } from 'pixi.js';
import { Rng, createRng } from '../math/rng';
/**
* Простая система частиц: эмиттер спавнит точки (Texture.WHITE, подкрашенные)
* с заданной скоростью, временем жизни и затуханием.
* Используется для атмосферы: пепел, мотыльки, пыль, искры.
*/
export interface EmitterOptions {
/** Цвет частиц (0xRRGGBB). */
color: number;
/** Частиц в секунду. */
rate: number;
/** Время жизни частицы, секунды (min..max). */
lifetime: [number, number];
/** Начальная скорость, виртуальных пикселей/сек: (minX..maxX, minY..maxY). */
velocity: { x: [number, number]; y: [number, number] };
/** Ускорение (гравитация/ветер), пикселей/сек². */
acceleration?: { x?: number; y?: number };
/** Размер частицы в пикселях (1..4). */
size?: number;
/** Спавн в прямоугольнике вокруг позиции эмиттера (по умолчанию точка). */
spawnArea?: { width: number; height: number };
/** Затухание альфы к концу жизни (по умолчанию true). */
fadeOut?: boolean;
/** Seed для повторяемого движения. */
seed?: number;
}
interface Particle {
sprite: Sprite;
vx: number;
vy: number;
age: number;
lifetime: number;
}
export class ParticleEmitter extends Container {
private particles: Particle[] = [];
private spawnTimer = 0;
private rng: Rng;
constructor(private options: EmitterOptions) {
super();
this.rng = createRng(options.seed ?? 0x9e3779b9);
}
update(dt: number): void {
// Спавн
this.spawnTimer += dt * this.options.rate;
while (this.spawnTimer >= 1) {
this.spawnTimer -= 1;
this.spawn();
}
// Обновление
const accX = this.options.acceleration?.x ?? 0;
const accY = this.options.acceleration?.y ?? 0;
for (const p of this.particles) {
p.age += dt;
p.vx += accX * dt;
p.vy += accY * dt;
p.sprite.x += p.vx * dt;
p.sprite.y += p.vy * dt;
if (this.options.fadeOut ?? true) {
p.sprite.alpha = Math.max(0, 1 - p.age / p.lifetime);
}
}
// Сбор «умерших»
const dead = this.particles.filter((p) => p.age >= p.lifetime);
for (const p of dead) {
p.sprite.destroy();
}
if (dead.length > 0) {
this.particles = this.particles.filter((p) => p.age < p.lifetime);
}
}
/** Убрать все частицы (например, при выходе со сцены). */
clear(): void {
for (const p of this.particles) p.sprite.destroy();
this.particles = [];
}
get count(): number {
return this.particles.length;
}
private spawn(): void {
const size = this.options.size ?? 2;
const sprite = new Sprite(Texture.WHITE);
sprite.width = size;
sprite.height = size;
sprite.tint = this.options.color;
const area = this.options.spawnArea;
sprite.x = area ? this.rng.int(0, area.width) - area.width / 2 : 0;
sprite.y = area ? this.rng.int(0, area.height) - area.height / 2 : 0;
sprite.alpha = 0; // появится в следующем кадре без «вспышки»
const v = this.options.velocity;
this.particles.push({
sprite,
vx: v.x[0] + this.rng.next() * (v.x[1] - v.x[0]),
vy: v.y[0] + this.rng.next() * (v.y[1] - v.y[0]),
age: 0,
lifetime: this.options.lifetime[0] + this.rng.next() * (this.options.lifetime[1] - this.options.lifetime[0])
});
this.addChild(sprite);
}
}