webwallgl 1.1.0 → 1.3.5
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/README.en.md +164 -4
- package/README.md +164 -4
- package/package.json +1 -1
- package/types.d.ts +166 -17
- package/webwallgl.d.ts +35 -15
- package/webwallgl.global.js +706 -88
- package/webwallgl.global.js.map +1 -1
- package/webwallgl.global.min.js +37 -37
- package/webwallgl.global.min.js.map +1 -1
- package/webwallgl.min.mjs +37 -37
- package/webwallgl.min.mjs.map +1 -1
- package/webwallgl.mjs +707 -89
- package/webwallgl.mjs.map +1 -1
package/webwallgl.mjs
CHANGED
|
@@ -47,10 +47,24 @@ function clear(rt) {
|
|
|
47
47
|
rt.raf = void 0;
|
|
48
48
|
for (const p of rt.videoPairs ?? []) p.destroy();
|
|
49
49
|
rt.videoPairs = void 0;
|
|
50
|
-
if (rt.sceneCleanup)
|
|
50
|
+
if (rt.sceneCleanup) {
|
|
51
|
+
try {
|
|
52
|
+
rt.sceneCleanup();
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
51
56
|
rt.sceneCleanup = void 0;
|
|
52
57
|
rt.sceneCtl = void 0;
|
|
53
58
|
rt.pointerCtl = void 0;
|
|
59
|
+
rt.mediaCtl = void 0;
|
|
60
|
+
const ds = rt.wallpaperDisposers ?? [];
|
|
61
|
+
rt.wallpaperDisposers = [];
|
|
62
|
+
for (const d of ds) {
|
|
63
|
+
try {
|
|
64
|
+
d();
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
54
68
|
if (rt.renderer) {
|
|
55
69
|
rt.renderer.dispose?.();
|
|
56
70
|
rt.renderer = void 0;
|
|
@@ -92,10 +106,18 @@ function clear(rt) {
|
|
|
92
106
|
rt.canvas = void 0;
|
|
93
107
|
rt.ctx = void 0;
|
|
94
108
|
rt.info = void 0;
|
|
109
|
+
clearSceneDebugGlobals();
|
|
95
110
|
resetFrameMeter(rt);
|
|
96
111
|
}
|
|
97
112
|
function destroyRuntime(rt) {
|
|
98
|
-
|
|
113
|
+
try {
|
|
114
|
+
clear(rt);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
if (rt.peekRaf !== void 0) {
|
|
118
|
+
cancelAnimationFrame(rt.peekRaf);
|
|
119
|
+
rt.peekRaf = void 0;
|
|
120
|
+
}
|
|
99
121
|
for (const off of rt.disposers.splice(0)) {
|
|
100
122
|
try {
|
|
101
123
|
off();
|
|
@@ -151,6 +173,32 @@ function applyCoverAlignToDom(rt) {
|
|
|
151
173
|
const obj = rt.video ?? rt.img;
|
|
152
174
|
if (obj && obj.isConnected) obj.style.objectPosition = pos;
|
|
153
175
|
}
|
|
176
|
+
const SCENE_DEBUG_GLOBALS = [
|
|
177
|
+
"__scene",
|
|
178
|
+
"__sceneLayers",
|
|
179
|
+
"__textures",
|
|
180
|
+
"__objScripts",
|
|
181
|
+
"__textWidgets",
|
|
182
|
+
"__mediaHooks",
|
|
183
|
+
"__mediaControl",
|
|
184
|
+
"__mediaStats",
|
|
185
|
+
"__mediaSet",
|
|
186
|
+
"__system",
|
|
187
|
+
"__liveSystem",
|
|
188
|
+
"__audioStats",
|
|
189
|
+
"__audioMute",
|
|
190
|
+
"__particleStats",
|
|
191
|
+
"__particleToggle",
|
|
192
|
+
"__pointerStats",
|
|
193
|
+
"__compositeStats",
|
|
194
|
+
"__compositeEnable"
|
|
195
|
+
];
|
|
196
|
+
function clearSceneDebugGlobals() {
|
|
197
|
+
const w = window;
|
|
198
|
+
for (const k of SCENE_DEBUG_GLOBALS) {
|
|
199
|
+
if (k in w) delete w[k];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
154
202
|
function syncCanvasSize(rt, canvas, cfg) {
|
|
155
203
|
const dpr = effectiveDpr(rt, cfg);
|
|
156
204
|
const w = Math.max(1, Math.round((canvas.clientWidth || window.innerWidth || 1) * dpr));
|
|
@@ -3539,6 +3587,11 @@ function rewriteXrayFragScale(fragGlsl) {
|
|
|
3539
3587
|
const from = usesVarying ? /unprojectedUVs\s*\*=\s*v_PointerScale\s*\*/ : /unprojectedUVs\s*\*=\s*g_PointerScale\s*\*/;
|
|
3540
3588
|
return out.replace(from, "unprojectedUVs *= " + XRAY_FRAG_UV_SCALE + " *");
|
|
3541
3589
|
}
|
|
3590
|
+
const XRAY_SIZE_FALLBACK = 1;
|
|
3591
|
+
function constantFallback(uniformName, declaredDefault) {
|
|
3592
|
+
if (uniformName === "g_PointerScale") return XRAY_SIZE_FALLBACK;
|
|
3593
|
+
return declaredDefault;
|
|
3594
|
+
}
|
|
3542
3595
|
function createRenderer(canvas, opts = {}) {
|
|
3543
3596
|
const gl = canvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false, alpha: false, preserveDrawingBuffer: true });
|
|
3544
3597
|
if (!gl) throw new Error("当前浏览器不支持 WebGL2");
|
|
@@ -3895,7 +3948,8 @@ function createRenderer(canvas, opts = {}) {
|
|
|
3895
3948
|
}
|
|
3896
3949
|
for (const [matKey, entry] of Object.entries(matMeta || {})) {
|
|
3897
3950
|
if (!constants || !(matKey in constants)) {
|
|
3898
|
-
|
|
3951
|
+
const dflt = constantFallback(entry.uniform, entry.default);
|
|
3952
|
+
if (dflt !== void 0) setConstant(uni, entry.uniform, dflt);
|
|
3899
3953
|
}
|
|
3900
3954
|
}
|
|
3901
3955
|
}
|
|
@@ -5114,9 +5168,11 @@ const rndMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
|
|
|
5114
5168
|
__proto__: null,
|
|
5115
5169
|
ALIGN,
|
|
5116
5170
|
XRAY_IDLE_SCREEN_UV,
|
|
5171
|
+
XRAY_SIZE_FALLBACK,
|
|
5117
5172
|
applyColorBlendCPU,
|
|
5118
5173
|
collectGroupDescendantIds,
|
|
5119
5174
|
colorBlendPlan,
|
|
5175
|
+
constantFallback,
|
|
5120
5176
|
createRenderer,
|
|
5121
5177
|
effectFboSize,
|
|
5122
5178
|
layerWantsPreserveBackdrop,
|
|
@@ -11340,19 +11396,84 @@ async function decodeGifFrames(src) {
|
|
|
11340
11396
|
dec.close?.();
|
|
11341
11397
|
return { width, height, frames };
|
|
11342
11398
|
}
|
|
11399
|
+
function attachVideoSpectrum(rt, cfg, v) {
|
|
11400
|
+
if (rt.audioBridge) return;
|
|
11401
|
+
const AC = window.AudioContext || window.webkitAudioContext;
|
|
11402
|
+
if (!AC) return;
|
|
11403
|
+
let ctx;
|
|
11404
|
+
let analyser;
|
|
11405
|
+
try {
|
|
11406
|
+
ctx = new AC();
|
|
11407
|
+
const srcNode = ctx.createMediaElementSource(v);
|
|
11408
|
+
analyser = ctx.createAnalyser();
|
|
11409
|
+
analyser.fftSize = 256;
|
|
11410
|
+
analyser.smoothingTimeConstant = 0.75;
|
|
11411
|
+
srcNode.connect(analyser);
|
|
11412
|
+
analyser.connect(ctx.destination);
|
|
11413
|
+
} catch (e) {
|
|
11414
|
+
reportDiag(rt, cfg, `media 频谱接管跳过: ${e?.message ?? e}`);
|
|
11415
|
+
return;
|
|
11416
|
+
}
|
|
11417
|
+
const bins = new Uint8Array(analyser.frequencyBinCount);
|
|
11418
|
+
const left = new Float32Array(64);
|
|
11419
|
+
const right = new Float32Array(64);
|
|
11420
|
+
const bridge = () => {
|
|
11421
|
+
if (ctx.state !== "running") return null;
|
|
11422
|
+
analyser.getByteFrequencyData(bins);
|
|
11423
|
+
const n = Math.min(64, bins.length);
|
|
11424
|
+
for (let i = 0; i < n; i++) {
|
|
11425
|
+
const x = bins[i] / 255;
|
|
11426
|
+
left[i] = x;
|
|
11427
|
+
right[i] = x;
|
|
11428
|
+
}
|
|
11429
|
+
for (let i = n; i < 64; i++) {
|
|
11430
|
+
left[i] = 0;
|
|
11431
|
+
right[i] = 0;
|
|
11432
|
+
}
|
|
11433
|
+
return { left, right };
|
|
11434
|
+
};
|
|
11435
|
+
rt.audioBridge = bridge;
|
|
11436
|
+
if (ctx.state === "suspended") {
|
|
11437
|
+
const kick = () => {
|
|
11438
|
+
void ctx.resume().catch(() => {
|
|
11439
|
+
});
|
|
11440
|
+
window.removeEventListener("pointerdown", kick);
|
|
11441
|
+
window.removeEventListener("keydown", kick);
|
|
11442
|
+
};
|
|
11443
|
+
window.addEventListener("pointerdown", kick, { once: true });
|
|
11444
|
+
window.addEventListener("keydown", kick, { once: true });
|
|
11445
|
+
(rt.wallpaperDisposers ??= []).push(() => {
|
|
11446
|
+
window.removeEventListener("pointerdown", kick);
|
|
11447
|
+
window.removeEventListener("keydown", kick);
|
|
11448
|
+
});
|
|
11449
|
+
}
|
|
11450
|
+
(rt.wallpaperDisposers ??= []).push(() => {
|
|
11451
|
+
if (rt.audioBridge === bridge) rt.audioBridge = null;
|
|
11452
|
+
void ctx.close().catch(() => {
|
|
11453
|
+
});
|
|
11454
|
+
});
|
|
11455
|
+
}
|
|
11343
11456
|
function mountMedia(rt, cfg) {
|
|
11344
11457
|
clear(rt);
|
|
11345
|
-
|
|
11458
|
+
const failHard = (why) => {
|
|
11459
|
+
reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
|
|
11460
|
+
rt.onError?.(new Error(`媒体壁纸(${cfg.type})${why}`));
|
|
11346
11461
|
rt.fallbackPage?.();
|
|
11462
|
+
};
|
|
11463
|
+
if (!cfg.src) {
|
|
11464
|
+
failHard("缺少资源 URL(cfg.src 为空)");
|
|
11347
11465
|
return;
|
|
11348
11466
|
}
|
|
11349
11467
|
const isVideo = cfg.type === "video";
|
|
11350
11468
|
const isGif = cfg.type === "gif";
|
|
11351
|
-
const
|
|
11469
|
+
const embedded = cfg.canvas instanceof HTMLCanvasElement;
|
|
11470
|
+
const c = embedded ? cfg.canvas : document.createElement("canvas");
|
|
11352
11471
|
const dpr = effectiveDpr(rt, cfg);
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
c.
|
|
11472
|
+
const vw = c.clientWidth || window.innerWidth || 1;
|
|
11473
|
+
const vh = c.clientHeight || window.innerHeight || 1;
|
|
11474
|
+
c.width = Math.max(1, Math.round(vw * dpr));
|
|
11475
|
+
c.height = Math.max(1, Math.round(vh * dpr));
|
|
11476
|
+
if (!embedded) c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
|
|
11356
11477
|
const gl2 = c.getContext("webgl2", {
|
|
11357
11478
|
premultipliedAlpha: false,
|
|
11358
11479
|
antialias: false,
|
|
@@ -11360,13 +11481,32 @@ function mountMedia(rt, cfg) {
|
|
|
11360
11481
|
preserveDrawingBuffer: true
|
|
11361
11482
|
});
|
|
11362
11483
|
if (!gl2) {
|
|
11363
|
-
reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE
|
|
11484
|
+
reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE`);
|
|
11485
|
+
if (embedded) {
|
|
11486
|
+
rt.onError?.(new Error("WEBGL2_UNAVAILABLE"));
|
|
11487
|
+
return;
|
|
11488
|
+
}
|
|
11364
11489
|
if (isVideo) mountVideoDom(rt, cfg);
|
|
11365
11490
|
else mountGifDom(rt, cfg);
|
|
11366
11491
|
return;
|
|
11367
11492
|
}
|
|
11368
|
-
rt.wrap?.appendChild(c);
|
|
11493
|
+
if (!embedded) rt.wrap?.appendChild(c);
|
|
11369
11494
|
rt.canvas = c;
|
|
11495
|
+
if (rt.onSceneInfo) {
|
|
11496
|
+
const hook = rt.onSceneInfo;
|
|
11497
|
+
rt.onSceneInfo = void 0;
|
|
11498
|
+
try {
|
|
11499
|
+
hook({
|
|
11500
|
+
width: vw,
|
|
11501
|
+
height: vh,
|
|
11502
|
+
layerCount: 0,
|
|
11503
|
+
hasModels: false,
|
|
11504
|
+
hasParticles: false,
|
|
11505
|
+
hasText: false
|
|
11506
|
+
});
|
|
11507
|
+
} catch {
|
|
11508
|
+
}
|
|
11509
|
+
}
|
|
11370
11510
|
let disposed = false;
|
|
11371
11511
|
rt.sceneCleanup = () => {
|
|
11372
11512
|
disposed = true;
|
|
@@ -11378,6 +11518,21 @@ function mountMedia(rt, cfg) {
|
|
|
11378
11518
|
let pauseImpl;
|
|
11379
11519
|
let resumeImpl;
|
|
11380
11520
|
let videoWasPlaying = false;
|
|
11521
|
+
rt.sceneAudio = {
|
|
11522
|
+
setVolume(v) {
|
|
11523
|
+
const vid = rt.video;
|
|
11524
|
+
if (!vid) return;
|
|
11525
|
+
const vol = Math.max(0, Math.min(1, Number(v) || 0));
|
|
11526
|
+
vid.volume = vol;
|
|
11527
|
+
vid.muted = vol <= 0;
|
|
11528
|
+
if (vol > 0 && vid.paused && !rt.paused) {
|
|
11529
|
+
void vid.play().catch((e) => {
|
|
11530
|
+
reportDiag(rt, cfg, `media 取消静音后自动播放被拒绝: ${e?.message ?? e}`);
|
|
11531
|
+
});
|
|
11532
|
+
}
|
|
11533
|
+
},
|
|
11534
|
+
audios: []
|
|
11535
|
+
};
|
|
11381
11536
|
rt.sceneCtl = {
|
|
11382
11537
|
pause() {
|
|
11383
11538
|
pauseImpl?.();
|
|
@@ -11391,8 +11546,7 @@ function mountMedia(rt, cfg) {
|
|
|
11391
11546
|
const fail = (why) => {
|
|
11392
11547
|
if (disposed) return;
|
|
11393
11548
|
disposed = true;
|
|
11394
|
-
|
|
11395
|
-
rt.fallbackPage?.();
|
|
11549
|
+
failHard(why);
|
|
11396
11550
|
};
|
|
11397
11551
|
void (async () => {
|
|
11398
11552
|
try {
|
|
@@ -11440,6 +11594,7 @@ function mountMedia(rt, cfg) {
|
|
|
11440
11594
|
});
|
|
11441
11595
|
if (!rt.paused) void v.play().catch(() => {
|
|
11442
11596
|
});
|
|
11597
|
+
attachVideoSpectrum(rt, cfg, v);
|
|
11443
11598
|
reportDiag(rt, cfg, `media video ${mediaW}x${mediaH} → scene 渲染`);
|
|
11444
11599
|
} else {
|
|
11445
11600
|
let decoded = false;
|
|
@@ -11539,6 +11694,14 @@ function mountMedia(rt, cfg) {
|
|
|
11539
11694
|
peek.x,
|
|
11540
11695
|
peek.y
|
|
11541
11696
|
).then(() => {
|
|
11697
|
+
if (rt.onFirstFrame) {
|
|
11698
|
+
const first = rt.onFirstFrame;
|
|
11699
|
+
rt.onFirstFrame = void 0;
|
|
11700
|
+
try {
|
|
11701
|
+
first();
|
|
11702
|
+
} catch {
|
|
11703
|
+
}
|
|
11704
|
+
}
|
|
11542
11705
|
if (disposed || rt.paused) return;
|
|
11543
11706
|
rt.raf = requestAnimationFrame(renderLoop);
|
|
11544
11707
|
}).catch((e) => fail(String(e.message || e).slice(0, 200)));
|
|
@@ -11621,6 +11784,48 @@ function mountGifDom(rt, cfg) {
|
|
|
11621
11784
|
rt.img = img;
|
|
11622
11785
|
}
|
|
11623
11786
|
const PKG_PATHS = ["scene.pkg", "scenes/scene.pkg", "gifscene.pkg"];
|
|
11787
|
+
const EXT_TYPES = {
|
|
11788
|
+
mp4: "video",
|
|
11789
|
+
webm: "video",
|
|
11790
|
+
mov: "video",
|
|
11791
|
+
m4v: "video",
|
|
11792
|
+
ogv: "video",
|
|
11793
|
+
gif: "gif",
|
|
11794
|
+
png: "image",
|
|
11795
|
+
jpg: "image",
|
|
11796
|
+
jpeg: "image",
|
|
11797
|
+
webp: "image",
|
|
11798
|
+
avif: "image",
|
|
11799
|
+
bmp: "image"
|
|
11800
|
+
};
|
|
11801
|
+
function typeFromMime(mime) {
|
|
11802
|
+
const m = mime.toLowerCase().split(";")[0].trim();
|
|
11803
|
+
if (m === "image/gif") return "gif";
|
|
11804
|
+
if (m.startsWith("video/")) return "video";
|
|
11805
|
+
if (m.startsWith("image/")) return "image";
|
|
11806
|
+
return null;
|
|
11807
|
+
}
|
|
11808
|
+
function sniffMediaType(url) {
|
|
11809
|
+
if (typeof url !== "string" || !url) return null;
|
|
11810
|
+
let path = url;
|
|
11811
|
+
const hash = path.indexOf("#");
|
|
11812
|
+
if (hash >= 0) path = path.slice(0, hash);
|
|
11813
|
+
const q = path.indexOf("?");
|
|
11814
|
+
if (q >= 0) path = path.slice(0, q);
|
|
11815
|
+
const seg = path.split("/").pop() || "";
|
|
11816
|
+
const dot = seg.lastIndexOf(".");
|
|
11817
|
+
if (dot < 0) return null;
|
|
11818
|
+
return EXT_TYPES[seg.slice(dot + 1).toLowerCase()] ?? null;
|
|
11819
|
+
}
|
|
11820
|
+
async function sniffMediaTypeByHead(url, init, signal) {
|
|
11821
|
+
try {
|
|
11822
|
+
const r = await fetch(url, { ...init, method: "HEAD", signal });
|
|
11823
|
+
if (!r.ok) return null;
|
|
11824
|
+
return typeFromMime(r.headers.get("content-type") || "");
|
|
11825
|
+
} catch {
|
|
11826
|
+
return null;
|
|
11827
|
+
}
|
|
11828
|
+
}
|
|
11624
11829
|
function httpSource(baseUrl, init) {
|
|
11625
11830
|
const base = baseUrl.replace(/\/+$/, "");
|
|
11626
11831
|
return {
|
|
@@ -11675,6 +11880,29 @@ function httpSource(baseUrl, init) {
|
|
|
11675
11880
|
if (signal?.aborted) throw new Error("aborted");
|
|
11676
11881
|
}
|
|
11677
11882
|
return { url: `${base}/${file}` };
|
|
11883
|
+
},
|
|
11884
|
+
/**
|
|
11885
|
+
* 媒体壁纸(video/gif/image)的资源地址:`{base}/{project.file}`。
|
|
11886
|
+
*
|
|
11887
|
+
* 与 webEntry 的区别是**没有默认文件名可兜底**:网页壁纸缺 file 时
|
|
11888
|
+
* index.html 是行业惯例,媒体壁纸的文件名(scene.mp4 / xxx.gif)完全由作者定,
|
|
11889
|
+
* 猜一个只会 404。拿不到 file 就返回 null,让 mount 报「无法解析媒体 URL」,
|
|
11890
|
+
* 而不是发一个必然失败的请求、再把那个 404 当成根因写进错误里。
|
|
11891
|
+
*/
|
|
11892
|
+
async mediaEntry(signal) {
|
|
11893
|
+
let file = "";
|
|
11894
|
+
try {
|
|
11895
|
+
const r = await fetch(`${base}/project.json`, { ...init, signal });
|
|
11896
|
+
if (r.ok) {
|
|
11897
|
+
const project = await r.json();
|
|
11898
|
+
if (project && typeof project.file === "string" && project.file.trim()) {
|
|
11899
|
+
file = project.file.trim().replace(/^\/+/, "");
|
|
11900
|
+
}
|
|
11901
|
+
}
|
|
11902
|
+
} catch {
|
|
11903
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
11904
|
+
}
|
|
11905
|
+
return file ? { url: `${base}/${file}` } : null;
|
|
11678
11906
|
}
|
|
11679
11907
|
};
|
|
11680
11908
|
}
|
|
@@ -11694,6 +11922,43 @@ function bytesSource(pkg2, project, key) {
|
|
|
11694
11922
|
project: async () => project ?? null
|
|
11695
11923
|
};
|
|
11696
11924
|
}
|
|
11925
|
+
function mediaSource(urlOrFile, options) {
|
|
11926
|
+
const isBlob = typeof urlOrFile !== "string";
|
|
11927
|
+
const named = urlOrFile;
|
|
11928
|
+
let type = options?.type ?? (isBlob ? typeFromMime(urlOrFile.type || "") : null) ?? sniffMediaType(isBlob ? String(named.name ?? "") : urlOrFile);
|
|
11929
|
+
let headTried = false;
|
|
11930
|
+
let objectUrl = null;
|
|
11931
|
+
const url = () => {
|
|
11932
|
+
if (!isBlob) return urlOrFile;
|
|
11933
|
+
if (!objectUrl) objectUrl = URL.createObjectURL(urlOrFile);
|
|
11934
|
+
return objectUrl;
|
|
11935
|
+
};
|
|
11936
|
+
const key = options?.key ?? (isBlob ? typeof named.name === "string" ? `media:${named.name}:${named.size}:${named.lastModified ?? 0}` : void 0 : `media:${urlOrFile}`);
|
|
11937
|
+
return {
|
|
11938
|
+
key,
|
|
11939
|
+
async scenePkg() {
|
|
11940
|
+
throw new Error("mediaSource 是纯媒体来源,没有 scene.pkg(请改用 httpSource/fileSource/bytesSource)");
|
|
11941
|
+
},
|
|
11942
|
+
// type 为 null 时也如实返回:resolveMountConfig 会再按 mediaEntry 的 URL
|
|
11943
|
+
// 嗅探一次(含 HEAD 兜底),仍认不出才落回 scene 并报错
|
|
11944
|
+
async project() {
|
|
11945
|
+
return type ? { type } : null;
|
|
11946
|
+
},
|
|
11947
|
+
async mediaEntry(signal) {
|
|
11948
|
+
if (!type && !isBlob && !headTried) {
|
|
11949
|
+
headTried = true;
|
|
11950
|
+
type = await sniffMediaTypeByHead(urlOrFile, void 0, signal);
|
|
11951
|
+
}
|
|
11952
|
+
return { url: url(), type: type ?? void 0 };
|
|
11953
|
+
},
|
|
11954
|
+
dispose() {
|
|
11955
|
+
if (objectUrl) {
|
|
11956
|
+
URL.revokeObjectURL(objectUrl);
|
|
11957
|
+
objectUrl = null;
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
};
|
|
11961
|
+
}
|
|
11697
11962
|
const LOOP_PREROLL_SEC = 0.5;
|
|
11698
11963
|
const LOOP_HOLD_SEC = 0.04;
|
|
11699
11964
|
const LOOP_SWAP_EPS = 0.08;
|
|
@@ -12974,9 +13239,10 @@ function mountScene(rt, cfg) {
|
|
|
12974
13239
|
const audioDriverRef = {
|
|
12975
13240
|
current: null
|
|
12976
13241
|
};
|
|
12977
|
-
let
|
|
13242
|
+
let liveMediaOverride = null;
|
|
13243
|
+
const currentMediaDriver = () => liveMediaOverride ?? rt.mediaSource ?? simMedia;
|
|
12978
13244
|
let windowDriver = simWindow;
|
|
12979
|
-
const audioSim = { enabled: supportsAudioProcessing };
|
|
13245
|
+
const audioSim = { enabled: supportsAudioProcessing && !rt.audioDisabled };
|
|
12980
13246
|
const zero = (n) => new Float32Array(n);
|
|
12981
13247
|
const SILENT_AUDIO = {
|
|
12982
13248
|
left16: zero(16),
|
|
@@ -13075,7 +13341,7 @@ function mountScene(rt, cfg) {
|
|
|
13075
13341
|
const shortcuts = system.createShortcutHandler((name) => {
|
|
13076
13342
|
reportDiag(rt, cfg, `openUserShortcut: ${name}`);
|
|
13077
13343
|
});
|
|
13078
|
-
const mediaSim = { enabled:
|
|
13344
|
+
const mediaSim = { enabled: !rt.mediaDisabled, override: null };
|
|
13079
13345
|
const mediaHooks = [];
|
|
13080
13346
|
let lastMediaSnap = null;
|
|
13081
13347
|
liveHold.lastSnap = {
|
|
@@ -13084,7 +13350,7 @@ function mountScene(rt, cfg) {
|
|
|
13084
13350
|
if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
|
|
13085
13351
|
}
|
|
13086
13352
|
};
|
|
13087
|
-
const mediaSnapshot = () =>
|
|
13353
|
+
const mediaSnapshot = () => currentMediaDriver().snapshot;
|
|
13088
13354
|
const registerMediaHook = (sb) => {
|
|
13089
13355
|
if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
|
|
13090
13356
|
mediaHooks.push(sb);
|
|
@@ -13129,36 +13395,30 @@ function mountScene(rt, cfg) {
|
|
|
13129
13395
|
}
|
|
13130
13396
|
lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
|
|
13131
13397
|
};
|
|
13398
|
+
const callDriver = (name) => {
|
|
13399
|
+
const drv = currentMediaDriver();
|
|
13400
|
+
const fn = drv?.[name];
|
|
13401
|
+
if (typeof fn === "function") {
|
|
13402
|
+
try {
|
|
13403
|
+
fn.call(drv);
|
|
13404
|
+
} catch (e) {
|
|
13405
|
+
reportDiag(rt, cfg, `media ${name} 失败: ${e?.message}`);
|
|
13406
|
+
}
|
|
13407
|
+
}
|
|
13408
|
+
dispatchMediaNow();
|
|
13409
|
+
return mediaSnapshot();
|
|
13410
|
+
};
|
|
13132
13411
|
const mediaControl = {
|
|
13133
13412
|
get snapshot() {
|
|
13134
13413
|
return mediaSnapshot();
|
|
13135
13414
|
},
|
|
13136
|
-
skipNext: () =>
|
|
13137
|
-
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
skipPrevious: () => {
|
|
13142
|
-
mediaDriver.skipPrevious();
|
|
13143
|
-
dispatchMediaNow();
|
|
13144
|
-
return mediaSnapshot();
|
|
13145
|
-
},
|
|
13146
|
-
play: () => {
|
|
13147
|
-
mediaDriver.play();
|
|
13148
|
-
dispatchMediaNow();
|
|
13149
|
-
return mediaSnapshot();
|
|
13150
|
-
},
|
|
13151
|
-
pause: () => {
|
|
13152
|
-
mediaDriver.pause();
|
|
13153
|
-
dispatchMediaNow();
|
|
13154
|
-
return mediaSnapshot();
|
|
13155
|
-
},
|
|
13156
|
-
playPause: () => {
|
|
13157
|
-
mediaDriver.playPause();
|
|
13158
|
-
dispatchMediaNow();
|
|
13159
|
-
return mediaSnapshot();
|
|
13160
|
-
}
|
|
13415
|
+
skipNext: () => callDriver("skipNext"),
|
|
13416
|
+
skipPrevious: () => callDriver("skipPrevious"),
|
|
13417
|
+
play: () => callDriver("play"),
|
|
13418
|
+
pause: () => callDriver("pause"),
|
|
13419
|
+
playPause: () => callDriver("playPause")
|
|
13161
13420
|
};
|
|
13421
|
+
rt.mediaCtl = mediaControl;
|
|
13162
13422
|
window.__mediaControl = mediaControl;
|
|
13163
13423
|
window.__system = {
|
|
13164
13424
|
media: mediaControl,
|
|
@@ -13279,6 +13539,18 @@ function mountScene(rt, cfg) {
|
|
|
13279
13539
|
}
|
|
13280
13540
|
}
|
|
13281
13541
|
if (cfg.liveSystem) {
|
|
13542
|
+
const liveSlot = {
|
|
13543
|
+
handle: null,
|
|
13544
|
+
dead: false
|
|
13545
|
+
};
|
|
13546
|
+
(rt.wallpaperDisposers ??= []).push(() => {
|
|
13547
|
+
liveSlot.dead = true;
|
|
13548
|
+
try {
|
|
13549
|
+
liveSlot.handle?.dispose();
|
|
13550
|
+
} catch {
|
|
13551
|
+
}
|
|
13552
|
+
liveSlot.handle = null;
|
|
13553
|
+
});
|
|
13282
13554
|
try {
|
|
13283
13555
|
const uploadLiveArtwork = async (info) => {
|
|
13284
13556
|
try {
|
|
@@ -13319,7 +13591,7 @@ function mountScene(rt, cfg) {
|
|
|
13319
13591
|
generated: true
|
|
13320
13592
|
});
|
|
13321
13593
|
}
|
|
13322
|
-
const snap =
|
|
13594
|
+
const snap = currentMediaDriver().snapshot;
|
|
13323
13595
|
if (palette) {
|
|
13324
13596
|
snap.primaryColor = media.mediaVec3(...palette.primary);
|
|
13325
13597
|
snap.secondaryColor = media.mediaVec3(...palette.secondary);
|
|
@@ -13344,39 +13616,48 @@ function mountScene(rt, cfg) {
|
|
|
13344
13616
|
void uploadLiveArtwork(info);
|
|
13345
13617
|
}
|
|
13346
13618
|
});
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13619
|
+
if (liveSlot.dead) {
|
|
13620
|
+
try {
|
|
13621
|
+
live.dispose();
|
|
13622
|
+
} catch {
|
|
13623
|
+
}
|
|
13624
|
+
live = null;
|
|
13625
|
+
} else {
|
|
13626
|
+
liveSlot.handle = live;
|
|
13627
|
+
liveMediaOverride = live.media;
|
|
13628
|
+
windowDriver = live.windowTitle;
|
|
13629
|
+
liveHold.mediaDriver = live.media;
|
|
13630
|
+
if (live.status().audio === "mic") audioDriverRef.current = live.audio;
|
|
13631
|
+
if (currentMediaDriver().snapshot.hasMedia) {
|
|
13632
|
+
for (const { name, event } of media.diffMediaEvents(null, currentMediaDriver().snapshot)) {
|
|
13633
|
+
for (const sb of mediaHooks) {
|
|
13634
|
+
try {
|
|
13635
|
+
sb.callMedia(name, event);
|
|
13636
|
+
} catch {
|
|
13637
|
+
}
|
|
13357
13638
|
}
|
|
13358
13639
|
}
|
|
13640
|
+
lastMediaSnap = media.cloneMediaSnapshot(currentMediaDriver().snapshot);
|
|
13359
13641
|
}
|
|
13360
|
-
|
|
13642
|
+
const st = live.status();
|
|
13643
|
+
reportDiag(
|
|
13644
|
+
rt,
|
|
13645
|
+
cfg,
|
|
13646
|
+
`liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
|
|
13647
|
+
);
|
|
13648
|
+
reportDiag(
|
|
13649
|
+
rt,
|
|
13650
|
+
cfg,
|
|
13651
|
+
`audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
|
|
13652
|
+
);
|
|
13653
|
+
window.__system = {
|
|
13654
|
+
media: mediaControl,
|
|
13655
|
+
windowTitle: windowDriver.snapshot,
|
|
13656
|
+
shortcuts: shortcuts.last,
|
|
13657
|
+
live: () => live.status()
|
|
13658
|
+
};
|
|
13659
|
+
window.__liveSystem = () => live.status();
|
|
13361
13660
|
}
|
|
13362
|
-
const st = live.status();
|
|
13363
|
-
reportDiag(
|
|
13364
|
-
rt,
|
|
13365
|
-
cfg,
|
|
13366
|
-
`liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
|
|
13367
|
-
);
|
|
13368
|
-
reportDiag(
|
|
13369
|
-
rt,
|
|
13370
|
-
cfg,
|
|
13371
|
-
`audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
|
|
13372
|
-
);
|
|
13373
|
-
window.__system = {
|
|
13374
|
-
media: mediaControl,
|
|
13375
|
-
windowTitle: windowDriver.snapshot,
|
|
13376
|
-
shortcuts: shortcuts.last,
|
|
13377
|
-
live: () => live.status()
|
|
13378
|
-
};
|
|
13379
|
-
window.__liveSystem = () => live.status();
|
|
13380
13661
|
} catch (e) {
|
|
13381
13662
|
reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
|
|
13382
13663
|
live = null;
|
|
@@ -14708,17 +14989,15 @@ function mountScene(rt, cfg) {
|
|
|
14708
14989
|
if (now - lastRender >= interval) {
|
|
14709
14990
|
lastRender = now;
|
|
14710
14991
|
markFrame(rt, now);
|
|
14711
|
-
if (rt.onFirstFrame) {
|
|
14712
|
-
const first = rt.onFirstFrame;
|
|
14713
|
-
rt.onFirstFrame = void 0;
|
|
14714
|
-
first();
|
|
14715
|
-
}
|
|
14716
14992
|
syncCanvasSize(rt, c, rt.cfg);
|
|
14717
14993
|
const t = (now - start - pauseAccum) / 1e3;
|
|
14718
14994
|
inputView.update(pointerSrc.state);
|
|
14719
14995
|
if (mediaSim.enabled) {
|
|
14720
14996
|
if (live?.media) live.media.pump();
|
|
14721
|
-
else
|
|
14997
|
+
else {
|
|
14998
|
+
const drv = currentMediaDriver();
|
|
14999
|
+
if (typeof drv?.update === "function") drv.update(t);
|
|
15000
|
+
}
|
|
14722
15001
|
const snap = mediaSnapshot();
|
|
14723
15002
|
const evts = media.diffMediaEvents(lastMediaSnap, snap);
|
|
14724
15003
|
if (evts.length) {
|
|
@@ -14824,6 +15103,14 @@ function mountScene(rt, cfg) {
|
|
|
14824
15103
|
}
|
|
14825
15104
|
const peek = rt.coverAlign;
|
|
14826
15105
|
void renderer.render(scene, textures, c.width, c.height, t, normalizeFit(rt.cfg.fit), peek.x, peek.y).then(() => {
|
|
15106
|
+
if (rt.onFirstFrame) {
|
|
15107
|
+
const first = rt.onFirstFrame;
|
|
15108
|
+
rt.onFirstFrame = void 0;
|
|
15109
|
+
try {
|
|
15110
|
+
first();
|
|
15111
|
+
} catch {
|
|
15112
|
+
}
|
|
15113
|
+
}
|
|
14827
15114
|
if (disposed || rt.paused) return;
|
|
14828
15115
|
try {
|
|
14829
15116
|
dispatchCursor();
|
|
@@ -15144,6 +15431,44 @@ function defaultAudioDriver() {
|
|
|
15144
15431
|
}
|
|
15145
15432
|
};
|
|
15146
15433
|
}
|
|
15434
|
+
function bridgeAudioDriver(rt) {
|
|
15435
|
+
const left = new Float32Array(64);
|
|
15436
|
+
const right = new Float32Array(64);
|
|
15437
|
+
return {
|
|
15438
|
+
snapshot() {
|
|
15439
|
+
const src = rt.audioBridge?.();
|
|
15440
|
+
const sl = src?.left;
|
|
15441
|
+
const sr = src?.right;
|
|
15442
|
+
const n = sl && sr ? Math.min(64, sl.length, sr.length) : 0;
|
|
15443
|
+
for (let i = 0; i < n; i++) {
|
|
15444
|
+
left[i] = Math.max(0, Math.min(1, Number(sl[i]) || 0));
|
|
15445
|
+
right[i] = Math.max(0, Math.min(1, Number(sr[i]) || 0));
|
|
15446
|
+
}
|
|
15447
|
+
for (let i = n; i < 64; i++) {
|
|
15448
|
+
left[i] = 0;
|
|
15449
|
+
right[i] = 0;
|
|
15450
|
+
}
|
|
15451
|
+
return { left, right };
|
|
15452
|
+
}
|
|
15453
|
+
};
|
|
15454
|
+
}
|
|
15455
|
+
function liveAudioDriver(handle) {
|
|
15456
|
+
const left = new Float32Array(64);
|
|
15457
|
+
const right = new Float32Array(64);
|
|
15458
|
+
return {
|
|
15459
|
+
tick() {
|
|
15460
|
+
handle.audio.pump();
|
|
15461
|
+
},
|
|
15462
|
+
snapshot() {
|
|
15463
|
+
const s = handle.audio.snapshot;
|
|
15464
|
+
for (let i = 0; i < 64; i++) {
|
|
15465
|
+
left[i] = Math.max(0, Math.min(1, Number(s.left64[i]) || 0));
|
|
15466
|
+
right[i] = Math.max(0, Math.min(1, Number(s.right64[i]) || 0));
|
|
15467
|
+
}
|
|
15468
|
+
return { left, right };
|
|
15469
|
+
}
|
|
15470
|
+
};
|
|
15471
|
+
}
|
|
15147
15472
|
function resolveContainer(rt, cfg) {
|
|
15148
15473
|
if (rt.wrap) return rt.wrap;
|
|
15149
15474
|
const el = cfg.canvas;
|
|
@@ -15458,8 +15783,10 @@ function pushMediaDiff(rt, prev, snap) {
|
|
|
15458
15783
|
}
|
|
15459
15784
|
return media.cloneMediaSnapshot(snap);
|
|
15460
15785
|
}
|
|
15461
|
-
function startAudioPump(rt, driver, frameClock) {
|
|
15786
|
+
function startAudioPump(rt, driver, frameClock, liveHold = { driver: null }) {
|
|
15462
15787
|
if (!driver) return;
|
|
15788
|
+
const bridged = bridgeAudioDriver(rt);
|
|
15789
|
+
const pick = () => rt.audioBridge ? bridged : liveHold.driver ?? driver;
|
|
15463
15790
|
let raf = 0;
|
|
15464
15791
|
let lastPush = 0;
|
|
15465
15792
|
const tick = (now) => {
|
|
@@ -15471,8 +15798,9 @@ function startAudioPump(rt, driver, frameClock) {
|
|
|
15471
15798
|
if (now - lastPush < interval * 0.85) return;
|
|
15472
15799
|
lastPush = now;
|
|
15473
15800
|
try {
|
|
15474
|
-
|
|
15475
|
-
|
|
15801
|
+
const cur = pick();
|
|
15802
|
+
cur.tick?.(now);
|
|
15803
|
+
const snap = cur.snapshot();
|
|
15476
15804
|
const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
|
|
15477
15805
|
weShimCall(rt, (w) => w.__wePushAudio?.(arr));
|
|
15478
15806
|
if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
|
|
@@ -15491,6 +15819,7 @@ function startAudioPump(rt, driver, frameClock) {
|
|
|
15491
15819
|
}
|
|
15492
15820
|
function startMediaPump(rt, driver) {
|
|
15493
15821
|
if (!driver) return;
|
|
15822
|
+
const pick = () => rt.mediaSource ?? driver;
|
|
15494
15823
|
let raf = 0;
|
|
15495
15824
|
let lastMedia = null;
|
|
15496
15825
|
let lastTick = 0;
|
|
@@ -15500,8 +15829,9 @@ function startMediaPump(rt, driver) {
|
|
|
15500
15829
|
if (now - lastTick < 200) return;
|
|
15501
15830
|
lastTick = now;
|
|
15502
15831
|
try {
|
|
15503
|
-
|
|
15504
|
-
|
|
15832
|
+
const cur = pick();
|
|
15833
|
+
cur.update?.(now / 1e3);
|
|
15834
|
+
lastMedia = pushMediaDiff(rt, lastMedia, cur.snapshot);
|
|
15505
15835
|
} catch {
|
|
15506
15836
|
}
|
|
15507
15837
|
};
|
|
@@ -15596,18 +15926,65 @@ function mountWeb(rt, cfg) {
|
|
|
15596
15926
|
}
|
|
15597
15927
|
installWebCtl(rt);
|
|
15598
15928
|
const cfgExt = cfg;
|
|
15599
|
-
const audioDriver = cfgExt._webAudio === null ? null : cfgExt._webAudio ?? defaultAudioDriver();
|
|
15600
|
-
const mediaDriver = cfgExt._webMedia === null ? null : cfgExt._webMedia ?? defaultMediaDriver();
|
|
15929
|
+
const audioDriver = cfgExt._webAudio === null || rt.audioDisabled ? null : cfgExt._webAudio ?? defaultAudioDriver();
|
|
15930
|
+
const mediaDriver = cfgExt._webMedia === null || rt.mediaDisabled ? null : cfgExt._webMedia ?? defaultMediaDriver();
|
|
15601
15931
|
const finishBare = (why) => {
|
|
15602
15932
|
reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
|
|
15603
15933
|
attachIframe(rt, cfg, container, entry, { injected: false });
|
|
15604
15934
|
startAudioPump(rt, null);
|
|
15605
15935
|
startMediaPump(rt, null);
|
|
15936
|
+
let beat = 0;
|
|
15937
|
+
const tick = (now) => {
|
|
15938
|
+
if (!rt.iframe) return;
|
|
15939
|
+
beat = requestAnimationFrame(tick);
|
|
15940
|
+
if (rt.paused) return;
|
|
15941
|
+
markFrame(rt, now);
|
|
15942
|
+
};
|
|
15943
|
+
beat = requestAnimationFrame(tick);
|
|
15944
|
+
(rt.wallpaperDisposers ??= []).push(() => cancelAnimationFrame(beat));
|
|
15606
15945
|
};
|
|
15607
15946
|
const frameClock = { last: 0 };
|
|
15947
|
+
const liveHold = { driver: null };
|
|
15608
15948
|
const startPumps = () => {
|
|
15609
|
-
startAudioPump(rt, audioDriver, frameClock);
|
|
15949
|
+
startAudioPump(rt, audioDriver, frameClock, liveHold);
|
|
15610
15950
|
startMediaPump(rt, mediaDriver);
|
|
15951
|
+
if (cfg.liveSystem && audioDriver) {
|
|
15952
|
+
const liveSlot = {
|
|
15953
|
+
handle: null,
|
|
15954
|
+
dead: false
|
|
15955
|
+
};
|
|
15956
|
+
(rt.wallpaperDisposers ??= []).push(() => {
|
|
15957
|
+
liveSlot.dead = true;
|
|
15958
|
+
liveHold.driver = null;
|
|
15959
|
+
try {
|
|
15960
|
+
liveSlot.handle?.dispose();
|
|
15961
|
+
} catch {
|
|
15962
|
+
}
|
|
15963
|
+
liveSlot.handle = null;
|
|
15964
|
+
});
|
|
15965
|
+
void (async () => {
|
|
15966
|
+
try {
|
|
15967
|
+
const live = await startLiveSystem({ origin: location.origin });
|
|
15968
|
+
if (liveSlot.dead) {
|
|
15969
|
+
try {
|
|
15970
|
+
live.dispose();
|
|
15971
|
+
} catch {
|
|
15972
|
+
}
|
|
15973
|
+
return;
|
|
15974
|
+
}
|
|
15975
|
+
liveSlot.handle = live;
|
|
15976
|
+
const st = live.status();
|
|
15977
|
+
if (st.audio === "mic") {
|
|
15978
|
+
liveHold.driver = liveAudioDriver(live);
|
|
15979
|
+
reportDiag(rt, cfg, "liveSystem: 网页壁纸音频改用麦克风");
|
|
15980
|
+
} else {
|
|
15981
|
+
reportDiag(rt, cfg, `liveSystem: 麦克风不可用(${st.audio}),网页壁纸沿用模拟源`);
|
|
15982
|
+
}
|
|
15983
|
+
} catch (e) {
|
|
15984
|
+
reportDiag(rt, cfg, `liveSystem: 启动失败,网页壁纸沿用模拟源 (${e?.message ?? e})`);
|
|
15985
|
+
}
|
|
15986
|
+
})();
|
|
15987
|
+
}
|
|
15611
15988
|
};
|
|
15612
15989
|
void (async () => {
|
|
15613
15990
|
const defaults = await fetchProjectWire(entry);
|
|
@@ -15674,6 +16051,99 @@ function mountWallpaper(rt, cfg) {
|
|
|
15674
16051
|
rt.onUnhandledType?.(cfg);
|
|
15675
16052
|
}
|
|
15676
16053
|
}
|
|
16054
|
+
class Color {
|
|
16055
|
+
x;
|
|
16056
|
+
y;
|
|
16057
|
+
z;
|
|
16058
|
+
constructor(x, y, z) {
|
|
16059
|
+
this.x = Number(x) || 0;
|
|
16060
|
+
this.y = Number(y) || 0;
|
|
16061
|
+
this.z = Number(z) || 0;
|
|
16062
|
+
}
|
|
16063
|
+
add(o) {
|
|
16064
|
+
return new Color(this.x + o.x, this.y + o.y, this.z + o.z);
|
|
16065
|
+
}
|
|
16066
|
+
subtract(o) {
|
|
16067
|
+
return new Color(this.x - o.x, this.y - o.y, this.z - o.z);
|
|
16068
|
+
}
|
|
16069
|
+
multiply(k) {
|
|
16070
|
+
if (typeof k === "number") return new Color(this.x * k, this.y * k, this.z * k);
|
|
16071
|
+
return new Color(this.x * k.x, this.y * k.y, this.z * k.z);
|
|
16072
|
+
}
|
|
16073
|
+
toString() {
|
|
16074
|
+
return `${this.x} ${this.y} ${this.z}`;
|
|
16075
|
+
}
|
|
16076
|
+
}
|
|
16077
|
+
function mediaColor(r, g, b) {
|
|
16078
|
+
if (r instanceof Color) return r;
|
|
16079
|
+
if (Array.isArray(r)) return new Color(r[0] ?? 0, r[1] ?? 0, r[2] ?? 0);
|
|
16080
|
+
if (typeof r === "object" && r !== null) {
|
|
16081
|
+
const o = r;
|
|
16082
|
+
return new Color(o.x ?? 0, o.y ?? 0, o.z ?? 0);
|
|
16083
|
+
}
|
|
16084
|
+
return new Color(r, g ?? 0, b ?? 0);
|
|
16085
|
+
}
|
|
16086
|
+
const DEF_PRIMARY = [0.35, 0.38, 0.45];
|
|
16087
|
+
const DEF_SECONDARY = [0.12, 0.13, 0.17];
|
|
16088
|
+
const DEF_TERTIARY = [0.72, 0.76, 0.84];
|
|
16089
|
+
const DEF_TEXT = [0.95, 0.96, 0.98];
|
|
16090
|
+
function locateLyric(lyrics, position) {
|
|
16091
|
+
if (!Array.isArray(lyrics) || lyrics.length === 0) return { line: "", index: -1 };
|
|
16092
|
+
let idx = -1;
|
|
16093
|
+
for (let i = 0; i < lyrics.length; i++) {
|
|
16094
|
+
const at = Number(lyrics[i]?.[0]);
|
|
16095
|
+
if (Number.isFinite(at) && at <= position) idx = i;
|
|
16096
|
+
else break;
|
|
16097
|
+
}
|
|
16098
|
+
return { line: idx >= 0 ? String(lyrics[idx][1] ?? "") : "", index: idx };
|
|
16099
|
+
}
|
|
16100
|
+
function buildSnapshot(init) {
|
|
16101
|
+
const position = Number(init.position) || 0;
|
|
16102
|
+
const lyrics = Array.isArray(init.lyrics) ? init.lyrics : [];
|
|
16103
|
+
const { line, index } = locateLyric(lyrics, position);
|
|
16104
|
+
const state = init.state !== void 0 ? init.state : init.playing === false ? 2 : init.playing ? 1 : 0;
|
|
16105
|
+
return {
|
|
16106
|
+
hasMedia: init.hasMedia ?? (init.title != null || init.artist != null || !!init.playing),
|
|
16107
|
+
state,
|
|
16108
|
+
title: String(init.title ?? ""),
|
|
16109
|
+
artist: String(init.artist ?? ""),
|
|
16110
|
+
album: String(init.album ?? ""),
|
|
16111
|
+
albumArtist: String(init.albumArtist ?? init.artist ?? ""),
|
|
16112
|
+
position,
|
|
16113
|
+
duration: Number(init.duration) || 0,
|
|
16114
|
+
hasThumbnail: init.hasThumbnail ?? false,
|
|
16115
|
+
primaryColor: mediaColor(init.primaryColor ?? DEF_PRIMARY),
|
|
16116
|
+
secondaryColor: mediaColor(init.secondaryColor ?? DEF_SECONDARY),
|
|
16117
|
+
tertiaryColor: mediaColor(init.tertiaryColor ?? DEF_TERTIARY),
|
|
16118
|
+
textColor: mediaColor(init.textColor ?? DEF_TEXT),
|
|
16119
|
+
highContrastColor: mediaColor(init.highContrastColor ?? init.textColor ?? DEF_TEXT),
|
|
16120
|
+
trackIndex: Number(init.trackIndex) || 0,
|
|
16121
|
+
lyrics,
|
|
16122
|
+
lyricLine: line,
|
|
16123
|
+
lyricIndex: index
|
|
16124
|
+
};
|
|
16125
|
+
}
|
|
16126
|
+
function createMediaSource(init = {}, controls = {}) {
|
|
16127
|
+
let cur = { ...init };
|
|
16128
|
+
let snap = buildSnapshot(cur);
|
|
16129
|
+
return {
|
|
16130
|
+
get snapshot() {
|
|
16131
|
+
return snap;
|
|
16132
|
+
},
|
|
16133
|
+
set(patch) {
|
|
16134
|
+
cur = { ...cur, ...patch };
|
|
16135
|
+
snap = buildSnapshot(cur);
|
|
16136
|
+
},
|
|
16137
|
+
// 宿主的快照由外部事件驱动,不需要按帧自行推进;留空实现满足接口即可
|
|
16138
|
+
update() {
|
|
16139
|
+
},
|
|
16140
|
+
skipNext: controls.skipNext,
|
|
16141
|
+
skipPrevious: controls.skipPrevious,
|
|
16142
|
+
play: controls.play,
|
|
16143
|
+
pause: controls.pause,
|
|
16144
|
+
playPause: controls.playPause
|
|
16145
|
+
};
|
|
16146
|
+
}
|
|
15677
16147
|
function normalizeFitOption(fit) {
|
|
15678
16148
|
if (fit === "fit") return "contain";
|
|
15679
16149
|
if (fit === "fill") return "cover";
|
|
@@ -15683,6 +16153,13 @@ function isWebProject(project) {
|
|
|
15683
16153
|
const t = project?.type;
|
|
15684
16154
|
return typeof t === "string" && t.toLowerCase() === "web";
|
|
15685
16155
|
}
|
|
16156
|
+
const MEDIA_TYPES = /* @__PURE__ */ new Set(["video", "gif", "image"]);
|
|
16157
|
+
function mediaProjectType(project) {
|
|
16158
|
+
const t = project?.type;
|
|
16159
|
+
if (typeof t !== "string") return null;
|
|
16160
|
+
const lower = t.toLowerCase();
|
|
16161
|
+
return MEDIA_TYPES.has(lower) ? lower : null;
|
|
16162
|
+
}
|
|
15686
16163
|
function ensureSceneCanvas(el) {
|
|
15687
16164
|
if (el instanceof HTMLCanvasElement) return el;
|
|
15688
16165
|
const existing = el.querySelector(":scope > canvas[data-webwallgl]");
|
|
@@ -15725,6 +16202,47 @@ async function resolveMountConfig(el, o) {
|
|
|
15725
16202
|
if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
|
|
15726
16203
|
return { ...base, type: "web", src: url, source: o.source };
|
|
15727
16204
|
}
|
|
16205
|
+
const mediaType = mediaProjectType(project);
|
|
16206
|
+
if (mediaType) {
|
|
16207
|
+
let url;
|
|
16208
|
+
let entryType;
|
|
16209
|
+
try {
|
|
16210
|
+
const entry = await o.source.mediaEntry?.();
|
|
16211
|
+
url = entry?.url;
|
|
16212
|
+
entryType = entry?.type;
|
|
16213
|
+
} catch {
|
|
16214
|
+
url = void 0;
|
|
16215
|
+
}
|
|
16216
|
+
if (!url && o.source.key) {
|
|
16217
|
+
const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") : "";
|
|
16218
|
+
if (file) url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
|
|
16219
|
+
}
|
|
16220
|
+
if (!url) {
|
|
16221
|
+
throw new Error(
|
|
16222
|
+
`媒体壁纸(${mediaType}):无法解析资源 URL(需要 Source.mediaEntry 或 project.file + httpSource)`
|
|
16223
|
+
);
|
|
16224
|
+
}
|
|
16225
|
+
const canvas2 = ensureSceneCanvas(el);
|
|
16226
|
+
const finalType = MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : mediaType;
|
|
16227
|
+
return { ...base, type: finalType, src: url, canvas: canvas2, source: o.source };
|
|
16228
|
+
}
|
|
16229
|
+
const declaredType = typeof project?.type === "string" ? String(project.type).trim() : "";
|
|
16230
|
+
if (!declaredType && typeof o.source.mediaEntry === "function") {
|
|
16231
|
+
let url;
|
|
16232
|
+
let entryType;
|
|
16233
|
+
try {
|
|
16234
|
+
const entry = await o.source.mediaEntry();
|
|
16235
|
+
url = entry?.url;
|
|
16236
|
+
entryType = entry?.type;
|
|
16237
|
+
} catch {
|
|
16238
|
+
url = void 0;
|
|
16239
|
+
}
|
|
16240
|
+
const sniffed = (MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : null) ?? (url ? sniffMediaType(url) : null);
|
|
16241
|
+
if (url && sniffed) {
|
|
16242
|
+
const canvas2 = ensureSceneCanvas(el);
|
|
16243
|
+
return { ...base, type: sniffed, src: url, canvas: canvas2, source: o.source };
|
|
16244
|
+
}
|
|
16245
|
+
}
|
|
15728
16246
|
const canvas = ensureSceneCanvas(el);
|
|
15729
16247
|
return { ...base, type: "scene", canvas, source: o.source };
|
|
15730
16248
|
}
|
|
@@ -15764,6 +16282,26 @@ function createScene(el, options) {
|
|
|
15764
16282
|
rt.onSceneInfo = (info) => {
|
|
15765
16283
|
rt.info = info;
|
|
15766
16284
|
};
|
|
16285
|
+
if ("audio" in o) applyAudio(o.audio ?? null);
|
|
16286
|
+
if ("media" in o) {
|
|
16287
|
+
rt.mediaSource = o.media ?? null;
|
|
16288
|
+
rt.mediaDisabled = o.media === null;
|
|
16289
|
+
}
|
|
16290
|
+
};
|
|
16291
|
+
const applyAudio = (src) => {
|
|
16292
|
+
rt.audioDisabled = src === null;
|
|
16293
|
+
if (!src) {
|
|
16294
|
+
rt.audioBridge = null;
|
|
16295
|
+
return;
|
|
16296
|
+
}
|
|
16297
|
+
rt.audioBridge = () => {
|
|
16298
|
+
try {
|
|
16299
|
+
const s = src.snapshot();
|
|
16300
|
+
return s && s.left && s.right ? s : null;
|
|
16301
|
+
} catch {
|
|
16302
|
+
return null;
|
|
16303
|
+
}
|
|
16304
|
+
};
|
|
15767
16305
|
};
|
|
15768
16306
|
const armFirstFrame = () => {
|
|
15769
16307
|
return new Promise((resolve) => {
|
|
@@ -15820,6 +16358,7 @@ function createScene(el, options) {
|
|
|
15820
16358
|
setFit(fit) {
|
|
15821
16359
|
rt.cfg.fit = fit;
|
|
15822
16360
|
resetCoverAlign(rt);
|
|
16361
|
+
rt.webRelayout?.();
|
|
15823
16362
|
},
|
|
15824
16363
|
setFps(fps) {
|
|
15825
16364
|
rt.cfg.sceneFps = fps;
|
|
@@ -15843,11 +16382,81 @@ function createScene(el, options) {
|
|
|
15843
16382
|
getProperties() {
|
|
15844
16383
|
return { ...rt.liveUserProps ?? {} };
|
|
15845
16384
|
},
|
|
16385
|
+
// 宿主频谱源。只存引用,实际每帧拉取在 scene-mount 的渲染循环里。
|
|
16386
|
+
// 与 __wp.setAudioBridge 同纪律:换场景不清空,装一次对之后所有场景生效
|
|
16387
|
+
// (wireOptions 也只在选项里出现 audio 时才覆盖,见那里的说明)。
|
|
16388
|
+
setAudio(src) {
|
|
16389
|
+
currentOptions = { ...currentOptions, audio: src };
|
|
16390
|
+
applyAudio(src);
|
|
16391
|
+
rt.audioDisabled = false;
|
|
16392
|
+
},
|
|
16393
|
+
// 系统媒体源。与 setAudio 同纪律:只存引用、换场景不清空,
|
|
16394
|
+
// scene 与 web 两条装配路径读同一个 rt.mediaSource。
|
|
16395
|
+
// 注意语义与 MountOptions.media:null 不同:这里的 null 按文档是
|
|
16396
|
+
// 「回落内置模拟源」,不是禁用(禁用只在挂载选项里表达)。
|
|
16397
|
+
setMedia(src) {
|
|
16398
|
+
currentOptions = { ...currentOptions, media: src };
|
|
16399
|
+
rt.mediaSource = src ?? null;
|
|
16400
|
+
rt.mediaDisabled = false;
|
|
16401
|
+
},
|
|
16402
|
+
// 媒体控制面。装配后由 mountScene 写入 rt.mediaCtl;未装配(或媒体/网页
|
|
16403
|
+
// 壁纸尚无控制面)时给一个惰性替身,读快照得空、控制方法静默无效 ——
|
|
16404
|
+
// 让调用方能无条件 `wp.media.playPause()` 而不必先判空。
|
|
16405
|
+
get media() {
|
|
16406
|
+
const ctl = rt.mediaCtl;
|
|
16407
|
+
if (ctl) return ctl;
|
|
16408
|
+
const empty = {
|
|
16409
|
+
hasMedia: false,
|
|
16410
|
+
state: 0,
|
|
16411
|
+
title: "",
|
|
16412
|
+
artist: "",
|
|
16413
|
+
album: "",
|
|
16414
|
+
albumArtist: "",
|
|
16415
|
+
position: 0,
|
|
16416
|
+
duration: 0,
|
|
16417
|
+
hasThumbnail: false,
|
|
16418
|
+
primaryColor: mediaColor(0, 0, 0),
|
|
16419
|
+
secondaryColor: mediaColor(0, 0, 0),
|
|
16420
|
+
tertiaryColor: mediaColor(0, 0, 0),
|
|
16421
|
+
textColor: mediaColor(1, 1, 1),
|
|
16422
|
+
highContrastColor: mediaColor(1, 1, 1),
|
|
16423
|
+
trackIndex: 0,
|
|
16424
|
+
lyrics: [],
|
|
16425
|
+
lyricLine: "",
|
|
16426
|
+
lyricIndex: -1
|
|
16427
|
+
};
|
|
16428
|
+
const noop = () => empty;
|
|
16429
|
+
return {
|
|
16430
|
+
get snapshot() {
|
|
16431
|
+
return empty;
|
|
16432
|
+
},
|
|
16433
|
+
skipNext: noop,
|
|
16434
|
+
skipPrevious: noop,
|
|
16435
|
+
play: noop,
|
|
16436
|
+
pause: noop,
|
|
16437
|
+
playPause: noop
|
|
16438
|
+
};
|
|
16439
|
+
},
|
|
16440
|
+
// 外部指针注入。pointerCtl 由 mountScene / mountWeb 各自装配时设置,
|
|
16441
|
+
// 媒体壁纸不设 —— 那时这里静默无效,与整页渲染器的 __wp.pushPointer 一致。
|
|
16442
|
+
pushPointer(u, v, buttons) {
|
|
16443
|
+
rt.pointerCtl?.push({ u, v, buttons });
|
|
16444
|
+
},
|
|
16445
|
+
pointerLeave() {
|
|
16446
|
+
rt.pointerCtl?.leave();
|
|
16447
|
+
},
|
|
15846
16448
|
async load(source) {
|
|
16449
|
+
const prev = currentOptions.source;
|
|
16450
|
+
if (prev && prev !== source) {
|
|
16451
|
+
try {
|
|
16452
|
+
prev.dispose?.();
|
|
16453
|
+
} catch {
|
|
16454
|
+
}
|
|
16455
|
+
}
|
|
15847
16456
|
currentOptions = { ...currentOptions, source };
|
|
15848
16457
|
wireOptions(currentOptions);
|
|
15849
16458
|
const cfg = await resolveMountConfig(boundEl, currentOptions);
|
|
15850
|
-
if (cfg.type
|
|
16459
|
+
if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
|
|
15851
16460
|
boundEl = cfg.canvas;
|
|
15852
16461
|
}
|
|
15853
16462
|
rt.cfg = cfg;
|
|
@@ -15874,6 +16483,10 @@ function createScene(el, options) {
|
|
|
15874
16483
|
},
|
|
15875
16484
|
destroy() {
|
|
15876
16485
|
destroyRuntime(rt);
|
|
16486
|
+
try {
|
|
16487
|
+
currentOptions.source?.dispose?.();
|
|
16488
|
+
} catch {
|
|
16489
|
+
}
|
|
15877
16490
|
rt.onDiagnostic = void 0;
|
|
15878
16491
|
rt.onError = void 0;
|
|
15879
16492
|
rt.onFirstFrame = void 0;
|
|
@@ -15901,11 +16514,11 @@ function createScene(el, options) {
|
|
|
15901
16514
|
currentOptions = o;
|
|
15902
16515
|
wireOptions(o);
|
|
15903
16516
|
const cfg = await resolveMountConfig(el, o);
|
|
15904
|
-
if (cfg.type
|
|
16517
|
+
if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
|
|
15905
16518
|
boundEl = cfg.canvas;
|
|
15906
16519
|
}
|
|
15907
16520
|
rt.cfg = cfg;
|
|
15908
|
-
rt.paused =
|
|
16521
|
+
rt.paused = false;
|
|
15909
16522
|
rt.info = void 0;
|
|
15910
16523
|
resetCoverAlign(rt);
|
|
15911
16524
|
if (o.properties && Object.keys(o.properties).length) {
|
|
@@ -15919,6 +16532,7 @@ function createScene(el, options) {
|
|
|
15919
16532
|
} finally {
|
|
15920
16533
|
failure.off();
|
|
15921
16534
|
}
|
|
16535
|
+
if (o.autoplay === false) instance.pause();
|
|
15922
16536
|
if ((o.volume ?? 0) > 0) instance.setVolume(o.volume);
|
|
15923
16537
|
if (o.properties && Object.keys(o.properties).length) instance.setProperties(o.properties);
|
|
15924
16538
|
};
|
|
@@ -15933,9 +16547,13 @@ async function mount(el, options) {
|
|
|
15933
16547
|
}
|
|
15934
16548
|
export {
|
|
15935
16549
|
bytesSource,
|
|
16550
|
+
createMediaSource,
|
|
15936
16551
|
createScene,
|
|
15937
16552
|
fileSource,
|
|
15938
16553
|
httpSource,
|
|
15939
|
-
|
|
16554
|
+
mediaColor,
|
|
16555
|
+
mediaSource,
|
|
16556
|
+
mount,
|
|
16557
|
+
sniffMediaType
|
|
15940
16558
|
};
|
|
15941
16559
|
//# sourceMappingURL=webwallgl.mjs.map
|