UNPKG

15.2 kBJavaScriptView Raw
1import { animate, cancelAnimation } from "../Utils/Utils";
2import { Canvas } from "./Canvas";
3import { EventListeners } from "./Utils/EventListeners";
4import { FrameManager } from "./Utils/FrameManager";
5import { Options } from "../Options/Classes/Options";
6import { Particles } from "./Particles";
7import { Retina } from "./Retina";
8import { getRangeValue } from "../Utils/NumberUtils";
9import { loadOptions } from "../Utils/OptionsUtils";
10function guardCheck(container) {
11 return container && !container.destroyed;
12}
13function loadContainerOptions(engine, container, ...sourceOptionsArr) {
14 const options = new Options(engine, container);
15 loadOptions(options, ...sourceOptionsArr);
16 return options;
17}
18const defaultPathGeneratorKey = "default", defaultPathGenerator = {
19 generate: (p) => {
20 const v = p.velocity.copy();
21 v.angle += (v.length * Math.PI) / 180;
22 return v;
23 },
24 init: () => {
25 },
26 update: () => {
27 },
28 reset: () => {
29 },
30};
31export class Container {
32 constructor(engine, id, sourceOptions) {
33 this.id = id;
34 this._engine = engine;
35 this.fpsLimit = 120;
36 this.smooth = false;
37 this._delay = 0;
38 this.duration = 0;
39 this.lifeTime = 0;
40 this._firstStart = true;
41 this.started = false;
42 this.destroyed = false;
43 this._paused = true;
44 this.lastFrameTime = 0;
45 this.zLayers = 100;
46 this.pageHidden = false;
47 this._sourceOptions = sourceOptions;
48 this._initialSourceOptions = sourceOptions;
49 this.retina = new Retina(this);
50 this.canvas = new Canvas(this);
51 this.particles = new Particles(this._engine, this);
52 this.frameManager = new FrameManager(this);
53 this.pathGenerators = new Map();
54 this.interactivity = {
55 mouse: {
56 clicking: false,
57 inside: false,
58 },
59 };
60 this.plugins = new Map();
61 this.drawers = new Map();
62 this._options = loadContainerOptions(this._engine, this);
63 this.actualOptions = loadContainerOptions(this._engine, this);
64 this._eventListeners = new EventListeners(this);
65 if (typeof IntersectionObserver !== "undefined" && IntersectionObserver) {
66 this._intersectionObserver = new IntersectionObserver((entries) => this._intersectionManager(entries));
67 }
68 this._engine.dispatchEvent("containerBuilt", { container: this });
69 }
70 get options() {
71 return this._options;
72 }
73 get sourceOptions() {
74 return this._sourceOptions;
75 }
76 addClickHandler(callback) {
77 if (!guardCheck(this)) {
78 return;
79 }
80 const el = this.interactivity.element;
81 if (!el) {
82 return;
83 }
84 const clickOrTouchHandler = (e, pos, radius) => {
85 if (!guardCheck(this)) {
86 return;
87 }
88 const pxRatio = this.retina.pixelRatio, posRetina = {
89 x: pos.x * pxRatio,
90 y: pos.y * pxRatio,
91 }, particles = this.particles.quadTree.queryCircle(posRetina, radius * pxRatio);
92 callback(e, particles);
93 };
94 const clickHandler = (e) => {
95 if (!guardCheck(this)) {
96 return;
97 }
98 const mouseEvent = e, pos = {
99 x: mouseEvent.offsetX || mouseEvent.clientX,
100 y: mouseEvent.offsetY || mouseEvent.clientY,
101 };
102 clickOrTouchHandler(e, pos, 1);
103 };
104 const touchStartHandler = () => {
105 if (!guardCheck(this)) {
106 return;
107 }
108 touched = true;
109 touchMoved = false;
110 };
111 const touchMoveHandler = () => {
112 if (!guardCheck(this)) {
113 return;
114 }
115 touchMoved = true;
116 };
117 const touchEndHandler = (e) => {
118 if (!guardCheck(this)) {
119 return;
120 }
121 if (touched && !touchMoved) {
122 const touchEvent = e;
123 let lastTouch = touchEvent.touches[touchEvent.touches.length - 1];
124 if (!lastTouch) {
125 lastTouch = touchEvent.changedTouches[touchEvent.changedTouches.length - 1];
126 if (!lastTouch) {
127 return;
128 }
129 }
130 const element = this.canvas.element, canvasRect = element ? element.getBoundingClientRect() : undefined, pos = {
131 x: lastTouch.clientX - (canvasRect ? canvasRect.left : 0),
132 y: lastTouch.clientY - (canvasRect ? canvasRect.top : 0),
133 };
134 clickOrTouchHandler(e, pos, Math.max(lastTouch.radiusX, lastTouch.radiusY));
135 }
136 touched = false;
137 touchMoved = false;
138 };
139 const touchCancelHandler = () => {
140 if (!guardCheck(this)) {
141 return;
142 }
143 touched = false;
144 touchMoved = false;
145 };
146 let touched = false, touchMoved = false;
147 el.addEventListener("click", clickHandler);
148 el.addEventListener("touchstart", touchStartHandler);
149 el.addEventListener("touchmove", touchMoveHandler);
150 el.addEventListener("touchend", touchEndHandler);
151 el.addEventListener("touchcancel", touchCancelHandler);
152 }
153 addPath(key, generator, override = false) {
154 if (!guardCheck(this) || (!override && this.pathGenerators.has(key))) {
155 return false;
156 }
157 this.pathGenerators.set(key, generator !== null && generator !== void 0 ? generator : defaultPathGenerator);
158 return true;
159 }
160 destroy() {
161 if (!guardCheck(this)) {
162 return;
163 }
164 this.stop();
165 this.particles.destroy();
166 this.canvas.destroy();
167 for (const [, drawer] of this.drawers) {
168 if (drawer.destroy) {
169 drawer.destroy(this);
170 }
171 }
172 for (const key of this.drawers.keys()) {
173 this.drawers.delete(key);
174 }
175 this._engine.plugins.destroy(this);
176 this.destroyed = true;
177 const mainArr = this._engine.dom(), idx = mainArr.findIndex((t) => t === this);
178 if (idx >= 0) {
179 mainArr.splice(idx, 1);
180 }
181 this._engine.dispatchEvent("containerDestroyed", { container: this });
182 }
183 draw(force) {
184 if (!guardCheck(this)) {
185 return;
186 }
187 let refreshTime = force;
188 this._drawAnimationFrame = animate()(async (timestamp) => {
189 if (refreshTime) {
190 this.lastFrameTime = undefined;
191 refreshTime = false;
192 }
193 await this.frameManager.nextFrame(timestamp);
194 });
195 }
196 exportConfiguration() {
197 return JSON.stringify(this.actualOptions, (key, value) => {
198 if (key === "_engine" || key === "_container") {
199 return;
200 }
201 return value;
202 }, 2);
203 }
204 exportImage(callback, type, quality) {
205 const element = this.canvas.element;
206 if (element) {
207 element.toBlob(callback, type !== null && type !== void 0 ? type : "image/png", quality);
208 }
209 }
210 exportImg(callback) {
211 this.exportImage(callback);
212 }
213 getAnimationStatus() {
214 return !this._paused && !this.pageHidden && guardCheck(this);
215 }
216 handleClickMode(mode) {
217 if (!guardCheck(this)) {
218 return;
219 }
220 this.particles.handleClickMode(mode);
221 for (const [, plugin] of this.plugins) {
222 if (plugin.handleClickMode) {
223 plugin.handleClickMode(mode);
224 }
225 }
226 }
227 async init() {
228 if (!guardCheck(this)) {
229 return;
230 }
231 const shapes = this._engine.plugins.getSupportedShapes();
232 for (const type of shapes) {
233 const drawer = this._engine.plugins.getShapeDrawer(type);
234 if (drawer) {
235 this.drawers.set(type, drawer);
236 }
237 }
238 this._options = loadContainerOptions(this._engine, this, this._initialSourceOptions, this.sourceOptions);
239 this.actualOptions = loadContainerOptions(this._engine, this, this._options);
240 const availablePlugins = this._engine.plugins.getAvailablePlugins(this);
241 for (const [id, plugin] of availablePlugins) {
242 this.plugins.set(id, plugin);
243 }
244 this.retina.init();
245 await this.canvas.init();
246 this.updateActualOptions();
247 this.canvas.initBackground();
248 this.canvas.resize();
249 this.zLayers = this.actualOptions.zLayers;
250 this.duration = getRangeValue(this.actualOptions.duration) * 1000;
251 this._delay = getRangeValue(this.actualOptions.delay) * 1000;
252 this.lifeTime = 0;
253 this.fpsLimit = this.actualOptions.fpsLimit > 0 ? this.actualOptions.fpsLimit : 120;
254 this.smooth = this.actualOptions.smooth;
255 for (const [, drawer] of this.drawers) {
256 if (drawer.init) {
257 await drawer.init(this);
258 }
259 }
260 for (const [, plugin] of this.plugins) {
261 if (plugin.init) {
262 await plugin.init();
263 }
264 }
265 this._engine.dispatchEvent("containerInit", { container: this });
266 this.particles.init();
267 this.particles.setDensity();
268 for (const [, plugin] of this.plugins) {
269 if (plugin.particlesSetup) {
270 plugin.particlesSetup();
271 }
272 }
273 this._engine.dispatchEvent("particlesSetup", { container: this });
274 }
275 async loadTheme(name) {
276 if (!guardCheck(this)) {
277 return;
278 }
279 this._currentTheme = name;
280 await this.refresh();
281 }
282 pause() {
283 if (!guardCheck(this)) {
284 return;
285 }
286 if (this._drawAnimationFrame !== undefined) {
287 cancelAnimation()(this._drawAnimationFrame);
288 delete this._drawAnimationFrame;
289 }
290 if (this._paused) {
291 return;
292 }
293 for (const [, plugin] of this.plugins) {
294 if (plugin.pause) {
295 plugin.pause();
296 }
297 }
298 if (!this.pageHidden) {
299 this._paused = true;
300 }
301 this._engine.dispatchEvent("containerPaused", { container: this });
302 }
303 play(force) {
304 if (!guardCheck(this)) {
305 return;
306 }
307 const needsUpdate = this._paused || force;
308 if (this._firstStart && !this.actualOptions.autoPlay) {
309 this._firstStart = false;
310 return;
311 }
312 if (this._paused) {
313 this._paused = false;
314 }
315 if (needsUpdate) {
316 for (const [, plugin] of this.plugins) {
317 if (plugin.play) {
318 plugin.play();
319 }
320 }
321 }
322 this._engine.dispatchEvent("containerPlay", { container: this });
323 this.draw(needsUpdate || false);
324 }
325 async refresh() {
326 if (!guardCheck(this)) {
327 return;
328 }
329 this.stop();
330 return this.start();
331 }
332 async reset() {
333 if (!guardCheck(this)) {
334 return;
335 }
336 this._options = loadContainerOptions(this._engine, this);
337 return this.refresh();
338 }
339 setNoise(noiseOrGenerator, init, update) {
340 if (!guardCheck(this)) {
341 return;
342 }
343 this.setPath(noiseOrGenerator, init, update);
344 }
345 setPath(pathOrGenerator, init, update) {
346 if (!pathOrGenerator || !guardCheck(this)) {
347 return;
348 }
349 const pathGenerator = Object.assign({}, defaultPathGenerator);
350 if (typeof pathOrGenerator === "function") {
351 pathGenerator.generate = pathOrGenerator;
352 if (init) {
353 pathGenerator.init = init;
354 }
355 if (update) {
356 pathGenerator.update = update;
357 }
358 }
359 else {
360 const oldGenerator = pathGenerator;
361 pathGenerator.generate = pathOrGenerator.generate || oldGenerator.generate;
362 pathGenerator.init = pathOrGenerator.init || oldGenerator.init;
363 pathGenerator.update = pathOrGenerator.update || oldGenerator.update;
364 }
365 this.addPath(defaultPathGeneratorKey, pathGenerator, true);
366 }
367 async start() {
368 if (!guardCheck(this) || this.started) {
369 return;
370 }
371 await this.init();
372 this.started = true;
373 await new Promise((resolve) => {
374 this._delayTimeout = setTimeout(async () => {
375 this._eventListeners.addListeners();
376 if (this.interactivity.element instanceof HTMLElement && this._intersectionObserver) {
377 this._intersectionObserver.observe(this.interactivity.element);
378 }
379 for (const [, plugin] of this.plugins) {
380 if (plugin.start) {
381 await plugin.start();
382 }
383 }
384 this._engine.dispatchEvent("containerStarted", { container: this });
385 this.play();
386 resolve();
387 }, this._delay);
388 });
389 }
390 stop() {
391 if (!guardCheck(this) || !this.started) {
392 return;
393 }
394 if (this._delayTimeout) {
395 clearTimeout(this._delayTimeout);
396 delete this._delayTimeout;
397 }
398 this._firstStart = true;
399 this.started = false;
400 this._eventListeners.removeListeners();
401 this.pause();
402 this.particles.clear();
403 this.canvas.clear();
404 if (this.interactivity.element instanceof HTMLElement && this._intersectionObserver) {
405 this._intersectionObserver.unobserve(this.interactivity.element);
406 }
407 for (const [, plugin] of this.plugins) {
408 if (plugin.stop) {
409 plugin.stop();
410 }
411 }
412 for (const key of this.plugins.keys()) {
413 this.plugins.delete(key);
414 }
415 this._sourceOptions = this._options;
416 this._engine.dispatchEvent("containerStopped", { container: this });
417 }
418 updateActualOptions() {
419 this.actualOptions.responsive = [];
420 const newMaxWidth = this.actualOptions.setResponsive(this.canvas.size.width, this.retina.pixelRatio, this._options);
421 this.actualOptions.setTheme(this._currentTheme);
422 if (this.responsiveMaxWidth === newMaxWidth) {
423 return false;
424 }
425 this.responsiveMaxWidth = newMaxWidth;
426 return true;
427 }
428 _intersectionManager(entries) {
429 if (!guardCheck(this) || !this.actualOptions.pauseOnOutsideViewport) {
430 return;
431 }
432 for (const entry of entries) {
433 if (entry.target !== this.interactivity.element) {
434 continue;
435 }
436 (entry.isIntersecting ? this.play : this.pause)();
437 }
438 }
439}