sa2kit 1.6.33 → 1.6.35

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.
Files changed (47) hide show
  1. package/dist/calendar/index.js +11 -8
  2. package/dist/calendar/index.js.map +1 -1
  3. package/dist/calendar/index.mjs +4 -1
  4. package/dist/calendar/index.mjs.map +1 -1
  5. package/dist/chunk-EGJPS7OL.mjs +98 -0
  6. package/dist/chunk-EGJPS7OL.mjs.map +1 -0
  7. package/dist/{chunk-EI27JKND.mjs → chunk-GMIUSZXC.mjs} +2 -2
  8. package/dist/{chunk-EI27JKND.mjs.map → chunk-GMIUSZXC.mjs.map} +1 -1
  9. package/dist/chunk-HHVDOIPV.js +105 -0
  10. package/dist/chunk-HHVDOIPV.js.map +1 -0
  11. package/dist/chunk-PMTY2AI4.js +1110 -0
  12. package/dist/chunk-PMTY2AI4.js.map +1 -0
  13. package/dist/{chunk-XGBE4SUV.js → chunk-SCDDMIF6.js} +2 -2
  14. package/dist/{chunk-XGBE4SUV.js.map → chunk-SCDDMIF6.js.map} +1 -1
  15. package/dist/chunk-SR4JFEHW.mjs +1071 -0
  16. package/dist/chunk-SR4JFEHW.mjs.map +1 -0
  17. package/dist/index.d.mts +26 -1
  18. package/dist/index.d.ts +26 -1
  19. package/dist/index.js +179 -109
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +4 -2
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/mikuFireworks3D/index.d.mts +174 -0
  24. package/dist/mikuFireworks3D/index.d.ts +174 -0
  25. package/dist/mikuFireworks3D/index.js +69 -0
  26. package/dist/mikuFireworks3D/index.js.map +1 -0
  27. package/dist/mikuFireworks3D/index.mjs +4 -0
  28. package/dist/mikuFireworks3D/index.mjs.map +1 -0
  29. package/dist/mikuFireworks3D/server/index.d.mts +146 -0
  30. package/dist/mikuFireworks3D/server/index.d.ts +146 -0
  31. package/dist/mikuFireworks3D/server/index.js +338 -0
  32. package/dist/mikuFireworks3D/server/index.js.map +1 -0
  33. package/dist/mikuFireworks3D/server/index.mjs +335 -0
  34. package/dist/mikuFireworks3D/server/index.mjs.map +1 -0
  35. package/dist/mikuFusionGame/index.d.mts +7 -2
  36. package/dist/mikuFusionGame/index.d.ts +7 -2
  37. package/dist/mikuFusionGame/index.js +141 -10
  38. package/dist/mikuFusionGame/index.js.map +1 -1
  39. package/dist/mikuFusionGame/index.mjs +141 -10
  40. package/dist/mikuFusionGame/index.mjs.map +1 -1
  41. package/dist/mmd/index.js +1 -1
  42. package/dist/mmd/index.mjs +2 -2
  43. package/dist/types-Cgk9zWhO.d.mts +70 -0
  44. package/dist/types-Cgk9zWhO.d.ts +70 -0
  45. package/dist/universalFile/server/index.js +5 -5
  46. package/dist/universalFile/server/index.mjs +1 -1
  47. package/package.json +11 -1
@@ -0,0 +1,1071 @@
1
+ import React3, { useState, useRef, useCallback, useMemo, useEffect } from 'react';
2
+ import * as THREE2 from 'three';
3
+
4
+ // src/mikuFireworks3D/components/MikuFireworks3D.tsx
5
+ function DanmakuPanel({ onSend }) {
6
+ const [text, setText] = useState("");
7
+ const emit = () => {
8
+ const value = text.trim();
9
+ if (!value) {
10
+ return;
11
+ }
12
+ onSend(value);
13
+ setText("");
14
+ };
15
+ return /* @__PURE__ */ React3.createElement("div", { className: "rounded-xl border border-slate-600/40 bg-slate-900/70 p-3 text-slate-100 backdrop-blur-sm" }, /* @__PURE__ */ React3.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React3.createElement(
16
+ "input",
17
+ {
18
+ type: "text",
19
+ value: text,
20
+ onChange: (event) => setText(event.target.value),
21
+ onKeyDown: (event) => {
22
+ if (event.key === "Enter") {
23
+ emit();
24
+ }
25
+ },
26
+ placeholder: "\u53D1\u9001\u5F39\u5E55\uFF08\u652F\u6301 /miku /avatar /normal\uFF09",
27
+ className: "flex-1 rounded-md border border-slate-600 bg-slate-950 px-2.5 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
28
+ }
29
+ ), /* @__PURE__ */ React3.createElement(
30
+ "button",
31
+ {
32
+ type: "button",
33
+ onClick: emit,
34
+ className: "rounded-md bg-cyan-400 px-3 py-2 text-sm font-medium text-slate-950 hover:bg-cyan-300"
35
+ },
36
+ "\u53D1\u9001"
37
+ )));
38
+ }
39
+ function FireworksCanvas({ canvasRef }) {
40
+ return /* @__PURE__ */ React3.createElement(
41
+ "canvas",
42
+ {
43
+ ref: canvasRef,
44
+ className: "absolute inset-0 h-full w-full",
45
+ style: {
46
+ background: "radial-gradient(circle at 20% 20%, rgba(57,197,187,0.15) 0%, rgba(6,8,22,1) 45%, rgba(4,6,15,1) 100%)"
47
+ }
48
+ }
49
+ );
50
+ }
51
+
52
+ // src/mikuFireworks3D/constants.ts
53
+ var DEFAULT_MAX_PARTICLES = 5e3;
54
+ var DEFAULT_MAX_ACTIVE_FIREWORKS = 12;
55
+ var FIREWORK_KIND_LABELS = {
56
+ normal: "\u666E\u901A\u70DF\u82B1",
57
+ miku: "MIKU \u4E3B\u9898",
58
+ avatar: "\u5934\u50CF\u70DF\u82B1"
59
+ };
60
+ var MIKU_PALETTE = ["#39c5bb", "#66e3db", "#7ad8ff", "#b0fff8", "#8cf7e0"];
61
+ var NORMAL_PALETTE = ["#ffe066", "#ff6b6b", "#4dabf7", "#c77dff", "#69db7c"];
62
+ var DANMAKU_MAX_LENGTH = 32;
63
+ var DANMAKU_TRACK_COUNT = 8;
64
+
65
+ // src/mikuFireworks3D/components/FireworksControlPanel.tsx
66
+ function FireworksControlPanel({
67
+ selectedKind,
68
+ onKindChange,
69
+ autoLaunchOnDanmaku,
70
+ onAutoLaunchChange,
71
+ avatarUrl,
72
+ onAvatarUrlChange,
73
+ onLaunch,
74
+ fps,
75
+ realtimeConnected,
76
+ onlineCount
77
+ }) {
78
+ return /* @__PURE__ */ React3.createElement("div", { className: "rounded-xl border border-slate-600/40 bg-slate-900/70 p-3 text-slate-100 backdrop-blur-sm" }, /* @__PURE__ */ React3.createElement("div", { className: "mb-3 flex flex-wrap items-center gap-2" }, Object.keys(FIREWORK_KIND_LABELS).map((kind) => {
79
+ const active = kind === selectedKind;
80
+ return /* @__PURE__ */ React3.createElement(
81
+ "button",
82
+ {
83
+ key: kind,
84
+ type: "button",
85
+ onClick: () => onKindChange(kind),
86
+ className: `rounded-md px-3 py-1.5 text-sm transition ${active ? "bg-cyan-500 text-slate-950" : "bg-slate-700/70 hover:bg-slate-600/80"}`
87
+ },
88
+ FIREWORK_KIND_LABELS[kind]
89
+ );
90
+ }), /* @__PURE__ */ React3.createElement(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ onClick: onLaunch,
95
+ className: "ml-auto rounded-md bg-emerald-400 px-3 py-1.5 text-sm font-medium text-slate-900 hover:bg-emerald-300"
96
+ },
97
+ "\u53D1\u5C04\u70DF\u82B1"
98
+ )), /* @__PURE__ */ React3.createElement("div", { className: "grid gap-2 md:grid-cols-2" }, /* @__PURE__ */ React3.createElement("label", { className: "flex items-center gap-2 text-sm" }, /* @__PURE__ */ React3.createElement(
99
+ "input",
100
+ {
101
+ type: "checkbox",
102
+ checked: autoLaunchOnDanmaku,
103
+ onChange: (event) => onAutoLaunchChange(event.target.checked)
104
+ }
105
+ ), "\u53D1\u9001\u5F39\u5E55\u540E\u81EA\u52A8\u653E\u70DF\u82B1"), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-slate-300" }, "FPS: ", fps), typeof realtimeConnected === "boolean" ? /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-slate-300" }, "\u5B9E\u65F6\u72B6\u6001: ", realtimeConnected ? "\u5DF2\u8FDE\u63A5" : "\u672A\u8FDE\u63A5", typeof onlineCount === "number" ? ` \xB7 \u5728\u7EBF ${onlineCount}` : "") : null), selectedKind === "avatar" ? /* @__PURE__ */ React3.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React3.createElement(
106
+ "input",
107
+ {
108
+ type: "url",
109
+ value: avatarUrl,
110
+ onChange: (event) => onAvatarUrlChange(event.target.value),
111
+ placeholder: "\u5934\u50CF\u56FE\u7247 URL\uFF08\u7528\u4E8E\u5934\u50CF\u70DF\u82B1\uFF09",
112
+ className: "w-full rounded-md border border-slate-600 bg-slate-950 px-2.5 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
113
+ }
114
+ )) : null);
115
+ }
116
+ function useDanmakuController(options) {
117
+ const [items, setItems] = useState([]);
118
+ const cursorRef = useRef(0);
119
+ const removeItem = useCallback((id) => {
120
+ setItems((prev) => prev.filter((item) => item.id !== id));
121
+ }, []);
122
+ const addIncoming = useCallback((message) => {
123
+ setItems((prev) => {
124
+ const track = cursorRef.current % DANMAKU_TRACK_COUNT;
125
+ const item = {
126
+ ...message,
127
+ track,
128
+ durationMs: 8e3 + Math.floor(Math.random() * 2800)
129
+ };
130
+ return [...prev.slice(-40), item];
131
+ });
132
+ cursorRef.current += 1;
133
+ }, []);
134
+ const send = useCallback(
135
+ (text, color, sendOptions) => {
136
+ const trimmed = text.trim();
137
+ if (!trimmed) {
138
+ return null;
139
+ }
140
+ const { content, launchKind } = parseCommand(trimmed);
141
+ const safeText = content.slice(0, DANMAKU_MAX_LENGTH);
142
+ if (!safeText) {
143
+ return null;
144
+ }
145
+ const message = {
146
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
147
+ text: safeText,
148
+ color,
149
+ timestamp: Date.now()
150
+ };
151
+ const optimistic = sendOptions?.optimistic ?? true;
152
+ if (optimistic) {
153
+ addIncoming(message);
154
+ }
155
+ options?.onSend?.(message);
156
+ return {
157
+ message,
158
+ launchKind
159
+ };
160
+ },
161
+ [addIncoming, options]
162
+ );
163
+ return useMemo(
164
+ () => ({
165
+ items,
166
+ send,
167
+ addIncoming,
168
+ removeItem
169
+ }),
170
+ [addIncoming, items, removeItem, send]
171
+ );
172
+ }
173
+ function parseCommand(text) {
174
+ if (text.startsWith("/miku ")) {
175
+ return { launchKind: "miku", content: text.replace("/miku ", "").trim() };
176
+ }
177
+ if (text === "/miku") {
178
+ return { launchKind: "miku", content: "MIKU!" };
179
+ }
180
+ if (text.startsWith("/avatar ")) {
181
+ return { launchKind: "avatar", content: text.replace("/avatar ", "").trim() };
182
+ }
183
+ if (text === "/avatar") {
184
+ return { launchKind: "avatar", content: "Avatar Firework!" };
185
+ }
186
+ if (text.startsWith("/normal ")) {
187
+ return { launchKind: "normal", content: text.replace("/normal ", "").trim() };
188
+ }
189
+ if (text === "/normal") {
190
+ return { launchKind: "normal", content: "Fireworks!" };
191
+ }
192
+ return { content: text };
193
+ }
194
+ function createCircularSpriteTexture() {
195
+ const size = 64;
196
+ const canvas = document.createElement("canvas");
197
+ canvas.width = size;
198
+ canvas.height = size;
199
+ const ctx = canvas.getContext("2d");
200
+ if (!ctx) {
201
+ return new THREE2.CanvasTexture(canvas);
202
+ }
203
+ const center = size / 2;
204
+ const gradient = ctx.createRadialGradient(center, center, 2, center, center, center);
205
+ gradient.addColorStop(0, "rgba(255,255,255,1)");
206
+ gradient.addColorStop(0.4, "rgba(255,255,255,0.9)");
207
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
208
+ ctx.fillStyle = gradient;
209
+ ctx.fillRect(0, 0, size, size);
210
+ const texture = new THREE2.CanvasTexture(canvas);
211
+ texture.needsUpdate = true;
212
+ return texture;
213
+ }
214
+
215
+ // src/mikuFireworks3D/utils/colorPalettes.ts
216
+ function pickPalette(kind) {
217
+ if (kind === "miku") {
218
+ return MIKU_PALETTE;
219
+ }
220
+ return NORMAL_PALETTE;
221
+ }
222
+ function pickRandomColor(colors) {
223
+ if (colors.length === 0) {
224
+ return "#ffffff";
225
+ }
226
+ const index = Math.floor(Math.random() * colors.length);
227
+ return colors[index] || "#ffffff";
228
+ }
229
+
230
+ // src/mikuFireworks3D/utils/avatarSprite.ts
231
+ async function sampleAvatarPoints(avatarUrl, sampleStep = 4, maxPoints = 500) {
232
+ const image = await loadImage(avatarUrl);
233
+ const size = 64;
234
+ const canvas = document.createElement("canvas");
235
+ canvas.width = size;
236
+ canvas.height = size;
237
+ const ctx = canvas.getContext("2d");
238
+ if (!ctx) {
239
+ return [];
240
+ }
241
+ ctx.clearRect(0, 0, size, size);
242
+ ctx.drawImage(image, 0, 0, size, size);
243
+ const { data } = ctx.getImageData(0, 0, size, size);
244
+ const points = [];
245
+ for (let y = 0; y < size; y += sampleStep) {
246
+ for (let x = 0; x < size; x += sampleStep) {
247
+ const index = (y * size + x) * 4;
248
+ const alpha = data[index + 3] ?? 0;
249
+ if (alpha < 24) {
250
+ continue;
251
+ }
252
+ const r = data[index] ?? 0;
253
+ const g = data[index + 1] ?? 0;
254
+ const b = data[index + 2] ?? 0;
255
+ const brightness = (r + g + b) / (3 * 255);
256
+ points.push({ x: x - size / 2, y: size / 2 - y, brightness });
257
+ if (points.length >= maxPoints) {
258
+ return points;
259
+ }
260
+ }
261
+ }
262
+ return points;
263
+ }
264
+ function loadImage(url) {
265
+ return new Promise((resolve, reject) => {
266
+ const image = new window.Image();
267
+ image.crossOrigin = "anonymous";
268
+ image.onload = () => resolve(image);
269
+ image.onerror = () => reject(new Error("Failed to load avatar image."));
270
+ image.src = url;
271
+ });
272
+ }
273
+
274
+ // src/mikuFireworks3D/engine/patterns/avatar.ts
275
+ async function createAvatarSeeds(avatarUrl, fallbackColor) {
276
+ const points = await sampleAvatarPoints(avatarUrl);
277
+ if (points.length === 0) {
278
+ return [];
279
+ }
280
+ return points.map((point) => {
281
+ const spread = 0.22;
282
+ return {
283
+ x: 0,
284
+ y: 0,
285
+ z: 0,
286
+ vx: point.x * spread + (Math.random() - 0.5) * 2.4,
287
+ vy: point.y * spread + Math.random() * 2.2,
288
+ vz: (Math.random() - 0.5) * 3.5,
289
+ life: 1.1 + point.brightness * 1.6,
290
+ color: fallbackColor
291
+ };
292
+ });
293
+ }
294
+
295
+ // src/mikuFireworks3D/engine/patterns/miku.ts
296
+ function createMikuSeeds(count) {
297
+ const seeds = [];
298
+ for (let i = 0; i < count; i += 1) {
299
+ const ratio = i / Math.max(count - 1, 1);
300
+ const angle = ratio * Math.PI * 2 * 2;
301
+ const radial = 7 + Math.random() * 9;
302
+ const spiralBoost = 3 + Math.random() * 4;
303
+ seeds.push({
304
+ x: 0,
305
+ y: 0,
306
+ z: 0,
307
+ vx: Math.cos(angle) * radial,
308
+ vy: Math.sin(angle * 0.5) * spiralBoost + 7,
309
+ vz: Math.sin(angle) * radial,
310
+ life: 1.2 + Math.random() * 1.5,
311
+ color: pickRandomColor(MIKU_PALETTE)
312
+ });
313
+ }
314
+ return seeds;
315
+ }
316
+
317
+ // src/mikuFireworks3D/engine/patterns/normal.ts
318
+ function createNormalSeeds(count, color) {
319
+ const seeds = [];
320
+ for (let i = 0; i < count; i += 1) {
321
+ const theta = Math.random() * Math.PI * 2;
322
+ const phi = Math.acos(2 * Math.random() - 1);
323
+ const speed = 8 + Math.random() * 12;
324
+ seeds.push({
325
+ x: 0,
326
+ y: 0,
327
+ z: 0,
328
+ vx: speed * Math.sin(phi) * Math.cos(theta),
329
+ vy: speed * Math.cos(phi),
330
+ vz: speed * Math.sin(phi) * Math.sin(theta),
331
+ life: 1 + Math.random() * 1.6,
332
+ color
333
+ });
334
+ }
335
+ return seeds;
336
+ }
337
+
338
+ // src/mikuFireworks3D/engine/emitters.ts
339
+ async function createSeedParticles(options) {
340
+ const palette = pickPalette(options.kind);
341
+ const color = options.color ?? pickRandomColor(palette);
342
+ if (options.kind === "miku") {
343
+ return createMikuSeeds(options.count);
344
+ }
345
+ if (options.kind === "avatar" && options.avatarUrl) {
346
+ const avatarSeeds = await createAvatarSeeds(options.avatarUrl, color);
347
+ if (avatarSeeds.length > 0) {
348
+ return avatarSeeds;
349
+ }
350
+ }
351
+ return createNormalSeeds(options.count, color);
352
+ }
353
+
354
+ // src/mikuFireworks3D/engine/particlePool.ts
355
+ var ParticlePool = class {
356
+ constructor() {
357
+ this.pool = [];
358
+ }
359
+ acquire() {
360
+ const reused = this.pool.pop();
361
+ if (reused) {
362
+ return reused;
363
+ }
364
+ return {
365
+ x: 0,
366
+ y: 0,
367
+ z: 0,
368
+ vx: 0,
369
+ vy: 0,
370
+ vz: 0,
371
+ life: 0,
372
+ maxLife: 1,
373
+ r: 1,
374
+ g: 1,
375
+ b: 1
376
+ };
377
+ }
378
+ release(particle) {
379
+ this.pool.push(particle);
380
+ }
381
+ };
382
+
383
+ // src/mikuFireworks3D/engine/postfx.ts
384
+ function evaluateDegradePolicy(fps) {
385
+ if (fps < 24) {
386
+ return { shouldDegrade: true, recommendedParticleScale: 0.65 };
387
+ }
388
+ if (fps < 34) {
389
+ return { shouldDegrade: true, recommendedParticleScale: 0.82 };
390
+ }
391
+ return { shouldDegrade: false, recommendedParticleScale: 1 };
392
+ }
393
+
394
+ // src/mikuFireworks3D/engine/FireworksEngine.ts
395
+ var FireworksEngine = class {
396
+ constructor(init) {
397
+ this.pool = new ParticlePool();
398
+ this.bursts = [];
399
+ this.animationFrameId = null;
400
+ this.lastTick = 0;
401
+ this.fpsWindow = { frames: 0, elapsed: 0, fps: 60, particleScale: 1 };
402
+ this.resizeObserver = null;
403
+ this.loop = () => {
404
+ const now = window.performance.now();
405
+ const dt = Math.min((now - this.lastTick) / 1e3, 0.05);
406
+ this.lastTick = now;
407
+ this.update(dt);
408
+ this.renderer.render(this.scene, this.camera);
409
+ this.animationFrameId = window.requestAnimationFrame(this.loop);
410
+ };
411
+ this.canvas = init.canvas;
412
+ this.container = init.container;
413
+ this.maxParticles = init.options?.maxParticles ?? DEFAULT_MAX_PARTICLES;
414
+ this.maxActiveFireworks = init.options?.maxActiveFireworks ?? DEFAULT_MAX_ACTIVE_FIREWORKS;
415
+ this.onError = init.options?.onError;
416
+ this.onFpsReport = init.options?.onFpsReport;
417
+ this.scene = new THREE2.Scene();
418
+ this.scene.background = new THREE2.Color("#060816");
419
+ this.camera = new THREE2.PerspectiveCamera(50, 1, 0.1, 1e3);
420
+ this.camera.position.set(0, 0, 45);
421
+ this.renderer = new THREE2.WebGLRenderer({
422
+ canvas: this.canvas,
423
+ alpha: true,
424
+ antialias: true,
425
+ powerPreference: "high-performance"
426
+ });
427
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
428
+ this.spriteTexture = createCircularSpriteTexture();
429
+ this.attachResizeObserver();
430
+ this.resize();
431
+ }
432
+ start() {
433
+ if (this.animationFrameId != null) {
434
+ return;
435
+ }
436
+ this.lastTick = window.performance.now();
437
+ this.loop();
438
+ }
439
+ stop() {
440
+ if (this.animationFrameId != null) {
441
+ window.cancelAnimationFrame(this.animationFrameId);
442
+ this.animationFrameId = null;
443
+ }
444
+ }
445
+ async launch(payload) {
446
+ try {
447
+ this.enforceBurstCap();
448
+ const particleBudget = Math.max(80, Math.floor(280 * this.fpsWindow.particleScale));
449
+ const seeds = await createSeedParticles({
450
+ kind: payload.kind,
451
+ count: particleBudget,
452
+ color: payload.color,
453
+ avatarUrl: payload.avatarUrl
454
+ });
455
+ if (seeds.length === 0) {
456
+ return;
457
+ }
458
+ const launchPosition = payload.position ?? {
459
+ x: (Math.random() - 0.5) * 18,
460
+ y: -4 + Math.random() * 12,
461
+ z: (Math.random() - 0.5) * 4
462
+ };
463
+ const particles = [];
464
+ const positions = new Float32Array(seeds.length * 3);
465
+ const colors = new Float32Array(seeds.length * 3);
466
+ const colorHelper = new THREE2.Color();
467
+ for (let i = 0; i < seeds.length; i += 1) {
468
+ const seed = seeds[i];
469
+ if (!seed) {
470
+ continue;
471
+ }
472
+ const particle = this.pool.acquire();
473
+ particle.x = launchPosition.x + seed.x;
474
+ particle.y = launchPosition.y + seed.y;
475
+ particle.z = launchPosition.z + seed.z;
476
+ particle.vx = seed.vx;
477
+ particle.vy = seed.vy;
478
+ particle.vz = seed.vz;
479
+ particle.life = seed.life;
480
+ particle.maxLife = seed.life;
481
+ colorHelper.set(seed.color);
482
+ particle.r = colorHelper.r;
483
+ particle.g = colorHelper.g;
484
+ particle.b = colorHelper.b;
485
+ particles.push(particle);
486
+ const offset = i * 3;
487
+ positions[offset] = particle.x;
488
+ positions[offset + 1] = particle.y;
489
+ positions[offset + 2] = particle.z;
490
+ colors[offset] = particle.r;
491
+ colors[offset + 1] = particle.g;
492
+ colors[offset + 2] = particle.b;
493
+ }
494
+ if (this.totalParticleCount() + particles.length > this.maxParticles) {
495
+ this.releaseParticles(particles);
496
+ return;
497
+ }
498
+ const geometry = new THREE2.BufferGeometry();
499
+ geometry.setAttribute("position", new THREE2.BufferAttribute(positions, 3));
500
+ geometry.setAttribute("color", new THREE2.BufferAttribute(colors, 3));
501
+ const material = new THREE2.PointsMaterial({
502
+ size: payload.kind === "miku" ? 0.42 : 0.36,
503
+ vertexColors: true,
504
+ map: this.spriteTexture,
505
+ transparent: true,
506
+ opacity: 1,
507
+ depthWrite: false,
508
+ blending: THREE2.AdditiveBlending
509
+ });
510
+ const points = new THREE2.Points(geometry, material);
511
+ this.scene.add(points);
512
+ const burst = {
513
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
514
+ points,
515
+ geometry,
516
+ material,
517
+ positions,
518
+ colors,
519
+ particles
520
+ };
521
+ this.bursts.push(burst);
522
+ } catch (error) {
523
+ this.onError?.(error instanceof Error ? error : new Error("Failed to launch firework."));
524
+ }
525
+ }
526
+ dispose() {
527
+ this.stop();
528
+ if (this.resizeObserver) {
529
+ this.resizeObserver.disconnect();
530
+ this.resizeObserver = null;
531
+ }
532
+ for (const burst of this.bursts) {
533
+ this.destroyBurst(burst);
534
+ }
535
+ this.bursts.length = 0;
536
+ this.spriteTexture.dispose();
537
+ this.renderer.dispose();
538
+ }
539
+ update(dt) {
540
+ const gravity = -8.8;
541
+ for (let b = this.bursts.length - 1; b >= 0; b -= 1) {
542
+ const burst = this.bursts[b];
543
+ if (!burst) {
544
+ continue;
545
+ }
546
+ let alive = 0;
547
+ for (let i = 0; i < burst.particles.length; i += 1) {
548
+ const particle = burst.particles[i];
549
+ if (!particle) {
550
+ continue;
551
+ }
552
+ particle.life -= dt;
553
+ if (particle.life <= 0) {
554
+ continue;
555
+ }
556
+ particle.vx *= 0.992;
557
+ particle.vy = particle.vy * 0.992 + gravity * dt;
558
+ particle.vz *= 0.992;
559
+ particle.x += particle.vx * dt;
560
+ particle.y += particle.vy * dt;
561
+ particle.z += particle.vz * dt;
562
+ const idx = i * 3;
563
+ burst.positions[idx] = particle.x;
564
+ burst.positions[idx + 1] = particle.y;
565
+ burst.positions[idx + 2] = particle.z;
566
+ const alpha = Math.max(particle.life / particle.maxLife, 0);
567
+ burst.colors[idx] = particle.r * alpha;
568
+ burst.colors[idx + 1] = particle.g * alpha;
569
+ burst.colors[idx + 2] = particle.b * alpha;
570
+ alive += 1;
571
+ }
572
+ const positionAttr = burst.geometry.getAttribute("position");
573
+ const colorAttr = burst.geometry.getAttribute("color");
574
+ positionAttr.needsUpdate = true;
575
+ colorAttr.needsUpdate = true;
576
+ burst.material.opacity = Math.min(1, 0.22 + alive / Math.max(burst.particles.length, 1));
577
+ if (alive === 0) {
578
+ this.bursts.splice(b, 1);
579
+ this.destroyBurst(burst);
580
+ }
581
+ }
582
+ this.updateFpsStats(dt);
583
+ }
584
+ updateFpsStats(dt) {
585
+ this.fpsWindow.frames += 1;
586
+ this.fpsWindow.elapsed += dt;
587
+ if (this.fpsWindow.elapsed < 0.6) {
588
+ return;
589
+ }
590
+ const fps = this.fpsWindow.frames / this.fpsWindow.elapsed;
591
+ this.fpsWindow.fps = fps;
592
+ const policy = evaluateDegradePolicy(fps);
593
+ this.fpsWindow.particleScale = policy.recommendedParticleScale;
594
+ this.onFpsReport?.(Math.round(fps));
595
+ this.fpsWindow.frames = 0;
596
+ this.fpsWindow.elapsed = 0;
597
+ }
598
+ totalParticleCount() {
599
+ return this.bursts.reduce((sum, burst) => sum + burst.particles.length, 0);
600
+ }
601
+ enforceBurstCap() {
602
+ while (this.bursts.length >= this.maxActiveFireworks) {
603
+ const burst = this.bursts.shift();
604
+ if (!burst) {
605
+ break;
606
+ }
607
+ this.destroyBurst(burst);
608
+ }
609
+ }
610
+ destroyBurst(burst) {
611
+ this.scene.remove(burst.points);
612
+ burst.geometry.dispose();
613
+ burst.material.dispose();
614
+ this.releaseParticles(burst.particles);
615
+ }
616
+ releaseParticles(particles) {
617
+ for (const particle of particles) {
618
+ this.pool.release(particle);
619
+ }
620
+ }
621
+ attachResizeObserver() {
622
+ this.resizeObserver = new ResizeObserver(() => this.resize());
623
+ this.resizeObserver.observe(this.container);
624
+ }
625
+ resize() {
626
+ const width = Math.max(1, this.container.clientWidth);
627
+ const height = Math.max(1, this.container.clientHeight);
628
+ this.camera.aspect = width / height;
629
+ this.camera.updateProjectionMatrix();
630
+ this.renderer.setSize(width, height, false);
631
+ }
632
+ };
633
+
634
+ // src/mikuFireworks3D/hooks/useFireworksEngine.ts
635
+ function useFireworksEngine(options) {
636
+ const containerRef = useRef(null);
637
+ const canvasRef = useRef(null);
638
+ const engineRef = useRef(null);
639
+ const [fps, setFps] = useState(60);
640
+ const { maxParticles, maxActiveFireworks, onError, onFpsReport, onLaunch } = options || {};
641
+ useEffect(() => {
642
+ if (!containerRef.current || !canvasRef.current) {
643
+ return;
644
+ }
645
+ const engine = new FireworksEngine({
646
+ container: containerRef.current,
647
+ canvas: canvasRef.current,
648
+ options: {
649
+ maxParticles,
650
+ maxActiveFireworks,
651
+ onError,
652
+ onFpsReport: (nextFps) => {
653
+ setFps(nextFps);
654
+ onFpsReport?.(nextFps);
655
+ }
656
+ }
657
+ });
658
+ engine.start();
659
+ engineRef.current = engine;
660
+ return () => {
661
+ engineRef.current?.dispose();
662
+ engineRef.current = null;
663
+ };
664
+ }, [maxParticles, maxActiveFireworks, onError, onFpsReport]);
665
+ const launch = useCallback(
666
+ (payload) => {
667
+ void engineRef.current?.launch(payload);
668
+ onLaunch?.(payload);
669
+ },
670
+ [onLaunch]
671
+ );
672
+ const api = useMemo(
673
+ () => ({
674
+ containerRef,
675
+ canvasRef,
676
+ launch,
677
+ fps
678
+ }),
679
+ [fps, launch]
680
+ );
681
+ return api;
682
+ }
683
+
684
+ // src/mikuFireworks3D/client/WebSocketTransport.ts
685
+ var WebSocketTransport = class {
686
+ constructor(config, callbacks) {
687
+ this.socket = null;
688
+ this.reconnectTimer = null;
689
+ this.isManualClose = false;
690
+ this.config = config;
691
+ this.callbacks = callbacks || {};
692
+ this.state = {
693
+ connected: false,
694
+ onlineCount: 0,
695
+ roomId: config.roomId
696
+ };
697
+ }
698
+ connect() {
699
+ if (this.socket && (this.socket.readyState === window.WebSocket.OPEN || this.socket.readyState === window.WebSocket.CONNECTING)) {
700
+ return;
701
+ }
702
+ this.isManualClose = false;
703
+ try {
704
+ this.socket = this.config.protocols ? new window.WebSocket(this.config.serverUrl, this.config.protocols) : new window.WebSocket(this.config.serverUrl);
705
+ } catch {
706
+ this.callbacks.onError?.(new Error("Failed to create WebSocket connection."));
707
+ return;
708
+ }
709
+ this.socket.onopen = () => {
710
+ this.updateState({ connected: true });
711
+ this.send({
712
+ type: "join",
713
+ roomId: this.config.roomId,
714
+ user: this.config.user
715
+ });
716
+ };
717
+ this.socket.onmessage = (event) => {
718
+ const parsed = parseServerMessage(event.data);
719
+ if (!parsed) {
720
+ return;
721
+ }
722
+ this.handleServerMessage(parsed);
723
+ };
724
+ this.socket.onerror = () => {
725
+ this.callbacks.onError?.(new Error("WebSocket transport error."));
726
+ };
727
+ this.socket.onclose = () => {
728
+ this.updateState({ connected: false });
729
+ this.scheduleReconnect();
730
+ };
731
+ }
732
+ disconnect() {
733
+ this.isManualClose = true;
734
+ if (this.reconnectTimer != null) {
735
+ window.clearTimeout(this.reconnectTimer);
736
+ this.reconnectTimer = null;
737
+ }
738
+ if (!this.socket) {
739
+ return;
740
+ }
741
+ this.send({ type: "leave" });
742
+ this.socket.close();
743
+ this.socket = null;
744
+ }
745
+ sendDanmaku(payload) {
746
+ this.send({
747
+ type: "danmaku.send",
748
+ payload
749
+ });
750
+ }
751
+ sendFirework(payload) {
752
+ this.send({
753
+ type: "firework.launch",
754
+ payload
755
+ });
756
+ }
757
+ getState() {
758
+ return this.state;
759
+ }
760
+ send(message) {
761
+ if (!this.socket || this.socket.readyState !== window.WebSocket.OPEN) {
762
+ return;
763
+ }
764
+ this.socket.send(JSON.stringify(message));
765
+ }
766
+ updateState(partial) {
767
+ this.state = {
768
+ ...this.state,
769
+ ...partial
770
+ };
771
+ this.callbacks.onStateChange?.(this.state);
772
+ }
773
+ handleServerMessage(message) {
774
+ if (message.type === "joined") {
775
+ this.updateState({ roomId: message.roomId, onlineCount: message.onlineCount });
776
+ return;
777
+ }
778
+ if (message.type === "room.user_joined" || message.type === "room.user_left") {
779
+ this.updateState({ onlineCount: message.onlineCount, roomId: message.roomId });
780
+ return;
781
+ }
782
+ if (message.type === "room.snapshot") {
783
+ this.updateState({ roomId: message.roomId, onlineCount: message.users.length });
784
+ this.callbacks.onSnapshot?.(message);
785
+ return;
786
+ }
787
+ if (message.type === "danmaku.broadcast") {
788
+ this.callbacks.onDanmakuBroadcast?.(message.event);
789
+ return;
790
+ }
791
+ if (message.type === "firework.broadcast") {
792
+ this.callbacks.onFireworkBroadcast?.(message.event);
793
+ return;
794
+ }
795
+ if (message.type === "error") {
796
+ this.callbacks.onError?.(new Error(`${message.code}: ${message.message}`));
797
+ }
798
+ }
799
+ scheduleReconnect() {
800
+ const reconnect = this.config.reconnect ?? true;
801
+ if (this.isManualClose || !reconnect) {
802
+ return;
803
+ }
804
+ if (this.reconnectTimer != null) {
805
+ return;
806
+ }
807
+ const delay = this.config.reconnectIntervalMs ?? 1500;
808
+ this.reconnectTimer = window.setTimeout(() => {
809
+ this.reconnectTimer = null;
810
+ this.connect();
811
+ }, delay);
812
+ }
813
+ };
814
+ function parseServerMessage(raw) {
815
+ if (typeof raw !== "string") {
816
+ return null;
817
+ }
818
+ try {
819
+ return JSON.parse(raw);
820
+ } catch {
821
+ return null;
822
+ }
823
+ }
824
+
825
+ // src/mikuFireworks3D/hooks/useFireworksRealtime.ts
826
+ function useFireworksRealtime(options) {
827
+ const { config, enabled, onDanmakuBroadcast, onFireworkBroadcast, onError, onStateChange } = options;
828
+ const transportRef = useRef(null);
829
+ const callbackRef = useRef({
830
+ onDanmakuBroadcast,
831
+ onFireworkBroadcast,
832
+ onError,
833
+ onStateChange
834
+ });
835
+ const serverUrl = config?.serverUrl ?? "";
836
+ const roomId = config?.roomId ?? "";
837
+ const userId = config?.user.userId ?? "";
838
+ const nickname = config?.user.nickname ?? "";
839
+ const avatarUrl = config?.user.avatarUrl ?? "";
840
+ const reconnect = config?.reconnect ?? true;
841
+ const reconnectIntervalMs = config?.reconnectIntervalMs ?? 1500;
842
+ const [state, setState] = useState({
843
+ connected: false,
844
+ onlineCount: 0,
845
+ roomId
846
+ });
847
+ useEffect(() => {
848
+ callbackRef.current = {
849
+ onDanmakuBroadcast,
850
+ onFireworkBroadcast,
851
+ onError,
852
+ onStateChange
853
+ };
854
+ }, [onDanmakuBroadcast, onError, onFireworkBroadcast, onStateChange]);
855
+ const normalizedConfig = useMemo(() => {
856
+ if (!serverUrl || !roomId || !userId) {
857
+ return void 0;
858
+ }
859
+ const protocols = Array.isArray(config?.protocols) ? [...config.protocols] : config?.protocols;
860
+ return {
861
+ serverUrl,
862
+ roomId,
863
+ user: {
864
+ userId,
865
+ nickname: nickname || void 0,
866
+ avatarUrl: avatarUrl || void 0
867
+ },
868
+ protocols,
869
+ reconnect,
870
+ reconnectIntervalMs
871
+ };
872
+ }, [avatarUrl, config?.protocols, nickname, reconnect, reconnectIntervalMs, roomId, serverUrl, userId]);
873
+ useEffect(() => {
874
+ if (!enabled || !normalizedConfig) {
875
+ transportRef.current?.disconnect();
876
+ transportRef.current = null;
877
+ setState({
878
+ connected: false,
879
+ onlineCount: 0,
880
+ roomId: normalizedConfig?.roomId
881
+ });
882
+ return;
883
+ }
884
+ const transport = new WebSocketTransport(normalizedConfig, {
885
+ onStateChange: (nextState) => {
886
+ setState(nextState);
887
+ callbackRef.current.onStateChange?.(nextState);
888
+ },
889
+ onDanmakuBroadcast: (event) => {
890
+ callbackRef.current.onDanmakuBroadcast?.({
891
+ id: event.id,
892
+ text: event.text,
893
+ color: event.color,
894
+ kind: event.kind,
895
+ userId: event.user.userId,
896
+ timestamp: event.timestamp
897
+ });
898
+ },
899
+ onFireworkBroadcast: (event) => {
900
+ callbackRef.current.onFireworkBroadcast?.(event.payload);
901
+ },
902
+ onError: (error) => {
903
+ callbackRef.current.onError?.(error);
904
+ }
905
+ });
906
+ transport.connect();
907
+ transportRef.current = transport;
908
+ return () => {
909
+ transport.disconnect();
910
+ if (transportRef.current === transport) {
911
+ transportRef.current = null;
912
+ }
913
+ };
914
+ }, [enabled, normalizedConfig]);
915
+ return useMemo(
916
+ () => ({
917
+ state,
918
+ sendDanmaku: (payload) => {
919
+ transportRef.current?.sendDanmaku(payload);
920
+ },
921
+ sendFirework: (payload) => {
922
+ transportRef.current?.sendFirework(payload);
923
+ }
924
+ }),
925
+ [state]
926
+ );
927
+ }
928
+
929
+ // src/mikuFireworks3D/components/MikuFireworks3D.tsx
930
+ function MikuFireworks3D({
931
+ width = "100%",
932
+ height = 520,
933
+ className,
934
+ defaultKind = "normal",
935
+ autoLaunchOnDanmaku = true,
936
+ maxParticles,
937
+ maxActiveFireworks,
938
+ defaultAvatarUrl = "",
939
+ onLaunch,
940
+ onDanmakuSend,
941
+ onError,
942
+ onFpsReport,
943
+ onRealtimeStateChange,
944
+ realtime
945
+ }) {
946
+ const [selectedKind, setSelectedKind] = useState(defaultKind);
947
+ const [avatarUrl, setAvatarUrl] = useState(defaultAvatarUrl);
948
+ const [autoLaunch, setAutoLaunch] = useState(autoLaunchOnDanmaku);
949
+ const { containerRef, canvasRef, launch, fps } = useFireworksEngine({
950
+ maxParticles,
951
+ maxActiveFireworks,
952
+ onLaunch,
953
+ onError,
954
+ onFpsReport
955
+ });
956
+ const { items, send, addIncoming, removeItem } = useDanmakuController({
957
+ onSend: onDanmakuSend
958
+ });
959
+ const realtimeEnabled = Boolean(realtime && (realtime.enabled ?? true));
960
+ const realtimeApi = useFireworksRealtime({
961
+ enabled: realtimeEnabled,
962
+ config: realtime,
963
+ onStateChange: onRealtimeStateChange,
964
+ onError,
965
+ onDanmakuBroadcast: (event) => {
966
+ addIncoming({
967
+ id: event.id,
968
+ userId: event.userId,
969
+ text: event.text,
970
+ color: event.color,
971
+ timestamp: event.timestamp
972
+ });
973
+ },
974
+ onFireworkBroadcast: (payload) => {
975
+ launch(payload);
976
+ }
977
+ });
978
+ const handleLaunch = (kind) => {
979
+ const payload = {
980
+ kind,
981
+ avatarUrl: kind === "avatar" ? avatarUrl || void 0 : void 0
982
+ };
983
+ if (realtimeEnabled && realtimeApi.state.connected) {
984
+ realtimeApi.sendFirework(payload);
985
+ return;
986
+ }
987
+ launch(payload);
988
+ };
989
+ const handleSendDanmaku = (text) => {
990
+ const result = send(text, void 0, {
991
+ optimistic: !(realtimeEnabled && realtimeApi.state.connected)
992
+ });
993
+ if (!result) {
994
+ return;
995
+ }
996
+ const launchKind = result.launchKind ?? selectedKind;
997
+ if (realtimeEnabled && realtimeApi.state.connected) {
998
+ realtimeApi.sendDanmaku({
999
+ text: result.message.text,
1000
+ color: result.message.color,
1001
+ kind: result.launchKind
1002
+ });
1003
+ if (autoLaunch) {
1004
+ realtimeApi.sendFirework({
1005
+ kind: launchKind,
1006
+ avatarUrl: launchKind === "avatar" ? avatarUrl || void 0 : void 0,
1007
+ message: result.message
1008
+ });
1009
+ }
1010
+ return;
1011
+ }
1012
+ if (autoLaunch || result.launchKind) {
1013
+ launch({
1014
+ kind: launchKind,
1015
+ avatarUrl: launchKind === "avatar" ? avatarUrl || void 0 : void 0,
1016
+ message: result.message
1017
+ });
1018
+ }
1019
+ };
1020
+ const containerStyle = useMemo(
1021
+ () => ({
1022
+ width,
1023
+ height,
1024
+ minHeight: 360
1025
+ }),
1026
+ [height, width]
1027
+ );
1028
+ return /* @__PURE__ */ React3.createElement("div", { className: `mx-auto flex w-full max-w-5xl flex-col gap-3 ${className || ""}` }, /* @__PURE__ */ React3.createElement("div", { ref: containerRef, className: "relative overflow-hidden rounded-2xl border border-slate-700", style: containerStyle }, /* @__PURE__ */ React3.createElement(FireworksCanvas, { canvasRef }), /* @__PURE__ */ React3.createElement("div", { className: "pointer-events-none absolute inset-0 overflow-hidden" }, items.map((item) => /* @__PURE__ */ React3.createElement(
1029
+ "div",
1030
+ {
1031
+ key: item.id,
1032
+ onAnimationEnd: () => removeItem(item.id),
1033
+ className: "absolute right-[-30%] whitespace-nowrap text-sm font-semibold text-white drop-shadow",
1034
+ style: {
1035
+ top: `${8 + item.track * 11}%`,
1036
+ color: item.color || "#ffffff",
1037
+ animation: `sa2kit-danmaku-move ${item.durationMs}ms linear forwards`
1038
+ }
1039
+ },
1040
+ item.text
1041
+ )))), /* @__PURE__ */ React3.createElement(
1042
+ FireworksControlPanel,
1043
+ {
1044
+ selectedKind,
1045
+ onKindChange: setSelectedKind,
1046
+ autoLaunchOnDanmaku: autoLaunch,
1047
+ onAutoLaunchChange: setAutoLaunch,
1048
+ avatarUrl,
1049
+ onAvatarUrlChange: setAvatarUrl,
1050
+ onLaunch: () => handleLaunch(selectedKind),
1051
+ fps,
1052
+ realtimeConnected: realtimeEnabled ? realtimeApi.state.connected : void 0,
1053
+ onlineCount: realtimeEnabled ? realtimeApi.state.onlineCount : void 0
1054
+ }
1055
+ ), /* @__PURE__ */ React3.createElement(DanmakuPanel, { onSend: handleSendDanmaku }), /* @__PURE__ */ React3.createElement("style", null, `
1056
+ @keyframes sa2kit-danmaku-move {
1057
+ 0% {
1058
+ transform: translateX(0);
1059
+ opacity: 1;
1060
+ }
1061
+ 100% {
1062
+ transform: translateX(-160vw);
1063
+ opacity: 0.92;
1064
+ }
1065
+ }
1066
+ `));
1067
+ }
1068
+
1069
+ export { DANMAKU_MAX_LENGTH, DANMAKU_TRACK_COUNT, DEFAULT_MAX_ACTIVE_FIREWORKS, DEFAULT_MAX_PARTICLES, DanmakuPanel, FIREWORK_KIND_LABELS, FireworksCanvas, FireworksControlPanel, MIKU_PALETTE, MikuFireworks3D, NORMAL_PALETTE, WebSocketTransport, useDanmakuController, useFireworksEngine, useFireworksRealtime };
1070
+ //# sourceMappingURL=chunk-SR4JFEHW.mjs.map
1071
+ //# sourceMappingURL=chunk-SR4JFEHW.mjs.map