@tsparticles/engine 4.0.0-alpha.1 → 4.0.0-alpha.3

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 (61) hide show
  1. package/638.min.js +1 -1
  2. package/638.min.js.LICENSE.txt +1 -1
  3. package/browser/Core/Canvas.js +15 -19
  4. package/browser/Core/Container.js +10 -15
  5. package/browser/Core/Engine.js +34 -14
  6. package/browser/Core/Interfaces/IParticleOpacityData.js +1 -0
  7. package/browser/Core/Interfaces/IParticleRotateData.js +1 -0
  8. package/browser/Core/Particle.js +95 -49
  9. package/browser/Utils/CanvasUtils.js +26 -83
  10. package/browser/Utils/ColorUtils.js +15 -2
  11. package/browser/Utils/MathUtils.js +3 -2
  12. package/browser/Utils/Utils.js +1 -1
  13. package/cjs/Core/Canvas.js +15 -19
  14. package/cjs/Core/Container.js +10 -15
  15. package/cjs/Core/Engine.js +34 -14
  16. package/cjs/Core/Interfaces/IParticleOpacityData.js +1 -0
  17. package/cjs/Core/Interfaces/IParticleRotateData.js +1 -0
  18. package/cjs/Core/Particle.js +95 -49
  19. package/cjs/Utils/CanvasUtils.js +26 -83
  20. package/cjs/Utils/ColorUtils.js +15 -2
  21. package/cjs/Utils/MathUtils.js +3 -2
  22. package/cjs/Utils/Utils.js +1 -1
  23. package/dist_browser_Core_Container_js.js +4 -4
  24. package/esm/Core/Canvas.js +15 -19
  25. package/esm/Core/Container.js +10 -15
  26. package/esm/Core/Engine.js +34 -14
  27. package/esm/Core/Interfaces/IParticleOpacityData.js +1 -0
  28. package/esm/Core/Interfaces/IParticleRotateData.js +1 -0
  29. package/esm/Core/Particle.js +95 -49
  30. package/esm/Utils/CanvasUtils.js +26 -83
  31. package/esm/Utils/ColorUtils.js +15 -2
  32. package/esm/Utils/MathUtils.js +3 -2
  33. package/esm/Utils/Utils.js +1 -1
  34. package/package.json +1 -1
  35. package/report.html +1 -1
  36. package/scripts/install.js +28 -9
  37. package/tsparticles.engine.js +6 -6
  38. package/tsparticles.engine.min.js +1 -1
  39. package/tsparticles.engine.min.js.LICENSE.txt +1 -1
  40. package/types/Core/Canvas.d.ts +3 -0
  41. package/types/Core/Container.d.ts +1 -0
  42. package/types/Core/Engine.d.ts +1 -1
  43. package/types/Core/Interfaces/IDrawParticleParams.d.ts +1 -1
  44. package/types/Core/Interfaces/IParticleOpacityData.d.ts +4 -0
  45. package/types/Core/Interfaces/IParticleRotateData.d.ts +4 -0
  46. package/types/Core/Interfaces/IParticleTransformValues.d.ts +4 -4
  47. package/types/Core/Interfaces/IParticleUpdater.d.ts +1 -1
  48. package/types/Core/Particle.d.ts +12 -0
  49. package/types/Types/CustomEventListener.d.ts +1 -1
  50. package/types/Utils/CanvasUtils.d.ts +6 -21
  51. package/types/Utils/EventDispatcher.d.ts +1 -1
  52. package/umd/Core/Canvas.js +14 -18
  53. package/umd/Core/Container.js +10 -15
  54. package/umd/Core/Engine.js +34 -14
  55. package/umd/Core/Interfaces/IParticleOpacityData.js +12 -0
  56. package/umd/Core/Interfaces/IParticleRotateData.js +12 -0
  57. package/umd/Core/Particle.js +94 -48
  58. package/umd/Utils/CanvasUtils.js +25 -82
  59. package/umd/Utils/ColorUtils.js +15 -2
  60. package/umd/Utils/MathUtils.js +3 -2
  61. package/umd/Utils/Utils.js +1 -1
@@ -12,11 +12,9 @@ import { safeIntersectionObserver } from "../Utils/Utils.js";
12
12
  function guardCheck(container) {
13
13
  return !container.destroyed;
14
14
  }
15
- function initDelta(value, fpsLimit = defaultFps, smooth = false) {
16
- return {
17
- value,
18
- factor: smooth ? defaultFps / fpsLimit : (defaultFps * value) / millisecondsToSeconds,
19
- };
15
+ function updateDelta(delta, value, fpsLimit = defaultFps, smooth = false) {
16
+ delta.value = value;
17
+ delta.factor = smooth ? defaultFps / fpsLimit : (defaultFps * value) / millisecondsToSeconds;
20
18
  }
21
19
  function loadContainerOptions(engine, container, ...sourceOptionsArr) {
22
20
  const options = new Options(engine, container);
@@ -25,6 +23,7 @@ function loadContainerOptions(engine, container, ...sourceOptionsArr) {
25
23
  }
26
24
  export class Container {
27
25
  constructor(engine, id, sourceOptions) {
26
+ this._delta = { value: 0, factor: 0 };
28
27
  this._intersectionManager = entries => {
29
28
  if (!guardCheck(this) || !this.actualOptions.pauseOnOutsideViewport) {
30
29
  return;
@@ -50,14 +49,14 @@ export class Container {
50
49
  return;
51
50
  }
52
51
  this._lastFrameTime ??= timestamp;
53
- const delta = initDelta(timestamp - this._lastFrameTime, this.fpsLimit, this._smooth);
54
- this.addLifeTime(delta.value);
52
+ updateDelta(this._delta, timestamp - this._lastFrameTime, this.fpsLimit, this._smooth);
53
+ this.addLifeTime(this._delta.value);
55
54
  this._lastFrameTime = timestamp;
56
- if (delta.value > millisecondsToSeconds) {
55
+ if (this._delta.value > millisecondsToSeconds) {
57
56
  this.draw(false);
58
57
  return;
59
58
  }
60
- this.canvas.drawParticles(delta);
59
+ this.canvas.drawParticles(this._delta);
61
60
  if (!this.alive()) {
62
61
  this.destroy();
63
62
  return;
@@ -222,15 +221,11 @@ export class Container {
222
221
  for (const effectDrawer of this.effectDrawers.values()) {
223
222
  effectDrawer.destroy?.(this);
224
223
  }
224
+ this.effectDrawers.clear();
225
225
  for (const shapeDrawer of this.shapeDrawers.values()) {
226
226
  shapeDrawer.destroy?.(this);
227
227
  }
228
- for (const key of this.effectDrawers.keys()) {
229
- this.effectDrawers.delete(key);
230
- }
231
- for (const key of this.shapeDrawers.keys()) {
232
- this.shapeDrawers.delete(key);
233
- }
228
+ this.shapeDrawers.clear();
234
229
  this._engine.clearPlugins(this);
235
230
  this.destroyed = true;
236
231
  if (remove) {
@@ -4,6 +4,7 @@ import { EventDispatcher } from "../Utils/EventDispatcher.js";
4
4
  import { EventType } from "../Enums/Types/EventType.js";
5
5
  import { getLogger } from "../Utils/LogUtils.js";
6
6
  import { getRandom } from "../Utils/MathUtils.js";
7
+ const fullPercent = "100%";
7
8
  async function getItemsFromInitializer(container, map, initializers, force = false) {
8
9
  let res = map.get(container);
9
10
  if (!res || force) {
@@ -25,6 +26,7 @@ async function getDataFromUrl(data) {
25
26
  return data.fallback;
26
27
  }
27
28
  const getCanvasFromContainer = (domContainer) => {
29
+ const documentSafe = safeDocument();
28
30
  let canvasEl;
29
31
  if (domContainer instanceof HTMLCanvasElement || domContainer.tagName.toLowerCase() === canvasTag) {
30
32
  canvasEl = domContainer;
@@ -37,28 +39,24 @@ const getCanvasFromContainer = (domContainer) => {
37
39
  canvasEl.dataset[generatedAttribute] = generatedFalse;
38
40
  }
39
41
  else {
40
- canvasEl = safeDocument().createElement(canvasTag);
42
+ canvasEl = documentSafe.createElement(canvasTag);
41
43
  canvasEl.dataset[generatedAttribute] = generatedTrue;
42
44
  domContainer.appendChild(canvasEl);
43
45
  }
44
46
  }
45
- const fullPercent = "100%";
46
- if (!canvasEl.style.width) {
47
- canvasEl.style.width = fullPercent;
48
- }
49
- if (!canvasEl.style.height) {
50
- canvasEl.style.height = fullPercent;
51
- }
47
+ canvasEl.style.width ||= fullPercent;
48
+ canvasEl.style.height ||= fullPercent;
52
49
  return canvasEl;
53
50
  }, getDomContainer = (id, source) => {
54
- let domContainer = source ?? safeDocument().getElementById(id);
51
+ const documentSafe = safeDocument();
52
+ let domContainer = source ?? document.getElementById(id);
55
53
  if (domContainer) {
56
54
  return domContainer;
57
55
  }
58
- domContainer = safeDocument().createElement("div");
56
+ domContainer = documentSafe.createElement("div");
59
57
  domContainer.id = id;
60
58
  domContainer.dataset[generatedAttribute] = generatedTrue;
61
- safeDocument().body.append(domContainer);
59
+ documentSafe.body.append(domContainer);
62
60
  return domContainer;
63
61
  };
64
62
  export class Engine {
@@ -95,7 +93,7 @@ export class Engine {
95
93
  return this._domArray;
96
94
  }
97
95
  get version() {
98
- return "4.0.0-alpha.1";
96
+ return "4.0.0-alpha.3";
99
97
  }
100
98
  addColorManager(manager) {
101
99
  this.colorManagers.set(manager.key, manager);
@@ -221,10 +219,32 @@ export class Engine {
221
219
  if (this._initialized) {
222
220
  return;
223
221
  }
224
- for (const loadPromise of this._loadPromises) {
225
- await loadPromise(this);
222
+ const executed = new Set(), allLoaders = new Set(this._loadPromises), stack = [...allLoaders];
223
+ while (stack.length) {
224
+ const loader = stack.shift();
225
+ if (!loader) {
226
+ continue;
227
+ }
228
+ if (executed.has(loader)) {
229
+ continue;
230
+ }
231
+ executed.add(loader);
232
+ const inner = [], origRegister = this.register.bind(this);
233
+ this.register = (...loaders) => {
234
+ inner.push(...loaders);
235
+ for (const loader of loaders) {
236
+ allLoaders.add(loader);
237
+ }
238
+ };
239
+ await loader(this);
240
+ this.register = origRegister;
241
+ stack.unshift(...inner);
242
+ this._loadPromises.delete(loader);
226
243
  }
227
244
  this._loadPromises.clear();
245
+ for (const loader of allLoaders) {
246
+ this._loadPromises.add(loader);
247
+ }
228
248
  this._initialized = true;
229
249
  }
230
250
  item(index) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { Vector, Vector3d } from "./Utils/Vectors.js";
2
2
  import { calcExactPositionOrRandomFromSize, clamp, degToRad, getParticleBaseVelocity, getParticleDirectionAngle, getRandom, getRangeValue, randomInRangeValue, setRangeValue, } from "../Utils/MathUtils.js";
3
- import { decayOffset, defaultAngle, defaultRetryCount, double, half, millisecondsToSeconds, minZ, none, randomColorValue, rollFactor, squareExp, tryCountIncrement, } from "./Utils/Constants.js";
3
+ import { decayOffset, defaultAngle, defaultOpacity, defaultRetryCount, defaultTransform, double, half, identity, millisecondsToSeconds, minZ, none, randomColorValue, rollFactor, squareExp, tryCountIncrement, zIndexFactorOffset, } from "./Utils/Constants.js";
4
4
  import { deepExtend, getPosition, initParticleNumericAnimationValue, isInArray, itemFromSingleOrMultiple, } from "../Utils/Utils.js";
5
5
  import { EventType } from "../Enums/Types/EventType.js";
6
6
  import { Interactivity } from "../Options/Classes/Interactivity/Interactivity.js";
@@ -39,51 +39,50 @@ function fixOutMode(data) {
39
39
  export class Particle {
40
40
  constructor(engine, container) {
41
41
  this.container = container;
42
- this._calcPosition = (container, position, zIndex, tryCount = defaultRetryCount) => {
43
- const plugins = container.plugins.values();
44
- for (const plugin of plugins) {
45
- const pluginPos = plugin.particlePosition?.(position, this);
46
- if (pluginPos) {
47
- return Vector3d.create(pluginPos.x, pluginPos.y, zIndex);
42
+ this._cachedOpacityData = {
43
+ opacity: defaultOpacity,
44
+ strokeOpacity: defaultOpacity,
45
+ };
46
+ this._cachedPosition = Vector3d.origin;
47
+ this._cachedRotateData = { sin: 0, cos: 0 };
48
+ this._cachedTransform = {
49
+ a: 1,
50
+ b: 0,
51
+ c: 0,
52
+ d: 1,
53
+ };
54
+ this._calcPosition = (position, zIndex) => {
55
+ let tryCount = defaultRetryCount, posVec = position ? Vector3d.create(position.x, position.y, zIndex) : undefined;
56
+ const container = this.container, plugins = Array.from(container.plugins.values()), outModes = this.options.move.outModes, radius = this.getRadius(), canvasSize = container.canvas.size, abortController = new AbortController(), { signal } = abortController;
57
+ while (!signal.aborted) {
58
+ for (const plugin of plugins) {
59
+ const pluginPos = plugin.particlePosition?.(posVec, this);
60
+ if (pluginPos) {
61
+ return Vector3d.create(pluginPos.x, pluginPos.y, zIndex);
62
+ }
48
63
  }
49
- }
50
- const canvasSize = container.canvas.size, exactPosition = calcExactPositionOrRandomFromSize({
51
- size: canvasSize,
52
- position: position,
53
- }), pos = Vector3d.create(exactPosition.x, exactPosition.y, zIndex), radius = this.getRadius(), outModes = this.options.move.outModes, fixHorizontal = (outMode) => {
54
- fixOutMode({
55
- outMode,
56
- checkModes: [OutMode.bounce],
57
- coord: pos.x,
58
- maxCoord: container.canvas.size.width,
59
- setCb: (value) => (pos.x += value),
60
- radius,
61
- });
62
- }, fixVertical = (outMode) => {
63
- fixOutMode({
64
- outMode,
65
- checkModes: [OutMode.bounce],
66
- coord: pos.y,
67
- maxCoord: container.canvas.size.height,
68
- setCb: (value) => (pos.y += value),
69
- radius,
70
- });
71
- };
72
- fixHorizontal(outModes.left ?? outModes.default);
73
- fixHorizontal(outModes.right ?? outModes.default);
74
- fixVertical(outModes.top ?? outModes.default);
75
- fixVertical(outModes.bottom ?? outModes.default);
76
- let isValidPosition = true;
77
- for (const plugin of plugins) {
78
- isValidPosition = plugin.checkParticlePosition?.(this, pos, tryCount);
79
- if (isValidPosition === false) {
80
- break;
64
+ const exactPosition = calcExactPositionOrRandomFromSize({
65
+ size: canvasSize,
66
+ position: posVec,
67
+ }), pos = Vector3d.create(exactPosition.x, exactPosition.y, zIndex);
68
+ this._fixHorizontal(pos, radius, outModes.left ?? outModes.default);
69
+ this._fixHorizontal(pos, radius, outModes.right ?? outModes.default);
70
+ this._fixVertical(pos, radius, outModes.top ?? outModes.default);
71
+ this._fixVertical(pos, radius, outModes.bottom ?? outModes.default);
72
+ let isValidPosition = true;
73
+ for (const plugin of plugins) {
74
+ isValidPosition = plugin.checkParticlePosition?.(this, pos, tryCount) ?? true;
75
+ if (!isValidPosition) {
76
+ break;
77
+ }
81
78
  }
79
+ if (isValidPosition) {
80
+ return pos;
81
+ }
82
+ tryCount += tryCountIncrement;
83
+ posVec = undefined;
82
84
  }
83
- if (!isValidPosition) {
84
- return this._calcPosition(container, undefined, zIndex, tryCount + tryCountIncrement);
85
- }
86
- return pos;
85
+ return posVec;
87
86
  };
88
87
  this._calculateVelocity = () => {
89
88
  const baseVelocity = getParticleBaseVelocity(this.direction), res = baseVelocity.copy(), moveOptions = this.options.move;
@@ -102,6 +101,26 @@ export class Particle {
102
101
  }
103
102
  return res;
104
103
  };
104
+ this._fixHorizontal = (pos, radius, outMode) => {
105
+ fixOutMode({
106
+ outMode,
107
+ checkModes: [OutMode.bounce],
108
+ coord: pos.x,
109
+ maxCoord: this.container.canvas.size.width,
110
+ setCb: (value) => (pos.x += value),
111
+ radius,
112
+ });
113
+ };
114
+ this._fixVertical = (pos, radius, outMode) => {
115
+ fixOutMode({
116
+ outMode,
117
+ checkModes: [OutMode.bounce],
118
+ coord: pos.y,
119
+ maxCoord: this.container.canvas.size.height,
120
+ setCb: (value) => (pos.y += value),
121
+ radius,
122
+ });
123
+ };
105
124
  this._getRollColor = color => {
106
125
  if (!color || !this.roll || (!this.backColor && !this.roll.alter)) {
107
126
  return color;
@@ -120,7 +139,11 @@ export class Particle {
120
139
  };
121
140
  this._initPosition = position => {
122
141
  const container = this.container, zIndexValue = getRangeValue(this.options.zIndex.value);
123
- this.position = this._calcPosition(container, position, clamp(zIndexValue, minZ, container.zLayers));
142
+ const initialPosition = this._calcPosition(position, clamp(zIndexValue, minZ, container.zLayers));
143
+ if (!initialPosition) {
144
+ throw new Error("a valid position cannot be found for particle");
145
+ }
146
+ this.position = initialPosition;
124
147
  this.initialPosition = this.position.copy();
125
148
  const canvasSize = container.canvas.size;
126
149
  this.moveCenter = {
@@ -180,19 +203,42 @@ export class Particle {
180
203
  getMass() {
181
204
  return this.getRadius() ** squareExp * Math.PI * half;
182
205
  }
206
+ getOpacity() {
207
+ const zIndexOptions = this.options.zIndex, zIndexFactor = zIndexFactorOffset - this.zIndexFactor, zOpacityFactor = zIndexFactor ** zIndexOptions.opacityRate, opacity = this.bubble.opacity ?? getRangeValue(this.opacity?.value ?? defaultOpacity), strokeOpacity = this.strokeOpacity ?? opacity;
208
+ this._cachedOpacityData.opacity = opacity * zOpacityFactor;
209
+ this._cachedOpacityData.strokeOpacity = strokeOpacity * zOpacityFactor;
210
+ return this._cachedOpacityData;
211
+ }
183
212
  getPosition() {
184
- return {
185
- x: this.position.x + this.offset.x,
186
- y: this.position.y + this.offset.y,
187
- z: this.position.z,
188
- };
213
+ this._cachedPosition.x = this.position.x + this.offset.x;
214
+ this._cachedPosition.y = this.position.y + this.offset.y;
215
+ this._cachedPosition.z = this.position.z;
216
+ return this._cachedPosition;
189
217
  }
190
218
  getRadius() {
191
219
  return this.bubble.radius ?? this.size.value;
192
220
  }
221
+ getRotateData() {
222
+ const angle = this.getAngle();
223
+ this._cachedRotateData.sin = Math.sin(angle);
224
+ this._cachedRotateData.cos = Math.cos(angle);
225
+ return this._cachedRotateData;
226
+ }
193
227
  getStrokeColor() {
194
228
  return this._getRollColor(this.bubble.color ?? getHslFromAnimation(this.strokeColor));
195
229
  }
230
+ getTransformData(externalTransform) {
231
+ const rotateData = this.getRotateData(), rotating = this.isRotating;
232
+ this._cachedTransform.a = rotateData.cos * (externalTransform.a ?? defaultTransform.a);
233
+ this._cachedTransform.b = rotating
234
+ ? rotateData.sin * (externalTransform.b ?? identity)
235
+ : (externalTransform.b ?? defaultTransform.b);
236
+ this._cachedTransform.c = rotating
237
+ ? -rotateData.sin * (externalTransform.c ?? identity)
238
+ : (externalTransform.c ?? defaultTransform.c);
239
+ this._cachedTransform.d = rotateData.cos * (externalTransform.d ?? defaultTransform.d);
240
+ return this._cachedTransform;
241
+ }
196
242
  init(id, position, overrideOptions, group) {
197
243
  const container = this.container, engine = this._engine;
198
244
  this.id = id;
@@ -1,4 +1,4 @@
1
- import { defaultTransform, identity, lFactor, minStrokeWidth, originPoint } from "../Core/Utils/Constants.js";
1
+ import { lFactor, minStrokeWidth, originPoint } from "../Core/Utils/Constants.js";
2
2
  import { AlterType } from "../Enums/Types/AlterType.js";
3
3
  export function clearDrawPlugin(context, plugin, delta) {
4
4
  if (!plugin.clearDraw) {
@@ -28,15 +28,7 @@ export function clear(context, dimension) {
28
28
  context.clearRect(originPoint.x, originPoint.y, dimension.width, dimension.height);
29
29
  }
30
30
  export function drawParticle(data) {
31
- const { container, context, particle, delta, colorStyles, radius, opacity, transform } = data, pos = particle.getPosition(), angle = particle.getAngle(), rotateData = {
32
- sin: Math.sin(angle),
33
- cos: Math.cos(angle),
34
- }, rotating = particle.isRotating, transformData = {
35
- a: rotateData.cos * (transform.a ?? defaultTransform.a),
36
- b: rotating ? rotateData.sin * (transform.b ?? identity) : (transform.b ?? defaultTransform.b),
37
- c: rotating ? -rotateData.sin * (transform.c ?? identity) : (transform.c ?? defaultTransform.c),
38
- d: rotateData.cos * (transform.d ?? defaultTransform.d),
39
- };
31
+ const { container, context, particle, delta, colorStyles, radius, opacity, transform } = data, pos = particle.getPosition(), transformData = particle.getTransformData(transform);
40
32
  context.setTransform(transformData.a, transformData.b, transformData.c, transformData.d, pos.x, pos.y);
41
33
  if (colorStyles.fill) {
42
34
  context.fillStyle = colorStyles.fill;
@@ -47,24 +39,25 @@ export function drawParticle(data) {
47
39
  context.strokeStyle = colorStyles.stroke;
48
40
  }
49
41
  const drawData = {
50
- container,
51
42
  context,
52
43
  particle,
53
44
  radius,
54
45
  opacity,
55
46
  delta,
47
+ pixelRatio: container.retina.pixelRatio,
48
+ fill: particle.shapeFill,
49
+ stroke: strokeWidth > minStrokeWidth || !particle.shapeFill,
56
50
  transformData,
57
- strokeWidth,
58
51
  };
59
- drawBeforeEffect(drawData);
60
- drawShapeBeforeDraw(drawData);
61
- drawShape(drawData);
62
- drawShapeAfterDraw(drawData);
63
- drawAfterEffect(drawData);
52
+ drawBeforeEffect(container, drawData);
53
+ drawShapeBeforeDraw(container, drawData);
54
+ drawShape(container, drawData);
55
+ drawShapeAfterDraw(container, drawData);
56
+ drawAfterEffect(container, drawData);
64
57
  context.resetTransform();
65
58
  }
66
- export function drawAfterEffect(data) {
67
- const { container, context, particle, radius, opacity, strokeWidth, delta, transformData } = data;
59
+ export function drawAfterEffect(container, data) {
60
+ const { particle } = data;
68
61
  if (!particle.effect) {
69
62
  return;
70
63
  }
@@ -72,20 +65,10 @@ export function drawAfterEffect(data) {
72
65
  if (!drawFunc) {
73
66
  return;
74
67
  }
75
- drawFunc({
76
- context,
77
- particle,
78
- radius,
79
- opacity,
80
- delta,
81
- pixelRatio: container.retina.pixelRatio,
82
- fill: particle.shapeFill,
83
- stroke: strokeWidth > minStrokeWidth || !particle.shapeFill,
84
- transformData: { ...transformData },
85
- });
68
+ drawFunc(data);
86
69
  }
87
- export function drawBeforeEffect(data) {
88
- const { container, context, particle, radius, opacity, strokeWidth, delta, transformData } = data;
70
+ export function drawBeforeEffect(container, data) {
71
+ const { particle } = data;
89
72
  if (!particle.effect) {
90
73
  return;
91
74
  }
@@ -93,20 +76,10 @@ export function drawBeforeEffect(data) {
93
76
  if (!drawer?.drawBefore) {
94
77
  return;
95
78
  }
96
- drawer.drawBefore({
97
- context,
98
- particle,
99
- radius,
100
- opacity,
101
- delta,
102
- pixelRatio: container.retina.pixelRatio,
103
- fill: particle.shapeFill,
104
- stroke: strokeWidth > minStrokeWidth || !particle.shapeFill,
105
- transformData: { ...transformData },
106
- });
79
+ drawer.drawBefore(data);
107
80
  }
108
- export function drawShape(data) {
109
- const { container, context, particle, radius, opacity, delta, strokeWidth, transformData } = data;
81
+ export function drawShape(container, data) {
82
+ const { context, particle, stroke } = data;
110
83
  if (!particle.shape) {
111
84
  return;
112
85
  }
@@ -115,29 +88,19 @@ export function drawShape(data) {
115
88
  return;
116
89
  }
117
90
  context.beginPath();
118
- drawer.draw({
119
- context,
120
- particle,
121
- radius,
122
- opacity,
123
- delta,
124
- pixelRatio: container.retina.pixelRatio,
125
- fill: particle.shapeFill,
126
- stroke: strokeWidth > minStrokeWidth || !particle.shapeFill,
127
- transformData: { ...transformData },
128
- });
91
+ drawer.draw(data);
129
92
  if (particle.shapeClose) {
130
93
  context.closePath();
131
94
  }
132
- if (strokeWidth > minStrokeWidth) {
95
+ if (stroke) {
133
96
  context.stroke();
134
97
  }
135
98
  if (particle.shapeFill) {
136
99
  context.fill();
137
100
  }
138
101
  }
139
- export function drawShapeAfterDraw(data) {
140
- const { container, context, particle, radius, opacity, strokeWidth, delta, transformData } = data;
102
+ export function drawShapeAfterDraw(container, data) {
103
+ const { particle } = data;
141
104
  if (!particle.shape) {
142
105
  return;
143
106
  }
@@ -145,20 +108,10 @@ export function drawShapeAfterDraw(data) {
145
108
  if (!drawer?.afterDraw) {
146
109
  return;
147
110
  }
148
- drawer.afterDraw({
149
- context,
150
- particle,
151
- radius,
152
- opacity,
153
- delta,
154
- pixelRatio: container.retina.pixelRatio,
155
- fill: particle.shapeFill,
156
- stroke: strokeWidth > minStrokeWidth || !particle.shapeFill,
157
- transformData: { ...transformData },
158
- });
111
+ drawer.afterDraw(data);
159
112
  }
160
- export function drawShapeBeforeDraw(data) {
161
- const { container, context, particle, radius, opacity, strokeWidth, delta, transformData } = data;
113
+ export function drawShapeBeforeDraw(container, data) {
114
+ const { particle } = data;
162
115
  if (!particle.shape) {
163
116
  return;
164
117
  }
@@ -166,17 +119,7 @@ export function drawShapeBeforeDraw(data) {
166
119
  if (!drawer?.beforeDraw) {
167
120
  return;
168
121
  }
169
- drawer.beforeDraw({
170
- context,
171
- particle,
172
- radius,
173
- opacity,
174
- delta,
175
- pixelRatio: container.retina.pixelRatio,
176
- fill: particle.shapeFill,
177
- stroke: strokeWidth > minStrokeWidth || !particle.shapeFill,
178
- transformData: { ...transformData },
179
- });
122
+ drawer.beforeDraw(data);
180
123
  }
181
124
  export function drawPlugin(context, plugin, delta) {
182
125
  if (!plugin.draw) {
@@ -3,6 +3,17 @@ import { decayOffset, defaultLoops, defaultOpacity, defaultRgbMin, defaultTime,
3
3
  import { isArray, isString } from "./TypeUtils.js";
4
4
  import { AnimationStatus } from "../Enums/AnimationStatus.js";
5
5
  import { itemFromArray } from "./Utils.js";
6
+ const styleCache = new Map(), maxCacheSize = 1000;
7
+ function getCachedStyle(key, generator) {
8
+ let cached = styleCache.get(key);
9
+ if (!cached) {
10
+ cached = generator();
11
+ if (styleCache.size < maxCacheSize) {
12
+ styleCache.set(key, cached);
13
+ }
14
+ }
15
+ return cached;
16
+ }
6
17
  function stringToRgba(engine, input) {
7
18
  if (!input) {
8
19
  return;
@@ -155,7 +166,8 @@ export function getRandomRgbColor(min) {
155
166
  };
156
167
  }
157
168
  export function getStyleFromRgb(color, hdr, opacity) {
158
- return hdr ? getHdrStyleFromRgb(color, opacity) : getSdrStyleFromRgb(color, opacity);
169
+ const op = opacity ?? defaultOpacity, key = `rgb-${color.r.toString()}-${color.g.toString()}-${color.b.toString()}-${hdr ? "hdr" : "sdr"}-${op.toString()}`;
170
+ return getCachedStyle(key, () => (hdr ? getHdrStyleFromRgb(color, opacity) : getSdrStyleFromRgb(color, opacity)));
159
171
  }
160
172
  function getHdrStyleFromRgb(color, opacity) {
161
173
  return `color(display-p3 ${(color.r / rgbMax).toString()} ${(color.g / rgbMax).toString()} ${(color.b / rgbMax).toString()} / ${(opacity ?? defaultOpacity).toString()})`;
@@ -164,7 +176,8 @@ function getSdrStyleFromRgb(color, opacity) {
164
176
  return `rgba(${color.r.toString()}, ${color.g.toString()}, ${color.b.toString()}, ${(opacity ?? defaultOpacity).toString()})`;
165
177
  }
166
178
  export function getStyleFromHsl(color, hdr, opacity) {
167
- return hdr ? getHdrStyleFromHsl(color, opacity) : getSdrStyleFromHsl(color, opacity);
179
+ const op = opacity ?? defaultOpacity, key = `hsl-${color.h.toString()}-${color.s.toString()}-${color.l.toString()}-${hdr ? "hdr" : "sdr"}-${op.toString()}`;
180
+ return getCachedStyle(key, () => (hdr ? getHdrStyleFromHsl(color, opacity) : getSdrStyleFromHsl(color, opacity)));
168
181
  }
169
182
  function getHdrStyleFromHsl(color, opacity) {
170
183
  return getHdrStyleFromRgb(hslToRgb(color), opacity);
@@ -137,9 +137,10 @@ export function calcPositionOrRandomFromSizeRanged(data) {
137
137
  return calcPositionOrRandomFromSize({ size: data.size, position });
138
138
  }
139
139
  export function calcExactPositionOrRandomFromSize(data) {
140
+ const { position, size } = data;
140
141
  return {
141
- x: data.position?.x ?? getRandom() * data.size.width,
142
- y: data.position?.y ?? getRandom() * data.size.height,
142
+ x: position?.x ?? getRandom() * size.width,
143
+ y: position?.y ?? getRandom() * size.height,
143
144
  };
144
145
  }
145
146
  export function calcExactPositionOrRandomFromSizeRanged(data) {
@@ -328,7 +328,7 @@ export function cloneStyle(style) {
328
328
  const clonedStyle = safeDocument().createElement("div").style;
329
329
  for (const key in style) {
330
330
  const styleKey = style[key];
331
- if (!Object.prototype.hasOwnProperty.call(style, key) || isNull(styleKey)) {
331
+ if (!Object.hasOwn(style, key) || isNull(styleKey)) {
332
332
  continue;
333
333
  }
334
334
  const styleValue = style.getPropertyValue?.(styleKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsparticles/engine",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.3",
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": {
package/report.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8"/>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
- <title>@tsparticles/engine [8 Jan 2026 at 08:56]</title>
6
+ <title>@tsparticles/engine [10 Jan 2026 at 19:10]</title>
7
7
  <link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABrVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+O1foceMD///+J0/qK1Pr7/v8Xdr/9///W8P4UdL7L7P0Scr2r4Pyj3vwad8D5/f/2/f+55f3E6f34+/2H0/ojfMKpzOd0rNgQcb3F3O/j9f7c8v6g3Pz0/P/w+v/q+P7n9v6T1/uQ1vuE0vqLut/y+v+Z2fvt+f+15Pzv9fuc2/vR7v2V2Pvd6/bg9P7I6/285/2y4/yp3/zp8vk8i8kqgMT7/P31+fyv4vxGkcz6/P6/6P3j7vfS5PNnpNUxhcbO7f7F6v3O4vHK3/DA2u631Ouy0eqXweKJud5wqthfoNMMbLvY8f73+v2dxeR8sNtTmdDx9/zX6PSjyeaCtd1YnNGX2PuQveCGt95Nls42h8dLlM3F4vBtAAAAM3RSTlMAAyOx0/sKBvik8opWGBMOAe3l1snDm2E9LSb06eHcu5JpHbarfHZCN9CBb08zzkdNS0kYaptYAAAFV0lEQVRYw92X51/aYBDHHS2O2qqttVbrqNq9m+TJIAYIShBkWwqIiCgoWvfeq7Z2/s29hyQNyUcR7LveGwVyXy6XH8/9rqxglLfUPLxVduUor3h0rfp2TYvpivk37929TkG037hffoX0+peVtZQc1589rigVUdXS/ABSAyEmGIO/1XfvldSK8vs3OqB6u3m0nxmIrvgB0dj7rr7Y9IbuF68hnfFaiHA/sxqm0wciIG43P60qKv9WXWc1RXGh/mFESFABTSBi0sNAKzqet17eCtOb3kZIDwxEEU0oAIJGYxNBDhBND29e0rtXXbcpuPmED9IhEAAQ/AXEaF8EPmnrrKsv0LvWR3fg5sWDNAFZOgAgaKvZDogHNU9MFwnnYROkc56RD5CjAbQX9Ow4g7upCsvYu55aSI/Nj0H1akgKQEUM94dwK65hYRmFU9MIcH/fqJYOZYcnuJSU/waKDgTOEVaVKhwrTRP5XzgSpAITYzom7UvkhFX5VutmxeNnWDjjswTKTyfgluNDGbUpWissXhF3s7mlSml+czWkg3D0l1nNjGNjz3myOQOa1KM/jOS6ebdbAVTCi4gljHSFrviza7tOgRWcS0MOUX9zdNgag5w7rRqA44Lzw0hr1WqES36dFliSJFlh2rXIae3FFcDDgKdxrUIDePr8jGcSClV1u7A9xeN0ModY/pHMxmR1EzRh8TJiwqsHmKW0l4FCEZI+jHio+JdPPE9qwQtTRxku2D8sIeRL2LnxWSllANCQGOIiqVHAz2ye2JR0DcH+HoxDkaADLjgxjKQ+AwCX/g0+DNgdG0ukYCONAe+dbc2IAc6fwt1ARoDSezNHxV2Cmzwv3O6lDMV55edBGwGK9n1+x2F8EDfAGCxug8MhpsMEcTEAWf3rx2vZhe/LAmtIn/6apE6PN0ULKgywD9mmdxbmFl3OvD5AS5fW5zLbv/YHmcsBTjf/afDz3MaZTVCfAP9z6/Bw6ycv8EUBWJIn9zYcoAWWlW9+OzO3vkTy8H+RANLmdrpOuYWdZYEXpo+TlCJrW5EARb7fF+bWdqf3hhyZI1nWJQHgznErZhbjoEsWqi8dQNoE294aldzFurwSABL2XXMf9+H1VQGke9exw5P/AnA5Pv5ngMul7LOvO922iwACu8WkCwLCafvM4CeWPxfA8lNHcWZSoi8EwMAIciKX2Z4SWCMAa3snCZ/G4EA8D6CMLNFsGQhkkz/gQNEBbPCbWsxGUpYVu3z8IyNAknwJkfPMEhLyrdi5RTyUVACkw4GSFRNWJNEW+fgPGwHD8/JxnRuLabN4CGNRkAE23na2+VmEAUmrYymSGjMAYqH84YUIyzgzs3XC7gNgH36Vcc4zKY9o9fgPBXUAiHHwVboBHGLiX6Zcjp1f2wu4tvzZKo0ecPnDtQYDQvJXaBeNzce45Fp28ZQLrEZVuFqgBwOalArKXnW1UzlnSusQKJqKYNuz4tOnI6sZG4zanpemv+7ySU2jbA9h6uhcgpfy6G2PahirDZ6zvq6zDduMVFTKvzw8wgyEdelwY9in3XkEPs3osJuwRQ4qTkfzifndg9Gfc4pdsu82+tTnHZTBa2EAMrqr2t43pguc8tNm7JQVQ2S0ukj2d22dhXYP0/veWtwKrCkNoNimAN5+Xr/oLrxswKbVJjteWrX7eR63o4j9q0GxnaBdWgGA5VStpanIjQmEhV0/nVt5VOFUvix6awJhPcAaTEShgrG+iGyvb5a0Ndb1YGHFPEwoqAinoaykaID1o1pdPNu7XsnCKQ3R+hwWIIhGvORcJUBYXe3Xa3vq/mF/N9V13ugufMkfXn+KHsRD0B8AAAAASUVORK5CYII=" type="image/x-icon" />
8
8
 
9
9
  <script>