@tsparticles/engine
Version:
Easily create highly customizable particle, confetti and fireworks animations and use them as animated backgrounds for your website. Ready to use components available also for React, Vue.js (2.x and 3.x), Angular, Svelte, jQuery, Preact, Riot.js, Inferno.
489 lines (488 loc) • 18.8 kB
JavaScript
import { countOffset, defaultDensityFactor, defaultRemoveQuantity, deleteCount, double, minCount, minIndex, minLimit, one, spatialHashGridCellSize, squareExp, } from "./Utils/Constants.js";
import { EventType } from "../Enums/Types/EventType.js";
import { LimitMode } from "../Enums/Modes/LimitMode.js";
import { Particle } from "./Particle.js";
import { SpatialHashGrid } from "./Utils/SpatialHashGrid.js";
import { getLogger } from "../Utils/LogUtils.js";
import { loadParticlesOptions } from "../Utils/ParticlesOptionsLoader.js";
import { toEnumValue } from "../Utils/TypeUtils.js";
export class ParticlesManager {
checkParticlePositionPlugins;
#array;
#container;
#grid;
#groupLimits;
#limit;
#nextId;
#particleBuckets;
#particleResetPlugins;
#particleUpdatePlugins;
#pluginManager;
#pool;
#postParticleUpdatePlugins;
#postUpdatePlugins;
#resizeFactor;
#updatePlugins;
#zBuckets;
constructor(pluginManager, container) {
this.#pluginManager = pluginManager;
this.#container = container;
this.#nextId = 0;
this.#array = [];
this.#pool = [];
this.#limit = 0;
this.#groupLimits = new Map();
this.#particleBuckets = new Map();
this.#zBuckets = this.#createBuckets(this.#container.zLayers);
this.#grid = new SpatialHashGrid(spatialHashGridCellSize);
this.checkParticlePositionPlugins = [];
this.#particleResetPlugins = [];
this.#particleUpdatePlugins = [];
this.#postUpdatePlugins = [];
this.#postParticleUpdatePlugins = [];
this.#updatePlugins = [];
}
get count() {
return this.#array.length;
}
get grid() {
return this.#grid;
}
addParticle(position, overrideOptions, group, initializer) {
if (this.#checkAndApplyLimits(group, this.count)) {
return;
}
try {
const particle = this.#pool.pop() ?? new Particle(this.#pluginManager, this.#container);
particle.init(this.#nextId, position, overrideOptions, group);
let canAdd = true;
if (initializer) {
canAdd = initializer(particle);
}
if (!canAdd) {
this.#pool.push(particle);
return;
}
this.#array.push(particle);
this.#insertParticleIntoBucket(particle);
this.#nextId++;
this.#container.dispatchEvent(EventType.particleAdded, {
particle,
});
return particle;
}
catch (e) {
getLogger().warning(`error adding particle: ${e}`);
}
return undefined;
}
clear() {
this.#array = [];
this.#particleBuckets.clear();
this.#resetBuckets(this.#container.zLayers);
}
destroy() {
this.#array = [];
this.#pool.length = 0;
this.#particleBuckets.clear();
this.#zBuckets = [];
this.checkParticlePositionPlugins = [];
this.#particleResetPlugins = [];
this.#particleUpdatePlugins = [];
this.#postUpdatePlugins = [];
this.#postParticleUpdatePlugins = [];
this.#updatePlugins = [];
}
drawParticles(delta) {
for (let i = this.#zBuckets.length - one; i >= minIndex; i--) {
const bucket = this.#zBuckets[i];
if (!bucket) {
continue;
}
for (const particle of bucket) {
particle.draw(delta);
}
}
}
filter(condition) {
return this.#array.filter(condition);
}
find(condition) {
return this.#array.find(condition);
}
get(index) {
return this.#array[index];
}
async init() {
const container = this.#container;
this.checkParticlePositionPlugins = [];
this.#updatePlugins = [];
this.#particleUpdatePlugins = [];
this.#postUpdatePlugins = [];
this.#particleResetPlugins = [];
this.#postParticleUpdatePlugins = [];
this.#particleBuckets.clear();
this.#resetBuckets(container.zLayers);
this.#grid = new SpatialHashGrid(spatialHashGridCellSize * container.retina.pixelRatio);
this.#applyLimits(container.actualOptions.particles);
const particlesOptions = container.actualOptions.particles, groups = particlesOptions.groups;
for (const group in groups) {
const groupData = groups[group];
if (groupData) {
this.#applyLimits(loadParticlesOptions(this.#pluginManager, this.#container, groupData), group, groupData);
}
}
await this.#initPlugins();
}
push(nb, position, overrideOptions, group) {
for (let i = 0; i < nb; i++) {
this.addParticle(position, overrideOptions, group);
}
}
async redraw() {
this.clear();
await this.init();
this.#container.canvas.render.drawParticles({ value: 0, factor: 0 });
}
remove(particle, group, override) {
this.removeAt(this.#array.indexOf(particle), undefined, group, override);
}
removeAt(index, quantity = defaultRemoveQuantity, group, override, matchGroup = true) {
if (index < minIndex || index > this.count) {
return;
}
let deleted = 0;
for (let i = index; deleted < quantity && i < this.count; i++) {
if (this.#removeParticle(i, group, override, matchGroup)) {
i--;
deleted++;
}
}
}
removeQuantity(quantity, group) {
this.removeAt(minIndex, quantity, group);
}
setDensity() {
const options = this.#container.actualOptions, groups = options.particles.groups;
let pluginsCount = 0;
for (const plugin of this.#container.plugins) {
if (plugin.particlesDensityCount) {
pluginsCount += plugin.particlesDensityCount();
}
}
for (const group in groups) {
const groupData = groups[group];
if (!groupData) {
continue;
}
const groupDataOptions = loadParticlesOptions(this.#pluginManager, this.#container, groupData);
this.#applyDensity(groupDataOptions, pluginsCount, group);
}
this.#applyDensity(options.particles, pluginsCount);
}
setResizeFactor(factor) {
this.#resizeFactor = factor;
}
update(delta) {
this.#grid.clear();
for (const plugin of this.#updatePlugins) {
plugin.update?.(delta);
}
const particlesToDelete = this.#updateParticlesPhase1(delta);
for (const plugin of this.#postUpdatePlugins) {
plugin.postUpdate?.(delta);
}
this.#updateParticlesPhase2(delta, particlesToDelete);
if (particlesToDelete.size) {
for (const particle of particlesToDelete) {
this.remove(particle, particle.group);
}
}
this.#resizeFactor = undefined;
}
#addToPool(...particles) {
this.#pool.push(...particles);
}
#applyDensity(options, pluginsCount, group, groupOptions) {
const numberOptions = options.number;
if (!numberOptions.density.enable) {
this.#applyLimits(options, group, groupOptions);
return;
}
const densityFactor = this.#initDensityFactor(numberOptions.density), optParticlesNumber = numberOptions.value, optParticlesLimit = numberOptions.limit.value > minLimit ? numberOptions.limit.value : optParticlesNumber, particlesNumber = Math.min(optParticlesNumber, optParticlesLimit) * densityFactor + pluginsCount, particlesCount = Math.min(this.count, this.filter(t => t.group === group).length);
if (group === undefined) {
this.#limit = numberOptions.limit.value * densityFactor;
}
else {
this.#groupLimits.set(group, {
limit: numberOptions.limit.value * densityFactor,
mode: toEnumValue(LimitMode, numberOptions.limit.mode, LimitMode.delete),
});
}
if (particlesCount < particlesNumber) {
this.push(Math.abs(particlesNumber - particlesCount), undefined, options, group);
}
else if (particlesCount > particlesNumber) {
this.removeQuantity(particlesCount - particlesNumber, group);
}
}
#applyLimits(options, group, groupOptions) {
if (group === undefined) {
this.#limit = options.number.limit.value;
const groups = this.#container.actualOptions.particles.groups;
for (const existingGroup of this.#groupLimits.keys()) {
if (!(existingGroup in groups)) {
this.#groupLimits.delete(existingGroup);
}
}
}
else {
const limitValue = groupOptions?.number?.limit?.value ?? options.number.limit.value;
if (limitValue > minLimit) {
this.#groupLimits.set(group, {
limit: limitValue,
mode: toEnumValue(LimitMode, groupOptions?.number?.limit?.mode ?? options.number.limit.mode, LimitMode.delete),
});
}
else {
this.#groupLimits.delete(group);
}
}
}
#checkAndApplyLimits(group, currentCount) {
const globalLimit = this.#limit, globalMode = this.#container.actualOptions.particles.number.limit.mode;
if (globalLimit > minLimit && globalMode === LimitMode.wait && currentCount >= globalLimit) {
return true;
}
const groupLimitEntry = group === undefined ? undefined : this.#groupLimits.get(group);
if (groupLimitEntry && groupLimitEntry.limit > minLimit && groupLimitEntry.mode === LimitMode.wait) {
const groupCount = this.filter(t => t.group === group).length;
if (groupCount >= groupLimitEntry.limit) {
return true;
}
}
if (groupLimitEntry && groupLimitEntry.limit > minLimit && groupLimitEntry.mode === LimitMode.delete) {
const groupCountToRemove = this.filter(t => t.group === group).length + countOffset - groupLimitEntry.limit;
if (groupCountToRemove > minCount) {
this.removeQuantity(groupCountToRemove, group);
}
}
if (globalLimit > minLimit && globalMode === LimitMode.delete) {
const globalCountToRemove = this.count + countOffset - globalLimit;
if (globalCountToRemove > minCount) {
this.#removeAny(globalCountToRemove);
}
}
return false;
}
#createBuckets(zLayers) {
const bucketCount = Math.max(Math.floor(zLayers), one);
return Array.from({ length: bucketCount }, () => []);
}
#getBucketIndex(zIndex) {
const maxBucketIndex = this.#zBuckets.length - one;
if (maxBucketIndex <= minIndex) {
return minIndex;
}
return Math.min(Math.max(Math.floor(zIndex), minIndex), maxBucketIndex);
}
#initDensityFactor(densityOptions) {
const container = this.#container;
if (!densityOptions.enable) {
return defaultDensityFactor;
}
const canvasSize = container.canvas.size, pxRatio = container.retina.pixelRatio;
if (!canvasSize.width || !canvasSize.height) {
return defaultDensityFactor;
}
return ((canvasSize.width * canvasSize.height) / (densityOptions.height * densityOptions.width * pxRatio ** squareExp));
}
async #initDrawersAndUpdaters() {
const container = this.#container;
await container.initDrawersAndUpdaters();
for (const drawer of container.effectDrawers.values()) {
await drawer.init?.(container);
}
for (const drawer of container.shapeDrawers.values()) {
await drawer.init?.(container);
}
}
async #initPlugins() {
await this.#initPluginsArrays();
await this.#initDrawersAndUpdaters();
let handled = false;
const container = this.#container;
for (const plugin of container.plugins) {
handled = plugin.particlesInitialization?.() ?? handled;
if (handled) {
break;
}
}
if (handled) {
return;
}
const particlesOptions = container.actualOptions.particles, groups = particlesOptions.groups;
for (const group in groups) {
const groupOptions = groups[group];
if (!groupOptions) {
continue;
}
for (let i = this.count, j = 0; j < groupOptions.number.value && i < particlesOptions.number.value; i++, j++) {
this.addParticle(undefined, groupOptions, group);
}
}
for (let i = this.count; i < particlesOptions.number.value; i++) {
this.addParticle();
}
}
async #initPluginsArrays() {
const container = this.#container;
for (const plugin of container.plugins) {
if (plugin.redrawInit) {
await plugin.redrawInit();
}
if (plugin.checkParticlePosition) {
this.checkParticlePositionPlugins.push(plugin);
}
if (plugin.update) {
this.#updatePlugins.push(plugin);
}
if (plugin.particleUpdate) {
this.#particleUpdatePlugins.push(plugin);
}
if (plugin.postUpdate) {
this.#postUpdatePlugins.push(plugin);
}
if (plugin.particleReset) {
this.#particleResetPlugins.push(plugin);
}
if (plugin.postParticleUpdate) {
this.#postParticleUpdatePlugins.push(plugin);
}
}
}
#insertParticleIntoBucket(particle) {
const bucketIndex = this.#getBucketIndex(particle.position.z), bucket = this.#zBuckets[bucketIndex];
if (!bucket) {
return;
}
bucket.push(particle);
this.#particleBuckets.set(particle.id, bucketIndex);
}
#removeAny(quantity) {
this.removeAt(minIndex, quantity, undefined, undefined, false);
}
#removeParticle(index, group, override, matchGroup = true) {
const particle = this.#array[index];
if (!particle) {
return false;
}
if (matchGroup && particle.group !== group) {
return false;
}
this.#array.splice(index, deleteCount);
this.#removeParticleFromBucket(particle);
particle.destroy(override);
this.#container.dispatchEvent(EventType.particleRemoved, {
particle,
});
this.#addToPool(particle);
return true;
}
#removeParticleFromBucket(particle) {
const bucketIndex = this.#particleBuckets.get(particle.id) ?? this.#getBucketIndex(particle.position.z), bucket = this.#zBuckets[bucketIndex];
if (!bucket) {
this.#particleBuckets.delete(particle.id);
return;
}
const idx = bucket.findIndex(p => p.id === particle.id);
if (idx >= minIndex) {
bucket.splice(idx, deleteCount);
}
this.#particleBuckets.delete(particle.id);
}
#resetBuckets(zLayers) {
const bucketCount = Math.max(Math.floor(zLayers), one);
if (this.#zBuckets.length !== bucketCount) {
this.#zBuckets = this.#createBuckets(bucketCount);
return;
}
for (const bucket of this.#zBuckets) {
bucket.length = minIndex;
}
}
#updateParticleBucket(particle) {
const newBucketIndex = this.#getBucketIndex(particle.position.z), currentBucketIndex = this.#particleBuckets.get(particle.id);
if (currentBucketIndex === undefined) {
this.#insertParticleIntoBucket(particle);
return;
}
if (currentBucketIndex === newBucketIndex) {
return;
}
const currentBucket = this.#zBuckets[currentBucketIndex];
if (currentBucket) {
const idx = currentBucket.findIndex(p => p.id === particle.id);
if (idx >= minIndex) {
currentBucket.splice(idx, deleteCount);
}
}
const newBucket = this.#zBuckets[newBucketIndex];
if (!newBucket) {
this.#particleBuckets.set(particle.id, newBucketIndex);
return;
}
newBucket.push(particle);
if (newBucket.length >= double) {
const prev = newBucket[newBucket.length - double];
if (prev && particle.id < prev.id) {
newBucket.sort((a, b) => a.id - b.id);
}
}
this.#particleBuckets.set(particle.id, newBucketIndex);
}
#updateParticlesPhase1(delta) {
const particlesToDelete = new Set(), resizeFactor = this.#resizeFactor;
for (const particle of this.#array) {
if (resizeFactor && !particle.ignoresResizeRatio) {
particle.position.x *= resizeFactor.width;
particle.position.y *= resizeFactor.height;
particle.initialPosition.x *= resizeFactor.width;
particle.initialPosition.y *= resizeFactor.height;
}
particle.ignoresResizeRatio = false;
for (const plugin of this.#particleResetPlugins) {
plugin.particleReset?.(particle);
}
for (const plugin of this.#particleUpdatePlugins) {
if (particle.destroyed) {
break;
}
plugin.particleUpdate?.(particle, delta);
}
if (particle.destroyed) {
particlesToDelete.add(particle);
continue;
}
this.#grid.insert(particle);
}
return particlesToDelete;
}
#updateParticlesPhase2(delta, particlesToDelete) {
for (const particle of this.#array) {
if (particle.destroyed) {
particlesToDelete.add(particle);
continue;
}
for (const updater of this.#container.particleUpdaters) {
updater.update(particle, delta);
}
if (!particle.spawning) {
for (const plugin of this.#postParticleUpdatePlugins) {
plugin.postParticleUpdate?.(particle, delta);
}
}
this.#updateParticleBucket(particle);
}
}
}