@tsparticles/interaction-external-attract 4.0.0-beta.16 → 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.
@@ -2,15 +2,19 @@ import { ExternalInteractorBase, mouseMoveEvent, } from "@tsparticles/plugin-int
2
2
  import { isInArray, millisecondsToSeconds } from "@tsparticles/engine";
3
3
  import { clickAttract, hoverAttract } from "./Utils.js";
4
4
  import { Attract } from "./Options/Classes/Attract.js";
5
- const attractMode = "attract";
5
+ const attractMode = "attract", minVelocityLengthSq = 0, minRestoreSpeed = 0.001, maxRestoreSpeed = 1, restoreEpsilon = 0.5;
6
6
  export class Attractor extends ExternalInteractorBase {
7
7
  handleClickMode;
8
+ _interactedThisFrame;
8
9
  _maxDistance;
9
10
  _pluginManager;
11
+ _restoreData;
10
12
  constructor(pluginManager, container) {
11
13
  super(container);
12
14
  this._pluginManager = pluginManager;
13
15
  this._maxDistance = 0;
16
+ this._interactedThisFrame = new Set();
17
+ this._restoreData = new Map();
14
18
  container.attract ??= { particles: [] };
15
19
  this.handleClickMode = (mode, interactivityData) => {
16
20
  const options = this.container.actualOptions, attract = options.interactivity?.modes.attract;
@@ -51,17 +55,23 @@ export class Attractor extends ExternalInteractorBase {
51
55
  container.retina.attractModeDistance = attract.distance * container.retina.pixelRatio;
52
56
  }
53
57
  interact(interactivityData) {
58
+ this._interactedThisFrame.clear();
54
59
  const container = this.container, options = container.actualOptions, mouseMoveStatus = interactivityData.status === mouseMoveEvent, events = options.interactivity?.events;
55
60
  if (!events) {
56
61
  return;
57
62
  }
58
63
  const { enable: hoverEnabled, mode: hoverMode } = events.onHover, { enable: clickEnabled, mode: clickMode } = events.onClick;
59
64
  if (mouseMoveStatus && hoverEnabled && isInArray(attractMode, hoverMode)) {
60
- hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
65
+ hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
66
+ this._trackInteractedParticle(p);
67
+ });
61
68
  }
62
69
  else if (clickEnabled && isInArray(attractMode, clickMode)) {
63
- clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
70
+ clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
71
+ this._trackInteractedParticle(p);
72
+ });
64
73
  }
74
+ this._restoreParticles();
65
75
  }
66
76
  isEnabled(interactivityData, particle) {
67
77
  const container = this.container, options = container.actualOptions, mouse = interactivityData.mouse, events = (particle?.interactivity ?? options.interactivity)?.events;
@@ -79,4 +89,61 @@ export class Attractor extends ExternalInteractorBase {
79
89
  }
80
90
  reset() {
81
91
  }
92
+ _restoreParticles() {
93
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
94
+ if (!restore?.enable || !this._restoreData.size) {
95
+ return;
96
+ }
97
+ const now = Date.now(), restoreDelay = restore.delay * millisecondsToSeconds, restoreSpeed = Math.max(minRestoreSpeed, Math.min(maxRestoreSpeed, restore.speed));
98
+ for (const [particle, restoreData] of this._restoreData) {
99
+ if (this._interactedThisFrame.has(particle)) {
100
+ continue;
101
+ }
102
+ if (particle.destroyed) {
103
+ this._restoreData.delete(particle);
104
+ continue;
105
+ }
106
+ const target = restoreData.target;
107
+ if (now - restoreData.lastInteractionTime < restoreDelay) {
108
+ continue;
109
+ }
110
+ let dx = target.x - particle.position.x, dy = target.y - particle.position.y, dz = target.z - particle.position.z;
111
+ if (restore.follow && particle.options.move.enable) {
112
+ const { x: vx, y: vy, z: vz } = particle.velocity, velocityLengthSq = vx * vx + vy * vy + vz * vz;
113
+ if (velocityLengthSq > minVelocityLengthSq) {
114
+ const parallelScale = (dx * vx + dy * vy + dz * vz) / velocityLengthSq;
115
+ dx -= vx * parallelScale;
116
+ dy -= vy * parallelScale;
117
+ dz -= vz * parallelScale;
118
+ }
119
+ }
120
+ particle.position.x += dx * restoreSpeed;
121
+ particle.position.y += dy * restoreSpeed;
122
+ particle.position.z += dz * restoreSpeed;
123
+ if (Math.abs(dx) <= restoreEpsilon && Math.abs(dy) <= restoreEpsilon) {
124
+ particle.position.x = target.x;
125
+ particle.position.y = target.y;
126
+ particle.position.z = target.z;
127
+ this._restoreData.delete(particle);
128
+ continue;
129
+ }
130
+ }
131
+ }
132
+ _trackInteractedParticle(particle) {
133
+ this._interactedThisFrame.add(particle);
134
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
135
+ if (!restore?.enable) {
136
+ return;
137
+ }
138
+ const now = Date.now();
139
+ let restoreData = this._restoreData.get(particle);
140
+ if (!restoreData) {
141
+ restoreData = {
142
+ target: particle.position.copy(),
143
+ lastInteractionTime: now,
144
+ };
145
+ this._restoreData.set(particle, restoreData);
146
+ }
147
+ restoreData.lastInteractionTime = now;
148
+ }
82
149
  }
@@ -5,6 +5,7 @@ export class Attract {
5
5
  easing;
6
6
  factor;
7
7
  maxSpeed;
8
+ restore;
8
9
  speed;
9
10
  constructor() {
10
11
  this.distance = 200;
@@ -13,6 +14,12 @@ export class Attract {
13
14
  this.factor = 1;
14
15
  this.maxSpeed = 50;
15
16
  this.speed = 1;
17
+ this.restore = {
18
+ enable: false,
19
+ delay: 0,
20
+ speed: 0.08,
21
+ follow: true,
22
+ };
16
23
  }
17
24
  load(data) {
18
25
  if (isNull(data)) {
@@ -36,5 +43,11 @@ export class Attract {
36
43
  if (data.speed !== undefined) {
37
44
  this.speed = data.speed;
38
45
  }
46
+ if (data.restore !== undefined) {
47
+ this.restore.enable = data.restore.enable ?? this.restore.enable;
48
+ this.restore.delay = data.restore.delay ?? this.restore.delay;
49
+ this.restore.speed = data.restore.speed ?? this.restore.speed;
50
+ this.restore.follow = data.restore.follow ?? this.restore.follow;
51
+ }
39
52
  }
40
53
  }
package/browser/Utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Circle, Vector, clamp, getDistances, identity, } from "@tsparticles/engine";
2
2
  const minFactor = 1, minRadius = 0, updateVector = Vector.origin;
3
- function processAttract(pluginManager, container, position, attractRadius, area, queryCb) {
3
+ function processAttract(pluginManager, container, position, attractRadius, area, queryCb, onAttractParticle) {
4
4
  const attractOptions = container.actualOptions.interactivity?.modes.attract;
5
5
  if (!attractOptions) {
6
6
  return;
@@ -10,10 +10,11 @@ function processAttract(pluginManager, container, position, attractRadius, area,
10
10
  const { dx, dy, distance } = getDistances(particle.position, position), velocity = attractOptions.speed * attractOptions.factor, attractFactor = clamp(pluginManager.getEasing(attractOptions.easing)(identity - distance / attractRadius) * velocity, minFactor, attractOptions.maxSpeed);
11
11
  updateVector.x = !distance ? velocity : (dx / distance) * attractFactor;
12
12
  updateVector.y = !distance ? velocity : (dy / distance) * attractFactor;
13
+ onAttractParticle?.(particle);
13
14
  particle.position.subFrom(updateVector);
14
15
  }
15
16
  }
16
- export function clickAttract(pluginManager, container, interactivityData, enabledCb) {
17
+ export function clickAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
17
18
  container.attract ??= { particles: [] };
18
19
  const { attract } = container;
19
20
  if (!attract.finish) {
@@ -28,16 +29,16 @@ export function clickAttract(pluginManager, container, interactivityData, enable
28
29
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
29
30
  return;
30
31
  }
31
- processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
32
+ processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
32
33
  }
33
34
  else if (attract.clicking === false) {
34
35
  attract.particles = [];
35
36
  }
36
37
  }
37
- export function hoverAttract(pluginManager, container, interactivityData, enabledCb) {
38
+ export function hoverAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
38
39
  const mousePos = interactivityData.mouse.position, attractRadius = container.retina.attractModeDistance;
39
40
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
40
41
  return;
41
42
  }
42
- processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
43
+ processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
43
44
  }
package/browser/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ensureInteractivityPluginLoaded } from "@tsparticles/plugin-interactivity";
2
2
  import { Attractor } from "./Attractor.js";
3
3
  export async function loadExternalAttractInteraction(engine) {
4
- engine.checkVersion("4.0.0-beta.16");
4
+ engine.checkVersion("4.0.0-beta.17");
5
5
  await engine.pluginManager.register((e) => {
6
6
  ensureInteractivityPluginLoaded(e);
7
7
  e.pluginManager.addInteractor?.("externalAttract", container => {
@@ -1,5 +1,5 @@
1
1
  export async function loadExternalAttractInteraction(engine) {
2
- engine.checkVersion("4.0.0-beta.16");
2
+ engine.checkVersion("4.0.0-beta.17");
3
3
  await engine.pluginManager.register(async (e) => {
4
4
  const { ensureInteractivityPluginLoaded } = await import("@tsparticles/plugin-interactivity/lazy");
5
5
  ensureInteractivityPluginLoaded(e);
package/cjs/Attractor.js CHANGED
@@ -2,15 +2,19 @@ import { ExternalInteractorBase, mouseMoveEvent, } from "@tsparticles/plugin-int
2
2
  import { isInArray, millisecondsToSeconds } from "@tsparticles/engine";
3
3
  import { clickAttract, hoverAttract } from "./Utils.js";
4
4
  import { Attract } from "./Options/Classes/Attract.js";
5
- const attractMode = "attract";
5
+ const attractMode = "attract", minVelocityLengthSq = 0, minRestoreSpeed = 0.001, maxRestoreSpeed = 1, restoreEpsilon = 0.5;
6
6
  export class Attractor extends ExternalInteractorBase {
7
7
  handleClickMode;
8
+ _interactedThisFrame;
8
9
  _maxDistance;
9
10
  _pluginManager;
11
+ _restoreData;
10
12
  constructor(pluginManager, container) {
11
13
  super(container);
12
14
  this._pluginManager = pluginManager;
13
15
  this._maxDistance = 0;
16
+ this._interactedThisFrame = new Set();
17
+ this._restoreData = new Map();
14
18
  container.attract ??= { particles: [] };
15
19
  this.handleClickMode = (mode, interactivityData) => {
16
20
  const options = this.container.actualOptions, attract = options.interactivity?.modes.attract;
@@ -51,17 +55,23 @@ export class Attractor extends ExternalInteractorBase {
51
55
  container.retina.attractModeDistance = attract.distance * container.retina.pixelRatio;
52
56
  }
53
57
  interact(interactivityData) {
58
+ this._interactedThisFrame.clear();
54
59
  const container = this.container, options = container.actualOptions, mouseMoveStatus = interactivityData.status === mouseMoveEvent, events = options.interactivity?.events;
55
60
  if (!events) {
56
61
  return;
57
62
  }
58
63
  const { enable: hoverEnabled, mode: hoverMode } = events.onHover, { enable: clickEnabled, mode: clickMode } = events.onClick;
59
64
  if (mouseMoveStatus && hoverEnabled && isInArray(attractMode, hoverMode)) {
60
- hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
65
+ hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
66
+ this._trackInteractedParticle(p);
67
+ });
61
68
  }
62
69
  else if (clickEnabled && isInArray(attractMode, clickMode)) {
63
- clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
70
+ clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
71
+ this._trackInteractedParticle(p);
72
+ });
64
73
  }
74
+ this._restoreParticles();
65
75
  }
66
76
  isEnabled(interactivityData, particle) {
67
77
  const container = this.container, options = container.actualOptions, mouse = interactivityData.mouse, events = (particle?.interactivity ?? options.interactivity)?.events;
@@ -79,4 +89,61 @@ export class Attractor extends ExternalInteractorBase {
79
89
  }
80
90
  reset() {
81
91
  }
92
+ _restoreParticles() {
93
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
94
+ if (!restore?.enable || !this._restoreData.size) {
95
+ return;
96
+ }
97
+ const now = Date.now(), restoreDelay = restore.delay * millisecondsToSeconds, restoreSpeed = Math.max(minRestoreSpeed, Math.min(maxRestoreSpeed, restore.speed));
98
+ for (const [particle, restoreData] of this._restoreData) {
99
+ if (this._interactedThisFrame.has(particle)) {
100
+ continue;
101
+ }
102
+ if (particle.destroyed) {
103
+ this._restoreData.delete(particle);
104
+ continue;
105
+ }
106
+ const target = restoreData.target;
107
+ if (now - restoreData.lastInteractionTime < restoreDelay) {
108
+ continue;
109
+ }
110
+ let dx = target.x - particle.position.x, dy = target.y - particle.position.y, dz = target.z - particle.position.z;
111
+ if (restore.follow && particle.options.move.enable) {
112
+ const { x: vx, y: vy, z: vz } = particle.velocity, velocityLengthSq = vx * vx + vy * vy + vz * vz;
113
+ if (velocityLengthSq > minVelocityLengthSq) {
114
+ const parallelScale = (dx * vx + dy * vy + dz * vz) / velocityLengthSq;
115
+ dx -= vx * parallelScale;
116
+ dy -= vy * parallelScale;
117
+ dz -= vz * parallelScale;
118
+ }
119
+ }
120
+ particle.position.x += dx * restoreSpeed;
121
+ particle.position.y += dy * restoreSpeed;
122
+ particle.position.z += dz * restoreSpeed;
123
+ if (Math.abs(dx) <= restoreEpsilon && Math.abs(dy) <= restoreEpsilon) {
124
+ particle.position.x = target.x;
125
+ particle.position.y = target.y;
126
+ particle.position.z = target.z;
127
+ this._restoreData.delete(particle);
128
+ continue;
129
+ }
130
+ }
131
+ }
132
+ _trackInteractedParticle(particle) {
133
+ this._interactedThisFrame.add(particle);
134
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
135
+ if (!restore?.enable) {
136
+ return;
137
+ }
138
+ const now = Date.now();
139
+ let restoreData = this._restoreData.get(particle);
140
+ if (!restoreData) {
141
+ restoreData = {
142
+ target: particle.position.copy(),
143
+ lastInteractionTime: now,
144
+ };
145
+ this._restoreData.set(particle, restoreData);
146
+ }
147
+ restoreData.lastInteractionTime = now;
148
+ }
82
149
  }
@@ -5,6 +5,7 @@ export class Attract {
5
5
  easing;
6
6
  factor;
7
7
  maxSpeed;
8
+ restore;
8
9
  speed;
9
10
  constructor() {
10
11
  this.distance = 200;
@@ -13,6 +14,12 @@ export class Attract {
13
14
  this.factor = 1;
14
15
  this.maxSpeed = 50;
15
16
  this.speed = 1;
17
+ this.restore = {
18
+ enable: false,
19
+ delay: 0,
20
+ speed: 0.08,
21
+ follow: true,
22
+ };
16
23
  }
17
24
  load(data) {
18
25
  if (isNull(data)) {
@@ -36,5 +43,11 @@ export class Attract {
36
43
  if (data.speed !== undefined) {
37
44
  this.speed = data.speed;
38
45
  }
46
+ if (data.restore !== undefined) {
47
+ this.restore.enable = data.restore.enable ?? this.restore.enable;
48
+ this.restore.delay = data.restore.delay ?? this.restore.delay;
49
+ this.restore.speed = data.restore.speed ?? this.restore.speed;
50
+ this.restore.follow = data.restore.follow ?? this.restore.follow;
51
+ }
39
52
  }
40
53
  }
package/cjs/Utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Circle, Vector, clamp, getDistances, identity, } from "@tsparticles/engine";
2
2
  const minFactor = 1, minRadius = 0, updateVector = Vector.origin;
3
- function processAttract(pluginManager, container, position, attractRadius, area, queryCb) {
3
+ function processAttract(pluginManager, container, position, attractRadius, area, queryCb, onAttractParticle) {
4
4
  const attractOptions = container.actualOptions.interactivity?.modes.attract;
5
5
  if (!attractOptions) {
6
6
  return;
@@ -10,10 +10,11 @@ function processAttract(pluginManager, container, position, attractRadius, area,
10
10
  const { dx, dy, distance } = getDistances(particle.position, position), velocity = attractOptions.speed * attractOptions.factor, attractFactor = clamp(pluginManager.getEasing(attractOptions.easing)(identity - distance / attractRadius) * velocity, minFactor, attractOptions.maxSpeed);
11
11
  updateVector.x = !distance ? velocity : (dx / distance) * attractFactor;
12
12
  updateVector.y = !distance ? velocity : (dy / distance) * attractFactor;
13
+ onAttractParticle?.(particle);
13
14
  particle.position.subFrom(updateVector);
14
15
  }
15
16
  }
16
- export function clickAttract(pluginManager, container, interactivityData, enabledCb) {
17
+ export function clickAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
17
18
  container.attract ??= { particles: [] };
18
19
  const { attract } = container;
19
20
  if (!attract.finish) {
@@ -28,16 +29,16 @@ export function clickAttract(pluginManager, container, interactivityData, enable
28
29
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
29
30
  return;
30
31
  }
31
- processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
32
+ processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
32
33
  }
33
34
  else if (attract.clicking === false) {
34
35
  attract.particles = [];
35
36
  }
36
37
  }
37
- export function hoverAttract(pluginManager, container, interactivityData, enabledCb) {
38
+ export function hoverAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
38
39
  const mousePos = interactivityData.mouse.position, attractRadius = container.retina.attractModeDistance;
39
40
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
40
41
  return;
41
42
  }
42
- processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
43
+ processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
43
44
  }
package/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ensureInteractivityPluginLoaded } from "@tsparticles/plugin-interactivity";
2
2
  import { Attractor } from "./Attractor.js";
3
3
  export async function loadExternalAttractInteraction(engine) {
4
- engine.checkVersion("4.0.0-beta.16");
4
+ engine.checkVersion("4.0.0-beta.17");
5
5
  await engine.pluginManager.register((e) => {
6
6
  ensureInteractivityPluginLoaded(e);
7
7
  e.pluginManager.addInteractor?.("externalAttract", container => {
package/cjs/index.lazy.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export async function loadExternalAttractInteraction(engine) {
2
- engine.checkVersion("4.0.0-beta.16");
2
+ engine.checkVersion("4.0.0-beta.17");
3
3
  await engine.pluginManager.register(async (e) => {
4
4
  const { ensureInteractivityPluginLoaded } = await import("@tsparticles/plugin-interactivity/lazy");
5
5
  ensureInteractivityPluginLoaded(e);
package/esm/Attractor.js CHANGED
@@ -2,15 +2,19 @@ import { ExternalInteractorBase, mouseMoveEvent, } from "@tsparticles/plugin-int
2
2
  import { isInArray, millisecondsToSeconds } from "@tsparticles/engine";
3
3
  import { clickAttract, hoverAttract } from "./Utils.js";
4
4
  import { Attract } from "./Options/Classes/Attract.js";
5
- const attractMode = "attract";
5
+ const attractMode = "attract", minVelocityLengthSq = 0, minRestoreSpeed = 0.001, maxRestoreSpeed = 1, restoreEpsilon = 0.5;
6
6
  export class Attractor extends ExternalInteractorBase {
7
7
  handleClickMode;
8
+ _interactedThisFrame;
8
9
  _maxDistance;
9
10
  _pluginManager;
11
+ _restoreData;
10
12
  constructor(pluginManager, container) {
11
13
  super(container);
12
14
  this._pluginManager = pluginManager;
13
15
  this._maxDistance = 0;
16
+ this._interactedThisFrame = new Set();
17
+ this._restoreData = new Map();
14
18
  container.attract ??= { particles: [] };
15
19
  this.handleClickMode = (mode, interactivityData) => {
16
20
  const options = this.container.actualOptions, attract = options.interactivity?.modes.attract;
@@ -51,17 +55,23 @@ export class Attractor extends ExternalInteractorBase {
51
55
  container.retina.attractModeDistance = attract.distance * container.retina.pixelRatio;
52
56
  }
53
57
  interact(interactivityData) {
58
+ this._interactedThisFrame.clear();
54
59
  const container = this.container, options = container.actualOptions, mouseMoveStatus = interactivityData.status === mouseMoveEvent, events = options.interactivity?.events;
55
60
  if (!events) {
56
61
  return;
57
62
  }
58
63
  const { enable: hoverEnabled, mode: hoverMode } = events.onHover, { enable: clickEnabled, mode: clickMode } = events.onClick;
59
64
  if (mouseMoveStatus && hoverEnabled && isInArray(attractMode, hoverMode)) {
60
- hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
65
+ hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
66
+ this._trackInteractedParticle(p);
67
+ });
61
68
  }
62
69
  else if (clickEnabled && isInArray(attractMode, clickMode)) {
63
- clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
70
+ clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
71
+ this._trackInteractedParticle(p);
72
+ });
64
73
  }
74
+ this._restoreParticles();
65
75
  }
66
76
  isEnabled(interactivityData, particle) {
67
77
  const container = this.container, options = container.actualOptions, mouse = interactivityData.mouse, events = (particle?.interactivity ?? options.interactivity)?.events;
@@ -79,4 +89,61 @@ export class Attractor extends ExternalInteractorBase {
79
89
  }
80
90
  reset() {
81
91
  }
92
+ _restoreParticles() {
93
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
94
+ if (!restore?.enable || !this._restoreData.size) {
95
+ return;
96
+ }
97
+ const now = Date.now(), restoreDelay = restore.delay * millisecondsToSeconds, restoreSpeed = Math.max(minRestoreSpeed, Math.min(maxRestoreSpeed, restore.speed));
98
+ for (const [particle, restoreData] of this._restoreData) {
99
+ if (this._interactedThisFrame.has(particle)) {
100
+ continue;
101
+ }
102
+ if (particle.destroyed) {
103
+ this._restoreData.delete(particle);
104
+ continue;
105
+ }
106
+ const target = restoreData.target;
107
+ if (now - restoreData.lastInteractionTime < restoreDelay) {
108
+ continue;
109
+ }
110
+ let dx = target.x - particle.position.x, dy = target.y - particle.position.y, dz = target.z - particle.position.z;
111
+ if (restore.follow && particle.options.move.enable) {
112
+ const { x: vx, y: vy, z: vz } = particle.velocity, velocityLengthSq = vx * vx + vy * vy + vz * vz;
113
+ if (velocityLengthSq > minVelocityLengthSq) {
114
+ const parallelScale = (dx * vx + dy * vy + dz * vz) / velocityLengthSq;
115
+ dx -= vx * parallelScale;
116
+ dy -= vy * parallelScale;
117
+ dz -= vz * parallelScale;
118
+ }
119
+ }
120
+ particle.position.x += dx * restoreSpeed;
121
+ particle.position.y += dy * restoreSpeed;
122
+ particle.position.z += dz * restoreSpeed;
123
+ if (Math.abs(dx) <= restoreEpsilon && Math.abs(dy) <= restoreEpsilon) {
124
+ particle.position.x = target.x;
125
+ particle.position.y = target.y;
126
+ particle.position.z = target.z;
127
+ this._restoreData.delete(particle);
128
+ continue;
129
+ }
130
+ }
131
+ }
132
+ _trackInteractedParticle(particle) {
133
+ this._interactedThisFrame.add(particle);
134
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
135
+ if (!restore?.enable) {
136
+ return;
137
+ }
138
+ const now = Date.now();
139
+ let restoreData = this._restoreData.get(particle);
140
+ if (!restoreData) {
141
+ restoreData = {
142
+ target: particle.position.copy(),
143
+ lastInteractionTime: now,
144
+ };
145
+ this._restoreData.set(particle, restoreData);
146
+ }
147
+ restoreData.lastInteractionTime = now;
148
+ }
82
149
  }
@@ -5,6 +5,7 @@ export class Attract {
5
5
  easing;
6
6
  factor;
7
7
  maxSpeed;
8
+ restore;
8
9
  speed;
9
10
  constructor() {
10
11
  this.distance = 200;
@@ -13,6 +14,12 @@ export class Attract {
13
14
  this.factor = 1;
14
15
  this.maxSpeed = 50;
15
16
  this.speed = 1;
17
+ this.restore = {
18
+ enable: false,
19
+ delay: 0,
20
+ speed: 0.08,
21
+ follow: true,
22
+ };
16
23
  }
17
24
  load(data) {
18
25
  if (isNull(data)) {
@@ -36,5 +43,11 @@ export class Attract {
36
43
  if (data.speed !== undefined) {
37
44
  this.speed = data.speed;
38
45
  }
46
+ if (data.restore !== undefined) {
47
+ this.restore.enable = data.restore.enable ?? this.restore.enable;
48
+ this.restore.delay = data.restore.delay ?? this.restore.delay;
49
+ this.restore.speed = data.restore.speed ?? this.restore.speed;
50
+ this.restore.follow = data.restore.follow ?? this.restore.follow;
51
+ }
39
52
  }
40
53
  }
package/esm/Utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Circle, Vector, clamp, getDistances, identity, } from "@tsparticles/engine";
2
2
  const minFactor = 1, minRadius = 0, updateVector = Vector.origin;
3
- function processAttract(pluginManager, container, position, attractRadius, area, queryCb) {
3
+ function processAttract(pluginManager, container, position, attractRadius, area, queryCb, onAttractParticle) {
4
4
  const attractOptions = container.actualOptions.interactivity?.modes.attract;
5
5
  if (!attractOptions) {
6
6
  return;
@@ -10,10 +10,11 @@ function processAttract(pluginManager, container, position, attractRadius, area,
10
10
  const { dx, dy, distance } = getDistances(particle.position, position), velocity = attractOptions.speed * attractOptions.factor, attractFactor = clamp(pluginManager.getEasing(attractOptions.easing)(identity - distance / attractRadius) * velocity, minFactor, attractOptions.maxSpeed);
11
11
  updateVector.x = !distance ? velocity : (dx / distance) * attractFactor;
12
12
  updateVector.y = !distance ? velocity : (dy / distance) * attractFactor;
13
+ onAttractParticle?.(particle);
13
14
  particle.position.subFrom(updateVector);
14
15
  }
15
16
  }
16
- export function clickAttract(pluginManager, container, interactivityData, enabledCb) {
17
+ export function clickAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
17
18
  container.attract ??= { particles: [] };
18
19
  const { attract } = container;
19
20
  if (!attract.finish) {
@@ -28,16 +29,16 @@ export function clickAttract(pluginManager, container, interactivityData, enable
28
29
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
29
30
  return;
30
31
  }
31
- processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
32
+ processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
32
33
  }
33
34
  else if (attract.clicking === false) {
34
35
  attract.particles = [];
35
36
  }
36
37
  }
37
- export function hoverAttract(pluginManager, container, interactivityData, enabledCb) {
38
+ export function hoverAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
38
39
  const mousePos = interactivityData.mouse.position, attractRadius = container.retina.attractModeDistance;
39
40
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
40
41
  return;
41
42
  }
42
- processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
43
+ processAttract(pluginManager, container, mousePos, attractRadius, new Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
43
44
  }
package/esm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ensureInteractivityPluginLoaded } from "@tsparticles/plugin-interactivity";
2
2
  import { Attractor } from "./Attractor.js";
3
3
  export async function loadExternalAttractInteraction(engine) {
4
- engine.checkVersion("4.0.0-beta.16");
4
+ engine.checkVersion("4.0.0-beta.17");
5
5
  await engine.pluginManager.register((e) => {
6
6
  ensureInteractivityPluginLoaded(e);
7
7
  e.pluginManager.addInteractor?.("externalAttract", container => {
package/esm/index.lazy.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export async function loadExternalAttractInteraction(engine) {
2
- engine.checkVersion("4.0.0-beta.16");
2
+ engine.checkVersion("4.0.0-beta.17");
3
3
  await engine.pluginManager.register(async (e) => {
4
4
  const { ensureInteractivityPluginLoaded } = await import("@tsparticles/plugin-interactivity/lazy");
5
5
  ensureInteractivityPluginLoaded(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsparticles/interaction-external-attract",
3
- "version": "4.0.0-beta.16",
3
+ "version": "4.0.0-beta.17",
4
4
  "description": "tsParticles attract external interaction",
5
5
  "homepage": "https://particles.js.org",
6
6
  "repository": {
@@ -101,7 +101,7 @@
101
101
  },
102
102
  "type": "module",
103
103
  "peerDependencies": {
104
- "@tsparticles/engine": "4.0.0-beta.16",
105
- "@tsparticles/plugin-interactivity": "4.0.0-beta.16"
104
+ "@tsparticles/engine": "4.0.0-beta.17",
105
+ "@tsparticles/plugin-interactivity": "4.0.0-beta.17"
106
106
  }
107
107
  }
package/report.html CHANGED
@@ -4930,7 +4930,7 @@ var drawChart = (function (exports) {
4930
4930
  </script>
4931
4931
  <script>
4932
4932
  /*<!--*/
4933
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"tsparticles.interaction.external.attract.js","children":[{"name":"dist/browser","children":[{"uid":"0ffb6b39-1","name":"Utils.js"},{"name":"Options/Classes/Attract.js","uid":"0ffb6b39-3"},{"uid":"0ffb6b39-5","name":"Attractor.js"},{"uid":"0ffb6b39-7","name":"index.js"},{"uid":"0ffb6b39-9","name":"browser.js"}]}]}],"isRoot":true},"nodeParts":{"0ffb6b39-1":{"renderedLength":2368,"gzipLength":0,"brotliLength":0,"metaUid":"0ffb6b39-0"},"0ffb6b39-3":{"renderedLength":1107,"gzipLength":0,"brotliLength":0,"metaUid":"0ffb6b39-2"},"0ffb6b39-5":{"renderedLength":3774,"gzipLength":0,"brotliLength":0,"metaUid":"0ffb6b39-4"},"0ffb6b39-7":{"renderedLength":421,"gzipLength":0,"brotliLength":0,"metaUid":"0ffb6b39-6"},"0ffb6b39-9":{"renderedLength":203,"gzipLength":0,"brotliLength":0,"metaUid":"0ffb6b39-8"}},"nodeMetas":{"0ffb6b39-0":{"id":"/dist/browser/Utils.js","moduleParts":{"tsparticles.interaction.external.attract.js":"0ffb6b39-1"},"imported":[{"uid":"0ffb6b39-11"}],"importedBy":[{"uid":"0ffb6b39-4"}]},"0ffb6b39-2":{"id":"/dist/browser/Options/Classes/Attract.js","moduleParts":{"tsparticles.interaction.external.attract.js":"0ffb6b39-3"},"imported":[{"uid":"0ffb6b39-11"}],"importedBy":[{"uid":"0ffb6b39-6"},{"uid":"0ffb6b39-4"}]},"0ffb6b39-4":{"id":"/dist/browser/Attractor.js","moduleParts":{"tsparticles.interaction.external.attract.js":"0ffb6b39-5"},"imported":[{"uid":"0ffb6b39-10"},{"uid":"0ffb6b39-11"},{"uid":"0ffb6b39-0"},{"uid":"0ffb6b39-2"}],"importedBy":[{"uid":"0ffb6b39-6"}]},"0ffb6b39-6":{"id":"/dist/browser/index.js","moduleParts":{"tsparticles.interaction.external.attract.js":"0ffb6b39-7"},"imported":[{"uid":"0ffb6b39-10"},{"uid":"0ffb6b39-4"},{"uid":"0ffb6b39-2"}],"importedBy":[{"uid":"0ffb6b39-8"}]},"0ffb6b39-8":{"id":"/dist/browser/browser.js","moduleParts":{"tsparticles.interaction.external.attract.js":"0ffb6b39-9"},"imported":[{"uid":"0ffb6b39-6"}],"importedBy":[],"isEntry":true},"0ffb6b39-10":{"id":"@tsparticles/plugin-interactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"0ffb6b39-6"},{"uid":"0ffb6b39-4"}],"isExternal":true},"0ffb6b39-11":{"id":"@tsparticles/engine","moduleParts":{},"imported":[],"importedBy":[{"uid":"0ffb6b39-4"},{"uid":"0ffb6b39-2"},{"uid":"0ffb6b39-0"}],"isExternal":true}},"env":{"rollup":"4.60.3"},"options":{"gzip":false,"brotli":false,"sourcemap":false}};
4933
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"tsparticles.interaction.external.attract.js","children":[{"name":"dist/browser","children":[{"uid":"b8e72f39-1","name":"Utils.js"},{"name":"Options/Classes/Attract.js","uid":"b8e72f39-3"},{"uid":"b8e72f39-5","name":"Attractor.js"},{"uid":"b8e72f39-7","name":"index.js"},{"uid":"b8e72f39-9","name":"browser.js"}]}]}],"isRoot":true},"nodeParts":{"b8e72f39-1":{"renderedLength":2506,"gzipLength":0,"brotliLength":0,"metaUid":"b8e72f39-0"},"b8e72f39-3":{"renderedLength":1666,"gzipLength":0,"brotliLength":0,"metaUid":"b8e72f39-2"},"b8e72f39-5":{"renderedLength":7074,"gzipLength":0,"brotliLength":0,"metaUid":"b8e72f39-4"},"b8e72f39-7":{"renderedLength":421,"gzipLength":0,"brotliLength":0,"metaUid":"b8e72f39-6"},"b8e72f39-9":{"renderedLength":203,"gzipLength":0,"brotliLength":0,"metaUid":"b8e72f39-8"}},"nodeMetas":{"b8e72f39-0":{"id":"/dist/browser/Utils.js","moduleParts":{"tsparticles.interaction.external.attract.js":"b8e72f39-1"},"imported":[{"uid":"b8e72f39-11"}],"importedBy":[{"uid":"b8e72f39-4"}]},"b8e72f39-2":{"id":"/dist/browser/Options/Classes/Attract.js","moduleParts":{"tsparticles.interaction.external.attract.js":"b8e72f39-3"},"imported":[{"uid":"b8e72f39-11"}],"importedBy":[{"uid":"b8e72f39-6"},{"uid":"b8e72f39-4"}]},"b8e72f39-4":{"id":"/dist/browser/Attractor.js","moduleParts":{"tsparticles.interaction.external.attract.js":"b8e72f39-5"},"imported":[{"uid":"b8e72f39-10"},{"uid":"b8e72f39-11"},{"uid":"b8e72f39-0"},{"uid":"b8e72f39-2"}],"importedBy":[{"uid":"b8e72f39-6"}]},"b8e72f39-6":{"id":"/dist/browser/index.js","moduleParts":{"tsparticles.interaction.external.attract.js":"b8e72f39-7"},"imported":[{"uid":"b8e72f39-10"},{"uid":"b8e72f39-4"},{"uid":"b8e72f39-2"}],"importedBy":[{"uid":"b8e72f39-8"}]},"b8e72f39-8":{"id":"/dist/browser/browser.js","moduleParts":{"tsparticles.interaction.external.attract.js":"b8e72f39-9"},"imported":[{"uid":"b8e72f39-6"}],"importedBy":[],"isEntry":true},"b8e72f39-10":{"id":"@tsparticles/plugin-interactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"b8e72f39-6"},{"uid":"b8e72f39-4"}],"isExternal":true},"b8e72f39-11":{"id":"@tsparticles/engine","moduleParts":{},"imported":[],"importedBy":[{"uid":"b8e72f39-4"},{"uid":"b8e72f39-2"},{"uid":"b8e72f39-0"}],"isExternal":true}},"env":{"rollup":"4.60.3"},"options":{"gzip":false,"brotli":false,"sourcemap":false}};
4934
4934
 
4935
4935
  const run = () => {
4936
4936
  const width = window.innerWidth;
@@ -1,5 +1,5 @@
1
1
  (function(g){g.__tsParticlesInternals=g.__tsParticlesInternals||{};g.__tsParticlesInternals.bundles=g.__tsParticlesInternals.bundles||{};g.__tsParticlesInternals.effects=g.__tsParticlesInternals.effects||{};g.__tsParticlesInternals.engine=g.__tsParticlesInternals.engine||{};g.__tsParticlesInternals.interactions=g.__tsParticlesInternals.interactions||{};g.__tsParticlesInternals.palettes=g.__tsParticlesInternals.palettes||{};g.__tsParticlesInternals.paths=g.__tsParticlesInternals.paths||{};g.__tsParticlesInternals.plugins=g.__tsParticlesInternals.plugins||{};g.__tsParticlesInternals.plugins=g.__tsParticlesInternals.plugins||{};g.__tsParticlesInternals.plugins.emittersShapes=g.__tsParticlesInternals.plugins.emittersShapes||{};g.__tsParticlesInternals.presets=g.__tsParticlesInternals.presets||{};g.__tsParticlesInternals.shapes=g.__tsParticlesInternals.shapes||{};g.__tsParticlesInternals.updaters=g.__tsParticlesInternals.updaters||{};g.__tsParticlesInternals.utils=g.__tsParticlesInternals.utils||{};g.__tsParticlesInternals.canvas=g.__tsParticlesInternals.canvas||{};g.__tsParticlesInternals.canvas=g.__tsParticlesInternals.canvas||{};g.__tsParticlesInternals.canvas.utils=g.__tsParticlesInternals.canvas.utils||{};g.__tsParticlesInternals.path=g.__tsParticlesInternals.path||{};g.__tsParticlesInternals.path=g.__tsParticlesInternals.path||{};g.__tsParticlesInternals.path.utils=g.__tsParticlesInternals.path.utils||{};var __tsProxyFactory=typeof Proxy!=="undefined"?function(obj){return new Proxy(obj,{get:function(target,key){if(!(key in target)){target[key]={};}return target[key];}});}:function(obj){return obj;};g.__tsParticlesInternals.bundles=__tsProxyFactory(g.__tsParticlesInternals.bundles);g.__tsParticlesInternals.effects=__tsProxyFactory(g.__tsParticlesInternals.effects);g.__tsParticlesInternals.interactions=__tsProxyFactory(g.__tsParticlesInternals.interactions);g.__tsParticlesInternals.palettes=__tsProxyFactory(g.__tsParticlesInternals.palettes);g.__tsParticlesInternals.paths=__tsProxyFactory(g.__tsParticlesInternals.paths);g.__tsParticlesInternals.plugins=__tsProxyFactory(g.__tsParticlesInternals.plugins);g.__tsParticlesInternals.plugins.emittersShapes=__tsProxyFactory(g.__tsParticlesInternals.plugins.emittersShapes);g.__tsParticlesInternals.presets=__tsProxyFactory(g.__tsParticlesInternals.presets);g.__tsParticlesInternals.shapes=__tsProxyFactory(g.__tsParticlesInternals.shapes);g.__tsParticlesInternals.updaters=__tsProxyFactory(g.__tsParticlesInternals.updaters);g.__tsParticlesInternals.utils=__tsProxyFactory(g.__tsParticlesInternals.utils);g.__tsParticlesInternals.canvas=__tsProxyFactory(g.__tsParticlesInternals.canvas);g.__tsParticlesInternals.path=__tsProxyFactory(g.__tsParticlesInternals.path);g.tsparticlesInternalExports=g.tsparticlesInternalExports||{};})(typeof globalThis!=="undefined"?globalThis:typeof window!=="undefined"?window:this);
2
- /* External Interaction v4.0.0-beta.16 */
2
+ /* External Interaction v4.0.0-beta.17 */
3
3
  (function (global, factory) {
4
4
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tsparticles/plugin-interactivity'), require('@tsparticles/engine')) :
5
5
  typeof define === 'function' && define.amd ? define(['exports', '@tsparticles/plugin-interactivity', '@tsparticles/engine'], factory) :
@@ -7,7 +7,7 @@
7
7
  })(this, (function (exports, pluginInteractivity, engine) { 'use strict';
8
8
 
9
9
  const minFactor = 1, minRadius = 0, updateVector = engine.Vector.origin;
10
- function processAttract(pluginManager, container, position, attractRadius, area, queryCb) {
10
+ function processAttract(pluginManager, container, position, attractRadius, area, queryCb, onAttractParticle) {
11
11
  const attractOptions = container.actualOptions.interactivity?.modes.attract;
12
12
  if (!attractOptions) {
13
13
  return;
@@ -17,10 +17,11 @@
17
17
  const { dx, dy, distance } = engine.getDistances(particle.position, position), velocity = attractOptions.speed * attractOptions.factor, attractFactor = engine.clamp(pluginManager.getEasing(attractOptions.easing)(engine.identity - distance / attractRadius) * velocity, minFactor, attractOptions.maxSpeed);
18
18
  updateVector.x = !distance ? velocity : (dx / distance) * attractFactor;
19
19
  updateVector.y = !distance ? velocity : (dy / distance) * attractFactor;
20
+ onAttractParticle?.(particle);
20
21
  particle.position.subFrom(updateVector);
21
22
  }
22
23
  }
23
- function clickAttract(pluginManager, container, interactivityData, enabledCb) {
24
+ function clickAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
24
25
  container.attract ??= { particles: [] };
25
26
  const { attract } = container;
26
27
  if (!attract.finish) {
@@ -35,18 +36,18 @@
35
36
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
36
37
  return;
37
38
  }
38
- processAttract(pluginManager, container, mousePos, attractRadius, new engine.Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
39
+ processAttract(pluginManager, container, mousePos, attractRadius, new engine.Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
39
40
  }
40
41
  else if (attract.clicking === false) {
41
42
  attract.particles = [];
42
43
  }
43
44
  }
44
- function hoverAttract(pluginManager, container, interactivityData, enabledCb) {
45
+ function hoverAttract(pluginManager, container, interactivityData, enabledCb, onAttractParticle) {
45
46
  const mousePos = interactivityData.mouse.position, attractRadius = container.retina.attractModeDistance;
46
47
  if (!attractRadius || attractRadius < minRadius || !mousePos) {
47
48
  return;
48
49
  }
49
- processAttract(pluginManager, container, mousePos, attractRadius, new engine.Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p));
50
+ processAttract(pluginManager, container, mousePos, attractRadius, new engine.Circle(mousePos.x, mousePos.y, attractRadius), (p) => enabledCb(p), onAttractParticle);
50
51
  }
51
52
 
52
53
  class Attract {
@@ -55,6 +56,7 @@
55
56
  easing;
56
57
  factor;
57
58
  maxSpeed;
59
+ restore;
58
60
  speed;
59
61
  constructor() {
60
62
  this.distance = 200;
@@ -63,6 +65,12 @@
63
65
  this.factor = 1;
64
66
  this.maxSpeed = 50;
65
67
  this.speed = 1;
68
+ this.restore = {
69
+ enable: false,
70
+ delay: 0,
71
+ speed: 0.08,
72
+ follow: true,
73
+ };
66
74
  }
67
75
  load(data) {
68
76
  if (engine.isNull(data)) {
@@ -86,18 +94,28 @@
86
94
  if (data.speed !== undefined) {
87
95
  this.speed = data.speed;
88
96
  }
97
+ if (data.restore !== undefined) {
98
+ this.restore.enable = data.restore.enable ?? this.restore.enable;
99
+ this.restore.delay = data.restore.delay ?? this.restore.delay;
100
+ this.restore.speed = data.restore.speed ?? this.restore.speed;
101
+ this.restore.follow = data.restore.follow ?? this.restore.follow;
102
+ }
89
103
  }
90
104
  }
91
105
 
92
- const attractMode = "attract";
106
+ const attractMode = "attract", minVelocityLengthSq = 0, minRestoreSpeed = 0.001, maxRestoreSpeed = 1, restoreEpsilon = 0.5;
93
107
  class Attractor extends pluginInteractivity.ExternalInteractorBase {
94
108
  handleClickMode;
109
+ _interactedThisFrame;
95
110
  _maxDistance;
96
111
  _pluginManager;
112
+ _restoreData;
97
113
  constructor(pluginManager, container) {
98
114
  super(container);
99
115
  this._pluginManager = pluginManager;
100
116
  this._maxDistance = 0;
117
+ this._interactedThisFrame = new Set();
118
+ this._restoreData = new Map();
101
119
  container.attract ??= { particles: [] };
102
120
  this.handleClickMode = (mode, interactivityData) => {
103
121
  const options = this.container.actualOptions, attract = options.interactivity?.modes.attract;
@@ -138,17 +156,23 @@
138
156
  container.retina.attractModeDistance = attract.distance * container.retina.pixelRatio;
139
157
  }
140
158
  interact(interactivityData) {
159
+ this._interactedThisFrame.clear();
141
160
  const container = this.container, options = container.actualOptions, mouseMoveStatus = interactivityData.status === pluginInteractivity.mouseMoveEvent, events = options.interactivity?.events;
142
161
  if (!events) {
143
162
  return;
144
163
  }
145
164
  const { enable: hoverEnabled, mode: hoverMode } = events.onHover, { enable: clickEnabled, mode: clickMode } = events.onClick;
146
165
  if (mouseMoveStatus && hoverEnabled && engine.isInArray(attractMode, hoverMode)) {
147
- hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
166
+ hoverAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
167
+ this._trackInteractedParticle(p);
168
+ });
148
169
  }
149
170
  else if (clickEnabled && engine.isInArray(attractMode, clickMode)) {
150
- clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p));
171
+ clickAttract(this._pluginManager, this.container, interactivityData, p => this.isEnabled(interactivityData, p), p => {
172
+ this._trackInteractedParticle(p);
173
+ });
151
174
  }
175
+ this._restoreParticles();
152
176
  }
153
177
  isEnabled(interactivityData, particle) {
154
178
  const container = this.container, options = container.actualOptions, mouse = interactivityData.mouse, events = (particle?.interactivity ?? options.interactivity)?.events;
@@ -166,10 +190,67 @@
166
190
  }
167
191
  reset() {
168
192
  }
193
+ _restoreParticles() {
194
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
195
+ if (!restore?.enable || !this._restoreData.size) {
196
+ return;
197
+ }
198
+ const now = Date.now(), restoreDelay = restore.delay * engine.millisecondsToSeconds, restoreSpeed = Math.max(minRestoreSpeed, Math.min(maxRestoreSpeed, restore.speed));
199
+ for (const [particle, restoreData] of this._restoreData) {
200
+ if (this._interactedThisFrame.has(particle)) {
201
+ continue;
202
+ }
203
+ if (particle.destroyed) {
204
+ this._restoreData.delete(particle);
205
+ continue;
206
+ }
207
+ const target = restoreData.target;
208
+ if (now - restoreData.lastInteractionTime < restoreDelay) {
209
+ continue;
210
+ }
211
+ let dx = target.x - particle.position.x, dy = target.y - particle.position.y, dz = target.z - particle.position.z;
212
+ if (restore.follow && particle.options.move.enable) {
213
+ const { x: vx, y: vy, z: vz } = particle.velocity, velocityLengthSq = vx * vx + vy * vy + vz * vz;
214
+ if (velocityLengthSq > minVelocityLengthSq) {
215
+ const parallelScale = (dx * vx + dy * vy + dz * vz) / velocityLengthSq;
216
+ dx -= vx * parallelScale;
217
+ dy -= vy * parallelScale;
218
+ dz -= vz * parallelScale;
219
+ }
220
+ }
221
+ particle.position.x += dx * restoreSpeed;
222
+ particle.position.y += dy * restoreSpeed;
223
+ particle.position.z += dz * restoreSpeed;
224
+ if (Math.abs(dx) <= restoreEpsilon && Math.abs(dy) <= restoreEpsilon) {
225
+ particle.position.x = target.x;
226
+ particle.position.y = target.y;
227
+ particle.position.z = target.z;
228
+ this._restoreData.delete(particle);
229
+ continue;
230
+ }
231
+ }
232
+ }
233
+ _trackInteractedParticle(particle) {
234
+ this._interactedThisFrame.add(particle);
235
+ const restore = this.container.actualOptions.interactivity?.modes.attract?.restore;
236
+ if (!restore?.enable) {
237
+ return;
238
+ }
239
+ const now = Date.now();
240
+ let restoreData = this._restoreData.get(particle);
241
+ if (!restoreData) {
242
+ restoreData = {
243
+ target: particle.position.copy(),
244
+ lastInteractionTime: now,
245
+ };
246
+ this._restoreData.set(particle, restoreData);
247
+ }
248
+ restoreData.lastInteractionTime = now;
249
+ }
169
250
  }
170
251
 
171
252
  async function loadExternalAttractInteraction(engine) {
172
- engine.checkVersion("4.0.0-beta.16");
253
+ engine.checkVersion("4.0.0-beta.17");
173
254
  await engine.pluginManager.register((e) => {
174
255
  pluginInteractivity.ensureInteractivityPluginLoaded(e);
175
256
  e.pluginManager.addInteractor?.("externalAttract", container => {
@@ -1 +1 @@
1
- !function(t){t.__tsParticlesInternals=t.__tsParticlesInternals||{},t.__tsParticlesInternals.bundles=t.__tsParticlesInternals.bundles||{},t.__tsParticlesInternals.effects=t.__tsParticlesInternals.effects||{},t.__tsParticlesInternals.engine=t.__tsParticlesInternals.engine||{},t.__tsParticlesInternals.interactions=t.__tsParticlesInternals.interactions||{},t.__tsParticlesInternals.palettes=t.__tsParticlesInternals.palettes||{},t.__tsParticlesInternals.paths=t.__tsParticlesInternals.paths||{},t.__tsParticlesInternals.plugins=t.__tsParticlesInternals.plugins||{},t.__tsParticlesInternals.plugins=t.__tsParticlesInternals.plugins||{},t.__tsParticlesInternals.plugins.emittersShapes=t.__tsParticlesInternals.plugins.emittersShapes||{},t.__tsParticlesInternals.presets=t.__tsParticlesInternals.presets||{},t.__tsParticlesInternals.shapes=t.__tsParticlesInternals.shapes||{},t.__tsParticlesInternals.updaters=t.__tsParticlesInternals.updaters||{},t.__tsParticlesInternals.utils=t.__tsParticlesInternals.utils||{},t.__tsParticlesInternals.canvas=t.__tsParticlesInternals.canvas||{},t.__tsParticlesInternals.canvas=t.__tsParticlesInternals.canvas||{},t.__tsParticlesInternals.canvas.utils=t.__tsParticlesInternals.canvas.utils||{},t.__tsParticlesInternals.path=t.__tsParticlesInternals.path||{},t.__tsParticlesInternals.path=t.__tsParticlesInternals.path||{},t.__tsParticlesInternals.path.utils=t.__tsParticlesInternals.path.utils||{};var s="undefined"!=typeof Proxy?function(t){return new Proxy(t,{get:function(t,s){return s in t||(t[s]={}),t[s]}})}:function(t){return t};t.__tsParticlesInternals.bundles=s(t.__tsParticlesInternals.bundles),t.__tsParticlesInternals.effects=s(t.__tsParticlesInternals.effects),t.__tsParticlesInternals.interactions=s(t.__tsParticlesInternals.interactions),t.__tsParticlesInternals.palettes=s(t.__tsParticlesInternals.palettes),t.__tsParticlesInternals.paths=s(t.__tsParticlesInternals.paths),t.__tsParticlesInternals.plugins=s(t.__tsParticlesInternals.plugins),t.__tsParticlesInternals.plugins.emittersShapes=s(t.__tsParticlesInternals.plugins.emittersShapes),t.__tsParticlesInternals.presets=s(t.__tsParticlesInternals.presets),t.__tsParticlesInternals.shapes=s(t.__tsParticlesInternals.shapes),t.__tsParticlesInternals.updaters=s(t.__tsParticlesInternals.updaters),t.__tsParticlesInternals.utils=s(t.__tsParticlesInternals.utils),t.__tsParticlesInternals.canvas=s(t.__tsParticlesInternals.canvas),t.__tsParticlesInternals.path=s(t.__tsParticlesInternals.path),t.tsparticlesInternalExports=t.tsparticlesInternalExports||{}}("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:this),function(t,s){"object"==typeof exports&&"undefined"!=typeof module?s(exports,require("@tsparticles/plugin-interactivity"),require("@tsparticles/engine")):"function"==typeof define&&define.amd?define(["exports","@tsparticles/plugin-interactivity","@tsparticles/engine"],s):s(((t="undefined"!=typeof globalThis?globalThis:t||self).__tsParticlesInternals=t.__tsParticlesInternals||{},t.__tsParticlesInternals.interactions=t.__tsParticlesInternals.interactions||{},t.__tsParticlesInternals.interactions.externalAttract=t.__tsParticlesInternals.interactions.externalAttract||{}),t.__tsParticlesInternals.plugins.interactivity,t.__tsParticlesInternals.engine)}(this,function(t,s,e){"use strict";const n=e.Vector.origin;function a(t,s,a,i,r,l){const c=s.actualOptions.interactivity?.modes.attract;if(!c)return;const o=s.particles.grid.query(r,l);for(const s of o){const{dx:r,dy:l,distance:o}=e.getDistances(s.position,a),_=c.speed*c.factor,p=e.clamp(t.getEasing(c.easing)(e.identity-o/i)*_,1,c.maxSpeed);n.x=o?r/o*p:_,n.y=o?l/o*p:_,s.position.subFrom(n)}}class i{distance;duration;easing;factor;maxSpeed;speed;constructor(){this.distance=200,this.duration=.4,this.easing=e.EasingType.easeOutQuad,this.factor=1,this.maxSpeed=50,this.speed=1}load(t){e.isNull(t)||(void 0!==t.distance&&(this.distance=t.distance),void 0!==t.duration&&(this.duration=t.duration),void 0!==t.easing&&(this.easing=t.easing),void 0!==t.factor&&(this.factor=t.factor),void 0!==t.maxSpeed&&(this.maxSpeed=t.maxSpeed),void 0!==t.speed&&(this.speed=t.speed))}}const r="attract";class l extends s.ExternalInteractorBase{handleClickMode;_maxDistance;_pluginManager;constructor(t,s){super(s),this._pluginManager=t,this._maxDistance=0,s.attract??={particles:[]},this.handleClickMode=(t,n)=>{const a=this.container.actualOptions,i=a.interactivity?.modes.attract;if(i&&t===r){s.attract??={particles:[]},s.attract.clicking=!0,s.attract.count=0;for(const t of s.attract.particles)this.isEnabled(n,t)&&t.velocity.setTo(t.initialVelocity);s.attract.particles=[],s.attract.finish=!1,setTimeout(()=>{s.destroyed||(s.attract??={particles:[]},s.attract.clicking=!1)},i.duration*e.millisecondsToSeconds)}}}get maxDistance(){return this._maxDistance}clear(){}init(){const t=this.container,s=t.actualOptions.interactivity?.modes.attract;s&&(this._maxDistance=s.distance,t.retina.attractModeDistance=s.distance*t.retina.pixelRatio)}interact(t){const n=this.container.actualOptions,i=t.status===s.mouseMoveEvent,l=n.interactivity?.events;if(!l)return;const{enable:c,mode:o}=l.onHover,{enable:_,mode:p}=l.onClick;i&&c&&e.isInArray(r,o)?function(t,s,n,i){const r=n.mouse.position,l=s.retina.attractModeDistance;!l||l<0||!r||a(t,s,r,l,new e.Circle(r.x,r.y,l),t=>i(t))}(this._pluginManager,this.container,t,s=>this.isEnabled(t,s)):_&&e.isInArray(r,p)&&function(t,s,n,i){s.attract??={particles:[]};const{attract:r}=s;if(r.finish||(r.count??=0,r.count++,r.count===s.particles.count&&(r.finish=!0)),r.clicking){const r=n.mouse.clickPosition,l=s.retina.attractModeDistance;if(!l||l<0||!r)return;a(t,s,r,l,new e.Circle(r.x,r.y,l),t=>i(t))}else!1===r.clicking&&(r.particles=[])}(this._pluginManager,this.container,t,s=>this.isEnabled(t,s))}isEnabled(t,s){const n=this.container.actualOptions,a=t.mouse,i=(s?.interactivity??n.interactivity)?.events;if(!(a.position&&i?.onHover.enable||a.clickPosition&&i?.onClick.enable))return!1;const l=i.onHover.mode,c=i.onClick.mode;return e.isInArray(r,l)||e.isInArray(r,c)}loadModeOptions(t,...s){t.attract??=new i;for(const e of s)t.attract.load(e?.attract)}reset(){}}async function c(t){t.checkVersion("4.0.0-beta.16"),await t.pluginManager.register(t=>{s.ensureInteractivityPluginLoaded(t),t.pluginManager.addInteractor?.("externalAttract",s=>Promise.resolve(new l(t.pluginManager,s)))})}const o=globalThis;o.__tsParticlesInternals=o.__tsParticlesInternals??{},o.loadExternalAttractInteraction=c,t.Attract=i,t.loadExternalAttractInteraction=c}),Object.assign(globalThis.window||globalThis,{loadExternalAttractInteraction:(globalThis.__tsParticlesInternals.interactions.externalAttract||{}).loadExternalAttractInteraction}),delete(globalThis.window||globalThis).tsparticlesInternalExports;
1
+ !function(t){t.__tsParticlesInternals=t.__tsParticlesInternals||{},t.__tsParticlesInternals.bundles=t.__tsParticlesInternals.bundles||{},t.__tsParticlesInternals.effects=t.__tsParticlesInternals.effects||{},t.__tsParticlesInternals.engine=t.__tsParticlesInternals.engine||{},t.__tsParticlesInternals.interactions=t.__tsParticlesInternals.interactions||{},t.__tsParticlesInternals.palettes=t.__tsParticlesInternals.palettes||{},t.__tsParticlesInternals.paths=t.__tsParticlesInternals.paths||{},t.__tsParticlesInternals.plugins=t.__tsParticlesInternals.plugins||{},t.__tsParticlesInternals.plugins=t.__tsParticlesInternals.plugins||{},t.__tsParticlesInternals.plugins.emittersShapes=t.__tsParticlesInternals.plugins.emittersShapes||{},t.__tsParticlesInternals.presets=t.__tsParticlesInternals.presets||{},t.__tsParticlesInternals.shapes=t.__tsParticlesInternals.shapes||{},t.__tsParticlesInternals.updaters=t.__tsParticlesInternals.updaters||{},t.__tsParticlesInternals.utils=t.__tsParticlesInternals.utils||{},t.__tsParticlesInternals.canvas=t.__tsParticlesInternals.canvas||{},t.__tsParticlesInternals.canvas=t.__tsParticlesInternals.canvas||{},t.__tsParticlesInternals.canvas.utils=t.__tsParticlesInternals.canvas.utils||{},t.__tsParticlesInternals.path=t.__tsParticlesInternals.path||{},t.__tsParticlesInternals.path=t.__tsParticlesInternals.path||{},t.__tsParticlesInternals.path.utils=t.__tsParticlesInternals.path.utils||{};var e="undefined"!=typeof Proxy?function(t){return new Proxy(t,{get:function(t,e){return e in t||(t[e]={}),t[e]}})}:function(t){return t};t.__tsParticlesInternals.bundles=e(t.__tsParticlesInternals.bundles),t.__tsParticlesInternals.effects=e(t.__tsParticlesInternals.effects),t.__tsParticlesInternals.interactions=e(t.__tsParticlesInternals.interactions),t.__tsParticlesInternals.palettes=e(t.__tsParticlesInternals.palettes),t.__tsParticlesInternals.paths=e(t.__tsParticlesInternals.paths),t.__tsParticlesInternals.plugins=e(t.__tsParticlesInternals.plugins),t.__tsParticlesInternals.plugins.emittersShapes=e(t.__tsParticlesInternals.plugins.emittersShapes),t.__tsParticlesInternals.presets=e(t.__tsParticlesInternals.presets),t.__tsParticlesInternals.shapes=e(t.__tsParticlesInternals.shapes),t.__tsParticlesInternals.updaters=e(t.__tsParticlesInternals.updaters),t.__tsParticlesInternals.utils=e(t.__tsParticlesInternals.utils),t.__tsParticlesInternals.canvas=e(t.__tsParticlesInternals.canvas),t.__tsParticlesInternals.path=e(t.__tsParticlesInternals.path),t.tsparticlesInternalExports=t.tsparticlesInternalExports||{}}("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:this),function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@tsparticles/plugin-interactivity"),require("@tsparticles/engine")):"function"==typeof define&&define.amd?define(["exports","@tsparticles/plugin-interactivity","@tsparticles/engine"],e):e(((t="undefined"!=typeof globalThis?globalThis:t||self).__tsParticlesInternals=t.__tsParticlesInternals||{},t.__tsParticlesInternals.interactions=t.__tsParticlesInternals.interactions||{},t.__tsParticlesInternals.interactions.externalAttract=t.__tsParticlesInternals.interactions.externalAttract||{}),t.__tsParticlesInternals.plugins.interactivity,t.__tsParticlesInternals.engine)}(this,function(t,e,s){"use strict";const a=s.Vector.origin;function n(t,e,n,i,r,l,c){const o=e.actualOptions.interactivity?.modes.attract;if(!o)return;const _=e.particles.grid.query(r,l);for(const e of _){const{dx:r,dy:l,distance:_}=s.getDistances(e.position,n),d=o.speed*o.factor,p=s.clamp(t.getEasing(o.easing)(s.identity-_/i)*d,1,o.maxSpeed);a.x=_?r/_*p:d,a.y=_?l/_*p:d,c?.(e),e.position.subFrom(a)}}class i{distance;duration;easing;factor;maxSpeed;restore;speed;constructor(){this.distance=200,this.duration=.4,this.easing=s.EasingType.easeOutQuad,this.factor=1,this.maxSpeed=50,this.speed=1,this.restore={enable:!1,delay:0,speed:.08,follow:!0}}load(t){s.isNull(t)||(void 0!==t.distance&&(this.distance=t.distance),void 0!==t.duration&&(this.duration=t.duration),void 0!==t.easing&&(this.easing=t.easing),void 0!==t.factor&&(this.factor=t.factor),void 0!==t.maxSpeed&&(this.maxSpeed=t.maxSpeed),void 0!==t.speed&&(this.speed=t.speed),void 0!==t.restore&&(this.restore.enable=t.restore.enable??this.restore.enable,this.restore.delay=t.restore.delay??this.restore.delay,this.restore.speed=t.restore.speed??this.restore.speed,this.restore.follow=t.restore.follow??this.restore.follow))}}const r="attract";class l extends e.ExternalInteractorBase{handleClickMode;_interactedThisFrame;_maxDistance;_pluginManager;_restoreData;constructor(t,e){super(e),this._pluginManager=t,this._maxDistance=0,this._interactedThisFrame=new Set,this._restoreData=new Map,e.attract??={particles:[]},this.handleClickMode=(t,a)=>{const n=this.container.actualOptions,i=n.interactivity?.modes.attract;if(i&&t===r){e.attract??={particles:[]},e.attract.clicking=!0,e.attract.count=0;for(const t of e.attract.particles)this.isEnabled(a,t)&&t.velocity.setTo(t.initialVelocity);e.attract.particles=[],e.attract.finish=!1,setTimeout(()=>{e.destroyed||(e.attract??={particles:[]},e.attract.clicking=!1)},i.duration*s.millisecondsToSeconds)}}}get maxDistance(){return this._maxDistance}clear(){}init(){const t=this.container,e=t.actualOptions.interactivity?.modes.attract;e&&(this._maxDistance=e.distance,t.retina.attractModeDistance=e.distance*t.retina.pixelRatio)}interact(t){this._interactedThisFrame.clear();const a=this.container.actualOptions,i=t.status===e.mouseMoveEvent,l=a.interactivity?.events;if(!l)return;const{enable:c,mode:o}=l.onHover,{enable:_,mode:d}=l.onClick;i&&c&&s.isInArray(r,o)?function(t,e,a,i,r){const l=a.mouse.position,c=e.retina.attractModeDistance;!c||c<0||!l||n(t,e,l,c,new s.Circle(l.x,l.y,c),t=>i(t),r)}(this._pluginManager,this.container,t,e=>this.isEnabled(t,e),t=>{this._trackInteractedParticle(t)}):_&&s.isInArray(r,d)&&function(t,e,a,i,r){e.attract??={particles:[]};const{attract:l}=e;if(l.finish||(l.count??=0,l.count++,l.count===e.particles.count&&(l.finish=!0)),l.clicking){const l=a.mouse.clickPosition,c=e.retina.attractModeDistance;if(!c||c<0||!l)return;n(t,e,l,c,new s.Circle(l.x,l.y,c),t=>i(t),r)}else!1===l.clicking&&(l.particles=[])}(this._pluginManager,this.container,t,e=>this.isEnabled(t,e),t=>{this._trackInteractedParticle(t)}),this._restoreParticles()}isEnabled(t,e){const a=this.container.actualOptions,n=t.mouse,i=(e?.interactivity??a.interactivity)?.events;if(!(n.position&&i?.onHover.enable||n.clickPosition&&i?.onClick.enable))return!1;const l=i.onHover.mode,c=i.onClick.mode;return s.isInArray(r,l)||s.isInArray(r,c)}loadModeOptions(t,...e){t.attract??=new i;for(const s of e)t.attract.load(s?.attract)}reset(){}_restoreParticles(){const t=this.container.actualOptions.interactivity?.modes.attract?.restore;if(!t?.enable||!this._restoreData.size)return;const e=Date.now(),a=t.delay*s.millisecondsToSeconds,n=Math.max(.001,Math.min(1,t.speed));for(const[s,i]of this._restoreData){if(this._interactedThisFrame.has(s))continue;if(s.destroyed){this._restoreData.delete(s);continue}const r=i.target;if(e-i.lastInteractionTime<a)continue;let l=r.x-s.position.x,c=r.y-s.position.y,o=r.z-s.position.z;if(t.follow&&s.options.move.enable){const{x:t,y:e,z:a}=s.velocity,n=t*t+e*e+a*a;if(n>0){const s=(l*t+c*e+o*a)/n;l-=t*s,c-=e*s,o-=a*s}}s.position.x+=l*n,s.position.y+=c*n,s.position.z+=o*n,Math.abs(l)<=.5&&Math.abs(c)<=.5&&(s.position.x=r.x,s.position.y=r.y,s.position.z=r.z,this._restoreData.delete(s))}}_trackInteractedParticle(t){this._interactedThisFrame.add(t);const e=this.container.actualOptions.interactivity?.modes.attract?.restore;if(!e?.enable)return;const s=Date.now();let a=this._restoreData.get(t);a||(a={target:t.position.copy(),lastInteractionTime:s},this._restoreData.set(t,a)),a.lastInteractionTime=s}}async function c(t){t.checkVersion("4.0.0-beta.17"),await t.pluginManager.register(t=>{e.ensureInteractivityPluginLoaded(t),t.pluginManager.addInteractor?.("externalAttract",e=>Promise.resolve(new l(t.pluginManager,e)))})}const o=globalThis;o.__tsParticlesInternals=o.__tsParticlesInternals??{},o.loadExternalAttractInteraction=c,t.Attract=i,t.loadExternalAttractInteraction=c}),Object.assign(globalThis.window||globalThis,{loadExternalAttractInteraction:(globalThis.__tsParticlesInternals.interactions.externalAttract||{}).loadExternalAttractInteraction}),delete(globalThis.window||globalThis).tsparticlesInternalExports;
@@ -3,8 +3,10 @@ import { ExternalInteractorBase, type IInteractivityData, type IModes, type Inte
3
3
  import { type PluginManager, type RecursivePartial } from "@tsparticles/engine";
4
4
  export declare class Attractor extends ExternalInteractorBase<AttractContainer> {
5
5
  handleClickMode: (mode: string, interactivityData: IInteractivityData) => void;
6
+ private readonly _interactedThisFrame;
6
7
  private _maxDistance;
7
8
  private readonly _pluginManager;
9
+ private readonly _restoreData;
8
10
  constructor(pluginManager: PluginManager, container: AttractContainer);
9
11
  get maxDistance(): number;
10
12
  clear(): void;
@@ -13,4 +15,6 @@ export declare class Attractor extends ExternalInteractorBase<AttractContainer>
13
15
  isEnabled(interactivityData: IInteractivityData, particle?: InteractivityParticle): boolean;
14
16
  loadModeOptions(options: Modes & AttractMode, ...sources: RecursivePartial<(IModes & IAttractMode) | undefined>[]): void;
15
17
  reset(): void;
18
+ private _restoreParticles;
19
+ private _trackInteractedParticle;
16
20
  }
@@ -1,11 +1,12 @@
1
1
  import { EasingType, type EasingTypeAlt, type IOptionLoader, type RecursivePartial } from "@tsparticles/engine";
2
- import type { IAttract } from "../Interfaces/IAttract.js";
2
+ import type { IAttract, IAttractRestore } from "../Interfaces/IAttract.js";
3
3
  export declare class Attract implements IAttract, IOptionLoader<IAttract> {
4
4
  distance: number;
5
5
  duration: number;
6
6
  easing: EasingType | EasingTypeAlt;
7
7
  factor: number;
8
8
  maxSpeed: number;
9
+ restore: IAttractRestore;
9
10
  speed: number;
10
11
  constructor();
11
12
  load(data?: RecursivePartial<IAttract>): void;
@@ -1,9 +1,16 @@
1
1
  import type { EasingType, EasingTypeAlt } from "@tsparticles/engine";
2
+ export interface IAttractRestore {
3
+ delay: number;
4
+ enable: boolean;
5
+ follow: boolean;
6
+ speed: number;
7
+ }
2
8
  export interface IAttract {
3
9
  distance: number;
4
10
  duration: number;
5
11
  easing: EasingType | EasingTypeAlt;
6
12
  factor: number;
7
13
  maxSpeed: number;
14
+ restore: IAttractRestore;
8
15
  speed: number;
9
16
  }
package/types/Types.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface IAttractMode {
8
8
  export interface AttractMode {
9
9
  attract?: Attract;
10
10
  }
11
- interface IContainerAttract {
11
+ export interface IContainerAttract {
12
12
  clicking?: boolean;
13
13
  count?: number;
14
14
  finish?: boolean;
@@ -21,4 +21,3 @@ export type AttractContainer = InteractivityContainer & {
21
21
  attractModeDistance?: number;
22
22
  };
23
23
  };
24
- export {};
package/types/Utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Particle, type PluginManager } from "@tsparticles/engine";
2
2
  import type { AttractContainer } from "./Types.js";
3
3
  import type { IInteractivityData } from "@tsparticles/plugin-interactivity";
4
- export declare function clickAttract(pluginManager: PluginManager, container: AttractContainer, interactivityData: IInteractivityData, enabledCb: (particle: Particle) => boolean): void;
5
- export declare function hoverAttract(pluginManager: PluginManager, container: AttractContainer, interactivityData: IInteractivityData, enabledCb: (particle: Particle) => boolean): void;
4
+ export declare function clickAttract(pluginManager: PluginManager, container: AttractContainer, interactivityData: IInteractivityData, enabledCb: (particle: Particle) => boolean, onAttractParticle?: (particle: Particle) => void): void;
5
+ export declare function hoverAttract(pluginManager: PluginManager, container: AttractContainer, interactivityData: IInteractivityData, enabledCb: (particle: Particle) => boolean, onAttractParticle?: (particle: Particle) => void): void;