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.

370 lines (369 loc) 12.6 kB
import { animate, cancelAnimation, getRangeValue } from "../Utils/MathUtils.js"; import { defaultFps, defaultFpsLimit, millisecondsToSeconds, minFpsLimit } from "./Utils/Constants.js"; import { CanvasManager } from "./CanvasManager.js"; import { EventListeners } from "./Utils/EventListeners.js"; import { EventType } from "../Enums/Types/EventType.js"; import { Options } from "../Options/Classes/Options.js"; import { ParticlesManager } from "./ParticlesManager.js"; import { Retina } from "./Retina.js"; import { getLogger } from "../Utils/LogUtils.js"; import { loadOptions } from "../Utils/OptionsUtils.js"; function guardCheck(container) { return !container.destroyed; } function updateDelta(delta, value, fpsLimit = defaultFps, smooth = false) { delta.value = value; delta.factor = smooth ? defaultFps / fpsLimit : (defaultFps * value) / millisecondsToSeconds; } function loadContainerOptions(pluginManager, container, ...sourceOptionsArr) { const options = new Options(pluginManager, container); loadOptions(options, ...sourceOptionsArr); return options; } export class Container { actualOptions; canvas; destroyed; effectDrawers; fpsLimit; hdr; id; pageHidden; particleCreatedPlugins; particleDestroyedPlugins; particlePositionPlugins; particleUpdaters; particles; plugins; retina; shapeDrawers; started; zLayers; #delay; #delayTimeout; #delta = { value: 0, factor: 0 }; #dispatchCallback; #drawAnimationFrame; #duration; #eventListeners; #firstStart; #initialSourceOptions; #lastFrameTime; #lifeTime; #onDestroy; #options; #paused; #pluginManager; #smooth; #sourceOptions; constructor(params) { const { dispatchCallback, pluginManager, id, onDestroy, sourceOptions } = params; this.#pluginManager = pluginManager; this.#dispatchCallback = dispatchCallback; this.#onDestroy = onDestroy; this.id = Symbol(id); this.fpsLimit = 120; this.hdr = false; this.#smooth = false; this.#delay = 0; this.#duration = 0; this.#lifeTime = 0; this.#firstStart = true; this.started = false; this.destroyed = false; this.#paused = true; this.#lastFrameTime = 0; this.zLayers = 100; this.pageHidden = false; this.#sourceOptions = sourceOptions; this.#initialSourceOptions = sourceOptions; this.effectDrawers = new Map(); this.shapeDrawers = new Map(); this.particleUpdaters = []; this.retina = new Retina(this); this.canvas = new CanvasManager(this.#pluginManager, this); this.particles = new ParticlesManager(this.#pluginManager, this); this.plugins = []; this.particleDestroyedPlugins = []; this.particleCreatedPlugins = []; this.particlePositionPlugins = []; this.#options = loadContainerOptions(this.#pluginManager, this); this.actualOptions = loadContainerOptions(this.#pluginManager, this); this.#eventListeners = new EventListeners(this); this.dispatchEvent(EventType.containerBuilt); } get animationStatus() { return !this.#paused && !this.pageHidden && guardCheck(this); } get options() { return this.#options; } get sourceOptions() { return this.#sourceOptions; } addLifeTime(value) { this.#lifeTime += value; } alive() { return !this.#duration || this.#lifeTime <= this.#duration; } destroy(remove = true) { if (!guardCheck(this)) { return; } this.stop(); this.particles.destroy(); this.canvas.destroy(); for (const [, effectDrawer] of this.effectDrawers) { effectDrawer.destroy?.(this); } for (const [, shapeDrawer] of this.shapeDrawers) { shapeDrawer.destroy?.(this); } for (const plugin of this.plugins) { plugin.destroy?.(); } this.effectDrawers = new Map(); this.shapeDrawers = new Map(); this.particleUpdaters = []; this.plugins.length = 0; this.#pluginManager.clearPlugins(this); this.destroyed = true; this.#onDestroy(remove); this.dispatchEvent(EventType.containerDestroyed); } dispatchEvent(type, data) { this.#dispatchCallback(type, { container: this, data, }); } draw(force) { if (!guardCheck(this)) { return; } let refreshTime = force; this.#drawAnimationFrame = animate((timestamp) => { if (refreshTime) { this.#lastFrameTime = undefined; refreshTime = false; } this.#nextFrame(timestamp); }); } async export(type, options = {}) { for (const plugin of this.plugins) { if (!plugin.export) { continue; } const res = await plugin.export(type, options); if (!res.supported) { continue; } return res.blob; } getLogger().error(`Export plugin with type ${type} not found`); return undefined; } async init() { if (!guardCheck(this)) { return; } const allContainerPlugins = new Map(); for (const plugin of this.#pluginManager.plugins) { const containerPlugin = await plugin.getPlugin(this); if (containerPlugin.preInit) { await containerPlugin.preInit(); } allContainerPlugins.set(plugin, containerPlugin); } await this.initDrawersAndUpdaters(); this.#options = loadContainerOptions(this.#pluginManager, this, this.#initialSourceOptions, this.sourceOptions); this.actualOptions = loadContainerOptions(this.#pluginManager, this, this.#options); this.plugins.length = 0; this.particleDestroyedPlugins.length = 0; this.particleCreatedPlugins.length = 0; this.particlePositionPlugins.length = 0; for (const [plugin, containerPlugin] of allContainerPlugins) { if (plugin.needsPlugin(this.actualOptions)) { this.plugins.push(containerPlugin); if (containerPlugin.particleCreated) { this.particleCreatedPlugins.push(containerPlugin); } if (containerPlugin.particleDestroyed) { this.particleDestroyedPlugins.push(containerPlugin); } if (containerPlugin.particlePosition) { this.particlePositionPlugins.push(containerPlugin); } } } this.retina.init(); this.canvas.init(); this.updateActualOptions(); this.canvas.initBackground(); this.canvas.resize(); const { delay, duration, fpsLimit, hdr, smooth, zLayers } = this.actualOptions; this.hdr = hdr; this.zLayers = zLayers; this.#duration = getRangeValue(duration) * millisecondsToSeconds; this.#delay = getRangeValue(delay) * millisecondsToSeconds; this.#lifeTime = 0; this.fpsLimit = fpsLimit > minFpsLimit ? fpsLimit : defaultFpsLimit; this.#smooth = smooth; for (const plugin of this.plugins) { await plugin.init?.(); } await this.particles.init(); this.dispatchEvent(EventType.containerInit); this.particles.setDensity(); for (const plugin of this.plugins) { plugin.particlesSetup?.(); } this.dispatchEvent(EventType.particlesSetup); } async initDrawersAndUpdaters() { const pluginManager = this.#pluginManager; this.effectDrawers = await pluginManager.getEffectDrawers(this, true); this.shapeDrawers = await pluginManager.getShapeDrawers(this, true); this.particleUpdaters = await pluginManager.getUpdaters(this, true); } pause() { if (!guardCheck(this)) { return; } if (this.#drawAnimationFrame !== undefined) { cancelAnimation(this.#drawAnimationFrame); this.#drawAnimationFrame = undefined; } if (this.#paused) { return; } for (const plugin of this.plugins) { plugin.pause?.(); } if (!this.pageHidden) { this.#paused = true; } this.dispatchEvent(EventType.containerPaused); } play(force) { if (!guardCheck(this)) { return; } const needsUpdate = this.#paused || force; if (this.#firstStart && !this.actualOptions.autoPlay) { this.#firstStart = false; return; } if (this.#paused) { this.#paused = false; } if (needsUpdate) { for (const plugin of this.plugins) { if (plugin.play) { plugin.play(); } } } this.dispatchEvent(EventType.containerPlay); this.draw(needsUpdate ?? false); } async refresh() { if (!guardCheck(this)) { return; } this.stop(); return this.start(); } async reset(sourceOptions) { if (!guardCheck(this)) { return; } this.#initialSourceOptions = sourceOptions; this.#sourceOptions = sourceOptions; this.#options = loadContainerOptions(this.#pluginManager, this, this.#initialSourceOptions, this.sourceOptions); this.actualOptions = loadContainerOptions(this.#pluginManager, this, this.#options); return this.refresh(); } async start() { if (!guardCheck(this) || this.started) { return; } await this.init(); this.started = true; await new Promise(resolve => { const start = async () => { this.#eventListeners.addListeners(); for (const plugin of this.plugins) { await plugin.start?.(); } this.dispatchEvent(EventType.containerStarted); this.play(); resolve(); }; this.#delayTimeout = setTimeout(() => void start(), this.#delay); }); } stop() { if (!guardCheck(this) || !this.started) { return; } if (this.#delayTimeout) { clearTimeout(this.#delayTimeout); this.#delayTimeout = undefined; } this.#firstStart = true; this.started = false; this.#eventListeners.removeListeners(); this.pause(); this.particles.clear(); this.canvas.stop(); for (const plugin of this.plugins) { plugin.stop?.(); } this.particleCreatedPlugins.length = 0; this.particleDestroyedPlugins.length = 0; this.particlePositionPlugins.length = 0; this.#sourceOptions = this.#options; this.dispatchEvent(EventType.containerStopped); } updateActualOptions() { let refresh = false; for (const plugin of this.plugins) { if (plugin.updateActualOptions) { refresh = plugin.updateActualOptions() || refresh; } } return refresh; } #nextFrame = (timestamp) => { try { if (!this.#smooth && this.#lastFrameTime !== undefined && timestamp < this.#lastFrameTime + millisecondsToSeconds / this.fpsLimit) { this.draw(false); return; } this.#lastFrameTime ??= timestamp; updateDelta(this.#delta, timestamp - this.#lastFrameTime, this.fpsLimit, this.#smooth); this.addLifeTime(this.#delta.value); this.#lastFrameTime = timestamp; if (this.#delta.value > millisecondsToSeconds) { this.draw(false); return; } this.canvas.render.drawParticles(this.#delta); if (!this.alive()) { this.destroy(); return; } if (this.animationStatus) { this.draw(false); } } catch (e) { getLogger().error("error in animation loop", e); } }; }