@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/src/demo.js ADDED
@@ -0,0 +1,298 @@
1
+ import "./styles.css";
2
+ import SparkFireworks from "./app.js";
3
+ import { eventBus } from "./utils/eventBus.js";
4
+ import { ControlPanel } from "./ui/ControlPanel.js";
5
+ import { PresetManager } from "./ui/PresetManager.js";
6
+ import script1 from "../script/script1.js?raw";
7
+ import script2 from "../script/script2.js?raw";
8
+ import script3 from "../script/script3.js?raw";
9
+ import script4 from "../script/script4.js?raw";
10
+ import script5 from "../script/script5.js?raw";
11
+
12
+ const appShell = document.querySelector("#app");
13
+ const canvas = document.querySelector("#fireworksCanvas");
14
+ const controlsContainer = document.querySelector("#controls");
15
+ const fpsCounter = document.querySelector("#fpsCounter");
16
+ const toastElement = document.querySelector("#toast");
17
+ const presetSelect = document.querySelector("#presetSelect");
18
+ const resetConfigButton = document.querySelector("#resetConfigButton");
19
+ const savePresetButton = document.querySelector("#savePresetButton");
20
+ const exportButton = document.querySelector("#exportButton");
21
+ const importInput = document.querySelector("#importInput");
22
+ const shareButton = document.querySelector("#shareButton");
23
+ const advancedModeButton = document.querySelector("#advancedModeButton");
24
+ const autoButton = document.querySelector("#autoButton");
25
+ const pauseButton = document.querySelector("#pauseButton");
26
+ const clearButton = document.querySelector("#clearButton");
27
+ const collapseButton = document.querySelector("#collapseButton");
28
+ const advancedScriptInput = document.querySelector("#advancedScriptInput");
29
+ const advancedScriptPresetSelect = document.querySelector("#advancedScriptPresetSelect");
30
+ const applyAdvancedScriptButton = document.querySelector("#applyAdvancedScriptButton");
31
+ const resetAdvancedScriptButton = document.querySelector("#resetAdvancedScriptButton");
32
+ const advancedScriptStatus = document.querySelector("#advancedScriptStatus");
33
+
34
+ let toastTimer = 0;
35
+ let scriptCompileTimer = 0;
36
+
37
+ const defaultAdvancedScript = `function createParticles(canvasData) {
38
+ const particles = [];
39
+ const count = 360;
40
+
41
+ for (let i = 0; i < count; i += 1) {
42
+ const angle = (i / count) * Math.PI * 2;
43
+ const petal = 0.6 + Math.abs(Math.sin(angle * 5)) * 0.85;
44
+ const speed = petal * (3 + Math.random() * 6.5);
45
+
46
+ particles.push({
47
+ x: canvasData.x,
48
+ y: canvasData.y,
49
+ vx: Math.cos(angle) * speed,
50
+ vy: Math.sin(angle) * speed,
51
+ size: 2 + Math.random() * 5,
52
+ life: 72 + Math.random() * 58,
53
+ startColor: i % 2 === 0 ? "#ffbf4d" : "#ff5a3c",
54
+ endColor: i % 3 === 0 ? "#fff2d0" : "#d12f2f",
55
+ textureType: i % 6 === 0 ? "star" : "circle",
56
+ rotationVelocity: -0.04 + Math.random() * 0.08,
57
+ secondaryEligible: false
58
+ });
59
+ }
60
+
61
+ return {
62
+ config: {
63
+ gravity: 0.08,
64
+ damping: 0.988,
65
+ enableTrails: true,
66
+ fadeTrail: 0.1,
67
+ glowIntensity: 1.1,
68
+ flicker: true,
69
+ sizeEndMultiplier: 0.25,
70
+ colorShiftRate: 0.04,
71
+ windEnabled: false,
72
+ textureType: "star"
73
+ },
74
+ particles
75
+ };
76
+ }`;
77
+
78
+ const advancedScriptPresets = [
79
+ ["default", "默认示例", defaultAdvancedScript],
80
+ ["script1", "script1:五角星爆炸", script1],
81
+ ["script2", "script2:爱心爆炸", script2],
82
+ ["script3", "script3:核心圆与放射线", script3],
83
+ ["script4", "script4:彩虹多层圆", script4],
84
+ ["script5", "script5:十二方向二次爆炸", script5]
85
+ ];
86
+
87
+ const toast = (message) => {
88
+ window.clearTimeout(toastTimer);
89
+ toastElement.textContent = message;
90
+ toastElement.classList.add("visible");
91
+ toastTimer = window.setTimeout(() => toastElement.classList.remove("visible"), 2400);
92
+ };
93
+
94
+ const fireworks = SparkFireworks.create(canvas, {
95
+ eventBus,
96
+ advancedMode: false,
97
+ createParticles: compileAdvancedScript(defaultAdvancedScript, false),
98
+ onFpsChange: (fps) => {
99
+ fpsCounter.textContent = `FPS ${fps}`;
100
+ },
101
+ onLowFps: () => {
102
+ controlPanel.updateValues(fireworks.getConfig());
103
+ toast("检测到帧率偏低,已降低推荐粒子数量");
104
+ },
105
+ onConfigChange: (config) => {
106
+ controlPanel?.updateValues(config);
107
+ autoButton.setAttribute("aria-pressed", String(config.autoLaunch));
108
+ }
109
+ });
110
+
111
+ advancedModeButton?.setAttribute("aria-pressed", String(fireworks.advancedMode));
112
+
113
+ const controlPanel = new ControlPanel(
114
+ controlsContainer,
115
+ fireworks.getConfig(),
116
+ eventBus,
117
+ fireworks.backgroundManager,
118
+ toast
119
+ );
120
+ controlPanel.render();
121
+ initializeAdvancedScriptEditor();
122
+
123
+ if (new URLSearchParams(window.location.search).get("embed") === "1") {
124
+ appShell.classList.add("embed-mode");
125
+ fireworks.startAutoPlay(1.2);
126
+ }
127
+
128
+ eventBus.on("config:change", ({ key, value }) => {
129
+ if (!key) return;
130
+ fireworks.setConfig({ [key]: value });
131
+ });
132
+
133
+ const presetManager = new PresetManager(
134
+ presetSelect,
135
+ () => fireworks.getConfig(),
136
+ (config, presetId) => {
137
+ if (presetId === "default" || presetId?.startsWith?.("builtin:")) {
138
+ fireworks.selectPreset(presetId);
139
+ return;
140
+ }
141
+ fireworks.setConfig(config);
142
+ },
143
+ toast
144
+ );
145
+ presetManager.renderOptions();
146
+ presetManager.bind();
147
+ presetManager.loadFromHash();
148
+
149
+ resetConfigButton.addEventListener("click", () => {
150
+ fireworks.resetConfig();
151
+ toast("已恢复初始化设置");
152
+ });
153
+
154
+ savePresetButton.addEventListener("click", () => presetManager.saveCurrent());
155
+ exportButton.addEventListener("click", () => presetManager.exportConfig());
156
+ importInput.addEventListener("change", () => {
157
+ presetManager.importConfig(importInput.files?.[0]);
158
+ importInput.value = "";
159
+ });
160
+ shareButton.addEventListener("click", () => presetManager.shareConfig());
161
+
162
+ advancedModeButton?.addEventListener("click", () => {
163
+ const enabled = !fireworks.advancedMode;
164
+ const creator = compileAdvancedScript(advancedScriptInput.value);
165
+ fireworks.setAdvancedMode(enabled, creator);
166
+ advancedModeButton.setAttribute("aria-pressed", String(enabled));
167
+ toast(enabled ? "高级模式已开启:所有参数均以脚本设置为准,页面设置可能被覆盖不生效" : "高级模式已关闭");
168
+ });
169
+
170
+ autoButton.addEventListener("click", () => {
171
+ const config = fireworks.getConfig();
172
+ if (config.autoLaunch) {
173
+ fireworks.stopAutoPlay();
174
+ toast("自动发射已关闭");
175
+ } else {
176
+ fireworks.startAutoPlay();
177
+ toast("自动发射已开启");
178
+ }
179
+ });
180
+
181
+ pauseButton.addEventListener("click", () => {
182
+ const paused = fireworks.togglePause();
183
+ pauseButton.setAttribute("aria-pressed", String(paused));
184
+ pauseButton.textContent = paused ? "恢复" : "暂停";
185
+ });
186
+
187
+ clearButton.addEventListener("click", () => {
188
+ fireworks.clearParticles();
189
+ toast("画布已清除");
190
+ });
191
+
192
+ collapseButton.addEventListener("click", () => {
193
+ const collapsed = appShell.classList.toggle("collapsed");
194
+ collapseButton.textContent = collapsed ? "展开" : "收起";
195
+ collapseButton.setAttribute("aria-expanded", String(!collapsed));
196
+ requestAnimationFrame(() => fireworks.resize());
197
+ });
198
+
199
+ document.addEventListener("visibilitychange", () => {
200
+ if (document.hidden) {
201
+ pauseButton.setAttribute("aria-pressed", "true");
202
+ pauseButton.textContent = "恢复";
203
+ }
204
+ });
205
+
206
+ window.fireworks = fireworks;
207
+
208
+ function initializeAdvancedScriptEditor() {
209
+ if (!advancedScriptInput) return;
210
+
211
+ advancedScriptInput.value = defaultAdvancedScript;
212
+ initializeAdvancedScriptPresets();
213
+
214
+ applyAdvancedScriptButton?.addEventListener("click", () => {
215
+ const creator = compileAdvancedScript(advancedScriptInput.value);
216
+ if (creator) {
217
+ fireworks.setParticleCreator(creator);
218
+ toast("高级脚本已应用");
219
+ }
220
+ });
221
+
222
+ resetAdvancedScriptButton?.addEventListener("click", () => {
223
+ advancedScriptInput.value = defaultAdvancedScript;
224
+ if (advancedScriptPresetSelect) {
225
+ advancedScriptPresetSelect.value = "default";
226
+ }
227
+ const creator = compileAdvancedScript(defaultAdvancedScript);
228
+ fireworks.setParticleCreator(creator);
229
+ toast("已恢复高级脚本示例");
230
+ });
231
+
232
+ advancedScriptInput.addEventListener("input", () => {
233
+ window.clearTimeout(scriptCompileTimer);
234
+ scriptCompileTimer = window.setTimeout(() => {
235
+ const creator = compileAdvancedScript(advancedScriptInput.value, false);
236
+ if (creator) {
237
+ fireworks.setParticleCreator(creator);
238
+ }
239
+ }, 500);
240
+ });
241
+ }
242
+
243
+ function initializeAdvancedScriptPresets() {
244
+ if (!advancedScriptPresetSelect) return;
245
+
246
+ advancedScriptPresetSelect.innerHTML = "";
247
+ for (const [value, label] of advancedScriptPresets) {
248
+ const option = document.createElement("option");
249
+ option.value = value;
250
+ option.textContent = label;
251
+ advancedScriptPresetSelect.append(option);
252
+ }
253
+
254
+ advancedScriptPresetSelect.value = "default";
255
+ advancedScriptPresetSelect.addEventListener("change", () => {
256
+ const preset = advancedScriptPresets.find(([value]) => value === advancedScriptPresetSelect.value);
257
+ if (!preset) return;
258
+ advancedScriptInput.value = preset[2];
259
+ const creator = compileAdvancedScript(preset[2]);
260
+ if (creator) {
261
+ fireworks.setParticleCreator(creator);
262
+ toast(`已加载${preset[1]}`);
263
+ }
264
+ });
265
+ }
266
+
267
+ function compileAdvancedScript(source, showToast = true) {
268
+ try {
269
+ const creatorFactory = new Function(
270
+ `"use strict";
271
+ ${source}
272
+ return createParticles;`
273
+ );
274
+ const creator = creatorFactory();
275
+ if (typeof creator !== "function") {
276
+ throw new Error("脚本必须声明 createParticles 函数");
277
+ }
278
+ const wrappedCreator = (canvasData) => {
279
+ const result = creator(canvasData);
280
+ if (Array.isArray(result)) return result;
281
+ if (result && Array.isArray(result.particles)) return result;
282
+ return [];
283
+ };
284
+ setScriptStatus("脚本已生效;开启高级模式后将使用这段脚本生成粒子。", "ok");
285
+ return wrappedCreator;
286
+ } catch (error) {
287
+ setScriptStatus(`脚本错误:${error.message}`, "error");
288
+ if (showToast) toast("高级脚本存在语法错误");
289
+ return null;
290
+ }
291
+ }
292
+
293
+ function setScriptStatus(message, type = "") {
294
+ if (!advancedScriptStatus) return;
295
+ advancedScriptStatus.textContent = message;
296
+ advancedScriptStatus.classList.toggle("ok", type === "ok");
297
+ advancedScriptStatus.classList.toggle("error", type === "error");
298
+ }
@@ -0,0 +1,100 @@
1
+ export class BackgroundManager {
2
+ constructor() {
3
+ this.image = null;
4
+ this.imageUrl = "";
5
+ this.stars = [];
6
+ }
7
+
8
+ resize(width, height) {
9
+ const count = Math.round(Math.min(260, Math.max(90, (width * height) / 9000)));
10
+ this.stars = Array.from({ length: count }, () => ({
11
+ x: Math.random() * width,
12
+ y: Math.random() * height,
13
+ r: Math.random() * 1.4 + 0.25,
14
+ a: Math.random() * 0.65 + 0.18
15
+ }));
16
+ }
17
+
18
+ setImage(file, onDone, onError) {
19
+ if (!file) return;
20
+ if (file.size > 2 * 1024 * 1024) {
21
+ onError?.("背景图片不能超过 2MB");
22
+ return;
23
+ }
24
+
25
+ if (this.imageUrl) URL.revokeObjectURL(this.imageUrl);
26
+ this.imageUrl = URL.createObjectURL(file);
27
+ const image = new Image();
28
+ image.onload = () => {
29
+ this.image = image;
30
+ onDone?.();
31
+ };
32
+ image.onerror = () => {
33
+ this.image = null;
34
+ onError?.("背景图片读取失败");
35
+ };
36
+ image.src = this.imageUrl;
37
+ }
38
+
39
+ draw(ctx, width, height, config) {
40
+ if (config.backgroundType === "image" && this.image) {
41
+ this.drawImage(ctx, width, height, config.backgroundImageMode);
42
+ return;
43
+ }
44
+
45
+ if (config.backgroundType === "linear") {
46
+ const gradient = ctx.createLinearGradient(0, 0, width, height);
47
+ gradient.addColorStop(0, config.backgroundColor);
48
+ gradient.addColorStop(1, config.backgroundColor2);
49
+ ctx.fillStyle = gradient;
50
+ ctx.fillRect(0, 0, width, height);
51
+ return;
52
+ }
53
+
54
+ if (config.backgroundType === "radial" || config.backgroundType === "starfield") {
55
+ const gradient = ctx.createRadialGradient(width * 0.5, height * 0.35, 0, width * 0.5, height * 0.5, Math.max(width, height));
56
+ gradient.addColorStop(0, config.backgroundColor2);
57
+ gradient.addColorStop(1, config.backgroundColor);
58
+ ctx.fillStyle = gradient;
59
+ ctx.fillRect(0, 0, width, height);
60
+ if (config.backgroundType === "starfield") this.drawStars(ctx);
61
+ return;
62
+ }
63
+
64
+ ctx.fillStyle = config.backgroundColor;
65
+ ctx.fillRect(0, 0, width, height);
66
+ }
67
+
68
+ drawStars(ctx) {
69
+ ctx.save();
70
+ for (const star of this.stars) {
71
+ ctx.fillStyle = `rgba(255, 255, 255, ${star.a})`;
72
+ ctx.beginPath();
73
+ ctx.arc(star.x, star.y, star.r, 0, Math.PI * 2);
74
+ ctx.fill();
75
+ }
76
+ ctx.restore();
77
+ }
78
+
79
+ drawImage(ctx, width, height, mode) {
80
+ if (mode === "tile") {
81
+ const pattern = ctx.createPattern(this.image, "repeat");
82
+ ctx.fillStyle = pattern;
83
+ ctx.fillRect(0, 0, width, height);
84
+ return;
85
+ }
86
+
87
+ const scale =
88
+ mode === "contain"
89
+ ? Math.min(width / this.image.width, height / this.image.height)
90
+ : Math.max(width / this.image.width, height / this.image.height);
91
+ const drawWidth = this.image.width * scale;
92
+ const drawHeight = this.image.height * scale;
93
+ const x = (width - drawWidth) / 2;
94
+ const y = (height - drawHeight) / 2;
95
+
96
+ ctx.fillStyle = "#050711";
97
+ ctx.fillRect(0, 0, width, height);
98
+ ctx.drawImage(this.image, x, y, drawWidth, drawHeight);
99
+ }
100
+ }