UNPKG

16.2 kBJavaScriptView Raw
1/**
2 * [[include:Container.md]]
3 * @packageDocumentation
4 */
5import { Canvas } from "./Canvas";
6import { Particles } from "./Particles";
7import { Retina } from "./Retina";
8import { FrameManager } from "./FrameManager";
9import { Options } from "../Options/Classes/Options";
10import { animate, cancelAnimation, EventListeners, getRangeValue, Plugins } from "../Utils";
11import { Vector } from "./Particle/Vector";
12/**
13 * The object loaded into an HTML element, it'll contain options loaded and all data to let everything working
14 * [[include:Container.md]]
15 * @category Core
16 */
17export class Container {
18 /**
19 * This is the core class, create an instance to have a new working particles manager
20 * @constructor
21 * @param id the id to identify this instance
22 * @param sourceOptions the options to load
23 * @param presets all the presets to load with options
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 // nothing required
51 },
52 update: () => {
53 // nothing required
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 /* tsParticles variables with default values */
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 /* options settings */
82 this._options.load(this._sourceOptions);
83 /* ---------- tsParticles - start ------------ */
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 * The options used by the container, it's a full [[Options]] object
91 */
92 get options() {
93 return this._options;
94 }
95 get sourceOptions() {
96 return this._sourceOptions;
97 }
98 /**
99 * Starts animations and resume from pause
100 * @param force
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 * Pauses animations
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 * Draws a frame
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 * Gets the animation status
155 * @returns `true` is playing, `false` is paused
156 */
157 getAnimationStatus() {
158 return !this.paused && !this.pageHidden;
159 }
160 /**
161 * Customise path generation
162 * @deprecated Use the new setPath
163 * @param noiseOrGenerator the [[IMovePathGenerator]] object or a function that generates a [[Vector]] object from [[Particle]]
164 * @param init the [[IMovePathGenerator]] init function, if the first parameter is a generator function
165 * @param update the [[IMovePathGenerator]] update function, if the first parameter is a generator function
166 */
167 setNoise(noiseOrGenerator, init, update) {
168 this.setPath(noiseOrGenerator, init, update);
169 }
170 /**
171 * Customise path generation
172 * @param pathOrGenerator the [[IMovePathGenerator]] object or a function that generates a [[Vector]] object from [[Particle]]
173 * @param init the [[IMovePathGenerator]] init function, if the first parameter is a generator function
174 * @param update the [[IMovePathGenerator]] update function, if the first parameter is a generator function
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 * Destroys the current container, invalidating it
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 * @deprecated this method is deprecated, please use the exportImage method
219 * @param callback The callback to handle the image
220 */
221 exportImg(callback) {
222 this.exportImage(callback);
223 }
224 /**
225 * Exports the current canvas image, `background` property of `options` won't be rendered because it's css related
226 * @param callback The callback to handle the image
227 * @param type The exported image type
228 * @param quality The exported image quality
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 * Exports the current configuration using `options` property
236 * @returns a JSON string created from `options` property
237 */
238 exportConfiguration() {
239 return JSON.stringify(this.actualOptions, undefined, 2);
240 }
241 /**
242 * Restarts the container, just a [[stop]]/[[start]] alias
243 */
244 refresh() {
245 /* restart */
246 this.stop();
247 return this.start();
248 }
249 reset() {
250 this._options = new Options();
251 return this.refresh();
252 }
253 /**
254 * Stops the container, opposite to `start`. Clears some resources and stops events.
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 * Loads the given theme, overriding the options
283 * @param name the theme name, if `undefined` resets the default options or the default theme
284 */
285 async loadTheme(name) {
286 this.currentTheme = name;
287 await this.refresh();
288 }
289 /**
290 * Starts the container, initializes what are needed to create animations and event handling
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 /* init canvas + particles */
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}