src/emitter/Emitter.js
import {
DEFAULT_BIND_EMITTER,
DEFAULT_BIND_EMITTER_EVENT,
DEFAULT_DAMPING,
DEFAULT_EMITTER_INDEX,
DEFAULT_EMITTER_RATE,
} from './constants';
import EventDispatcher, {
EMITTER_DEAD,
PARTICLE_CREATED,
PARTICLE_DEAD,
PARTICLE_UPDATE,
SYSTEM_UPDATE,
} from '../events';
import { INTEGRATION_TYPE_EULER, integrate } from '../math';
import { Util, uid } from '../utils';
import { InitializerUtil } from '../initializer';
import Particle from '../core/Particle';
import isNumber from 'lodash/isNumber';
import { EMITTER_TYPE_EMITTER as type } from './types';
/**
* Emitters are the System engine's particle factories. They cause particles to
* be rendered by emitting them, and store all particle initializers and behaviours.
*
*/
export default class Emitter extends Particle {
/**
* Constructs an Emitter instance.
*
* @param {object} properties - The properties to instantiate the emitter with
* @return void
*/
constructor(properties) {
super(properties);
/**
* @desc The class type.
* @type {string}
*/
this.type = type;
/**
* @desc The particles emitted by this emitter.
* @type {array}
*/
this.particles = [];
/**
* @desc The initializers for particles emitted by this emitter.
* @type {array}
*/
this.initializers = [];
/**
* @desc The behaviours for particles emitted by this emitter.
* @type {array}
*/
this.behaviours = [];
/**
* @desc The behaviours for the emitter.
* @type {array}
*/
this.emitterBehaviours = [];
/**
* @desc The current emit iteration.
* @type {integer}
*/
this.currentEmitTime = 0;
/**
* @desc The total number of times the emitter should emit particles.
* @type {integer}
*/
this.totalEmitTimes = -1;
/**
* @desc The friction coefficient for all particle to emit by.
* @type {number}
*/
this.damping = DEFAULT_DAMPING;
/**
* @desc Ensures that particles emitted by this emitter are positioned
* according to the emitter's properties.
* @type {boolean}
*/
this.bindEmitter = DEFAULT_BIND_EMITTER;
/**
* @desc Determines if the emitter will dispatch internal events. Defaults
* to false
* @type {boolean}
*/
this.bindEmitterEvent = DEFAULT_BIND_EMITTER_EVENT;
/**
* @desc The number of particles to emit per second (a [particle]/b [s])
* @type {Rate}
*/
this.rate = DEFAULT_EMITTER_RATE;
/**
* @desc Determines if the emitter is emitting particles or not.
* @type {boolean}
*/
this.isEmitting = false;
/**
* @desc The emitter's id.
* @type {string}
*/
this.id = `emitter-${uid()}`;
this.cID = 0;
this.name = 'Emitter';
/**
* @desc The index of the emitter as it is added to the system.
* @type {number|undefined}
*/
this.index = DEFAULT_EMITTER_INDEX;
/**
* @desc The emitter's internal event dispatcher.
* @type {EventDispatcher}
*/
this.eventDispatcher = new EventDispatcher();
}
/**
* Proxy method for the internal event dispatcher's dispatchEvent method.
*
* @param {string} event - The event to dispatch
* @param {object<Particle>} [target=this] - The event target
*/
dispatch(event, target = this) {
this.eventDispatcher.dispatchEvent(event, target);
}
/**
* Sets the emitter rate.
*
* @param {Rate} rate - a rate initializer object
* @return {Emitter}
*/
setRate(rate) {
this.rate = rate;
return this;
}
/**
* Sets the position of the emitter.
*
* @param {object} newPosition - an object the new x, y and z props
* @return {Emitter}
*/
setPosition(newPosition = {}) {
const { position } = this;
const { x = position.x, y = position.y, z = position.z } = newPosition;
this.position.set(x, y, z);
return this;
}
/**
* Sets the rotation of the emitter.
*
* @param {object} newRotation - an object the new x, y and z props
* @return {Emitter}
*/
setRotation(newRotation = {}) {
const { rotation } = this;
const { x = rotation.x, y = rotation.y, z = rotation.z } = newRotation;
this.rotation.set(x, y, z);
return this;
}
/**
* Sets the total number of times the emitter should emit particles as well as
* the emitter's life. Also intializes the emitter rate.
* This enables the emitter to emit particles.
*
* @param {number} [totalEmitTimes=Infinity] - the total number of times to emit particles
* @param {number} [life=Infinity] - the life of this emitter in milliseconds
* @return {Emitter}
*/
emit(totalEmitTimes = Infinity, life = Infinity) {
this.currentEmitTime = 0;
this.totalEmitTimes = isNumber(totalEmitTimes) ? totalEmitTimes : Infinity;
if (totalEmitTimes === 1) {
this.life = totalEmitTimes;
} else {
this.life = isNumber(life) ? life : Infinity;
}
this.rate.init();
this.isEmitting = true;
return this;
}
/**
* Experimental emit method that is designed to be called from the System.emit method.
*
* @return {Emitter}
*/
experimental_emit() {
const { isEmitting, totalEmitTimes, life } = this;
if (!isEmitting) {
this.currentEmitTime = 0;
if (!totalEmitTimes) {
this.setTotalEmitTimes(Infinity);
}
if (!life) {
this.setLife(Infinity);
}
this.rate.init();
this.isEmitting = true;
}
return this;
}
/**
* Sets the total emit times for the emitter.
*
* @param {number} [totalEmitTimes=Infinity] - the total number of times to emit particles
* @return {Emitter}
*/
setTotalEmitTimes(totalEmitTimes = Infinity) {
this.totalEmitTimes = isNumber(totalEmitTimes) ? totalEmitTimes : Infinity;
return this;
}
/**
* Sets the life of the emitter.
*
* @param {number} [life=Infinity] - the life of this emitter in milliseconds
* @return {Emitter}
*/
setLife(life = Infinity) {
if (this.totalEmitTimes === 1) {
this.life = this.totalEmitTimes;
} else {
this.life = isNumber(life) ? life : Infinity;
}
return this;
}
/**
* Stops the emitter from emitting particles.
*
* @return void
*/
stopEmit() {
this.totalEmitTimes = -1;
this.currentEmitTime = 0;
this.isEmitting = false;
}
/**
* Kills all of the emitter's particles.
*
* @return void
*/
removeAllParticles() {
let i = this.particles.length;
while (i--) {
this.particles[i].dead = true;
}
}
/**
* Adds a particle initializer to the emitter.
* Each initializer is run on each particle when they are created.
*
* @param {Initializer} initializer - The initializer to add
* @return {Emitter}
*/
addInitializer(initializer) {
this.initializers.push(initializer);
return this;
}
/**
* Adds multiple particle initializers to the emitter.
*
* @param {array<Initializer>} initializers - an array of particle initializers
* @return {Emitter}
*/
addInitializers(initializers) {
let i = initializers.length;
while (i--) {
this.addInitializer(initializers[i]);
}
return this;
}
/**
* Sets the emitter's particle initializers.
*
* @param {array<Initializer>} initializers - an array of particle initializers
* @return {Emitter}
*/
setInitializers(initializers) {
this.initializers = initializers;
return this;
}
/**
* Removes an initializer from the emitter's initializers array.
*
* @param {Initializer} initializer - The initializer to remove
* @return {Emitter}
*/
removeInitializer(initializer) {
const index = this.initializers.indexOf(initializer);
if (index > -1) {
this.initializers.splice(index, 1);
}
return this;
}
/**
* Removes all initializers.
*
* @return {Emitter}
*/
removeAllInitializers() {
Util.destroyArray(this.initializers);
return this;
}
/**
* Adds a behaviour to the emitter. All emitter behaviours are added to each particle when
* they are emitted.
*
* @param {Behaviour} behaviour - The behaviour to add to the emitter
* @return {Emitter}
*/
addBehaviour(behaviour) {
this.behaviours.push(behaviour);
return this;
}
/**
* Adds multiple behaviours to the emitter.
*
* @param {array<Behaviour>} behaviours - an array of emitter behaviours
* @return {Emitter}
*/
addBehaviours(behaviours) {
let i = behaviours.length;
while (i--) {
this.addBehaviour(behaviours[i]);
}
return this;
}
/**
* Sets the emitter's behaviours.
*
* @param {array<Behaviour>} behaviours - an array of emitter behaviours
* @return {Emitter}
*/
setBehaviours(behaviours) {
this.behaviours = behaviours;
return this;
}
/**
* Removes the behaviour from the emitter's behaviours array.
*
* @param {Behaviour} behaviour - The behaviour to remove
* @return {Emitter}
*/
removeBehaviour(behaviour) {
const index = this.behaviours.indexOf(behaviour);
if (index > -1) {
this.behaviours.splice(index, 1);
}
return this;
}
/**
* Removes all behaviours from the emitter.
*
* @return {Emitter}
*/
removeAllBehaviours() {
Util.destroyArray(this.behaviours);
return this;
}
/**
* Adds an emitter behaviour to the emitter.
*
* @param {Behaviour} behaviour - The behaviour to add to the emitter
* @return {Emitter}
*/
addEmitterBehaviour(behaviour) {
this.emitterBehaviours.push(behaviour);
behaviour.initialize(this);
return this;
}
/**
* Adds multiple behaviours to the emitter.
*
* @param {array<Behaviour>} behaviours - an array of emitter behaviours
* @return {Emitter}
*/
addEmitterBehaviours(behaviours) {
let i = behaviours.length;
while (i--) {
this.addEmitterBehaviour(behaviours[i]);
}
return this;
}
/**
* Sets the emitter's behaviours.
*
* @param {array<Behaviour>} behaviours - an array of emitter behaviours
* @return {Emitter}
*/
setEmitterBehaviours(behaviours) {
const length = behaviours.length;
this.emitterBehaviours = behaviours;
for (let i = 0; i < length; i++) {
this.emitterBehaviours[i].initialize(this);
}
return this;
}
/**
* Removes the behaviour from the emitter's behaviours array.
*
* @param {Behaviour} behaviour - The behaviour to remove
* @return {Emitter}
*/
removeEmitterBehaviour(behaviour) {
const index = this.emitterBehaviours.indexOf(behaviour);
if (index > -1) {
this.emitterBehaviours.splice(index, 1);
}
return this;
}
/**
* Removes all behaviours from the emitter.
*
* @return {Emitter}
*/
removeAllEmitterBehaviours() {
Util.destroyArray(this.emitterBehaviours);
return this;
}
/**
* Adds the event listener for the EMITTER_DEAD event.
*
* @param {onEmitterDead} - The function to call when the EMITTER_DEAD is dispatched.
* @return {Emitter}
*/
addOnEmitterDeadEventListener(onEmitterDead) {
this.eventDispatcher.addEventListener(`${this.id}_${EMITTER_DEAD}`, () =>
onEmitterDead()
);
return this;
}
/**
* Creates a particle by retreiving one from the pool and setting it up with
* the supplied initializer and behaviour.
*
* @return {Emitter}
*/
createParticle() {
const particle = this.parent.pool.get(Particle);
const index = this.particles.length;
this.setupParticle(particle, index);
this.parent && this.parent.dispatch(PARTICLE_CREATED, particle);
this.bindEmitterEvent && this.dispatch(PARTICLE_CREATED, particle);
return particle;
}
/**
* Sets up a particle by running all initializers on it and setting its behaviours.
* Also adds the particle to this.particles.
*
* @param {Particle} particle - The particle to setup
* @return void
*/
setupParticle(particle, index) {
const { initializers, behaviours } = this;
InitializerUtil.initialize(this, particle, initializers);
particle.addBehaviours(behaviours);
particle.parent = this;
particle.index = index;
this.particles.push(particle);
}
/**
* Updates the emitter according to the time passed by calling the generate
* and integrate methods. The generate method creates particles, the integrate
* method updates existing particles.
*
* If the emitter age is greater than time, the emitter is killed.
*
* This method also indexes/deindexes particles.
*
* @param {number} time - System engine time
* @return void
*/
update(time) {
if (!this.isEmitting && this.particles.length === 0) {
return;
}
this.age += time;
if (this.dead || this.age >= this.life) {
this.destroy();
}
if (this.isEmitting)
{
this.generate(time);
}
this.integrate(time);
let i = this.particles.length;
while (i--) {
const particle = this.particles[i];
if (particle.dead) {
this.parent && this.parent.dispatch(PARTICLE_DEAD, particle);
this.bindEmitterEvent && this.dispatch(PARTICLE_DEAD, particle);
this.parent.pool.expire(particle.reset());
this.particles.splice(i, 1);
if(this.particles.length === 0)
{
this.parent && this.parent.dispatch(SYSTEM_UPDATE);
}
}
}
this.updateEmitterBehaviours(time);
}
/**
* Updates the emitter's emitter behaviours.
*
* @param {number} time - System engine time
* @return void
*/
updateEmitterBehaviours(time) {
if (this.sleep) {
return;
}
const length = this.emitterBehaviours.length;
for (let i = 0; i < length; i++) {
this.emitterBehaviours[i].applyBehaviour(this, time, i);
}
}
/**
* Runs the integration algorithm on the emitter and all particles.
* Updates the particles with the timstamp passed.
*
* @param {number} time - System engine time
* @return void
*/
integrate(time) {
const integrationType = this.parent
? this.parent.integrationType
: INTEGRATION_TYPE_EULER;
const damping = 1 - this.damping;
integrate(this, time, damping, integrationType);
let index = this.particles.length;
while (index--) {
const particle = this.particles[index];
particle.update(time, index);
integrate(particle, time, damping, integrationType);
this.parent && this.parent.dispatch(PARTICLE_UPDATE, particle);
this.bindEmitterEvent && this.dispatch(PARTICLE_UPDATE, particle);
}
}
/**
* Generates new particles.
*
* @param {number} time - System engine time
* @return void
*/
generate(time) {
if (this.totalEmitTimes === 1) {
let i = this.rate.getValue(99999);
if (i > 0) {
this.cID = i;
}
while (i--) {
this.createParticle();
}
this.totalEmitTimes = 0;
return;
}
this.currentEmitTime += time;
if (this.currentEmitTime < this.totalEmitTimes) {
let i = this.rate.getValue(time);
if (i > 0) {
this.cID = i;
}
while (i--) {
this.createParticle();
}
}
}
/**
* Kills the emitter.
*
* @return void
*/
destroy() {
this.dead = true;
this.energy = 0;
this.totalEmitTimes = -1;
if (this.particles.length == 0) {
this.isEmitting = false;
this.removeAllInitializers();
this.removeAllBehaviours();
this.dispatch(`${this.id}_${EMITTER_DEAD}`);
this.parent && this.parent.removeEmitter(this);
}
}
}