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.
458 lines (457 loc) • 16.2 kB
JavaScript
import { getLogger, isFunction } from "../Utils/Utils";
import { Canvas } from "./Canvas";
import { EventListeners } from "./Utils/EventListeners";
import { Options } from "../Options/Classes/Options";
import { Particles } from "./Particles";
import { Retina } from "./Retina";
import { errorPrefix } from "./Utils/Constants";
import { getRangeValue } from "../Utils/NumberUtils";
import { loadOptions } from "../Utils/OptionsUtils";
function guardCheck(container) {
return container && !container.destroyed;
}
function initDelta(value, fpsLimit = 60, smooth = false) {
return {
value,
factor: smooth ? 60 / fpsLimit : (60 * value) / 1000,
};
}
function loadContainerOptions(engine, container, ...sourceOptionsArr) {
const options = new Options(engine, container);
loadOptions(options, ...sourceOptionsArr);
return options;
}
const defaultPathGeneratorKey = "default", defaultPathGenerator = {
generate: (p) => p.velocity,
init: () => {
},
update: () => {
},
reset: () => {
},
};
export class Container {
constructor(engine, id, sourceOptions) {
this.id = id;
this._intersectionManager = (entries) => {
if (!guardCheck(this) || !this.actualOptions.pauseOnOutsideViewport) {
return;
}
for (const entry of entries) {
if (entry.target !== this.interactivity.element) {
continue;
}
(entry.isIntersecting ? this.play : this.pause)();
}
};
this._nextFrame = async (timestamp) => {
try {
if (!this.smooth &&
this.lastFrameTime !== undefined &&
timestamp < this.lastFrameTime + 1000 / this.fpsLimit) {
this.draw(false);
return;
}
this.lastFrameTime ??= timestamp;
const delta = initDelta(timestamp - this.lastFrameTime, this.fpsLimit, this.smooth);
this.addLifeTime(delta.value);
this.lastFrameTime = timestamp;
if (delta.value > 1000) {
this.draw(false);
return;
}
await this.particles.draw(delta);
if (!this.alive()) {
this.destroy();
return;
}
if (this.getAnimationStatus()) {
this.draw(false);
}
}
catch (e) {
getLogger().error(`${errorPrefix} in animation loop`, e);
}
};
this._engine = engine;
this.fpsLimit = 120;
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.retina = new Retina(this);
this.canvas = new Canvas(this);
this.particles = new Particles(this._engine, this);
this.pathGenerators = new Map();
this.interactivity = {
mouse: {
clicking: false,
inside: false,
},
};
this.plugins = new Map();
this.drawers = new Map();
this._options = loadContainerOptions(this._engine, this);
this.actualOptions = loadContainerOptions(this._engine, this);
this._eventListeners = new EventListeners(this);
if (typeof IntersectionObserver !== "undefined" && IntersectionObserver) {
this._intersectionObserver = new IntersectionObserver((entries) => this._intersectionManager(entries));
}
this._engine.dispatchEvent("containerBuilt", { container: this });
}
get options() {
return this._options;
}
get sourceOptions() {
return this._sourceOptions;
}
addClickHandler(callback) {
if (!guardCheck(this)) {
return;
}
const el = this.interactivity.element;
if (!el) {
return;
}
const clickOrTouchHandler = (e, pos, radius) => {
if (!guardCheck(this)) {
return;
}
const pxRatio = this.retina.pixelRatio, posRetina = {
x: pos.x * pxRatio,
y: pos.y * pxRatio,
}, particles = this.particles.quadTree.queryCircle(posRetina, radius * pxRatio);
callback(e, particles);
};
const clickHandler = (e) => {
if (!guardCheck(this)) {
return;
}
const mouseEvent = e, pos = {
x: mouseEvent.offsetX || mouseEvent.clientX,
y: mouseEvent.offsetY || mouseEvent.clientY,
};
clickOrTouchHandler(e, pos, 1);
};
const touchStartHandler = () => {
if (!guardCheck(this)) {
return;
}
touched = true;
touchMoved = false;
};
const touchMoveHandler = () => {
if (!guardCheck(this)) {
return;
}
touchMoved = true;
};
const touchEndHandler = (e) => {
if (!guardCheck(this)) {
return;
}
if (touched && !touchMoved) {
const touchEvent = e;
let lastTouch = touchEvent.touches[touchEvent.touches.length - 1];
if (!lastTouch) {
lastTouch = touchEvent.changedTouches[touchEvent.changedTouches.length - 1];
if (!lastTouch) {
return;
}
}
const element = this.canvas.element, canvasRect = element ? element.getBoundingClientRect() : undefined, pos = {
x: lastTouch.clientX - (canvasRect ? canvasRect.left : 0),
y: lastTouch.clientY - (canvasRect ? canvasRect.top : 0),
};
clickOrTouchHandler(e, pos, Math.max(lastTouch.radiusX, lastTouch.radiusY));
}
touched = false;
touchMoved = false;
};
const touchCancelHandler = () => {
if (!guardCheck(this)) {
return;
}
touched = false;
touchMoved = false;
};
let touched = false, touchMoved = false;
el.addEventListener("click", clickHandler);
el.addEventListener("touchstart", touchStartHandler);
el.addEventListener("touchmove", touchMoveHandler);
el.addEventListener("touchend", touchEndHandler);
el.addEventListener("touchcancel", touchCancelHandler);
}
addLifeTime(value) {
this._lifeTime += value;
}
addPath(key, generator, override = false) {
if (!guardCheck(this) || (!override && this.pathGenerators.has(key))) {
return false;
}
this.pathGenerators.set(key, generator ?? defaultPathGenerator);
return true;
}
alive() {
return !this._duration || this._lifeTime <= this._duration;
}
destroy() {
if (!guardCheck(this)) {
return;
}
this.stop();
this.particles.destroy();
this.canvas.destroy();
for (const [, drawer] of this.drawers) {
drawer.destroy && drawer.destroy(this);
}
for (const key of this.drawers.keys()) {
this.drawers.delete(key);
}
this._engine.plugins.destroy(this);
this.destroyed = true;
const mainArr = this._engine.dom(), idx = mainArr.findIndex((t) => t === this);
if (idx >= 0) {
mainArr.splice(idx, 1);
}
this._engine.dispatchEvent("containerDestroyed", { container: this });
}
draw(force) {
if (!guardCheck(this)) {
return;
}
let refreshTime = force;
this._drawAnimationFrame = requestAnimationFrame(async (timestamp) => {
if (refreshTime) {
this.lastFrameTime = undefined;
refreshTime = false;
}
await 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(`${errorPrefix} - Export plugin with type ${type} not found`);
}
getAnimationStatus() {
return !this._paused && !this.pageHidden && guardCheck(this);
}
handleClickMode(mode) {
if (!guardCheck(this)) {
return;
}
this.particles.handleClickMode(mode);
for (const [, plugin] of this.plugins) {
plugin.handleClickMode && plugin.handleClickMode(mode);
}
}
async init() {
if (!guardCheck(this)) {
return;
}
const shapes = this._engine.plugins.getSupportedShapes();
for (const type of shapes) {
const drawer = this._engine.plugins.getShapeDrawer(type);
if (drawer) {
this.drawers.set(type, drawer);
}
}
this._options = loadContainerOptions(this._engine, this, this._initialSourceOptions, this.sourceOptions);
this.actualOptions = loadContainerOptions(this._engine, this, this._options);
const availablePlugins = this._engine.plugins.getAvailablePlugins(this);
for (const [id, plugin] of availablePlugins) {
this.plugins.set(id, plugin);
}
this.retina.init();
await this.canvas.init();
this.updateActualOptions();
this.canvas.initBackground();
this.canvas.resize();
this.zLayers = this.actualOptions.zLayers;
this._duration = getRangeValue(this.actualOptions.duration) * 1000;
this._delay = getRangeValue(this.actualOptions.delay) * 1000;
this._lifeTime = 0;
this.fpsLimit = this.actualOptions.fpsLimit > 0 ? this.actualOptions.fpsLimit : 120;
this.smooth = this.actualOptions.smooth;
for (const [, drawer] of this.drawers) {
drawer.init && (await drawer.init(this));
}
for (const [, plugin] of this.plugins) {
plugin.init && (await plugin.init());
}
this._engine.dispatchEvent("containerInit", { container: this });
this.particles.init();
this.particles.setDensity();
for (const [, plugin] of this.plugins) {
plugin.particlesSetup && plugin.particlesSetup();
}
this._engine.dispatchEvent("particlesSetup", { container: this });
}
async loadTheme(name) {
if (!guardCheck(this)) {
return;
}
this._currentTheme = name;
await this.refresh();
}
pause() {
if (!guardCheck(this)) {
return;
}
if (this._drawAnimationFrame !== undefined) {
cancelAnimationFrame(this._drawAnimationFrame);
delete this._drawAnimationFrame;
}
if (this._paused) {
return;
}
for (const [, plugin] of this.plugins) {
plugin.pause && plugin.pause();
}
if (!this.pageHidden) {
this._paused = true;
}
this._engine.dispatchEvent("containerPaused", { container: this });
}
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._engine.dispatchEvent("containerPlay", { container: this });
this.draw(needsUpdate || false);
}
async refresh() {
if (!guardCheck(this)) {
return;
}
this.stop();
return this.start();
}
async reset() {
if (!guardCheck(this)) {
return;
}
this._initialSourceOptions = undefined;
this._options = loadContainerOptions(this._engine, this);
this.actualOptions = loadContainerOptions(this._engine, this, this._options);
return this.refresh();
}
setNoise(noiseOrGenerator, init, update) {
if (!guardCheck(this)) {
return;
}
this.setPath(noiseOrGenerator, init, update);
}
setPath(pathOrGenerator, init, update) {
if (!pathOrGenerator || !guardCheck(this)) {
return;
}
const pathGenerator = { ...defaultPathGenerator };
if (isFunction(pathOrGenerator)) {
pathGenerator.generate = pathOrGenerator;
if (init) {
pathGenerator.init = init;
}
if (update) {
pathGenerator.update = update;
}
}
else {
const oldGenerator = pathGenerator;
pathGenerator.generate = pathOrGenerator.generate || oldGenerator.generate;
pathGenerator.init = pathOrGenerator.init || oldGenerator.init;
pathGenerator.update = pathOrGenerator.update || oldGenerator.update;
}
this.addPath(defaultPathGeneratorKey, pathGenerator, true);
}
async start() {
if (!guardCheck(this) || this.started) {
return;
}
await this.init();
this.started = true;
await new Promise((resolve) => {
this._delayTimeout = setTimeout(async () => {
this._eventListeners.addListeners();
if (this.interactivity.element instanceof HTMLElement && this._intersectionObserver) {
this._intersectionObserver.observe(this.interactivity.element);
}
for (const [, plugin] of this.plugins) {
plugin.start && (await plugin.start());
}
this._engine.dispatchEvent("containerStarted", { container: this });
this.play();
resolve();
}, this._delay);
});
}
stop() {
if (!guardCheck(this) || !this.started) {
return;
}
if (this._delayTimeout) {
clearTimeout(this._delayTimeout);
delete this._delayTimeout;
}
this._firstStart = true;
this.started = false;
this._eventListeners.removeListeners();
this.pause();
this.particles.clear();
this.canvas.stop();
if (this.interactivity.element instanceof HTMLElement && this._intersectionObserver) {
this._intersectionObserver.unobserve(this.interactivity.element);
}
for (const [, plugin] of this.plugins) {
plugin.stop && plugin.stop();
}
for (const key of this.plugins.keys()) {
this.plugins.delete(key);
}
this._sourceOptions = this._options;
this._engine.dispatchEvent("containerStopped", { container: this });
}
updateActualOptions() {
this.actualOptions.responsive = [];
const newMaxWidth = this.actualOptions.setResponsive(this.canvas.size.width, this.retina.pixelRatio, this._options);
this.actualOptions.setTheme(this._currentTheme);
if (this.responsiveMaxWidth === newMaxWidth) {
return false;
}
this.responsiveMaxWidth = newMaxWidth;
return true;
}
}