particle-canvas-pro 1.4.0 → 1.4.2
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.
- package/dist/bundle.cjs.js +1 -660
- package/dist/bundle.esm.js +1 -655
- package/dist/bundle.js +1 -665
- package/package.json +1 -1
package/dist/bundle.cjs.js
CHANGED
|
@@ -1,660 +1 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
|
-
class ParticleCanvas {
|
|
6
|
-
/**
|
|
7
|
-
* 初始化配置
|
|
8
|
-
* @param {ParticleCanvasOptions} options - 配置对象
|
|
9
|
-
*/
|
|
10
|
-
constructor(options = {}) {
|
|
11
|
-
// 配置参数的最大值常量
|
|
12
|
-
this.MAX_PARTICLE_NUMBER = 800;
|
|
13
|
-
this.MAX_PARTICLE_SPEED = 3;
|
|
14
|
-
this.MAX_PARTICLE_SIZE = 10;
|
|
15
|
-
this.MAX_LINK_DISTANCE = 300;
|
|
16
|
-
this.MAX_LINE_WIDTH = 5;
|
|
17
|
-
this.MAX_LINE_OPACITY = 1;
|
|
18
|
-
this.MAX_TRAIL_VALUE = 1;
|
|
19
|
-
this.MAX_PARTICLE_BLUR = 5;
|
|
20
|
-
this.MAX_TRAIL_INTENSITY = 5;
|
|
21
|
-
// 验证并限制配置参数
|
|
22
|
-
const validatedOptions = this.validateAndLimitOptions(options);
|
|
23
|
-
// 合并默认配置和用户配置
|
|
24
|
-
this.config = {
|
|
25
|
-
// 画布容器:可以是DOM元素或选择器字符串,默认为document.body
|
|
26
|
-
canvasContainer: validatedOptions.canvasContainer || document.body,
|
|
27
|
-
// 是否作为页面背景:true-背景模式,false-内嵌模式,默认为true
|
|
28
|
-
isBackground: validatedOptions.isBackground !== undefined ? validatedOptions.isBackground : true,
|
|
29
|
-
// 画布背景颜色:支持所有CSS颜色格式,默认为深蓝色半透明
|
|
30
|
-
canvasBackgroundColor: validatedOptions.canvasBackgroundColor || 'rgb(10, 10, 25)',
|
|
31
|
-
// 粒子数量:控制画布中粒子的总数,默认为150
|
|
32
|
-
particleNumber: validatedOptions.particleNumber || 150,
|
|
33
|
-
// 粒子速度:控制粒子的运动速度,值越大移动越快,默认为1
|
|
34
|
-
particleSpeed: validatedOptions.particleSpeed || 1,
|
|
35
|
-
// 粒子大小:控制每个粒子的半径大小,默认为3像素
|
|
36
|
-
particleSize: validatedOptions.particleSize || 5,
|
|
37
|
-
// 粒子颜色:支持单色字符串或多色数组,默认为rgba(156, 74, 255, 0.6)
|
|
38
|
-
particleColor: validatedOptions.particleColor || 'rgba(156, 74, 255, 0.6)',
|
|
39
|
-
// 粒子阴影模糊效果:控制粒子阴影的模糊程度,值越大阴影越模糊,默认为2
|
|
40
|
-
particleBlur: validatedOptions.particleBlur !== undefined ? validatedOptions.particleBlur : 2,
|
|
41
|
-
// 连线宽度:控制粒子间连线的粗细,默认为1像素
|
|
42
|
-
lineWidth: validatedOptions.lineWidth || 1,
|
|
43
|
-
// 连线颜色:粒子间连线的颜色,默认为白色#ffffff
|
|
44
|
-
lineColor: validatedOptions.lineColor || '#ffffff',
|
|
45
|
-
// 连线透明度:控制连线的透明程度,0-1之间,默认为0.3
|
|
46
|
-
lineOpacity: validatedOptions.lineOpacity || 0.3,
|
|
47
|
-
// 连接距离:粒子间产生连线的最大距离,默认为120像素
|
|
48
|
-
linkDistance: validatedOptions.linkDistance || 120,
|
|
49
|
-
// 是否显示粒子连线:控制是否绘制粒子间的连线,默认为true
|
|
50
|
-
showLine: validatedOptions.showLine !== undefined ? validatedOptions.showLine : true,
|
|
51
|
-
// 画布尺寸:当isBackground为false时生效,格式为[宽度, 高度],默认为[800, 600]
|
|
52
|
-
canvasSize: validatedOptions.canvasSize || [800, 600],
|
|
53
|
-
// 是否显示粒子轨迹效果,默认为true
|
|
54
|
-
showTrail: validatedOptions.showTrail !== undefined ? validatedOptions.showTrail : true,
|
|
55
|
-
// 粒子轨迹效果强度,默认为2
|
|
56
|
-
trailIntensity: validatedOptions.trailIntensity || 2
|
|
57
|
-
};
|
|
58
|
-
// 粒子数组
|
|
59
|
-
this.particles = [];
|
|
60
|
-
// 动画相关变量
|
|
61
|
-
this.animationId = null; // 动画帧ID
|
|
62
|
-
this.isRunning = false; // 动画运行状态
|
|
63
|
-
this.lastTime = 0; // 上一帧时间
|
|
64
|
-
this.fps = 60; // 当前帧率
|
|
65
|
-
this.fpsInterval = 1000 / 60; // 帧间隔(毫秒)
|
|
66
|
-
this.then = Date.now(); // 上一帧时间戳
|
|
67
|
-
this.frameCount = 0; // 帧计数器
|
|
68
|
-
this.lastFpsUpdate = Date.now(); // 上次FPS更新时间
|
|
69
|
-
this.resizeTimer = null; // 防抖计时器
|
|
70
|
-
this.wasRunningBeforeHide = false; // 页面隐藏前的运行状态
|
|
71
|
-
this.lastFrameTime = performance.now(); // 初始化时间戳
|
|
72
|
-
// 初始化粒子系统
|
|
73
|
-
this.init();
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 验证并限制配置选项,确保参数在合理范围内
|
|
77
|
-
* @param options 用户提供的配置选项
|
|
78
|
-
* @returns 验证后的配置选项
|
|
79
|
-
*/
|
|
80
|
-
validateAndLimitOptions(options) {
|
|
81
|
-
const validatedOptions = Object.assign({}, options);
|
|
82
|
-
// 验证粒子数量
|
|
83
|
-
if (options.particleNumber !== undefined) {
|
|
84
|
-
if (options.particleNumber > this.MAX_PARTICLE_NUMBER) {
|
|
85
|
-
console.warn(`Warning: particleNumber (${options.particleNumber}) exceeds maximum allowed value (${this.MAX_PARTICLE_NUMBER}). Using maximum value instead.`);
|
|
86
|
-
validatedOptions.particleNumber = this.MAX_PARTICLE_NUMBER;
|
|
87
|
-
}
|
|
88
|
-
else if (options.particleNumber < 1) {
|
|
89
|
-
console.warn(`Warning: particleNumber (${options.particleNumber}) is below minimum value (1). Using default value instead.`);
|
|
90
|
-
validatedOptions.particleNumber = 150;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// 验证粒子速度
|
|
94
|
-
if (options.particleSpeed !== undefined) {
|
|
95
|
-
if (options.particleSpeed > this.MAX_PARTICLE_SPEED) {
|
|
96
|
-
console.warn(`Warning: particleSpeed (${options.particleSpeed}) exceeds maximum allowed value (${this.MAX_PARTICLE_SPEED}). Using maximum value instead.`);
|
|
97
|
-
validatedOptions.particleSpeed = this.MAX_PARTICLE_SPEED;
|
|
98
|
-
}
|
|
99
|
-
else if (options.particleSpeed < 0) {
|
|
100
|
-
console.warn(`Warning: particleSpeed (${options.particleSpeed}) is below minimum value (0). Using default value instead.`);
|
|
101
|
-
validatedOptions.particleSpeed = 1;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// 验证粒子大小
|
|
105
|
-
if (options.particleSize !== undefined) {
|
|
106
|
-
if (options.particleSize > this.MAX_PARTICLE_SIZE) {
|
|
107
|
-
console.warn(`Warning: particleSize (${options.particleSize}) exceeds maximum allowed value (${this.MAX_PARTICLE_SIZE}). Using maximum value instead.`);
|
|
108
|
-
validatedOptions.particleSize = this.MAX_PARTICLE_SIZE;
|
|
109
|
-
}
|
|
110
|
-
else if (options.particleSize < 1) {
|
|
111
|
-
console.warn(`Warning: particleSize (${options.particleSize}) is below minimum value (1). Using default value instead.`);
|
|
112
|
-
validatedOptions.particleSize = 3;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// 验证粒子阴影模糊效果
|
|
116
|
-
if (options.particleBlur !== undefined) {
|
|
117
|
-
if (options.particleBlur > this.MAX_PARTICLE_BLUR) {
|
|
118
|
-
console.warn(`Warning: particleBlur (${options.particleBlur}) exceeds maximum allowed value (${this.MAX_PARTICLE_BLUR}). Using maximum value instead.`);
|
|
119
|
-
validatedOptions.particleBlur = this.MAX_PARTICLE_BLUR;
|
|
120
|
-
}
|
|
121
|
-
else if (options.particleBlur < 0) {
|
|
122
|
-
console.warn(`Warning: particleBlur (${options.particleBlur}) is below minimum value (0). Using default value instead.`);
|
|
123
|
-
validatedOptions.particleBlur = 2;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// 验证连接距离
|
|
127
|
-
if (options.linkDistance !== undefined) {
|
|
128
|
-
if (options.linkDistance > this.MAX_LINK_DISTANCE) {
|
|
129
|
-
console.warn(`Warning: linkDistance (${options.linkDistance}) exceeds maximum allowed value (${this.MAX_LINK_DISTANCE}). Using maximum value instead.`);
|
|
130
|
-
validatedOptions.linkDistance = this.MAX_LINK_DISTANCE;
|
|
131
|
-
}
|
|
132
|
-
else if (options.linkDistance < 10) {
|
|
133
|
-
console.warn(`Warning: linkDistance (${options.linkDistance}) is below minimum value (10). Using default value instead.`);
|
|
134
|
-
validatedOptions.linkDistance = 120;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// 验证连线宽度
|
|
138
|
-
if (options.lineWidth !== undefined) {
|
|
139
|
-
if (options.lineWidth > this.MAX_LINE_WIDTH) {
|
|
140
|
-
console.warn(`Warning: lineWidth (${options.lineWidth}) exceeds maximum allowed value (${this.MAX_LINE_WIDTH}). Using maximum value instead.`);
|
|
141
|
-
validatedOptions.lineWidth = this.MAX_LINE_WIDTH;
|
|
142
|
-
}
|
|
143
|
-
else if (options.lineWidth < 0) {
|
|
144
|
-
console.warn(`Warning: lineWidth (${options.lineWidth}) is below minimum value (0). Using default value instead.`);
|
|
145
|
-
validatedOptions.lineWidth = 1;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// 验证连线透明度
|
|
149
|
-
if (options.lineOpacity !== undefined) {
|
|
150
|
-
if (options.lineOpacity > this.MAX_LINE_OPACITY) {
|
|
151
|
-
console.warn(`Warning: lineOpacity (${options.lineOpacity}) exceeds maximum allowed value (${this.MAX_LINE_OPACITY}). Using maximum value instead.`);
|
|
152
|
-
validatedOptions.lineOpacity = this.MAX_LINE_OPACITY;
|
|
153
|
-
}
|
|
154
|
-
else if (options.lineOpacity < 0) {
|
|
155
|
-
console.warn(`Warning: lineOpacity (${options.lineOpacity}) is below minimum value (0). Using default value instead.`);
|
|
156
|
-
validatedOptions.lineOpacity = 0.3;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// 验证粒子轨迹效果强度
|
|
160
|
-
if (options.trailIntensity !== undefined) {
|
|
161
|
-
if (options.trailIntensity > this.MAX_TRAIL_INTENSITY) {
|
|
162
|
-
console.warn(`Warning: trailIntensity (${options.trailIntensity}) exceeds maximum allowed value (${this.MAX_TRAIL_INTENSITY}). Using maximum value instead.`);
|
|
163
|
-
validatedOptions.trailIntensity = this.MAX_TRAIL_INTENSITY;
|
|
164
|
-
}
|
|
165
|
-
else if (options.trailIntensity < 1) {
|
|
166
|
-
console.warn(`Warning: trailIntensity (${options.trailIntensity}) is below minimum value (1). Using default value instead.`);
|
|
167
|
-
validatedOptions.trailIntensity = 2;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return validatedOptions;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* 初始化粒子系统
|
|
174
|
-
* 创建画布、设置容器、创建粒子、绑定事件
|
|
175
|
-
*/
|
|
176
|
-
init() {
|
|
177
|
-
// 创建画布元素
|
|
178
|
-
this.canvas = document.createElement('canvas');
|
|
179
|
-
// 获取2D绘图上下文
|
|
180
|
-
const context = this.canvas.getContext('2d');
|
|
181
|
-
if (!context) {
|
|
182
|
-
throw new Error('Could not get 2D context from canvas');
|
|
183
|
-
}
|
|
184
|
-
this.ctx = context;
|
|
185
|
-
// 设置画布容器
|
|
186
|
-
if (typeof this.config.canvasContainer === 'string') {
|
|
187
|
-
const container = document.querySelector(this.config.canvasContainer);
|
|
188
|
-
if (!container) {
|
|
189
|
-
throw new Error(`Canvas container not found: ${this.config.canvasContainer}`);
|
|
190
|
-
}
|
|
191
|
-
this.container = container;
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
this.container = this.config.canvasContainer;
|
|
195
|
-
}
|
|
196
|
-
// 检查容器是否存在
|
|
197
|
-
if (!this.container) {
|
|
198
|
-
console.error('Canvas container not found');
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
// 将画布添加到容器中
|
|
202
|
-
this.container.appendChild(this.canvas);
|
|
203
|
-
// 设置画布样式和尺寸
|
|
204
|
-
this.setupCanvas();
|
|
205
|
-
// 创建粒子
|
|
206
|
-
this.createParticles();
|
|
207
|
-
// 绑定事件监听器
|
|
208
|
-
this.bindEvents();
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* 设置画布样式和尺寸
|
|
212
|
-
* 根据配置决定是背景模式还是内嵌模式
|
|
213
|
-
*/
|
|
214
|
-
setupCanvas() {
|
|
215
|
-
if (this.config.isBackground) {
|
|
216
|
-
// 背景模式:画布覆盖整个视口,作为页面背景
|
|
217
|
-
this.canvas.style.position = 'fixed';
|
|
218
|
-
this.canvas.style.top = '0';
|
|
219
|
-
this.canvas.style.left = '0';
|
|
220
|
-
this.canvas.style.width = '100%';
|
|
221
|
-
this.canvas.style.height = '100%';
|
|
222
|
-
this.canvas.style.zIndex = '-1'; // 置于底层
|
|
223
|
-
// 设置画布实际像素尺寸
|
|
224
|
-
this.canvas.width = window.innerWidth;
|
|
225
|
-
this.canvas.height = window.innerHeight;
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
// 内嵌模式:画布作为页面中的普通元素
|
|
229
|
-
this.canvas.style.display = 'block';
|
|
230
|
-
const [width, height] = this.config.canvasSize;
|
|
231
|
-
// 设置画布尺寸
|
|
232
|
-
this.canvas.width = width;
|
|
233
|
-
this.canvas.height = height;
|
|
234
|
-
this.canvas.style.width = width + 'px';
|
|
235
|
-
this.canvas.style.height = height + 'px';
|
|
236
|
-
}
|
|
237
|
-
// 禁用画布的鼠标事件,防止干扰页面交互
|
|
238
|
-
this.canvas.style.pointerEvents = 'none';
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* 创建粒子数组
|
|
242
|
-
* 根据配置生成指定数量的粒子
|
|
243
|
-
*/
|
|
244
|
-
createParticles() {
|
|
245
|
-
this.particles = [];
|
|
246
|
-
// 判断是否使用多色模式
|
|
247
|
-
const isMultiColor = Array.isArray(this.config.particleColor);
|
|
248
|
-
// 创建指定数量的粒子
|
|
249
|
-
for (let i = 0; i < this.config.particleNumber; i++) {
|
|
250
|
-
// 根据颜色模式选择粒子颜色
|
|
251
|
-
let particleColor;
|
|
252
|
-
if (isMultiColor) {
|
|
253
|
-
// 多色模式:随机选择颜色
|
|
254
|
-
const colors = this.config.particleColor;
|
|
255
|
-
particleColor = colors[Math.floor(Math.random() * colors.length)];
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
// 单色模式:使用固定颜色
|
|
259
|
-
particleColor = this.config.particleColor;
|
|
260
|
-
}
|
|
261
|
-
// 创建粒子对象
|
|
262
|
-
this.particles.push({
|
|
263
|
-
x: Math.random() * this.canvas.width, // 随机X坐标
|
|
264
|
-
y: Math.random() * this.canvas.height, // 随机Y坐标
|
|
265
|
-
vx: (Math.random() - 0.5) * 2 * this.config.particleSpeed, // X轴速度(-speed到+speed)
|
|
266
|
-
vy: (Math.random() - 0.5) * 2 * this.config.particleSpeed, // Y轴速度
|
|
267
|
-
size: this.config.particleSize, // 粒子大小
|
|
268
|
-
color: particleColor, // 粒子颜色
|
|
269
|
-
originalColor: particleColor, // 原始颜色(备份)
|
|
270
|
-
pulse: 0, // 脉动值(用于脉动动画)
|
|
271
|
-
pulseSpeed: Math.random() * 0.05 + 0.02, // 脉动速度(随机值)
|
|
272
|
-
// 粒子历史位置数组,用于轨迹效果
|
|
273
|
-
trailPositions: []
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* 更新粒子状态
|
|
279
|
-
* 计算每个粒子的新位置和状态
|
|
280
|
-
* @param deltaTime 时间增量(毫秒)
|
|
281
|
-
*/
|
|
282
|
-
updateParticles(deltaTime) {
|
|
283
|
-
// 时间因子:将deltaTime转换为相对于60fps的比例因子
|
|
284
|
-
// 60fps时,deltaTime约为16.67ms,时间因子为1
|
|
285
|
-
const timeFactor = deltaTime / 16.67;
|
|
286
|
-
for (const particle of this.particles) {
|
|
287
|
-
// 脉动效果:更新脉动值(基于时间因子)
|
|
288
|
-
particle.pulse += particle.pulseSpeed * timeFactor;
|
|
289
|
-
// 脉动值循环(0-2π)
|
|
290
|
-
if (particle.pulse > Math.PI * 2) {
|
|
291
|
-
particle.pulse = 0;
|
|
292
|
-
}
|
|
293
|
-
// 保存当前位置到历史位置
|
|
294
|
-
if (this.config.showTrail) {
|
|
295
|
-
// 添加当前位置到历史
|
|
296
|
-
particle.trailPositions.unshift({ x: particle.x, y: particle.y });
|
|
297
|
-
// 限制历史位置数量
|
|
298
|
-
if (particle.trailPositions.length > this.config.trailIntensity * 10) {
|
|
299
|
-
particle.trailPositions.length = this.config.trailIntensity * 10;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// 更新粒子位置:当前位置 + 速度 * 时间因子
|
|
303
|
-
particle.x += particle.vx * timeFactor;
|
|
304
|
-
particle.y += particle.vy * timeFactor;
|
|
305
|
-
// 边界检测:碰到边界时反弹
|
|
306
|
-
if (particle.x < 0 || particle.x > this.canvas.width)
|
|
307
|
-
particle.vx *= -1;
|
|
308
|
-
if (particle.y < 0 || particle.y > this.canvas.height)
|
|
309
|
-
particle.vy *= -1;
|
|
310
|
-
// 确保粒子在画布范围内
|
|
311
|
-
particle.x = Math.max(0, Math.min(this.canvas.width, particle.x));
|
|
312
|
-
particle.y = Math.max(0, Math.min(this.canvas.height, particle.y));
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* 绘制粒子轨迹效果
|
|
317
|
-
* 在粒子后面绘制自然的光影拖尾效果
|
|
318
|
-
*/
|
|
319
|
-
drawTrailEffect() {
|
|
320
|
-
if (!this.config.showTrail)
|
|
321
|
-
return;
|
|
322
|
-
// 保存上下文状态
|
|
323
|
-
this.ctx.save();
|
|
324
|
-
for (const particle of this.particles) {
|
|
325
|
-
// 只有有历史位置时才绘制
|
|
326
|
-
if (particle.trailPositions.length < 2)
|
|
327
|
-
continue;
|
|
328
|
-
// 创建粒子光影路径
|
|
329
|
-
this.ctx.beginPath();
|
|
330
|
-
// 移动到第一个点(最新位置)
|
|
331
|
-
this.ctx.moveTo(particle.trailPositions[0].x, particle.trailPositions[0].y);
|
|
332
|
-
// 添加路径点
|
|
333
|
-
for (let i = 1; i < particle.trailPositions.length; i++) {
|
|
334
|
-
this.ctx.lineTo(particle.trailPositions[i].x, particle.trailPositions[i].y);
|
|
335
|
-
}
|
|
336
|
-
// 设置线条样式 - 宽度与粒子大小匹配
|
|
337
|
-
const lineWidth = particle.size * 1.2; // 比粒子稍大一点,形成光晕效果
|
|
338
|
-
this.ctx.lineWidth = lineWidth;
|
|
339
|
-
this.ctx.lineCap = 'round';
|
|
340
|
-
this.ctx.lineJoin = 'round';
|
|
341
|
-
// 创建线性渐变(从当前点到历史点)
|
|
342
|
-
const startPos = particle.trailPositions[0];
|
|
343
|
-
const endPos = particle.trailPositions[particle.trailPositions.length - 1];
|
|
344
|
-
const gradient = this.ctx.createLinearGradient(startPos.x, startPos.y, endPos.x, endPos.y);
|
|
345
|
-
// 计算颜色
|
|
346
|
-
const color = this.parseColor(particle.color);
|
|
347
|
-
// 渐变从完全不透明到完全透明
|
|
348
|
-
const baseAlpha = 0.35; // 降低基础透明度,更加自然
|
|
349
|
-
gradient.addColorStop(0, `rgba(${color.r}, ${color.g}, ${color.b}, ${baseAlpha})`);
|
|
350
|
-
gradient.addColorStop(0.3, `rgba(${color.r}, ${color.g}, ${color.b}, ${baseAlpha * 0.6})`);
|
|
351
|
-
gradient.addColorStop(0.7, `rgba(${color.r}, ${color.g}, ${color.b}, ${baseAlpha * 0.2})`);
|
|
352
|
-
gradient.addColorStop(1, `rgba(${color.r}, ${color.g}, ${color.b}, 0)`);
|
|
353
|
-
this.ctx.strokeStyle = gradient;
|
|
354
|
-
this.ctx.stroke();
|
|
355
|
-
}
|
|
356
|
-
// 恢复上下文状态
|
|
357
|
-
this.ctx.restore();
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* 解析颜色字符串为RGB对象
|
|
361
|
-
* @param colorStr 颜色字符串
|
|
362
|
-
* @returns RGB对象
|
|
363
|
-
*/
|
|
364
|
-
parseColor(colorStr) {
|
|
365
|
-
// 如果是rgba格式
|
|
366
|
-
if (colorStr.startsWith('rgba')) {
|
|
367
|
-
const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);
|
|
368
|
-
if (rgbaMatch) {
|
|
369
|
-
return {
|
|
370
|
-
r: parseInt(rgbaMatch[1]),
|
|
371
|
-
g: parseInt(rgbaMatch[2]),
|
|
372
|
-
b: parseInt(rgbaMatch[3]),
|
|
373
|
-
a: parseFloat(rgbaMatch[4])
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
// 如果是rgb格式
|
|
378
|
-
else if (colorStr.startsWith('rgb')) {
|
|
379
|
-
const rgbMatch = colorStr.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
|
380
|
-
if (rgbMatch) {
|
|
381
|
-
return {
|
|
382
|
-
r: parseInt(rgbMatch[1]),
|
|
383
|
-
g: parseInt(rgbMatch[2]),
|
|
384
|
-
b: parseInt(rgbMatch[3])
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// 如果是十六进制格式
|
|
389
|
-
else if (colorStr.startsWith('#')) {
|
|
390
|
-
// 移除#号
|
|
391
|
-
let hex = colorStr.replace('#', '');
|
|
392
|
-
// 解析RGB值
|
|
393
|
-
let r, g, b;
|
|
394
|
-
if (hex.length === 3) {
|
|
395
|
-
r = parseInt(hex[0] + hex[0], 16);
|
|
396
|
-
g = parseInt(hex[1] + hex[1], 16);
|
|
397
|
-
b = parseInt(hex[2] + hex[2], 16);
|
|
398
|
-
}
|
|
399
|
-
else if (hex.length === 6) {
|
|
400
|
-
r = parseInt(hex.substring(0, 2), 16);
|
|
401
|
-
g = parseInt(hex.substring(2, 4), 16);
|
|
402
|
-
b = parseInt(hex.substring(4, 6), 16);
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
// 默认返回白色
|
|
406
|
-
return { r: 255, g: 255, b: 255 };
|
|
407
|
-
}
|
|
408
|
-
return { r, g, b };
|
|
409
|
-
}
|
|
410
|
-
// 默认返回白色
|
|
411
|
-
return { r: 255, g: 255, b: 255 };
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* 绘制粒子和连线
|
|
415
|
-
* 在画布上渲染粒子系统
|
|
416
|
-
*/
|
|
417
|
-
drawParticles() {
|
|
418
|
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
419
|
-
let backgroundColor = this.config.canvasBackgroundColor;
|
|
420
|
-
if (backgroundColor.startsWith('rgba')) {
|
|
421
|
-
const rgbaMatch = backgroundColor.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);
|
|
422
|
-
if (rgbaMatch) {
|
|
423
|
-
const r = rgbaMatch[1];
|
|
424
|
-
const g = rgbaMatch[2];
|
|
425
|
-
const b = rgbaMatch[3];
|
|
426
|
-
const a = parseFloat(rgbaMatch[4]);
|
|
427
|
-
if (a < 1) {
|
|
428
|
-
backgroundColor = `rgb(${r}, ${g}, ${b})`;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// 绘制背景
|
|
433
|
-
this.ctx.fillStyle = backgroundColor;
|
|
434
|
-
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
435
|
-
// 绘制粒子轨迹效果(在绘制连线和粒子之前)
|
|
436
|
-
this.drawTrailEffect();
|
|
437
|
-
// 绘制粒子间的连线
|
|
438
|
-
if (this.config.showLine) {
|
|
439
|
-
// 遍历所有粒子对
|
|
440
|
-
for (let i = 0; i < this.particles.length; i++) {
|
|
441
|
-
for (let j = i + 1; j < this.particles.length; j++) {
|
|
442
|
-
// 计算两个粒子之间的距离
|
|
443
|
-
const dx = this.particles[i].x - this.particles[j].x;
|
|
444
|
-
const dy = this.particles[i].y - this.particles[j].y;
|
|
445
|
-
const distance = Math.sqrt(dx * dx + dy * dy); // 欧氏距离
|
|
446
|
-
// 如果距离小于连接距离,绘制连线
|
|
447
|
-
if (distance < this.config.linkDistance) {
|
|
448
|
-
// 根据距离计算连线透明度(越近越不透明)
|
|
449
|
-
const alpha = this.config.lineOpacity * (1 - distance / this.config.linkDistance);
|
|
450
|
-
this.ctx.strokeStyle = this.config.lineColor;
|
|
451
|
-
this.ctx.globalAlpha = alpha; // 设置透明度
|
|
452
|
-
this.ctx.lineWidth = this.config.lineWidth;
|
|
453
|
-
// 绘制连线
|
|
454
|
-
this.ctx.beginPath();
|
|
455
|
-
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
|
|
456
|
-
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
|
|
457
|
-
this.ctx.stroke();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// 绘制粒子
|
|
463
|
-
this.ctx.globalAlpha = 1; // 重置透明度
|
|
464
|
-
for (const particle of this.particles) {
|
|
465
|
-
// 计算脉动大小(基于正弦波)
|
|
466
|
-
const pulseSize = particle.size * (1 + Math.sin(particle.pulse) * 0.2);
|
|
467
|
-
// 设置粒子发光效果(只有当particleBlur大于0时才启用)
|
|
468
|
-
if (this.config.particleBlur > 0) {
|
|
469
|
-
this.ctx.shadowColor = particle.color;
|
|
470
|
-
this.ctx.shadowBlur = this.config.particleBlur * 4;
|
|
471
|
-
}
|
|
472
|
-
// 绘制粒子(圆形)
|
|
473
|
-
this.ctx.beginPath();
|
|
474
|
-
this.ctx.arc(particle.x, particle.y, pulseSize, 0, Math.PI * 2);
|
|
475
|
-
this.ctx.fillStyle = particle.color;
|
|
476
|
-
this.ctx.fill();
|
|
477
|
-
// 重置阴影效果
|
|
478
|
-
this.ctx.shadowBlur = 0;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* 更新FPS计数器
|
|
483
|
-
* 计算并更新当前帧率
|
|
484
|
-
*/
|
|
485
|
-
updateFPS() {
|
|
486
|
-
this.frameCount++;
|
|
487
|
-
const now = Date.now();
|
|
488
|
-
const elapsed = now - this.lastFpsUpdate;
|
|
489
|
-
// 每秒更新一次FPS显示
|
|
490
|
-
if (elapsed >= 1000) {
|
|
491
|
-
this.fps = Math.round((this.frameCount * 1000) / elapsed);
|
|
492
|
-
this.frameCount = 0;
|
|
493
|
-
this.lastFpsUpdate = now;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* 动画循环
|
|
498
|
-
* 使用requestAnimationFrame实现平滑动画
|
|
499
|
-
*/
|
|
500
|
-
animate() {
|
|
501
|
-
if (!this.isRunning)
|
|
502
|
-
return;
|
|
503
|
-
const now = performance.now();
|
|
504
|
-
const deltaTime = now - this.lastFrameTime;
|
|
505
|
-
// 限制最小和最大时间增量,避免极端情况
|
|
506
|
-
const clampedDeltaTime = Math.min(Math.max(deltaTime, 1), 100); // 限制在1-100ms之间
|
|
507
|
-
// 更新粒子状态(传入时间增量)
|
|
508
|
-
this.updateParticles(clampedDeltaTime);
|
|
509
|
-
this.drawParticles();
|
|
510
|
-
this.updateFPS();
|
|
511
|
-
// 更新上一帧时间戳
|
|
512
|
-
this.lastFrameTime = now;
|
|
513
|
-
// 请求下一帧动画
|
|
514
|
-
this.animationId = requestAnimationFrame(() => this.animate());
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* 开始粒子动画
|
|
518
|
-
*/
|
|
519
|
-
start() {
|
|
520
|
-
if (this.isRunning)
|
|
521
|
-
return;
|
|
522
|
-
this.isRunning = true;
|
|
523
|
-
this.lastFrameTime = performance.now();
|
|
524
|
-
this.animate();
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* 暂停粒子动画
|
|
528
|
-
*/
|
|
529
|
-
pause() {
|
|
530
|
-
this.isRunning = false;
|
|
531
|
-
if (this.animationId) {
|
|
532
|
-
cancelAnimationFrame(this.animationId);
|
|
533
|
-
this.animationId = null;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* 重置粒子系统
|
|
538
|
-
* 重新创建粒子并开始动画
|
|
539
|
-
*/
|
|
540
|
-
reset() {
|
|
541
|
-
// 仅在动画运行时重新开始
|
|
542
|
-
const wasRunning = this.isRunning;
|
|
543
|
-
if (wasRunning) {
|
|
544
|
-
this.pause();
|
|
545
|
-
}
|
|
546
|
-
// 重新创建粒子
|
|
547
|
-
this.createParticles();
|
|
548
|
-
// 如果之前是运行状态,重新开始
|
|
549
|
-
if (wasRunning) {
|
|
550
|
-
this.start();
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
// 如果之前是暂停状态,只绘制一帧
|
|
554
|
-
this.drawParticles();
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* 调整画布尺寸
|
|
559
|
-
* 主要用于响应窗口大小变化,优化性能
|
|
560
|
-
*/
|
|
561
|
-
resize() {
|
|
562
|
-
if (this.config.isBackground) {
|
|
563
|
-
// 更新画布尺寸
|
|
564
|
-
const oldWidth = this.canvas.width;
|
|
565
|
-
const oldHeight = this.canvas.height;
|
|
566
|
-
const newWidth = window.innerWidth;
|
|
567
|
-
const newHeight = window.innerHeight;
|
|
568
|
-
this.canvas.width = newWidth;
|
|
569
|
-
this.canvas.height = newHeight;
|
|
570
|
-
// 如果只是尺寸变化,调整粒子位置而不是重新创建
|
|
571
|
-
if (this.particles.length > 0) {
|
|
572
|
-
// 缩放粒子位置以适应新尺寸
|
|
573
|
-
const scaleX = newWidth / oldWidth;
|
|
574
|
-
const scaleY = newHeight / oldHeight;
|
|
575
|
-
for (const particle of this.particles) {
|
|
576
|
-
// 缩放粒子位置
|
|
577
|
-
particle.x *= scaleX;
|
|
578
|
-
particle.y *= scaleY;
|
|
579
|
-
// 确保粒子在新画布范围内
|
|
580
|
-
particle.x = Math.max(0, Math.min(newWidth, particle.x));
|
|
581
|
-
particle.y = Math.max(0, Math.min(newHeight, particle.y));
|
|
582
|
-
// 如果粒子靠近边界,调整速度方向
|
|
583
|
-
if (particle.x <= 0 || particle.x >= newWidth)
|
|
584
|
-
particle.vx *= -1;
|
|
585
|
-
if (particle.y <= 0 || particle.y >= newHeight)
|
|
586
|
-
particle.vy *= -1;
|
|
587
|
-
// 修复:缩放粒子的历史位置,避免光影效果异常
|
|
588
|
-
if (this.config.showTrail) {
|
|
589
|
-
for (let i = 0; i < particle.trailPositions.length; i++) {
|
|
590
|
-
const pos = particle.trailPositions[i];
|
|
591
|
-
pos.x *= scaleX;
|
|
592
|
-
pos.y *= scaleY;
|
|
593
|
-
// 确保历史位置也在画布范围内
|
|
594
|
-
pos.x = Math.max(0, Math.min(newWidth, pos.x));
|
|
595
|
-
pos.y = Math.max(0, Math.min(newHeight, pos.y));
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
// 立即绘制一帧,避免空白
|
|
600
|
-
this.drawParticles();
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
// 如果没有粒子,重新创建
|
|
604
|
-
this.createParticles();
|
|
605
|
-
this.drawParticles();
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* 销毁粒子系统
|
|
611
|
-
* 停止动画并移除画布
|
|
612
|
-
*/
|
|
613
|
-
destroy() {
|
|
614
|
-
this.pause();
|
|
615
|
-
if (this.canvas && this.canvas.parentNode) {
|
|
616
|
-
this.canvas.parentNode.removeChild(this.canvas);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* 绑定事件监听器
|
|
621
|
-
*/
|
|
622
|
-
bindEvents() {
|
|
623
|
-
// 窗口大小变化时调整画布尺寸(使用防抖)
|
|
624
|
-
window.addEventListener('resize', () => {
|
|
625
|
-
if (this.config.isBackground) {
|
|
626
|
-
// 清除之前的计时器
|
|
627
|
-
if (this.resizeTimer) {
|
|
628
|
-
clearTimeout(this.resizeTimer);
|
|
629
|
-
}
|
|
630
|
-
// 设置防抖计时器(150毫秒延迟)
|
|
631
|
-
this.resizeTimer = window.setTimeout(() => {
|
|
632
|
-
this.resize();
|
|
633
|
-
}, 150);
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
// 页面可见性变化时处理动画
|
|
637
|
-
document.addEventListener('visibilitychange', () => {
|
|
638
|
-
if (document.hidden) {
|
|
639
|
-
// 页面隐藏时记录运行状态并暂停动画以节省资源
|
|
640
|
-
this.wasRunningBeforeHide = this.isRunning;
|
|
641
|
-
if (this.isRunning) {
|
|
642
|
-
this.pause();
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
// 页面恢复可见时,如果之前是运行状态,重新开始动画
|
|
647
|
-
if (this.wasRunningBeforeHide) {
|
|
648
|
-
this.start();
|
|
649
|
-
}
|
|
650
|
-
// 重置状态
|
|
651
|
-
this.wasRunningBeforeHide = false;
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const ParticleCanvasPro = ParticleCanvas;
|
|
658
|
-
|
|
659
|
-
exports.ParticleCanvas = ParticleCanvas;
|
|
660
|
-
exports.default = ParticleCanvasPro;
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});class i{constructor(i={}){this.MAX_PARTICLE_NUMBER=800,this.MAX_PARTICLE_SPEED=3,this.MAX_PARTICLE_SIZE=10,this.MAX_LINK_DISTANCE=300,this.MAX_LINE_WIDTH=5,this.MAX_LINE_OPACITY=1,this.MAX_TRAIL_VALUE=1,this.MAX_PARTICLE_BLUR=5,this.MAX_TRAIL_INTENSITY=5;const t=this.validateAndLimitOptions(i);this.config={canvasContainer:t.canvasContainer||document.body,isBackground:void 0===t.isBackground||t.isBackground,canvasBackgroundColor:t.canvasBackgroundColor||"rgb(10, 10, 25)",particleNumber:t.particleNumber||150,particleSpeed:t.particleSpeed||1,particleSize:t.particleSize||5,particleColor:t.particleColor||"rgba(156, 74, 255, 0.6)",particleBlur:void 0!==t.particleBlur?t.particleBlur:2,lineWidth:t.lineWidth||1,lineColor:t.lineColor||"#ffffff",lineOpacity:t.lineOpacity||.3,linkDistance:t.linkDistance||120,showLine:void 0===t.showLine||t.showLine,canvasSize:t.canvasSize||[800,600],showTrail:void 0===t.showTrail||t.showTrail,trailIntensity:t.trailIntensity||2},this.particles=[],this.animationId=null,this.isRunning=!1,this.lastTime=0,this.fps=60,this.fpsInterval=1e3/60,this.then=Date.now(),this.frameCount=0,this.lastFpsUpdate=Date.now(),this.resizeTimer=null,this.wasRunningBeforeHide=!1,this.lastFrameTime=performance.now(),this.init()}validateAndLimitOptions(i){const t=Object.assign({},i);return void 0!==i.particleNumber&&(i.particleNumber>this.MAX_PARTICLE_NUMBER?(console.warn(`Warning: particleNumber (${i.particleNumber}) exceeds maximum allowed value (${this.MAX_PARTICLE_NUMBER}). Using maximum value instead.`),t.particleNumber=this.MAX_PARTICLE_NUMBER):i.particleNumber<1&&(console.warn(`Warning: particleNumber (${i.particleNumber}) is below minimum value (1). Using default value instead.`),t.particleNumber=150)),void 0!==i.particleSpeed&&(i.particleSpeed>this.MAX_PARTICLE_SPEED?(console.warn(`Warning: particleSpeed (${i.particleSpeed}) exceeds maximum allowed value (${this.MAX_PARTICLE_SPEED}). Using maximum value instead.`),t.particleSpeed=this.MAX_PARTICLE_SPEED):i.particleSpeed<0&&(console.warn(`Warning: particleSpeed (${i.particleSpeed}) is below minimum value (0). Using default value instead.`),t.particleSpeed=1)),void 0!==i.particleSize&&(i.particleSize>this.MAX_PARTICLE_SIZE?(console.warn(`Warning: particleSize (${i.particleSize}) exceeds maximum allowed value (${this.MAX_PARTICLE_SIZE}). Using maximum value instead.`),t.particleSize=this.MAX_PARTICLE_SIZE):i.particleSize<1&&(console.warn(`Warning: particleSize (${i.particleSize}) is below minimum value (1). Using default value instead.`),t.particleSize=3)),void 0!==i.particleBlur&&(i.particleBlur>this.MAX_PARTICLE_BLUR?(console.warn(`Warning: particleBlur (${i.particleBlur}) exceeds maximum allowed value (${this.MAX_PARTICLE_BLUR}). Using maximum value instead.`),t.particleBlur=this.MAX_PARTICLE_BLUR):i.particleBlur<0&&(console.warn(`Warning: particleBlur (${i.particleBlur}) is below minimum value (0). Using default value instead.`),t.particleBlur=2)),void 0!==i.linkDistance&&(i.linkDistance>this.MAX_LINK_DISTANCE?(console.warn(`Warning: linkDistance (${i.linkDistance}) exceeds maximum allowed value (${this.MAX_LINK_DISTANCE}). Using maximum value instead.`),t.linkDistance=this.MAX_LINK_DISTANCE):i.linkDistance<10&&(console.warn(`Warning: linkDistance (${i.linkDistance}) is below minimum value (10). Using default value instead.`),t.linkDistance=120)),void 0!==i.lineWidth&&(i.lineWidth>this.MAX_LINE_WIDTH?(console.warn(`Warning: lineWidth (${i.lineWidth}) exceeds maximum allowed value (${this.MAX_LINE_WIDTH}). Using maximum value instead.`),t.lineWidth=this.MAX_LINE_WIDTH):i.lineWidth<0&&(console.warn(`Warning: lineWidth (${i.lineWidth}) is below minimum value (0). Using default value instead.`),t.lineWidth=1)),void 0!==i.lineOpacity&&(i.lineOpacity>this.MAX_LINE_OPACITY?(console.warn(`Warning: lineOpacity (${i.lineOpacity}) exceeds maximum allowed value (${this.MAX_LINE_OPACITY}). Using maximum value instead.`),t.lineOpacity=this.MAX_LINE_OPACITY):i.lineOpacity<0&&(console.warn(`Warning: lineOpacity (${i.lineOpacity}) is below minimum value (0). Using default value instead.`),t.lineOpacity=.3)),void 0!==i.trailIntensity&&(i.trailIntensity>this.MAX_TRAIL_INTENSITY?(console.warn(`Warning: trailIntensity (${i.trailIntensity}) exceeds maximum allowed value (${this.MAX_TRAIL_INTENSITY}). Using maximum value instead.`),t.trailIntensity=this.MAX_TRAIL_INTENSITY):i.trailIntensity<1&&(console.warn(`Warning: trailIntensity (${i.trailIntensity}) is below minimum value (1). Using default value instead.`),t.trailIntensity=2)),t}init(){this.canvas=document.createElement("canvas");const i=this.canvas.getContext("2d");if(!i)throw new Error("Could not get 2D context from canvas");if(this.ctx=i,"string"==typeof this.config.canvasContainer){const i=document.querySelector(this.config.canvasContainer);if(!i)throw new Error(`Canvas container not found: ${this.config.canvasContainer}`);this.container=i}else this.container=this.config.canvasContainer;this.container?(this.container.appendChild(this.canvas),this.setupCanvas(),this.createParticles(),this.bindEvents()):console.error("Canvas container not found")}setupCanvas(){if(this.config.isBackground)this.canvas.style.position="fixed",this.canvas.style.top="0",this.canvas.style.left="0",this.canvas.style.width="100%",this.canvas.style.height="100%",this.canvas.style.zIndex="-1",this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight;else{this.canvas.style.display="block";const[i,t]=this.config.canvasSize;this.canvas.width=i,this.canvas.height=t,this.canvas.style.width=i+"px",this.canvas.style.height=t+"px"}this.canvas.style.pointerEvents="none"}createParticles(){this.particles=[];const i=Array.isArray(this.config.particleColor);for(let t=0;t<this.config.particleNumber;t++){let t;if(i){const i=this.config.particleColor;t=i[Math.floor(Math.random()*i.length)]}else t=this.config.particleColor;this.particles.push({x:Math.random()*this.canvas.width,y:Math.random()*this.canvas.height,vx:2*(Math.random()-.5)*this.config.particleSpeed,vy:2*(Math.random()-.5)*this.config.particleSpeed,size:this.config.particleSize,color:t,originalColor:t,pulse:0,pulseSpeed:.05*Math.random()+.02,trailPositions:[]})}}updateParticles(i){const t=i/16.67;for(const i of this.particles)i.pulse+=i.pulseSpeed*t,i.pulse>2*Math.PI&&(i.pulse=0),this.config.showTrail&&(i.trailPositions.unshift({x:i.x,y:i.y}),i.trailPositions.length>10*this.config.trailIntensity&&(i.trailPositions.length=10*this.config.trailIntensity)),i.x+=i.vx*t,i.y+=i.vy*t,(i.x<0||i.x>this.canvas.width)&&(i.vx*=-1),(i.y<0||i.y>this.canvas.height)&&(i.vy*=-1),i.x=Math.max(0,Math.min(this.canvas.width,i.x)),i.y=Math.max(0,Math.min(this.canvas.height,i.y))}drawTrailEffect(){if(this.config.showTrail){this.ctx.save();for(const i of this.particles){if(i.trailPositions.length<2)continue;this.ctx.beginPath(),this.ctx.moveTo(i.trailPositions[0].x,i.trailPositions[0].y);for(let t=1;t<i.trailPositions.length;t++)this.ctx.lineTo(i.trailPositions[t].x,i.trailPositions[t].y);const t=1.2*i.size;this.ctx.lineWidth=t,this.ctx.lineCap="round",this.ctx.lineJoin="round";const s=i.trailPositions[0],e=i.trailPositions[i.trailPositions.length-1],a=this.ctx.createLinearGradient(s.x,s.y,e.x,e.y),n=this.parseColor(i.color),r=.35;a.addColorStop(0,`rgba(${n.r}, ${n.g}, ${n.b}, ${r})`),a.addColorStop(.3,`rgba(${n.r}, ${n.g}, ${n.b}, ${.6*r})`),a.addColorStop(.7,`rgba(${n.r}, ${n.g}, ${n.b}, ${.2*r})`),a.addColorStop(1,`rgba(${n.r}, ${n.g}, ${n.b}, 0)`),this.ctx.strokeStyle=a,this.ctx.stroke()}this.ctx.restore()}}parseColor(i){if(i.startsWith("rgba")){const t=i.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);if(t)return{r:parseInt(t[1]),g:parseInt(t[2]),b:parseInt(t[3]),a:parseFloat(t[4])}}else if(i.startsWith("rgb")){const t=i.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);if(t)return{r:parseInt(t[1]),g:parseInt(t[2]),b:parseInt(t[3])}}else if(i.startsWith("#")){let t,s,e,a=i.replace("#","");if(3===a.length)t=parseInt(a[0]+a[0],16),s=parseInt(a[1]+a[1],16),e=parseInt(a[2]+a[2],16);else{if(6!==a.length)return{r:255,g:255,b:255};t=parseInt(a.substring(0,2),16),s=parseInt(a.substring(2,4),16),e=parseInt(a.substring(4,6),16)}return{r:t,g:s,b:e}}return{r:255,g:255,b:255}}drawParticles(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);let i=this.config.canvasBackgroundColor;if(i.startsWith("rgba")){const t=i.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);if(t){const s=t[1],e=t[2],a=t[3];parseFloat(t[4])<1&&(i=`rgb(${s}, ${e}, ${a})`)}}if(this.ctx.fillStyle=i,this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height),this.drawTrailEffect(),this.config.showLine)for(let i=0;i<this.particles.length;i++)for(let t=i+1;t<this.particles.length;t++){const s=this.particles[i].x-this.particles[t].x,e=this.particles[i].y-this.particles[t].y,a=Math.sqrt(s*s+e*e);if(a<this.config.linkDistance){const s=this.config.lineOpacity*(1-a/this.config.linkDistance);this.ctx.strokeStyle=this.config.lineColor,this.ctx.globalAlpha=s,this.ctx.lineWidth=this.config.lineWidth,this.ctx.beginPath(),this.ctx.moveTo(this.particles[i].x,this.particles[i].y),this.ctx.lineTo(this.particles[t].x,this.particles[t].y),this.ctx.stroke()}}this.ctx.globalAlpha=1;for(const i of this.particles){const t=i.size*(1+.2*Math.sin(i.pulse));this.config.particleBlur>0&&(this.ctx.shadowColor=i.color,this.ctx.shadowBlur=4*this.config.particleBlur),this.ctx.beginPath(),this.ctx.arc(i.x,i.y,t,0,2*Math.PI),this.ctx.fillStyle=i.color,this.ctx.fill(),this.ctx.shadowBlur=0}}updateFPS(){this.frameCount++;const i=Date.now(),t=i-this.lastFpsUpdate;t>=1e3&&(this.fps=Math.round(1e3*this.frameCount/t),this.frameCount=0,this.lastFpsUpdate=i)}animate(){if(!this.isRunning)return;const i=performance.now(),t=i-this.lastFrameTime,s=Math.min(Math.max(t,1),100);this.updateParticles(s),this.drawParticles(),this.updateFPS(),this.lastFrameTime=i,this.animationId=requestAnimationFrame(()=>this.animate())}start(){this.isRunning||(this.isRunning=!0,this.lastFrameTime=performance.now(),this.animate())}pause(){this.isRunning=!1,this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null)}reset(){const i=this.isRunning;i&&this.pause(),this.createParticles(),i?this.start():this.drawParticles()}resize(){if(this.config.isBackground){const i=this.canvas.width,t=this.canvas.height,s=window.innerWidth,e=window.innerHeight;if(this.canvas.width=s,this.canvas.height=e,this.particles.length>0){const a=s/i,n=e/t;for(const i of this.particles)if(i.x*=a,i.y*=n,i.x=Math.max(0,Math.min(s,i.x)),i.y=Math.max(0,Math.min(e,i.y)),(i.x<=0||i.x>=s)&&(i.vx*=-1),(i.y<=0||i.y>=e)&&(i.vy*=-1),this.config.showTrail)for(let t=0;t<i.trailPositions.length;t++){const r=i.trailPositions[t];r.x*=a,r.y*=n,r.x=Math.max(0,Math.min(s,r.x)),r.y=Math.max(0,Math.min(e,r.y))}this.drawParticles()}else this.createParticles(),this.drawParticles()}}destroy(){this.pause(),this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas)}bindEvents(){window.addEventListener("resize",()=>{this.config.isBackground&&(this.resizeTimer&&clearTimeout(this.resizeTimer),this.resizeTimer=window.setTimeout(()=>{this.resize()},150))}),document.addEventListener("visibilitychange",()=>{document.hidden?(this.wasRunningBeforeHide=this.isRunning,this.isRunning&&this.pause()):(this.wasRunningBeforeHide&&this.start(),this.wasRunningBeforeHide=!1)})}}const t=i;exports.ParticleCanvas=i,exports.default=t;
|