ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
226 lines (225 loc) • 8.45 kB
TypeScript
/**
* Particle System Plugin for ECSpresso
*
* High-performance particle system where particles live outside the ECS in
* pre-allocated pools. Renders via PixiJS v8's ParticleContainer + Particle API.
* Renderer2D is a required dependency.
*
* Follows the established plugin pattern: immutable shared config
* (ParticleEffectConfig) + mutable per-entity state (ParticleEmitter) component,
* side-storage Map for PixiJS objects, kit pattern for typed helpers.
*/
import { type BasePluginOptions } from 'ecspresso';
import type { BaseWorld } from 'ecspresso';
import type { ComponentsConfig } from '../../type-utils';
import type { TransformComponentTypes } from 'ecspresso/plugins/spatial/transform';
/** BaseWorld narrowed to particle components for typed access in helpers. */
type ParticleWorld = BaseWorld<ParticleComponentTypes>;
/** Fixed value or random range [min, max] */
export type ParticleValue = number | readonly [number, number];
/** Emission geometry */
export type EmissionShape = 'point' | 'circle';
/** Blend modes for particle rendering */
export type ParticleBlendMode = 'normal' | 'add' | 'multiply' | 'screen';
/**
* User-facing config input for defining a particle effect.
* All properties optional except maxParticles and texture.
*/
export interface ParticleEffectInput {
/** Pool size — maximum simultaneous particles */
maxParticles: number;
/** PixiJS Texture for particles */
texture: unknown;
/** Particles per second (0 = burst-only, default: 10) */
spawnRate?: number;
/** Particles per burst (default: 0) */
burstCount?: number;
/** Emitter lifetime in seconds (-1 = infinite, default: -1) */
duration?: number;
/** Per-particle lifetime in seconds (default: 1) */
lifetime?: ParticleValue;
/** Initial speed in pixels/second (default: 100) */
speed?: ParticleValue;
/** Emission direction in radians (default: [0, 2*PI]) */
angle?: ParticleValue;
/** Spawn geometry (default: 'point') */
emissionShape?: EmissionShape;
/** Radius for 'circle' shape (default: 0) */
emissionRadius?: number;
/** Acceleration in pixels/second^2 (default: {x: 0, y: 0}) */
gravity?: {
readonly x: number;
readonly y: number;
};
/** Initial scale (default: 1) */
startSize?: ParticleValue;
/** Final scale (default: same as startSize) */
endSize?: ParticleValue;
/** Initial opacity (default: 1) */
startAlpha?: ParticleValue;
/** Final opacity (default: 0) */
endAlpha?: ParticleValue;
/** Initial hex color (default: 0xffffff) */
startTint?: number;
/** Final hex color (default: same as startTint) */
endTint?: number;
/** Initial rotation in radians (default: 0) */
startRotation?: ParticleValue;
/** Rotation velocity in rad/s (default: 0) */
rotationSpeed?: ParticleValue;
/** Blend mode (default: 'normal') */
blendMode?: ParticleBlendMode;
/** Particles in world coordinates (default: true) */
worldSpace?: boolean;
}
/**
* Frozen, fully-resolved particle effect config.
* Output of defineParticleEffect.
*/
export interface ParticleEffectConfig {
readonly maxParticles: number;
readonly texture: unknown;
readonly spawnRate: number;
readonly burstCount: number;
readonly duration: number;
readonly lifetime: ParticleValue;
readonly speed: ParticleValue;
readonly angle: ParticleValue;
readonly emissionShape: EmissionShape;
readonly emissionRadius: number;
readonly gravity: {
readonly x: number;
readonly y: number;
};
readonly startSize: ParticleValue;
readonly endSize: ParticleValue;
readonly startAlpha: ParticleValue;
readonly endAlpha: ParticleValue;
readonly startTint: number;
readonly endTint: number;
readonly startRotation: ParticleValue;
readonly rotationSpeed: ParticleValue;
readonly blendMode: ParticleBlendMode;
readonly worldSpace: boolean;
}
/**
* Mutable per-particle state. Pre-allocated, never GC'd.
*/
export interface ParticleState {
active: boolean;
x: number;
y: number;
vx: number;
vy: number;
life: number;
maxLife: number;
size: number;
startSize: number;
endSize: number;
alpha: number;
startAlpha: number;
endAlpha: number;
tint: number;
rotation: number;
rotationSpeed: number;
}
/**
* Per-entity emitter state stored as an ECS component.
*/
export interface ParticleEmitter {
readonly config: ParticleEffectConfig;
activeCount: number;
spawnAccumulator: number;
elapsed: number;
playing: boolean;
pendingBurst: number;
finished: boolean;
onComplete?: (data: ParticleEmitterEventData) => void;
}
/**
* Component types provided by the particle plugin.
*/
export interface ParticleComponentTypes {
particleEmitter: ParticleEmitter;
}
/**
* Data published when an emitter completes.
*/
export interface ParticleEmitterEventData {
entityId: number;
}
export interface ParticlePluginOptions<G extends string = 'particles'> extends BasePluginOptions<G> {
}
/**
* Sample a ParticleValue: returns fixed value or random within [min, max].
*/
export declare function sampleRange(value: ParticleValue): number;
/**
* Linear interpolation between two hex colors (RGB channels).
*/
export declare function lerpTint(start: number, end: number, t: number): number;
/**
* Define a particle effect config with defaults applied and frozen.
*/
export declare function defineParticleEffect(input: ParticleEffectInput): ParticleEffectConfig;
/**
* Create a particleEmitter component suitable for spreading into spawn().
*/
export declare function createParticleEmitter(config: ParticleEffectConfig, options?: {
playing?: boolean;
onComplete?: (data: ParticleEmitterEventData) => void;
}): Pick<ParticleComponentTypes, 'particleEmitter'>;
/**
* Queue a burst of particles on an emitter.
* Returns false if entity has no particleEmitter component.
*/
export declare function burstParticles(ecs: ParticleWorld, entityId: number, count?: number): boolean;
/**
* Stop an emitter from spawning new particles.
* Existing particles continue their lifecycle.
*/
export declare function stopEmitter(ecs: ParticleWorld, entityId: number): boolean;
/**
* Resume a stopped emitter.
*/
export declare function resumeEmitter(ecs: ParticleWorld, entityId: number): boolean;
/**
* Runtime data stored outside the ECS, keyed by entity ID.
*/
export interface EmitterRuntimeData {
particles: ParticleState[];
pixiContainer: unknown;
pixiParticles: unknown[];
}
export declare const particlePresets: {
readonly explosion: (texture: unknown, overrides?: Partial<ParticleEffectInput>) => ParticleEffectConfig;
readonly smoke: (texture: unknown, overrides?: Partial<ParticleEffectInput>) => ParticleEffectConfig;
readonly fire: (texture: unknown, overrides?: Partial<ParticleEffectInput>) => ParticleEffectConfig;
readonly sparkle: (texture: unknown, overrides?: Partial<ParticleEffectInput>) => ParticleEffectConfig;
readonly trail: (texture: unknown, overrides?: Partial<ParticleEffectInput>) => ParticleEffectConfig;
};
type ParticleLabels = 'particle-update' | 'particle-render-sync';
type ParticleRequires = ComponentsConfig<TransformComponentTypes & {
renderLayer: string;
}>;
/**
* Create a particle system plugin for ECSpresso.
*
* Provides:
* - Pre-allocated particle pools outside the entity system
* - Continuous and burst emission modes
* - Velocity, gravity, lifetime, interpolation (size, alpha, tint, rotation)
* - World-space and local-space particle emission
* - PixiJS ParticleContainer rendering (via renderer2D dependency)
* - Presets for common effects (explosion, smoke, fire, sparkle, trail)
*
* Renderer2D is a required dependency.
*/
export declare function createParticlePlugin<G extends string = 'particles'>(options?: ParticlePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, ParticleComponentTypes>, ParticleRequires, ParticleLabels, G, never, "particle-emitters">;
/**
* Get the runtime data for an emitter entity.
* Useful for tests and advanced usage.
* @internal Exported for testing only.
*/
export declare function getEmitterData(emitterDataMap: Map<number, EmitterRuntimeData>, entityId: number): EmitterRuntimeData | undefined;
export {};