UNPKG

16.4 kBJavaScriptView Raw
1import { ParticlesOptions } from "../Options/Classes/Particles/ParticlesOptions";
2import { Shape } from "../Options/Classes/Particles/Shape/Shape";
3import { AnimationStatus, DestroyMode, OutMode, RotateDirection, StartValueType, } from "../Enums";
4import { alterHsl, clamp, colorToRgb, deepExtend, getDistance, getHslFromAnimation, getParticleBaseVelocity, getParticleDirectionAngle, getRangeMax, getRangeMin, getRangeValue, getValue, isInArray, itemFromArray, Plugins, randomInRange, setRangeValue, } from "../Utils";
5import { Vector } from "./Particle/Vector";
6import { Vector3d } from "./Particle/Vector3d";
7const fixOutMode = (data) => {
8 if (isInArray(data.outMode, data.checkModes) || isInArray(data.outMode, data.checkModes)) {
9 if (data.coord > data.maxCoord - data.radius * 2) {
10 data.setCb(-data.radius);
11 }
12 else if (data.coord < data.radius * 2) {
13 data.setCb(data.radius);
14 }
15 }
16};
17/**
18 * The single particle object
19 * @category Core
20 */
21export class Particle {
22 constructor(id, container, position, overrideOptions, group) {
23 var _a, _b, _c, _d, _e, _f, _g, _h, _j;
24 this.id = id;
25 this.container = container;
26 this.group = group;
27 this.fill = true;
28 this.close = true;
29 this.lastPathTime = 0;
30 this.destroyed = false;
31 this.unbreakable = false;
32 this.splitCount = 0;
33 this.misplaced = false;
34 this.retina = {
35 maxDistance: {},
36 };
37 const pxRatio = container.retina.pixelRatio;
38 const mainOptions = container.actualOptions;
39 const particlesOptions = new ParticlesOptions();
40 particlesOptions.load(mainOptions.particles);
41 const shapeType = particlesOptions.shape.type;
42 const reduceDuplicates = particlesOptions.reduceDuplicates;
43 this.shape = shapeType instanceof Array ? itemFromArray(shapeType, this.id, reduceDuplicates) : shapeType;
44 if (overrideOptions === null || overrideOptions === void 0 ? void 0 : overrideOptions.shape) {
45 if (overrideOptions.shape.type) {
46 const overrideShapeType = overrideOptions.shape.type;
47 this.shape =
48 overrideShapeType instanceof Array
49 ? itemFromArray(overrideShapeType, this.id, reduceDuplicates)
50 : overrideShapeType;
51 }
52 const shapeOptions = new Shape();
53 shapeOptions.load(overrideOptions.shape);
54 if (this.shape) {
55 this.shapeData = this.loadShapeData(shapeOptions, reduceDuplicates);
56 }
57 }
58 else {
59 this.shapeData = this.loadShapeData(particlesOptions.shape, reduceDuplicates);
60 }
61 if (overrideOptions !== undefined) {
62 particlesOptions.load(overrideOptions);
63 }
64 if (((_a = this.shapeData) === null || _a === void 0 ? void 0 : _a.particles) !== undefined) {
65 particlesOptions.load((_b = this.shapeData) === null || _b === void 0 ? void 0 : _b.particles);
66 }
67 this.fill = (_d = (_c = this.shapeData) === null || _c === void 0 ? void 0 : _c.fill) !== null && _d !== void 0 ? _d : this.fill;
68 this.close = (_f = (_e = this.shapeData) === null || _e === void 0 ? void 0 : _e.close) !== null && _f !== void 0 ? _f : this.close;
69 this.options = particlesOptions;
70 this.pathDelay = getValue(this.options.move.path.delay) * 1000;
71 const zIndexValue = getRangeValue(this.options.zIndex.value);
72 container.retina.initParticle(this);
73 /* size */
74 const sizeOptions = this.options.size, sizeRange = sizeOptions.value;
75 this.size = {
76 enable: sizeOptions.animation.enable,
77 value: getValue(sizeOptions) * container.retina.pixelRatio,
78 max: getRangeMax(sizeRange) * pxRatio,
79 min: getRangeMin(sizeRange) * pxRatio,
80 loops: 0,
81 maxLoops: sizeOptions.animation.count,
82 };
83 const sizeAnimation = sizeOptions.animation;
84 if (sizeAnimation.enable) {
85 this.size.status = AnimationStatus.increasing;
86 switch (sizeAnimation.startValue) {
87 case StartValueType.min:
88 this.size.value = this.size.min;
89 this.size.status = AnimationStatus.increasing;
90 break;
91 case StartValueType.random:
92 this.size.value = randomInRange(this.size) * pxRatio;
93 this.size.status = Math.random() >= 0.5 ? AnimationStatus.increasing : AnimationStatus.decreasing;
94 break;
95 case StartValueType.max:
96 default:
97 this.size.value = this.size.max;
98 this.size.status = AnimationStatus.decreasing;
99 break;
100 }
101 this.size.velocity =
102 (((_g = this.retina.sizeAnimationSpeed) !== null && _g !== void 0 ? _g : container.retina.sizeAnimationSpeed) / 100) *
103 container.retina.reduceFactor;
104 if (!sizeAnimation.sync) {
105 this.size.velocity *= Math.random();
106 }
107 }
108 this.direction = getParticleDirectionAngle(this.options.move.direction);
109 this.bubble = {
110 inRange: false,
111 };
112 /* animation - velocity for speed */
113 this.initialVelocity = this.calculateVelocity();
114 this.velocity = this.initialVelocity.copy();
115 this.moveDecay = 1 - getRangeValue(this.options.move.decay);
116 /* position */
117 this.position = this.calcPosition(container, position, clamp(zIndexValue, 0, container.zLayers));
118 this.initialPosition = this.position.copy();
119 /* parallax */
120 this.offset = Vector.origin;
121 const particles = container.particles;
122 particles.needsSort = particles.needsSort || particles.lastZIndex < this.position.z;
123 particles.lastZIndex = this.position.z;
124 // Scale z-index factor
125 this.zIndexFactor = this.position.z / container.zLayers;
126 this.sides = 24;
127 let drawer = container.drawers.get(this.shape);
128 if (!drawer) {
129 drawer = Plugins.getShapeDrawer(this.shape);
130 if (drawer) {
131 container.drawers.set(this.shape, drawer);
132 }
133 }
134 if (drawer === null || drawer === void 0 ? void 0 : drawer.loadShape) {
135 drawer === null || drawer === void 0 ? void 0 : drawer.loadShape(this);
136 }
137 const sideCountFunc = drawer === null || drawer === void 0 ? void 0 : drawer.getSidesCount;
138 if (sideCountFunc) {
139 this.sides = sideCountFunc(this);
140 }
141 this.life = this.loadLife();
142 this.spawning = this.life.delay > 0;
143 if (this.options.move.spin.enable) {
144 const spinPos = (_h = this.options.move.spin.position) !== null && _h !== void 0 ? _h : { x: 50, y: 50 };
145 const spinCenter = {
146 x: (spinPos.x / 100) * container.canvas.size.width,
147 y: (spinPos.y / 100) * container.canvas.size.height,
148 };
149 const pos = this.getPosition();
150 const distance = getDistance(pos, spinCenter);
151 this.spin = {
152 center: spinCenter,
153 direction: this.velocity.x >= 0 ? RotateDirection.clockwise : RotateDirection.counterClockwise,
154 angle: this.velocity.angle,
155 radius: distance,
156 acceleration: (_j = this.retina.spinAcceleration) !== null && _j !== void 0 ? _j : getRangeValue(this.options.move.spin.acceleration),
157 };
158 }
159 this.shadowColor = colorToRgb(this.options.shadow.color);
160 for (const updater of container.particles.updaters) {
161 if (updater.init) {
162 updater.init(this);
163 }
164 }
165 if (drawer && drawer.particleInit) {
166 drawer.particleInit(container, this);
167 }
168 for (const [, plugin] of container.plugins) {
169 if (plugin.particleCreated) {
170 plugin.particleCreated(this);
171 }
172 }
173 }
174 isVisible() {
175 return !this.destroyed && !this.spawning && this.isInsideCanvas();
176 }
177 isInsideCanvas() {
178 const radius = this.getRadius();
179 const canvasSize = this.container.canvas.size;
180 return (this.position.x >= -radius &&
181 this.position.y >= -radius &&
182 this.position.y <= canvasSize.height + radius &&
183 this.position.x <= canvasSize.width + radius);
184 }
185 draw(delta) {
186 const container = this.container;
187 for (const [, plugin] of container.plugins) {
188 container.canvas.drawParticlePlugin(plugin, this, delta);
189 }
190 container.canvas.drawParticle(this, delta);
191 }
192 getPosition() {
193 return {
194 x: this.position.x + this.offset.x,
195 y: this.position.y + this.offset.y,
196 z: this.position.z,
197 };
198 }
199 getRadius() {
200 var _a;
201 return (_a = this.bubble.radius) !== null && _a !== void 0 ? _a : this.size.value;
202 }
203 getMass() {
204 return (this.getRadius() ** 2 * Math.PI) / 2;
205 }
206 getFillColor() {
207 var _a, _b, _c;
208 const color = (_a = this.bubble.color) !== null && _a !== void 0 ? _a : getHslFromAnimation(this.color);
209 if (color && this.roll && (this.backColor || this.roll.alter)) {
210 const rolled = Math.floor(((_c = (_b = this.roll) === null || _b === void 0 ? void 0 : _b.angle) !== null && _c !== void 0 ? _c : 0) / (Math.PI / 2)) % 2;
211 if (rolled) {
212 if (this.backColor) {
213 return this.backColor;
214 }
215 if (this.roll.alter) {
216 return alterHsl(color, this.roll.alter.type, this.roll.alter.value);
217 }
218 }
219 }
220 return color;
221 }
222 getStrokeColor() {
223 var _a, _b;
224 return (_b = (_a = this.bubble.color) !== null && _a !== void 0 ? _a : getHslFromAnimation(this.strokeColor)) !== null && _b !== void 0 ? _b : this.getFillColor();
225 }
226 destroy(override) {
227 this.destroyed = true;
228 this.bubble.inRange = false;
229 if (this.unbreakable) {
230 return;
231 }
232 this.destroyed = true;
233 this.bubble.inRange = false;
234 for (const [, plugin] of this.container.plugins) {
235 if (plugin.particleDestroyed) {
236 plugin.particleDestroyed(this, override);
237 }
238 }
239 if (override) {
240 return;
241 }
242 const destroyOptions = this.options.destroy;
243 if (destroyOptions.mode === DestroyMode.split) {
244 this.split();
245 }
246 }
247 /**
248 * This method is used when the particle has lost a life and needs some value resets
249 */
250 reset() {
251 if (this.opacity) {
252 this.opacity.loops = 0;
253 }
254 this.size.loops = 0;
255 }
256 split() {
257 const splitOptions = this.options.destroy.split;
258 if (splitOptions.count >= 0 && this.splitCount++ > splitOptions.count) {
259 return;
260 }
261 const rate = getRangeValue(splitOptions.rate.value);
262 for (let i = 0; i < rate; i++) {
263 this.container.particles.addSplitParticle(this);
264 }
265 }
266 calcPosition(container, position, zIndex, tryCount = 0) {
267 var _a, _b, _c, _d, _e, _f;
268 for (const [, plugin] of container.plugins) {
269 const pluginPos = plugin.particlePosition !== undefined ? plugin.particlePosition(position, this) : undefined;
270 if (pluginPos !== undefined) {
271 return Vector3d.create(pluginPos.x, pluginPos.y, zIndex);
272 }
273 }
274 const canvasSize = container.canvas.size;
275 const pos = Vector3d.create((_a = position === null || position === void 0 ? void 0 : position.x) !== null && _a !== void 0 ? _a : Math.random() * canvasSize.width, (_b = position === null || position === void 0 ? void 0 : position.y) !== null && _b !== void 0 ? _b : Math.random() * canvasSize.height, zIndex);
276 const radius = this.getRadius();
277 /* check position - into the canvas */
278 const outModes = this.options.move.outModes, fixHorizontal = (outMode) => {
279 fixOutMode({
280 outMode,
281 checkModes: [OutMode.bounce, OutMode.bounceHorizontal],
282 coord: pos.x,
283 maxCoord: container.canvas.size.width,
284 setCb: (value) => (pos.x += value),
285 radius,
286 });
287 }, fixVertical = (outMode) => {
288 fixOutMode({
289 outMode,
290 checkModes: [OutMode.bounce, OutMode.bounceVertical],
291 coord: pos.y,
292 maxCoord: container.canvas.size.height,
293 setCb: (value) => (pos.y += value),
294 radius,
295 });
296 };
297 fixHorizontal((_c = outModes.left) !== null && _c !== void 0 ? _c : outModes.default);
298 fixHorizontal((_d = outModes.right) !== null && _d !== void 0 ? _d : outModes.default);
299 fixVertical((_e = outModes.top) !== null && _e !== void 0 ? _e : outModes.default);
300 fixVertical((_f = outModes.bottom) !== null && _f !== void 0 ? _f : outModes.default);
301 if (this.checkOverlap(pos, tryCount)) {
302 return this.calcPosition(container, undefined, zIndex, tryCount + 1);
303 }
304 return pos;
305 }
306 checkOverlap(pos, tryCount = 0) {
307 const collisionsOptions = this.options.collisions;
308 const radius = this.getRadius();
309 if (!collisionsOptions.enable) {
310 return false;
311 }
312 const overlapOptions = collisionsOptions.overlap;
313 if (overlapOptions.enable) {
314 return false;
315 }
316 const retries = overlapOptions.retries;
317 if (retries >= 0 && tryCount > retries) {
318 throw new Error("Particle is overlapping and can't be placed");
319 }
320 let overlaps = false;
321 for (const particle of this.container.particles.array) {
322 if (getDistance(pos, particle.position) < radius + particle.getRadius()) {
323 overlaps = true;
324 break;
325 }
326 }
327 return overlaps;
328 }
329 calculateVelocity() {
330 const baseVelocity = getParticleBaseVelocity(this.direction);
331 const res = baseVelocity.copy();
332 const moveOptions = this.options.move;
333 const rad = (Math.PI / 180) * moveOptions.angle.value;
334 const radOffset = (Math.PI / 180) * moveOptions.angle.offset;
335 const range = {
336 left: radOffset - rad / 2,
337 right: radOffset + rad / 2,
338 };
339 if (!moveOptions.straight) {
340 res.angle += randomInRange(setRangeValue(range.left, range.right));
341 }
342 if (moveOptions.random && typeof moveOptions.speed === "number") {
343 res.length *= Math.random();
344 }
345 return res;
346 }
347 loadShapeData(shapeOptions, reduceDuplicates) {
348 const shapeData = shapeOptions.options[this.shape];
349 if (shapeData) {
350 return deepExtend({}, shapeData instanceof Array ? itemFromArray(shapeData, this.id, reduceDuplicates) : shapeData);
351 }
352 }
353 loadLife() {
354 const container = this.container;
355 const particlesOptions = this.options;
356 const lifeOptions = particlesOptions.life;
357 const life = {
358 delay: container.retina.reduceFactor
359 ? ((getRangeValue(lifeOptions.delay.value) * (lifeOptions.delay.sync ? 1 : Math.random())) /
360 container.retina.reduceFactor) *
361 1000
362 : 0,
363 delayTime: 0,
364 duration: container.retina.reduceFactor
365 ? ((getRangeValue(lifeOptions.duration.value) * (lifeOptions.duration.sync ? 1 : Math.random())) /
366 container.retina.reduceFactor) *
367 1000
368 : 0,
369 time: 0,
370 count: particlesOptions.life.count,
371 };
372 if (life.duration <= 0) {
373 life.duration = -1;
374 }
375 if (life.count <= 0) {
376 life.count = -1;
377 }
378 return life;
379 }
380}