UNPKG

@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.

443 lines (442 loc) 17 kB
import { countOffset, defaultDensityFactor, defaultRemoveQuantity, deleteCount, double, empty, 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/OptionsUtils.js"; export class ParticlesManager { checkParticlePositionPlugins; grid; #array; #container; #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; } addParticle(position, overrideOptions, group, initializer) { const limitMode = this.#container.actualOptions.particles.number.limit.mode, limit = group === undefined ? this.#limit : (this.#groupLimits.get(group) ?? this.#limit), currentCount = this.count; if (limit > minLimit) { switch (limitMode) { case LimitMode.delete: { const countToRemove = currentCount + countOffset - limit; if (countToRemove > minCount) { this.removeQuantity(countToRemove); } break; } case LimitMode.wait: if (currentCount >= limit) { return; } break; default: break; } } 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, options = container.actualOptions; 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); 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); } } await this.#container.initDrawersAndUpdaters(); for (const drawer of this.#container.effectDrawers.values()) { await drawer.init?.(container); } for (const drawer of this.#container.shapeDrawers.values()) { await drawer.init?.(container); } let handled = false; for (const plugin of container.plugins) { handled = plugin.particlesInitialization?.() ?? handled; if (handled) { break; } } if (!handled) { const particlesOptions = options.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(); } } } 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) { 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)) { 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); } } this.#resizeFactor = undefined; } #addToPool = (...particles) => { this.#pool.push(...particles); }; #applyDensity = (options, pluginsCount, group, groupOptions) => { const numberOptions = options.number; if (!numberOptions.density.enable) { if (group === undefined) { this.#limit = numberOptions.limit.value; } else if (groupOptions?.number.limit.value ?? numberOptions.limit.value) { this.#groupLimits.set(group, groupOptions?.number.limit.value ?? numberOptions.limit.value); } 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, numberOptions.limit.value * densityFactor); } if (particlesCount < particlesNumber) { this.push(Math.abs(particlesNumber - particlesCount), undefined, options, group); } else if (particlesCount > particlesNumber) { this.removeQuantity(particlesCount - particlesNumber, group); } }; #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); }; #getParticleInsertIndex = (bucket, particleId) => { let start = minIndex, end = bucket.length; while (start < end) { const middle = Math.floor((start + end) / double), middleParticle = bucket[middle]; if (!middleParticle) { end = middle; continue; } if (middleParticle.id < particleId) { start = middle + one; } else { end = middle; } } return start; }; #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)); }; #insertParticleIntoBucket = (particle) => { const bucketIndex = this.#getBucketIndex(particle.position.z), bucket = this.#zBuckets[bucketIndex]; if (!bucket) { return; } bucket.splice(this.#getParticleInsertIndex(bucket, particle.id), empty, particle); this.#particleBuckets.set(particle.id, bucketIndex); }; #removeParticle = (index, group, override) => { const particle = this.#array[index]; if (!particle) { return false; } if (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 particleIndex = this.#getParticleInsertIndex(bucket, particle.id); if (bucket[particleIndex]?.id !== particle.id) { this.#particleBuckets.delete(particle.id); return; } bucket.splice(particleIndex, 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 particleIndex = this.#getParticleInsertIndex(currentBucket, particle.id); if (currentBucket[particleIndex]?.id === particle.id) { currentBucket.splice(particleIndex, deleteCount); } } const newBucket = this.#zBuckets[newBucketIndex]; if (!newBucket) { this.#particleBuckets.set(particle.id, newBucketIndex); return; } newBucket.splice(this.#getParticleInsertIndex(newBucket, particle.id), empty, particle); 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); } }; }