3361 lines
122 KiB
JavaScript
3361 lines
122 KiB
JavaScript
/*!
|
|
* @pixi/particle-emitter - v4.3.1
|
|
* Compiled Wed, 18 Aug 2021 13:35:05 UTC
|
|
*
|
|
* @pixi/particle-emitter is licensed under the MIT License.
|
|
* http://www.opensource.org/licenses/mit-license
|
|
*/
|
|
import { Point } from '@pixi/math';
|
|
import { Texture } from '@pixi/core';
|
|
import { BLEND_MODES } from '@pixi/constants';
|
|
import { Sprite } from '@pixi/sprite';
|
|
import { settings } from '@pixi/settings';
|
|
import { Ticker } from '@pixi/ticker';
|
|
import { Container, DisplayObject } from '@pixi/display';
|
|
|
|
/**
|
|
* 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;
|
|
|
|
PolygonalChain.editorConfig = {
|
|
type: 'list',
|
|
name: '',
|
|
title: 'PolygonalChain',
|
|
description: 'A series of lines along which particles are spawned randomly.',
|
|
entryType: {
|
|
type: 'list',
|
|
name: '',
|
|
title: 'Line',
|
|
description: 'A series of points describing connected line segments',
|
|
entryType: {
|
|
type: 'point',
|
|
name: '',
|
|
title: 'Point',
|
|
description: 'An individual point in a line segment',
|
|
default: { x: 0, y: 0 },
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
|
|
Rectangle.editorConfig = {
|
|
type: 'object',
|
|
name: '',
|
|
title: 'Rectangle',
|
|
description: 'A rectangular shape which particles are spawned inside randomly.',
|
|
props: [
|
|
{
|
|
type: 'number',
|
|
name: 'x',
|
|
title: 'X',
|
|
description: 'The position of the left edge of the rectangle.',
|
|
default: 0,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'y',
|
|
title: 'Y',
|
|
description: 'The position of the top edge of the rectangle.',
|
|
default: 0,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'w',
|
|
title: 'Width',
|
|
description: 'The width of the rectangle.',
|
|
default: 10,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'h',
|
|
title: 'Height',
|
|
description: 'The height of the rectangle.',
|
|
default: 10,
|
|
},
|
|
],
|
|
};
|
|
|
|
Rectangle.editorConfig = {
|
|
type: 'object',
|
|
name: '',
|
|
title: 'Torus',
|
|
description: 'A circle or ring shape which particles are spawned inside randomly.',
|
|
props: [
|
|
{
|
|
type: 'number',
|
|
name: 'x',
|
|
title: 'Center X',
|
|
description: 'The center x position of the circle.',
|
|
default: 0,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'y',
|
|
title: 'Center Y',
|
|
description: 'The center y position of the circle.',
|
|
default: 0,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'radius',
|
|
title: 'Radius',
|
|
description: 'The outer radius of the circle.',
|
|
default: 10,
|
|
min: 0,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'innerRadius',
|
|
title: 'Inner Radius',
|
|
description: 'The inner radius of the circle. Values greater than 0 make it into a torus/ring.',
|
|
default: 0,
|
|
min: 0,
|
|
},
|
|
{
|
|
type: 'boolean',
|
|
name: 'rotation',
|
|
title: 'Apply Rotation',
|
|
description: 'If particles should be rotated to face/move away from the center of the circle.',
|
|
default: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
/**
|
|
* 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 : ParticleUtils.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' ? ParticleUtils.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' ? ParticleUtils.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' ? ParticleUtils.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' ? ParticleUtils.hexToRGB(data.end) : data.end, 1);
|
|
}
|
|
return start;
|
|
}
|
|
}
|
|
|
|
// get Texture.from(), only supports V5 and V6 with individual packages
|
|
/**
|
|
* @hidden
|
|
*/
|
|
const TextureFromString = Texture.from;
|
|
function GetTextureFromString(s) {
|
|
return TextureFromString(s);
|
|
}
|
|
/**
|
|
* Contains helper functions for particles and emitters to use.
|
|
*/
|
|
var ParticleUtils;
|
|
(function (ParticleUtils) {
|
|
/**
|
|
* If errors and warnings should be logged within the library.
|
|
*/
|
|
ParticleUtils.verbose = false;
|
|
ParticleUtils.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;
|
|
}
|
|
ParticleUtils.rotatePoint = rotatePoint;
|
|
/**
|
|
* 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;
|
|
}
|
|
ParticleUtils.combineRGBComponents = combineRGBComponents;
|
|
/**
|
|
* Reduces the point to a length of 1.
|
|
* @param point The point to normalize
|
|
*/
|
|
function normalize(point) {
|
|
const oneOverLen = 1 / ParticleUtils.length(point);
|
|
point.x *= oneOverLen;
|
|
point.y *= oneOverLen;
|
|
}
|
|
ParticleUtils.normalize = normalize;
|
|
/**
|
|
* 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;
|
|
}
|
|
ParticleUtils.scaleBy = scaleBy;
|
|
/**
|
|
* 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));
|
|
}
|
|
ParticleUtils.length = length;
|
|
/**
|
|
* 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;
|
|
}
|
|
ParticleUtils.hexToRGB = hexToRGB;
|
|
/**
|
|
* 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)))));
|
|
};
|
|
}
|
|
ParticleUtils.generateEase = generateEase;
|
|
/**
|
|
* 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();
|
|
while (name.indexOf(' ') >= 0) {
|
|
name = name.replace(' ', '_');
|
|
}
|
|
return BLEND_MODES[name] || BLEND_MODES.NORMAL;
|
|
}
|
|
ParticleUtils.getBlendMode = getBlendMode;
|
|
/**
|
|
* 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(ParticleUtils.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 = ParticleUtils.hexToRGB(current.value);
|
|
const nextVal = ParticleUtils.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;
|
|
}
|
|
ParticleUtils.createSteppedGradient = createSteppedGradient;
|
|
})(ParticleUtils || (ParticleUtils = {}));
|
|
|
|
/**
|
|
* 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 = {}));
|
|
|
|
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);
|
|
}
|
|
ParticleUtils.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 = ParticleUtils.length(vel);
|
|
// if we are going faster than we should, clamp at the max speed
|
|
// DO NOT recalculate vector length
|
|
if (currentSpeed > this.maxSpeed) {
|
|
ParticleUtils.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;
|
|
|
|
AccelerationBehavior.editorConfig = {
|
|
category: 'movement',
|
|
title: 'Acceleration',
|
|
props: [
|
|
{
|
|
type: 'number',
|
|
name: 'minStart',
|
|
title: 'Minimum Start Speed',
|
|
description: 'Minimum speed when initializing the particle.',
|
|
default: 100,
|
|
min: 0,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'maxStart',
|
|
title: 'Maximum Start Speed',
|
|
description: 'Maximum speed when initializing the particle.',
|
|
default: 100,
|
|
min: 0,
|
|
},
|
|
{
|
|
type: 'point',
|
|
name: 'accel',
|
|
title: 'Acceleration',
|
|
description: 'Constant acceleration, in the coordinate space of the particle parent.',
|
|
default: { x: 0, y: 50 },
|
|
},
|
|
{
|
|
type: 'boolean',
|
|
name: 'rotate',
|
|
title: 'Rotate with Movement',
|
|
description: 'Rotate the particle with its direction of movement. '
|
|
+ 'While initial movement direction reacts to rotation settings, this overrides any dynamic rotation.',
|
|
default: true,
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'maxSpeed',
|
|
title: 'Maximum Speed',
|
|
description: 'Maximum linear speed. 0 is unlimited.',
|
|
default: 0,
|
|
min: 0,
|
|
},
|
|
],
|
|
};
|
|
|
|
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 ParticleUtils.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 ParticleUtils.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 ParticleUtils.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;
|
|
|
|
AlphaBehavior.editorConfig = {
|
|
category: 'alpha',
|
|
title: 'Alpha (interpolated)',
|
|
props: [
|
|
{
|
|
type: 'numberList',
|
|
name: 'alpha',
|
|
title: 'Alpha',
|
|
description: 'Transparency of the particles from 0 (transparent) to 1 (opaque)',
|
|
default: 1,
|
|
min: 0,
|
|
max: 1,
|
|
},
|
|
],
|
|
};
|
|
StaticAlphaBehavior.editorConfig = {
|
|
category: 'alpha',
|
|
title: 'Alpha (static)',
|
|
props: [
|
|
{
|
|
type: 'number',
|
|
name: 'alpha',
|
|
title: 'Alpha',
|
|
description: 'Transparency of the particles from 0 (transparent) to 1 (opaque)',
|
|
default: 1,
|
|
min: 0,
|
|
max: 1,
|
|
},
|
|
],
|
|
};
|
|
|
|
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;
|
|
|
|
const AnimatedArt = {
|
|
type: 'object',
|
|
name: 'anim',
|
|
title: 'Animated Particle Art',
|
|
description: 'An individual particle animation',
|
|
props: [
|
|
{
|
|
type: 'number',
|
|
name: 'framerate',
|
|
title: 'Framerate',
|
|
description: 'Animation framerate. A value of -1 means to match the lifetime of the particle.',
|
|
default: 30,
|
|
min: -1,
|
|
},
|
|
{
|
|
type: 'boolean',
|
|
name: 'loop',
|
|
title: 'Loop',
|
|
description: 'If the animation loops.',
|
|
default: false,
|
|
},
|
|
{
|
|
type: 'list',
|
|
name: 'textures',
|
|
title: 'Frames',
|
|
description: 'Animation frames.',
|
|
entryType: {
|
|
type: 'object',
|
|
name: '',
|
|
title: 'Frame',
|
|
description: 'A single frame of the animation',
|
|
props: [
|
|
{
|
|
type: 'image',
|
|
name: 'texture',
|
|
title: 'Texture',
|
|
description: 'The texture for this frame',
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'count',
|
|
title: 'Count',
|
|
description: 'How many frames to hold this frame.',
|
|
default: 1,
|
|
min: 1,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
RandomAnimatedTextureBehavior.editorConfig = {
|
|
category: 'art',
|
|
title: 'Animated Texture (random)',
|
|
props: [
|
|
{
|
|
type: 'list',
|
|
name: 'anims',
|
|
title: 'Particle Animations',
|
|
description: 'Animation configuration to use for each particle, randomly chosen from the list.',
|
|
entryType: AnimatedArt,
|
|
},
|
|
],
|
|
};
|
|
SingleAnimatedTextureBehavior.editorConfig = {
|
|
category: 'art',
|
|
title: 'Animated Texture (single)',
|
|
props: [
|
|
AnimatedArt
|
|
],
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
ParticleUtils.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;
|
|
|
|
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');
|
|
|
|
ShapeSpawn.editorConfig = {
|
|
category: 'spawn',
|
|
title: 'Shape',
|
|
props: [
|
|
{
|
|
type: 'subconfig',
|
|
name: 'type',
|
|
title: 'Shape',
|
|
description: 'The shape to use for picking spawn locations.',
|
|
dictionaryProp: 'shapes',
|
|
configName: 'data',
|
|
}
|
|
],
|
|
};
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
|
|
// 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 : ParticleUtils.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
|
|
ParticleUtils.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) {
|
|
ParticleUtils.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) {
|
|
ParticleUtils.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 = {};
|
|
|
|
var index = {
|
|
__proto__: null,
|
|
Rectangle: Rectangle,
|
|
Torus: Torus,
|
|
PolygonalChain: PolygonalChain
|
|
};
|
|
|
|
var Types = {
|
|
__proto__: null
|
|
};
|
|
|
|
class BlendModeBehavior {
|
|
constructor(config) {
|
|
this.order = BehaviorOrder.Normal;
|
|
this.value = config.blendMode;
|
|
}
|
|
initParticles(first) {
|
|
let next = first;
|
|
while (next) {
|
|
next.blendMode = ParticleUtils.getBlendMode(this.value);
|
|
next = next.next;
|
|
}
|
|
}
|
|
}
|
|
BlendModeBehavior.type = 'blendMode';
|
|
BlendModeBehavior.editorConfig = null;
|
|
|
|
class BurstSpawn {
|
|
constructor(config) {
|
|
this.order = BehaviorOrder.Spawn;
|
|
this.spacing = config.spacing * ParticleUtils.DEG_TO_RADS;
|
|
this.start = config.start * ParticleUtils.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;
|
|
ParticleUtils.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 = ParticleUtils.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) {
|
|
if (ParticleUtils.verbose) {
|
|
console.error('PathParticle: error in parsing path expression');
|
|
}
|
|
this.path = null;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (ParticleUtils.verbose) {
|
|
console.error('PathParticle requires a path string in extraData!');
|
|
}
|
|
// 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);
|
|
ParticleUtils.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 * ParticleUtils.DEG_TO_RADS;
|
|
this.maxStart = config.maxStart * ParticleUtils.DEG_TO_RADS;
|
|
this.minSpeed = config.minSpeed * ParticleUtils.DEG_TO_RADS;
|
|
this.maxSpeed = config.maxSpeed * ParticleUtils.DEG_TO_RADS;
|
|
this.accel = config.accel * ParticleUtils.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 * ParticleUtils.DEG_TO_RADS;
|
|
this.max = config.max * ParticleUtils.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 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);
|
|
}
|
|
ParticleUtils.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;
|
|
ParticleUtils.normalize(vel);
|
|
ParticleUtils.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);
|
|
}
|
|
ParticleUtils.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, GetTextureFromString, LinkedListContainer, Particle, ParticleUtils, PropertyList, PropertyNode, index$1 as behaviors, upgradeConfig };
|
|
//# sourceMappingURL=pixi-particles.es-editor.js.map
|