@tsparticles/engine 4.0.0-beta.15 → 4.0.0-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/browser/Core/CanvasManager.js +73 -36
  2. package/browser/Core/Engine.js +11 -5
  3. package/browser/Core/Particle.js +1 -3
  4. package/browser/Core/ParticlesManager.js +157 -78
  5. package/browser/Core/Retina.js +2 -3
  6. package/browser/Core/Utils/EventListeners.js +1 -1
  7. package/cjs/Core/CanvasManager.js +73 -36
  8. package/cjs/Core/Engine.js +11 -5
  9. package/cjs/Core/Particle.js +1 -3
  10. package/cjs/Core/ParticlesManager.js +157 -78
  11. package/cjs/Core/Retina.js +2 -3
  12. package/cjs/Core/Utils/EventListeners.js +1 -1
  13. package/esm/Core/CanvasManager.js +73 -36
  14. package/esm/Core/Engine.js +11 -5
  15. package/esm/Core/Particle.js +1 -3
  16. package/esm/Core/ParticlesManager.js +157 -78
  17. package/esm/Core/Retina.js +2 -3
  18. package/esm/Core/Utils/EventListeners.js +1 -1
  19. package/package.json +1 -1
  20. package/report.html +1 -1
  21. package/scripts/install.js +321 -220
  22. package/tsparticles.engine.js +245 -126
  23. package/tsparticles.engine.min.js +1 -1
  24. package/types/Core/CanvasManager.d.ts +3 -2
  25. package/types/Core/Container.d.ts +1 -2
  26. package/types/Core/Interfaces/IContainerPlugin.d.ts +7 -8
  27. package/types/Core/Interfaces/IDrawParticleParams.d.ts +1 -2
  28. package/types/Core/Interfaces/ILoadParams.d.ts +1 -1
  29. package/types/Core/Interfaces/IParticleUpdater.d.ts +1 -2
  30. package/types/Core/Interfaces/IShapeDrawData.d.ts +1 -2
  31. package/types/Core/ParticlesManager.d.ts +11 -5
  32. package/types/Core/RenderManager.d.ts +2 -3
  33. package/types/Core/Utils/PluginManager.d.ts +5 -6
  34. package/types/Options/Classes/Options.d.ts +1 -2
  35. package/types/Utils/CanvasUtils.d.ts +4 -5
  36. package/types/Utils/LogUtils.d.ts +1 -2
  37. package/types/Utils/Utils.d.ts +1 -2
  38. package/types/export-types.d.ts +0 -1
  39. package/browser/Types/CanvasContextType.js +0 -1
  40. package/cjs/Types/CanvasContextType.js +0 -1
  41. package/esm/Types/CanvasContextType.js +0 -1
  42. package/types/Types/CanvasContextType.d.ts +0 -1
@@ -2,6 +2,25 @@ import { cloneStyle, getFullScreenStyle, safeMatchMedia, safeMutationObserver }
2
2
  import { defaultZoom, generatedAttribute, half } from "./Utils/Constants.js";
3
3
  import { getStyleFromRgb, rangeColorToRgb } from "../Utils/ColorUtils.js";
4
4
  import { RenderManager } from "./RenderManager.js";
5
+ const transferredCanvases = new WeakMap(), getTransferredCanvas = (canvas) => {
6
+ const transferredCanvas = transferredCanvases.get(canvas);
7
+ if (transferredCanvas) {
8
+ return transferredCanvas;
9
+ }
10
+ if (typeof canvas.transferControlToOffscreen !== "function") {
11
+ throw new TypeError("OffscreenCanvas is required but not supported by this browser");
12
+ }
13
+ try {
14
+ const offscreenCanvas = canvas.transferControlToOffscreen();
15
+ transferredCanvases.set(canvas, offscreenCanvas);
16
+ return offscreenCanvas;
17
+ }
18
+ catch {
19
+ throw new TypeError("OffscreenCanvas transfer failed");
20
+ }
21
+ }, isHtmlCanvasElement = (canvas) => {
22
+ return typeof HTMLCanvasElement !== "undefined" && canvas instanceof HTMLCanvasElement;
23
+ };
5
24
  function setStyle(canvas, style, important = false) {
6
25
  if (!style) {
7
26
  return;
@@ -32,8 +51,9 @@ function setStyle(canvas, style, important = false) {
32
51
  }
33
52
  }
34
53
  export class CanvasManager {
35
- element;
54
+ domElement;
36
55
  render;
56
+ renderCanvas;
37
57
  size;
38
58
  zoom = defaultZoom;
39
59
  _container;
@@ -68,9 +88,10 @@ export class CanvasManager {
68
88
  destroy() {
69
89
  this.stop();
70
90
  if (this._generated) {
71
- const element = this.element;
91
+ const element = this.domElement;
72
92
  element?.remove();
73
- this.element = undefined;
93
+ this.domElement = undefined;
94
+ this.renderCanvas = undefined;
74
95
  }
75
96
  else {
76
97
  this._resetOriginalStyle();
@@ -103,16 +124,17 @@ export class CanvasManager {
103
124
  this._initStyle();
104
125
  this.initBackground();
105
126
  this._safeMutationObserver(obs => {
106
- if (!this.element || !(this.element instanceof Node)) {
127
+ const element = this.domElement;
128
+ if (!element || !(element instanceof Node)) {
107
129
  return;
108
130
  }
109
- obs.observe(this.element, { attributes: true });
131
+ obs.observe(element, { attributes: true });
110
132
  });
111
133
  this.initPlugins();
112
134
  this.render.init();
113
135
  }
114
136
  initBackground() {
115
- const { _container } = this, options = _container.actualOptions, background = options.background, element = this.element;
137
+ const { _container } = this, options = _container.actualOptions, background = options.background, element = this.domElement;
116
138
  if (!element) {
117
139
  return;
118
140
  }
@@ -137,21 +159,30 @@ export class CanvasManager {
137
159
  }
138
160
  }
139
161
  loadCanvas(canvas) {
140
- if (this._generated && this.element) {
141
- this.element.remove();
162
+ if (this._generated && this.domElement) {
163
+ this.domElement.remove();
164
+ }
165
+ const container = this._container, domCanvas = isHtmlCanvasElement(canvas) ? canvas : undefined;
166
+ this.domElement = domCanvas;
167
+ this._generated = domCanvas ? domCanvas.dataset[generatedAttribute] === "true" : false;
168
+ this.renderCanvas = domCanvas ? getTransferredCanvas(domCanvas) : canvas;
169
+ const domElement = this.domElement;
170
+ if (domElement) {
171
+ domElement.ariaHidden = "true";
172
+ this._originalStyle = cloneStyle(domElement.style);
173
+ }
174
+ const standardSize = this._standardSize, renderCanvas = this.renderCanvas;
175
+ if (domElement) {
176
+ standardSize.height = domElement.offsetHeight;
177
+ standardSize.width = domElement.offsetWidth;
178
+ }
179
+ else {
180
+ standardSize.height = renderCanvas.height;
181
+ standardSize.width = renderCanvas.width;
142
182
  }
143
- const container = this._container;
144
- this._generated =
145
- generatedAttribute in canvas.dataset ? canvas.dataset[generatedAttribute] === "true" : this._generated;
146
- this.element = canvas;
147
- this.element.ariaHidden = "true";
148
- this._originalStyle = cloneStyle(this.element.style);
149
- const standardSize = this._standardSize;
150
- standardSize.height = canvas.offsetHeight;
151
- standardSize.width = canvas.offsetWidth;
152
183
  const pxRatio = this._container.retina.pixelRatio, retinaSize = this.size;
153
- canvas.height = retinaSize.height = standardSize.height * pxRatio;
154
- canvas.width = retinaSize.width = standardSize.width * pxRatio;
184
+ renderCanvas.height = retinaSize.height = standardSize.height * pxRatio;
185
+ renderCanvas.width = retinaSize.width = standardSize.width * pxRatio;
155
186
  const canSupportHdrQuery = safeMatchMedia("(color-gamut: p3)");
156
187
  this.render.setContextSettings({
157
188
  alpha: true,
@@ -159,42 +190,48 @@ export class CanvasManager {
159
190
  desynchronized: true,
160
191
  willReadFrequently: false,
161
192
  });
162
- this.render.setContext(this.element.getContext("2d", this.render.settings));
193
+ this.render.setContext(renderCanvas.getContext("2d", this.render.settings));
163
194
  this._safeMutationObserver(obs => {
164
195
  obs.disconnect();
165
196
  });
166
197
  container.retina.init();
167
198
  this.initBackground();
168
199
  this._safeMutationObserver(obs => {
169
- if (!this.element || !(this.element instanceof Node)) {
200
+ const element = this.domElement;
201
+ if (!element || !(element instanceof Node)) {
170
202
  return;
171
203
  }
172
- obs.observe(this.element, { attributes: true });
204
+ obs.observe(element, { attributes: true });
173
205
  });
174
206
  }
175
207
  resize() {
176
- if (!this.element) {
208
+ const element = this.domElement;
209
+ if (!element) {
210
+ return false;
211
+ }
212
+ const container = this._container, renderCanvas = this.renderCanvas;
213
+ if (renderCanvas === undefined) {
177
214
  return false;
178
215
  }
179
- const container = this._container, currentSize = container.canvas._standardSize, newSize = {
180
- width: this.element.offsetWidth,
181
- height: this.element.offsetHeight,
216
+ const currentSize = container.canvas._standardSize, newSize = {
217
+ width: element.offsetWidth,
218
+ height: element.offsetHeight,
182
219
  }, pxRatio = container.retina.pixelRatio, retinaSize = {
183
220
  width: newSize.width * pxRatio,
184
221
  height: newSize.height * pxRatio,
185
222
  };
186
223
  if (newSize.height === currentSize.height &&
187
224
  newSize.width === currentSize.width &&
188
- retinaSize.height === this.element.height &&
189
- retinaSize.width === this.element.width) {
225
+ retinaSize.height === renderCanvas.height &&
226
+ retinaSize.width === renderCanvas.width) {
190
227
  return false;
191
228
  }
192
229
  const oldSize = { ...currentSize };
193
230
  currentSize.height = newSize.height;
194
231
  currentSize.width = newSize.width;
195
232
  const canvasSize = this.size;
196
- this.element.width = canvasSize.width = retinaSize.width;
197
- this.element.height = canvasSize.height = retinaSize.height;
233
+ renderCanvas.width = canvasSize.width = retinaSize.width;
234
+ renderCanvas.height = canvasSize.height = retinaSize.height;
198
235
  if (this._container.started) {
199
236
  container.particles.setResizeFactor({
200
237
  width: currentSize.width / oldSize.width,
@@ -204,7 +241,7 @@ export class CanvasManager {
204
241
  return true;
205
242
  }
206
243
  setPointerEvents(type) {
207
- const element = this.element;
244
+ const element = this.domElement;
208
245
  if (!element) {
209
246
  return;
210
247
  }
@@ -223,7 +260,7 @@ export class CanvasManager {
223
260
  this.render.stop();
224
261
  }
225
262
  async windowResize() {
226
- if (!this.element || !this.resize()) {
263
+ if (!this.domElement || !this.resize()) {
227
264
  return;
228
265
  }
229
266
  const container = this._container, needsRefresh = container.updateActualOptions();
@@ -239,7 +276,7 @@ export class CanvasManager {
239
276
  }
240
277
  };
241
278
  _initStyle = () => {
242
- const element = this.element, options = this._container.actualOptions;
279
+ const element = this.domElement, options = this._container.actualOptions;
243
280
  if (!element) {
244
281
  return;
245
282
  }
@@ -261,7 +298,7 @@ export class CanvasManager {
261
298
  }
262
299
  };
263
300
  _repairStyle = () => {
264
- const element = this.element;
301
+ const element = this.domElement;
265
302
  if (!element) {
266
303
  return;
267
304
  }
@@ -281,7 +318,7 @@ export class CanvasManager {
281
318
  });
282
319
  };
283
320
  _resetOriginalStyle = () => {
284
- const element = this.element, originalStyle = this._originalStyle;
321
+ const element = this.domElement, originalStyle = this._originalStyle;
285
322
  if (!element || !originalStyle) {
286
323
  return;
287
324
  }
@@ -294,7 +331,7 @@ export class CanvasManager {
294
331
  callback(this._mutationObserver);
295
332
  };
296
333
  _setFullScreenStyle = () => {
297
- const element = this.element;
334
+ const element = this.domElement;
298
335
  if (!element) {
299
336
  return;
300
337
  }
@@ -32,7 +32,7 @@ const getCanvasFromContainer = (domContainer) => {
32
32
  }
33
33
  }
34
34
  else {
35
- const existingCanvases = domContainer.getElementsByTagName(canvasTag), foundCanvas = existingCanvases[canvasFirstIndex];
35
+ const existingCanvases = domContainer.getElementsByTagName(canvasTag), foundCanvas = existingCanvases.item(canvasFirstIndex);
36
36
  if (foundCanvas) {
37
37
  canvasEl = foundCanvas;
38
38
  canvasEl.dataset[generatedAttribute] = generatedFalse;
@@ -69,7 +69,7 @@ export class Engine {
69
69
  return this._domArray;
70
70
  }
71
71
  get version() {
72
- return "4.0.0-beta.15";
72
+ return "4.0.0-beta.17";
73
73
  }
74
74
  addEventListener(type, listener) {
75
75
  this._eventDispatcher.addEventListener(type, listener);
@@ -100,7 +100,11 @@ export class Engine {
100
100
  }
101
101
  async load(params) {
102
102
  await this.init();
103
- const { Container } = await import("./Container.js"), id = params.id ?? params.element?.id ?? `tsparticles${Math.floor(getRandom() * loadRandomFactor).toString()}`, { index, url } = params, options = url ? await getDataFromUrl({ fallback: params.options, url, index }) : params.options, currentOptions = itemFromSingleOrMultiple(options, index), { items } = this, oldIndex = items.findIndex(v => v.id.description === id), newItem = new Container({
103
+ let domSourceElement;
104
+ if (typeof HTMLElement !== "undefined" && params.element instanceof HTMLElement) {
105
+ domSourceElement = params.element;
106
+ }
107
+ const { Container } = await import("./Container.js"), id = params.id ?? domSourceElement?.id ?? `tsparticles${Math.floor(getRandom() * loadRandomFactor).toString()}`, { index, url } = params, options = url ? await getDataFromUrl({ fallback: params.options, url, index }) : params.options, currentOptions = itemFromSingleOrMultiple(options, index), { items } = this, oldIndex = items.findIndex(v => v.id.description === id), newItem = new Container({
104
108
  dispatchCallback: (eventType, args) => {
105
109
  this.dispatchEvent(eventType, args);
106
110
  },
@@ -127,8 +131,10 @@ export class Engine {
127
131
  else {
128
132
  items.push(newItem);
129
133
  }
130
- const domContainer = getDomContainer(id, params.element), canvasEl = getCanvasFromContainer(domContainer);
131
- newItem.canvas.loadCanvas(canvasEl);
134
+ const sourceCanvas = typeof OffscreenCanvas !== "undefined" && params.element instanceof OffscreenCanvas
135
+ ? params.element
136
+ : getCanvasFromContainer(getDomContainer(id, domSourceElement));
137
+ newItem.canvas.loadCanvas(sourceCanvas);
132
138
  await newItem.start();
133
139
  return newItem;
134
140
  }
@@ -242,8 +242,6 @@ export class Particle {
242
242
  this._initPosition(position);
243
243
  this.initialVelocity = this._calculateVelocity();
244
244
  this.velocity = this.initialVelocity.copy();
245
- const particles = container.particles;
246
- particles.setLastZIndex(this.position.z);
247
245
  this.zIndexFactor = this.position.z / container.zLayers;
248
246
  this.sides = 24;
249
247
  let effectDrawer, shapeDrawer;
@@ -393,7 +391,7 @@ export class Particle {
393
391
  return color;
394
392
  };
395
393
  _initPosition = position => {
396
- const container = this._container, zIndexValue = getRangeValue(this.options.zIndex.value), initialPosition = this._calcPosition(position, clamp(zIndexValue, minZ, container.zLayers));
394
+ const container = this._container, zIndexValue = Math.floor(getRangeValue(this.options.zIndex.value)), initialPosition = this._calcPosition(position, clamp(zIndexValue, minZ, container.zLayers));
397
395
  if (!initialPosition) {
398
396
  throw new Error("a valid position cannot be found for particle");
399
397
  }
@@ -1,4 +1,4 @@
1
- import { countOffset, defaultDensityFactor, defaultRemoveQuantity, deleteCount, lengthOffset, minCount, minIndex, minLimit, spatialHashGridCellSize, squareExp, } from "./Utils/Constants.js";
1
+ import { countOffset, defaultDensityFactor, defaultRemoveQuantity, deleteCount, double, empty, minCount, minIndex, minLimit, one, spatialHashGridCellSize, squareExp, } from "./Utils/Constants.js";
2
2
  import { EventType } from "../Enums/Types/EventType.js";
3
3
  import { LimitMode } from "../Enums/Modes/LimitMode.js";
4
4
  import { Particle } from "./Particle.js";
@@ -12,10 +12,8 @@ export class ParticlesManager {
12
12
  _container;
13
13
  _groupLimits;
14
14
  _limit;
15
- _maxZIndex;
16
- _minZIndex;
17
- _needsSort;
18
15
  _nextId;
16
+ _particleBuckets;
19
17
  _particleResetPlugins;
20
18
  _particleUpdatePlugins;
21
19
  _pluginManager;
@@ -24,19 +22,17 @@ export class ParticlesManager {
24
22
  _postUpdatePlugins;
25
23
  _resizeFactor;
26
24
  _updatePlugins;
27
- _zArray;
25
+ _zBuckets;
28
26
  constructor(pluginManager, container) {
29
27
  this._pluginManager = pluginManager;
30
28
  this._container = container;
31
29
  this._nextId = 0;
32
30
  this._array = [];
33
- this._zArray = [];
34
31
  this._pool = [];
35
32
  this._limit = 0;
36
33
  this._groupLimits = new Map();
37
- this._needsSort = false;
38
- this._minZIndex = 0;
39
- this._maxZIndex = 0;
34
+ this._particleBuckets = new Map();
35
+ this._zBuckets = this._createBuckets(this._container.zLayers);
40
36
  this.grid = new SpatialHashGrid(spatialHashGridCellSize);
41
37
  this.checkParticlePositionPlugins = [];
42
38
  this._particleResetPlugins = [];
@@ -80,7 +76,7 @@ export class ParticlesManager {
80
76
  return;
81
77
  }
82
78
  this._array.push(particle);
83
- this._zArray.push(particle);
79
+ this._insertParticleIntoBucket(particle);
84
80
  this._nextId++;
85
81
  this._container.dispatchEvent(EventType.particleAdded, {
86
82
  particle,
@@ -94,12 +90,14 @@ export class ParticlesManager {
94
90
  }
95
91
  clear() {
96
92
  this._array = [];
97
- this._zArray = [];
93
+ this._particleBuckets.clear();
94
+ this._resetBuckets(this._container.zLayers);
98
95
  }
99
96
  destroy() {
100
97
  this._array = [];
101
98
  this._pool.length = 0;
102
- this._zArray = [];
99
+ this._particleBuckets.clear();
100
+ this._zBuckets = [];
103
101
  this.checkParticlePositionPlugins = [];
104
102
  this._particleResetPlugins = [];
105
103
  this._particleUpdatePlugins = [];
@@ -108,8 +106,14 @@ export class ParticlesManager {
108
106
  this._updatePlugins = [];
109
107
  }
110
108
  drawParticles(delta) {
111
- for (const particle of this._zArray) {
112
- particle.draw(delta);
109
+ for (let i = this._zBuckets.length - one; i >= minIndex; i--) {
110
+ const bucket = this._zBuckets[i];
111
+ if (!bucket) {
112
+ continue;
113
+ }
114
+ for (const particle of bucket) {
115
+ particle.draw(delta);
116
+ }
113
117
  }
114
118
  }
115
119
  filter(condition) {
@@ -123,15 +127,14 @@ export class ParticlesManager {
123
127
  }
124
128
  async init() {
125
129
  const container = this._container, options = container.actualOptions;
126
- this._minZIndex = 0;
127
- this._maxZIndex = 0;
128
- this._needsSort = false;
129
130
  this.checkParticlePositionPlugins = [];
130
131
  this._updatePlugins = [];
131
132
  this._particleUpdatePlugins = [];
132
133
  this._postUpdatePlugins = [];
133
134
  this._particleResetPlugins = [];
134
135
  this._postParticleUpdatePlugins = [];
136
+ this._particleBuckets.clear();
137
+ this._resetBuckets(container.zLayers);
135
138
  this.grid = new SpatialHashGrid(spatialHashGridCellSize * container.retina.pixelRatio);
136
139
  for (const plugin of container.plugins) {
137
140
  if (plugin.redrawInit) {
@@ -232,79 +235,25 @@ export class ParticlesManager {
232
235
  }
233
236
  this._applyDensity(options.particles, pluginsCount);
234
237
  }
235
- setLastZIndex(zIndex) {
236
- this._needsSort ||= zIndex >= this._maxZIndex || (zIndex > this._minZIndex && zIndex < this._maxZIndex);
237
- }
238
238
  setResizeFactor(factor) {
239
239
  this._resizeFactor = factor;
240
240
  }
241
241
  update(delta) {
242
- const particlesToDelete = new Set();
243
242
  this.grid.clear();
244
243
  for (const plugin of this._updatePlugins) {
245
244
  plugin.update?.(delta);
246
245
  }
247
- const resizeFactor = this._resizeFactor;
248
- for (const particle of this._array) {
249
- if (resizeFactor && !particle.ignoresResizeRatio) {
250
- particle.position.x *= resizeFactor.width;
251
- particle.position.y *= resizeFactor.height;
252
- particle.initialPosition.x *= resizeFactor.width;
253
- particle.initialPosition.y *= resizeFactor.height;
254
- }
255
- particle.ignoresResizeRatio = false;
256
- for (const plugin of this._particleResetPlugins) {
257
- plugin.particleReset?.(particle);
258
- }
259
- for (const plugin of this._particleUpdatePlugins) {
260
- if (particle.destroyed) {
261
- break;
262
- }
263
- plugin.particleUpdate?.(particle, delta);
264
- }
265
- if (particle.destroyed) {
266
- particlesToDelete.add(particle);
267
- continue;
268
- }
269
- this.grid.insert(particle);
270
- }
246
+ const particlesToDelete = this._updateParticlesPhase1(delta);
271
247
  for (const plugin of this._postUpdatePlugins) {
272
248
  plugin.postUpdate?.(delta);
273
249
  }
274
- for (const particle of this._array) {
275
- if (particle.destroyed) {
276
- particlesToDelete.add(particle);
277
- continue;
278
- }
279
- for (const updater of this._container.particleUpdaters) {
280
- updater.update(particle, delta);
281
- }
282
- if (!particle.destroyed && !particle.spawning) {
283
- for (const plugin of this._postParticleUpdatePlugins) {
284
- plugin.postParticleUpdate?.(particle, delta);
285
- }
286
- }
287
- else if (particle.destroyed) {
288
- particlesToDelete.add(particle);
289
- }
290
- }
250
+ this._updateParticlesPhase2(delta, particlesToDelete);
291
251
  if (particlesToDelete.size) {
292
252
  for (const particle of particlesToDelete) {
293
253
  this.remove(particle);
294
254
  }
295
255
  }
296
256
  delete this._resizeFactor;
297
- if (this._needsSort) {
298
- const zArray = this._zArray;
299
- zArray.sort((a, b) => b.position.z - a.position.z || a.id - b.id);
300
- const firstItem = zArray[minIndex], lastItem = zArray[zArray.length - lengthOffset];
301
- if (!firstItem || !lastItem) {
302
- return;
303
- }
304
- this._maxZIndex = firstItem.position.z;
305
- this._minZIndex = lastItem.position.z;
306
- this._needsSort = false;
307
- }
308
257
  }
309
258
  _addToPool = (...particles) => {
310
259
  this._pool.push(...particles);
@@ -334,13 +283,52 @@ export class ParticlesManager {
334
283
  this.removeQuantity(particlesCount - particlesNumber, group);
335
284
  }
336
285
  };
286
+ _createBuckets = (zLayers) => {
287
+ const bucketCount = Math.max(Math.floor(zLayers), one);
288
+ return Array.from({ length: bucketCount }, () => []);
289
+ };
290
+ _getBucketIndex = (zIndex) => {
291
+ const maxBucketIndex = this._zBuckets.length - one;
292
+ if (maxBucketIndex <= minIndex) {
293
+ return minIndex;
294
+ }
295
+ return Math.min(Math.max(Math.floor(zIndex), minIndex), maxBucketIndex);
296
+ };
297
+ _getParticleInsertIndex = (bucket, particleId) => {
298
+ let start = minIndex, end = bucket.length;
299
+ while (start < end) {
300
+ const middle = Math.floor((start + end) / double), middleParticle = bucket[middle];
301
+ if (!middleParticle) {
302
+ end = middle;
303
+ continue;
304
+ }
305
+ if (middleParticle.id < particleId) {
306
+ start = middle + one;
307
+ }
308
+ else {
309
+ end = middle;
310
+ }
311
+ }
312
+ return start;
313
+ };
337
314
  _initDensityFactor = densityOptions => {
338
315
  const container = this._container;
339
- if (!container.canvas.element || !densityOptions.enable) {
316
+ if (!densityOptions.enable) {
340
317
  return defaultDensityFactor;
341
318
  }
342
- const canvas = container.canvas.element, pxRatio = container.retina.pixelRatio;
343
- return (canvas.width * canvas.height) / (densityOptions.height * densityOptions.width * pxRatio ** squareExp);
319
+ const canvasSize = container.canvas.size, pxRatio = container.retina.pixelRatio;
320
+ if (!canvasSize.width || !canvasSize.height) {
321
+ return defaultDensityFactor;
322
+ }
323
+ return ((canvasSize.width * canvasSize.height) / (densityOptions.height * densityOptions.width * pxRatio ** squareExp));
324
+ };
325
+ _insertParticleIntoBucket = (particle) => {
326
+ const bucketIndex = this._getBucketIndex(particle.position.z), bucket = this._zBuckets[bucketIndex];
327
+ if (!bucket) {
328
+ return;
329
+ }
330
+ bucket.splice(this._getParticleInsertIndex(bucket, particle.id), empty, particle);
331
+ this._particleBuckets.set(particle.id, bucketIndex);
344
332
  };
345
333
  _removeParticle = (index, group, override) => {
346
334
  const particle = this._array[index];
@@ -350,9 +338,8 @@ export class ParticlesManager {
350
338
  if (particle.group !== group) {
351
339
  return false;
352
340
  }
353
- const zIdx = this._zArray.indexOf(particle);
354
341
  this._array.splice(index, deleteCount);
355
- this._zArray.splice(zIdx, deleteCount);
342
+ this._removeParticleFromBucket(particle);
356
343
  particle.destroy(override);
357
344
  this._container.dispatchEvent(EventType.particleRemoved, {
358
345
  particle,
@@ -360,4 +347,96 @@ export class ParticlesManager {
360
347
  this._addToPool(particle);
361
348
  return true;
362
349
  };
350
+ _removeParticleFromBucket = (particle) => {
351
+ const bucketIndex = this._particleBuckets.get(particle.id) ?? this._getBucketIndex(particle.position.z), bucket = this._zBuckets[bucketIndex];
352
+ if (!bucket) {
353
+ this._particleBuckets.delete(particle.id);
354
+ return;
355
+ }
356
+ const particleIndex = this._getParticleInsertIndex(bucket, particle.id);
357
+ if (bucket[particleIndex]?.id !== particle.id) {
358
+ this._particleBuckets.delete(particle.id);
359
+ return;
360
+ }
361
+ bucket.splice(particleIndex, deleteCount);
362
+ this._particleBuckets.delete(particle.id);
363
+ };
364
+ _resetBuckets = (zLayers) => {
365
+ const bucketCount = Math.max(Math.floor(zLayers), one);
366
+ if (this._zBuckets.length !== bucketCount) {
367
+ this._zBuckets = this._createBuckets(bucketCount);
368
+ return;
369
+ }
370
+ for (const bucket of this._zBuckets) {
371
+ bucket.length = minIndex;
372
+ }
373
+ };
374
+ _updateParticleBucket = (particle) => {
375
+ const newBucketIndex = this._getBucketIndex(particle.position.z), currentBucketIndex = this._particleBuckets.get(particle.id);
376
+ if (currentBucketIndex === undefined) {
377
+ this._insertParticleIntoBucket(particle);
378
+ return;
379
+ }
380
+ if (currentBucketIndex === newBucketIndex) {
381
+ return;
382
+ }
383
+ const currentBucket = this._zBuckets[currentBucketIndex];
384
+ if (currentBucket) {
385
+ const particleIndex = this._getParticleInsertIndex(currentBucket, particle.id);
386
+ if (currentBucket[particleIndex]?.id === particle.id) {
387
+ currentBucket.splice(particleIndex, deleteCount);
388
+ }
389
+ }
390
+ const newBucket = this._zBuckets[newBucketIndex];
391
+ if (!newBucket) {
392
+ this._particleBuckets.set(particle.id, newBucketIndex);
393
+ return;
394
+ }
395
+ newBucket.splice(this._getParticleInsertIndex(newBucket, particle.id), empty, particle);
396
+ this._particleBuckets.set(particle.id, newBucketIndex);
397
+ };
398
+ _updateParticlesPhase1 = (delta) => {
399
+ const particlesToDelete = new Set(), resizeFactor = this._resizeFactor;
400
+ for (const particle of this._array) {
401
+ if (resizeFactor && !particle.ignoresResizeRatio) {
402
+ particle.position.x *= resizeFactor.width;
403
+ particle.position.y *= resizeFactor.height;
404
+ particle.initialPosition.x *= resizeFactor.width;
405
+ particle.initialPosition.y *= resizeFactor.height;
406
+ }
407
+ particle.ignoresResizeRatio = false;
408
+ for (const plugin of this._particleResetPlugins) {
409
+ plugin.particleReset?.(particle);
410
+ }
411
+ for (const plugin of this._particleUpdatePlugins) {
412
+ if (particle.destroyed) {
413
+ break;
414
+ }
415
+ plugin.particleUpdate?.(particle, delta);
416
+ }
417
+ if (particle.destroyed) {
418
+ particlesToDelete.add(particle);
419
+ continue;
420
+ }
421
+ this.grid.insert(particle);
422
+ }
423
+ return particlesToDelete;
424
+ };
425
+ _updateParticlesPhase2 = (delta, particlesToDelete) => {
426
+ for (const particle of this._array) {
427
+ if (particle.destroyed) {
428
+ particlesToDelete.add(particle);
429
+ continue;
430
+ }
431
+ for (const updater of this._container.particleUpdaters) {
432
+ updater.update(particle, delta);
433
+ }
434
+ if (!particle.spawning) {
435
+ for (const plugin of this._postParticleUpdatePlugins) {
436
+ plugin.postParticleUpdate?.(particle, delta);
437
+ }
438
+ }
439
+ this._updateParticleBucket(particle);
440
+ }
441
+ };
363
442
  }
@@ -13,9 +13,8 @@ export class Retina {
13
13
  const container = this.container, options = container.actualOptions;
14
14
  this.pixelRatio = options.detectRetina ? devicePixelRatio : defaultRatio;
15
15
  this.reduceFactor = defaultReduceFactor;
16
- const ratio = this.pixelRatio, canvas = container.canvas;
17
- if (canvas.element) {
18
- const element = canvas.element;
16
+ const ratio = this.pixelRatio, canvas = container.canvas, element = canvas.domElement;
17
+ if (element) {
19
18
  canvas.size.width = element.offsetWidth * ratio;
20
19
  canvas.size.height = element.offsetHeight * ratio;
21
20
  }
@@ -66,7 +66,7 @@ export class EventListeners {
66
66
  manageListener(globalThis, resizeEvent, handlers.resize, add);
67
67
  return;
68
68
  }
69
- const canvasEl = container.canvas.element;
69
+ const canvasEl = container.canvas.domElement;
70
70
  if (this._resizeObserver && !add) {
71
71
  if (canvasEl) {
72
72
  this._resizeObserver.unobserve(canvasEl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsparticles/engine",
3
- "version": "4.0.0-beta.15",
3
+ "version": "4.0.0-beta.17",
4
4
  "description": "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.",
5
5
  "homepage": "https://particles.js.org",
6
6
  "scripts": {