import { generateEase , rotatePoint , SimpleEase } from './ParticleUtils' ; import { Particle } from './Particle' ; import { EmitterConfigV3 } from './EmitterConfig' ; import { Container } from '@pixi/display' ; import { settings } from '@pixi/settings' ; import { Point } from '@pixi/math' ; import { Ticker } from '@pixi/ticker' ; import { BehaviorOrder , IEmitterBehavior , IEmitterBehaviorClass } from './behaviors/Behaviors' ; hidden const ticker = Ticker . shared ; /**
 * Key used in sorted order to determine when to set particle position from the emitter position
 * and rotation.
 */ const PositionParticle = Symbol ( 'Position particle per emitter position' ) ; /**
 * A particle emitter.
 */ export class Emitter { private static knownBehaviors : { [ key ] : IEmitterBehaviorClass ; } = { } ; param constructor public static registerBehavior ( constructor ) { Emitter . knownBehaviors [ constructor . type ] = constructor ; } /**
     * Active initialization behaviors for this emitter.
     */ protected initBehaviors : ( IEmitterBehavior | typeof PositionParticle ) [ ] ; /**
     * Active update behaviors for this emitter.
     */ protected updateBehaviors : IEmitterBehavior [ ] ; /**
     * Active recycle behaviors for this emitter.
     */ protected recycleBehaviors : IEmitterBehavior [ ] ; /**
     * The minimum lifetime for a particle, in seconds.
     */ public minLifetime : number ; /**
     * The maximum lifetime for a particle, in seconds.
     */ public maxLifetime : number ; /**
     * An easing function for nonlinear interpolation of values. Accepts a single
     * parameter of time as a value from 0-1, inclusive. Expected outputs are values
     * from 0-1, inclusive.
     */ public customEase : SimpleEase ; /**
     * Time between particle spawns in seconds.
     */ protected _frequency : number ; /**
     * Chance that a particle will be spawned on each opportunity to spawn one.
     * 0 is 0%, 1 is 100%.
     */ public spawnChance : number ; /**
     * Maximum number of particles to keep alive at a time. If this limit
     * is reached, no more particles will spawn until some have died.
     */ public maxParticles : number ; /**
     * The amount of time in seconds to emit for before setting emit to false.
     * A value of -1 is an unlimited amount of time.
     */ public emitterLifetime : number ; /**
     * Position at which to spawn particles, relative to the emitter's owner's origin.
     * For example, the flames of a rocket travelling right might have a spawnPos
     * of {x:-50, y:0}.
     * to spawn at the rear of the rocket.
     * To change this, use updateSpawnPos().
     */ public spawnPos : Point ; /**
     * Number of particles to spawn time that the frequency allows for particles to spawn.
     */ public particlesPerWave : number ; /**
     * Rotation of the emitter or emitter's owner in degrees. This is added to
     * the calculated spawn angle.
     * To change this, use rotate().
     */ protected rotation : number ; /**
     * The world position of the emitter's owner, to add spawnPos to when
     * spawning particles. To change this, use updateOwnerPos().
     */ protected ownerPos : Point ; /**
     * The origin + spawnPos in the previous update, so that the spawn position
     * can be interpolated to space out particles better.
     */ protected _prevEmitterPos : Point ; /**
     * If _prevEmitterPos is valid, to prevent interpolation on the first update
     */ protected _prevPosIsValid : boolean ; /**
     * If either ownerPos or spawnPos has changed since the previous update.
     */ protected _posChanged : boolean ; /**
     * The container to add particles to.
     */ protected _parent : Container ; /**
     * If particles should be added at the back of the display list instead of the front.
     */ public addAtBack : boolean ; /**
     * The current number of active particles.
     */ public particleCount : number ; /**
     * If particles should be emitted during update() calls. Setting this to false
     * stops new particles from being created, but allows existing ones to die out.
     */ protected _emit : boolean ; /**
     * The timer for when to spawn particles in seconds, where numbers less
     * than 0 mean that particles should be spawned.
     */ protected _spawnTimer : number ; /**
     * The life of the emitter in seconds.
     */ protected _emitterLife : number ; /**
     * The particles that are active and on the display list. This is the first particle in a
     * linked list.
     */ protected _activeParticlesFirst : Particle ; /**
     * The particles that are active and on the display list. This is the last particle in a
     * linked list.
     */ protected _activeParticlesLast : Particle ; /**
     * The particles that are not currently being used. This is the first particle in a
     * linked list.
     */ protected _poolFirst : Particle ; /**
     * The original config object that this emitter was initialized with.
     */ protected _origConfig : any ; /**
     * If the update function is called automatically from the shared ticker.
     * Setting this to false requires calling the update function manually.
     */ protected _autoUpdate : boolean ; /**
     * If the emitter should destroy itself when all particles have died out. This is set by
     * playOnceAndDestroy();
     */ protected _destroyWhenComplete : boolean ; /**
     * A callback for when all particles have died out. This is set by
     * playOnceAndDestroy() or playOnce();
     */ protected _completeCallback : ( ) => void ; param particleParent param particleImages param config param config . emit param config . autoUpdate constructor ( particleParent , config ) { this . initBehaviors = [ ] ; this . updateBehaviors = [ ] ; this . recycleBehaviors = [ ] ; this . minLifetime = 0 ; this . maxLifetime = 0 ; this . customEase = null ; this . _frequency = 1 ; this . spawnChance = 1 ; this . maxParticles = 1000 ; this . emitterLifetime = - 1 ; this . spawnPos = new Point ( ) ; this . particlesPerWave = 1 ; this . rotation = 0 ; this . ownerPos = new Point ( ) ; this . _prevEmitterPos = new Point ( ) ; this . _prevPosIsValid = false ; this . _posChanged = false ; this . _parent = null ; this . addAtBack = false ; this . particleCount = 0 ; this . _emit = false ; this . _spawnTimer = 0 ; this . _emitterLife = - 1 ; this . _activeParticlesFirst = null ; this . _activeParticlesLast = null ; this . _poolFirst = null ; this . _origConfig = null ; this . _autoUpdate = false ; this . _destroyWhenComplete = false ; this . _completeCallback = null ; this . parent = particleParent ; if ( config ) { this . init ( config ) ; } this . recycle = this . recycle ; this . update = this . update ; this . rotate = this . rotate ; this . updateSpawnPos = this . updateSpawnPos ; this . updateOwnerPos = this . updateOwnerPos ; } /**
     * Time between particle spawns in seconds. If this value is not a number greater than 0,
     * it will be set to 1 (particle per second) to prevent infinite loops.
     */ public get frequency ( ) : number { return this . _frequency ; } public set frequency ( value ) { if ( typeof value === 'number' && value > 0 ) { this . _frequency = value ; } else { this . _frequency = 1 ; } } /**
    * The container to add particles to. Settings this will dump any active particles.
    */ public get parent ( ) : Container { return this . _parent ; } public set parent ( value ) { this . cleanup ( ) ; this . _parent = value ; } param config public init ( config ) { if ( ! config ) { return ; } this . cleanup ( ) ; this . _origConfig = config ; this . minLifetime = config . lifetime . min ; this . maxLifetime = config . lifetime . max ; if ( config . ease ) { this . customEase = typeof config . ease === 'function' ? config . ease : generateEase ( config . ease ) ; } else { this . customEase = null ; } this . particlesPerWave = 1 ; if ( config . particlesPerWave && config . particlesPerWave > 1 ) { this . particlesPerWave = config . particlesPerWave ; } this . frequency = config . frequency ; this . spawnChance = ( typeof config . spawnChance === 'number' && config . spawnChance > 0 ) ? config . spawnChance : 1 ; this . emitterLifetime = config . emitterLifetime || - 1 ; this . maxParticles = config . maxParticles > 0 ? config . maxParticles : 1000 ; this . addAtBack = ! ! config . addAtBack ; this . rotation = 0 ; this . ownerPos . set ( 0 ) ; if ( config . pos ) { this . spawnPos . copyFrom ( config . pos ) ; } else { this . spawnPos . set ( 0 ) ; } this . _prevEmitterPos . copyFrom ( this . spawnPos ) ; this . _prevPosIsValid = false ; this . _spawnTimer = 0 ; this . emit = config . emit === undefined ? true : ! ! config . emit ; this . autoUpdate = ! ! config . autoUpdate ; const behaviors = config . behaviors . map ( ( data ) => { const constructor = Emitter . knownBehaviors [ data . type ] ; if ( ! constructor ) { console . error ( `Unknown behavior: ${ data . type }` ) ; return null ; } return new constructor ( data . config ) ; } ) . filter ( ( b ) => ! ! b ) ; behaviors . push ( PositionParticle ) ; behaviors . sort ( ( a , b ) => { if ( a === PositionParticle ) { return ( b as IEmitterBehavior ) . order === BehaviorOrder . Spawn ? 1 : - 1 ; } else if ( b === PositionParticle ) { return ( a as IEmitterBehavior ) . order === BehaviorOrder . Spawn ? - 1 : 1 ; } return ( a as IEmitterBehavior ) . order - ( b as IEmitterBehavior ) . order ; } ) ; this . initBehaviors = behaviors . slice ( ) ; this . updateBehaviors = behaviors . filter ( ( b ) => b !== PositionParticle && b . updateParticle ) as IEmitterBehavior [ ] ; this . recycleBehaviors = behaviors . filter ( ( b ) => b !== PositionParticle && b . recycleParticle ) as IEmitterBehavior [ ] ; } param type public getBehavior ( type ) { if ( ! Emitter . knownBehaviors [ type ] ) return null ; return this . initBehaviors . find ( ( b ) => b instanceof Emitter . knownBehaviors [ type ] ) as IEmitterBehavior || null ; } param count public fillPool ( count ) { for ( ; count > 0 ; -- count ) { const p = new Particle ( this ) ; p . next = this . _poolFirst ; this . _poolFirst = p ; } } param particle param fromCleanup internal public recycle ( particle , fromCleanup = false ) { for ( let i = 0 ; i < this . recycleBehaviors . length ; ++ i ) { this . recycleBehaviors [ i ] . recycleParticle ( particle , ! fromCleanup ) ; } if ( particle . next ) { particle . next . prev = particle . prev ; } if ( particle . prev ) { particle . prev . next = particle . next ; } if ( particle === this . _activeParticlesLast ) { this . _activeParticlesLast = particle . prev ; } if ( particle === this . _activeParticlesFirst ) { this . _activeParticlesFirst = particle . next ; } particle . prev = null ; particle . next = this . _poolFirst ; this . _poolFirst = particle ; if ( particle . parent ) { particle . parent . removeChild ( particle ) ; } -- this . particleCount ; } param newRot public rotate ( newRot ) { if ( this . rotation === newRot ) return ; const diff = newRot - this . rotation ; this . rotation = newRot ; rotatePoint ( diff , this . spawnPos ) ; this . _posChanged = true ; } param x param y public updateSpawnPos ( x , y ) { this . _posChanged = true ; this . spawnPos . x = x ; this . spawnPos . y = y ; } param x param y public updateOwnerPos ( x , y ) { this . _posChanged = true ; this . ownerPos . x = x ; this . ownerPos . y = y ; } /**
     * Prevents emitter position interpolation in the next update.
     * This should be used if you made a major position change of your emitter's owner
     * that was not normal movement.
     */ public resetPositionTracking ( ) { this . _prevPosIsValid = false ; } /**
     * If particles should be emitted during update() calls. Setting this to false
     * stops new particles from being created, but allows existing ones to die out.
     */ public get emit ( ) : boolean { return this . _emit ; } public set emit ( value ) { this . _emit = ! ! value ; this . _emitterLife = this . emitterLifetime ; } /**
     * If the update function is called automatically from the shared ticker.
     * Setting this to false requires calling the update function manually.
     */ public get autoUpdate ( ) : boolean { return this . _autoUpdate ; } public set autoUpdate ( value ) { if ( this . _autoUpdate && ! value ) { ticker . remove ( this . update , this ) ; } else if ( ! this . _autoUpdate && value ) { ticker . add ( this . update , this ) ; } this . _autoUpdate = ! ! value ; } param callback public playOnceAndDestroy ( callback ? ) { this . autoUpdate = true ; this . emit = true ; this . _destroyWhenComplete = true ; this . _completeCallback = callback ; } param callback public playOnce ( callback ? ) { this . emit = true ; this . _completeCallback = callback ; } param delta public update ( delta ) { if ( this . _autoUpdate ) { delta = delta / settings . TARGET_FPMS / 1000 ; } if ( ! this . _parent ) return ; for ( let particle = this . _activeParticlesFirst , next ; particle ; particle = next ) { next = particle . next ; particle . age += delta ; if ( particle . age > particle . maxLife || particle . age < 0 ) { this . recycle ( particle ) ; } else { let lerp = particle . age * particle . oneOverLife ; if ( this . customEase ) { if ( this . customEase . length === 4 ) { lerp = ( this . customEase as any ) ( lerp , 0 , 1 , 1 ) ; } else { lerp = this . customEase ( lerp ) ; } } particle . agePercent = lerp ; for ( let i = 0 ; i < this . updateBehaviors . length ; ++ i ) { if ( this . updateBehaviors [ i ] . updateParticle ( particle , delta ) ) { this . recycle ( particle ) ; break ; } } } } let prevX ; let prevY ; if ( this . _prevPosIsValid ) { prevX = this . _prevEmitterPos . x ; prevY = this . _prevEmitterPos . y ; } const curX = this . ownerPos . x + this . spawnPos . x ; const curY = this . ownerPos . y + this . spawnPos . y ; if ( this . _emit ) { this . _spawnTimer -= delta < 0 ? 0 : delta ; while ( this . _spawnTimer <= 0 ) { if ( this . _emitterLife >= 0 ) { this . _emitterLife -= this . _frequency ; if ( this . _emitterLife <= 0 ) { this . _spawnTimer = 0 ; this . _emitterLife = 0 ; this . emit = false ; break ; } } if ( this . particleCount >= this . maxParticles ) { this . _spawnTimer += this . _frequency ; continue ; } let emitPosX ; let emitPosY ; if ( this . _prevPosIsValid && this . _posChanged ) { const lerp = 1 + ( this . _spawnTimer / delta ) ; emitPosX = ( ( curX - prevX ) * lerp ) + prevX ; emitPosY = ( ( curY - prevY ) * lerp ) + prevY ; } else { emitPosX = curX ; emitPosY = curY ; } let waveFirst = null ; let waveLast = null ; for ( let len = Math . min ( this . particlesPerWave , this . maxParticles - this . particleCount ) , i = 0 ; i < len ; ++ i ) { if ( this . spawnChance < 1 && Math . random ( ) >= this . spawnChance ) { continue ; } let lifetime ; if ( this . minLifetime === this . maxLifetime ) { lifetime = this . minLifetime ; } else { lifetime = ( Math . random ( ) * ( this . maxLifetime - this . minLifetime ) ) + this . minLifetime ; } if ( - this . _spawnTimer >= lifetime ) { continue ; } let p ; if ( this . _poolFirst ) { p = this . _poolFirst ; this . _poolFirst = this . _poolFirst . next ; p . next = null ; } else { p = new Particle ( this ) ; } p . init ( lifetime ) ; if ( this . addAtBack ) { this . _parent . addChildAt ( p , 0 ) ; } else { this . _parent . addChild ( p ) ; } if ( waveFirst ) { waveLast . next = p ; p . prev = waveLast ; waveLast = p ; } else { waveLast = waveFirst = p ; } ++ this . particleCount ; } if ( waveFirst ) { if ( this . _activeParticlesLast ) { this . _activeParticlesLast . next = waveFirst ; waveFirst . prev = this . _activeParticlesLast ; this . _activeParticlesLast = waveLast ; } else { this . _activeParticlesFirst = waveFirst ; this . _activeParticlesLast = waveLast ; } for ( let i = 0 ; i < this . initBehaviors . length ; ++ i ) { const behavior = this . initBehaviors [ i ] ; if ( behavior === PositionParticle ) { for ( let particle = waveFirst , next ; particle ; particle = next ) { next = particle . next ; if ( this . rotation !== 0 ) { rotatePoint ( this . rotation , particle . position ) ; particle . rotation += this . rotation ; } particle . position . x += emitPosX ; particle . position . y += emitPosY ; particle . age += delta ; let lerp = particle . age * particle . oneOverLife ; if ( this . customEase ) { if ( this . customEase . length === 4 ) { lerp = ( this . customEase as any ) ( lerp , 0 , 1 , 1 ) ; } else { lerp = this . customEase ( lerp ) ; } } particle . agePercent = lerp ; } } else { behavior . initParticles ( waveFirst ) ; } } for ( let particle = waveFirst , next ; particle ; particle = next ) { next = particle . next ; for ( let i = 0 ; i < this . updateBehaviors . length ; ++ i ) { if ( this . updateBehaviors [ i ] . updateParticle ( particle , - this . _spawnTimer ) ) { this . recycle ( particle ) ; break ; } } } } this . _spawnTimer += this . _frequency ; } } if ( this . _posChanged ) { this . _prevEmitterPos . x = curX ; this . _prevEmitterPos . y = curY ; this . _prevPosIsValid = true ; this . _posChanged = false ; } if ( ! this . _emit && ! this . _activeParticlesFirst ) { if ( this . _completeCallback ) { const cb = this . _completeCallback ; this . _completeCallback = null ; cb ( ) ; } if ( this . _destroyWhenComplete ) { this . destroy ( ) ; } } } /**
     * Emits a single wave of particles, using standard spawnChance & particlesPerWave settings. Does not affect
     * regular spawning through the frequency, and ignores the emit property. The max particle count is respected, however,
     * so if there are already too many particles then nothing will happen.
     */ public emitNow ( ) { const emitPosX = this . ownerPos . x + this . spawnPos . x ; const emitPosY = this . ownerPos . y + this . spawnPos . y ; let waveFirst = null ; let waveLast = null ; for ( let len = Math . min ( this . particlesPerWave , this . maxParticles - this . particleCount ) , i = 0 ; i < len ; ++ i ) { if ( this . spawnChance < 1 && Math . random ( ) >= this . spawnChance ) { continue ; } let p ; if ( this . _poolFirst ) { p = this . _poolFirst ; this . _poolFirst = this . _poolFirst . next ; p . next = null ; } else { p = new Particle ( this ) ; } let lifetime ; if ( this . minLifetime === this . maxLifetime ) { lifetime = this . minLifetime ; } else { lifetime = ( Math . random ( ) * ( this . maxLifetime - this . minLifetime ) ) + this . minLifetime ; } p . init ( lifetime ) ; if ( this . addAtBack ) { this . _parent . addChildAt ( p , 0 ) ; } else { this . _parent . addChild ( p ) ; } if ( waveFirst ) { waveLast . next = p ; p . prev = waveLast ; waveLast = p ; } else { waveLast = waveFirst = p ; } ++ this . particleCount ; } if ( waveFirst ) { if ( this . _activeParticlesLast ) { this . _activeParticlesLast . next = waveFirst ; waveFirst . prev = this . _activeParticlesLast ; this . _activeParticlesLast = waveLast ; } else { this . _activeParticlesFirst = waveFirst ; this . _activeParticlesLast = waveLast ; } for ( let i = 0 ; i < this . initBehaviors . length ; ++ i ) { const behavior = this . initBehaviors [ i ] ; if ( behavior === PositionParticle ) { for ( let particle = waveFirst , next ; particle ; particle = next ) { next = particle . next ; if ( this . rotation !== 0 ) { rotatePoint ( this . rotation , particle . position ) ; particle . rotation += this . rotation ; } particle . position . x += emitPosX ; particle . position . y += emitPosY ; } } else { behavior . initParticles ( waveFirst ) ; } } } } /**
     * Kills all active particles immediately.
     */ public cleanup ( ) { let particle ; let next ; for ( particle = this . _activeParticlesFirst ; particle ; particle = next ) { next = particle . next ; this . recycle ( particle , true ) ; } this . _activeParticlesFirst = this . _activeParticlesLast = null ; this . particleCount = 0 ; } /**
     * If this emitter has been destroyed. Note that a destroyed emitter can still be reused, after
     * having a new parent set and being reinitialized.
     */ public get destroyed ( ) : boolean { return ! ( this . _parent && this . initBehaviors . length ) ; } /**
     * Destroys the emitter and all of its particles.
     */ public destroy ( ) { this . autoUpdate = false ; this . cleanup ( ) ; let next ; for ( let particle = this . _poolFirst ; particle ; particle = next ) { next = particle . next ; particle . destroy ( ) ; } this . _poolFirst = this . _parent = this . spawnPos = this . ownerPos = this . customEase = this . _completeCallback = null ; this . initBehaviors . length = this . updateBehaviors . length = this . recycleBehaviors . length = 0 ; } }