dsh-wallpaper_share 26.8.22-T

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/lib/client.js ADDED
@@ -0,0 +1,2283 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-wallpaper_share",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/WallpaperSharePanel.tsx
10
+ /**
11
+ * wallpaper_share 会话视图标签页:当前壁纸信息、同步开关、显示器选择、
12
+ * 专注模式、渲染模式,以及透明度 / 模糊 / 阴影三个滑块(即时生效)。
13
+ * 样式类名由 PANEL_CSS 在 apply 阶段注入,不依赖 CSS Modules。
14
+ */
15
+ function WallpaperSharePanel() {
16
+ const [, force] = (0, react.useState)(0);
17
+ const [info, setInfo] = (0, react.useState)(store.info);
18
+ const [enabled, setEnabled] = (0, react.useState)(store.settings.enabled);
19
+ const [alpha, setAlpha] = (0, react.useState)(store.settings.panelAlpha);
20
+ const [blur, setBlur] = (0, react.useState)(store.settings.blur);
21
+ const [shadow, setShadow] = (0, react.useState)(store.settings.shadow);
22
+ const [status, setStatus] = (0, react.useState)("");
23
+ const [monitor, setMonitor] = (0, react.useState)(store.settings.monitor);
24
+ const [focus, setFocus] = (0, react.useState)(store.settings.focus);
25
+ const [renderMode, setRenderMode] = (0, react.useState)(store.settings.renderMode);
26
+ const [immersive, setImmersive] = (0, react.useState)(store.settings.immersive);
27
+ (0, react.useEffect)(() => store.subscribe(() => {
28
+ setInfo(store.info);
29
+ force((x) => x + 1);
30
+ }), []);
31
+ const flash = (text) => {
32
+ setStatus(text);
33
+ window.setTimeout(() => setStatus(""), 3500);
34
+ };
35
+ const onAlpha = (v) => {
36
+ store.settings.panelAlpha = v;
37
+ setAlpha(v);
38
+ store.actions.applyTheme();
39
+ };
40
+ const onBlur = (v) => {
41
+ store.settings.blur = v;
42
+ setBlur(v);
43
+ store.actions.applyBackground();
44
+ };
45
+ const onShadow = (v) => {
46
+ store.settings.shadow = v;
47
+ setShadow(v);
48
+ store.actions.applyBackground();
49
+ };
50
+ const onPower = () => {
51
+ const next = !store.settings.enabled;
52
+ store.settings.enabled = next;
53
+ setEnabled(next);
54
+ store.actions.applyBackground();
55
+ flash(next ? "已开启壁纸同步" : "已关闭壁纸同步");
56
+ };
57
+ const onMonitor = (v) => {
58
+ store.settings.monitor = v;
59
+ setMonitor(v);
60
+ store.actions.repoll();
61
+ };
62
+ const onFocus = () => {
63
+ const next = !store.settings.focus;
64
+ store.settings.focus = next;
65
+ setFocus(next);
66
+ store.actions.applyTheme();
67
+ store.actions.applyBackground();
68
+ flash(next ? "专注模式已开启:任务中 30%/15px/90%,空闲 9%/6px/40%" : "专注模式已关闭,恢复手动滑块");
69
+ };
70
+ const onImmersive = () => {
71
+ const next = !store.settings.immersive;
72
+ store.settings.immersive = next;
73
+ setImmersive(next);
74
+ store.actions.applyImmersive();
75
+ store.notify();
76
+ flash(next ? "沉浸模式已开启:对话上边栏/输入框已隐藏,壁纸可交互(Esc 或右上角按钮退出)" : "已退出沉浸模式");
77
+ };
78
+ const onRenderMode = () => {
79
+ const next = store.settings.renderMode === "source" ? "preview" : "source";
80
+ store.settings.renderMode = next;
81
+ setRenderMode(next);
82
+ store.actions.applyBackground();
83
+ if (next === "source") {
84
+ const kind = store.info !== null ? store.info.source.kind : "";
85
+ if (kind === "video") flash("增强模式:播放壁纸源视频");
86
+ else if (kind === "web") flash("增强模式:加载 Web 壁纸页面");
87
+ else if (kind === "scene") flash(store.info?.scene?.live === true ? "增强模式:Scene 实时渲染中" : "增强模式:Scene(renderer 未出帧,回退纹理/预览)");
88
+ else flash("当前壁纸(" + (kind === "" ? "无" : kind) + ")仅支持预览,增强模式自动回退");
89
+ } else flash("性能模式:使用静态预览图");
90
+ };
91
+ const wallpaper = info !== null && info.wallpaper !== null ? info.wallpaper : null;
92
+ const title = wallpaper === null ? info !== null && info.kind === "web" ? "当前为网页壁纸(无本地预览)" : "Wallpaper Engine 尚未应用壁纸" : wallpaper.title;
93
+ const subtitle = wallpaper === null ? "在 Wallpaper Engine 中应用壁纸后,此处会同步显示" : wallpaper.type + (info !== null && info.kind === "image" ? " · 已同步静态预览" : " · 无静态预览图") + (info !== null && info.monitor !== "" ? " · 显示器 " + info.monitor : "") + (info !== null && info.kind === "scene" && info.scene !== null ? " · Scene[" + (info.scene.mode ?? "browser") + "] " + (info.scene.live ? "live " + String(info.scene.status?.fps ?? "?") + "fps" : info.scene.model === true ? "model 渲染" : "fallback:" + info.scene.fallback) : "");
94
+ const monitors = info !== null && Array.isArray(info.monitors) && info.monitors.length > 1 ? info.monitors : null;
95
+ const focusVisuals = focus ? store.settings.taskActive ? FOCUS_WORK : FOCUS_IDLE : null;
96
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
97
+ className: "wesync-panel",
98
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
99
+ className: "wesync-card",
100
+ children: [
101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
102
+ className: "wesync-title",
103
+ children: title
104
+ }),
105
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
106
+ className: "wesync-sub",
107
+ children: subtitle
108
+ }),
109
+ monitors !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
110
+ className: "wesync-row",
111
+ children: [
112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: "背景显示器" }),
113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
114
+ className: "wesync-select",
115
+ value: monitor,
116
+ onChange: (e) => onMonitor(e.target.value),
117
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
118
+ value: "",
119
+ children: "自动 · 跟随最新变化"
120
+ }), monitors.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
121
+ value: m.key,
122
+ children: m.key + " · " + m.title
123
+ }, m.key))]
124
+ }),
125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", { children: monitor === "" ? "auto" : monitor })
126
+ ]
127
+ }) : null,
128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
129
+ className: "wesync-actions",
130
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
131
+ className: "wesync-btn",
132
+ onClick: onPower,
133
+ children: enabled ? "⏻ 同步开启" : "⏻ 同步关闭"
134
+ })
135
+ }),
136
+ status !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
137
+ className: "wesync-status",
138
+ children: status
139
+ }) : null
140
+ ]
141
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
142
+ className: "wesync-card",
143
+ children: [
144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
145
+ className: "wesync-sub",
146
+ children: "视觉效果 · 即时生效"
147
+ }),
148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
149
+ className: "wesync-actions",
150
+ children: [
151
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
152
+ className: ["wesync-btn", focus ? "wesync-focusOn" : "wesync-focusOff"].join(" "),
153
+ onClick: onFocus,
154
+ children: focus ? store.settings.taskActive ? "专注模式 · 任务进行中" : "专注模式 · 已完成" : "开启专注模式"
155
+ }),
156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
157
+ className: ["wesync-btn", renderMode === "source" ? "wesync-sourceOn" : "wesync-sourceOff"].join(" "),
158
+ onClick: onRenderMode,
159
+ children: renderMode === "source" ? "渲染:增强(源文件)" : "渲染:性能(预览)"
160
+ }),
161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
162
+ className: ["wesync-btn", immersive ? "wesync-focusOn" : "wesync-focusOff"].join(" "),
163
+ onClick: onImmersive,
164
+ children: immersive ? "沉浸模式 · 已开启" : "开启沉浸模式"
165
+ })
166
+ ]
167
+ }),
168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
169
+ label: "面板透明度",
170
+ min: 0,
171
+ max: 100,
172
+ value: focusVisuals !== null ? focusVisuals.panelAlpha : alpha,
173
+ unit: "%",
174
+ disabled: focusVisuals !== null,
175
+ onChange: onAlpha
176
+ }),
177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
178
+ label: "背景模糊",
179
+ min: 0,
180
+ max: 30,
181
+ value: focusVisuals !== null ? focusVisuals.blur : blur,
182
+ unit: "px",
183
+ disabled: focusVisuals !== null,
184
+ onChange: onBlur
185
+ }),
186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
187
+ label: "阴影深度",
188
+ min: 0,
189
+ max: 100,
190
+ value: focusVisuals !== null ? focusVisuals.shadow : shadow,
191
+ unit: "%",
192
+ disabled: focusVisuals !== null,
193
+ onChange: onShadow
194
+ })
195
+ ]
196
+ })]
197
+ });
198
+ }
199
+ function Slider(props) {
200
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
201
+ className: "wesync-row",
202
+ style: props.disabled === true ? { opacity: .45 } : void 0,
203
+ children: [
204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: props.label }),
205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
206
+ type: "range",
207
+ min: props.min,
208
+ max: props.max,
209
+ step: 1,
210
+ value: props.value,
211
+ disabled: props.disabled,
212
+ onChange: (e) => props.onChange(Number(e.target.value))
213
+ }),
214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", { children: String(props.value) + props.unit })
215
+ ]
216
+ });
217
+ }
218
+ //#endregion
219
+ //#region src/client/panelStyle.ts
220
+ /**
221
+ * 面板样式(独立构建不再依赖 CSS Modules,运行时注入 <style>)。
222
+ */
223
+ const PANEL_CSS = `
224
+ .wesync-panel {
225
+ padding: 24px;
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 16px;
229
+ max-width: 660px;
230
+ box-sizing: border-box;
231
+ }
232
+
233
+ .wesync-card {
234
+ padding: 16px 18px;
235
+ border-radius: 12px;
236
+ background: var(--dsw-alias-bg-layer-1);
237
+ border: 1px solid var(--dsw-alias-border-l1);
238
+ }
239
+
240
+ .wesync-title {
241
+ font-size: 15px;
242
+ font-weight: 600;
243
+ color: var(--dsw-alias-label-primary);
244
+ margin: 0 0 4px 0;
245
+ overflow: hidden;
246
+ text-overflow: ellipsis;
247
+ white-space: nowrap;
248
+ }
249
+
250
+ .wesync-sub {
251
+ font-size: 12px;
252
+ color: var(--dsw-alias-label-secondary);
253
+ }
254
+
255
+ .wesync-status {
256
+ font-size: 12px;
257
+ color: var(--dsw-alias-label-secondary);
258
+ margin-top: 10px;
259
+ }
260
+
261
+ .wesync-actions {
262
+ display: flex;
263
+ gap: 8px;
264
+ margin-top: 12px;
265
+ flex-wrap: wrap;
266
+ }
267
+
268
+ .wesync-btn {
269
+ padding: 6px 14px;
270
+ border-radius: 999px;
271
+ border: 1px solid var(--dsw-alias-border-l1);
272
+ background: var(--dsw-alias-bg-layer-2);
273
+ color: var(--dsw-alias-label-primary);
274
+ cursor: pointer;
275
+ font-size: 13px;
276
+ font-family: inherit;
277
+ }
278
+
279
+ .wesync-btn:hover:not(:disabled) {
280
+ background: var(--dsw-alias-bg-overlay);
281
+ }
282
+
283
+ .wesync-btn:disabled {
284
+ opacity: 0.5;
285
+ cursor: default;
286
+ }
287
+
288
+ .wesync-focusOff {
289
+ background: rgba(139, 92, 246, 0.18);
290
+ border-color: rgba(139, 92, 246, 0.55);
291
+ color: #c4b5fd;
292
+ }
293
+
294
+ .wesync-focusOff:hover:not(:disabled) {
295
+ background: rgba(139, 92, 246, 0.32);
296
+ }
297
+
298
+ .wesync-focusOn {
299
+ background: rgba(46, 160, 67, 0.20);
300
+ border-color: rgba(46, 160, 67, 0.55);
301
+ color: #7ee2a8;
302
+ }
303
+
304
+ .wesync-focusOn:hover:not(:disabled) {
305
+ background: rgba(46, 160, 67, 0.32);
306
+ }
307
+
308
+ .wesync-sourceOff {
309
+ background: rgba(249, 115, 22, 0.15);
310
+ border-color: rgba(249, 115, 22, 0.5);
311
+ color: #fdba74;
312
+ }
313
+
314
+ .wesync-sourceOff:hover:not(:disabled) {
315
+ background: rgba(249, 115, 22, 0.28);
316
+ }
317
+
318
+ .wesync-sourceOn {
319
+ background: rgba(46, 160, 67, 0.20);
320
+ border-color: rgba(46, 160, 67, 0.55);
321
+ color: #7ee2a8;
322
+ }
323
+
324
+ .wesync-sourceOn:hover:not(:disabled) {
325
+ background: rgba(46, 160, 67, 0.32);
326
+ }
327
+
328
+ .wesync-row {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 12px;
332
+ margin-top: 12px;
333
+ }
334
+
335
+ .wesync-row label {
336
+ flex: 0 0 92px;
337
+ font-size: 12px;
338
+ color: var(--dsw-alias-label-secondary);
339
+ }
340
+
341
+ .wesync-row input[type='range'] {
342
+ flex: 1;
343
+ accent-color: var(--dsw-alias-brand-primary);
344
+ height: 20px;
345
+ }
346
+
347
+ .wesync-select {
348
+ flex: 1;
349
+ padding: 4px 8px;
350
+ border-radius: 8px;
351
+ border: 1px solid var(--dsw-alias-border-l1);
352
+ background: var(--dsw-alias-bg-layer-2);
353
+ color: var(--dsw-alias-label-primary);
354
+ font-size: 12px;
355
+ font-family: inherit;
356
+ }
357
+
358
+ .wesync-row output {
359
+ flex: 0 0 44px;
360
+ text-align: right;
361
+ font-size: 12px;
362
+ color: var(--dsw-alias-label-secondary);
363
+ font-variant-numeric: tabular-nums;
364
+ }
365
+ `;
366
+ //#endregion
367
+ //#region src/client/SceneCanvas.ts
368
+ /**
369
+ * SceneCanvas —— 浏览器半的 scene 动态背景层。
370
+ *
371
+ * 通过 WebSocket(/we-sync/scene/stream)接收 Node 中继的编码帧,
372
+ * 解码为 ImageBitmap 后按 requestAnimationFrame 画到 <canvas>,覆盖铺满。
373
+ *
374
+ * 职责:canvas resize / devicePixelRatio / 帧解码 / rAF 调度 / 可见性暂停 /
375
+ * 自动重连 / 模糊与缩放(模糊 opacity 仍在 CSS 层,不进 renderer)。
376
+ */
377
+ const MAX_RECONNECT = 5;
378
+ const RECONNECT_DELAY_MS = 1e3;
379
+ var SceneCanvas = class {
380
+ el = null;
381
+ ctx = null;
382
+ ws = null;
383
+ rafId = 0;
384
+ needDraw = false;
385
+ latest = null;
386
+ dpr = 1;
387
+ live = false;
388
+ closed = false;
389
+ retries = 0;
390
+ reconnectTimer = null;
391
+ blurPx = 0;
392
+ scale = 1;
393
+ handlers;
394
+ constructor(handlers = {}) {
395
+ this.handlers = handlers;
396
+ }
397
+ get isLive() {
398
+ return this.live;
399
+ }
400
+ start(monitor, version) {
401
+ this.stop();
402
+ this.closed = false;
403
+ this.retries = 0;
404
+ this.el = document.createElement("canvas");
405
+ this.el.style.position = "fixed";
406
+ this.el.style.top = "0";
407
+ this.el.style.left = "0";
408
+ this.el.style.width = "100%";
409
+ this.el.style.height = "100%";
410
+ this.el.style.zIndex = "-2";
411
+ this.el.style.pointerEvents = "none";
412
+ this.el.style.border = "0";
413
+ document.body.appendChild(this.el);
414
+ this.ctx = this.el.getContext("2d");
415
+ this.resize();
416
+ this.applyVisuals();
417
+ window.addEventListener("resize", this.onResize);
418
+ document.addEventListener("visibilitychange", this.onVisibility);
419
+ const proto = location.protocol === "https:" ? "wss:" : "ws:";
420
+ const query = (monitor !== "" ? "monitor=" + encodeURIComponent(monitor) : "") + (monitor !== "" ? "&v=" : "v=") + encodeURIComponent(String(version));
421
+ this.connect(proto + "//" + location.host + "/we-sync/scene/stream?" + query);
422
+ }
423
+ stop() {
424
+ this.closed = true;
425
+ this.setLive(false);
426
+ if (this.reconnectTimer !== null) {
427
+ clearTimeout(this.reconnectTimer);
428
+ this.reconnectTimer = null;
429
+ }
430
+ if (this.rafId !== 0) {
431
+ cancelAnimationFrame(this.rafId);
432
+ this.rafId = 0;
433
+ }
434
+ if (this.ws !== null) {
435
+ try {
436
+ this.ws.onclose = null;
437
+ this.ws.onerror = null;
438
+ this.ws.onmessage = null;
439
+ this.ws.close();
440
+ } catch {}
441
+ this.ws = null;
442
+ }
443
+ if (this.latest !== null) {
444
+ try {
445
+ this.latest.close();
446
+ } catch {}
447
+ this.latest = null;
448
+ }
449
+ window.removeEventListener("resize", this.onResize);
450
+ document.removeEventListener("visibilitychange", this.onVisibility);
451
+ if (this.el !== null) {
452
+ this.el.remove();
453
+ this.el = null;
454
+ this.ctx = null;
455
+ }
456
+ }
457
+ applyVisuals(blurPx, scale) {
458
+ if (blurPx !== void 0) this.blurPx = blurPx;
459
+ if (scale !== void 0) this.scale = scale;
460
+ if (this.el !== null) {
461
+ this.el.style.filter = "blur(" + Math.round(this.blurPx) + "px)";
462
+ this.el.style.transform = "scale(" + this.scale.toFixed(3) + ")";
463
+ }
464
+ }
465
+ connect(url) {
466
+ if (this.closed) return;
467
+ let ws;
468
+ try {
469
+ ws = new WebSocket(url);
470
+ } catch {
471
+ this.fail();
472
+ return;
473
+ }
474
+ ws.binaryType = "arraybuffer";
475
+ this.ws = ws;
476
+ ws.onopen = () => {
477
+ this.retries = 0;
478
+ };
479
+ ws.onmessage = (ev) => this.onMessage(ev);
480
+ ws.onerror = () => {};
481
+ ws.onclose = () => {
482
+ if (this.closed) return;
483
+ this.ws = null;
484
+ if (this.retries < MAX_RECONNECT) {
485
+ this.retries += 1;
486
+ this.reconnectTimer = setTimeout(() => this.connect(url), RECONNECT_DELAY_MS);
487
+ } else this.fail();
488
+ };
489
+ }
490
+ fail() {
491
+ this.setLive(false);
492
+ this.closed = true;
493
+ }
494
+ onMessage(ev) {
495
+ if (this.closed) return;
496
+ const buf = ev.data;
497
+ if (!(buf instanceof ArrayBuffer)) return;
498
+ const view = new DataView(buf);
499
+ if (buf.byteLength < 9) return;
500
+ const format = view.getUint8(0);
501
+ const w = view.getUint32(1, true);
502
+ const h = view.getUint32(5, true);
503
+ if (w < 1 || h < 1 || w > 16384 || h > 16384) return;
504
+ const payload = new Uint8Array(buf, 9);
505
+ this.decode(format, w, h, payload);
506
+ }
507
+ decode(format, w, h, payload) {
508
+ let promise;
509
+ if (format === 0) promise = createImageBitmap(new Blob([payload], { type: "image/jpeg" }));
510
+ else if (format === 1) promise = createImageBitmap(new Blob([payload], { type: "image/webp" }));
511
+ else if (format === 2 || format === 3) {
512
+ let px = new Uint8ClampedArray(payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength));
513
+ if (format === 3) px = this.bgraToRgba(payload);
514
+ promise = createImageBitmap(new ImageData(px, w, h));
515
+ } else return;
516
+ promise.then((bmp) => {
517
+ if (this.closed) {
518
+ bmp.close();
519
+ return;
520
+ }
521
+ if (this.latest !== null) try {
522
+ this.latest.close();
523
+ } catch {}
524
+ this.latest = bmp;
525
+ this.retries = 0;
526
+ this.setLive(true);
527
+ this.scheduleDraw();
528
+ }).catch(() => {});
529
+ }
530
+ bgraToRgba(payload) {
531
+ const out = new Uint8ClampedArray(payload.length);
532
+ for (let i = 0; i < payload.length; i += 4) {
533
+ out[i] = payload[i + 2];
534
+ out[i + 1] = payload[i + 1];
535
+ out[i + 2] = payload[i];
536
+ out[i + 3] = payload[i + 3];
537
+ }
538
+ return out;
539
+ }
540
+ scheduleDraw() {
541
+ this.needDraw = true;
542
+ if (this.rafId === 0 && !document.hidden) this.rafId = requestAnimationFrame(this.draw);
543
+ }
544
+ draw = () => {
545
+ this.rafId = 0;
546
+ if (this.closed || this.ctx === null || this.el === null) return;
547
+ if (this.needDraw && this.latest !== null) {
548
+ this.needDraw = false;
549
+ this.drawCover(this.ctx, this.latest, this.el.width, this.el.height);
550
+ }
551
+ };
552
+ /** 以 cover 方式绘制(等比裁切铺满),与 background-size: cover 对齐 */
553
+ drawCover(ctx, bmp, cw, ch) {
554
+ const iw = bmp.width;
555
+ const ih = bmp.height;
556
+ if (iw === 0 || ih === 0) return;
557
+ const scale = Math.max(cw / iw, ch / ih);
558
+ const sw = cw / scale;
559
+ const sh = ch / scale;
560
+ const sx = (iw - sw) / 2;
561
+ const sy = (ih - sh) / 2;
562
+ ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, cw, ch);
563
+ }
564
+ resize() {
565
+ if (this.el === null) return;
566
+ this.dpr = window.devicePixelRatio || 1;
567
+ const w = Math.max(1, Math.round(this.el.clientWidth * this.dpr));
568
+ const h = Math.max(1, Math.round(this.el.clientHeight * this.dpr));
569
+ if (this.el.width !== w) this.el.width = w;
570
+ if (this.el.height !== h) this.el.height = h;
571
+ }
572
+ onResize = () => {
573
+ this.resize();
574
+ this.scheduleDraw();
575
+ };
576
+ onVisibility = () => {
577
+ if (document.hidden) {
578
+ if (this.rafId !== 0) {
579
+ cancelAnimationFrame(this.rafId);
580
+ this.rafId = 0;
581
+ }
582
+ } else this.scheduleDraw();
583
+ };
584
+ setLive(live) {
585
+ if (this.live === live) return;
586
+ this.live = live;
587
+ if (this.handlers.onLiveChange !== void 0) this.handlers.onLiveChange(live);
588
+ }
589
+ };
590
+ //#endregion
591
+ //#region src/scene/ScenePuppet.ts
592
+ /** 按相对时间 t 在关键帧间线性插值(循环动画自动回卷处理) */
593
+ function sampleAnimation(anim, t) {
594
+ const kf = anim.keyframes;
595
+ if (kf.length === 0) return null;
596
+ if (kf.length === 1) return {
597
+ values: kf[0].values,
598
+ t: kf[0].t
599
+ };
600
+ let peak = 0;
601
+ for (let i = 1; i < kf.length; i++) if (kf[i].t > kf[peak].t) peak = i;
602
+ const period = kf[peak].t - kf[0].t;
603
+ if (period <= 0) {
604
+ const n = kf.length;
605
+ const idx = (t % n + n) % n;
606
+ const i0 = Math.floor(idx);
607
+ const frac = idx - i0;
608
+ if (i0 + 1 >= n) return {
609
+ values: kf[i0].values,
610
+ t: i0
611
+ };
612
+ return {
613
+ values: kf[i0].values.map((v, k) => v + (kf[i0 + 1].values[k] - v) * frac),
614
+ t: i0 + frac
615
+ };
616
+ }
617
+ const startT = kf[0].t;
618
+ const curve = [];
619
+ for (let i = 0; i <= peak; i++) curve.push({
620
+ p: kf[i].t - startT,
621
+ values: kf[i].values
622
+ });
623
+ for (let i = peak; i < kf.length; i++) curve.push({
624
+ p: period - (kf[i].t - startT),
625
+ values: kf[i].values
626
+ });
627
+ const mono = [];
628
+ let lastP = -Infinity;
629
+ for (const c of curve) if (c.p >= lastP) {
630
+ mono.push(c);
631
+ lastP = c.p;
632
+ }
633
+ if (mono.length < 2) return {
634
+ values: kf[0].values,
635
+ t: kf[0].t
636
+ };
637
+ const prog = (t % period + period) % period;
638
+ let a = mono[0];
639
+ for (let i = 1; i < mono.length; i++) {
640
+ const b = mono[i];
641
+ if (prog <= b.p) {
642
+ const span = b.p - a.p;
643
+ const frac = span > 0 ? Math.min(1, Math.max(0, (prog - a.p) / span)) : 0;
644
+ return {
645
+ values: a.values.map((v, k) => v + (b.values[k] - v) * frac),
646
+ t: prog + startT
647
+ };
648
+ }
649
+ a = b;
650
+ }
651
+ return {
652
+ values: mono[mono.length - 1].values,
653
+ t: prog + startT
654
+ };
655
+ }
656
+ //#endregion
657
+ //#region src/client/ParticleRuntime.ts
658
+ var ParticleRuntime = class {
659
+ desc;
660
+ rateScale;
661
+ sizeScale;
662
+ particles = [];
663
+ acc = 0;
664
+ time = 0;
665
+ /** 纹理染色缓存(颜色 → 染色 canvas) */
666
+ tintCache = /* @__PURE__ */ new Map();
667
+ constructor(desc, rateScale = 1, sizeScale = 1) {
668
+ this.desc = desc;
669
+ this.rateScale = rateScale;
670
+ this.sizeScale = sizeScale;
671
+ }
672
+ get count() {
673
+ return this.particles.length;
674
+ }
675
+ update(dt) {
676
+ this.time += dt;
677
+ const em = this.desc.emitter;
678
+ const ini = this.desc.initializers;
679
+ const ops = this.desc.operators;
680
+ this.acc += em.rate * this.rateScale * dt;
681
+ while (this.acc >= 1 && this.particles.length < this.desc.maxCount) {
682
+ this.acc -= 1;
683
+ this.spawn(em, ini);
684
+ }
685
+ const g = ops.gravity ?? [
686
+ 0,
687
+ 0,
688
+ 0
689
+ ];
690
+ const drag = ops.drag ?? 0;
691
+ const angDrag = ops.angularDrag ?? 0;
692
+ const angForce = ops.angularForce ?? [
693
+ 0,
694
+ 0,
695
+ 0
696
+ ];
697
+ const fade = ops.alphaFade;
698
+ const osc = ops.oscillateAlpha;
699
+ const oscPos = ops.oscillatePosition;
700
+ const sizeChanges = ops.sizeChanges ?? [];
701
+ const turb = ops.turbulence;
702
+ for (const p of this.particles) {
703
+ p.life -= dt;
704
+ const frac = 1 - p.life / p.maxLife;
705
+ p.x += p.vx * dt;
706
+ p.y += p.vy * dt;
707
+ p.vx += g[0] * dt;
708
+ p.vy += g[1] * dt;
709
+ if (drag > 0) {
710
+ p.vx *= Math.max(0, 1 - drag * dt);
711
+ p.vy *= Math.max(0, 1 - drag * dt);
712
+ }
713
+ if (angDrag > 0) p.angVel *= Math.max(0, 1 - angDrag * dt);
714
+ p.angVel += angForce[2] * dt;
715
+ p.rot += p.angVel * dt;
716
+ if (oscPos !== void 0) {
717
+ const sw = Math.sin(this.time * p.oscFreq + p.oscPhase);
718
+ p.x += sw * oscPos.mask[0] * dt;
719
+ p.y += sw * oscPos.mask[1] * dt;
720
+ }
721
+ if (turb !== void 0) {
722
+ const phase = this.time * (turb.speedMin + (turb.speedMax - turb.speedMin) * .5) + p.phase;
723
+ p.x += Math.sin(phase) * turb.scale * 100 * dt;
724
+ p.y += Math.cos(phase * .7) * turb.scale * 100 * dt;
725
+ }
726
+ let a = 1;
727
+ if (fade !== void 0) {
728
+ const fadeIn = fade.fadeIn ?? 0;
729
+ const fadeOut = fade.fadeOut ?? 0;
730
+ if (fadeIn > 0 && frac < fadeIn) a = Math.min(a, frac / fadeIn);
731
+ if (fadeOut > 0) {
732
+ const tail = 1 - frac;
733
+ if (tail < fadeOut) a = Math.min(a, tail / fadeOut);
734
+ }
735
+ }
736
+ if (osc !== void 0) {
737
+ const s = Math.sin(this.time * osc.frequencyMax * Math.PI * 2 + p.phase);
738
+ a *= osc.scaleMin + (1 - osc.scaleMin) * Math.max(0, s);
739
+ }
740
+ for (const sc of sizeChanges) if (frac >= sc.startTime) {
741
+ const span = Math.max(1e-4, (sc.endTime ?? 1) - sc.startTime);
742
+ const t = Math.min(1, Math.max(0, (frac - sc.startTime) / span));
743
+ p.size = p.baseSize * (sc.startValue + (sc.endValue - sc.startValue) * t);
744
+ }
745
+ p.alpha = Math.max(0, Math.min(1, a));
746
+ }
747
+ this.particles = this.particles.filter((p) => p.life > 0);
748
+ }
749
+ /**
750
+ * 绘制(局部坐标 → 世界变换 → 画布)。
751
+ * 混合模式按材质 blending:translucent → alpha 混合(source-over,雾/雪等半透明);
752
+ * additive → 'lighter'(光效/火花)。t 为图层世界变换(含 parent 合并)。
753
+ * 粒子局部 y 向上 → 绘制时翻转。粒子颜色按 colorrandom 染色(缓存染色纹理)。
754
+ */
755
+ draw(ctx, ox, oy, s, t, tex) {
756
+ const lx = t.sx;
757
+ const ly = t.sy;
758
+ const px0 = ox + t.ox * s;
759
+ const py0 = oy + t.oy * s;
760
+ const additive = this.desc.blending === "additive";
761
+ ctx.save();
762
+ if (additive) ctx.globalCompositeOperation = "lighter";
763
+ for (const p of this.particles) {
764
+ const x = px0 + p.x * lx * s;
765
+ const y = py0 - p.y * ly * s;
766
+ const ps = Math.max(2, p.size * lx * s);
767
+ const aspect = tex.width / tex.height;
768
+ const pw = aspect >= 1 ? ps * aspect : ps;
769
+ const ph = aspect >= 1 ? ps : ps / aspect;
770
+ const img = this.tinted(tex, p.color);
771
+ ctx.globalAlpha = Math.max(0, Math.min(1, p.alpha));
772
+ if (p.rot !== 0) {
773
+ ctx.save();
774
+ ctx.translate(x, y);
775
+ ctx.rotate(p.rot);
776
+ ctx.drawImage(img, -pw / 2, -ph / 2, pw, ph);
777
+ ctx.restore();
778
+ } else ctx.drawImage(img, x - pw / 2, y - ph / 2, pw, ph);
779
+ }
780
+ ctx.restore();
781
+ }
782
+ /** 纹理染色(source-in 保留 alpha),按颜色缓存 */
783
+ tinted(tex, color) {
784
+ const key = color[0] + "," + color[1] + "," + color[2];
785
+ const hit = this.tintCache.get(key);
786
+ if (hit !== void 0) return hit;
787
+ const c = document.createElement("canvas");
788
+ c.width = tex.width;
789
+ c.height = tex.height;
790
+ const g = c.getContext("2d");
791
+ if (g !== null) {
792
+ g.drawImage(tex, 0, 0);
793
+ g.globalCompositeOperation = "source-in";
794
+ g.fillStyle = "rgb(" + color[0] + "," + color[1] + "," + color[2] + ")";
795
+ g.fillRect(0, 0, c.width, c.height);
796
+ }
797
+ this.tintCache.set(key, c);
798
+ return c;
799
+ }
800
+ spawn(em, ini) {
801
+ let x = 0;
802
+ let y = 0;
803
+ const [dx, dy] = em.directions;
804
+ if (em.type === "boxrandom") {
805
+ const d = Array.isArray(em.distanceMax) ? em.distanceMax : [
806
+ em.distanceMax,
807
+ em.distanceMax,
808
+ 0
809
+ ];
810
+ x = (Math.random() * 2 - 1) * d[0] * .5;
811
+ y = (Math.random() * 2 - 1) * d[1] * .5;
812
+ } else {
813
+ const maxD = typeof em.distanceMax === "number" ? em.distanceMax : Math.hypot(em.distanceMax[0], em.distanceMax[1]);
814
+ const dist = em.distanceMin + Math.random() * Math.max(0, maxD - em.distanceMin);
815
+ x = (Math.random() * 2 - 1) * dist * dx;
816
+ y = (Math.random() * 2 - 1) * dist * dy;
817
+ }
818
+ x += em.origin[0];
819
+ y += em.origin[1];
820
+ const life = rand(ini.lifetime ?? [1, 1]);
821
+ let size;
822
+ if (ini.size !== void 0) {
823
+ const [smn, smx] = ini.size;
824
+ const exp = ini.sizeExponent ?? 1;
825
+ size = (smn + Math.pow(Math.random(), exp) * Math.max(0, smx - smn)) * this.sizeScale;
826
+ } else size = 32 * this.sizeScale;
827
+ let vx = 0;
828
+ let vy = 0;
829
+ if (ini.velocityMin !== void 0 && ini.velocityMax !== void 0) {
830
+ vx = rand(ini.velocityMin[0], ini.velocityMax[0]);
831
+ vy = rand(ini.velocityMin[1], ini.velocityMax[1]);
832
+ }
833
+ if (ini.turbulentVelocity !== void 0) {
834
+ const tv = ini.turbulentVelocity;
835
+ vx += (Math.random() * 2 - 1) * Math.abs(tv.scale) * 100;
836
+ vy += (Math.random() * 2 - 1) * Math.abs(tv.scale) * 100;
837
+ }
838
+ const alpha = rand(ini.alphaMin ?? 1, ini.alphaMax ?? 1);
839
+ let cr = 255;
840
+ let cg = 255;
841
+ let cb = 255;
842
+ if (ini.colorMin !== void 0 && ini.colorMax !== void 0) {
843
+ cr = Math.round(rand(ini.colorMin[0], ini.colorMax[0]));
844
+ cg = Math.round(rand(ini.colorMin[1], ini.colorMax[1]));
845
+ cb = Math.round(rand(ini.colorMin[2], ini.colorMax[2]));
846
+ }
847
+ const rot = ini.rotation !== void 0 ? rand(ini.rotation[0], ini.rotation[1]) : 0;
848
+ const angVel = ini.angularVelocity !== void 0 ? rand(ini.angularVelocity[0], ini.angularVelocity[1]) : 0;
849
+ const ob = this.desc.overbright > 0 ? this.desc.overbright : 1;
850
+ cr = Math.min(255, Math.round(cr * ob));
851
+ cg = Math.min(255, Math.round(cg * ob));
852
+ cb = Math.min(255, Math.round(cb * ob));
853
+ const osc = this.desc.operators.oscillatePosition;
854
+ const oscFreq = osc !== void 0 ? rand(osc.frequencyMin, osc.frequencyMax) : 0;
855
+ const oscPhase = Math.random() * Math.PI * 2;
856
+ this.particles.push({
857
+ x,
858
+ y,
859
+ vx,
860
+ vy,
861
+ life,
862
+ maxLife: Math.max(.001, life),
863
+ baseSize: size,
864
+ size,
865
+ alpha,
866
+ color: [
867
+ cr,
868
+ cg,
869
+ cb
870
+ ],
871
+ rot,
872
+ angVel,
873
+ phase: Math.random() * Math.PI * 2,
874
+ oscPhase,
875
+ oscFreq
876
+ });
877
+ }
878
+ };
879
+ function rand(a, b) {
880
+ if (Array.isArray(a)) {
881
+ const [mn, mx] = a;
882
+ return mn + Math.random() * Math.max(0, mx - mn);
883
+ }
884
+ if (b === void 0) return a;
885
+ return a + Math.random() * Math.max(0, b - a);
886
+ }
887
+ //#endregion
888
+ //#region src/client/WaterwavesGL.ts
889
+ const VERT_SRC = `
890
+ attribute vec2 a_Pos;
891
+ varying vec2 v_UV;
892
+ void main() {
893
+ gl_Position = vec4(a_Pos, 0.0, 1.0);
894
+ v_UV = a_Pos * 0.5 + 0.5;
895
+ }
896
+ `;
897
+ const FRAG_SRC = `
898
+ precision mediump float;
899
+ // 独立实现的水波扰动(数学事实:沿某方向传播的正弦波 + 垂直方向扰动)。
900
+ // 行为参考 Wallpaper Engine 官方 waterwaves 效果(黑盒观察),代码为独立编写。
901
+ uniform sampler2D u_Src;
902
+ uniform sampler2D u_MaskTex;
903
+ uniform float u_UseMask;
904
+ uniform float u_MaskAlpha;
905
+ uniform float u_Clock;
906
+ uniform vec4 u_Params[4]; // x=方向角, y=速度, z=尺度, w=强度
907
+ uniform float u_Power[4]; // 波形指数
908
+ uniform int u_Count;
909
+ varying vec2 v_Uv;
910
+ void main() {
911
+ vec2 uv = v_Uv;
912
+ float gate = 1.0;
913
+ if (u_UseMask > 0.5) {
914
+ vec4 m = texture2D(u_MaskTex, uv);
915
+ gate = u_MaskAlpha > 0.5 ? m.a : m.r;
916
+ }
917
+ vec2 total = vec2(0.0);
918
+ for (int i = 0; i < 4; i++) {
919
+ if (i >= u_Count) break;
920
+ vec4 p = u_Params[i];
921
+ float sinA = sin(p.x);
922
+ float cosA = cos(p.x);
923
+ // 波相位沿 (-sinA, cosA) 方向随空间与时间变化
924
+ float phase = u_Clock * p.y + (uv.x * -sinA + uv.y * cosA) * p.z;
925
+ float wave = sin(phase);
926
+ // 扰动沿 (cosA, sinA),幅度为强度平方的指数波形
927
+ float amp = pow(abs(wave), u_Power[i]) * sign(wave) * p.w * p.w;
928
+ total += amp * vec2(cosA, sinA);
929
+ }
930
+ uv += total * gate;
931
+ gl_FragColor = texture2D(u_Src, uv);
932
+ }
933
+ `;
934
+ var WaterwavesGL = class {
935
+ canvas = null;
936
+ gl = null;
937
+ prog = null;
938
+ locs = {};
939
+ vbo = null;
940
+ texCache = /* @__PURE__ */ new Map();
941
+ curW = 0;
942
+ curH = 0;
943
+ /** WebGL 是否可用 */
944
+ static get available() {
945
+ try {
946
+ const c = document.createElement("canvas");
947
+ return !!(c.getContext("webgl") || c.getContext("experimental-webgl"));
948
+ } catch {
949
+ return false;
950
+ }
951
+ }
952
+ ensure() {
953
+ if (this.gl !== null && this.prog !== null) return true;
954
+ try {
955
+ const c = document.createElement("canvas");
956
+ const gl = c.getContext("webgl") || c.getContext("experimental-webgl");
957
+ if (gl === null) return false;
958
+ this.canvas = c;
959
+ this.gl = gl;
960
+ const compile = (type, src) => {
961
+ const sh = gl.createShader(type);
962
+ if (sh === null) return null;
963
+ gl.shaderSource(sh, src);
964
+ gl.compileShader(sh);
965
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
966
+ console.warn("waterwaves shader: " + gl.getShaderInfoLog(sh));
967
+ return null;
968
+ }
969
+ return sh;
970
+ };
971
+ const vs = compile(gl.VERTEX_SHADER, VERT_SRC);
972
+ const fs = compile(gl.FRAGMENT_SHADER, FRAG_SRC);
973
+ if (vs === null || fs === null) return false;
974
+ const prog = gl.createProgram();
975
+ if (prog === null) return false;
976
+ gl.attachShader(prog, vs);
977
+ gl.attachShader(prog, fs);
978
+ gl.linkProgram(prog);
979
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return false;
980
+ this.prog = prog;
981
+ gl.useProgram(prog);
982
+ for (const name of [
983
+ "u_Src",
984
+ "u_MaskTex",
985
+ "u_UseMask",
986
+ "u_MaskAlpha",
987
+ "u_Clock",
988
+ "u_Params",
989
+ "u_Power",
990
+ "u_Count"
991
+ ]) this.locs[name] = gl.getUniformLocation(prog, name);
992
+ const aPos = gl.getAttribLocation(prog, "a_Pos");
993
+ this.vbo = gl.createBuffer();
994
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
995
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
996
+ -1,
997
+ -1,
998
+ 1,
999
+ -1,
1000
+ -1,
1001
+ 1,
1002
+ 1,
1003
+ 1
1004
+ ]), gl.STATIC_DRAW);
1005
+ gl.enableVertexAttribArray(aPos);
1006
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
1007
+ return true;
1008
+ } catch {
1009
+ return false;
1010
+ }
1011
+ }
1012
+ uploadTexture(key, src, w, h) {
1013
+ const gl = this.gl;
1014
+ if (gl === null) return null;
1015
+ const hit = this.texCache.get(key);
1016
+ if (hit !== void 0) return hit;
1017
+ const tex = gl.createTexture();
1018
+ if (tex === null) return null;
1019
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1020
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
1021
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
1022
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1023
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1024
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1025
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1026
+ this.texCache.set(key, tex);
1027
+ return tex;
1028
+ }
1029
+ /**
1030
+ * 渲染 waterwaves 效果到离屏 WebGL canvas(逐像素 UV 场扰动)。
1031
+ * src:图层纹理;mask:mask 纹理(null = 无);maskUseA:mask 用 A 通道(R8 alpha 语义)。
1032
+ */
1033
+ render(src, w, h, mask, maskUseA, waves, time, key) {
1034
+ if (!this.ensure()) return null;
1035
+ const gl = this.gl;
1036
+ const prog = this.prog;
1037
+ if (gl === null || prog === null || this.canvas === null) return null;
1038
+ if (this.curW !== w || this.curH !== h) {
1039
+ this.canvas.width = w;
1040
+ this.canvas.height = h;
1041
+ this.curW = w;
1042
+ this.curH = h;
1043
+ }
1044
+ gl.viewport(0, 0, w, h);
1045
+ gl.useProgram(prog);
1046
+ const tex = this.uploadTexture("tex:" + key, src, w, h);
1047
+ if (tex === null) return null;
1048
+ gl.activeTexture(gl.TEXTURE0);
1049
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1050
+ gl.uniform1i(this.locs["u_Src"], 0);
1051
+ if (mask !== null) {
1052
+ const mtex = this.uploadTexture("mask:" + key, mask, 0, 0);
1053
+ gl.activeTexture(gl.TEXTURE1);
1054
+ gl.bindTexture(gl.TEXTURE_2D, mtex);
1055
+ gl.uniform1i(this.locs["u_MaskTex"], 1);
1056
+ gl.uniform1f(this.locs["u_UseMask"], 1);
1057
+ gl.uniform1f(this.locs["u_MaskAlpha"], maskUseA ? 1 : 0);
1058
+ } else gl.uniform1f(this.locs["u_UseMask"], 0);
1059
+ gl.uniform1f(this.locs["u_Clock"], time);
1060
+ const wv = [];
1061
+ const ex = [];
1062
+ const n = Math.min(4, waves.length);
1063
+ for (let i = 0; i < 4; i++) if (i < n) {
1064
+ wv.push(waves[i].direction, waves[i].speed, waves[i].scale, waves[i].strength);
1065
+ ex.push(Math.max(.5, Math.min(4, waves[i].exponent)));
1066
+ } else {
1067
+ wv.push(0, 0, 0, 0);
1068
+ ex.push(1);
1069
+ }
1070
+ gl.uniform4fv(this.locs["u_Params"], new Float32Array(wv));
1071
+ gl.uniform1fv(this.locs["u_Power"], new Float32Array(ex));
1072
+ gl.uniform1i(this.locs["u_Count"], n);
1073
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
1074
+ return this.canvas;
1075
+ }
1076
+ /** 释放(场景切换时) */
1077
+ dispose() {
1078
+ for (const t of this.texCache.values()) if (this.gl !== null) this.gl.deleteTexture(t);
1079
+ this.texCache.clear();
1080
+ }
1081
+ };
1082
+ //#endregion
1083
+ //#region src/client/SceneModelRenderer.ts
1084
+ /**
1085
+ * puppet 网格离屏渲染:把部件网格(三角形 + UV 纹理)渲染一次到离屏 canvas。
1086
+ * 模型空间(y-up,原点=图片中心)→ canvas 像素(y 向下):
1087
+ * x_c = x_m, y_c = -y_m(绘制时经场景变换把图片中心对齐图层锚点)。
1088
+ * UV v 翻转(模型 v-up → 纹理 v-down)。
1089
+ * 每三角形:clip 路径 + 仿射变换(UV 三角 → 位置三角)+ drawImage 纹理。
1090
+ * anim 可选:{rot, bx, by} = root 骨骼旋转(绕骨骼 0 bind 位置旋转的蒙皮)——
1091
+ * 顶点 skinPos = w0 × Rz(rot; bx,by) × pos + (1-w0) × pos(骨骼 1-3 权重静态 = raw)。
1092
+ */
1093
+ function buildMeshCanvas(mesh, tex, anim) {
1094
+ const posArr = [];
1095
+ if (anim !== void 0 && anim !== null) {
1096
+ const c = Math.cos(anim.rot);
1097
+ const sn = Math.sin(anim.rot);
1098
+ const bx = anim.bx;
1099
+ const by = anim.by;
1100
+ for (const v of mesh.vertices) {
1101
+ const w0 = v.weights !== void 0 ? v.weights[0] ?? 0 : 0;
1102
+ const rx = bx + c * (v.pos[0] - bx) - sn * (v.pos[1] - by);
1103
+ const ry = by + sn * (v.pos[0] - bx) + c * (v.pos[1] - by);
1104
+ posArr.push([w0 * rx + (1 - w0) * v.pos[0], w0 * ry + (1 - w0) * v.pos[1]]);
1105
+ }
1106
+ } else for (const v of mesh.vertices) posArr.push([v.pos[0], v.pos[1]]);
1107
+ let mnx = Infinity;
1108
+ let mny = Infinity;
1109
+ let mxx = -Infinity;
1110
+ let mxy = -Infinity;
1111
+ for (const [x, y] of posArr) {
1112
+ const yy = -y;
1113
+ if (x < mnx) mnx = x;
1114
+ if (yy < mny) mny = yy;
1115
+ if (x > mxx) mxx = x;
1116
+ if (yy > mxy) mxy = yy;
1117
+ }
1118
+ const c0 = document.createElement("canvas");
1119
+ c0.width = 1;
1120
+ c0.height = 1;
1121
+ if (!Number.isFinite(mnx) || mxx - mnx > 2e4 || mxy - mny > 2e4) return {
1122
+ canvas: c0,
1123
+ originX: 0,
1124
+ originY: 0
1125
+ };
1126
+ const pad = 4;
1127
+ const cw = Math.max(1, Math.ceil(mxx - mnx) + 8);
1128
+ const ch = Math.max(1, Math.ceil(mxy - mny) + 8);
1129
+ const c = document.createElement("canvas");
1130
+ c.width = cw;
1131
+ c.height = ch;
1132
+ const g = c.getContext("2d");
1133
+ if (g === null) return {
1134
+ canvas: c,
1135
+ originX: pad - mnx,
1136
+ originY: pad - mny
1137
+ };
1138
+ g.translate(pad - mnx, pad - mny);
1139
+ const tw = tex.width;
1140
+ const th = tex.height;
1141
+ const verts = mesh.vertices;
1142
+ const idx = mesh.indices;
1143
+ for (let i = 0; i + 2 < idx.length; i += 3) {
1144
+ const a = verts[idx[i]];
1145
+ const b = verts[idx[i + 1]];
1146
+ const cc = verts[idx[i + 2]];
1147
+ if (a === void 0 || b === void 0 || cc === void 0) continue;
1148
+ const fv = (val) => (mesh.flipV ? 1 - val : val) * th;
1149
+ const u0 = a.uv[0] * tw;
1150
+ const v0 = fv(a.uv[1]);
1151
+ const u1 = b.uv[0] * tw;
1152
+ const v1 = fv(b.uv[1]);
1153
+ const u2 = cc.uv[0] * tw;
1154
+ const v2 = fv(cc.uv[1]);
1155
+ const x0 = posArr[idx[i]][0];
1156
+ const y0 = -posArr[idx[i]][1];
1157
+ const x1 = posArr[idx[i + 1]][0];
1158
+ const y1 = -posArr[idx[i + 1]][1];
1159
+ const x2 = posArr[idx[i + 2]][0];
1160
+ const y2 = -posArr[idx[i + 2]][1];
1161
+ const det = (u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0);
1162
+ if (Math.abs(det) < 1e-9) continue;
1163
+ g.save();
1164
+ g.beginPath();
1165
+ g.moveTo(x0, y0);
1166
+ g.lineTo(x1, y1);
1167
+ g.lineTo(x2, y2);
1168
+ g.closePath();
1169
+ g.clip();
1170
+ const m00 = ((x1 - x0) * (v2 - v0) - (x2 - x0) * (v1 - v0)) / det;
1171
+ const m01 = ((u1 - u0) * (x2 - x0) - (u2 - u0) * (x1 - x0)) / det;
1172
+ const m10 = ((y1 - y0) * (v2 - v0) - (y2 - y0) * (v1 - v0)) / det;
1173
+ const m11 = ((u1 - u0) * (y2 - y0) - (u2 - u0) * (y1 - y0)) / det;
1174
+ g.transform(m00, m10, m01, m11, x0 - m00 * u0 - m01 * v0, y0 - m10 * u0 - m11 * v0);
1175
+ g.drawImage(tex, 0, 0);
1176
+ g.restore();
1177
+ }
1178
+ return {
1179
+ canvas: c,
1180
+ originX: pad - mnx,
1181
+ originY: pad - mny
1182
+ };
1183
+ }
1184
+ /**
1185
+ * 粒子纹理径向软边合成:中心不衰减,边缘 30% 区间线性淡出到透明。
1186
+ * 用于雾/雪/光晕类粒子,避免硬边方块在大尺寸 + additive 下叠加成"白线"。
1187
+ */
1188
+ function makeSoftTexture(src) {
1189
+ const w = src.width;
1190
+ const h = src.height;
1191
+ const c = document.createElement("canvas");
1192
+ c.width = w;
1193
+ c.height = h;
1194
+ const g = c.getContext("2d");
1195
+ if (g === null) return c;
1196
+ g.drawImage(src, 0, 0);
1197
+ const cx = w / 2;
1198
+ const cy = h / 2;
1199
+ const r = Math.max(1, Math.min(w, h) / 2);
1200
+ const grad = g.createRadialGradient(cx, cy, r * .65, cx, cy, r);
1201
+ grad.addColorStop(0, "rgba(255,255,255,1)");
1202
+ grad.addColorStop(1, "rgba(255,255,255,0)");
1203
+ g.globalCompositeOperation = "destination-in";
1204
+ g.fillStyle = grad;
1205
+ g.fillRect(0, 0, w, h);
1206
+ return c;
1207
+ }
1208
+ /**
1209
+ * waterwaves 效果(Canvas2D 条带近似),对照官方 shader:
1210
+ * vert: v_Direction = rotateVec2((0,1), θ) = (-sinθ, cosθ) ← 传播方向
1211
+ * frag: distance = t*speed + dot(uv, v_Direction)*scale
1212
+ * offset = (v_Direction.y, -v_Direction.x) = (cosθ, sinθ) ← 扰动方向
1213
+ * texCoord += sign(sin)^exp * |sin|^exp * strength² * offset * mask
1214
+ * 条带 = 等 phase 线(垂直 v_Direction,即沿 offset),带内沿 offset 整体平移;
1215
+ * 多个 waterwaves(ww1-ww4)扰动叠加;mask 限制扰动区域。
1216
+ */
1217
+ function applyWaterwaves(src, w, h, waves, time, mask) {
1218
+ const c = document.createElement("canvas");
1219
+ c.width = w;
1220
+ c.height = h;
1221
+ const g = c.getContext("2d");
1222
+ if (g === null) return c;
1223
+ const theta = waves[0].direction;
1224
+ const offx = Math.cos(theta);
1225
+ const offy = Math.sin(theta);
1226
+ const bands = w * h > 9e5 ? 32 : 48;
1227
+ const horizontal = Math.abs(offx) >= Math.abs(offy);
1228
+ let maskAvg = null;
1229
+ if (mask !== null && mask !== void 0) {
1230
+ const mc = document.createElement("canvas");
1231
+ mc.width = 64;
1232
+ mc.height = 64;
1233
+ const mg = mc.getContext("2d");
1234
+ if (mg !== null) {
1235
+ mg.drawImage(mask, 0, 0, 64, 64);
1236
+ const img = mg.getImageData(0, 0, 64, 64);
1237
+ maskAvg = [];
1238
+ for (let i = 0; i < bands; i++) {
1239
+ let sumR = 0;
1240
+ let sumA = 0;
1241
+ let cnt = 0;
1242
+ if (horizontal) {
1243
+ const x0 = Math.floor(i / bands * 64);
1244
+ const x1 = Math.max(x0 + 1, Math.floor((i + 1) / bands * 64));
1245
+ for (let x = x0; x < x1; x++) for (let y = 0; y < 64; y++) {
1246
+ sumR += img.data[(y * 64 + x) * 4];
1247
+ sumA += img.data[(y * 64 + x) * 4 + 3];
1248
+ cnt++;
1249
+ }
1250
+ } else {
1251
+ const y0 = Math.floor(i / bands * 64);
1252
+ const y1 = Math.max(y0 + 1, Math.floor((i + 1) / bands * 64));
1253
+ for (let y = y0; y < y1; y++) for (let x = 0; x < 64; x++) {
1254
+ sumR += img.data[(y * 64 + x) * 4];
1255
+ sumA += img.data[(y * 64 + x) * 4 + 3];
1256
+ cnt++;
1257
+ }
1258
+ }
1259
+ const useA = sumR >= cnt * 254;
1260
+ maskAvg.push(cnt > 0 ? (useA ? sumA : sumR) / cnt / 255 : 0);
1261
+ }
1262
+ }
1263
+ }
1264
+ if (horizontal) {
1265
+ const bw = w / bands;
1266
+ for (let i = 0; i < bands; i++) {
1267
+ const x0 = i * bw;
1268
+ const cx = (x0 + bw / 2) / w;
1269
+ let disp = 0;
1270
+ for (const p of waves) {
1271
+ const s = p.strength * p.strength;
1272
+ const e = Math.max(.5, Math.min(4, p.exponent));
1273
+ const phase = time * p.speed + (cx * -Math.sin(p.direction) + .5 * Math.cos(p.direction)) * p.scale;
1274
+ const val = Math.sin(phase);
1275
+ disp += Math.sign(val) * Math.pow(Math.abs(val), e) * s * Math.cos(p.direction) * w;
1276
+ }
1277
+ disp *= maskAvg !== null ? maskAvg[i] : 1;
1278
+ g.drawImage(src, x0, 0, bw + .5, h, x0 + disp, 0, bw + .5, h);
1279
+ }
1280
+ } else {
1281
+ const bh = h / bands;
1282
+ for (let i = 0; i < bands; i++) {
1283
+ const y0 = i * bh;
1284
+ const cy = (y0 + bh / 2) / h;
1285
+ let disp = 0;
1286
+ for (const p of waves) {
1287
+ const s = p.strength * p.strength;
1288
+ const e = Math.max(.5, Math.min(4, p.exponent));
1289
+ const phase = time * p.speed + (.5 * -Math.sin(p.direction) + cy * Math.cos(p.direction)) * p.scale;
1290
+ const val = Math.sin(phase);
1291
+ disp += Math.sign(val) * Math.pow(Math.abs(val), e) * s * Math.sin(p.direction) * h;
1292
+ }
1293
+ disp *= maskAvg !== null ? maskAvg[i] : 1;
1294
+ g.drawImage(src, 0, y0, w, bh + .5, 0, y0 + disp, w, bh + .5);
1295
+ }
1296
+ }
1297
+ return c;
1298
+ }
1299
+ var SceneModelRenderer = class {
1300
+ el = null;
1301
+ ctx = null;
1302
+ model = null;
1303
+ base = null;
1304
+ layerTextures = /* @__PURE__ */ new Map();
1305
+ /** 效果 mask 纹理(waterwaves/shake opacitymask)+ 通道模式(true=R8 alpha 语义用 A) */
1306
+ effectMasks = /* @__PURE__ */ new Map();
1307
+ /** WebGL waterwaves 渲染器(惰性创建) */
1308
+ wwGL = null;
1309
+ /** 图层纹理的 Image 内容区域尺寸(tex 画布内左上角);无则用位图原生尺寸 */
1310
+ layerTexImage = /* @__PURE__ */ new Map();
1311
+ /** 图层世界变换(递归 parent 合并;局部 y-up 翻转) */
1312
+ worldTransform = /* @__PURE__ */ new Map();
1313
+ /** 图层 id → 图层(链式查找 puppet 祖先用) */
1314
+ byId = /* @__PURE__ */ new Map();
1315
+ runtimes = /* @__PURE__ */ new Map();
1316
+ particleTextures = /* @__PURE__ */ new Map();
1317
+ /** puppet 动画状态:puppet 图层 id → { 动画, 播放时间 } */
1318
+ puppetAnims = /* @__PURE__ */ new Map();
1319
+ /** 每帧计算的动画变换:puppet 图层 id → 平移/旋转 */
1320
+ animXform = /* @__PURE__ */ new Map();
1321
+ /** puppet 网格离屏渲染缓存:图层 id → { canvas, 模型原点 } */
1322
+ meshCanvases = /* @__PURE__ */ new Map();
1323
+ dpr = 1;
1324
+ live = false;
1325
+ closed = false;
1326
+ rafId = 0;
1327
+ lastT = 0;
1328
+ /** 全局动画时间(秒,effects/粒子用) */
1329
+ animTime = 0;
1330
+ blurPx = 0;
1331
+ scale = 1;
1332
+ monitor = "";
1333
+ version = 0;
1334
+ handlers;
1335
+ constructor(handlers = {}) {
1336
+ this.handlers = handlers;
1337
+ }
1338
+ get isLive() {
1339
+ return this.live;
1340
+ }
1341
+ start(monitor, version) {
1342
+ this.stop();
1343
+ this.closed = false;
1344
+ this.monitor = monitor;
1345
+ this.version = version;
1346
+ this.el = document.createElement("canvas");
1347
+ this.el.style.position = "fixed";
1348
+ this.el.style.top = "0";
1349
+ this.el.style.left = "0";
1350
+ this.el.style.width = "100%";
1351
+ this.el.style.height = "100%";
1352
+ this.el.style.zIndex = "-2";
1353
+ this.el.style.pointerEvents = "none";
1354
+ this.el.style.border = "0";
1355
+ document.body.appendChild(this.el);
1356
+ this.ctx = this.el.getContext("2d");
1357
+ this.resize();
1358
+ this.applyVisuals();
1359
+ window.addEventListener("resize", this.onResize);
1360
+ document.addEventListener("visibilitychange", this.onVisibility);
1361
+ this.load();
1362
+ }
1363
+ stop() {
1364
+ this.closed = true;
1365
+ if (this.rafId !== 0) {
1366
+ cancelAnimationFrame(this.rafId);
1367
+ this.rafId = 0;
1368
+ }
1369
+ window.removeEventListener("resize", this.onResize);
1370
+ document.removeEventListener("visibilitychange", this.onVisibility);
1371
+ if (this.el !== null) {
1372
+ this.el.remove();
1373
+ this.el = null;
1374
+ this.ctx = null;
1375
+ }
1376
+ this.model = null;
1377
+ this.base = null;
1378
+ for (const bmp of this.layerTextures.values()) try {
1379
+ bmp.close();
1380
+ } catch {}
1381
+ this.layerTextures.clear();
1382
+ this.layerTexImage.clear();
1383
+ this.worldTransform.clear();
1384
+ this.byId.clear();
1385
+ this.puppetAnims.clear();
1386
+ this.animXform.clear();
1387
+ this.meshCanvases.clear();
1388
+ for (const v of this.effectMasks.values()) try {
1389
+ if ("close" in v.bmp) v.bmp.close();
1390
+ } catch {}
1391
+ this.effectMasks.clear();
1392
+ if (this.wwGL !== null) {
1393
+ this.wwGL.dispose();
1394
+ this.wwGL = null;
1395
+ }
1396
+ for (const bmp of this.particleTextures.values()) try {
1397
+ if ("close" in bmp) bmp.close();
1398
+ } catch {}
1399
+ this.particleTextures.clear();
1400
+ this.runtimes.clear();
1401
+ this.setLive(false);
1402
+ }
1403
+ applyVisuals(blurPx, scale) {
1404
+ if (blurPx !== void 0) this.blurPx = blurPx;
1405
+ if (scale !== void 0) this.scale = scale;
1406
+ if (this.el !== null) {
1407
+ this.el.style.filter = "blur(" + Math.round(this.blurPx) + "px)";
1408
+ this.el.style.transform = "scale(" + this.scale.toFixed(3) + ")";
1409
+ }
1410
+ }
1411
+ async load() {
1412
+ if (this.closed) return;
1413
+ let model;
1414
+ try {
1415
+ const res = await fetch("/we-sync/scene/model?monitor=" + encodeURIComponent(this.monitor) + "&v=" + this.version, { cache: "no-store" });
1416
+ if (!res.ok) throw new Error("model " + res.status);
1417
+ model = await res.json();
1418
+ } catch {
1419
+ this.fail();
1420
+ return;
1421
+ }
1422
+ if (this.closed) return;
1423
+ this.model = model;
1424
+ this.byId.clear();
1425
+ for (const l of model.layers) this.byId.set(l.id, l);
1426
+ this.computeWorldTransforms();
1427
+ this.setLive(true);
1428
+ this.loadBase(model);
1429
+ const jobs = [];
1430
+ for (const layer of model.layers) jobs.push(this.loadLayerTexture(layer));
1431
+ for (const layer of model.layers) if (layer.particle !== null && layer.particle.textureNames.length > 0) {
1432
+ this.runtimes.set(layer.id, new ParticleRuntime(layer.particle, model.particleRateScale, model.particleSizeScale));
1433
+ jobs.push(this.loadParticleTexture(layer.id, layer.particle.textureNames[0]));
1434
+ }
1435
+ for (const layer of model.layers) {
1436
+ if (layer.puppet === null || layer.puppet.animations.length === 0) continue;
1437
+ const anim = layer.animationIds.length > 0 ? layer.puppet.animations.find((a) => layer.animationIds.includes(a.id)) ?? layer.puppet.animations[0] : layer.puppet.animations[0];
1438
+ if (anim.keyframes.length < 2) continue;
1439
+ const kf = anim.keyframes;
1440
+ let maxSpan = 0;
1441
+ for (let vi = 0; vi < 8; vi++) {
1442
+ let mn = Infinity;
1443
+ let mx = -Infinity;
1444
+ for (const k of kf) {
1445
+ const v = k.values[vi];
1446
+ if (!Number.isFinite(v)) continue;
1447
+ if (v < mn) mn = v;
1448
+ if (v > mx) mx = v;
1449
+ }
1450
+ if (Number.isFinite(mn) && mx - mn > maxSpan) maxSpan = mx - mn;
1451
+ }
1452
+ if (maxSpan < .01) continue;
1453
+ this.puppetAnims.set(layer.id, {
1454
+ anim,
1455
+ time: 0
1456
+ });
1457
+ }
1458
+ if (jobs.length > 0) await Promise.all(jobs);
1459
+ if (!this.closed) this.startAnimation();
1460
+ }
1461
+ /**
1462
+ * 递归合并 parent 层级变换(含 attachment 骨骼挂载)。
1463
+ * 顶层(无 parent):WE 场景坐标 **y 向上** → 屏幕 y = 场景高 - origin.y。
1464
+ * 子图层:局部坐标 y 向上,父 scale 施加于子的位移与尺寸。
1465
+ * attachment(如 "head"/"Attachment"):子层挂到 parent puppet 的具名骨骼,
1466
+ * 锚点 = parent 锚点 + 骨骼局部位置(y-up)+ 子层 origin。
1467
+ */
1468
+ computeWorldTransforms() {
1469
+ const model = this.model;
1470
+ if (model === null) return;
1471
+ const H = model.height;
1472
+ const byId = /* @__PURE__ */ new Map();
1473
+ for (const l of model.layers) byId.set(l.id, l);
1474
+ const cache = /* @__PURE__ */ new Map();
1475
+ const walk = (l) => {
1476
+ const hit = cache.get(l.id);
1477
+ if (hit !== void 0) return hit;
1478
+ let t;
1479
+ const parent = l.parent !== null ? byId.get(l.parent) : void 0;
1480
+ if (parent !== void 0) {
1481
+ const p = walk(parent);
1482
+ const bp = l.attachment !== null && parent.puppet !== null ? parent.puppet.bonePositions?.[l.attachment] : void 0;
1483
+ t = {
1484
+ ox: p.ox + p.sx * (l.origin[0] + (bp !== void 0 ? bp[0] : 0)),
1485
+ oy: p.oy - p.sy * (l.origin[1] + (bp !== void 0 ? bp[1] : 0)),
1486
+ sx: p.sx * (l.scale[0] ?? 1),
1487
+ sy: p.sy * (l.scale[1] ?? 1)
1488
+ };
1489
+ } else t = {
1490
+ ox: l.origin[0],
1491
+ oy: H - l.origin[1],
1492
+ sx: l.scale[0] ?? 1,
1493
+ sy: l.scale[1] ?? 1
1494
+ };
1495
+ cache.set(l.id, t);
1496
+ return t;
1497
+ };
1498
+ for (const l of model.layers) walk(l);
1499
+ this.worldTransform = cache;
1500
+ }
1501
+ async loadParticleTexture(layerId, name) {
1502
+ try {
1503
+ const res = await fetch("/we-sync/asset/texture?name=" + encodeURIComponent(name), { cache: "no-store" });
1504
+ if (!res.ok) return;
1505
+ const blob = await res.blob();
1506
+ const bmp = await createImageBitmap(blob);
1507
+ if (this.closed) {
1508
+ bmp.close();
1509
+ return;
1510
+ }
1511
+ let tex = bmp;
1512
+ if (bmp.width < 128 && bmp.height < 128) {
1513
+ tex = makeSoftTexture(bmp);
1514
+ bmp.close();
1515
+ }
1516
+ if (this.closed) return;
1517
+ this.particleTextures.set(layerId, tex);
1518
+ } catch {}
1519
+ }
1520
+ async loadLayerTexture(layer) {
1521
+ if (this.layerTextures.has(layer.id)) return;
1522
+ const candidates = layer.decodableTexture !== null ? [layer.decodableTexture, ...layer.textureRefs.filter((t) => t !== layer.decodableTexture)] : layer.textureRefs;
1523
+ for (const name of candidates) {
1524
+ if (this.closed) return;
1525
+ const got = await this.fetchTexture(name);
1526
+ if (got === null) continue;
1527
+ if (this.closed) {
1528
+ got.bmp.close();
1529
+ return;
1530
+ }
1531
+ this.layerTextures.set(layer.id, got.bmp);
1532
+ if (got.imgW > 0 && got.imgH > 0) this.layerTexImage.set(layer.id, [got.imgW, got.imgH]);
1533
+ this.startAnimation();
1534
+ return;
1535
+ }
1536
+ for (const e of layer.effects) {
1537
+ const m = e.type === "waterwaves" || e.type === "shake" ? e.mask : null;
1538
+ if (m === null || this.effectMasks.has(layer.id)) continue;
1539
+ try {
1540
+ const res = await fetch("/we-sync/scene/texture?monitor=" + encodeURIComponent(this.monitor) + "&name=" + encodeURIComponent(m), { cache: "no-store" });
1541
+ if (!res.ok) continue;
1542
+ const blob = await res.blob();
1543
+ const bmp = await createImageBitmap(blob);
1544
+ if (this.closed) {
1545
+ bmp.close();
1546
+ return;
1547
+ }
1548
+ let useA = false;
1549
+ let flowDir = [0, -1];
1550
+ try {
1551
+ const tc = document.createElement("canvas");
1552
+ tc.width = 16;
1553
+ tc.height = 16;
1554
+ const tg = tc.getContext("2d");
1555
+ if (tg !== null) {
1556
+ tg.drawImage(bmp, 0, 0, 16, 16);
1557
+ const px = tg.getImageData(0, 0, 16, 16);
1558
+ let all255 = true;
1559
+ let sr = 0;
1560
+ let sg = 0;
1561
+ let n = 0;
1562
+ for (let i = 0; i < px.data.length; i += 4) {
1563
+ if (px.data[i] < 254) all255 = false;
1564
+ sr += px.data[i];
1565
+ sg += px.data[i + 1];
1566
+ n++;
1567
+ }
1568
+ useA = all255;
1569
+ if (!all255 && n > 0) {
1570
+ flowDir = [(sr / n / 255 - .498) * 2, (sg / n / 255 - .498) * 2];
1571
+ const len = Math.hypot(flowDir[0], flowDir[1]);
1572
+ if (len > .01) {
1573
+ flowDir[0] /= len;
1574
+ flowDir[1] /= len;
1575
+ }
1576
+ }
1577
+ }
1578
+ } catch {}
1579
+ this.effectMasks.set(layer.id, {
1580
+ bmp,
1581
+ useA,
1582
+ flowDir
1583
+ });
1584
+ this.startAnimation();
1585
+ } catch {}
1586
+ }
1587
+ }
1588
+ async fetchTexture(name) {
1589
+ try {
1590
+ const res = await fetch("/we-sync/scene/texture?monitor=" + encodeURIComponent(this.monitor) + "&name=" + encodeURIComponent(name), { cache: "no-store" });
1591
+ if (!res.ok) return null;
1592
+ const blob = await res.blob();
1593
+ const bmp = await createImageBitmap(blob);
1594
+ const imgW = Number(res.headers.get("X-WE-Image-W"));
1595
+ const imgH = Number(res.headers.get("X-WE-Image-H"));
1596
+ return {
1597
+ bmp,
1598
+ imgW: Number.isFinite(imgW) && imgW > 0 ? imgW : bmp.width,
1599
+ imgH: Number.isFinite(imgH) && imgH > 0 ? imgH : bmp.height
1600
+ };
1601
+ } catch {
1602
+ return null;
1603
+ }
1604
+ }
1605
+ async loadBase(model) {
1606
+ try {
1607
+ const res = await fetch("/we-sync/preview?v=" + this.version, { cache: "no-store" });
1608
+ if (!res.ok) return;
1609
+ const blob = await res.blob();
1610
+ const img = new Image();
1611
+ img.src = URL.createObjectURL(blob);
1612
+ await new Promise((resolve, reject) => {
1613
+ img.onload = () => resolve();
1614
+ img.onerror = () => reject(/* @__PURE__ */ new Error("preview decode"));
1615
+ });
1616
+ if (this.closed) {
1617
+ URL.revokeObjectURL(img.src);
1618
+ return;
1619
+ }
1620
+ this.base = img;
1621
+ this.startAnimation();
1622
+ } catch {}
1623
+ }
1624
+ fail() {
1625
+ this.setLive(false);
1626
+ this.closed = true;
1627
+ }
1628
+ startAnimation() {
1629
+ if (this.rafId === 0 && !document.hidden) {
1630
+ this.lastT = performance.now();
1631
+ this.rafId = requestAnimationFrame(this.draw);
1632
+ }
1633
+ }
1634
+ draw = () => {
1635
+ this.rafId = 0;
1636
+ if (this.closed || this.ctx === null || this.el === null) return;
1637
+ const now = performance.now();
1638
+ const dt = Math.min(.1, (now - this.lastT) / 1e3);
1639
+ this.lastT = now;
1640
+ this.animTime += dt;
1641
+ for (const rt of this.runtimes.values()) rt.update(dt);
1642
+ this.updatePuppetAnims(dt);
1643
+ this.renderScene();
1644
+ this.rafId = requestAnimationFrame(this.draw);
1645
+ };
1646
+ /**
1647
+ * 更新 puppet 动画 → 部件变换(装配根整体呼吸 + 部件自身摆动)。
1648
+ * 帧值布局(实测):[pos3][rotZ(v4)][scale3];v4 摆动 = 绕 z 旋转(呼吸/头发/草);
1649
+ * v0/v1(或 v6/v7,petal 类)变化 = 位置位移(相对首帧)。
1650
+ */
1651
+ updatePuppetAnims(dt) {
1652
+ this.animXform.clear();
1653
+ for (const [layerId, st] of this.puppetAnims) {
1654
+ st.time += dt;
1655
+ const kf = st.anim.keyframes;
1656
+ if (kf.length === 0) continue;
1657
+ let peak = 0;
1658
+ for (let i = 1; i < kf.length; i++) if (kf[i].t > kf[peak].t) peak = i;
1659
+ const period = kf[peak].t - kf[0].t;
1660
+ if (period > 5e6) continue;
1661
+ const dur = st.anim.duration > 0 ? st.anim.duration : 3;
1662
+ const t = period > 0 ? st.time * period / dur : st.time * (kf.length - 1) / dur;
1663
+ const s = sampleAnimation(st.anim, t);
1664
+ if (s === null) continue;
1665
+ const v = s.values;
1666
+ const base = st.anim.keyframes[0].values;
1667
+ const spans = [
1668
+ 0,
1669
+ 0,
1670
+ 0,
1671
+ 0,
1672
+ 0,
1673
+ 0,
1674
+ 0,
1675
+ 0
1676
+ ];
1677
+ for (let vi = 0; vi < 8; vi++) {
1678
+ let mn = Infinity;
1679
+ let mx = -Infinity;
1680
+ for (const k of kf) {
1681
+ const val = k.values[vi];
1682
+ if (!Number.isFinite(val)) continue;
1683
+ if (val < mn) mn = val;
1684
+ if (val > mx) mx = val;
1685
+ }
1686
+ if (Number.isFinite(mn)) spans[vi] = mx - mn;
1687
+ }
1688
+ const qx = v[3];
1689
+ const qy = v[4];
1690
+ const qz = v[5];
1691
+ const qw = v[6];
1692
+ const qlen2 = qx * qx + qy * qy + qz * qz + qw * qw;
1693
+ let rot;
1694
+ if (Math.abs(qlen2 - 1) < .05) rot = 2 * Math.atan2(qz, qw);
1695
+ else rot = v[4];
1696
+ let dx = 0;
1697
+ let dy = 0;
1698
+ if (spans[0] > .5) dy += v[0] - base[0];
1699
+ if (spans[6] > .5) dx += v[6] - base[6];
1700
+ if (spans[7] > .5) dy += v[7] - base[7];
1701
+ this.animXform.set(layerId, {
1702
+ dx,
1703
+ dy,
1704
+ rot
1705
+ });
1706
+ }
1707
+ }
1708
+ renderScene() {
1709
+ const ctx = this.ctx;
1710
+ if (ctx === null || this.el === null) return;
1711
+ const cw = this.el.clientWidth;
1712
+ const ch = this.el.clientHeight;
1713
+ if (cw === 0 || ch === 0) return;
1714
+ ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1715
+ ctx.clearRect(0, 0, cw, ch);
1716
+ const model = this.model;
1717
+ if (model === null) return;
1718
+ if (this.base !== null) this.drawCoverBase(ctx, this.base, cw, ch);
1719
+ else if (model.clearColor !== null) {
1720
+ ctx.fillStyle = "rgb(" + Math.round(model.clearColor[0] * 255) + "," + Math.round(model.clearColor[1] * 255) + "," + Math.round(model.clearColor[2] * 255) + ")";
1721
+ ctx.fillRect(0, 0, cw, ch);
1722
+ }
1723
+ const s = Math.max(cw / model.width, ch / model.height);
1724
+ const ox = (cw - model.width * s) / 2;
1725
+ const oy = (ch - model.height * s) / 2;
1726
+ for (const layer of model.layers) {
1727
+ if (!layer.visible || layer.alpha <= 0) continue;
1728
+ const t = this.worldTransform.get(layer.id);
1729
+ let ax = 0;
1730
+ let ay = 0;
1731
+ let arot = 0;
1732
+ const selfXf = this.animXform.get(layer.id);
1733
+ if (selfXf !== void 0 && t !== void 0) {
1734
+ ax = selfXf.dx;
1735
+ ay = -selfXf.dy;
1736
+ arot = selfXf.rot;
1737
+ } else if (layer.parent !== null) {
1738
+ let anchorId = null;
1739
+ let p = layer.parent;
1740
+ while (p !== null && this.byId.has(p)) {
1741
+ if (this.animXform.has(p)) {
1742
+ anchorId = p;
1743
+ break;
1744
+ }
1745
+ p = this.byId.get(p)?.parent ?? null;
1746
+ }
1747
+ if (anchorId !== null && t !== void 0) {
1748
+ const xf = this.animXform.get(anchorId);
1749
+ const pt = this.worldTransform.get(anchorId);
1750
+ if (xf !== void 0 && pt !== void 0) {
1751
+ const relx = t.ox - pt.ox;
1752
+ const rely = t.oy - pt.oy;
1753
+ const c = Math.cos(xf.rot);
1754
+ const sn = Math.sin(xf.rot);
1755
+ ax = pt.ox + c * relx - sn * rely - t.ox;
1756
+ ay = pt.oy + sn * relx + c * rely - t.oy;
1757
+ arot = xf.rot;
1758
+ }
1759
+ }
1760
+ }
1761
+ const px = ox + ((t !== void 0 ? t.ox : layer.origin[0]) + ax) * s;
1762
+ const py = oy + ((t !== void 0 ? t.oy : layer.origin[1]) + ay) * s;
1763
+ const rt = this.runtimes.get(layer.id);
1764
+ const ptex = this.particleTextures.get(layer.id);
1765
+ if (rt !== void 0 && ptex !== void 0) {
1766
+ const wt = t ?? {
1767
+ ox: layer.origin[0],
1768
+ oy: layer.origin[1],
1769
+ sx: layer.scale[0] ?? 1,
1770
+ sy: layer.scale[1] ?? 1
1771
+ };
1772
+ rt.draw(ctx, ox, oy, s, wt, ptex);
1773
+ continue;
1774
+ }
1775
+ ctx.save();
1776
+ ctx.translate(px, py);
1777
+ const animB0 = selfXf !== void 0 && layer.puppet !== null ? layer.puppet.bones[0]?.bind ?? null : null;
1778
+ const rotAngle = (layer.angles[2] ?? 0) * Math.PI / 180 + arot;
1779
+ if (animB0 !== null && animB0.length >= 15 && rotAngle !== 0) {
1780
+ const sxv = (t !== void 0 ? t.sx : layer.scale[0] ?? 1) * s;
1781
+ const syv = (t !== void 0 ? t.sy : layer.scale[1] ?? 1) * s;
1782
+ const bx = animB0[12] * sxv;
1783
+ const by = -animB0[13] * syv;
1784
+ ctx.translate(bx, by);
1785
+ ctx.rotate(rotAngle);
1786
+ ctx.translate(-bx, -by);
1787
+ } else ctx.rotate(rotAngle);
1788
+ ctx.scale((t !== void 0 ? t.sx : layer.scale[0] ?? 1) * s, (t !== void 0 ? t.sy : layer.scale[1] ?? 1) * s);
1789
+ if (layer.alpha < 1) ctx.globalAlpha = Math.max(0, Math.min(1, layer.alpha));
1790
+ const bmp = this.layerTextures.get(layer.id) ?? null;
1791
+ if (model.puppetMeshRender && layer.puppet !== null && layer.puppet.mesh !== null && bmp !== null) {
1792
+ const selfXf2 = this.animXform.get(layer.id);
1793
+ const b0 = layer.puppet.bones[0]?.bind ?? null;
1794
+ const animSkin = selfXf2 !== void 0 && b0 !== null && b0.length >= 15 ? {
1795
+ rot: selfXf2.rot,
1796
+ bx: b0[12],
1797
+ by: b0[13]
1798
+ } : null;
1799
+ const key = layer.id + ":" + (animSkin !== null ? animSkin.rot.toFixed(4) : "static");
1800
+ let mc = this.meshCanvases.get(layer.id);
1801
+ if (mc === void 0 || mc.animKey !== key) {
1802
+ const built = buildMeshCanvas(layer.puppet.mesh, bmp, animSkin);
1803
+ mc = {
1804
+ canvas: built.canvas,
1805
+ originX: built.originX,
1806
+ originY: built.originY,
1807
+ animKey: key
1808
+ };
1809
+ this.meshCanvases.set(layer.id, mc);
1810
+ }
1811
+ ctx.drawImage(mc.canvas, -mc.originX, -mc.originY);
1812
+ } else if (bmp !== null) {
1813
+ const ti = this.layerTexImage.get(layer.id);
1814
+ const sw = ti !== void 0 ? ti[0] : bmp.width;
1815
+ const sh = ti !== void 0 ? ti[1] : bmp.height;
1816
+ const dw = layer.size !== null ? layer.size[0] : sw;
1817
+ const dh = layer.size !== null ? layer.size[1] : sh;
1818
+ const effScale = model.effectStrengthScale ?? 1;
1819
+ const wws = layer.effects.filter((e) => e.type === "waterwaves").map((e) => ({
1820
+ ...e,
1821
+ strength: e.strength * effScale
1822
+ }));
1823
+ const shk = layer.effects.find((e) => e.type === "shake");
1824
+ if (wws.length > 0) {
1825
+ const maskInfo = this.effectMasks.get(layer.id);
1826
+ let eff = null;
1827
+ if (this.wwGL !== null || WaterwavesGL.available) {
1828
+ if (this.wwGL === null) this.wwGL = new WaterwavesGL();
1829
+ eff = this.wwGL.render(bmp, sw, sh, maskInfo !== void 0 ? maskInfo.bmp : null, maskInfo !== void 0 ? maskInfo.useA : false, wws, this.animTime, String(layer.id));
1830
+ }
1831
+ if (eff === null) eff = applyWaterwaves(bmp, sw, sh, wws, this.animTime, maskInfo !== void 0 ? maskInfo.bmp : null);
1832
+ ctx.drawImage(eff, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
1833
+ } else if (shk !== void 0 && shk.type === "shake") {
1834
+ const maskInfo2 = this.effectMasks.get(layer.id);
1835
+ const fd = maskInfo2 !== void 0 ? maskInfo2.flowDir : [0, -1];
1836
+ const offset = Math.sin(this.animTime * shk.speed);
1837
+ const amp = shk.strength * shk.strength * effScale;
1838
+ const dx = offset * amp * fd[0] * dw;
1839
+ const dy = offset * amp * fd[1] * dh;
1840
+ ctx.drawImage(bmp, 0, 0, sw, sh, -dw / 2 + dx, -dh / 2 + dy, dw, dh);
1841
+ } else ctx.drawImage(bmp, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
1842
+ } else {
1843
+ ctx.fillStyle = "rgba(120, 170, 255, 0.5)";
1844
+ ctx.beginPath();
1845
+ ctx.arc(0, 0, 3, 0, Math.PI * 2);
1846
+ ctx.fill();
1847
+ }
1848
+ ctx.restore();
1849
+ ctx.font = "10px system-ui, sans-serif";
1850
+ ctx.textBaseline = "top";
1851
+ ctx.fillStyle = "rgba(255,255,255,0.85)";
1852
+ ctx.strokeStyle = "rgba(0,0,0,0.55)";
1853
+ const label = "#" + layer.id + " " + layer.name + " [" + layer.kind + (this.layerTextures.has(layer.id) ? " tex" : "") + "]";
1854
+ ctx.lineWidth = 3;
1855
+ ctx.strokeText(label, px + 6, py + 6);
1856
+ ctx.fillText(label, px + 6, py + 6);
1857
+ }
1858
+ ctx.strokeStyle = "rgba(255,255,255,0.28)";
1859
+ ctx.lineWidth = 1;
1860
+ ctx.strokeRect(ox, oy, model.width * s, model.height * s);
1861
+ }
1862
+ drawCoverBase(ctx, img, cw, ch) {
1863
+ const iw = img.naturalWidth;
1864
+ const ih = img.naturalHeight;
1865
+ if (iw === 0 || ih === 0) return;
1866
+ const s = Math.max(cw / iw, ch / ih);
1867
+ const sw = cw / s;
1868
+ const sh = ch / s;
1869
+ ctx.drawImage(img, (iw - sw) / 2, (ih - sh) / 2, sw, sh, 0, 0, cw, ch);
1870
+ }
1871
+ resize() {
1872
+ if (this.el === null) return;
1873
+ this.dpr = window.devicePixelRatio || 1;
1874
+ const w = Math.max(1, Math.round(this.el.clientWidth * this.dpr));
1875
+ const h = Math.max(1, Math.round(this.el.clientHeight * this.dpr));
1876
+ if (this.el.width !== w) this.el.width = w;
1877
+ if (this.el.height !== h) this.el.height = h;
1878
+ }
1879
+ onResize = () => {
1880
+ this.resize();
1881
+ };
1882
+ onVisibility = () => {
1883
+ if (document.hidden) {
1884
+ if (this.rafId !== 0) {
1885
+ cancelAnimationFrame(this.rafId);
1886
+ this.rafId = 0;
1887
+ }
1888
+ } else this.startAnimation();
1889
+ };
1890
+ setLive(live) {
1891
+ if (this.live === live) return;
1892
+ this.live = live;
1893
+ if (this.handlers.onLiveChange !== void 0) this.handlers.onLiveChange(live);
1894
+ }
1895
+ };
1896
+ //#endregion
1897
+ //#region src/client/index.ts
1898
+ /**
1899
+ * dsh-wallpaper_share · browser half(内部 id / 路由前缀仍为 we-sync)
1900
+ * 玻璃面板主题覆盖 + 壁纸背景层 + wallpaper_share 会话视图标签页。
1901
+ * 与 node half 通过同源 HTTP 路由(/we-sync/state、/we-sync/preview、
1902
+ * /we-sync/source、/we-sync/scene)通信,不依赖任何 RPC 基础设施。
1903
+ * 多显示器:?monitor= 锁定某台;不传则跟随"最近变化"的一台。
1904
+ */
1905
+ const inject = ["slots", "theme"];
1906
+ /** 专注模式:任务进行中 */
1907
+ const FOCUS_WORK = {
1908
+ panelAlpha: 30,
1909
+ blur: 15,
1910
+ shadow: 90
1911
+ };
1912
+ /** 专注模式:任务全部完成 */
1913
+ const FOCUS_IDLE = {
1914
+ panelAlpha: 9,
1915
+ blur: 6,
1916
+ shadow: 40
1917
+ };
1918
+ /** 当前生效的视觉参数(专注模式覆盖用户滑块值) */
1919
+ function effectiveVisuals() {
1920
+ if (store.settings.focus) return store.settings.taskActive ? FOCUS_WORK : FOCUS_IDLE;
1921
+ return {
1922
+ panelAlpha: store.settings.panelAlpha,
1923
+ blur: store.settings.blur,
1924
+ shadow: store.settings.shadow
1925
+ };
1926
+ }
1927
+ /** 包内单例 store:apply 循环更新,面板组件订阅渲染。 */
1928
+ const store = {
1929
+ info: null,
1930
+ settings: {
1931
+ enabled: true,
1932
+ panelAlpha: 72,
1933
+ blur: 6,
1934
+ shadow: 30,
1935
+ monitor: "",
1936
+ focus: false,
1937
+ taskActive: false,
1938
+ renderMode: "preview",
1939
+ immersive: false,
1940
+ approvalPending: false
1941
+ },
1942
+ listeners: /* @__PURE__ */ new Set(),
1943
+ actions: {
1944
+ applyTheme: () => {},
1945
+ applyBackground: () => {},
1946
+ applyImmersive: () => {},
1947
+ repoll: () => {}
1948
+ },
1949
+ subscribe(fn) {
1950
+ store.listeners.add(fn);
1951
+ return () => {
1952
+ store.listeners.delete(fn);
1953
+ };
1954
+ },
1955
+ notify() {
1956
+ for (const fn of store.listeners) fn();
1957
+ }
1958
+ };
1959
+ function apply(ctx) {
1960
+ const theme = ctx.get("theme");
1961
+ const slots = ctx.get("slots");
1962
+ if (theme === void 0 || slots === void 0) return;
1963
+ const sessions = ctx.get("sessions");
1964
+ const workspaces = ctx.get("workspaces");
1965
+ const themeService = theme;
1966
+ const slotsService = slots;
1967
+ let themeDisposer = null;
1968
+ function applyTheme() {
1969
+ if (themeDisposer !== null) {
1970
+ themeDisposer();
1971
+ themeDisposer = null;
1972
+ }
1973
+ const a = .3 + effectiveVisuals().panelAlpha / 100 * .6;
1974
+ const dark = {
1975
+ "--dsw-alias-bg-base": "rgba(15,16,20," + a.toFixed(3) + ")",
1976
+ "--dsw-alias-bg-layer-1": "rgba(24,26,32," + (a * .95).toFixed(3) + ")",
1977
+ "--dsw-alias-bg-layer-2": "rgba(31,33,40," + (a * .9).toFixed(3) + ")",
1978
+ "--dsw-alias-bg-overlay": "rgba(22,24,29," + Math.min(a + .12, .96).toFixed(3) + ")",
1979
+ "--dsw-specific-sidebar-fill": "rgba(13,14,17," + (a * .92).toFixed(3) + ")"
1980
+ };
1981
+ const light = {
1982
+ "--dsw-alias-bg-base": "rgba(246,247,250," + Math.min(a + .1, .95).toFixed(3) + ")",
1983
+ "--dsw-alias-bg-layer-1": "rgba(255,255,255," + (a * .95).toFixed(3) + ")",
1984
+ "--dsw-alias-bg-layer-2": "rgba(251,252,253," + (a * .9).toFixed(3) + ")",
1985
+ "--dsw-alias-bg-overlay": "rgba(255,255,255," + Math.min(a + .14, .97).toFixed(3) + ")",
1986
+ "--dsw-specific-sidebar-fill": "rgba(238,240,244," + (a * .92).toFixed(3) + ")"
1987
+ };
1988
+ const tokens = {};
1989
+ for (const key of Object.keys(dark)) tokens[key] = {
1990
+ light: light[key] ?? "",
1991
+ dark: dark[key] ?? ""
1992
+ };
1993
+ themeDisposer = themeService.overrideTokens("we-sync", tokens);
1994
+ }
1995
+ const styleTag = document.createElement("style");
1996
+ styleTag.dataset.plugin = "dsh-wallpaper_share";
1997
+ document.head.appendChild(styleTag);
1998
+ const panelStyleTag = document.createElement("style");
1999
+ panelStyleTag.dataset.plugin = "dsh-wallpaper_share";
2000
+ panelStyleTag.textContent = PANEL_CSS;
2001
+ document.head.appendChild(panelStyleTag);
2002
+ let mediaEl = null;
2003
+ let sceneCanvas = null;
2004
+ function stopSceneCanvas() {
2005
+ if (sceneCanvas !== null) {
2006
+ sceneCanvas.stop();
2007
+ sceneCanvas = null;
2008
+ }
2009
+ }
2010
+ let sceneModelRenderer = null;
2011
+ function stopSceneModelRenderer() {
2012
+ if (sceneModelRenderer !== null) {
2013
+ sceneModelRenderer.stop();
2014
+ sceneModelRenderer = null;
2015
+ }
2016
+ }
2017
+ function stopSceneLayers() {
2018
+ stopSceneCanvas();
2019
+ stopSceneModelRenderer();
2020
+ }
2021
+ function setMedia(el) {
2022
+ if (mediaEl !== null && mediaEl !== el) {
2023
+ if (mediaEl instanceof HTMLVideoElement) mediaEl.pause();
2024
+ mediaEl.remove();
2025
+ }
2026
+ mediaEl = el;
2027
+ if (el !== null) {
2028
+ el.style.position = "fixed";
2029
+ el.style.top = "0";
2030
+ el.style.left = "0";
2031
+ el.style.width = "100%";
2032
+ el.style.height = "100%";
2033
+ el.style.zIndex = "-2";
2034
+ el.style.pointerEvents = "none";
2035
+ el.style.border = "0";
2036
+ document.body.appendChild(el);
2037
+ }
2038
+ }
2039
+ const immersiveStyleTag = document.createElement("style");
2040
+ immersiveStyleTag.dataset.plugin = "dsh-wallpaper_share";
2041
+ document.head.appendChild(immersiveStyleTag);
2042
+ const orbBtn = document.createElement("button");
2043
+ orbBtn.type = "button";
2044
+ orbBtn.title = "";
2045
+ orbBtn.style.cssText = "position:fixed;left:11px;top:232px;width:34px;height:34px;border-radius:50%;border:3px solid rgba(255,255,255,0.4);cursor:pointer;z-index:2147483001;opacity:0;visibility:hidden;background:rgba(15,16,20,0.4);box-shadow:0 2px 8px rgba(0,0,0,0.45);outline:none;transition:opacity 0.25s ease, visibility 0.25s ease, border-color 0.25s ease;";
2046
+ document.body.appendChild(orbBtn);
2047
+ const STATUS_COLORS = {
2048
+ approval: "#eab308",
2049
+ running: "#3b82f6",
2050
+ idle: "#22c55e"
2051
+ };
2052
+ function syncStatus() {
2053
+ const approval = document.querySelector("[data-approval-key]") !== null;
2054
+ if (approval !== store.settings.approvalPending) {
2055
+ store.settings.approvalPending = approval;
2056
+ store.notify();
2057
+ }
2058
+ const color = approval ? STATUS_COLORS.approval : store.settings.taskActive ? STATUS_COLORS.running : STATUS_COLORS.idle;
2059
+ orbBtn.style.borderColor = color;
2060
+ orbBtn.title = approval ? "等待授权" : store.settings.taskActive ? "任务进行中" : "空闲";
2061
+ const sidebarCollapsed = document.querySelector("[data-sidebar-collapsed]") !== null;
2062
+ orbBtn.style.opacity = sidebarCollapsed ? "1" : "0";
2063
+ orbBtn.style.visibility = sidebarCollapsed ? "visible" : "hidden";
2064
+ }
2065
+ function applyImmersive() {
2066
+ const on = store.settings.immersive;
2067
+ immersiveStyleTag.textContent = on ? "[data-phase] > header, [data-composer-seat] { opacity: 0 !important; pointer-events: none !important; transition: opacity 0.3s ease !important; }" : "";
2068
+ if (mediaEl instanceof HTMLIFrameElement) {
2069
+ mediaEl.style.zIndex = on ? "2147483000" : "-2";
2070
+ mediaEl.style.pointerEvents = on ? "auto" : "none";
2071
+ mediaEl.style.left = on ? "56px" : "0";
2072
+ mediaEl.style.width = on ? "calc(100% - 56px)" : "100%";
2073
+ }
2074
+ }
2075
+ orbBtn.addEventListener("click", () => {
2076
+ if (!store.settings.immersive) {
2077
+ const snap = sessions?.list.getSnapshot();
2078
+ const id = snap?.current;
2079
+ if (!(id === void 0 || snap != null && snap.byId[id]?.blank === true) && typeof workspaces?.startSession === "function") workspaces.startSession();
2080
+ }
2081
+ store.settings.immersive = !store.settings.immersive;
2082
+ applyImmersive();
2083
+ store.notify();
2084
+ });
2085
+ function onDocClick(ev) {
2086
+ if (!store.settings.immersive) return;
2087
+ const frame = document.querySelector("[data-sidebar-collapsed]");
2088
+ if (frame === null) return;
2089
+ const sidebarCol = frame.firstElementChild;
2090
+ if (sidebarCol === null) return;
2091
+ const target = ev.target;
2092
+ if (target instanceof Element) {
2093
+ const btn = target.closest("button");
2094
+ if (btn !== null && sidebarCol.contains(btn)) {
2095
+ store.settings.immersive = false;
2096
+ applyImmersive();
2097
+ store.notify();
2098
+ }
2099
+ }
2100
+ }
2101
+ document.addEventListener("click", onDocClick, true);
2102
+ function onImmersiveKey(ev) {
2103
+ if (ev.key === "Escape" && store.settings.immersive) {
2104
+ store.settings.immersive = false;
2105
+ applyImmersive();
2106
+ store.notify();
2107
+ }
2108
+ }
2109
+ document.addEventListener("keydown", onImmersiveKey);
2110
+ syncStatus();
2111
+ let statusRaf = null;
2112
+ const scheduleSync = () => {
2113
+ if (statusRaf !== null) return;
2114
+ statusRaf = requestAnimationFrame(() => {
2115
+ statusRaf = null;
2116
+ syncStatus();
2117
+ });
2118
+ };
2119
+ const statusObserver = new MutationObserver(() => {
2120
+ scheduleSync();
2121
+ });
2122
+ statusObserver.observe(document.body, {
2123
+ childList: true,
2124
+ subtree: true,
2125
+ attributes: true,
2126
+ attributeFilter: ["data-sidebar-collapsed"]
2127
+ });
2128
+ function applyBackground() {
2129
+ const info = store.info;
2130
+ const visuals = effectiveVisuals();
2131
+ const enabled = store.settings.enabled;
2132
+ const blurPx = Math.round(visuals.blur);
2133
+ const scale = 1 + blurPx / 400;
2134
+ const shadowAlpha = visuals.shadow / 100 * .6;
2135
+ const monitorKey = info !== null && info.monitor !== "" ? info.monitor : "";
2136
+ const monitorQuery = store.settings.monitor !== "" ? "&monitor=" + encodeURIComponent(store.settings.monitor) : "";
2137
+ const rawSourceKind = enabled && info !== null && store.settings.renderMode === "source" ? info.source.kind : "";
2138
+ const sceneEnhance = rawSourceKind === "scene" && info !== null && (info.scene?.available === true || info.source.scene === true);
2139
+ const sourceKind = rawSourceKind === "video" || rawSourceKind === "web" || rawSourceKind === "image" || sceneEnhance ? rawSourceKind : "";
2140
+ let imgUrl = "none";
2141
+ if (enabled && info !== null) {
2142
+ if (sourceKind === "image") imgUrl = "url(\"/we-sync/source?monitor=" + encodeURIComponent(monitorKey) + "&v=" + info.version + "\")";
2143
+ else if (sourceKind === "scene") imgUrl = info.source.scene ? "url(\"/we-sync/scene?monitor=" + encodeURIComponent(monitorKey) + "&v=" + info.version + "\")" : "url(\"/we-sync/preview?v=" + info.version + monitorQuery + "\")";
2144
+ else if (sourceKind === "" && info.kind === "image") imgUrl = "url(\"/we-sync/preview?v=" + info.version + monitorQuery + "\")";
2145
+ }
2146
+ styleTag.textContent = "html { background-color: #0d0e12; }" + (imgUrl !== "none" ? "body::before { content: \"\"; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -2; background-image: " + imgUrl + "; background-size: cover; background-position: center; background-repeat: no-repeat; filter: blur(" + blurPx + "px); transform: scale(" + scale.toFixed(3) + "); transition: filter 0.12s linear; }" : "") + "body::after { content: \"\"; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; background: linear-gradient(rgba(6,8,12," + shadowAlpha.toFixed(3) + "), rgba(6,8,12," + (shadowAlpha * .85).toFixed(3) + ")); }";
2147
+ if (sourceKind === "scene" && info !== null) {
2148
+ const sceneMode = info.scene?.mode === "external" ? "external" : "browser";
2149
+ if (sceneMode === "external" && info.scene?.available === true) {
2150
+ if (sceneCanvas === null) sceneCanvas = new SceneCanvas();
2151
+ sceneCanvas.applyVisuals(blurPx, scale);
2152
+ sceneCanvas.start(monitorKey, info.version);
2153
+ stopSceneModelRenderer();
2154
+ } else if (sceneMode === "browser") {
2155
+ if (sceneModelRenderer === null) sceneModelRenderer = new SceneModelRenderer();
2156
+ sceneModelRenderer.applyVisuals(blurPx, scale);
2157
+ sceneModelRenderer.start(monitorKey, info.version);
2158
+ stopSceneCanvas();
2159
+ } else stopSceneLayers();
2160
+ setMedia(null);
2161
+ } else if (sourceKind === "video" && info !== null) {
2162
+ let video = mediaEl instanceof HTMLVideoElement ? mediaEl : null;
2163
+ if (video === null) {
2164
+ video = document.createElement("video");
2165
+ video.muted = true;
2166
+ video.loop = true;
2167
+ video.playsInline = true;
2168
+ video.autoplay = true;
2169
+ setMedia(video);
2170
+ }
2171
+ const src = "/we-sync/source?monitor=" + encodeURIComponent(monitorKey) + "&v=" + info.version;
2172
+ if (video.src !== location.origin + src) video.src = src;
2173
+ video.style.filter = "blur(" + blurPx + "px)";
2174
+ video.style.transform = "scale(" + scale.toFixed(3) + ")";
2175
+ video.style.objectFit = "cover";
2176
+ const p = video.play();
2177
+ if (p !== void 0 && p !== null) p.catch(() => {});
2178
+ } else if (sourceKind === "web" && info !== null) {
2179
+ let frame = mediaEl instanceof HTMLIFrameElement ? mediaEl : null;
2180
+ if (frame === null) {
2181
+ frame = document.createElement("iframe");
2182
+ frame.setAttribute("sandbox", "allow-scripts allow-same-origin");
2183
+ setMedia(frame);
2184
+ }
2185
+ const src = typeof info.webPort === "number" && info.webPort > 0 ? "http://127.0.0.1:" + info.webPort + "/index.html?monitor=" + encodeURIComponent(monitorKey) + "&v=" + info.version : location.origin + "/we-sync/wallpaper/index.html?monitor=" + encodeURIComponent(monitorKey) + "&v=" + info.version;
2186
+ if (frame.src !== src) frame.src = src;
2187
+ frame.style.filter = "blur(" + blurPx + "px)";
2188
+ } else {
2189
+ stopSceneLayers();
2190
+ setMedia(null);
2191
+ }
2192
+ applyImmersive();
2193
+ }
2194
+ let polling = false;
2195
+ let lastHash = "";
2196
+ let lastWebPort = -1;
2197
+ async function poll() {
2198
+ if (polling) return;
2199
+ polling = true;
2200
+ try {
2201
+ const monitorQuery = store.settings.monitor !== "" ? "?monitor=" + encodeURIComponent(store.settings.monitor) : "";
2202
+ const res = await fetch("/we-sync/state" + monitorQuery, { cache: "no-store" });
2203
+ if (!res.ok) return;
2204
+ const info = await res.json();
2205
+ const changed = typeof info.hash === "string" && info.hash !== lastHash;
2206
+ const portChanged = typeof info.webPort === "number" && info.webPort !== lastWebPort;
2207
+ store.info = info;
2208
+ store.notify();
2209
+ if (changed || portChanged) {
2210
+ lastHash = typeof info.hash === "string" ? info.hash : lastHash;
2211
+ lastWebPort = typeof info.webPort === "number" ? info.webPort : lastWebPort;
2212
+ applyBackground();
2213
+ }
2214
+ } catch {}
2215
+ polling = false;
2216
+ }
2217
+ store.actions.applyTheme = applyTheme;
2218
+ store.actions.applyBackground = applyBackground;
2219
+ store.actions.applyImmersive = applyImmersive;
2220
+ store.actions.repoll = () => {
2221
+ lastHash = "";
2222
+ poll();
2223
+ };
2224
+ ctx.effect(() => () => {
2225
+ styleTag.remove();
2226
+ panelStyleTag.remove();
2227
+ immersiveStyleTag.remove();
2228
+ orbBtn.remove();
2229
+ statusObserver.disconnect();
2230
+ document.removeEventListener("keydown", onImmersiveKey);
2231
+ document.removeEventListener("click", onDocClick, true);
2232
+ stopSceneLayers();
2233
+ setMedia(null);
2234
+ if (themeDisposer !== null) {
2235
+ themeDisposer();
2236
+ themeDisposer = null;
2237
+ }
2238
+ });
2239
+ ctx.effect(() => {
2240
+ const timer = setInterval(() => {
2241
+ poll();
2242
+ }, 2500);
2243
+ poll();
2244
+ return () => clearInterval(timer);
2245
+ });
2246
+ if (sessions !== void 0) {
2247
+ const updateTaskState = () => {
2248
+ const snapshot = sessions.list.getSnapshot();
2249
+ const active = snapshot != null && Object.values(snapshot.byId).some((s) => s.running === true);
2250
+ if (active !== store.settings.taskActive) {
2251
+ store.settings.taskActive = active;
2252
+ if (store.settings.focus) {
2253
+ applyTheme();
2254
+ applyBackground();
2255
+ }
2256
+ syncStatus();
2257
+ store.notify();
2258
+ }
2259
+ };
2260
+ ctx.effect(() => sessions.list.subscribe(updateTaskState));
2261
+ updateTaskState();
2262
+ }
2263
+ applyTheme();
2264
+ applyBackground();
2265
+ slotsService.inject("conversation.view", () => slotsService.register({
2266
+ name: "conversation.view",
2267
+ id: "wallpaper_share",
2268
+ order: 20,
2269
+ label: "wallpaper_share"
2270
+ }, WallpaperSharePanel));
2271
+ }
2272
+ //#endregion
2273
+ exports.FOCUS_IDLE = FOCUS_IDLE;
2274
+ exports.FOCUS_WORK = FOCUS_WORK;
2275
+ exports.apply = apply;
2276
+ exports.effectiveVisuals = effectiveVisuals;
2277
+ exports.inject = inject;
2278
+ exports.store = store;
2279
+ return module.exports;
2280
+ }
2281
+ });
2282
+
2283
+ //# sourceMappingURL=client.js.map