440 lines
15 KiB
JavaScript
440 lines
15 KiB
JavaScript
import { animate, cancelAnimation } from "../Utils/Utils";
|
|
import { Canvas } from "./Canvas";
|
|
import { EventListeners } from "./Utils/EventListeners";
|
|
import { FrameManager } from "./Utils/FrameManager";
|
|
import { Options } from "../Options/Classes/Options";
|
|
import { Particles } from "./Particles";
|
|
import { Retina } from "./Retina";
|
|
import { getRangeValue } from "../Utils/NumberUtils";
|
|
import { loadOptions } from "../Utils/OptionsUtils";
|
|
function guardCheck(container) {
|
|
return container && !container.destroyed;
|
|
}
|
|
function loadContainerOptions(engine, container, ...sourceOptionsArr) {
|
|
const options = new Options(engine, container);
|
|
loadOptions(options, ...sourceOptionsArr);
|
|
return options;
|
|
}
|
|
const defaultPathGeneratorKey = "default", defaultPathGenerator = {
|
|
generate: (p) => {
|
|
const v = p.velocity.copy();
|
|
v.angle += (v.length * Math.PI) / 180;
|
|
return v;
|
|
},
|
|
init: () => {
|
|
},
|
|
update: () => {
|
|
},
|
|
reset: () => {
|
|
},
|
|
};
|
|
export class Container {
|
|
constructor(engine, id, sourceOptions) {
|
|
this.id = id;
|
|
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.frameManager = new FrameManager(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);
|
|
}
|
|
addPath(key, generator, override = false) {
|
|
if (!guardCheck(this) || (!override && this.pathGenerators.has(key))) {
|
|
return false;
|
|
}
|
|
this.pathGenerators.set(key, generator !== null && generator !== void 0 ? generator : defaultPathGenerator);
|
|
return true;
|
|
}
|
|
destroy() {
|
|
if (!guardCheck(this)) {
|
|
return;
|
|
}
|
|
this.stop();
|
|
this.particles.destroy();
|
|
this.canvas.destroy();
|
|
for (const [, drawer] of this.drawers) {
|
|
if (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 = animate()(async (timestamp) => {
|
|
if (refreshTime) {
|
|
this.lastFrameTime = undefined;
|
|
refreshTime = false;
|
|
}
|
|
await this.frameManager.nextFrame(timestamp);
|
|
});
|
|
}
|
|
exportConfiguration() {
|
|
return JSON.stringify(this.actualOptions, (key, value) => {
|
|
if (key === "_engine" || key === "_container") {
|
|
return;
|
|
}
|
|
return value;
|
|
}, 2);
|
|
}
|
|
exportImage(callback, type, quality) {
|
|
const element = this.canvas.element;
|
|
if (element) {
|
|
element.toBlob(callback, type !== null && type !== void 0 ? type : "image/png", quality);
|
|
}
|
|
}
|
|
exportImg(callback) {
|
|
this.exportImage(callback);
|
|
}
|
|
getAnimationStatus() {
|
|
return !this._paused && !this.pageHidden && guardCheck(this);
|
|
}
|
|
handleClickMode(mode) {
|
|
if (!guardCheck(this)) {
|
|
return;
|
|
}
|
|
this.particles.handleClickMode(mode);
|
|
for (const [, plugin] of this.plugins) {
|
|
if (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) {
|
|
if (drawer.init) {
|
|
await drawer.init(this);
|
|
}
|
|
}
|
|
for (const [, plugin] of this.plugins) {
|
|
if (plugin.init) {
|
|
await plugin.init();
|
|
}
|
|
}
|
|
this._engine.dispatchEvent("containerInit", { container: this });
|
|
this.particles.init();
|
|
this.particles.setDensity();
|
|
for (const [, plugin] of this.plugins) {
|
|
if (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) {
|
|
cancelAnimation()(this._drawAnimationFrame);
|
|
delete this._drawAnimationFrame;
|
|
}
|
|
if (this._paused) {
|
|
return;
|
|
}
|
|
for (const [, plugin] of this.plugins) {
|
|
if (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._options = loadContainerOptions(this._engine, this);
|
|
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 = Object.assign({}, defaultPathGenerator);
|
|
if (typeof pathOrGenerator === "function") {
|
|
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) {
|
|
if (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.clear();
|
|
if (this.interactivity.element instanceof HTMLElement && this._intersectionObserver) {
|
|
this._intersectionObserver.unobserve(this.interactivity.element);
|
|
}
|
|
for (const [, plugin] of this.plugins) {
|
|
if (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;
|
|
}
|
|
_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)();
|
|
}
|
|
}
|
|
}
|