UNPKG

@urso/revolt-fx

Version:

Particle and Effect System for Pixi.js

596 lines 22.5 kB
/// <reference types="pixi.js" /> var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import * as PIXI from "pixi.js"; import { ComponentType } from "./ComponentType"; import { EffectSequence } from "./EffectSequence"; import { EffectSequenceComponentType } from "./EffectSequenceComponentType"; import { MovieClip } from "./MovieClip"; import { Particle } from "./Particle"; import { ParticleEmitter } from "./ParticleEmitter"; import { Sanitizer } from "./Sanitizer"; import { Sprite } from "./Sprite"; import { BoxEmitterCore } from "./core/BoxEmitterCore"; import { CircleEmitterCore } from "./core/CircleEmitterCore"; import { RingEmitterCore } from "./core/RingEmitterCore"; import deepClone from "./util/DeepClone"; import { LinkedList } from "./util/LinkedList"; export class FX { constructor() { this.useBlendModes = true; this.particleCount = 0; this.emitterCount = 0; this.effectSequenceCount = 0; this.maxParticles = 5000; this.particleFac = 1; this._active = false; this._effects = new LinkedList(); this.__containers = {}; this.clearCache(); this.start(); } // ********************************************************************************************* // * Public * // ********************************************************************************************* /** * Starts the process. * * @param {} - No parameters. * @return {} - No return value. */ start() { this._active = true; this._timeElapsed = Date.now(); } /** * Pauses the execution of the function. * * @param {} * @return {} */ pause() { this._active = false; } /** * Updates the state of the object based on the elapsed time. * * @param {number} delta - The time delta to update by. Default is 1. */ update(delta = 1) { if (!this.active) return; const t = Date.now(); let dt = (t - this._timeElapsed) * 0.001; dt *= delta; const list = this._effects; let node = list.first; let next; while (node) { next = node.next; node.update(dt); node = next; } this._timeElapsed = t; } /** * Clears the cache by resetting all cache objects to empty values. * * @param {} * @return {} */ clearCache() { this._cache = { particles: [], mcs: [], sprites: [], effectSequences: [], emitters: [], cores: {} }; this._settingsCache = { mcs: {}, sprites: {}, emitters: {}, effectSequences: {} }; this._nameMaps = { emitters: {}, effectSequences: {} }; } /** * Sets the value of the floorY property for all emitters in the settings cache. * * @param {number} value - The new value for the floorY property. */ setFloorY(value) { const s = this._settingsCache.emitters; for (let n in s) { s[n].floorY = value; } } /** * Disposes of all the effects in the list and clears the cache. */ dispose() { let list = this._effects; let node = list.first; while (node) { node.dispose(); node = node.next; } list.clear(); this.clearCache(); } /** * Loads the bundle files and returns a promise that resolves to the parsed sprite sheet result. * * @param {string} bundleSettingsUrl - The URL of the bundle settings. * @param {string} spritesheetUrl - The URL of the sprite sheet. * @param {string} spritesheetFilter - The filter for the sprite sheet. Default is an empty string. * @param {string[]} additionalAssets - An array of additional asset URLs. Default is an empty array. * @return {Promise<IParseSpriteSheetResult>} A promise that resolves to the parsed sprite sheet result. */ loadBundleFiles(bundleSettingsUrl, spritesheetUrl, spritesheetFilter = '', additionalAssets = []) { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { const data = { 'rfx_spritesheet': spritesheetUrl, 'rfx_bundleSettings': bundleSettingsUrl, }; for (var i in additionalAssets) { data[i] = additionalAssets[i]; } PIXI.Assets.addBundle('rfx_assets', data); const assets = yield PIXI.Assets.loadBundle('rfx_assets'); resolve(this.initBundle(assets.rfx_bundleSettings)); })); } /** * Initializes the bundle with the given settings and optionally clears the cache. * * @param {any} bundleSettings - The settings for the bundle. * @param {boolean} clearCache - Whether to clear the cache or not. Optional, default is false. * @returns {IParseSpriteSheetResult} The result of parsing the sprite sheet. */ initBundle(bundleSettings, clearCache) { if (bundleSettings.__h !== FX._bundleHash) { throw new Error('Invalid settings file.'); } if (bundleSettings.__v != FX.settingsVersion) { throw new Error('Settings version mismatch.'); } Sanitizer.sanitizeBundle(bundleSettings); if (clearCache) { this.clearCache(); } for (let n in bundleSettings.emitters) { const preset = bundleSettings.emitters[n]; this.addParticleEmitter(preset.id, preset); } for (let n in bundleSettings.sequences) { const preset = bundleSettings.sequences[n]; this.addEffectSequence(preset.id, preset); } this.useBlendModes = bundleSettings.useBlendModes; this.maxParticles = bundleSettings.maxParticles; return this.parseTextureCache(bundleSettings.spritesheetFilter); } /** * Adds a particle emitter to the FX object. * * @param {string} componentId - The unique identifier for the emitter component. * @param {IEmitterSettings} settings - The settings for the emitter. * @throws {Error} Throws an error if the componentId already exists. * @return {FX} Returns the FX object for chaining. */ addParticleEmitter(componentId, settings) { if (this._settingsCache.emitters[componentId]) throw new Error(`ComponentId '${componentId}' already exists.`); this._settingsCache.emitters[componentId] = settings; this._nameMaps.emitters[settings.name] = settings; return this; } /** * Adds an effect sequence to the component with the specified ID. * * @param {string} componentId - The ID of the component. * @param {IEffectSequenceSettings} settings - The settings for the effect sequence. * @throws {Error} If a component with the same ID already exists. * @return {FX} The current instance of the FX class. */ addEffectSequence(componentId, settings) { if (this._settingsCache.effectSequences[componentId]) throw new Error(`ComponentId '${componentId}' already exists.`); this._settingsCache.effectSequences[componentId] = settings; this._nameMaps.effectSequences[settings.name] = settings; return this; } /** * Initializes a sprite with the specified component ID and settings. * * @param {string} componentId - The ID of the component. * @param {ISpriteSettings} settings - The settings for the sprite. * @throws {Error} Throws an error if the component ID already exists. * @returns {FX} Returns the current instance of the FX class. */ initSprite(componentId, settings) { if (this._settingsCache.sprites[componentId]) throw new Error(`ComponentId '${componentId}' already exists.`); this._settingsCache.sprites[componentId] = settings; return this; } /** * Initializes a movie clip with the specified component ID and settings. * * @param {string} componentId - The unique identifier for the movie clip component. * @param {IMovieClipSettings} settings - The settings for the movie clip. * @return {FX} The instance of the FX class. */ initMovieClip(componentId, settings) { if (this._settingsCache.mcs[componentId]) throw new Error(`ComponentId '${componentId}' already exists.`); this._settingsCache.mcs[componentId] = settings; return this; } /** * Retrieves the movie clips from the settings cache. * * @return {Object} An object containing movie clip settings. */ getMovieClips() { return this._settingsCache.mcs; } /** * Retrieves the sprites from the settings cache. * * @return {Object} An object containing sprite settings. */ getSprites() { return this._settingsCache.sprites; } /** * Adds a container to the __containers object with the specified key. * * @param {string} key - The key used to identify the container in the __containers object. * @param {PIXI.Container} container - The container to be added. */ addContainer(key, container) { this.__containers[key] = container; } /** * Retrieves the EffectSequence object with the specified name. * * @param {string} name - The name of the EffectSequence to retrieve. * @return {EffectSequence} - The EffectSequence object with the specified name. */ getEffectSequence(name, cloneSettings = false) { const settings = this._nameMaps.effectSequences[name]; if (!settings) throw new Error(`Settings not defined for '${name}'`); return this.getEffectSequenceById(settings.id, cloneSettings); } /** * Retrieves an EffectSequence object by its component ID. * * @param {string} componentId - The ID of the component. * @return {EffectSequence} The retrieved EffectSequence object. */ getEffectSequenceById(componentId, cloneSettings = false) { const pool = this._cache.effectSequences; let effectSequence; let settings = this._settingsCache.effectSequences[componentId]; if (!settings) throw new Error(`Settings not defined for '${componentId}'`); if (pool.length == 0) { effectSequence = new EffectSequence(componentId); effectSequence.__fx = this; } else { effectSequence = pool.pop(); } if (cloneSettings) { settings = deepClone(settings); settings.__isClone = true; } effectSequence.__applySettings(settings); return effectSequence; } /** * Retrieves a particle emitter by its name. * * @param {string} name - The name of the particle emitter. * @param {boolean} autoRecycleOnComplete - (Optional) Specifies whether the emitter should auto recycle when complete. Default is true. * @param {boolean} cloneSettings - (Optional) Specifies whether the emitter settings should be cloned. Default is false. * @return {ParticleEmitter} The particle emitter with the specified name. */ getParticleEmitter(name, autoRecycleOnComplete = true, cloneSettings = false) { const settings = this._nameMaps.emitters[name]; if (!settings) throw new Error(`Settings not defined for '${name}'`); return this.getParticleEmitterById(settings.id, autoRecycleOnComplete, cloneSettings); } /** * Retrieves a particle emitter by its component ID. * * @param {string} componentId - The ID of the component. * @param {boolean} autoRecycleOnComplete - Whether the emitter should automatically recycle itself when it completes. * @param {boolean} cloneSettings - Whether to clone the settings object before applying them to the emitter. * @return {ParticleEmitter} The retrieved particle emitter. */ getParticleEmitterById(componentId, autoRecycleOnComplete = true, cloneSettings = false) { const pool = this._cache.emitters; let emitter; let settings = this._settingsCache.emitters[componentId]; if (!settings) throw new Error(`Settings not defined for '${componentId}'`); if (pool.length == 0) { emitter = new ParticleEmitter(componentId); emitter.__fx = this; } else { emitter = pool.pop(); } if (cloneSettings) { settings = deepClone(settings); settings.__isClone = true; } emitter.autoRecycleOnComplete = autoRecycleOnComplete; emitter.__applySettings(settings); return emitter; } /** * Creates a particle emitter from the specified settings. * * @param {IEmitterSettings} settings - The settings of the emitter to create. * @param {boolean} autoRecycleOnComplete - Whether the emitter should automatically recycle itself when it completes. * @return {ParticleEmitter} The created particle emitter. */ createParticleEmitterFrom(settings, autoRecycleOnComplete = true) { const pool = this._cache.emitters; let emitter; if (pool.length == 0) { emitter = new ParticleEmitter(settings.id); emitter.__fx = this; } else { emitter = pool.pop(); } emitter.autoRecycleOnComplete = autoRecycleOnComplete; emitter.__applySettings(settings); return emitter; } createEffectSequenceEmitterFrom(settings) { const pool = this._cache.effectSequences; let effectSequence; if (pool.length == 0) { effectSequence = new EffectSequence(settings.id); effectSequence.__fx = this; } else { effectSequence = pool.pop(); } effectSequence.__applySettings(settings); return effectSequence; } /** * Stops the specified particle emitter. * * @param {ParticleEmitter} emitter - The particle emitter to stop. * @param {boolean} [dispose=false] - Whether to dispose the emitter or recycle it. */ stopEmitter(emitter, dispose = false) { if (emitter.list === this._effects) { this._effects.remove(emitter); } if (dispose) { emitter.dispose(); } else { this.__recycleEmitter(emitter); } } /** * Stops all effects. * * @param {none} none - This function does not take any parameters. * @return {void} This function does not return a value. */ stopAllEffects() { const list = this._effects.toArray(); for (let node of list) { node.recycle(); } } /** * Parses a sprite sheet. * * @param {PIXI.Spritesheet} spriteSheet - The sprite sheet to parse. * @param {string} filter - Optional filter to apply to the sprite sheet. * @return {IParseSpriteSheetResult} The result of parsing the sprite sheet. */ parseSpriteSheet(spriteSheet, filter) { return this.parseObject(spriteSheet.data.frames, filter); } /** * Parses the texture cache and returns the result as an IParseSpriteSheetResult object. * * @param {string} [filter] - An optional parameter to filter the results. * @returns {IParseSpriteSheetResult} - The parsed sprite sheet result. */ parseTextureCache(filter) { return this.parseObject(PIXI['Cache']['_cache'], filter); } /** * Returns if the FX instance is active. * * @return {boolean} The value of the 'active' property. */ get active() { return this._active; } // ********************************************************************************************* // * Internal * // ********************************************************************************************* __addActiveEffect(effect) { this._effects.add(effect); } __removeActiveEffect(effect) { this._effects.remove(effect); } __getSprite(componentId) { const cache = this._cache.sprites; let pool = cache[componentId]; if (cache[componentId] == null) { pool = cache[componentId] = []; } if (pool.length == 0) { const settings = this._settingsCache.sprites[componentId]; if (settings == null) throw new Error(`Settings not defined for '${componentId}'`); const sprite = new Sprite(componentId, settings.texture, settings.anchorX, settings.anchorY); sprite.__fx = this; return sprite; } return pool.pop(); } __getMovieClip(componentId) { const cache = this._cache.mcs; let pool = cache[componentId]; if (cache[componentId] == null) { pool = cache[componentId] = []; } if (pool.length == 0) { let settings = this._settingsCache.mcs[componentId]; if (settings == null) throw new Error(`Settings not defined for '${componentId}'`); const mc = new MovieClip(componentId, settings.textures, settings.anchorX, settings.anchorY); mc.__fx = this; return mc; } return pool.pop(); } __getParticle() { let cache = this._cache, pool = cache.particles; if (pool.length == 0) { const particle = new Particle(); particle.__fx = this; return particle; } return pool.pop(); } __getEmitterCore(type, emitter) { let cache = this._cache.cores; let pool = cache[type]; if (pool == null) { pool = cache[type] = []; } if (pool.length == 0) { return new FX.__emitterCores[type](type); } return pool.pop(); } __recycleParticle(particle) { this._cache.particles.push(particle); } __recycleSprite(componentId, object) { this._cache.sprites[componentId].push(object); } __recycleMovieClip(componentId, object) { this._cache.mcs[componentId].push(object); } __recycleEmitter(emitter) { this._effects.remove(emitter); this.__recycleEmitterCore(emitter.core); this._cache.emitters.push(emitter); } __recycleEffectSequence(effectSequence) { this._effects.remove(effectSequence); this._cache.effectSequences.push(effectSequence); } __recycleEmitterCore(core) { this._cache.cores[core.type].push(core); } __getBlendMode(value) { if (PIXI['BLEND_MODES'] === undefined && typeof value == 'number') { return this.useBlendModes ? FX._blendModes[value] : 'normal'; } return value; } __getSequenceSettings(componentId) { let settings = this._settingsCache.effectSequences[componentId]; if (!settings) throw new Error(`Settings not defined for '${componentId}'`); return settings; } __getEmitterSettings(componentId) { let settings = this._settingsCache.emitters[componentId]; if (!settings) throw new Error(`Settings not defined for '${componentId}'`); return settings; } // ********************************************************************************************* // * Private * // ********************************************************************************************* parseObject(object, filter) { let frames; if (object instanceof Map) { frames = new Map(); const mapObject = object; const values = mapObject.values(); for (const [key, value] of mapObject) { if (value instanceof PIXI.Texture) { frames[key] = value; } } } else { frames = object; } const mcs = {}; const result = { sprites: [], movieClips: [] }; for (let i in frames) { if (filter && i.indexOf(filter) == -1) { continue; } this.initSprite(i, { texture: i, anchorX: 0.5, anchorY: 0.5 }); result.sprites.push(i); if (i.substr(0, 3) == 'mc_') { const parts = i.split('_'); const group = parts[1]; if (mcs[group] == null) mcs[group] = []; mcs[group].push(i); } } for (let i in mcs) { let textures = mcs[i]; result.movieClips.push(i); this.initMovieClip(i, { textures: textures, anchorX: 0.5, anchorY: 0.5 }); } return result; } } FX.settingsVersion = 0; FX.version = '1.3.4'; FX._bundleHash = '80c6df7fb0d3d898f34ce0031c037fef'; FX.ComponentType = ComponentType; FX.EffectSequenceComponentType = EffectSequenceComponentType; FX._blendModes = ['normal', 'add', 'multiply', 'screen']; FX.__emitterCores = { circle: CircleEmitterCore, box: BoxEmitterCore, ring: RingEmitterCore }; export var SpawnType; (function (SpawnType) { SpawnType[SpawnType["ParticleEmitter"] = 0] = "ParticleEmitter"; SpawnType[SpawnType["EffectSequence"] = 1] = "EffectSequence"; })(SpawnType || (SpawnType = {})); //# sourceMappingURL=FX.js.map