/*!
 * @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
