/*! * @pixi/particle-emitter - v4.3.1 * Compiled Thu, 19 Aug 2021 03:42:44 UTC * * @pixi/particle-emitter is licensed under the MIT License. * http://www.opensource.org/licenses/mit-license */ import { Texture } from '@pixi/core'; import { BLEND_MODES } from '@pixi/constants'; import { Sprite } from '@pixi/sprite'; import { settings } from '@pixi/settings'; import { Point } from '@pixi/math'; import { Ticker } from '@pixi/ticker'; import { Container, DisplayObject } from '@pixi/display'; /** * A single node in a PropertyList. */ class PropertyNode { /** * @param value The value for this node * @param time The time for this node, between 0-1 * @param [ease] Custom ease for this list. Only relevant for the first node. */ constructor(value, time, ease) { this.value = value; this.time = time; this.next = null; this.isStepped = false; if (ease) { this.ease = typeof ease === 'function' ? ease : generateEase(ease); } else { this.ease = null; } } /** * Creates a list of property values from a data object {list, isStepped} with a list of objects in * the form {value, time}. Alternatively, the data object can be in the deprecated form of * {start, end}. * @param data The data for the list. * @param data.list The array of value and time objects. * @param data.isStepped If the list is stepped rather than interpolated. * @param data.ease Custom ease for this list. * @return The first node in the list */ // eslint-disable-next-line max-len static createList(data) { if ('list' in data) { const array = data.list; let node; const { value, time } = array[0]; // eslint-disable-next-line max-len const first = node = new PropertyNode(typeof value === 'string' ? hexToRGB(value) : value, time, data.ease); // only set up subsequent nodes if there are a bunch or the 2nd one is different from the first if (array.length > 2 || (array.length === 2 && array[1].value !== value)) { for (let i = 1; i < array.length; ++i) { const { value, time } = array[i]; node.next = new PropertyNode(typeof value === 'string' ? hexToRGB(value) : value, time); node = node.next; } } first.isStepped = !!data.isStepped; return first; } // Handle deprecated version here const start = new PropertyNode(typeof data.start === 'string' ? hexToRGB(data.start) : data.start, 0); // only set up a next value if it is different from the starting value if (data.end !== data.start) { start.next = new PropertyNode(typeof data.end === 'string' ? hexToRGB(data.end) : data.end, 1); } return start; } } /** * The method used by behaviors to fetch textures. Defaults to Texture.from. */ // get Texture.from(), only supports V5 and V6 with individual packages // eslint-disable-next-line prefer-const let GetTextureFromString = Texture.from; /** * If errors and warnings should be logged within the library. */ const verbose = false; const DEG_TO_RADS = Math.PI / 180; /** * Rotates a point by a given angle. * @param angle The angle to rotate by in radians * @param p The point to rotate around 0,0. */ function rotatePoint(angle, p) { if (!angle) return; const s = Math.sin(angle); const c = Math.cos(angle); const xnew = (p.x * c) - (p.y * s); const ynew = (p.x * s) + (p.y * c); p.x = xnew; p.y = ynew; } /** * Combines separate color components (0-255) into a single uint color. * @param r The red value of the color * @param g The green value of the color * @param b The blue value of the color * @return The color in the form of 0xRRGGBB */ function combineRGBComponents(r, g, b /* , a*/) { return /* a << 24 |*/ (r << 16) | (g << 8) | b; } /** * Returns the length (or magnitude) of this point. * @param point The point to measure length * @return The length of this point. */ function length(point) { return Math.sqrt((point.x * point.x) + (point.y * point.y)); } /** * Reduces the point to a length of 1. * @param point The point to normalize */ function normalize(point) { const oneOverLen = 1 / length(point); point.x *= oneOverLen; point.y *= oneOverLen; } /** * Multiplies the x and y values of this point by a value. * @param point The point to scaleBy * @param value The value to scale by. */ function scaleBy(point, value) { point.x *= value; point.y *= value; } /** * Converts a hex string from "#AARRGGBB", "#RRGGBB", "0xAARRGGBB", "0xRRGGBB", * "AARRGGBB", or "RRGGBB" to an object of ints of 0-255, as * {r, g, b, (a)}. * @param color The input color string. * @param output An object to put the output in. If omitted, a new object is created. * @return The object with r, g, and b properties, possibly with an a property. */ function hexToRGB(color, output) { if (!output) { output = {}; } if (color.charAt(0) === '#') { color = color.substr(1); } else if (color.indexOf('0x') === 0) { color = color.substr(2); } let alpha; if (color.length === 8) { alpha = color.substr(0, 2); color = color.substr(2); } output.r = parseInt(color.substr(0, 2), 16); // Red output.g = parseInt(color.substr(2, 2), 16); // Green output.b = parseInt(color.substr(4, 2), 16); // Blue if (alpha) { output.a = parseInt(alpha, 16); } return output; } /** * Generates a custom ease function, based on the GreenSock custom ease, as demonstrated * by the related tool at http://www.greensock.com/customease/. * @param segments An array of segments, as created by * http://www.greensock.com/customease/. * @return A function that calculates the percentage of change at * a given point in time (0-1 inclusive). */ function generateEase(segments) { const qty = segments.length; const oneOverQty = 1 / qty; /* * Calculates the percentage of change at a given point in time (0-1 inclusive). * @param {Number} time The time of the ease, 0-1 inclusive. * @return {Number} The percentage of the change, 0-1 inclusive (unless your * ease goes outside those bounds). */ // eslint-disable-next-line func-names return function (time) { const i = (qty * time) | 0; // do a quick floor operation const t = (time - (i * oneOverQty)) * qty; const s = segments[i] || segments[qty - 1]; return (s.s + (t * ((2 * (1 - t) * (s.cp - s.s)) + (t * (s.e - s.s))))); }; } /** * Gets a blend mode, ensuring that it is valid. * @param name The name of the blend mode to get. * @return The blend mode as specified in the PIXI.BLEND_MODES enumeration. */ function getBlendMode(name) { if (!name) return BLEND_MODES.NORMAL; name = name.toUpperCase().replace(/ /g, '_'); return BLEND_MODES[name] || BLEND_MODES.NORMAL; } /** * Converts a list of {value, time} objects starting at time 0 and ending at time 1 into an evenly * spaced stepped list of PropertyNodes for color values. This is primarily to handle conversion of * linear gradients to fewer colors, allowing for some optimization for Canvas2d fallbacks. * @param list The list of data to convert. * @param [numSteps=10] The number of steps to use. * @return The blend mode as specified in the PIXI.blendModes enumeration. */ function createSteppedGradient(list, numSteps = 10) { if (typeof numSteps !== 'number' || numSteps <= 0) { numSteps = 10; } const first = new PropertyNode(hexToRGB(list[0].value), list[0].time); first.isStepped = true; let currentNode = first; let current = list[0]; let nextIndex = 1; let next = list[nextIndex]; for (let i = 1; i < numSteps; ++i) { let lerp = i / numSteps; // ensure we are on the right segment, if multiple while (lerp > next.time) { current = next; next = list[++nextIndex]; } // convert the lerp value to the segment range lerp = (lerp - current.time) / (next.time - current.time); const curVal = hexToRGB(current.value); const nextVal = hexToRGB(next.value); const output = { r: ((nextVal.r - curVal.r) * lerp) + curVal.r, g: ((nextVal.g - curVal.g) * lerp) + curVal.g, b: ((nextVal.b - curVal.b) * lerp) + curVal.b, }; currentNode.next = new PropertyNode(output, i / numSteps); currentNode = currentNode.next; } // we don't need to have a PropertyNode for time of 1, because in a stepped version at that point // the particle has died of old age return first; } var ParticleUtils = { __proto__: null, GetTextureFromString: GetTextureFromString, verbose: verbose, DEG_TO_RADS: DEG_TO_RADS, rotatePoint: rotatePoint, combineRGBComponents: combineRGBComponents, length: length, normalize: normalize, scaleBy: scaleBy, hexToRGB: hexToRGB, generateEase: generateEase, getBlendMode: getBlendMode, createSteppedGradient: createSteppedGradient }; /** * An individual particle image. You shouldn't have to deal with these. */ class Particle extends Sprite { /** * @param emitter The emitter that controls this particle. */ constructor(emitter) { // start off the sprite with a blank texture, since we are going to replace it // later when the particle is initialized. super(); // initialize LinkedListChild props so they are included in underlying JS class definition this.prevChild = this.nextChild = null; this.emitter = emitter; this.config = {}; // particles should be centered this.anchor.x = this.anchor.y = 0.5; this.maxLife = 0; this.age = 0; this.agePercent = 0; this.oneOverLife = 0; this.next = null; this.prev = null; // save often used functions on the instance instead of the prototype for better speed this.init = this.init; this.kill = this.kill; } /** * Initializes the particle for use, based on the properties that have to * have been set already on the particle. */ init(maxLife) { this.maxLife = maxLife; // reset the age this.age = this.agePercent = 0; // reset the sprite props this.rotation = 0; this.position.x = this.position.y = 0; this.scale.x = this.scale.y = 1; this.tint = 0xffffff; this.alpha = 1; // save our lerp helper this.oneOverLife = 1 / this.maxLife; // ensure visibility this.visible = true; } /** * Kills the particle, removing it from the display list * and telling the emitter to recycle it. */ kill() { this.emitter.recycle(this); } /** * Destroys the particle, removing references and preventing future use. */ destroy() { if (this.parent) { this.parent.removeChild(this); } this.emitter = this.next = this.prev = null; super.destroy(); } } /** * Standard behavior order values, specifying when/how they are used. Other numeric values can be used, * but only the Spawn value will be handled in a special way. All other values will be sorted numerically. * Behaviors with the same value will not be given any specific sort order, as they are assumed to not * interfere with each other. */ var BehaviorOrder; (function (BehaviorOrder) { /** * Spawn - initial placement and/or rotation. This happens before rotation/translation due to * emitter rotation/position is applied. */ BehaviorOrder[BehaviorOrder["Spawn"] = 0] = "Spawn"; /** * Normal priority, for things that don't matter when they are applied. */ BehaviorOrder[BehaviorOrder["Normal"] = 2] = "Normal"; /** * Delayed priority, for things that need to read other values in order to act correctly. */ BehaviorOrder[BehaviorOrder["Late"] = 5] = "Late"; })(BehaviorOrder || (BehaviorOrder = {})); // get the shared ticker, only supports V5 and V6 with individual packages /** * @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. */ class Emitter { /** * @param particleParent The container to add the particles to. * @param particleImages A texture or array of textures to use * for the particles. Strings will be turned * into textures via Texture.fromImage(). * @param config A configuration object containing settings for the emitter. * @param config.emit If config.emit is explicitly passed as false, the * Emitter will start disabled. * @param config.autoUpdate If config.autoUpdate is explicitly passed as * true, the Emitter will automatically call * update via the PIXI shared ticker. */ constructor(particleParent, config) { this.initBehaviors = []; this.updateBehaviors = []; this.recycleBehaviors = []; // properties for individual particles this.minLifetime = 0; this.maxLifetime = 0; this.customEase = null; // properties for spawning particles this._frequency = 1; this.spawnChance = 1; this.maxParticles = 1000; this.emitterLifetime = -1; this.spawnPos = new Point(); this.particlesPerWave = 1; // emitter properties 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; // set the initial parent this.parent = particleParent; if (config) { this.init(config); } // save often used functions on the instance instead of the prototype for better speed this.recycle = this.recycle; this.update = this.update; this.rotate = this.rotate; this.updateSpawnPos = this.updateSpawnPos; this.updateOwnerPos = this.updateOwnerPos; } static registerBehavior(constructor) { Emitter.knownBehaviors[constructor.type] = constructor; } /** * 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. */ get frequency() { return this._frequency; } set frequency(value) { // do some error checking to prevent infinite loops 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. */ get parent() { return this._parent; } set parent(value) { this.cleanup(); this._parent = value; } /** * Sets up the emitter based on the config settings. * @param config A configuration object containing settings for the emitter. */ init(config) { if (!config) { return; } // clean up any existing particles this.cleanup(); // store the original config and particle images, in case we need to re-initialize // when the particle constructor is changed this._origConfig = config; // ///////////////////////// // Particle Properties // // ///////////////////////// // set up the lifetime this.minLifetime = config.lifetime.min; this.maxLifetime = config.lifetime.max; // use the custom ease if provided if (config.ease) { this.customEase = typeof config.ease === 'function' ? config.ease : generateEase(config.ease); } else { this.customEase = null; } // //////////////////////// // Emitter Properties // // //////////////////////// // reset spawn type specific settings this.particlesPerWave = 1; if (config.particlesPerWave && config.particlesPerWave > 1) { this.particlesPerWave = config.particlesPerWave; } // set the spawning frequency this.frequency = config.frequency; this.spawnChance = (typeof config.spawnChance === 'number' && config.spawnChance > 0) ? config.spawnChance : 1; // set the emitter lifetime this.emitterLifetime = config.emitterLifetime || -1; // set the max particles this.maxParticles = config.maxParticles > 0 ? config.maxParticles : 1000; // determine if we should add the particle at the back of the list or not this.addAtBack = !!config.addAtBack; // reset the emitter position and rotation variables 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); // previous emitter position is invalid and should not be used for interpolation this._prevPosIsValid = false; // start emitting this._spawnTimer = 0; this.emit = config.emit === undefined ? true : !!config.emit; this.autoUpdate = !!config.autoUpdate; // //////////////////////// // Behaviors // // //////////////////////// 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.order === BehaviorOrder.Spawn ? 1 : -1; } else if (b === PositionParticle) { return a.order === BehaviorOrder.Spawn ? -1 : 1; } return a.order - b.order; }); this.initBehaviors = behaviors.slice(); this.updateBehaviors = behaviors.filter((b) => b !== PositionParticle && b.updateParticle); this.recycleBehaviors = behaviors.filter((b) => b !== PositionParticle && b.recycleParticle); } /** * Gets the instantiated behavior of the specified type, if any. * @param type The behavior type to find. */ getBehavior(type) { return this.initBehaviors.find((b) => b instanceof Emitter.knownBehaviors[type]) || null; } /** * Fills the pool with the specified number of particles, so that they don't have to be instantiated later. * @param count The number of particles to create. */ fillPool(count) { for (; count > 0; --count) { const p = new Particle(this); p.next = this._poolFirst; this._poolFirst = p; } } /** * Recycles an individual particle. For internal use only. * @param particle The particle to recycle. * @param fromCleanup If this is being called to manually clean up all particles. * @internal */ 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; } // add to pool particle.prev = null; particle.next = this._poolFirst; this._poolFirst = particle; // remove child from display, or make it invisible if it is in a ParticleContainer if (particle.parent) { particle.parent.removeChild(particle); } // decrease count --this.particleCount; } /** * Sets the rotation of the emitter to a new value. This rotates the spawn position in addition * to particle direction. * @param newRot The new rotation, in degrees. */ rotate(newRot) { if (this.rotation === newRot) return; // caclulate the difference in rotation for rotating spawnPos const diff = newRot - this.rotation; this.rotation = newRot; // rotate spawnPos rotatePoint(diff, this.spawnPos); // mark the position as having changed this._posChanged = true; } /** * Changes the spawn position of the emitter. * @param x The new x value of the spawn position for the emitter. * @param y The new y value of the spawn position for the emitter. */ updateSpawnPos(x, y) { this._posChanged = true; this.spawnPos.x = x; this.spawnPos.y = y; } /** * Changes the position of the emitter's owner. You should call this if you are adding * particles to the world container that your emitter's owner is moving around in. * @param x The new x value of the emitter's owner. * @param y The new y value of the emitter's owner. */ 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. */ 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. */ get emit() { return this._emit; } 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. */ get autoUpdate() { return this._autoUpdate; } 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; } /** * Starts emitting particles, sets autoUpdate to true, and sets up the Emitter to destroy itself * when particle emission is complete. * @param callback Callback for when emission is complete (all particles have died off) */ playOnceAndDestroy(callback) { this.autoUpdate = true; this.emit = true; this._destroyWhenComplete = true; this._completeCallback = callback; } /** * Starts emitting particles and optionally calls a callback when particle emission is complete. * @param callback Callback for when emission is complete (all particles have died off) */ playOnce(callback) { this.emit = true; this._completeCallback = callback; } /** * Updates all particles spawned by this emitter and emits new ones. * @param delta Time elapsed since the previous frame, in __seconds__. */ update(delta) { if (this._autoUpdate) { delta = delta / settings.TARGET_FPMS / 1000; } // if we don't have a parent to add particles to, then don't do anything. // this also works as a isDestroyed check if (!this._parent) return; // == update existing particles == // update all particle lifetimes before turning them over to behaviors for (let particle = this._activeParticlesFirst, next; particle; particle = next) { // save next particle in case we recycle this one next = particle.next; // increase age particle.age += delta; // recycle particle if it is too old if (particle.age > particle.maxLife || particle.age < 0) { this.recycle(particle); } else { // determine our interpolation value let lerp = particle.age * particle.oneOverLife; // lifetime / maxLife; // global ease affects all interpolation calculations if (this.customEase) { if (this.customEase.length === 4) { // the t, b, c, d parameters that some tween libraries use // (time, initial value, end value, duration) lerp = this.customEase(lerp, 0, 1, 1); } else { // the simplified version that we like that takes // one parameter, time from 0-1. TweenJS eases provide this usage. lerp = this.customEase(lerp); } } // set age percent for all interpolation calculations particle.agePercent = lerp; // let each behavior run wild on the active particles 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 the previous position is valid, store these for later interpolation if (this._prevPosIsValid) { prevX = this._prevEmitterPos.x; prevY = this._prevEmitterPos.y; } // store current position of the emitter as local variables const curX = this.ownerPos.x + this.spawnPos.x; const curY = this.ownerPos.y + this.spawnPos.y; // spawn new particles if (this._emit) { // decrease spawn timer this._spawnTimer -= delta < 0 ? 0 : delta; // while _spawnTimer < 0, we have particles to spawn while (this._spawnTimer <= 0) { // determine if the emitter should stop spawning if (this._emitterLife >= 0) { this._emitterLife -= this._frequency; if (this._emitterLife <= 0) { this._spawnTimer = 0; this._emitterLife = 0; this.emit = false; break; } } // determine if we have hit the particle limit if (this.particleCount >= this.maxParticles) { this._spawnTimer += this._frequency; continue; } let emitPosX; let emitPosY; // If the position has changed and this isn't the first spawn, // interpolate the spawn position if (this._prevPosIsValid && this._posChanged) { // 1 - _spawnTimer / delta, but _spawnTimer is negative const lerp = 1 + (this._spawnTimer / delta); emitPosX = ((curX - prevX) * lerp) + prevX; emitPosY = ((curY - prevY) * lerp) + prevY; } // otherwise just set to the spawn position else { emitPosX = curX; emitPosY = curY; } let waveFirst = null; let waveLast = null; // create enough particles to fill the wave for (let len = Math.min(this.particlesPerWave, this.maxParticles - this.particleCount), i = 0; i < len; ++i) { // see if we actually spawn one if (this.spawnChance < 1 && Math.random() >= this.spawnChance) { continue; } // determine the particle lifetime let lifetime; if (this.minLifetime === this.maxLifetime) { lifetime = this.minLifetime; } else { lifetime = (Math.random() * (this.maxLifetime - this.minLifetime)) + this.minLifetime; } // only make the particle if it wouldn't immediately destroy itself if (-this._spawnTimer >= lifetime) { continue; } // create particle let p; if (this._poolFirst) { p = this._poolFirst; this._poolFirst = this._poolFirst.next; p.next = null; } else { p = new Particle(this); } // initialize particle p.init(lifetime); // add the particle to the display list if (this.addAtBack) { this._parent.addChildAt(p, 0); } else { this._parent.addChild(p); } // add particles to list of ones in this wave if (waveFirst) { waveLast.next = p; p.prev = waveLast; waveLast = p; } else { waveLast = waveFirst = p; } // increase our particle count ++this.particleCount; } if (waveFirst) { // add particle to list of active particles if (this._activeParticlesLast) { this._activeParticlesLast.next = waveFirst; waveFirst.prev = this._activeParticlesLast; this._activeParticlesLast = waveLast; } else { this._activeParticlesFirst = waveFirst; this._activeParticlesLast = waveLast; } // run behavior init on particles for (let i = 0; i < this.initBehaviors.length; ++i) { const behavior = this.initBehaviors[i]; // if we hit our special key, interrupt behaviors to apply // emitter position/rotation if (behavior === PositionParticle) { for (let particle = waveFirst, next; particle; particle = next) { // save next particle in case we recycle this one next = particle.next; // rotate the particle's position by the emitter's rotation if (this.rotation !== 0) { rotatePoint(this.rotation, particle.position); particle.rotation += this.rotation; } // offset by the emitter's position particle.position.x += emitPosX; particle.position.y += emitPosY; // also, just update the particle's age properties while we are looping through particle.age += delta; // determine our interpolation value let lerp = particle.age * particle.oneOverLife; // lifetime / maxLife; // global ease affects all interpolation calculations if (this.customEase) { if (this.customEase.length === 4) { // the t, b, c, d parameters that some tween libraries use // (time, initial value, end value, duration) lerp = this.customEase(lerp, 0, 1, 1); } else { // the simplified version that we like that takes // one parameter, time from 0-1. TweenJS eases provide this usage. lerp = this.customEase(lerp); } } // set age percent for all interpolation calculations particle.agePercent = lerp; } } else { behavior.initParticles(waveFirst); } } for (let particle = waveFirst, next; particle; particle = next) { // save next particle in case we recycle this one next = particle.next; // now update the particles by the time passed, so the particles are spread out properly for (let i = 0; i < this.updateBehaviors.length; ++i) { // we want a positive delta, because a negative delta messes things up if (this.updateBehaviors[i].updateParticle(particle, -this._spawnTimer)) { // bail if the particle got reycled this.recycle(particle); break; } } } } // increase timer and continue on to any other particles that need to be created this._spawnTimer += this._frequency; } } // if the position changed before this update, then keep track of that if (this._posChanged) { this._prevEmitterPos.x = curX; this._prevEmitterPos.y = curY; this._prevPosIsValid = true; this._posChanged = false; } // if we are all done and should destroy ourselves, take care of that 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. */ emitNow() { const emitPosX = this.ownerPos.x + this.spawnPos.x; const emitPosY = this.ownerPos.y + this.spawnPos.y; let waveFirst = null; let waveLast = null; // create enough particles to fill the wave for (let len = Math.min(this.particlesPerWave, this.maxParticles - this.particleCount), i = 0; i < len; ++i) { // see if we actually spawn one if (this.spawnChance < 1 && Math.random() >= this.spawnChance) { continue; } // create particle 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; } // initialize particle p.init(lifetime); // add the particle to the display list if (this.addAtBack) { this._parent.addChildAt(p, 0); } else { this._parent.addChild(p); } // add particles to list of ones in this wave if (waveFirst) { waveLast.next = p; p.prev = waveLast; waveLast = p; } else { waveLast = waveFirst = p; } // increase our particle count ++this.particleCount; } if (waveFirst) { // add particle to list of active particles if (this._activeParticlesLast) { this._activeParticlesLast.next = waveFirst; waveFirst.prev = this._activeParticlesLast; this._activeParticlesLast = waveLast; } else { this._activeParticlesFirst = waveFirst; this._activeParticlesLast = waveLast; } // run behavior init on particles for (let i = 0; i < this.initBehaviors.length; ++i) { const behavior = this.initBehaviors[i]; // if we hit our special key, interrupt behaviors to apply // emitter position/rotation if (behavior === PositionParticle) { for (let particle = waveFirst, next; particle; particle = next) { // save next particle in case we recycle this one next = particle.next; // rotate the particle's position by the emitter's rotation if (this.rotation !== 0) { rotatePoint(this.rotation, particle.position); particle.rotation += this.rotation; } // offset by the emitter's position particle.position.x += emitPosX; particle.position.y += emitPosY; } } else { behavior.initParticles(waveFirst); } } } } /** * Kills all active particles immediately. */ 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; } /** * Destroys the emitter and all of its particles. */ destroy() { // make sure we aren't still listening to any tickers this.autoUpdate = false; // puts all active particles in the pool, and removes them from the particle parent this.cleanup(); // wipe the pool clean let next; for (let particle = this._poolFirst; particle; particle = next) { // store next value so we don't lose it in our destroy call next = particle.next; particle.destroy(); } this._poolFirst = this._parent = this.spawnPos = this.ownerPos = this.customEase = this._completeCallback = null; } } Emitter.knownBehaviors = {}; /** * A rectangle for generating spawn positions. */ class Rectangle { constructor(config) { this.x = config.x; this.y = config.y; this.w = config.w; this.h = config.h; } getRandPos(particle) { // place the particle at a random point in the rectangle particle.x = (Math.random() * this.w) + this.x; particle.y = (Math.random() * this.h) + this.y; } } Rectangle.type = 'rect'; Rectangle.editorConfig = null; /** * A class for spawning particles in a circle or ring. * Can optionally apply rotation to particles so that they are aimed away from the center of the circle. */ class Torus { constructor(config) { this.x = config.x || 0; this.y = config.y || 0; this.radius = config.radius; this.innerRadius = config.innerRadius || 0; this.rotation = !!config.affectRotation; } getRandPos(particle) { // place the particle at a random radius in the ring if (this.innerRadius !== this.radius) { particle.x = (Math.random() * (this.radius - this.innerRadius)) + this.innerRadius; } else { particle.x = this.radius; } particle.y = 0; // rotate the point to a random angle in the circle const angle = Math.random() * Math.PI * 2; if (this.rotation) { particle.rotation += angle; } rotatePoint(angle, particle.position); // now add in the center of the torus particle.position.x += this.x; particle.position.y += this.y; } } Torus.type = 'torus'; Torus.editorConfig = null; /** * Chain of line segments for generating spawn positions. */ class PolygonalChain { /** * @param data Point data for polygon chains. Either a list of points for a single chain, or a list of chains. */ constructor(data) { this.segments = []; this.countingLengths = []; this.totalLength = 0; this.init(data); } /** * @param data Point data for polygon chains. Either a list of points for a single chain, or a list of chains. */ init(data) { // if data is not present, set up a segment of length 0 if (!data || !data.length) { this.segments.push({ p1: { x: 0, y: 0 }, p2: { x: 0, y: 0 }, l: 0 }); } else if (Array.isArray(data[0])) { // list of segment chains, each defined as a list of points for (let i = 0; i < data.length; ++i) { // loop through the chain, connecting points const chain = data[i]; let prevPoint = chain[0]; for (let j = 1; j < chain.length; ++j) { const second = chain[j]; this.segments.push({ p1: prevPoint, p2: second, l: 0 }); prevPoint = second; } } } else { let prevPoint = data[0]; // list of points for (let i = 1; i < data.length; ++i) { const second = data[i]; this.segments.push({ p1: prevPoint, p2: second, l: 0 }); prevPoint = second; } } // now go through our segments to calculate the lengths so that we // can set up a nice weighted random distribution for (let i = 0; i < this.segments.length; ++i) { const { p1, p2 } = this.segments[i]; const segLength = Math.sqrt(((p2.x - p1.x) * (p2.x - p1.x)) + ((p2.y - p1.y) * (p2.y - p1.y))); // save length so we can turn a random number into a 0-1 interpolation value later this.segments[i].l = segLength; this.totalLength += segLength; // keep track of the length so far, counting up this.countingLengths.push(this.totalLength); } } /** * Gets a random point in the chain. * @param out The point to store the selected position in. */ getRandPos(out) { // select a random spot in the length of the chain const rand = Math.random() * this.totalLength; let chosenSeg; let lerp; // if only one segment, it wins if (this.segments.length === 1) { chosenSeg = this.segments[0]; lerp = rand; } else { // otherwise, go through countingLengths until we have determined // which segment we chose for (let i = 0; i < this.countingLengths.length; ++i) { if (rand < this.countingLengths[i]) { chosenSeg = this.segments[i]; // set lerp equal to the length into that segment // (i.e. the remainder after subtracting all the segments before it) lerp = i === 0 ? rand : rand - this.countingLengths[i - 1]; break; } } } // divide lerp by the segment length, to result in a 0-1 number. lerp /= chosenSeg.l || 1; const { p1, p2 } = chosenSeg; // now calculate the position in the segment that the lerp value represents out.x = p1.x + (lerp * (p2.x - p1.x)); out.y = p1.y + (lerp * (p2.y - p1.y)); } } PolygonalChain.type = 'polygonalChain'; PolygonalChain.editorConfig = null; var index = { __proto__: null, Rectangle: Rectangle, Torus: Torus, PolygonalChain: PolygonalChain }; var Types = { __proto__: null }; class AccelerationBehavior { constructor(config) { var _a; // doesn't _really_ need to be late, but doing so ensures that we can override any // rotation behavior that is mistakenly added this.order = BehaviorOrder.Late; this.minStart = config.minStart; this.maxStart = config.maxStart; this.accel = config.accel; this.rotate = !!config.rotate; this.maxSpeed = (_a = config.maxSpeed) !== null && _a !== void 0 ? _a : 0; } initParticles(first) { let next = first; while (next) { const speed = (Math.random() * (this.maxStart - this.minStart)) + this.minStart; if (!next.config.velocity) { next.config.velocity = new Point(speed, 0); } else { next.config.velocity.set(speed, 0); } rotatePoint(next.rotation, next.config.velocity); next = next.next; } } updateParticle(particle, deltaSec) { const vel = particle.config.velocity; const oldVX = vel.x; const oldVY = vel.y; vel.x += this.accel.x * deltaSec; vel.y += this.accel.y * deltaSec; if (this.maxSpeed) { const currentSpeed = length(vel); // if we are going faster than we should, clamp at the max speed // DO NOT recalculate vector length if (currentSpeed > this.maxSpeed) { scaleBy(vel, this.maxSpeed / currentSpeed); } } // calculate position delta by the midpoint between our old velocity and our new velocity particle.x += (oldVX + vel.x) / 2 * deltaSec; particle.y += (oldVY + vel.y) / 2 * deltaSec; if (this.rotate) { particle.rotation = Math.atan2(vel.y, vel.x); } } } AccelerationBehavior.type = 'moveAcceleration'; AccelerationBehavior.editorConfig = null; function intValueSimple(lerp) { if (this.ease) lerp = this.ease(lerp); return ((this.first.next.value - this.first.value) * lerp) + this.first.value; } function intColorSimple(lerp) { if (this.ease) lerp = this.ease(lerp); const curVal = this.first.value; const nextVal = this.first.next.value; const r = ((nextVal.r - curVal.r) * lerp) + curVal.r; const g = ((nextVal.g - curVal.g) * lerp) + curVal.g; const b = ((nextVal.b - curVal.b) * lerp) + curVal.b; return combineRGBComponents(r, g, b); } function intValueComplex(lerp) { if (this.ease) lerp = this.ease(lerp); // make sure we are on the right segment let current = this.first; let next = current.next; while (lerp > next.time) { current = next; next = next.next; } // convert the lerp value to the segment range lerp = (lerp - current.time) / (next.time - current.time); return ((next.value - current.value) * lerp) + current.value; } function intColorComplex(lerp) { if (this.ease) lerp = this.ease(lerp); // make sure we are on the right segment let current = this.first; let next = current.next; while (lerp > next.time) { current = next; next = next.next; } // convert the lerp value to the segment range lerp = (lerp - current.time) / (next.time - current.time); const curVal = current.value; const nextVal = next.value; const r = ((nextVal.r - curVal.r) * lerp) + curVal.r; const g = ((nextVal.g - curVal.g) * lerp) + curVal.g; const b = ((nextVal.b - curVal.b) * lerp) + curVal.b; return combineRGBComponents(r, g, b); } function intValueStepped(lerp) { if (this.ease) lerp = this.ease(lerp); // make sure we are on the right segment let current = this.first; while (current.next && lerp > current.next.time) { current = current.next; } return current.value; } function intColorStepped(lerp) { if (this.ease) lerp = this.ease(lerp); // make sure we are on the right segment let current = this.first; while (current.next && lerp > current.next.time) { current = current.next; } const curVal = current.value; return combineRGBComponents(curVal.r, curVal.g, curVal.b); } /** * Singly linked list container for keeping track of interpolated properties for particles. * Each Particle will have one of these for each interpolated property. */ class PropertyList { /** * @param isColor If this list handles color values */ constructor(isColor = false) { this.first = null; this.isColor = !!isColor; this.interpolate = null; this.ease = null; } /** * Resets the list for use. * @param first The first node in the list. * @param first.isStepped If the values should be stepped instead of interpolated linearly. */ reset(first) { this.first = first; const isSimple = first.next && first.next.time >= 1; if (isSimple) { this.interpolate = this.isColor ? intColorSimple : intValueSimple; } else if (first.isStepped) { this.interpolate = this.isColor ? intColorStepped : intValueStepped; } else { this.interpolate = this.isColor ? intColorComplex : intValueComplex; } this.ease = this.first.ease; } } class AlphaBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.list = new PropertyList(false); this.list.reset(PropertyNode.createList(config.alpha)); } initParticles(first) { let next = first; while (next) { next.alpha = this.list.first.value; next = next.next; } } updateParticle(particle) { particle.alpha = this.list.interpolate(particle.agePercent); } } AlphaBehavior.type = 'alpha'; AlphaBehavior.editorConfig = null; class StaticAlphaBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.value = config.alpha; } initParticles(first) { let next = first; while (next) { next.alpha = this.value; next = next.next; } } } StaticAlphaBehavior.type = 'alphaStatic'; StaticAlphaBehavior.editorConfig = null; function getTextures(textures) { const outTextures = []; for (let j = 0; j < textures.length; ++j) { let tex = textures[j]; if (typeof tex === 'string') { outTextures.push(GetTextureFromString(tex)); } else if (tex instanceof Texture) { outTextures.push(tex); } // assume an object with extra data determining duplicate frame data else { let dupe = tex.count || 1; if (typeof tex.texture === 'string') { tex = GetTextureFromString(tex.texture); } else // if(tex.texture instanceof Texture) { tex = tex.texture; } for (; dupe > 0; --dupe) { outTextures.push(tex); } } } return outTextures; } class RandomAnimatedTextureBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.anims = []; for (let i = 0; i < config.anims.length; ++i) { const anim = config.anims[i]; const textures = getTextures(anim.textures); // eslint-disable-next-line no-nested-ternary const framerate = anim.framerate < 0 ? -1 : (anim.framerate > 0 ? anim.framerate : 60); const parsedAnim = { textures, duration: framerate > 0 ? textures.length / framerate : 0, framerate, loop: framerate > 0 ? !!anim.loop : false, }; this.anims.push(parsedAnim); } } initParticles(first) { let next = first; while (next) { const index = Math.floor(Math.random() * this.anims.length); const anim = next.config.anim = this.anims[index]; next.texture = anim.textures[0]; next.config.animElapsed = 0; // if anim should match particle life exactly if (anim.framerate === -1) { next.config.animDuration = next.maxLife; next.config.animFramerate = anim.textures.length / next.maxLife; } else { next.config.animDuration = anim.duration; next.config.animFramerate = anim.framerate; } next = next.next; } } updateParticle(particle, deltaSec) { const config = particle.config; const anim = config.anim; config.animElapsed += deltaSec; if (config.animElapsed >= config.animDuration) { // loop elapsed back around if (config.anim.loop) { config.animElapsed = config.animElapsed % config.animDuration; } // subtract a small amount to prevent attempting to go past the end of the animation else { config.animElapsed = config.animDuration - 0.000001; } } // add a very small number to the frame and then floor it to avoid // the frame being one short due to floating point errors. const frame = ((config.animElapsed * config.animFramerate) + 0.0000001) | 0; // in the very rare case that framerate * elapsed math ends up going past the end, use the last texture particle.texture = anim.textures[frame] || anim.textures[anim.textures.length - 1] || Texture.EMPTY; } } RandomAnimatedTextureBehavior.type = 'animatedRandom'; RandomAnimatedTextureBehavior.editorConfig = null; class SingleAnimatedTextureBehavior { constructor(config) { this.order = BehaviorOrder.Normal; const anim = config.anim; const textures = getTextures(anim.textures); // eslint-disable-next-line no-nested-ternary const framerate = anim.framerate < 0 ? -1 : (anim.framerate > 0 ? anim.framerate : 60); this.anim = { textures, duration: framerate > 0 ? textures.length / framerate : 0, framerate, loop: framerate > 0 ? !!anim.loop : false, }; } initParticles(first) { let next = first; const anim = this.anim; while (next) { next.texture = anim.textures[0]; next.config.animElapsed = 0; // if anim should match particle life exactly if (anim.framerate === -1) { next.config.animDuration = next.maxLife; next.config.animFramerate = anim.textures.length / next.maxLife; } else { next.config.animDuration = anim.duration; next.config.animFramerate = anim.framerate; } next = next.next; } } updateParticle(particle, deltaSec) { const anim = this.anim; const config = particle.config; config.animElapsed += deltaSec; if (config.animElapsed >= config.animDuration) { // loop elapsed back around if (config.anim.loop) { config.animElapsed = config.animElapsed % config.animDuration; } // subtract a small amount to prevent attempting to go past the end of the animation else { config.animElapsed = config.animDuration - 0.000001; } } // add a very small number to the frame and then floor it to avoid // the frame being one short due to floating point errors. const frame = ((config.animElapsed * config.animFramerate) + 0.0000001) | 0; // in the very rare case that framerate * elapsed math ends up going past the end, use the last texture particle.texture = anim.textures[frame] || anim.textures[anim.textures.length - 1] || Texture.EMPTY; } } SingleAnimatedTextureBehavior.type = 'animatedSingle'; SingleAnimatedTextureBehavior.editorConfig = null; class BlendModeBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.value = config.blendMode; } initParticles(first) { let next = first; while (next) { next.blendMode = getBlendMode(this.value); next = next.next; } } } BlendModeBehavior.type = 'blendMode'; BlendModeBehavior.editorConfig = null; class BurstSpawn { constructor(config) { this.order = BehaviorOrder.Spawn; this.spacing = config.spacing * DEG_TO_RADS; this.start = config.start * DEG_TO_RADS; this.distance = config.distance; } initParticles(first) { let count = 0; let next = first; while (next) { let angle; if (this.spacing) { angle = this.start + (this.spacing * count); } else { angle = Math.random() * Math.PI * 2; } next.rotation = angle; if (this.distance) { next.position.x = this.distance; rotatePoint(angle, next.position); } next = next.next; ++count; } } } BurstSpawn.type = 'spawnBurst'; BurstSpawn.editorConfig = null; class ColorBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.list = new PropertyList(true); this.list.reset(PropertyNode.createList(config.color)); } initParticles(first) { let next = first; const color = this.list.first.value; const tint = combineRGBComponents(color.r, color.g, color.b); while (next) { next.tint = tint; next = next.next; } } updateParticle(particle) { particle.tint = this.list.interpolate(particle.agePercent); } } ColorBehavior.type = 'color'; ColorBehavior.editorConfig = null; class StaticColorBehavior { constructor(config) { this.order = BehaviorOrder.Normal; let color = config.color; if (color.charAt(0) === '#') { color = color.substr(1); } else if (color.indexOf('0x') === 0) { color = color.substr(2); } this.value = parseInt(color, 16); } initParticles(first) { let next = first; while (next) { next.tint = this.value; next = next.next; } } } StaticColorBehavior.type = 'colorStatic'; StaticColorBehavior.editorConfig = null; class OrderedTextureBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.index = 0; this.textures = config.textures.map((tex) => (typeof tex === 'string' ? GetTextureFromString(tex) : tex)); } initParticles(first) { let next = first; while (next) { next.texture = this.textures[this.index]; if (++this.index >= this.textures.length) { this.index = 0; } next = next.next; } } } OrderedTextureBehavior.type = 'textureOrdered'; OrderedTextureBehavior.editorConfig = null; /** * A helper point for math things. * @hidden */ const helperPoint = new Point(); /** * A hand picked list of Math functions (and a couple properties) that are * allowable. They should be used without the preceding "Math." * @hidden */ const MATH_FUNCS = [ 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2', 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'atan2', 'cbrt', 'ceil', 'cos', 'cosh', 'exp', 'expm1', 'floor', 'fround', 'hypot', 'log', 'log1p', 'log10', 'log2', 'max', 'min', 'pow', 'random', 'round', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', ]; /** * create an actual regular expression object from the string * @hidden */ const WHITELISTER = new RegExp([ // Allow the 4 basic operations, parentheses and all numbers/decimals, as well // as 'x', for the variable usage. '[01234567890\\.\\*\\-\\+\\/\\(\\)x ,]', ].concat(MATH_FUNCS).join('|'), 'g'); /** * Parses a string into a function for path following. * This involves whitelisting the string for safety, inserting "Math." to math function * names, and using `new Function()` to generate a function. * @hidden * @param pathString The string to parse. * @return The path function - takes x, outputs y. */ function parsePath(pathString) { const matches = pathString.match(WHITELISTER); for (let i = matches.length - 1; i >= 0; --i) { if (MATH_FUNCS.indexOf(matches[i]) >= 0) { matches[i] = `Math.${matches[i]}`; } } pathString = matches.join(''); // eslint-disable-next-line no-new-func return new Function('x', `return ${pathString};`); } /** * A particle that follows a path defined by an algebraic expression, e.g. "sin(x)" or * "5x + 3". * To use this class, the particle config must have a "path" string in the * "extraData" parameter. This string should have "x" in it to represent movement (from the * speed settings of the particle). It may have numbers, parentheses, the four basic * operations, and the following Math functions or properties (without the preceding "Math."): * "pow", "sqrt", "abs", "floor", "round", "ceil", "E", "PI", "sin", "cos", "tan", "asin", * "acos", "atan", "atan2", "log". * The overall movement of the particle and the expression value become x and y positions for * the particle, respectively. The final position is rotated by the spawn rotation/angle of * the particle. * * Some example paths: * * "sin(x/10) * 20" // A sine wave path. * "cos(x/100) * 30" // Particles curve counterclockwise (for medium speed/low lifetime particles) * "pow(x/10, 2) / 2" // Particles curve clockwise (remember, +y is down). */ class PathBehavior { constructor(config) { var _a; // *MUST* happen after other behaviors do initialization so that we can read initial transformations this.order = BehaviorOrder.Late; if (config.path) { if (typeof config.path === 'function') { this.path = config.path; } else { try { this.path = parsePath(config.path); } catch (e) { this.path = null; } } } else { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type this.path = (x) => x; } this.list = new PropertyList(false); this.list.reset(PropertyNode.createList(config.speed)); this.minMult = (_a = config.minMult) !== null && _a !== void 0 ? _a : 1; } initParticles(first) { let next = first; while (next) { /* * The initial rotation in degrees of the particle, because the direction of the path * is based on that. */ next.config.initRotation = next.rotation; /* The initial position of the particle, as all path movement is added to that. */ if (!next.config.initPosition) { next.config.initPosition = new Point(next.x, next.y); } else { next.config.initPosition.copyFrom(next.position); } /* Total single directional movement, due to speed. */ next.config.movement = 0; // also do speed multiplier, since this includes basic speed movement const mult = (Math.random() * (1 - this.minMult)) + this.minMult; next.config.speedMult = mult; next = next.next; } } updateParticle(particle, deltaSec) { // increase linear movement based on speed const speed = this.list.interpolate(particle.agePercent) * particle.config.speedMult; particle.config.movement += speed * deltaSec; // set up the helper point for rotation helperPoint.x = particle.config.movement; helperPoint.y = this.path(helperPoint.x); rotatePoint(particle.config.initRotation, helperPoint); particle.position.x = particle.config.initPosition.x + helperPoint.x; particle.position.y = particle.config.initPosition.y + helperPoint.y; } } PathBehavior.type = 'movePath'; PathBehavior.editorConfig = null; class PointSpawn { constructor() { this.order = BehaviorOrder.Spawn; } // eslint-disable-next-line @typescript-eslint/no-unused-vars initParticles(_first) { // really just a no-op } } PointSpawn.type = 'spawnPoint'; PointSpawn.editorConfig = null; class RandomTextureBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.textures = config.textures.map((tex) => (typeof tex === 'string' ? GetTextureFromString(tex) : tex)); } initParticles(first) { let next = first; while (next) { const index = Math.floor(Math.random() * this.textures.length); next.texture = this.textures[index]; next = next.next; } } } RandomTextureBehavior.type = 'textureRandom'; RandomTextureBehavior.editorConfig = null; class RotationBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.minStart = config.minStart * DEG_TO_RADS; this.maxStart = config.maxStart * DEG_TO_RADS; this.minSpeed = config.minSpeed * DEG_TO_RADS; this.maxSpeed = config.maxSpeed * DEG_TO_RADS; this.accel = config.accel * DEG_TO_RADS; } initParticles(first) { let next = first; while (next) { if (this.minStart === this.maxStart) { next.rotation += this.maxStart; } else { next.rotation += (Math.random() * (this.maxStart - this.minStart)) + this.minStart; } next.config.rotSpeed = (Math.random() * (this.maxSpeed - this.minSpeed)) + this.minSpeed; next = next.next; } } updateParticle(particle, deltaSec) { if (this.accel) { const oldSpeed = particle.config.rotSpeed; particle.config.rotSpeed += this.accel * deltaSec; particle.rotation += (particle.config.rotSpeed + oldSpeed) / 2 * deltaSec; } else { particle.rotation += particle.config.rotSpeed * deltaSec; } } } RotationBehavior.type = 'rotation'; RotationBehavior.editorConfig = null; class StaticRotationBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.min = config.min * DEG_TO_RADS; this.max = config.max * DEG_TO_RADS; } initParticles(first) { let next = first; while (next) { if (this.min === this.max) { next.rotation += this.max; } else { next.rotation += (Math.random() * (this.max - this.min)) + this.min; } next = next.next; } } } StaticRotationBehavior.type = 'rotationStatic'; StaticRotationBehavior.editorConfig = null; class NoRotationBehavior { constructor() { this.order = BehaviorOrder.Late + 1; } initParticles(first) { let next = first; while (next) { next.rotation = 0; next = next.next; } } } NoRotationBehavior.type = 'noRotation'; NoRotationBehavior.editorConfig = null; class ScaleBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.list = new PropertyList(false); this.list.reset(PropertyNode.createList(config.scale)); this.minMult = config.minMult; } initParticles(first) { let next = first; while (next) { const mult = (Math.random() * (1 - this.minMult)) + this.minMult; next.config.scaleMult = mult; next.scale.x = next.scale.y = this.list.first.value * mult; next = next.next; } } updateParticle(particle) { particle.scale.x = particle.scale.y = this.list.interpolate(particle.agePercent) * particle.config.scaleMult; } } ScaleBehavior.type = 'scale'; ScaleBehavior.editorConfig = null; class StaticScaleBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.min = config.min; this.max = config.max; } initParticles(first) { let next = first; while (next) { const scale = (Math.random() * (this.max - this.min)) + this.min; next.scale.x = next.scale.y = scale; next = next.next; } } } StaticScaleBehavior.type = 'scaleStatic'; StaticScaleBehavior.editorConfig = null; class ShapeSpawn { constructor(config) { this.order = BehaviorOrder.Spawn; const ShapeClass = ShapeSpawn.shapes[config.type]; if (!ShapeClass) { throw new Error(`No shape found with type '${config.type}'`); } this.shape = new ShapeClass(config.data); } /** * Registers a shape to be used by the ShapeSpawn behavior. * @param constructor The shape class constructor to use, with a static `type` property to reference it by. * @param typeOverride An optional type override, primarily for registering a shape under multiple names. */ static registerShape(constructor, typeOverride) { ShapeSpawn.shapes[typeOverride || constructor.type] = constructor; } initParticles(first) { let next = first; while (next) { this.shape.getRandPos(next); next = next.next; } } } ShapeSpawn.type = 'spawnShape'; ShapeSpawn.editorConfig = null; /** * Dictionary of all registered shape classes. */ ShapeSpawn.shapes = {}; ShapeSpawn.registerShape(PolygonalChain); ShapeSpawn.registerShape(Rectangle); ShapeSpawn.registerShape(Torus); ShapeSpawn.registerShape(Torus, 'circle'); class SingleTextureBehavior { constructor(config) { this.order = BehaviorOrder.Normal; this.texture = typeof config.texture === 'string' ? GetTextureFromString(config.texture) : config.texture; } initParticles(first) { let next = first; while (next) { next.texture = this.texture; next = next.next; } } } SingleTextureBehavior.type = 'textureSingle'; SingleTextureBehavior.editorConfig = null; class SpeedBehavior { constructor(config) { var _a; this.order = BehaviorOrder.Late; this.list = new PropertyList(false); this.list.reset(PropertyNode.createList(config.speed)); this.minMult = (_a = config.minMult) !== null && _a !== void 0 ? _a : 1; } initParticles(first) { let next = first; while (next) { const mult = (Math.random() * (1 - this.minMult)) + this.minMult; next.config.speedMult = mult; if (!next.config.velocity) { next.config.velocity = new Point(this.list.first.value * mult, 0); } else { next.config.velocity.set(this.list.first.value * mult, 0); } rotatePoint(next.rotation, next.config.velocity); next = next.next; } } updateParticle(particle, deltaSec) { const speed = this.list.interpolate(particle.agePercent) * particle.config.speedMult; const vel = particle.config.velocity; normalize(vel); scaleBy(vel, speed); particle.x += vel.x * deltaSec; particle.y += vel.y * deltaSec; } } SpeedBehavior.type = 'moveSpeed'; SpeedBehavior.editorConfig = null; class StaticSpeedBehavior { constructor(config) { this.order = BehaviorOrder.Late; this.min = config.min; this.max = config.max; } initParticles(first) { let next = first; while (next) { const speed = (Math.random() * (this.max - this.min)) + this.min; if (!next.config.velocity) { next.config.velocity = new Point(speed, 0); } else { next.config.velocity.set(speed, 0); } rotatePoint(next.rotation, next.config.velocity); next = next.next; } } updateParticle(particle, deltaSec) { const velocity = particle.config.velocity; particle.x += velocity.x * deltaSec; particle.y += velocity.y * deltaSec; } } StaticSpeedBehavior.type = 'moveSpeedStatic'; StaticSpeedBehavior.editorConfig = null; // export support types for external use var index$1 = { __proto__: null, spawnShapes: index, editor: Types, get BehaviorOrder () { return BehaviorOrder; }, AccelerationBehavior: AccelerationBehavior, AlphaBehavior: AlphaBehavior, StaticAlphaBehavior: StaticAlphaBehavior, RandomAnimatedTextureBehavior: RandomAnimatedTextureBehavior, SingleAnimatedTextureBehavior: SingleAnimatedTextureBehavior, BlendModeBehavior: BlendModeBehavior, BurstSpawn: BurstSpawn, ColorBehavior: ColorBehavior, StaticColorBehavior: StaticColorBehavior, OrderedTextureBehavior: OrderedTextureBehavior, PathBehavior: PathBehavior, PointSpawn: PointSpawn, RandomTextureBehavior: RandomTextureBehavior, RotationBehavior: RotationBehavior, StaticRotationBehavior: StaticRotationBehavior, NoRotationBehavior: NoRotationBehavior, ScaleBehavior: ScaleBehavior, StaticScaleBehavior: StaticScaleBehavior, ShapeSpawn: ShapeSpawn, SingleTextureBehavior: SingleTextureBehavior, SpeedBehavior: SpeedBehavior, StaticSpeedBehavior: StaticSpeedBehavior }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function upgradeConfig(config, art) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; // just ensure we aren't given any V3 config data if ('behaviors' in config) { return config; } const out = { lifetime: config.lifetime, ease: config.ease, particlesPerWave: config.particlesPerWave, frequency: config.frequency, spawnChance: config.spawnChance, emitterLifetime: config.emitterLifetime, maxParticles: config.maxParticles, addAtBack: config.addAtBack, pos: config.pos, emit: config.emit, autoUpdate: config.autoUpdate, behaviors: [], }; // set up the alpha if (config.alpha) { if ('start' in config.alpha) { if (config.alpha.start === config.alpha.end) { if (config.alpha.start !== 1) { out.behaviors.push({ type: 'alphaStatic', config: { alpha: config.alpha.start }, }); } } else { const list = { list: [ { time: 0, value: config.alpha.start }, { time: 1, value: config.alpha.end }, ], }; out.behaviors.push({ type: 'alpha', config: { alpha: list }, }); } } else if (config.alpha.list.length === 1) { if (config.alpha.list[0].value !== 1) { out.behaviors.push({ type: 'alphaStatic', config: { alpha: config.alpha.list[0].value }, }); } } else { out.behaviors.push({ type: 'alpha', config: { alpha: config.alpha }, }); } } // acceleration movement if (config.acceleration && (config.acceleration.x || config.acceleration.y)) { let minStart; let maxStart; if ('start' in config.speed) { minStart = config.speed.start * ((_a = config.speed.minimumSpeedMultiplier) !== null && _a !== void 0 ? _a : 1); maxStart = config.speed.start; } else { minStart = config.speed.list[0].value * ((_b = config.minimumSpeedMultiplier) !== null && _b !== void 0 ? _b : 1); maxStart = config.speed.list[0].value; } out.behaviors.push({ type: 'moveAcceleration', config: { accel: config.acceleration, minStart, maxStart, rotate: !config.noRotation, maxSpeed: config.maxSpeed, }, }); } // path movement else if ((_c = config.extraData) === null || _c === void 0 ? void 0 : _c.path) { let list; let mult; if ('start' in config.speed) { mult = (_d = config.speed.minimumSpeedMultiplier) !== null && _d !== void 0 ? _d : 1; if (config.speed.start === config.speed.end) { list = { list: [{ time: 0, value: config.speed.start }], }; } else { list = { list: [ { time: 0, value: config.speed.start }, { time: 1, value: config.speed.end }, ], }; } } else { list = config.speed; mult = ((_e = config.minimumSpeedMultiplier) !== null && _e !== void 0 ? _e : 1); } out.behaviors.push({ type: 'movePath', config: { path: config.extraData.path, speed: list, minMult: mult, }, }); } // normal speed movement else { if ('start' in config.speed) { if (config.speed.start === config.speed.end) { out.behaviors.push({ type: 'moveSpeedStatic', config: { min: config.speed.start * ((_f = config.speed.minimumSpeedMultiplier) !== null && _f !== void 0 ? _f : 1), max: config.speed.start, }, }); } else { const list = { list: [ { time: 0, value: config.speed.start }, { time: 1, value: config.speed.end }, ], }; out.behaviors.push({ type: 'moveSpeed', config: { speed: list, minMult: config.speed.minimumSpeedMultiplier }, }); } } else if (config.speed.list.length === 1) { out.behaviors.push({ type: 'moveSpeedStatic', config: { min: config.speed.list[0].value * ((_g = config.minimumSpeedMultiplier) !== null && _g !== void 0 ? _g : 1), max: config.speed.list[0].value, }, }); } else { out.behaviors.push({ type: 'moveSpeed', config: { speed: config.speed, minMult: ((_h = config.minimumSpeedMultiplier) !== null && _h !== void 0 ? _h : 1) }, }); } } // scale if (config.scale) { if ('start' in config.scale) { const mult = (_j = config.scale.minimumScaleMultiplier) !== null && _j !== void 0 ? _j : 1; if (config.scale.start === config.scale.end) { out.behaviors.push({ type: 'scaleStatic', config: { min: config.scale.start * mult, max: config.scale.start, }, }); } else { const list = { list: [ { time: 0, value: config.scale.start }, { time: 1, value: config.scale.end }, ], }; out.behaviors.push({ type: 'scale', config: { scale: list, minMult: mult }, }); } } else if (config.scale.list.length === 1) { const mult = (_k = config.minimumScaleMultiplier) !== null && _k !== void 0 ? _k : 1; const scale = config.scale.list[0].value; out.behaviors.push({ type: 'scaleStatic', config: { min: scale * mult, max: scale }, }); } else { out.behaviors.push({ type: 'scale', config: { scale: config.scale, minMult: (_l = config.minimumScaleMultiplier) !== null && _l !== void 0 ? _l : 1 }, }); } } // color if (config.color) { if ('start' in config.color) { if (config.color.start === config.color.end) { if (config.color.start !== 'ffffff') { out.behaviors.push({ type: 'colorStatic', config: { color: config.color.start }, }); } } else { const list = { list: [ { time: 0, value: config.color.start }, { time: 1, value: config.color.end }, ], }; out.behaviors.push({ type: 'color', config: { color: list }, }); } } else if (config.color.list.length === 1) { if (config.color.list[0].value !== 'ffffff') { out.behaviors.push({ type: 'colorStatic', config: { color: config.color.list[0].value }, }); } } else { out.behaviors.push({ type: 'color', config: { color: config.color }, }); } } // rotation if (config.rotationAcceleration || ((_m = config.rotationSpeed) === null || _m === void 0 ? void 0 : _m.min) || config.rotationSpeed.max) { out.behaviors.push({ type: 'rotation', config: { accel: config.rotationAcceleration || 0, minSpeed: ((_o = config.rotationSpeed) === null || _o === void 0 ? void 0 : _o.min) || 0, maxSpeed: ((_p = config.rotationSpeed) === null || _p === void 0 ? void 0 : _p.max) || 0, minStart: ((_q = config.startRotation) === null || _q === void 0 ? void 0 : _q.min) || 0, maxStart: ((_r = config.startRotation) === null || _r === void 0 ? void 0 : _r.max) || 0, }, }); } else if (((_s = config.startRotation) === null || _s === void 0 ? void 0 : _s.min) || ((_t = config.startRotation) === null || _t === void 0 ? void 0 : _t.max)) { out.behaviors.push({ type: 'rotationStatic', config: { min: ((_u = config.startRotation) === null || _u === void 0 ? void 0 : _u.min) || 0, max: ((_v = config.startRotation) === null || _v === void 0 ? void 0 : _v.max) || 0, }, }); } if (config.noRotation) { out.behaviors.push({ type: 'noRotation', config: {}, }); } // blend mode if (config.blendMode && config.blendMode !== 'normal') { out.behaviors.push({ type: 'blendMode', config: { blendMode: config.blendMode, }, }); } // animated if (Array.isArray(art) && typeof art[0] !== 'string' && 'framerate' in art[0]) { for (let i = 0; i < art.length; ++i) { if (art[i].framerate === 'matchLife') { art[i].framerate = -1; } } out.behaviors.push({ type: 'animatedRandom', config: { anims: art, }, }); } else if (typeof art !== 'string' && 'framerate' in art) { if (art.framerate === 'matchLife') { art.framerate = -1; } out.behaviors.push({ type: 'animatedSingle', config: { anim: art, }, }); } // ordered art else if (config.orderedArt && Array.isArray(art)) { out.behaviors.push({ type: 'textureOrdered', config: { textures: art, }, }); } // random texture else if (Array.isArray(art)) { out.behaviors.push({ type: 'textureRandom', config: { textures: art, }, }); } // single texture else { out.behaviors.push({ type: 'textureSingle', config: { texture: art, }, }); } // spawn burst if (config.spawnType === 'burst') { out.behaviors.push({ type: 'spawnBurst', config: { start: config.angleStart || 0, spacing: config.particleSpacing, // older formats bursted from a single point distance: 0, }, }); } // spawn point else if (config.spawnType === 'point') { out.behaviors.push({ type: 'spawnPoint', config: {}, }); } // spawn shape else { let shape; if (config.spawnType === 'ring') { shape = { type: 'torus', data: { x: config.spawnCircle.x, y: config.spawnCircle.y, radius: config.spawnCircle.r, innerRadius: config.spawnCircle.minR, affectRotation: true, }, }; } else if (config.spawnType === 'circle') { shape = { type: 'torus', data: { x: config.spawnCircle.x, y: config.spawnCircle.y, radius: config.spawnCircle.r, innerRadius: 0, affectRotation: false, }, }; } else if (config.spawnType === 'rect') { shape = { type: 'rect', data: config.spawnRect, }; } else if (config.spawnType === 'polygonalChain') { shape = { type: 'polygonalChain', data: config.spawnPolygon, }; } out.behaviors.push({ type: 'spawnShape', config: shape, }); } return out; } /** * A semi-experimental Container that uses a doubly linked list to manage children instead of an * array. This means that adding/removing children often is not the same performance hit that * it would to be continually pushing/splicing. * However, this is primarily intended to be used for heavy particle usage, and may not handle * edge cases well if used as a complete Container replacement. */ class LinkedListContainer extends Container { constructor() { super(...arguments); this._firstChild = null; this._lastChild = null; this._childCount = 0; } get firstChild() { return this._firstChild; } get lastChild() { return this._lastChild; } get childCount() { return this._childCount; } addChild(...children) { // if there is only one argument we can bypass looping through the them if (children.length > 1) { // loop through the array and add all children for (let i = 0; i < children.length; i++) { // eslint-disable-next-line prefer-rest-params this.addChild(children[i]); } } else { const child = children[0]; // if the child has a parent then lets remove it as PixiJS objects can only exist in one place if (child.parent) { child.parent.removeChild(child); } child.parent = this; this.sortDirty = true; // ensure child transform will be recalculated child.transform._parentID = -1; // add to list if we have a list if (this._lastChild) { this._lastChild.nextChild = child; child.prevChild = this._lastChild; this._lastChild = child; } // otherwise initialize the list else { this._firstChild = this._lastChild = child; } // update child count ++this._childCount; // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(); this.emit('childAdded', child, this, this._childCount); child.emit('added', this); } return children[0]; } addChildAt(child, index) { if (index < 0 || index > this._childCount) { throw new Error(`addChildAt: The index ${index} supplied is out of bounds ${this._childCount}`); } if (child.parent) { child.parent.removeChild(child); } child.parent = this; this.sortDirty = true; // ensure child transform will be recalculated child.transform._parentID = -1; const c = child; // if no children, do basic initialization if (!this._firstChild) { this._firstChild = this._lastChild = c; } // add at beginning (back) else if (index === 0) { this._firstChild.prevChild = c; c.nextChild = this._firstChild; this._firstChild = c; } // add at end (front) else if (index === this._childCount) { this._lastChild.nextChild = c; c.prevChild = this._lastChild; this._lastChild = c; } // otherwise we have to start counting through the children to find the right one // - SLOW, only provided to fully support the possibility of use else { let i = 0; let target = this._firstChild; while (i < index) { target = target.nextChild; ++i; } // insert before the target that we found at the specified index target.prevChild.nextChild = c; c.prevChild = target.prevChild; c.nextChild = target; target.prevChild = c; } // update child count ++this._childCount; // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); this.emit('childAdded', child, this, index); return child; } /** * Adds a child to the container to be rendered below another child. * * @param child The child to add * @param relative - The current child to add the new child relative to. * @return The child that was added. */ addChildBelow(child, relative) { if (relative.parent !== this) { throw new Error(`addChildBelow: The relative target must be a child of this parent`); } if (child.parent) { child.parent.removeChild(child); } child.parent = this; this.sortDirty = true; // ensure child transform will be recalculated child.transform._parentID = -1; // insert before the target that we were given relative.prevChild.nextChild = child; child.prevChild = relative.prevChild; child.nextChild = relative; relative.prevChild = child; if (this._firstChild === relative) { this._firstChild = child; } // update child count ++this._childCount; // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(); this.emit('childAdded', child, this, this._childCount); child.emit('added', this); return child; } /** * Adds a child to the container to be rendered above another child. * * @param child The child to add * @param relative - The current child to add the new child relative to. * @return The child that was added. */ addChildAbove(child, relative) { if (relative.parent !== this) { throw new Error(`addChildBelow: The relative target must be a child of this parent`); } if (child.parent) { child.parent.removeChild(child); } child.parent = this; this.sortDirty = true; // ensure child transform will be recalculated child.transform._parentID = -1; // insert after the target that we were given relative.nextChild.prevChild = child; child.nextChild = relative.nextChild; child.prevChild = relative; relative.nextChild = child; if (this._lastChild === relative) { this._lastChild = child; } // update child count ++this._childCount; // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(); this.emit('childAdded', child, this, this._childCount); child.emit('added', this); return child; } swapChildren(child, child2) { if (child === child2 || child.parent !== this || child2.parent !== this) { return; } const { prevChild, nextChild } = child; child.prevChild = child2.prevChild; child.nextChild = child2.nextChild; child2.prevChild = prevChild; child2.nextChild = nextChild; if (this._firstChild === child) { this._firstChild = child2; } else if (this._firstChild === child2) { this._firstChild = child; } if (this._lastChild === child) { this._lastChild = child2; } else if (this._lastChild === child2) { this._lastChild = child; } this.onChildrenChange(); } getChildIndex(child) { let index = 0; let test = this._firstChild; while (test) { if (test === child) { break; } test = test.nextChild; ++index; } if (!test) { throw new Error('The supplied DisplayObject must be a child of the caller'); } return index; } setChildIndex(child, index) { if (index < 0 || index >= this._childCount) { throw new Error(`The index ${index} supplied is out of bounds ${this._childCount}`); } if (child.parent !== this) { throw new Error('The supplied DisplayObject must be a child of the caller'); } // remove child if (child.nextChild) { child.nextChild.prevChild = child.prevChild; } if (child.prevChild) { child.prevChild.nextChild = child.nextChild; } if (this._firstChild === child) { this._firstChild = child.nextChild; } if (this._lastChild === child) { this._lastChild = child.prevChild; } child.nextChild = null; child.prevChild = null; // do addChildAt if (!this._firstChild) { this._firstChild = this._lastChild = child; } else if (index === 0) { this._firstChild.prevChild = child; child.nextChild = this._firstChild; this._firstChild = child; } else if (index === this._childCount) { this._lastChild.nextChild = child; child.prevChild = this._lastChild; this._lastChild = child; } else { let i = 0; let target = this._firstChild; while (i < index) { target = target.nextChild; ++i; } target.prevChild.nextChild = child; child.prevChild = target.prevChild; child.nextChild = target; target.prevChild = child; } this.onChildrenChange(index); } removeChild(...children) { // if there is only one argument we can bypass looping through the them if (children.length > 1) { // loop through the arguments property and remove all children for (let i = 0; i < children.length; i++) { this.removeChild(children[i]); } } else { const child = children[0]; // bail if not actually our child if (child.parent !== this) return null; child.parent = null; // ensure child transform will be recalculated child.transform._parentID = -1; // swap out child references if (child.nextChild) { child.nextChild.prevChild = child.prevChild; } if (child.prevChild) { child.prevChild.nextChild = child.nextChild; } if (this._firstChild === child) { this._firstChild = child.nextChild; } if (this._lastChild === child) { this._lastChild = child.prevChild; } // clear sibling references child.nextChild = null; child.prevChild = null; // update child count --this._childCount; // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(); child.emit('removed', this); this.emit('childRemoved', child, this); } return children[0]; } getChildAt(index) { if (index < 0 || index >= this._childCount) { throw new Error(`getChildAt: Index (${index}) does not exist.`); } if (index === 0) { return this._firstChild; } // add at end (front) else if (index === this._childCount) { return this._lastChild; } // otherwise we have to start counting through the children to find the right one // - SLOW, only provided to fully support the possibility of use let i = 0; let target = this._firstChild; while (i < index) { target = target.nextChild; ++i; } return target; } removeChildAt(index) { const child = this.getChildAt(index); // ensure child transform will be recalculated.. child.parent = null; child.transform._parentID = -1; // swap out child references if (child.nextChild) { child.nextChild.prevChild = child.prevChild; } if (child.prevChild) { child.prevChild.nextChild = child.nextChild; } if (this._firstChild === child) { this._firstChild = child.nextChild; } if (this._lastChild === child) { this._lastChild = child.prevChild; } // clear sibling references child.nextChild = null; child.prevChild = null; // update child count --this._childCount; // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); this.emit('childRemoved', child, this, index); return child; } removeChildren(beginIndex = 0, endIndex = this._childCount) { const begin = beginIndex; const end = endIndex; const range = end - begin; if (range > 0 && range <= end) { const removed = []; let child = this._firstChild; for (let i = 0; i <= end && child; ++i, child = child.nextChild) { if (i >= begin) { removed.push(child); } } // child before removed section const prevChild = removed[0].prevChild; // child after removed section const nextChild = removed[removed.length - 1].nextChild; if (!nextChild) { // if we removed the last child, then the new last child is the one before // the removed section this._lastChild = prevChild; } else { // otherwise, stitch the child before the section to the child after nextChild.prevChild = prevChild; } if (!prevChild) { // if we removed the first child, then the new first child is the one after // the removed section this._firstChild = nextChild; } else { // otherwise stich the child after the section to the one before prevChild.nextChild = nextChild; } for (let i = 0; i < removed.length; ++i) { // clear parenting and sibling references for all removed children removed[i].parent = null; if (removed[i].transform) { removed[i].transform._parentID = -1; } removed[i].nextChild = null; removed[i].prevChild = null; } this._boundsID++; this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) { removed[i].emit('removed', this); this.emit('childRemoved', removed[i], this, i); } return removed; } else if (range === 0 && this._childCount === 0) { return []; } throw new RangeError('removeChildren: numeric values are outside the acceptable range.'); } /** * Updates the transform on all children of this container for rendering. * Copied from and overrides PixiJS v5 method (v4 method is identical) */ updateTransform() { this._boundsID++; this.transform.updateTransform(this.parent.transform); // TODO: check render flags, how to process stuff here this.worldAlpha = this.alpha * this.parent.worldAlpha; let child; let next; for (child = this._firstChild; child; child = next) { next = child.nextChild; if (child.visible) { child.updateTransform(); } } } /** * Recalculates the bounds of the container. * Copied from and overrides PixiJS v5 method (v4 method is identical) */ calculateBounds() { this._bounds.clear(); this._calculateBounds(); let child; let next; for (child = this._firstChild; child; child = next) { next = child.nextChild; if (!child.visible || !child.renderable) { continue; } child.calculateBounds(); // TODO: filter+mask, need to mask both somehow if (child._mask) { const maskObject = (child._mask.maskObject || child._mask); maskObject.calculateBounds(); this._bounds.addBoundsMask(child._bounds, maskObject._bounds); } else if (child.filterArea) { this._bounds.addBoundsArea(child._bounds, child.filterArea); } else { this._bounds.addBounds(child._bounds); } } this._bounds.updateID = this._boundsID; } /** * Retrieves the local bounds of the displayObject as a rectangle object. Copied from and overrides PixiJS v5 method */ getLocalBounds(rect, skipChildrenUpdate = false) { // skip Container's getLocalBounds, go directly to DisplayObject const result = DisplayObject.prototype.getLocalBounds.call(this, rect); if (!skipChildrenUpdate) { let child; let next; for (child = this._firstChild; child; child = next) { next = child.nextChild; if (child.visible) { child.updateTransform(); } } } return result; } /** * Renders the object using the WebGL renderer. Copied from and overrides PixiJS v5 method */ render(renderer) { // if the object is not visible or the alpha is 0 then no need to render this element if (!this.visible || this.worldAlpha <= 0 || !this.renderable) { return; } // do a quick check to see if this element has a mask or a filter. if (this._mask || (this.filters && this.filters.length)) { this.renderAdvanced(renderer); } else { this._render(renderer); let child; let next; // simple render children! for (child = this._firstChild; child; child = next) { next = child.nextChild; child.render(renderer); } } } /** * Render the object using the WebGL renderer and advanced features. Copied from and overrides PixiJS v5 method */ renderAdvanced(renderer) { renderer.batch.flush(); const filters = this.filters; const mask = this._mask; // _enabledFilters note: As of development, _enabledFilters is not documented in pixi.js // types but is in code of current release (5.2.4). // push filter first as we need to ensure the stencil buffer is correct for any masking if (filters) { if (!this._enabledFilters) { this._enabledFilters = []; } this._enabledFilters.length = 0; for (let i = 0; i < filters.length; i++) { if (filters[i].enabled) { this._enabledFilters.push(filters[i]); } } if (this._enabledFilters.length) { renderer.filter.push(this, this._enabledFilters); } } if (mask) { renderer.mask.push(this, this._mask); } // add this object to the batch, only rendered if it has a texture. this._render(renderer); let child; let next; // now loop through the children and make sure they get rendered for (child = this._firstChild; child; child = next) { next = child.nextChild; child.render(renderer); } renderer.batch.flush(); if (mask) { renderer.mask.pop(this); } if (filters && this._enabledFilters && this._enabledFilters.length) { renderer.filter.pop(); } } /** * Renders the object using the Canvas renderer. Copied from and overrides PixiJS Canvas mixin in V5 and V6. */ renderCanvas(renderer) { // if not visible or the alpha is 0 then no need to render this if (!this.visible || this.worldAlpha <= 0 || !this.renderable) { return; } if (this._mask) { renderer.maskManager.pushMask(this._mask); } this._renderCanvas(renderer); let child; let next; for (child = this._firstChild; child; child = next) { next = child.nextChild; child.renderCanvas(renderer); } if (this._mask) { renderer.maskManager.popMask(renderer); } } } Emitter.registerBehavior(AccelerationBehavior); Emitter.registerBehavior(AlphaBehavior); Emitter.registerBehavior(StaticAlphaBehavior); Emitter.registerBehavior(RandomAnimatedTextureBehavior); Emitter.registerBehavior(SingleAnimatedTextureBehavior); Emitter.registerBehavior(BlendModeBehavior); Emitter.registerBehavior(BurstSpawn); Emitter.registerBehavior(ColorBehavior); Emitter.registerBehavior(StaticColorBehavior); Emitter.registerBehavior(OrderedTextureBehavior); Emitter.registerBehavior(PathBehavior); Emitter.registerBehavior(PointSpawn); Emitter.registerBehavior(RandomTextureBehavior); Emitter.registerBehavior(RotationBehavior); Emitter.registerBehavior(StaticRotationBehavior); Emitter.registerBehavior(NoRotationBehavior); Emitter.registerBehavior(ScaleBehavior); Emitter.registerBehavior(StaticScaleBehavior); Emitter.registerBehavior(ShapeSpawn); Emitter.registerBehavior(SingleTextureBehavior); Emitter.registerBehavior(SpeedBehavior); Emitter.registerBehavior(StaticSpeedBehavior); export { Emitter, LinkedListContainer, Particle, ParticleUtils, PropertyList, PropertyNode, index$1 as behaviors, upgradeConfig }; //# sourceMappingURL=pixi-particles.es.js.map