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