@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,205 @@
|
|
|
1
|
+
import { controlsSchema } from "../config/defaults.js";
|
|
2
|
+
|
|
3
|
+
export class ControlPanel {
|
|
4
|
+
constructor(container, config, eventBus, backgroundManager, toast) {
|
|
5
|
+
this.container = container;
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.eventBus = eventBus;
|
|
8
|
+
this.backgroundManager = backgroundManager;
|
|
9
|
+
this.toast = toast;
|
|
10
|
+
this.inputs = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
this.container.innerHTML = "";
|
|
15
|
+
|
|
16
|
+
for (const section of controlsSchema) {
|
|
17
|
+
const details = document.createElement("details");
|
|
18
|
+
details.className = "control-section";
|
|
19
|
+
details.open = true;
|
|
20
|
+
|
|
21
|
+
const summary = document.createElement("summary");
|
|
22
|
+
summary.textContent = section.title;
|
|
23
|
+
details.append(summary);
|
|
24
|
+
|
|
25
|
+
const body = document.createElement("div");
|
|
26
|
+
body.className = "section-body";
|
|
27
|
+
|
|
28
|
+
for (const control of section.controls) {
|
|
29
|
+
body.append(this.createControl(control));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
details.append(body);
|
|
33
|
+
this.container.append(details);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
updateValues(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
for (const [key, inputSet] of this.inputs) {
|
|
40
|
+
if (!(key in config)) continue;
|
|
41
|
+
for (const input of inputSet) {
|
|
42
|
+
if (input.type === "checkbox") input.checked = Boolean(config[key]);
|
|
43
|
+
else if (input.getAttribute?.("aria-pressed") !== null) {
|
|
44
|
+
input.setAttribute("aria-pressed", String(Boolean(config[key])));
|
|
45
|
+
}
|
|
46
|
+
else input.value = config[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createControl(control) {
|
|
52
|
+
const row = document.createElement("div");
|
|
53
|
+
row.className = "control-row";
|
|
54
|
+
const label = document.createElement("label");
|
|
55
|
+
label.textContent = control.label;
|
|
56
|
+
label.htmlFor = `control-${control.key}`;
|
|
57
|
+
row.append(label);
|
|
58
|
+
|
|
59
|
+
if (control.type === "range") {
|
|
60
|
+
row.append(this.createRange(control));
|
|
61
|
+
} else if (control.type === "boolean") {
|
|
62
|
+
row.append(this.createSwitch(control));
|
|
63
|
+
} else if (control.type === "select") {
|
|
64
|
+
row.append(this.createSelect(control));
|
|
65
|
+
} else if (control.type === "color") {
|
|
66
|
+
row.append(this.createColor(control));
|
|
67
|
+
} else if (control.type === "file") {
|
|
68
|
+
row.append(this.createFile(control));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (control.hint) {
|
|
72
|
+
const hint = document.createElement("p");
|
|
73
|
+
hint.className = "control-hint";
|
|
74
|
+
hint.textContent = control.hint;
|
|
75
|
+
row.append(hint);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return row;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createRange(control) {
|
|
82
|
+
const wrap = document.createElement("div");
|
|
83
|
+
wrap.className = "range-control";
|
|
84
|
+
const range = document.createElement("input");
|
|
85
|
+
range.id = `control-${control.key}`;
|
|
86
|
+
range.type = "range";
|
|
87
|
+
range.min = control.min;
|
|
88
|
+
range.max = control.max;
|
|
89
|
+
range.step = control.step;
|
|
90
|
+
range.value = this.config[control.key];
|
|
91
|
+
|
|
92
|
+
const number = document.createElement("input");
|
|
93
|
+
number.type = "number";
|
|
94
|
+
number.min = control.min;
|
|
95
|
+
number.max = control.max;
|
|
96
|
+
number.step = control.step;
|
|
97
|
+
number.value = this.config[control.key];
|
|
98
|
+
|
|
99
|
+
const update = (raw) => {
|
|
100
|
+
const value = Number(raw);
|
|
101
|
+
if (!Number.isFinite(value)) return;
|
|
102
|
+
const clamped = Math.min(control.max, Math.max(control.min, value));
|
|
103
|
+
range.value = clamped;
|
|
104
|
+
number.value = clamped;
|
|
105
|
+
this.setValue(control.key, clamped);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
range.addEventListener("input", () => update(range.value));
|
|
109
|
+
number.addEventListener("input", () => update(number.value));
|
|
110
|
+
this.register(control.key, range, number);
|
|
111
|
+
wrap.append(range, number);
|
|
112
|
+
return wrap;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
createSwitch(control) {
|
|
116
|
+
const button = document.createElement("button");
|
|
117
|
+
button.id = `control-${control.key}`;
|
|
118
|
+
button.className = "switch-control";
|
|
119
|
+
button.type = "button";
|
|
120
|
+
button.setAttribute("aria-label", control.label);
|
|
121
|
+
button.setAttribute("aria-pressed", String(Boolean(this.config[control.key])));
|
|
122
|
+
button.addEventListener("click", () => {
|
|
123
|
+
const value = button.getAttribute("aria-pressed") !== "true";
|
|
124
|
+
button.setAttribute("aria-pressed", String(value));
|
|
125
|
+
this.setValue(control.key, value);
|
|
126
|
+
});
|
|
127
|
+
this.register(control.key, button);
|
|
128
|
+
return button;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
createSelect(control) {
|
|
132
|
+
const select = document.createElement("select");
|
|
133
|
+
select.id = `control-${control.key}`;
|
|
134
|
+
for (const [value, label] of control.options) {
|
|
135
|
+
const option = document.createElement("option");
|
|
136
|
+
option.value = value;
|
|
137
|
+
option.textContent = label;
|
|
138
|
+
select.append(option);
|
|
139
|
+
}
|
|
140
|
+
select.value = this.config[control.key];
|
|
141
|
+
select.addEventListener("change", () => this.setValue(control.key, select.value));
|
|
142
|
+
this.register(control.key, select);
|
|
143
|
+
return select;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
createColor(control) {
|
|
147
|
+
const wrap = document.createElement("div");
|
|
148
|
+
wrap.className = "swatch-grid";
|
|
149
|
+
const color = document.createElement("input");
|
|
150
|
+
color.id = `control-${control.key}`;
|
|
151
|
+
color.type = "color";
|
|
152
|
+
color.value = this.config[control.key];
|
|
153
|
+
|
|
154
|
+
const text = document.createElement("input");
|
|
155
|
+
text.type = "text";
|
|
156
|
+
text.value = this.config[control.key];
|
|
157
|
+
text.maxLength = 7;
|
|
158
|
+
|
|
159
|
+
const update = (value) => {
|
|
160
|
+
if (!/^#[0-9a-f]{6}$/i.test(value)) return;
|
|
161
|
+
color.value = value;
|
|
162
|
+
text.value = value;
|
|
163
|
+
this.setValue(control.key, value);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
color.addEventListener("input", () => update(color.value));
|
|
167
|
+
text.addEventListener("change", () => update(text.value));
|
|
168
|
+
this.register(control.key, color, text);
|
|
169
|
+
wrap.append(color, text);
|
|
170
|
+
return wrap;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
createFile(control) {
|
|
174
|
+
const label = document.createElement("label");
|
|
175
|
+
label.className = "file-button";
|
|
176
|
+
label.textContent = "选择图片";
|
|
177
|
+
const input = document.createElement("input");
|
|
178
|
+
input.id = `control-${control.key}`;
|
|
179
|
+
input.type = "file";
|
|
180
|
+
input.accept = "image/png,image/jpeg,image/webp";
|
|
181
|
+
input.addEventListener("change", () => {
|
|
182
|
+
const file = input.files?.[0];
|
|
183
|
+
this.backgroundManager.setImage(
|
|
184
|
+
file,
|
|
185
|
+
() => {
|
|
186
|
+
this.setValue("backgroundType", "image");
|
|
187
|
+
this.toast("背景图片已应用");
|
|
188
|
+
},
|
|
189
|
+
this.toast
|
|
190
|
+
);
|
|
191
|
+
input.value = "";
|
|
192
|
+
});
|
|
193
|
+
label.append(input);
|
|
194
|
+
return label;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
register(key, ...inputs) {
|
|
198
|
+
this.inputs.set(key, inputs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setValue(key, value) {
|
|
202
|
+
this.config[key] = value;
|
|
203
|
+
this.eventBus.emit("config:change", { key, value, config: this.config });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class PerformancePanel {
|
|
2
|
+
constructor(element) {
|
|
3
|
+
this.element = element;
|
|
4
|
+
this.frames = 0;
|
|
5
|
+
this.elapsed = 0;
|
|
6
|
+
this.fps = 0;
|
|
7
|
+
this.lowFpsTime = 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
update(deltaMs) {
|
|
11
|
+
this.frames += 1;
|
|
12
|
+
this.elapsed += deltaMs;
|
|
13
|
+
if (this.elapsed >= 500) {
|
|
14
|
+
this.fps = Math.round((this.frames * 1000) / this.elapsed);
|
|
15
|
+
this.element.textContent = `FPS ${this.fps}`;
|
|
16
|
+
this.lowFpsTime = this.fps < 45 ? this.lowFpsTime + this.elapsed : 0;
|
|
17
|
+
this.frames = 0;
|
|
18
|
+
this.elapsed = 0;
|
|
19
|
+
return this.lowFpsTime > 2200;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { builtInPresets, defaultConfig, sanitizeConfig } from "../config/defaults.js";
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = "spark-fireworks-presets";
|
|
4
|
+
|
|
5
|
+
export class PresetManager {
|
|
6
|
+
constructor(select, getConfig, applyConfig, toast) {
|
|
7
|
+
this.select = select;
|
|
8
|
+
this.getConfig = getConfig;
|
|
9
|
+
this.applyConfig = applyConfig;
|
|
10
|
+
this.toast = toast;
|
|
11
|
+
this.localPresets = this.loadLocal();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
renderOptions() {
|
|
15
|
+
this.select.innerHTML = "";
|
|
16
|
+
this.appendOption("", "选择预设");
|
|
17
|
+
this.appendOption("default", "默认配置");
|
|
18
|
+
|
|
19
|
+
for (const [id, preset] of Object.entries(builtInPresets)) {
|
|
20
|
+
this.appendOption(`builtin:${id}`, preset.name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const [id, preset] of Object.entries(this.localPresets)) {
|
|
24
|
+
this.appendOption(`local:${id}`, `本地:${preset.name}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
bind() {
|
|
29
|
+
this.select.addEventListener("change", () => {
|
|
30
|
+
const value = this.select.value;
|
|
31
|
+
if (!value) return;
|
|
32
|
+
if (value === "default") {
|
|
33
|
+
this.applyConfig({ ...defaultConfig }, value);
|
|
34
|
+
this.toast("已加载默认配置");
|
|
35
|
+
} else if (value.startsWith("builtin:")) {
|
|
36
|
+
const id = value.replace("builtin:", "");
|
|
37
|
+
this.applyConfig({ ...defaultConfig, ...builtInPresets[id].config }, value);
|
|
38
|
+
this.toast(`已加载${builtInPresets[id].name}`);
|
|
39
|
+
} else if (value.startsWith("local:")) {
|
|
40
|
+
const id = value.replace("local:", "");
|
|
41
|
+
this.applyConfig({ ...defaultConfig, ...this.localPresets[id].config }, value);
|
|
42
|
+
this.toast(`已加载${this.localPresets[id].name}`);
|
|
43
|
+
}
|
|
44
|
+
this.select.value = "";
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
saveCurrent() {
|
|
49
|
+
const name = window.prompt("预设名称", `我的烟花 ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}`);
|
|
50
|
+
if (!name) return;
|
|
51
|
+
const id = crypto.randomUUID?.() ?? String(Date.now());
|
|
52
|
+
this.localPresets[id] = {
|
|
53
|
+
name: name.slice(0, 28),
|
|
54
|
+
config: sanitizeConfig(this.getConfig())
|
|
55
|
+
};
|
|
56
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.localPresets));
|
|
57
|
+
this.renderOptions();
|
|
58
|
+
this.toast("预设已保存到本地");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
exportConfig() {
|
|
62
|
+
const blob = new Blob([JSON.stringify(sanitizeConfig(this.getConfig()), null, 2)], {
|
|
63
|
+
type: "application/json"
|
|
64
|
+
});
|
|
65
|
+
const url = URL.createObjectURL(blob);
|
|
66
|
+
const link = document.createElement("a");
|
|
67
|
+
link.href = url;
|
|
68
|
+
link.download = `spark-fireworks-${Date.now()}.json`;
|
|
69
|
+
link.click();
|
|
70
|
+
URL.revokeObjectURL(url);
|
|
71
|
+
this.toast("配置已导出");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async importConfig(file) {
|
|
75
|
+
if (!file) return;
|
|
76
|
+
try {
|
|
77
|
+
const text = await file.text();
|
|
78
|
+
const parsed = JSON.parse(text);
|
|
79
|
+
this.applyConfig(sanitizeConfig(parsed));
|
|
80
|
+
this.toast("配置已导入");
|
|
81
|
+
} catch {
|
|
82
|
+
this.toast("导入失败:JSON 格式无效");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
shareConfig() {
|
|
87
|
+
const json = JSON.stringify(sanitizeConfig(this.getConfig()));
|
|
88
|
+
const encoded = btoa(unescape(encodeURIComponent(json)));
|
|
89
|
+
const url = `${location.origin}${location.pathname}#config=${encoded}`;
|
|
90
|
+
navigator.clipboard
|
|
91
|
+
?.writeText(url)
|
|
92
|
+
.then(() => this.toast("分享链接已复制"))
|
|
93
|
+
.catch(() => {
|
|
94
|
+
location.hash = `config=${encoded}`;
|
|
95
|
+
this.toast("分享链接已写入地址栏");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
loadFromHash() {
|
|
100
|
+
const match = location.hash.match(/config=([^&]+)/);
|
|
101
|
+
if (!match) return false;
|
|
102
|
+
try {
|
|
103
|
+
const json = decodeURIComponent(escape(atob(match[1])));
|
|
104
|
+
this.applyConfig(sanitizeConfig(JSON.parse(json)));
|
|
105
|
+
this.toast("已从分享链接加载配置");
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
this.toast("分享链接配置无效");
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
appendOption(value, label) {
|
|
114
|
+
const option = document.createElement("option");
|
|
115
|
+
option.value = value;
|
|
116
|
+
option.textContent = label;
|
|
117
|
+
this.select.append(option);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
loadLocal() {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
|
123
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
124
|
+
} catch {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { clamp, lerp, randomInt } from "./math.js";
|
|
2
|
+
|
|
3
|
+
export function hexToRgb(hex) {
|
|
4
|
+
const normalized = hex.replace("#", "");
|
|
5
|
+
const int = Number.parseInt(normalized, 16);
|
|
6
|
+
return {
|
|
7
|
+
r: (int >> 16) & 255,
|
|
8
|
+
g: (int >> 8) & 255,
|
|
9
|
+
b: int & 255
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rgbToCss({ r, g, b }, alpha = 1) {
|
|
14
|
+
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${clamp(alpha, 0, 1)})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function mixColor(start, end, t) {
|
|
18
|
+
const a = typeof start === "string" ? hexToRgb(start) : start;
|
|
19
|
+
const b = typeof end === "string" ? hexToRgb(end) : end;
|
|
20
|
+
return {
|
|
21
|
+
r: lerp(a.r, b.r, t),
|
|
22
|
+
g: lerp(a.g, b.g, t),
|
|
23
|
+
b: lerp(a.b, b.b, t)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function randomVividColor() {
|
|
28
|
+
const hue = randomInt(0, 360);
|
|
29
|
+
return hslToHex(hue, randomInt(72, 96), randomInt(54, 68));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hslToHex(h, s, l) {
|
|
33
|
+
s /= 100;
|
|
34
|
+
l /= 100;
|
|
35
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
36
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
37
|
+
const m = l - c / 2;
|
|
38
|
+
let r = 0;
|
|
39
|
+
let g = 0;
|
|
40
|
+
let b = 0;
|
|
41
|
+
|
|
42
|
+
if (h < 60) [r, g, b] = [c, x, 0];
|
|
43
|
+
else if (h < 120) [r, g, b] = [x, c, 0];
|
|
44
|
+
else if (h < 180) [r, g, b] = [0, c, x];
|
|
45
|
+
else if (h < 240) [r, g, b] = [0, x, c];
|
|
46
|
+
else if (h < 300) [r, g, b] = [x, 0, c];
|
|
47
|
+
else [r, g, b] = [c, 0, x];
|
|
48
|
+
|
|
49
|
+
return `#${toHex((r + m) * 255)}${toHex((g + m) * 255)}${toHex((b + m) * 255)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toHex(value) {
|
|
53
|
+
return Math.round(value).toString(16).padStart(2, "0");
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class EventBus {
|
|
2
|
+
listeners = new Map();
|
|
3
|
+
|
|
4
|
+
on(event, callback) {
|
|
5
|
+
if (!this.listeners.has(event)) {
|
|
6
|
+
this.listeners.set(event, new Set());
|
|
7
|
+
}
|
|
8
|
+
this.listeners.get(event).add(callback);
|
|
9
|
+
return () => this.off(event, callback);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
off(event, callback) {
|
|
13
|
+
this.listeners.get(event)?.delete(callback);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
emit(event, payload) {
|
|
17
|
+
this.listeners.get(event)?.forEach((callback) => callback(payload));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const eventBus = new EventBus();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function clamp(value, min, max) {
|
|
2
|
+
return Math.min(max, Math.max(min, value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function lerp(a, b, t) {
|
|
6
|
+
return a + (b - a) * t;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function random(min, max) {
|
|
10
|
+
return Math.random() * (max - min) + min;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function randomInt(min, max) {
|
|
14
|
+
return Math.floor(random(min, max + 1));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pick(array) {
|
|
18
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalize(x, y) {
|
|
22
|
+
const length = Math.hypot(x, y) || 1;
|
|
23
|
+
return { x: x / length, y: y / length };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mapRange(value, inMin, inMax, outMin, outMax) {
|
|
27
|
+
const t = (value - inMin) / (inMax - inMin || 1);
|
|
28
|
+
return lerp(outMin, outMax, t);
|
|
29
|
+
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
rollupOptions: {
|
|
7
|
+
input: {
|
|
8
|
+
index: resolve(__dirname, "index.html"),
|
|
9
|
+
notFound: resolve(__dirname, "404.html"),
|
|
10
|
+
demo: resolve(__dirname, "demo.html"),
|
|
11
|
+
guide: resolve(__dirname, "guide.html"),
|
|
12
|
+
advancedGuide: resolve(__dirname, "advanced-guide.html"),
|
|
13
|
+
apiDownload: resolve(__dirname, "api-download.html")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
build: {
|
|
5
|
+
lib: {
|
|
6
|
+
entry: "src/app.js",
|
|
7
|
+
name: "SparkFireworks",
|
|
8
|
+
fileName: "spark-fireworks",
|
|
9
|
+
formats: ["es", "umd"]
|
|
10
|
+
},
|
|
11
|
+
outDir: "dist-lib",
|
|
12
|
+
emptyOutDir: true,
|
|
13
|
+
rollupOptions: {
|
|
14
|
+
output: {
|
|
15
|
+
exports: "named"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|