@mar7th/firework 1.0.0
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/404.html +25 -0
- package/LICENSE +21 -0
- package/advanced-guide.html +135 -0
- package/api-download.html +164 -0
- package/demo.html +83 -0
- package/dist-lib/spark-fireworks.js +875 -0
- package/dist-lib/spark-fireworks.umd.cjs +1 -0
- package/docs/api-guide.md +750 -0
- package/docs/performance-optimization-plan.md +396 -0
- package/examples/production.html +203 -0
- package/favicon.jpg +0 -0
- package/guide.html +89 -0
- package/index.html +60 -0
- package/package.json +38 -0
- package/script/script1.js +34 -0
- package/script/script2.js +38 -0
- package/script/script3.js +63 -0
- package/script/script4.js +51 -0
- package/script/script5.js +55 -0
- package/scripts/copy-dist-lib.mjs +4 -0
- package/src/app.js +49 -0
- package/src/config/defaults.js +302 -0
- package/src/core/Firework.js +213 -0
- package/src/core/FireworksSimulator.js +439 -0
- package/src/core/Particle.js +174 -0
- package/src/core/ParticleSystem.js +56 -0
- package/src/core/Renderer.js +44 -0
- package/src/demo.js +298 -0
- package/src/effects/BackgroundManager.js +100 -0
- package/src/pages.css +388 -0
- package/src/pages.js +1 -0
- package/src/plugin.js +9 -0
- package/src/styles.css +544 -0
- package/src/ui/ControlPanel.js +205 -0
- package/src/ui/PerformancePanel.js +23 -0
- package/src/ui/PresetManager.js +128 -0
- package/src/utils/color.js +54 -0
- package/src/utils/eventBus.js +21 -0
- package/src/utils/math.js +29 -0
- package/vite.config.js +17 -0
- package/vite.lib.config.js +19 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
export const MAX_PARTICLES = 1400;
|
|
2
|
+
|
|
3
|
+
export const defaultConfig = {
|
|
4
|
+
particleCount: 180,
|
|
5
|
+
particleMinSize: 2.5,
|
|
6
|
+
particleMaxSize: 7,
|
|
7
|
+
sizeEndMultiplier: 0.35,
|
|
8
|
+
gravity: 0.18,
|
|
9
|
+
damping: 0.982,
|
|
10
|
+
initialSpeedMin: 2.8,
|
|
11
|
+
initialSpeedMax: 8.2,
|
|
12
|
+
particleLife: 94,
|
|
13
|
+
rotationSpeed: 0.025,
|
|
14
|
+
colorShiftRate: 0.035,
|
|
15
|
+
colorStart: "#ffbf4d",
|
|
16
|
+
colorEnd: "#ff5a3c",
|
|
17
|
+
useRandomColors: false,
|
|
18
|
+
textureType: "circle",
|
|
19
|
+
glowIntensity: 0.9,
|
|
20
|
+
explosionShape: "sphere",
|
|
21
|
+
enableSecondaryBurst: false,
|
|
22
|
+
secondaryBurstRatio: 0.2,
|
|
23
|
+
enableTrails: true,
|
|
24
|
+
showLaunchTrail: true,
|
|
25
|
+
windEnabled: false,
|
|
26
|
+
windX: 0,
|
|
27
|
+
windY: 0,
|
|
28
|
+
flicker: true,
|
|
29
|
+
fadeTrail: 0.18,
|
|
30
|
+
autoLaunch: false,
|
|
31
|
+
autoInterval: 900,
|
|
32
|
+
autoLaunchIntervalSeconds: 0.9,
|
|
33
|
+
backgroundType: "starfield",
|
|
34
|
+
backgroundColor: "#170d17",
|
|
35
|
+
backgroundColor2: "#3a1a24",
|
|
36
|
+
backgroundImageMode: "cover"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const controlsSchema = [
|
|
40
|
+
{
|
|
41
|
+
title: "样式",
|
|
42
|
+
controls: [
|
|
43
|
+
{ key: "colorStart", label: "起始颜色", type: "color" },
|
|
44
|
+
{ key: "colorEnd", label: "结束颜色", type: "color" },
|
|
45
|
+
{ key: "useRandomColors", label: "随机颜色", type: "boolean" },
|
|
46
|
+
{
|
|
47
|
+
key: "textureType",
|
|
48
|
+
label: "粒子形状",
|
|
49
|
+
type: "select",
|
|
50
|
+
options: [
|
|
51
|
+
["circle", "圆形"],
|
|
52
|
+
["square", "方形"],
|
|
53
|
+
["star", "星形"],
|
|
54
|
+
["heart", "心形"]
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{ key: "glowIntensity", label: "辉光强度", type: "range", min: 0, max: 1.5, step: 0.05 }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: "特效",
|
|
62
|
+
controls: [
|
|
63
|
+
{
|
|
64
|
+
key: "explosionShape",
|
|
65
|
+
label: "爆炸形状",
|
|
66
|
+
type: "select",
|
|
67
|
+
options: [
|
|
68
|
+
["sphere", "球形"],
|
|
69
|
+
["ring", "环形"],
|
|
70
|
+
["starburst", "星形"],
|
|
71
|
+
["flower", "花朵"],
|
|
72
|
+
["willow", "柳絮"],
|
|
73
|
+
["scatter", "随机散落"],
|
|
74
|
+
["heart", "爱心"]
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "enableSecondaryBurst",
|
|
79
|
+
label: "二次爆炸",
|
|
80
|
+
type: "boolean",
|
|
81
|
+
hint: "二次爆炸对性能影响较大,谨慎开启。"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "secondaryBurstRatio",
|
|
85
|
+
label: "二次比例",
|
|
86
|
+
type: "range",
|
|
87
|
+
min: 0,
|
|
88
|
+
max: 1,
|
|
89
|
+
step: 0.05,
|
|
90
|
+
hint: "这里只控制二次爆炸比例;自定义二次爆炸效果需要使用高级模式自定义脚本。"
|
|
91
|
+
},
|
|
92
|
+
{ key: "enableTrails", label: "粒子拖尾", type: "boolean", hint: "拖尾会影响性能,酌情开启。" },
|
|
93
|
+
{ key: "showLaunchTrail", label: "升空轨迹", type: "boolean" },
|
|
94
|
+
{ key: "windEnabled", label: "风场", type: "boolean" },
|
|
95
|
+
{ key: "windX", label: "水平风", type: "range", min: -0.35, max: 0.35, step: 0.01 },
|
|
96
|
+
{ key: "windY", label: "垂直风", type: "range", min: -0.35, max: 0.35, step: 0.01 },
|
|
97
|
+
{ key: "flicker", label: "闪烁", type: "boolean" },
|
|
98
|
+
{ key: "fadeTrail", label: "拖尾长度", type: "range", min: 0.04, max: 0.55, step: 0.01 }
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
title: "粒子",
|
|
103
|
+
controls: [
|
|
104
|
+
{ key: "particleCount", label: "粒子数量", type: "range", min: 10, max: 1000, step: 10 },
|
|
105
|
+
{ key: "initialSpeedMin", label: "最小速度", type: "range", min: 0, max: 20, step: 0.1 },
|
|
106
|
+
{ key: "initialSpeedMax", label: "最大速度", type: "range", min: 0, max: 20, step: 0.1 },
|
|
107
|
+
{ key: "damping", label: "速度衰减", type: "range", min: 0.95, max: 1, step: 0.001 },
|
|
108
|
+
{ key: "gravity", label: "重力影响", type: "range", min: -1, max: 2, step: 0.01 },
|
|
109
|
+
{ key: "particleLife", label: "生命周期", type: "range", min: 30, max: 180, step: 1 },
|
|
110
|
+
{ key: "particleMinSize", label: "最小大小", type: "range", min: 2, max: 15, step: 0.5 },
|
|
111
|
+
{ key: "particleMaxSize", label: "最大大小", type: "range", min: 2, max: 15, step: 0.5 },
|
|
112
|
+
{ key: "sizeEndMultiplier", label: "结束比例", type: "range", min: 0, max: 2, step: 0.05 },
|
|
113
|
+
{ key: "rotationSpeed", label: "旋转速度", type: "range", min: 0, max: 0.105, step: 0.005 },
|
|
114
|
+
{ key: "colorShiftRate", label: "变色速率", type: "range", min: 0, max: 0.1, step: 0.005 },
|
|
115
|
+
{ key: "autoLaunchIntervalSeconds", label: "自动间隔(秒)", type: "range", min: 0.25, max: 3, step: 0.05 }
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: "背景",
|
|
120
|
+
controls: [
|
|
121
|
+
{
|
|
122
|
+
key: "backgroundType",
|
|
123
|
+
label: "背景样式",
|
|
124
|
+
type: "select",
|
|
125
|
+
options: [
|
|
126
|
+
["solid", "纯色"],
|
|
127
|
+
["linear", "线性渐变"],
|
|
128
|
+
["radial", "径向渐变"],
|
|
129
|
+
["starfield", "星空"],
|
|
130
|
+
["image", "图片"]
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{ key: "backgroundColor", label: "背景色 A", type: "color" },
|
|
134
|
+
{ key: "backgroundColor2", label: "背景色 B", type: "color" },
|
|
135
|
+
{
|
|
136
|
+
key: "backgroundImageMode",
|
|
137
|
+
label: "图片填充",
|
|
138
|
+
type: "select",
|
|
139
|
+
options: [
|
|
140
|
+
["cover", "覆盖"],
|
|
141
|
+
["contain", "完整"],
|
|
142
|
+
["tile", "平铺"]
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{ key: "backgroundImage", label: "上传图片", type: "file" }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
export const builtInPresets = {
|
|
151
|
+
"classic-gold": {
|
|
152
|
+
name: "经典礼花",
|
|
153
|
+
config: {
|
|
154
|
+
colorStart: "#ffd166",
|
|
155
|
+
colorEnd: "#ff3d2e",
|
|
156
|
+
explosionShape: "sphere",
|
|
157
|
+
particleCount: 220,
|
|
158
|
+
gravity: 0.22,
|
|
159
|
+
damping: 0.981,
|
|
160
|
+
glowIntensity: 0.9,
|
|
161
|
+
enableSecondaryBurst: false,
|
|
162
|
+
textureType: "circle",
|
|
163
|
+
fadeTrail: 0.18
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
aurora: {
|
|
167
|
+
name: "梦幻极光",
|
|
168
|
+
config: {
|
|
169
|
+
colorStart: "#4cc9f0",
|
|
170
|
+
colorEnd: "#80ffdb",
|
|
171
|
+
explosionShape: "ring",
|
|
172
|
+
particleCount: 260,
|
|
173
|
+
gravity: 0.03,
|
|
174
|
+
damping: 0.992,
|
|
175
|
+
glowIntensity: 1.1,
|
|
176
|
+
fadeTrail: 0.08,
|
|
177
|
+
textureType: "circle"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
fountain: {
|
|
181
|
+
name: "星光喷泉",
|
|
182
|
+
config: {
|
|
183
|
+
colorStart: "#f7f3a1",
|
|
184
|
+
colorEnd: "#fca311",
|
|
185
|
+
explosionShape: "flower",
|
|
186
|
+
particleCount: 420,
|
|
187
|
+
particleMinSize: 2,
|
|
188
|
+
particleMaxSize: 4.5,
|
|
189
|
+
initialSpeedMin: 4,
|
|
190
|
+
initialSpeedMax: 11,
|
|
191
|
+
gravity: 0.32,
|
|
192
|
+
damping: 0.976,
|
|
193
|
+
textureType: "star"
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
ghost: {
|
|
197
|
+
name: "幽灵鬼火",
|
|
198
|
+
config: {
|
|
199
|
+
colorStart: "#7bdff2",
|
|
200
|
+
colorEnd: "#b2f7ef",
|
|
201
|
+
explosionShape: "scatter",
|
|
202
|
+
particleCount: 160,
|
|
203
|
+
initialSpeedMin: 0.8,
|
|
204
|
+
initialSpeedMax: 4.6,
|
|
205
|
+
gravity: -0.01,
|
|
206
|
+
damping: 0.989,
|
|
207
|
+
enableSecondaryBurst: true,
|
|
208
|
+
glowIntensity: 1.2,
|
|
209
|
+
fadeTrail: 0.12
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
meteor: {
|
|
213
|
+
name: "节日流星",
|
|
214
|
+
config: {
|
|
215
|
+
colorStart: "#ffafcc",
|
|
216
|
+
colorEnd: "#ffffff",
|
|
217
|
+
explosionShape: "willow",
|
|
218
|
+
particleCount: 300,
|
|
219
|
+
initialSpeedMin: 7,
|
|
220
|
+
initialSpeedMax: 13,
|
|
221
|
+
damping: 0.965,
|
|
222
|
+
gravity: 0.16,
|
|
223
|
+
windEnabled: true,
|
|
224
|
+
windX: 0.12,
|
|
225
|
+
textureType: "square"
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
heart: {
|
|
229
|
+
name: "爱心爆发",
|
|
230
|
+
config: {
|
|
231
|
+
colorStart: "#ff4d6d",
|
|
232
|
+
colorEnd: "#ffd6e0",
|
|
233
|
+
explosionShape: "heart",
|
|
234
|
+
particleCount: 260,
|
|
235
|
+
gravity: 0.08,
|
|
236
|
+
damping: 0.986,
|
|
237
|
+
textureType: "heart",
|
|
238
|
+
glowIntensity: 1
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export function sanitizeConfig(input) {
|
|
244
|
+
const clean = { ...defaultConfig };
|
|
245
|
+
if (!input || typeof input !== "object") {
|
|
246
|
+
return clean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const schemas = controlsSchema.flatMap((section) => section.controls);
|
|
250
|
+
for (const item of schemas) {
|
|
251
|
+
if (!(item.key in input)) continue;
|
|
252
|
+
|
|
253
|
+
if (item.type === "range") {
|
|
254
|
+
const value = Number(input[item.key]);
|
|
255
|
+
if (Number.isFinite(value)) {
|
|
256
|
+
clean[item.key] = Math.min(item.max, Math.max(item.min, value));
|
|
257
|
+
}
|
|
258
|
+
} else if (item.type === "boolean") {
|
|
259
|
+
clean[item.key] = Boolean(input[item.key]);
|
|
260
|
+
} else if (item.type === "select") {
|
|
261
|
+
const allowed = item.options.map(([value]) => value);
|
|
262
|
+
if (allowed.includes(input[item.key])) clean[item.key] = input[item.key];
|
|
263
|
+
} else if (item.type === "color") {
|
|
264
|
+
const value = String(input[item.key]);
|
|
265
|
+
if (/^#[0-9a-f]{6}$/i.test(value)) clean[item.key] = value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if ("autoLaunch" in input) {
|
|
270
|
+
clean.autoLaunch = Boolean(input.autoLaunch);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ("autoInterval" in input) {
|
|
274
|
+
const intervalMs = Number(input.autoInterval);
|
|
275
|
+
if (Number.isFinite(intervalMs)) {
|
|
276
|
+
clean.autoInterval = clamp(intervalMs, 250, 3000);
|
|
277
|
+
clean.autoLaunchIntervalSeconds = toSeconds(clean.autoInterval);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if ("autoLaunchIntervalSeconds" in input) {
|
|
282
|
+
const intervalSeconds = Number(input.autoLaunchIntervalSeconds);
|
|
283
|
+
if (Number.isFinite(intervalSeconds)) {
|
|
284
|
+
clean.autoLaunchIntervalSeconds = clamp(intervalSeconds, 0.25, 3);
|
|
285
|
+
clean.autoInterval = toMilliseconds(clean.autoLaunchIntervalSeconds);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return clean;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function clamp(value, min, max) {
|
|
293
|
+
return Math.min(max, Math.max(min, value));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function toMilliseconds(seconds) {
|
|
297
|
+
return Math.round(seconds * 1000);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function toSeconds(milliseconds) {
|
|
301
|
+
return Math.round((milliseconds / 1000) * 100) / 100;
|
|
302
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { random, randomInt } from "../utils/math.js";
|
|
2
|
+
import { randomVividColor } from "../utils/color.js";
|
|
3
|
+
|
|
4
|
+
export class FireworkFactory {
|
|
5
|
+
constructor(getConfig) {
|
|
6
|
+
this.getConfig = getConfig;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
createExplosion(x, y, override = {}) {
|
|
10
|
+
const config = { ...this.getConfig(), ...override };
|
|
11
|
+
const count = Math.min(1000, Math.max(10, Math.round(config.particleCount)));
|
|
12
|
+
const particles = [];
|
|
13
|
+
const randomStart = config.useRandomColors ? randomVividColor() : config.colorStart;
|
|
14
|
+
const randomEnd = config.useRandomColors ? randomVividColor() : config.colorEnd;
|
|
15
|
+
const secondaryCount = config.enableSecondaryBurst
|
|
16
|
+
? Math.round(count * clampRatio(config.secondaryBurstRatio))
|
|
17
|
+
: 0;
|
|
18
|
+
const secondaryIndexes = createSecondaryIndexes(count, secondaryCount);
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < count; i += 1) {
|
|
21
|
+
const vector = createVector(config.explosionShape, i, count);
|
|
22
|
+
const speed = vector.speedMultiplier * random(config.initialSpeedMin, config.initialSpeedMax);
|
|
23
|
+
const jitterX = config.explosionShape === "heart" ? vector.offsetX : 0;
|
|
24
|
+
const jitterY = config.explosionShape === "heart" ? vector.offsetY : 0;
|
|
25
|
+
const startColor = config.useRandomColors && i % 4 === 0 ? randomVividColor() : randomStart;
|
|
26
|
+
const endColor = config.useRandomColors && i % 5 === 0 ? randomVividColor() : randomEnd;
|
|
27
|
+
const size = random(config.particleMinSize, config.particleMaxSize);
|
|
28
|
+
const life = random(config.particleLife * 0.78, config.particleLife * 1.18);
|
|
29
|
+
|
|
30
|
+
particles.push({
|
|
31
|
+
x: x + jitterX,
|
|
32
|
+
y: y + jitterY,
|
|
33
|
+
vx: vector.x * speed,
|
|
34
|
+
vy: vector.y * speed,
|
|
35
|
+
size,
|
|
36
|
+
life,
|
|
37
|
+
startColor,
|
|
38
|
+
endColor,
|
|
39
|
+
textureType: config.textureType,
|
|
40
|
+
rotationVelocity: random(-config.rotationSpeed, config.rotationSpeed),
|
|
41
|
+
secondaryEligible: secondaryIndexes.has(i)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return particles;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
createSubExplosion(x, y, source = null) {
|
|
49
|
+
const baseConfig = source?.config || this.getConfig();
|
|
50
|
+
const particleConfig = this.createConfigFromParticle(source, baseConfig);
|
|
51
|
+
const overrides = source?.secondaryBurstConfig || {};
|
|
52
|
+
const config = {
|
|
53
|
+
...particleConfig,
|
|
54
|
+
...overrides,
|
|
55
|
+
enableSecondaryBurst: false
|
|
56
|
+
};
|
|
57
|
+
const particleCount = "particleCount" in overrides
|
|
58
|
+
? config.particleCount
|
|
59
|
+
: Math.max(12, Math.round(particleConfig.particleCount * 0.16));
|
|
60
|
+
const particleLife = "particleLife" in overrides
|
|
61
|
+
? config.particleLife
|
|
62
|
+
: Math.max(28, particleConfig.particleLife * 0.48);
|
|
63
|
+
const initialSpeedMin = "initialSpeedMin" in overrides
|
|
64
|
+
? config.initialSpeedMin
|
|
65
|
+
: Math.max(0.4, particleConfig.initialSpeedMin * 0.38);
|
|
66
|
+
const initialSpeedMax = "initialSpeedMax" in overrides
|
|
67
|
+
? config.initialSpeedMax
|
|
68
|
+
: Math.max(1, particleConfig.initialSpeedMax * 0.56);
|
|
69
|
+
|
|
70
|
+
return this.createExplosion(x, y, {
|
|
71
|
+
...config,
|
|
72
|
+
particleCount,
|
|
73
|
+
particleLife,
|
|
74
|
+
initialSpeedMin,
|
|
75
|
+
initialSpeedMax,
|
|
76
|
+
enableSecondaryBurst: false
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
createConfigFromParticle(source, baseConfig) {
|
|
81
|
+
if (!source) return baseConfig;
|
|
82
|
+
return {
|
|
83
|
+
...baseConfig,
|
|
84
|
+
colorStart: source.startColor || baseConfig.colorStart,
|
|
85
|
+
colorEnd: source.endColor || baseConfig.colorEnd,
|
|
86
|
+
useRandomColors: false,
|
|
87
|
+
textureType: source.textureType || baseConfig.textureType,
|
|
88
|
+
particleMinSize: source.size,
|
|
89
|
+
particleMaxSize: source.size,
|
|
90
|
+
particleLife: source.maxLife,
|
|
91
|
+
enableSecondaryBurst: false
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
createLaunch(startX, startY, targetX, targetY) {
|
|
96
|
+
const config = this.getConfig();
|
|
97
|
+
return new LaunchShell(startX, startY, targetX, targetY, config);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clampRatio(value) {
|
|
102
|
+
if (!Number.isFinite(Number(value))) return 0;
|
|
103
|
+
return Math.min(1, Math.max(0, Number(value)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createSecondaryIndexes(count, secondaryCount) {
|
|
107
|
+
const indexes = new Set();
|
|
108
|
+
while (indexes.size < secondaryCount) {
|
|
109
|
+
indexes.add(randomInt(0, count - 1));
|
|
110
|
+
}
|
|
111
|
+
return indexes;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class LaunchShell {
|
|
115
|
+
constructor(startX, startY, targetX, targetY, config) {
|
|
116
|
+
this.x = startX;
|
|
117
|
+
this.y = startY;
|
|
118
|
+
this.targetX = targetX;
|
|
119
|
+
this.targetY = targetY;
|
|
120
|
+
this.color = config.colorStart;
|
|
121
|
+
this.life = 1;
|
|
122
|
+
this.path = [];
|
|
123
|
+
const dx = targetX - startX;
|
|
124
|
+
const dy = targetY - startY;
|
|
125
|
+
const distance = Math.hypot(dx, dy) || 1;
|
|
126
|
+
const speed = Math.min(18, Math.max(9, distance / 28));
|
|
127
|
+
this.vx = (dx / distance) * speed;
|
|
128
|
+
this.vy = (dy / distance) * speed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
update(step) {
|
|
132
|
+
this.path.push({ x: this.x, y: this.y });
|
|
133
|
+
if (this.path.length > 18) this.path.shift();
|
|
134
|
+
this.x += this.vx * step;
|
|
135
|
+
this.y += this.vy * step;
|
|
136
|
+
this.vy += 0.03 * step;
|
|
137
|
+
return Math.hypot(this.targetX - this.x, this.targetY - this.y) < 14 || this.vy > 1.5;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
draw(ctx, showTrail) {
|
|
141
|
+
ctx.save();
|
|
142
|
+
ctx.strokeStyle = "rgba(255, 230, 170, 0.35)";
|
|
143
|
+
ctx.fillStyle = this.color;
|
|
144
|
+
ctx.shadowColor = this.color;
|
|
145
|
+
ctx.shadowBlur = 16;
|
|
146
|
+
|
|
147
|
+
if (showTrail && this.path.length > 1) {
|
|
148
|
+
ctx.beginPath();
|
|
149
|
+
this.path.forEach((point, index) => {
|
|
150
|
+
if (index === 0) ctx.moveTo(point.x, point.y);
|
|
151
|
+
else ctx.lineTo(point.x, point.y);
|
|
152
|
+
});
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ctx.beginPath();
|
|
157
|
+
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
|
|
158
|
+
ctx.fill();
|
|
159
|
+
ctx.restore();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createVector(shape, index, count) {
|
|
164
|
+
if (shape === "ring") {
|
|
165
|
+
const angle = (index / count) * Math.PI * 2;
|
|
166
|
+
return { x: Math.cos(angle), y: Math.sin(angle), speedMultiplier: 1 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (shape === "starburst") {
|
|
170
|
+
const spoke = randomInt(0, 4);
|
|
171
|
+
const base = (spoke / 5) * Math.PI * 2 - Math.PI / 2;
|
|
172
|
+
const angle = base + random(-0.16, 0.16);
|
|
173
|
+
return { x: Math.cos(angle), y: Math.sin(angle), speedMultiplier: index % 3 === 0 ? 1.25 : 0.62 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (shape === "flower") {
|
|
177
|
+
const angle = (index / count) * Math.PI * 2;
|
|
178
|
+
const petal = 0.58 + Math.abs(Math.sin(angle * 6)) * 0.72;
|
|
179
|
+
return { x: Math.cos(angle), y: Math.sin(angle), speedMultiplier: petal };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (shape === "willow") {
|
|
183
|
+
const angle = random(-Math.PI * 0.92, -Math.PI * 0.08);
|
|
184
|
+
return { x: Math.cos(angle), y: Math.sin(angle), speedMultiplier: random(0.28, 1.35) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (shape === "scatter") {
|
|
188
|
+
const angle = random(0, Math.PI * 2);
|
|
189
|
+
return { x: Math.cos(angle), y: Math.sin(angle), speedMultiplier: random(0.2, 1.55) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (shape === "heart") {
|
|
193
|
+
const t = random(0, Math.PI * 2);
|
|
194
|
+
const hx = 16 * Math.pow(Math.sin(t), 3);
|
|
195
|
+
const hy = -(
|
|
196
|
+
13 * Math.cos(t) -
|
|
197
|
+
5 * Math.cos(2 * t) -
|
|
198
|
+
2 * Math.cos(3 * t) -
|
|
199
|
+
Math.cos(4 * t)
|
|
200
|
+
);
|
|
201
|
+
const length = Math.hypot(hx, hy) || 1;
|
|
202
|
+
return {
|
|
203
|
+
x: hx / length,
|
|
204
|
+
y: hy / length,
|
|
205
|
+
offsetX: hx * 1.8,
|
|
206
|
+
offsetY: hy * 1.8,
|
|
207
|
+
speedMultiplier: random(0.72, 1.12)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const angle = random(0, Math.PI * 2);
|
|
212
|
+
return { x: Math.cos(angle), y: Math.sin(angle), speedMultiplier: Math.sqrt(Math.random()) };
|
|
213
|
+
}
|