webwallgl 1.0.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 +238 -3
- package/README.md +237 -3
- package/package.json +1 -1
- package/types.d.ts +175 -18
- package/webwallgl.d.ts +37 -17
- package/webwallgl.global.js +2244 -217
- package/webwallgl.global.js.map +1 -1
- package/webwallgl.global.min.js +1241 -33
- package/webwallgl.global.min.js.map +1 -1
- package/webwallgl.min.mjs +1241 -33
- package/webwallgl.min.mjs.map +1 -1
- package/webwallgl.mjs +2245 -218
- package/webwallgl.mjs.map +1 -1
package/webwallgl.mjs
CHANGED
|
@@ -29,14 +29,42 @@ function createRuntime(opts) {
|
|
|
29
29
|
window.__blockContextMenu = (doc) => doc.addEventListener("contextmenu", block, true);
|
|
30
30
|
})();
|
|
31
31
|
function clear(rt) {
|
|
32
|
-
if (rt.
|
|
32
|
+
if (rt.iframe) {
|
|
33
|
+
try {
|
|
34
|
+
rt.iframe.contentWindow?.location.replace("about:blank");
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (rt.wrap) {
|
|
39
|
+
rt.wrap.innerHTML = "";
|
|
40
|
+
} else if (rt.iframe?.isConnected) {
|
|
41
|
+
try {
|
|
42
|
+
rt.iframe.remove();
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
33
46
|
if (rt.raf !== void 0) cancelAnimationFrame(rt.raf);
|
|
34
47
|
rt.raf = void 0;
|
|
35
48
|
for (const p of rt.videoPairs ?? []) p.destroy();
|
|
36
49
|
rt.videoPairs = void 0;
|
|
37
|
-
if (rt.sceneCleanup)
|
|
50
|
+
if (rt.sceneCleanup) {
|
|
51
|
+
try {
|
|
52
|
+
rt.sceneCleanup();
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
38
56
|
rt.sceneCleanup = void 0;
|
|
39
57
|
rt.sceneCtl = void 0;
|
|
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
|
+
}
|
|
40
68
|
if (rt.renderer) {
|
|
41
69
|
rt.renderer.dispose?.();
|
|
42
70
|
rt.renderer = void 0;
|
|
@@ -78,10 +106,18 @@ function clear(rt) {
|
|
|
78
106
|
rt.canvas = void 0;
|
|
79
107
|
rt.ctx = void 0;
|
|
80
108
|
rt.info = void 0;
|
|
109
|
+
clearSceneDebugGlobals();
|
|
81
110
|
resetFrameMeter(rt);
|
|
82
111
|
}
|
|
83
112
|
function destroyRuntime(rt) {
|
|
84
|
-
|
|
113
|
+
try {
|
|
114
|
+
clear(rt);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
if (rt.peekRaf !== void 0) {
|
|
118
|
+
cancelAnimationFrame(rt.peekRaf);
|
|
119
|
+
rt.peekRaf = void 0;
|
|
120
|
+
}
|
|
85
121
|
for (const off of rt.disposers.splice(0)) {
|
|
86
122
|
try {
|
|
87
123
|
off();
|
|
@@ -120,7 +156,7 @@ function reportDiag(rt, cfg, msg) {
|
|
|
120
156
|
} catch {
|
|
121
157
|
}
|
|
122
158
|
try {
|
|
123
|
-
const origin = cfg.mediaBase ? new URL(cfg.mediaBase).origin : "";
|
|
159
|
+
const origin = cfg.mediaBase ? new URL(cfg.mediaBase, window.location.href).origin : "";
|
|
124
160
|
if (origin) {
|
|
125
161
|
const img = new Image();
|
|
126
162
|
img.src = `${origin}/diag?msg=${encodeURIComponent(`scene ${cfg.src ?? "?"}: ${msg.slice(0, 500)}`)}`;
|
|
@@ -137,6 +173,32 @@ function applyCoverAlignToDom(rt) {
|
|
|
137
173
|
const obj = rt.video ?? rt.img;
|
|
138
174
|
if (obj && obj.isConnected) obj.style.objectPosition = pos;
|
|
139
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
|
+
}
|
|
140
202
|
function syncCanvasSize(rt, canvas, cfg) {
|
|
141
203
|
const dpr = effectiveDpr(rt, cfg);
|
|
142
204
|
const w = Math.max(1, Math.round((canvas.clientWidth || window.innerWidth || 1) * dpr));
|
|
@@ -863,6 +925,31 @@ function parseNum(v, dflt) {
|
|
|
863
925
|
}
|
|
864
926
|
return dflt;
|
|
865
927
|
}
|
|
928
|
+
function isRenderInert(o) {
|
|
929
|
+
if (!o) return false;
|
|
930
|
+
return !o.image && !o.model && !o.particle && o.text == null && !o.size;
|
|
931
|
+
}
|
|
932
|
+
function composeChildTransform(parentWorld, childLocal, parentScalePropagates) {
|
|
933
|
+
const pscale = parentScalePropagates ? parentWorld.scale : [1, 1, 1];
|
|
934
|
+
const ca = (parentWorld.angles[2] || 0) * Math.PI / 180;
|
|
935
|
+
const cos = Math.cos(ca);
|
|
936
|
+
const sin = Math.sin(ca);
|
|
937
|
+
const ox = childLocal.origin[0] * pscale[0];
|
|
938
|
+
const oy = childLocal.origin[1] * pscale[1];
|
|
939
|
+
return {
|
|
940
|
+
origin: [
|
|
941
|
+
parentWorld.origin[0] + ox * cos - oy * sin,
|
|
942
|
+
parentWorld.origin[1] + ox * sin + oy * cos,
|
|
943
|
+
parentWorld.origin[2] + (childLocal.origin[2] || 0)
|
|
944
|
+
],
|
|
945
|
+
scale: [
|
|
946
|
+
pscale[0] * childLocal.scale[0],
|
|
947
|
+
pscale[1] * childLocal.scale[1],
|
|
948
|
+
pscale[2] * childLocal.scale[2]
|
|
949
|
+
],
|
|
950
|
+
angles: [childLocal.angles[0], childLocal.angles[1], (parentWorld.angles[2] || 0) + childLocal.angles[2]]
|
|
951
|
+
};
|
|
952
|
+
}
|
|
866
953
|
function parseScene(sceneJson, project) {
|
|
867
954
|
const properties = project && project.general && project.general.properties || {};
|
|
868
955
|
const objects = sceneJson.objects || [];
|
|
@@ -879,6 +966,11 @@ function parseScene(sceneJson, project) {
|
|
|
879
966
|
scale: parseVec3(o.scale || "1 1 1"),
|
|
880
967
|
angles: parseVec3(o.angles || "0 0 0")
|
|
881
968
|
}));
|
|
969
|
+
const localSnapshot = local.map((c) => ({
|
|
970
|
+
origin: c.origin.slice(),
|
|
971
|
+
scale: c.scale.slice(),
|
|
972
|
+
angles: c.angles.slice()
|
|
973
|
+
}));
|
|
882
974
|
for (let pass = 0; pass < 8; pass++) {
|
|
883
975
|
let changed = false;
|
|
884
976
|
for (const c of local) {
|
|
@@ -892,20 +984,11 @@ function parseScene(sceneJson, project) {
|
|
|
892
984
|
const pr = objects[pIdx];
|
|
893
985
|
const prs = pr.scale;
|
|
894
986
|
const runtimeBound = prs !== null && typeof prs === "object" && (typeof prs.script === "string" || prs.user !== void 0);
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const ox = c.origin[0] * pscale[0];
|
|
901
|
-
const oy = c.origin[1] * pscale[1];
|
|
902
|
-
c.origin[0] = pc.origin[0] + ox * cos - oy * sin;
|
|
903
|
-
c.origin[1] = pc.origin[1] + ox * sin + oy * cos;
|
|
904
|
-
c.origin[2] = pc.origin[2] + c.origin[2];
|
|
905
|
-
c.angles[2] = pc.angles[2] + c.angles[2];
|
|
906
|
-
c.scale[0] = pscale[0] * c.scale[0];
|
|
907
|
-
c.scale[1] = pscale[1] * c.scale[1];
|
|
908
|
-
c.scale[2] = pscale[2] * c.scale[2];
|
|
987
|
+
const propagateScale = !(runtimeBound && isRenderInert(pr));
|
|
988
|
+
const w = composeChildTransform(pc, c, propagateScale);
|
|
989
|
+
c.origin = w.origin;
|
|
990
|
+
c.scale = w.scale;
|
|
991
|
+
c.angles = w.angles;
|
|
909
992
|
c.parent = null;
|
|
910
993
|
changed = true;
|
|
911
994
|
}
|
|
@@ -1125,6 +1208,18 @@ function parseScene(sceneJson, project) {
|
|
|
1125
1208
|
origin: layerOrigin,
|
|
1126
1209
|
scale: world.scale,
|
|
1127
1210
|
angles: world.angles,
|
|
1211
|
+
// [we-scene patch] 父级相对变换(WE 场景图的真实语义)。origin/scale/angles
|
|
1212
|
+
// 上的脚本与关键帧动画一律在这层空间收发,再由 recomposeWorld 合成回上面的
|
|
1213
|
+
// world 三件套。渲染 / hittest / getTransformMatrix 仍只读 world,不受影响。
|
|
1214
|
+
// isPostProcess 层的 world 被强制成整幅画布,local 对它无意义(recompose 跳过)。
|
|
1215
|
+
localOrigin: localSnapshot[i].origin,
|
|
1216
|
+
localScale: localSnapshot[i].scale,
|
|
1217
|
+
localAngles: localSnapshot[i].angles,
|
|
1218
|
+
// 「渲染惰性纯容器」:父 scale 是否传给子层由父级这个标志决定,
|
|
1219
|
+
// 判据与 parse 合并阶段逐字相同(见 isRenderInert)。
|
|
1220
|
+
renderInert: isRenderInert(o),
|
|
1221
|
+
// 父 scale 绑了脚本/用户属性(运行时可变)。与 renderInert 一起决定传播闸门。
|
|
1222
|
+
scaleRuntimeBound: !!(o.scale !== null && typeof o.scale === "object" && (typeof o.scale.script === "string" || o.scale.user !== void 0)),
|
|
1128
1223
|
size: layerSize,
|
|
1129
1224
|
alignment: o.alignment || "center",
|
|
1130
1225
|
color: parseColor(o.color),
|
|
@@ -1235,15 +1330,110 @@ function resolveMaterial(modelJson) {
|
|
|
1235
1330
|
cropoffset: modelJson.cropoffset ? parseVec2(modelJson.cropoffset) : null
|
|
1236
1331
|
};
|
|
1237
1332
|
}
|
|
1333
|
+
function recomposeWorld(layers, dirty) {
|
|
1334
|
+
if (!layers || layers.length === 0) return;
|
|
1335
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1336
|
+
for (const l of layers) {
|
|
1337
|
+
if (l && l.id !== void 0 && l.id !== null) byId.set(l.id, l);
|
|
1338
|
+
}
|
|
1339
|
+
const depthOf = (l) => {
|
|
1340
|
+
let d = 0;
|
|
1341
|
+
let p = l.parentId;
|
|
1342
|
+
for (let guard = 0; p !== void 0 && p !== null && guard < 64; guard++) {
|
|
1343
|
+
const parent = byId.get(p);
|
|
1344
|
+
if (!parent) break;
|
|
1345
|
+
d++;
|
|
1346
|
+
p = parent.parentId;
|
|
1347
|
+
}
|
|
1348
|
+
return d;
|
|
1349
|
+
};
|
|
1350
|
+
const targets = [];
|
|
1351
|
+
for (const l of layers) {
|
|
1352
|
+
if (!l || !l.localOrigin) continue;
|
|
1353
|
+
if (l.isPostProcess) continue;
|
|
1354
|
+
if (dirty && !dirty.has(l.id)) continue;
|
|
1355
|
+
targets.push(l);
|
|
1356
|
+
}
|
|
1357
|
+
targets.sort((a, b) => depthOf(a) - depthOf(b));
|
|
1358
|
+
for (const l of targets) {
|
|
1359
|
+
const parent = l.parentId !== void 0 && l.parentId !== null ? byId.get(l.parentId) : null;
|
|
1360
|
+
let w;
|
|
1361
|
+
if (!parent) {
|
|
1362
|
+
w = { origin: l.localOrigin.slice(), scale: l.localScale.slice(), angles: l.localAngles.slice() };
|
|
1363
|
+
} else {
|
|
1364
|
+
const propagateScale = !(parent.scaleRuntimeBound && parent.renderInert);
|
|
1365
|
+
w = composeChildTransform(
|
|
1366
|
+
{ origin: parent.origin, scale: parent.scale, angles: parent.angles },
|
|
1367
|
+
{ origin: l.localOrigin, scale: l.localScale, angles: l.localAngles },
|
|
1368
|
+
propagateScale
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
const d = l.attachBindDelta;
|
|
1372
|
+
if (d) {
|
|
1373
|
+
w.origin[0] += d[0];
|
|
1374
|
+
w.origin[1] += d[1];
|
|
1375
|
+
}
|
|
1376
|
+
l.origin[0] = w.origin[0];
|
|
1377
|
+
l.origin[1] = w.origin[1];
|
|
1378
|
+
l.origin[2] = w.origin[2];
|
|
1379
|
+
l.scale[0] = w.scale[0];
|
|
1380
|
+
l.scale[1] = w.scale[1];
|
|
1381
|
+
l.scale[2] = w.scale[2];
|
|
1382
|
+
l.angles[0] = w.angles[0];
|
|
1383
|
+
l.angles[1] = w.angles[1];
|
|
1384
|
+
l.angles[2] = w.angles[2];
|
|
1385
|
+
if (l.attachBase) {
|
|
1386
|
+
l.attachBase[0] = w.origin[0];
|
|
1387
|
+
l.attachBase[1] = w.origin[1];
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function collectTransformDirty(layers, extraSeeds) {
|
|
1392
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
1393
|
+
if (!layers || layers.length === 0) return dirty;
|
|
1394
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1395
|
+
for (const l of layers) {
|
|
1396
|
+
if (!l || l.parentId === void 0 || l.parentId === null) continue;
|
|
1397
|
+
const list = childrenOf.get(l.parentId);
|
|
1398
|
+
if (list) list.push(l);
|
|
1399
|
+
else childrenOf.set(l.parentId, [l]);
|
|
1400
|
+
}
|
|
1401
|
+
const TRANSFORM_FIELDS = ["origin", "scale", "angles"];
|
|
1402
|
+
const seeds = [];
|
|
1403
|
+
for (const l of layers) {
|
|
1404
|
+
if (!l || l.id === void 0 || l.id === null) continue;
|
|
1405
|
+
const scripts = l.objectScripts || null;
|
|
1406
|
+
const anims = l.objectAnimations || null;
|
|
1407
|
+
const bound = TRANSFORM_FIELDS.some((f) => scripts && scripts[f] || anims && anims[f]);
|
|
1408
|
+
if (bound) seeds.push(l);
|
|
1409
|
+
}
|
|
1410
|
+
if (extraSeeds) {
|
|
1411
|
+
for (const l of extraSeeds) if (l && l.id !== void 0 && l.id !== null) seeds.push(l);
|
|
1412
|
+
}
|
|
1413
|
+
const stack = seeds.slice();
|
|
1414
|
+
for (let guard = 0; stack.length > 0 && guard < 1e5; guard++) {
|
|
1415
|
+
const l = stack.pop();
|
|
1416
|
+
if (!l || l.id === void 0 || l.id === null) continue;
|
|
1417
|
+
if (dirty.has(l.id)) continue;
|
|
1418
|
+
dirty.add(l.id);
|
|
1419
|
+
const kids = childrenOf.get(l.id);
|
|
1420
|
+
if (kids) for (const c of kids) stack.push(c);
|
|
1421
|
+
}
|
|
1422
|
+
return dirty;
|
|
1423
|
+
}
|
|
1238
1424
|
const sceneMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1239
1425
|
__proto__: null,
|
|
1240
1426
|
applySolidFromModel,
|
|
1427
|
+
collectTransformDirty,
|
|
1428
|
+
composeChildTransform,
|
|
1429
|
+
isRenderInert,
|
|
1241
1430
|
parseBool,
|
|
1242
1431
|
parseColor,
|
|
1243
1432
|
parseNum,
|
|
1244
1433
|
parseScene,
|
|
1245
1434
|
parseVec2,
|
|
1246
1435
|
parseVec3,
|
|
1436
|
+
recomposeWorld,
|
|
1247
1437
|
recomputeLayerVisibility,
|
|
1248
1438
|
resolveMaterial
|
|
1249
1439
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
@@ -1647,12 +1837,36 @@ function expandMacrosIn(text, depth) {
|
|
|
1647
1837
|
const { defs, fns } = collectMacros(text);
|
|
1648
1838
|
if (defs.size === 0 && fns.size === 0) break;
|
|
1649
1839
|
const lines = text.split("\n");
|
|
1840
|
+
const defLine = /* @__PURE__ */ new Map();
|
|
1841
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1842
|
+
const dm = /^[ \t]*#define[ \t]+([A-Za-z_][A-Za-z0-9_]*)/.exec(lines[i]);
|
|
1843
|
+
if (dm && !defLine.has(dm[1])) defLine.set(dm[1], i);
|
|
1844
|
+
}
|
|
1845
|
+
const declLine = /* @__PURE__ */ new Map();
|
|
1846
|
+
{
|
|
1847
|
+
const TYPES = "(?:float|int|bool|vec[234]|ivec[234]|bvec[234]|mat[234])";
|
|
1848
|
+
for (const name of defs.keys()) {
|
|
1849
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1850
|
+
const re = new RegExp(
|
|
1851
|
+
"^\\s*(?:const\\s+|uniform\\s+|varying\\s+|in\\s+|out\\s+|attribute\\s+)*" + TYPES + "\\s+" + esc + "\\s*[=;]"
|
|
1852
|
+
);
|
|
1853
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1854
|
+
if (re.test(lines[i])) {
|
|
1855
|
+
declLine.set(name, i);
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1650
1861
|
let changed = false;
|
|
1651
1862
|
for (let i = 0; i < lines.length; i++) {
|
|
1652
1863
|
const line = lines[i];
|
|
1653
1864
|
if (/^[ \t]*#/.test(line)) continue;
|
|
1654
1865
|
let l = line;
|
|
1655
1866
|
for (const [name, val] of defs) {
|
|
1867
|
+
const dl = defLine.get(name);
|
|
1868
|
+
if (dl !== void 0 && i < dl) continue;
|
|
1869
|
+
if (declLine.get(name) === i) continue;
|
|
1656
1870
|
const re = new RegExp("\\b" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
|
|
1657
1871
|
if (re.test(l)) {
|
|
1658
1872
|
l = replaceWord(l, name, val);
|
|
@@ -1660,6 +1874,8 @@ function expandMacrosIn(text, depth) {
|
|
|
1660
1874
|
}
|
|
1661
1875
|
}
|
|
1662
1876
|
for (const [name, info] of fns) {
|
|
1877
|
+
const dl = defLine.get(name);
|
|
1878
|
+
if (dl !== void 0 && i < dl) continue;
|
|
1663
1879
|
if (l.includes(name)) {
|
|
1664
1880
|
l = expandFunctionMacro(l, name, info, depth);
|
|
1665
1881
|
changed = true;
|
|
@@ -1934,6 +2150,13 @@ function isDeclaration(text, idx) {
|
|
|
1934
2150
|
const word = text.slice(p + 1, e);
|
|
1935
2151
|
return GLSL_TYPES.has(word);
|
|
1936
2152
|
}
|
|
2153
|
+
function collectIntNames(code) {
|
|
2154
|
+
const names = /* @__PURE__ */ new Set();
|
|
2155
|
+
let m;
|
|
2156
|
+
const declRe = /\b(?:const\s+)?int\s+([A-Za-z_]\w*)\s*[=;)\u0003]/g;
|
|
2157
|
+
while ((m = declRe.exec(code)) !== null) names.add(m[1]);
|
|
2158
|
+
return names;
|
|
2159
|
+
}
|
|
1937
2160
|
function rewriteCall(text, callName, fn) {
|
|
1938
2161
|
let out = "";
|
|
1939
2162
|
let i = 0;
|
|
@@ -2018,23 +2241,73 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2018
2241
|
const dim = sw.length;
|
|
2019
2242
|
return fn + "(vec" + dim + "(" + num2 + "), " + expr + ")";
|
|
2020
2243
|
});
|
|
2021
|
-
code = code.replace(
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
code = code.replace(
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2244
|
+
code = code.replace(
|
|
2245
|
+
/(\.([xyzwrgba]{2,4})\s*=\s*)(max|min)\(\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*,\s*([^;]+?)\s*\)\s*;/g,
|
|
2246
|
+
(all, lead, sw, fn, scalar, vecExpr) => {
|
|
2247
|
+
if (!/\.[xyzwrgba]{2,4}\b|\bvec[234]\s*\(/.test(vecExpr)) return all;
|
|
2248
|
+
return `${lead}${fn}(vec${sw.length}(${scalar}), ${vecExpr});`;
|
|
2249
|
+
}
|
|
2250
|
+
);
|
|
2251
|
+
code = code.replace(
|
|
2252
|
+
/(^|[^\w)\]])([+-])([+-])(?=[\d.])/g,
|
|
2253
|
+
(all, pre, s1, s2) => pre + (s1 === s2 ? "+" : "-")
|
|
2254
|
+
);
|
|
2255
|
+
const sciHoles = [];
|
|
2256
|
+
code = code.replace(/\b\d+(?:\.\d+)?[eE][+-]?\d+\b/g, (m) => {
|
|
2257
|
+
sciHoles.push(m);
|
|
2258
|
+
return "" + "".repeat(sciHoles.length) + "";
|
|
2259
|
+
});
|
|
2260
|
+
const forHoles = [];
|
|
2261
|
+
code = code.replace(/\bfor\s*\(\s*int\s+([A-Za-z_]\w*)([^)]*)\)/g, (m, name, rest) => {
|
|
2262
|
+
forHoles.push(rest + ")");
|
|
2263
|
+
return `for (int ${name}${"".repeat(forHoles.length)}`;
|
|
2264
|
+
});
|
|
2265
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
2266
|
+
const before = code;
|
|
2267
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
|
|
2268
|
+
code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2269
|
+
code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2270
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
|
|
2271
|
+
code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2272
|
+
code = code.replace(/([A-Za-z_]\w*\.(?:xyzw|xyz|xy|zw|rgba|rgb|rg|x|y|z|w|r|g|b|a))\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2273
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
|
|
2274
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*([A-Za-z_]\w*\.(?:xyzw|xyz|xy|zw|rgba|rgb|rg|x|y|z|w|r|g|b|a))/g, "$1$2.0 $3 $4");
|
|
2275
|
+
code = code.replace(
|
|
2276
|
+
/(^|[^\w.])(\d+)\s*([*/+-])\s*(\()/g,
|
|
2277
|
+
(all, pre, num2, op, open, offset, whole) => {
|
|
2278
|
+
let depth = 0;
|
|
2279
|
+
let end = offset + all.length - 1;
|
|
2280
|
+
for (; end < whole.length; end++) {
|
|
2281
|
+
const ch = whole[end];
|
|
2282
|
+
if (ch === "(") depth++;
|
|
2283
|
+
else if (ch === ")") {
|
|
2284
|
+
depth--;
|
|
2285
|
+
if (depth === 0) {
|
|
2286
|
+
end++;
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
} else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
|
|
2290
|
+
}
|
|
2291
|
+
for (; end < whole.length; end++) {
|
|
2292
|
+
const ch = whole[end];
|
|
2293
|
+
if (ch === ";" || ch === "," || ch === "\n" || ch === ")") break;
|
|
2294
|
+
}
|
|
2295
|
+
const seg = whole.slice(offset, end);
|
|
2296
|
+
return /\d\.\d/.test(seg) ? `${pre}${num2}.0 ${op} ${open}` : all;
|
|
2297
|
+
}
|
|
2298
|
+
);
|
|
2299
|
+
{
|
|
2300
|
+
const floatNames = /* @__PURE__ */ new Set();
|
|
2301
|
+
const declRe = /\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?(?:float|vec2|vec3|vec4|mat2|mat3|mat4)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
2302
|
+
let dm;
|
|
2303
|
+
while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
|
|
2304
|
+
if (floatNames.size > 0) {
|
|
2305
|
+
const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
|
|
2306
|
+
code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
|
|
2307
|
+
code = code.replace(new RegExp("\\b(" + alt + ")\\s*([+-])\\s*(\\d+)(?![\\d.])", "g"), "$1 $2 $3.0");
|
|
2308
|
+
}
|
|
2037
2309
|
}
|
|
2310
|
+
if (code === before) break;
|
|
2038
2311
|
}
|
|
2039
2312
|
{
|
|
2040
2313
|
const FLOAT_BUILTINS = [
|
|
@@ -2170,6 +2443,67 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2170
2443
|
return pre + lhs + " = " + rhs + "." + SW[lw] + ";";
|
|
2171
2444
|
});
|
|
2172
2445
|
}
|
|
2446
|
+
{
|
|
2447
|
+
const floatDecl = /* @__PURE__ */ new Set();
|
|
2448
|
+
{
|
|
2449
|
+
const fdre = /\b(?:uniform|varying|attribute|in|out|const)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
|
|
2450
|
+
let fd;
|
|
2451
|
+
while ((fd = fdre.exec(code)) !== null) floatDecl.add(fd[1]);
|
|
2452
|
+
}
|
|
2453
|
+
const vecW = (expr) => {
|
|
2454
|
+
const e = expr.trim();
|
|
2455
|
+
{
|
|
2456
|
+
const c = /^vec([234])\s*\(/.exec(e);
|
|
2457
|
+
if (c) {
|
|
2458
|
+
let depth = 0;
|
|
2459
|
+
for (let i = e.indexOf("("); i < e.length; i++) {
|
|
2460
|
+
if (e[i] === "(") depth++;
|
|
2461
|
+
else if (e[i] === ")") {
|
|
2462
|
+
depth--;
|
|
2463
|
+
if (depth === 0) return i === e.length - 1 ? Number(c[1]) : 0;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return 0;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (/^texture(?:Lod)?\s*\(/.test(e)) {
|
|
2470
|
+
let depth = 0;
|
|
2471
|
+
for (let i = e.indexOf("("); i < e.length; i++) {
|
|
2472
|
+
if (e[i] === "(") depth++;
|
|
2473
|
+
else if (e[i] === ")") {
|
|
2474
|
+
depth--;
|
|
2475
|
+
if (depth === 0) return i === e.length - 1 ? 4 : 0;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return 0;
|
|
2479
|
+
}
|
|
2480
|
+
const m = /^([A-Za-z_]\w*)(?:\.([xyzwrgba]{2,4}))?\s*[*/]\s*([^*/]+)$/.exec(e);
|
|
2481
|
+
if (!m) return 0;
|
|
2482
|
+
const rhsPart = m[3];
|
|
2483
|
+
if (/\bvec[234]\s*\(|\.[xyzwrgba]{2,4}\b/.test(rhsPart)) return 0;
|
|
2484
|
+
if (m[2]) return m[2].length;
|
|
2485
|
+
if (floatDecl.has(m[1])) return 0;
|
|
2486
|
+
return width.get(m[1]) || 0;
|
|
2487
|
+
};
|
|
2488
|
+
code = code.replace(
|
|
2489
|
+
/(^|[;{}\n]\s*)float\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
|
|
2490
|
+
(all, pre, name, rhs) => {
|
|
2491
|
+
const w = vecW(rhs);
|
|
2492
|
+
if (w < 2) return all;
|
|
2493
|
+
return `${pre}float ${name} = (${rhs.trim()}).x;`;
|
|
2494
|
+
}
|
|
2495
|
+
);
|
|
2496
|
+
const SWN = { 2: "xy", 3: "xyz" };
|
|
2497
|
+
code = code.replace(
|
|
2498
|
+
/(^|[;{}\n]\s*)vec([23])\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
|
|
2499
|
+
(all, pre, dim, name, rhs) => {
|
|
2500
|
+
const lw = Number(dim);
|
|
2501
|
+
const rw = vecW(rhs);
|
|
2502
|
+
if (rw <= lw) return all;
|
|
2503
|
+
return `${pre}vec${dim} ${name} = (${rhs.trim()}).${SWN[lw]};`;
|
|
2504
|
+
}
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2173
2507
|
if (width.size > 0) {
|
|
2174
2508
|
const floatNames = /* @__PURE__ */ new Set();
|
|
2175
2509
|
const fre = /\b(?:uniform|varying|attribute|in|out)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
|
|
@@ -2211,6 +2545,7 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2211
2545
|
code = code.replace(/(^|[;{}\n]\s*)([A-Za-z_]\w*)\s*=\s*([^;]+);/g, (all, pre, lhs, rhs) => {
|
|
2212
2546
|
const lw = width.get(lhs);
|
|
2213
2547
|
if (!lw) return all;
|
|
2548
|
+
if (floatNames.has(lhs)) return all;
|
|
2214
2549
|
const r = rhs.trim();
|
|
2215
2550
|
if (new RegExp("^vec" + lw + "\\s*\\(").test(r)) return all;
|
|
2216
2551
|
if (width.has(r)) return all;
|
|
@@ -2254,6 +2589,52 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2254
2589
|
new RegExp(`\\bint\\s+([A-Za-z_]\\w*)\\s*=\\s*((?:${FLOAT_FNS})\\s*\\()`, "g"),
|
|
2255
2590
|
"float $1 = $2"
|
|
2256
2591
|
);
|
|
2592
|
+
code = code.replace(
|
|
2593
|
+
/\bfloat\s+([A-Za-z_]\w*)\s*=\s*(int\s*\([^;]*\))\s*;/g,
|
|
2594
|
+
"float $1 = float($2);"
|
|
2595
|
+
);
|
|
2596
|
+
{
|
|
2597
|
+
const intNames = collectIntNames(code);
|
|
2598
|
+
if (intNames.size > 0) {
|
|
2599
|
+
code = code.replace(
|
|
2600
|
+
/\b(const\s+)?float\s+([A-Za-z_]\w*)\s*=\s*([^;{}]+);/g,
|
|
2601
|
+
(all, cst, name, rhs) => {
|
|
2602
|
+
const body = rhs.trim();
|
|
2603
|
+
if (/\./.test(body)) return all;
|
|
2604
|
+
if (/[A-Za-z_]\w*\s*\(/.test(body)) return all;
|
|
2605
|
+
const ids = body.match(/[A-Za-z_]\w*/g);
|
|
2606
|
+
if (!ids || !ids.length) return all;
|
|
2607
|
+
if (!ids.every((x) => intNames.has(x))) return all;
|
|
2608
|
+
return `${cst || ""}float ${name} = float(${body});`;
|
|
2609
|
+
}
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
{
|
|
2614
|
+
const intNames = collectIntNames(code);
|
|
2615
|
+
const floatNames = /* @__PURE__ */ new Set();
|
|
2616
|
+
let fm;
|
|
2617
|
+
const fDeclRe = /\b(?:const\s+|uniform\s+|varying\s+|in\s+|out\s+)*float\s+([A-Za-z_]\w*)/g;
|
|
2618
|
+
while ((fm = fDeclRe.exec(code)) !== null) floatNames.add(fm[1]);
|
|
2619
|
+
for (const n of [...intNames]) {
|
|
2620
|
+
if (floatNames.has(n)) {
|
|
2621
|
+
intNames.delete(n);
|
|
2622
|
+
floatNames.delete(n);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (intNames.size > 0 && floatNames.size > 0) {
|
|
2626
|
+
const iAlt = [...intNames].sort((a, b) => b.length - a.length).join("|");
|
|
2627
|
+
const fAlt = [...floatNames].sort((a, b) => b.length - a.length).join("|");
|
|
2628
|
+
code = code.replace(
|
|
2629
|
+
new RegExp(`\\b(${iAlt})\\s*([*/+-])\\s*(${fAlt})\\b`, "g"),
|
|
2630
|
+
(all, a, op, b) => `float(${a}) ${op} ${b}`
|
|
2631
|
+
);
|
|
2632
|
+
code = code.replace(
|
|
2633
|
+
new RegExp(`\\b(${fAlt})\\s*([*/+-])\\s*(${iAlt})\\b`, "g"),
|
|
2634
|
+
(all, a, op, b) => `${a} ${op} float(${b})`
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2257
2638
|
{
|
|
2258
2639
|
const boolNames = /* @__PURE__ */ new Set();
|
|
2259
2640
|
const boolRe = /\bbool\s+([A-Za-z_]\w*)\s*=/g;
|
|
@@ -2267,6 +2648,17 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2267
2648
|
);
|
|
2268
2649
|
}
|
|
2269
2650
|
}
|
|
2651
|
+
{
|
|
2652
|
+
const CMP = /\(\s*([^()&|]+?)\s*(<=|>=|<|>|==|!=)\s*([^()&|]+?)\s*\)/g;
|
|
2653
|
+
code = code.replace(
|
|
2654
|
+
new RegExp(CMP.source + "\\s*([*/])", "g"),
|
|
2655
|
+
(all, lhs, op, rhs, mulOp) => `float(${lhs.trim()} ${op} ${rhs.trim()}) ${mulOp}`
|
|
2656
|
+
);
|
|
2657
|
+
code = code.replace(
|
|
2658
|
+
new RegExp("([-+*/]=\\s*)" + CMP.source, "g"),
|
|
2659
|
+
(all, assign, lhs, op, rhs) => `${assign}float(${lhs.trim()} ${op} ${rhs.trim()})`
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2270
2662
|
{
|
|
2271
2663
|
const names = /* @__PURE__ */ new Set();
|
|
2272
2664
|
for (const fm of code.matchAll(/\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?float\s+([A-Za-z_]\w*)\s*\[/g)) {
|
|
@@ -2285,6 +2677,17 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2285
2677
|
let p = close1 + 1;
|
|
2286
2678
|
while (p < code.length && /[ \t]/.test(code[p])) p++;
|
|
2287
2679
|
if (code[p] !== "[") {
|
|
2680
|
+
const e12 = code.slice(open1 + 1, close1);
|
|
2681
|
+
const t = e12.trim();
|
|
2682
|
+
const isFloatish = /\d\.\d/.test(t) || /^[A-Za-z_]\w*$/.test(t) && new RegExp("\\bfloat\\s+" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b").test(code);
|
|
2683
|
+
const alreadyInt = /^\s*int\s*\(/.test(t) || /^-?\d+$/.test(t);
|
|
2684
|
+
if (isFloatish && !alreadyInt) {
|
|
2685
|
+
out += code.slice(last, fm.index);
|
|
2686
|
+
out += fm[1] + "[int(" + t + ")]";
|
|
2687
|
+
last = close1 + 1;
|
|
2688
|
+
re.lastIndex = last;
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2288
2691
|
re.lastIndex = close1 + 1;
|
|
2289
2692
|
continue;
|
|
2290
2693
|
}
|
|
@@ -2425,6 +2828,20 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2425
2828
|
}).join("\n");
|
|
2426
2829
|
}
|
|
2427
2830
|
}
|
|
2831
|
+
code = code.replace(
|
|
2832
|
+
/^(\s*in\s+(?:highp|mediump|lowp\s+)?)(vec[234]|float)(\s+)([A-Za-z_]\w*)(\s*;)/gm,
|
|
2833
|
+
(all, pre, ty, sp, name, tail) => {
|
|
2834
|
+
const vt = vertTypes.get(name);
|
|
2835
|
+
if (!vt || RANK[vt] >= RANK[ty]) return all;
|
|
2836
|
+
const CH = "xyzw";
|
|
2837
|
+
const RG = "rgba";
|
|
2838
|
+
const over = new RegExp(
|
|
2839
|
+
"\\b" + name + "\\s*\\.\\s*[" + CH + RG + "]*[" + CH.slice(RANK[vt]) + RG.slice(RANK[vt]) + "]"
|
|
2840
|
+
);
|
|
2841
|
+
if (over.test(code)) return all;
|
|
2842
|
+
return pre + vt + sp + name + tail;
|
|
2843
|
+
}
|
|
2844
|
+
);
|
|
2428
2845
|
}
|
|
2429
2846
|
{
|
|
2430
2847
|
const inVecN = /* @__PURE__ */ new Map();
|
|
@@ -2484,18 +2901,27 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2484
2901
|
let body = code.slice(braceIdx + 1);
|
|
2485
2902
|
const decls = [];
|
|
2486
2903
|
for (const name of written) {
|
|
2904
|
+
const shadowed = new RegExp(
|
|
2905
|
+
"(?:^|[;{}\\n])\\s*(?:highp|mediump|lowp\\s+)?(?:vec[234]|float|int|bool)\\s+" + name + "\\s*[=;]"
|
|
2906
|
+
).test(body);
|
|
2907
|
+
if (shadowed) continue;
|
|
2487
2908
|
const tm = new RegExp("^\\s*in\\s+(?:highp|mediump|lowp\\s+)?(vec[234]|float)\\s+" + name + "\\s*;", "m").exec(code);
|
|
2488
2909
|
const ty = tm ? tm[1] : "vec4";
|
|
2489
2910
|
decls.push(" " + ty + " " + name + "_rw = " + name + ";");
|
|
2490
2911
|
body = replaceWord(body, name, name + "_rw");
|
|
2491
2912
|
}
|
|
2492
|
-
|
|
2913
|
+
if (decls.length > 0) {
|
|
2914
|
+
body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
|
|
2915
|
+
}
|
|
2493
2916
|
code = head + body;
|
|
2494
2917
|
}
|
|
2495
2918
|
}
|
|
2496
2919
|
}
|
|
2497
2920
|
code = code.replace(/\[(?:unroll|loop|branch|flatten)\]\s*/g, "");
|
|
2498
2921
|
code = code.replace(/\bstatic\s+/g, "");
|
|
2922
|
+
if (forHoles.length > 0) {
|
|
2923
|
+
code = code.replace(/\u0003(\u0004+)\u0003/g, (m, marks) => forHoles[marks.length - 1]);
|
|
2924
|
+
}
|
|
2499
2925
|
{
|
|
2500
2926
|
const floatNames = /* @__PURE__ */ new Set();
|
|
2501
2927
|
for (const m of code.matchAll(/\b(?:uniform[ \t]+)?(?:highp|mediump|lowp)?[ \t]*float[ \t]+([A-Za-z_]\w*)[ \t]*[;=]/g)) {
|
|
@@ -2525,6 +2951,9 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
|
|
|
2525
2951
|
return line;
|
|
2526
2952
|
}).join("\n");
|
|
2527
2953
|
}
|
|
2954
|
+
if (sciHoles.length > 0) {
|
|
2955
|
+
code = code.replace(/\u0001(\u0002+)\u0001/g, (m, marks) => sciHoles[marks.length - 1]);
|
|
2956
|
+
}
|
|
2528
2957
|
let prologue = "#version 300 es\n";
|
|
2529
2958
|
if (stage === "vert") {
|
|
2530
2959
|
prologue += "precision highp float;\n";
|
|
@@ -3158,6 +3587,11 @@ function rewriteXrayFragScale(fragGlsl) {
|
|
|
3158
3587
|
const from = usesVarying ? /unprojectedUVs\s*\*=\s*v_PointerScale\s*\*/ : /unprojectedUVs\s*\*=\s*g_PointerScale\s*\*/;
|
|
3159
3588
|
return out.replace(from, "unprojectedUVs *= " + XRAY_FRAG_UV_SCALE + " *");
|
|
3160
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
|
+
}
|
|
3161
3595
|
function createRenderer(canvas, opts = {}) {
|
|
3162
3596
|
const gl = canvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false, alpha: false, preserveDrawingBuffer: true });
|
|
3163
3597
|
if (!gl) throw new Error("当前浏览器不支持 WebGL2");
|
|
@@ -3340,8 +3774,9 @@ function createRenderer(canvas, opts = {}) {
|
|
|
3340
3774
|
uni.set(base, { loc: gl.getUniformLocation(prog, info.name), type: GL_TYPES[info.type] || "unknown", size: info.size });
|
|
3341
3775
|
}
|
|
3342
3776
|
const matMeta = { ...parseMaterialMeta(src.vert), ...parseMaterialMeta(src.frag) };
|
|
3777
|
+
const ndcDirect = /gl_Position\s*=\s*vec4\s*\(\s*a_Position/.test(src.vert) && !/[aA]_Position[\s\S]{0,40}mul\s*\(/.test(src.vert);
|
|
3343
3778
|
const samplerDefaults = new Map([...parseSamplerDefaults(src.vert), ...parseSamplerDefaults(src.frag)]);
|
|
3344
|
-
const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl };
|
|
3779
|
+
const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl, ndcDirect };
|
|
3345
3780
|
progCache.set(key, entry);
|
|
3346
3781
|
return entry;
|
|
3347
3782
|
}
|
|
@@ -3513,7 +3948,8 @@ function createRenderer(canvas, opts = {}) {
|
|
|
3513
3948
|
}
|
|
3514
3949
|
for (const [matKey, entry] of Object.entries(matMeta || {})) {
|
|
3515
3950
|
if (!constants || !(matKey in constants)) {
|
|
3516
|
-
|
|
3951
|
+
const dflt = constantFallback(entry.uniform, entry.default);
|
|
3952
|
+
if (dflt !== void 0) setConstant(uni, entry.uniform, dflt);
|
|
3517
3953
|
}
|
|
3518
3954
|
}
|
|
3519
3955
|
}
|
|
@@ -4093,7 +4529,13 @@ function createRenderer(canvas, opts = {}) {
|
|
|
4093
4529
|
}
|
|
4094
4530
|
const drawLayers = cam.perspective ? scene.layers.slice().sort((a, b) => Number(!!b.isSkybox) - Number(!!a.isSkybox)) : scene.layers;
|
|
4095
4531
|
for (const layer of drawLayers) {
|
|
4096
|
-
if (
|
|
4532
|
+
if (layer.destroyed) continue;
|
|
4533
|
+
if (!layer.visible) {
|
|
4534
|
+
if (pendingEmptyCompose.has(layer.id)) {
|
|
4535
|
+
captureEmptyComposeAtZOrder(layer, cam, viewProj, width, height);
|
|
4536
|
+
}
|
|
4537
|
+
continue;
|
|
4538
|
+
}
|
|
4097
4539
|
if (layer.isPostProcess && !(layer.effects || []).some((e) => e.visible)) continue;
|
|
4098
4540
|
if (groupChildIds.has(layer.id)) continue;
|
|
4099
4541
|
if (layer.isContainer) {
|
|
@@ -4552,7 +4994,13 @@ function createRenderer(canvas, opts = {}) {
|
|
|
4552
4994
|
gl.bindFramebuffer(gl.FRAMEBUFFER, outFBO.fbo);
|
|
4553
4995
|
gl.viewport(0, 0, outFBO.width, outFBO.height);
|
|
4554
4996
|
gl.bindVertexArray(vao);
|
|
4555
|
-
|
|
4997
|
+
const usePixelQuad = !progEntry.ndcDirect;
|
|
4998
|
+
if (usePixelQuad) {
|
|
4999
|
+
uploadQuad("passPx" + outFBO.width + "x" + outFBO.height, layerQuad(outFBO.width, outFBO.height));
|
|
5000
|
+
} else {
|
|
5001
|
+
uploadQuad("pass", PASS_QUAD);
|
|
5002
|
+
}
|
|
5003
|
+
const passMVP = usePixelQuad ? mat4Transpose(mat4Ortho(0, outFBO.width, 0, outFBO.height, -1e4, 1e4)) : IDENT_M4;
|
|
4556
5004
|
const texNames = mp.textures || [];
|
|
4557
5005
|
const maxTex = Math.max(texNames.length, 8);
|
|
4558
5006
|
const resolutions = /* @__PURE__ */ new Map();
|
|
@@ -4581,7 +5029,7 @@ function createRenderer(canvas, opts = {}) {
|
|
|
4581
5029
|
usedUnits.add(ti);
|
|
4582
5030
|
resolutions.set(ti, [t.width, t.height, t.width, t.height]);
|
|
4583
5031
|
}
|
|
4584
|
-
bindSystemUniforms(uni, layer, time, cam.projW, cam.projH,
|
|
5032
|
+
bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, passMVP, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
|
|
4585
5033
|
bindConstants(
|
|
4586
5034
|
uni,
|
|
4587
5035
|
animatedConstants(
|
|
@@ -4720,9 +5168,11 @@ const rndMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
|
|
|
4720
5168
|
__proto__: null,
|
|
4721
5169
|
ALIGN,
|
|
4722
5170
|
XRAY_IDLE_SCREEN_UV,
|
|
5171
|
+
XRAY_SIZE_FALLBACK,
|
|
4723
5172
|
applyColorBlendCPU,
|
|
4724
5173
|
collectGroupDescendantIds,
|
|
4725
5174
|
colorBlendPlan,
|
|
5175
|
+
constantFallback,
|
|
4726
5176
|
createRenderer,
|
|
4727
5177
|
effectFboSize,
|
|
4728
5178
|
layerWantsPreserveBackdrop,
|
|
@@ -5088,20 +5538,7 @@ class ParticleSystem {
|
|
|
5088
5538
|
this.model = model || {};
|
|
5089
5539
|
this.override = override || {};
|
|
5090
5540
|
this.layer = layer || null;
|
|
5091
|
-
|
|
5092
|
-
const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
|
|
5093
|
-
const la = layer && layer.angles ? layer.angles : [0, 0, 0];
|
|
5094
|
-
this.originX = lo[0] || 0;
|
|
5095
|
-
this.originY = lo[1] || 0;
|
|
5096
|
-
this.originZ = lo[2] || 0;
|
|
5097
|
-
this.scaleX = ls[0] === 0 ? 1 : ls[0];
|
|
5098
|
-
this.scaleY = ls[1] === 0 ? 1 : ls[1];
|
|
5099
|
-
this.angleZ = (la[2] || 0) * Math.PI / 180;
|
|
5100
|
-
const asx = Math.abs(this.scaleX);
|
|
5101
|
-
const asy = Math.abs(this.scaleY);
|
|
5102
|
-
this.sysScale = Math.min(asx, asy) || 1;
|
|
5103
|
-
this.spriteStretchX = asx / this.sysScale;
|
|
5104
|
-
this.spriteStretchY = asy / this.sysScale;
|
|
5541
|
+
this.syncLayerTransform();
|
|
5105
5542
|
this.maxCount = Math.max(1, Math.min(2e4, num(this.model.maxcount, 100)));
|
|
5106
5543
|
this.simTime = 0;
|
|
5107
5544
|
this.paused = false;
|
|
@@ -5506,6 +5943,31 @@ class ParticleSystem {
|
|
|
5506
5943
|
setVisible(v) {
|
|
5507
5944
|
this.visible = v;
|
|
5508
5945
|
}
|
|
5946
|
+
/**
|
|
5947
|
+
* [we-scene patch] 从图层重新读取变换(构造时也走这里)。
|
|
5948
|
+
*
|
|
5949
|
+
* 发射器变换原先只在构造时缓存一次、之后**从不刷新**。父组一旦带脚本/动画
|
|
5950
|
+
* 变换(全库 17 个粒子层有脚本化祖先),图层被 recomposeWorld 挪走了,
|
|
5951
|
+
* 粒子却仍从旧位置喷出来 —— 画面上是「人物滑走了、他的火焰留在原地」。
|
|
5952
|
+
* 宿主在 recompose 之后对脏子树里的粒子层调用本方法。
|
|
5953
|
+
*/
|
|
5954
|
+
syncLayerTransform() {
|
|
5955
|
+
const layer = this.layer;
|
|
5956
|
+
const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
|
|
5957
|
+
const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
|
|
5958
|
+
const la = layer && layer.angles ? layer.angles : [0, 0, 0];
|
|
5959
|
+
this.originX = lo[0] || 0;
|
|
5960
|
+
this.originY = lo[1] || 0;
|
|
5961
|
+
this.originZ = lo[2] || 0;
|
|
5962
|
+
this.scaleX = ls[0] === 0 ? 1 : ls[0];
|
|
5963
|
+
this.scaleY = ls[1] === 0 ? 1 : ls[1];
|
|
5964
|
+
this.angleZ = (la[2] || 0) * Math.PI / 180;
|
|
5965
|
+
const asx = Math.abs(this.scaleX);
|
|
5966
|
+
const asy = Math.abs(this.scaleY);
|
|
5967
|
+
this.sysScale = Math.min(asx, asy) || 1;
|
|
5968
|
+
this.spriteStretchX = asx / this.sysScale;
|
|
5969
|
+
this.spriteStretchY = asy / this.sysScale;
|
|
5970
|
+
}
|
|
5509
5971
|
// 宿主每帧提供鼠标位置(世界像素);转到局部空间供控制点使用
|
|
5510
5972
|
setPointer(worldX, worldY) {
|
|
5511
5973
|
const dx = worldX - this.originX;
|
|
@@ -7834,6 +8296,7 @@ function applyAttachmentBindOrigins(layers) {
|
|
|
7834
8296
|
for (const c of desc) {
|
|
7835
8297
|
c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
|
|
7836
8298
|
}
|
|
8299
|
+
layer.attachBindDelta = [d[0], d[1]];
|
|
7837
8300
|
follows.push({
|
|
7838
8301
|
layer,
|
|
7839
8302
|
parent,
|
|
@@ -7848,6 +8311,9 @@ function applyAttachmentBindOrigins(layers) {
|
|
|
7848
8311
|
f.baseY = f.layer.origin[1];
|
|
7849
8312
|
f.subtree = [{ layer: f.layer, x: f.layer.origin[0], y: f.layer.origin[1] }];
|
|
7850
8313
|
for (const c of f.desc) f.subtree.push({ layer: c, x: c.origin[0], y: c.origin[1] });
|
|
8314
|
+
for (const s of f.subtree) {
|
|
8315
|
+
if (!s.layer.attachBase) s.layer.attachBase = [s.x, s.y];
|
|
8316
|
+
}
|
|
7851
8317
|
}
|
|
7852
8318
|
return follows;
|
|
7853
8319
|
}
|
|
@@ -7873,14 +8339,20 @@ function followAttachments(follows, time, getBoneOverrides) {
|
|
|
7873
8339
|
}
|
|
7874
8340
|
deltas.push(parentMeshToWorldDelta(f.parent, cur[12] - f.bindX, cur[13] - f.bindY));
|
|
7875
8341
|
}
|
|
8342
|
+
const baseOf = (s) => {
|
|
8343
|
+
const ab = s.layer.attachBase;
|
|
8344
|
+
if (ab) return ab;
|
|
8345
|
+
return [s.x, s.y];
|
|
8346
|
+
};
|
|
7876
8347
|
const seen = /* @__PURE__ */ new Set();
|
|
7877
8348
|
for (const f of follows) {
|
|
7878
8349
|
const tree = f.subtree || [{ layer: f.layer, x: f.baseX, y: f.baseY }];
|
|
7879
8350
|
for (const s of tree) {
|
|
7880
8351
|
if (seen.has(s.layer)) continue;
|
|
7881
8352
|
seen.add(s.layer);
|
|
7882
|
-
|
|
7883
|
-
s.layer.origin[
|
|
8353
|
+
const b = baseOf(s);
|
|
8354
|
+
s.layer.origin[0] = b[0];
|
|
8355
|
+
s.layer.origin[1] = b[1];
|
|
7884
8356
|
}
|
|
7885
8357
|
}
|
|
7886
8358
|
for (let i = 0; i < follows.length; i++) {
|
|
@@ -8767,6 +9239,19 @@ function evalTextScript(script, scriptprops, opts = {}) {
|
|
|
8767
9239
|
if (opts.onError) opts.onError(e, "applyUserProperties");
|
|
8768
9240
|
}
|
|
8769
9241
|
},
|
|
9242
|
+
/** 直调 update 不做文本加工:层可见性脚本(visible.script)要拿原始返回值
|
|
9243
|
+
* ——布尔控可见,callUpdate 会把它吞成 null(防画到画面上的文字版语义)。 */
|
|
9244
|
+
callUpdateRaw(value) {
|
|
9245
|
+
if (!fns.update || sandbox.disabled) return void 0;
|
|
9246
|
+
try {
|
|
9247
|
+
return fns.update(value);
|
|
9248
|
+
} catch (e) {
|
|
9249
|
+
sandbox.errCount++;
|
|
9250
|
+
if (opts.onError) opts.onError(e, "update");
|
|
9251
|
+
if (sandbox.errCount >= 3) sandbox.disabled = true;
|
|
9252
|
+
return void 0;
|
|
9253
|
+
}
|
|
9254
|
+
},
|
|
8770
9255
|
/** 求值当前文本:返回新文本;undefined/null 保留原值;连续出错 3 次熔断回退静态文本 */
|
|
8771
9256
|
callUpdate(value) {
|
|
8772
9257
|
if (!fns.update || sandbox.disabled) return null;
|
|
@@ -9458,13 +9943,8 @@ function makeObjectLayerProxy(layer, opts) {
|
|
|
9458
9943
|
Object.defineProperty(proxy, key, {
|
|
9459
9944
|
enumerable: true,
|
|
9460
9945
|
get() {
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
store[key].x = a[0] || 0;
|
|
9464
|
-
store[key].y = a[1] || 0;
|
|
9465
|
-
store[key].z = a[2] || 0;
|
|
9466
|
-
}
|
|
9467
|
-
return store[key];
|
|
9946
|
+
const a = layer && Array.isArray(layer[key]) ? layer[key] : null;
|
|
9947
|
+
return makeVec3(a || [0, 0, 0]);
|
|
9468
9948
|
},
|
|
9469
9949
|
set(v) {
|
|
9470
9950
|
const a = normVec(v);
|
|
@@ -9660,7 +10140,8 @@ function evalObjectScript(script, scriptprops, opts = {}) {
|
|
|
9660
10140
|
const hasCursorHook = !!(fns && (fns.cursorClick || fns.cursorEnter || fns.cursorLeave || fns.cursorDown || fns.cursorUp || fns.cursorMove));
|
|
9661
10141
|
const hasMediaHook = !!(fns && MEDIA_CALLBACKS.some((n) => fns[n]));
|
|
9662
10142
|
const hasApplyHook = !!(fns && typeof fns.applyUserProperties === "function");
|
|
9663
|
-
|
|
10143
|
+
const usesEngineClock = /\bengine\s*\.\s*(runtime|frametime)\b/.test(body);
|
|
10144
|
+
if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook && !usesEngineClock) return null;
|
|
9664
10145
|
const sandbox = {
|
|
9665
10146
|
engine,
|
|
9666
10147
|
scriptProperties: spValues,
|
|
@@ -9961,7 +10442,7 @@ function createEngineTimers(host = {}, opts = {}) {
|
|
|
9961
10442
|
cancel.handle = state.handle;
|
|
9962
10443
|
return cancel;
|
|
9963
10444
|
}
|
|
9964
|
-
function
|
|
10445
|
+
function clearTimeout2(h) {
|
|
9965
10446
|
if (typeof h === "function") {
|
|
9966
10447
|
h();
|
|
9967
10448
|
return;
|
|
@@ -9982,7 +10463,7 @@ function createEngineTimers(host = {}, opts = {}) {
|
|
|
9982
10463
|
return {
|
|
9983
10464
|
setTimeout,
|
|
9984
10465
|
setInterval: setInterval2,
|
|
9985
|
-
clearTimeout,
|
|
10466
|
+
clearTimeout: clearTimeout2,
|
|
9986
10467
|
clearInterval: clearInterval2,
|
|
9987
10468
|
dispose,
|
|
9988
10469
|
/** 测试与诊断用:尚未触发且未取消的定时器数 */
|
|
@@ -10036,6 +10517,8 @@ function createSimulatedAudio(seed = 20260830) {
|
|
|
10036
10517
|
const BANDS2 = 64;
|
|
10037
10518
|
const rawL = new Float32Array(BANDS2);
|
|
10038
10519
|
const rawR = new Float32Array(BANDS2);
|
|
10520
|
+
const preL64 = new Float32Array(BANDS2);
|
|
10521
|
+
const preR64 = new Float32Array(BANDS2);
|
|
10039
10522
|
const left64 = new Float32Array(BANDS2);
|
|
10040
10523
|
const right64 = new Float32Array(BANDS2);
|
|
10041
10524
|
const left32 = new Float32Array(32);
|
|
@@ -10049,6 +10532,15 @@ function createSimulatedAudio(seed = 20260830) {
|
|
|
10049
10532
|
right32,
|
|
10050
10533
|
left16,
|
|
10051
10534
|
right16,
|
|
10535
|
+
/**
|
|
10536
|
+
* 未钳位(pre-GAIN、pre-clamp)的 64 band,含左右声道 pan。
|
|
10537
|
+
* left64/right64 是 `min(1, v*GAIN)` 之后的值:底鼓段基底就已到 ~0.6、峰值贴 1,
|
|
10538
|
+
* 波峰因数被压平——网页作者按「峰值过阈值」判定敲击时(1520828134 猫爪
|
|
10539
|
+
* `audioArray[i] > 0.5`),事后再乘任何标量都无法把基底与峰值分开。
|
|
10540
|
+
* 网页驱动改对本数组做 gamma 对比扩展,音条墙仍走已标定的 left64/right64。
|
|
10541
|
+
*/
|
|
10542
|
+
preL64,
|
|
10543
|
+
preR64,
|
|
10052
10544
|
/** vumeter:整体响度 0..1(粒子 audioprocessing / 文字脚本 average 用) */
|
|
10053
10545
|
level: 0,
|
|
10054
10546
|
/** 渲染器诊断:当前是否处于「静音段」 */
|
|
@@ -10094,6 +10586,7 @@ function createSimulatedAudio(seed = 20260830) {
|
|
|
10094
10586
|
if (fq >= 0.45) v += hatV * 0.5 * ((fq - 0.45) / 0.55);
|
|
10095
10587
|
v += riser * 0.5;
|
|
10096
10588
|
v *= tilt;
|
|
10589
|
+
const vPre = v;
|
|
10097
10590
|
v = Math.min(1, v * GAIN);
|
|
10098
10591
|
const width = 0.06 + fq * 0.2;
|
|
10099
10592
|
const pan = vnoise(beat * 0.13 + i * 0.35, 11) * width;
|
|
@@ -10102,6 +10595,8 @@ function createSimulatedAudio(seed = 20260830) {
|
|
|
10102
10595
|
const floorV = silent ? 0.012 : 0;
|
|
10103
10596
|
rawL[i] = Math.max(floorV, l);
|
|
10104
10597
|
rawR[i] = Math.max(floorV, r);
|
|
10598
|
+
preL64[i] = Math.max(floorV, Math.max(0, vPre * (1 - pan)));
|
|
10599
|
+
preR64[i] = Math.max(floorV, Math.max(0, vPre * (1 + pan)));
|
|
10105
10600
|
if (i < 48) levelSum += (rawL[i] + rawR[i]) * 0.5;
|
|
10106
10601
|
}
|
|
10107
10602
|
left64.set(rawL);
|
|
@@ -10599,15 +11094,7 @@ function createPointerSource(opts = {}) {
|
|
|
10599
11094
|
state.screenH = v.h || 1;
|
|
10600
11095
|
}
|
|
10601
11096
|
readViewport();
|
|
10602
|
-
function
|
|
10603
|
-
readViewport();
|
|
10604
|
-
let x = ev.clientX;
|
|
10605
|
-
let y = ev.clientY;
|
|
10606
|
-
if (target && typeof target.getBoundingClientRect === "function") {
|
|
10607
|
-
const r = target.getBoundingClientRect();
|
|
10608
|
-
x -= r.left;
|
|
10609
|
-
y -= r.top;
|
|
10610
|
-
}
|
|
11097
|
+
function applyMove(x, y) {
|
|
10611
11098
|
const u = x / state.screenW;
|
|
10612
11099
|
const v = y / state.screenH;
|
|
10613
11100
|
if (!state.has) {
|
|
@@ -10624,18 +11111,35 @@ function createPointerSource(opts = {}) {
|
|
|
10624
11111
|
state.moveCount++;
|
|
10625
11112
|
state.lastEventTime = Date.now();
|
|
10626
11113
|
}
|
|
11114
|
+
function applyButtons(mask) {
|
|
11115
|
+
const left = (mask & 1) !== 0;
|
|
11116
|
+
if (left === state.leftDown) return;
|
|
11117
|
+
state.leftDown = left;
|
|
11118
|
+
if (left) state.downCount++;
|
|
11119
|
+
else state.upCount++;
|
|
11120
|
+
state.lastEventTime = Date.now();
|
|
11121
|
+
}
|
|
11122
|
+
function onMove(ev) {
|
|
11123
|
+
readViewport();
|
|
11124
|
+
let x = ev.clientX;
|
|
11125
|
+
let y = ev.clientY;
|
|
11126
|
+
if (target && typeof target.getBoundingClientRect === "function") {
|
|
11127
|
+
const r = target.getBoundingClientRect();
|
|
11128
|
+
x -= r.left;
|
|
11129
|
+
y -= r.top;
|
|
11130
|
+
}
|
|
11131
|
+
applyMove(x, y);
|
|
11132
|
+
}
|
|
10627
11133
|
function onDown(ev) {
|
|
10628
11134
|
if (ev.button !== void 0 && ev.button !== 0) return;
|
|
10629
|
-
|
|
10630
|
-
state.downCount++;
|
|
11135
|
+
applyButtons(1);
|
|
10631
11136
|
}
|
|
10632
11137
|
function onUp(ev) {
|
|
10633
11138
|
if (ev.button !== void 0 && ev.button !== 0) return;
|
|
10634
|
-
|
|
10635
|
-
state.upCount++;
|
|
11139
|
+
applyButtons(0);
|
|
10636
11140
|
}
|
|
10637
11141
|
function onLeaveWindow() {
|
|
10638
|
-
|
|
11142
|
+
applyButtons(0);
|
|
10639
11143
|
}
|
|
10640
11144
|
let attached = false;
|
|
10641
11145
|
if (target && target.addEventListener) {
|
|
@@ -10650,6 +11154,42 @@ function createPointerSource(opts = {}) {
|
|
|
10650
11154
|
}
|
|
10651
11155
|
return {
|
|
10652
11156
|
state,
|
|
11157
|
+
/**
|
|
11158
|
+
* 外部注入指针状态(宿主轮询系统鼠标后推入)。协议见 docs/INTEGRATION.md。
|
|
11159
|
+
*
|
|
11160
|
+
* 接**归一化**坐标而不是像素:宿主知道自己那块屏的 points 尺寸,除法在它那边
|
|
11161
|
+
* 做更准(混合 DPI 多显示器下无需任何 DPR 折算);这里再乘回 screenW/H 得到
|
|
11162
|
+
* input.cursorScreenPosition 要的像素。
|
|
11163
|
+
*
|
|
11164
|
+
* u/v 是 [0,1]、原点左上、**Y 朝下** —— 与 DOM 路径的 state.u/v 同一空间
|
|
11165
|
+
* (见文件头坐标约定)。宿主不要替 shader 翻 Y。
|
|
11166
|
+
*
|
|
11167
|
+
* 不在这里推进 last:外部推送频率(~90Hz)高于帧率,若在推送里推进 last,
|
|
11168
|
+
* `length(g_PointerPosition - g_PointerPositionLast)` 会恒接近 0,
|
|
11169
|
+
* cursorripple 完全不起波且无报错(与 DOM 路径同一个坑,见文件头)。
|
|
11170
|
+
*
|
|
11171
|
+
* @param {{u:number, v:number, buttons?:number}} p 归一化位置 + 按键位掩码(bit0 左)
|
|
11172
|
+
*/
|
|
11173
|
+
pushExternal(p) {
|
|
11174
|
+
if (!p) return;
|
|
11175
|
+
readViewport();
|
|
11176
|
+
const u = Number(p.u);
|
|
11177
|
+
const v = Number(p.v);
|
|
11178
|
+
if (Number.isFinite(u) && Number.isFinite(v)) {
|
|
11179
|
+
applyMove(u * state.screenW, v * state.screenH);
|
|
11180
|
+
}
|
|
11181
|
+
applyButtons(Number(p.buttons) || 0);
|
|
11182
|
+
},
|
|
11183
|
+
/**
|
|
11184
|
+
* 外部指针离开本窗口(鼠标移到了别的显示器)。
|
|
11185
|
+
*
|
|
11186
|
+
* **只清按键,保留位置与 has** —— 清 has 会让 xray 开窗突然跳到相机外
|
|
11187
|
+
* (renderer.js 的 XRAY_IDLE_SCREEN_UV)、视差弹回中心,画面会明显抽一下。
|
|
11188
|
+
* 语义与 DOM 的 onLeaveWindow 一致:位置停在最后已知点,只是不再按着键。
|
|
11189
|
+
*/
|
|
11190
|
+
pushExternalLeave() {
|
|
11191
|
+
applyButtons(0);
|
|
11192
|
+
},
|
|
10653
11193
|
/**
|
|
10654
11194
|
* 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current。
|
|
10655
11195
|
* 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
|
|
@@ -10856,19 +11396,84 @@ async function decodeGifFrames(src) {
|
|
|
10856
11396
|
dec.close?.();
|
|
10857
11397
|
return { width, height, frames };
|
|
10858
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
|
+
}
|
|
10859
11456
|
function mountMedia(rt, cfg) {
|
|
10860
11457
|
clear(rt);
|
|
10861
|
-
|
|
11458
|
+
const failHard = (why) => {
|
|
11459
|
+
reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
|
|
11460
|
+
rt.onError?.(new Error(`媒体壁纸(${cfg.type})${why}`));
|
|
10862
11461
|
rt.fallbackPage?.();
|
|
11462
|
+
};
|
|
11463
|
+
if (!cfg.src) {
|
|
11464
|
+
failHard("缺少资源 URL(cfg.src 为空)");
|
|
10863
11465
|
return;
|
|
10864
11466
|
}
|
|
10865
11467
|
const isVideo = cfg.type === "video";
|
|
10866
11468
|
const isGif = cfg.type === "gif";
|
|
10867
|
-
const
|
|
11469
|
+
const embedded = cfg.canvas instanceof HTMLCanvasElement;
|
|
11470
|
+
const c = embedded ? cfg.canvas : document.createElement("canvas");
|
|
10868
11471
|
const dpr = effectiveDpr(rt, cfg);
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
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%;";
|
|
10872
11477
|
const gl2 = c.getContext("webgl2", {
|
|
10873
11478
|
premultipliedAlpha: false,
|
|
10874
11479
|
antialias: false,
|
|
@@ -10876,13 +11481,32 @@ function mountMedia(rt, cfg) {
|
|
|
10876
11481
|
preserveDrawingBuffer: true
|
|
10877
11482
|
});
|
|
10878
11483
|
if (!gl2) {
|
|
10879
|
-
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
|
+
}
|
|
10880
11489
|
if (isVideo) mountVideoDom(rt, cfg);
|
|
10881
11490
|
else mountGifDom(rt, cfg);
|
|
10882
11491
|
return;
|
|
10883
11492
|
}
|
|
10884
|
-
rt.wrap?.appendChild(c);
|
|
11493
|
+
if (!embedded) rt.wrap?.appendChild(c);
|
|
10885
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
|
+
}
|
|
10886
11510
|
let disposed = false;
|
|
10887
11511
|
rt.sceneCleanup = () => {
|
|
10888
11512
|
disposed = true;
|
|
@@ -10894,6 +11518,21 @@ function mountMedia(rt, cfg) {
|
|
|
10894
11518
|
let pauseImpl;
|
|
10895
11519
|
let resumeImpl;
|
|
10896
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
|
+
};
|
|
10897
11536
|
rt.sceneCtl = {
|
|
10898
11537
|
pause() {
|
|
10899
11538
|
pauseImpl?.();
|
|
@@ -10907,8 +11546,7 @@ function mountMedia(rt, cfg) {
|
|
|
10907
11546
|
const fail = (why) => {
|
|
10908
11547
|
if (disposed) return;
|
|
10909
11548
|
disposed = true;
|
|
10910
|
-
|
|
10911
|
-
rt.fallbackPage?.();
|
|
11549
|
+
failHard(why);
|
|
10912
11550
|
};
|
|
10913
11551
|
void (async () => {
|
|
10914
11552
|
try {
|
|
@@ -10956,6 +11594,7 @@ function mountMedia(rt, cfg) {
|
|
|
10956
11594
|
});
|
|
10957
11595
|
if (!rt.paused) void v.play().catch(() => {
|
|
10958
11596
|
});
|
|
11597
|
+
attachVideoSpectrum(rt, cfg, v);
|
|
10959
11598
|
reportDiag(rt, cfg, `media video ${mediaW}x${mediaH} → scene 渲染`);
|
|
10960
11599
|
} else {
|
|
10961
11600
|
let decoded = false;
|
|
@@ -11055,6 +11694,14 @@ function mountMedia(rt, cfg) {
|
|
|
11055
11694
|
peek.x,
|
|
11056
11695
|
peek.y
|
|
11057
11696
|
).then(() => {
|
|
11697
|
+
if (rt.onFirstFrame) {
|
|
11698
|
+
const first = rt.onFirstFrame;
|
|
11699
|
+
rt.onFirstFrame = void 0;
|
|
11700
|
+
try {
|
|
11701
|
+
first();
|
|
11702
|
+
} catch {
|
|
11703
|
+
}
|
|
11704
|
+
}
|
|
11058
11705
|
if (disposed || rt.paused) return;
|
|
11059
11706
|
rt.raf = requestAnimationFrame(renderLoop);
|
|
11060
11707
|
}).catch((e) => fail(String(e.message || e).slice(0, 200)));
|
|
@@ -11137,6 +11784,48 @@ function mountGifDom(rt, cfg) {
|
|
|
11137
11784
|
rt.img = img;
|
|
11138
11785
|
}
|
|
11139
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
|
+
}
|
|
11140
11829
|
function httpSource(baseUrl, init) {
|
|
11141
11830
|
const base = baseUrl.replace(/\/+$/, "");
|
|
11142
11831
|
return {
|
|
@@ -11176,6 +11865,44 @@ function httpSource(baseUrl, init) {
|
|
|
11176
11865
|
} catch {
|
|
11177
11866
|
return null;
|
|
11178
11867
|
}
|
|
11868
|
+
},
|
|
11869
|
+
async webEntry(signal) {
|
|
11870
|
+
let file = "index.html";
|
|
11871
|
+
try {
|
|
11872
|
+
const r = await fetch(`${base}/project.json`, { ...init, signal });
|
|
11873
|
+
if (r.ok) {
|
|
11874
|
+
const project = await r.json();
|
|
11875
|
+
if (project && typeof project.file === "string" && project.file.trim()) {
|
|
11876
|
+
file = project.file.trim().replace(/^\/+/, "");
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
} catch {
|
|
11880
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
11881
|
+
}
|
|
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;
|
|
11179
11906
|
}
|
|
11180
11907
|
};
|
|
11181
11908
|
}
|
|
@@ -11195,6 +11922,43 @@ function bytesSource(pkg2, project, key) {
|
|
|
11195
11922
|
project: async () => project ?? null
|
|
11196
11923
|
};
|
|
11197
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
|
+
}
|
|
11198
11962
|
const LOOP_PREROLL_SEC = 0.5;
|
|
11199
11963
|
const LOOP_HOLD_SEC = 0.04;
|
|
11200
11964
|
const LOOP_SWAP_EPS = 0.08;
|
|
@@ -12232,7 +12996,48 @@ const SYSTEM_FONT_FAMILIES = {
|
|
|
12232
12996
|
systemfont_simhei: "SimHei, 'Heiti SC', sans-serif"
|
|
12233
12997
|
};
|
|
12234
12998
|
const fontFaceCache = /* @__PURE__ */ new Map();
|
|
12999
|
+
function fontKeyHash(key) {
|
|
13000
|
+
let h = 5381;
|
|
13001
|
+
for (let i = 0; i < key.length; i++) h = (h << 5) + h + key.charCodeAt(i) | 0;
|
|
13002
|
+
return (h >>> 0).toString(36);
|
|
13003
|
+
}
|
|
13004
|
+
function releaseFontFaces(keys) {
|
|
13005
|
+
for (const key of keys) {
|
|
13006
|
+
const entry = fontFaceCache.get(key);
|
|
13007
|
+
if (!entry) continue;
|
|
13008
|
+
entry.refs--;
|
|
13009
|
+
if (entry.refs > 0) continue;
|
|
13010
|
+
fontFaceCache.delete(key);
|
|
13011
|
+
try {
|
|
13012
|
+
const dead = [];
|
|
13013
|
+
document.fonts.forEach((f) => {
|
|
13014
|
+
if (f.family === entry.family) dead.push(f);
|
|
13015
|
+
});
|
|
13016
|
+
for (const f of dead) document.fonts.delete(f);
|
|
13017
|
+
} catch {
|
|
13018
|
+
}
|
|
13019
|
+
}
|
|
13020
|
+
}
|
|
12235
13021
|
const pkgCache = /* @__PURE__ */ new Map();
|
|
13022
|
+
const PKG_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
13023
|
+
let pkgCacheBytes = 0;
|
|
13024
|
+
function pkgCacheEvict(currentKey) {
|
|
13025
|
+
while (pkgCache.size > 0 && (pkgCache.size > 2 || pkgCacheBytes > PKG_CACHE_MAX_BYTES)) {
|
|
13026
|
+
let oldestKey = null;
|
|
13027
|
+
let oldestAt = Infinity;
|
|
13028
|
+
for (const [k, v] of pkgCache) {
|
|
13029
|
+
if (k === currentKey) continue;
|
|
13030
|
+
if (v.at < oldestAt) {
|
|
13031
|
+
oldestAt = v.at;
|
|
13032
|
+
oldestKey = k;
|
|
13033
|
+
}
|
|
13034
|
+
}
|
|
13035
|
+
if (!oldestKey) break;
|
|
13036
|
+
const victim = pkgCache.get(oldestKey);
|
|
13037
|
+
pkgCacheBytes -= victim.parsed.fileSize || 0;
|
|
13038
|
+
pkgCache.delete(oldestKey);
|
|
13039
|
+
}
|
|
13040
|
+
}
|
|
12236
13041
|
async function loadParsedPkg(rt, cfg, source, signal) {
|
|
12237
13042
|
const cacheKey = source.key;
|
|
12238
13043
|
if (cacheKey) {
|
|
@@ -12255,38 +13060,40 @@ async function loadParsedPkg(rt, cfg, source, signal) {
|
|
|
12255
13060
|
const parsed = pkg.parsePkg(bytes);
|
|
12256
13061
|
if (!cacheKey) return parsed;
|
|
12257
13062
|
pkgCache.set(cacheKey, { parsed, at: Date.now() });
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
let oldestAt = Infinity;
|
|
12261
|
-
for (const [k, v] of pkgCache) {
|
|
12262
|
-
if (k === cacheKey) continue;
|
|
12263
|
-
if (v.at < oldestAt) {
|
|
12264
|
-
oldestAt = v.at;
|
|
12265
|
-
oldestKey = k;
|
|
12266
|
-
}
|
|
12267
|
-
}
|
|
12268
|
-
if (oldestKey) pkgCache.delete(oldestKey);
|
|
12269
|
-
}
|
|
13063
|
+
pkgCacheBytes += parsed.fileSize || 0;
|
|
13064
|
+
pkgCacheEvict(cacheKey);
|
|
12270
13065
|
return parsed;
|
|
12271
13066
|
}
|
|
12272
13067
|
function mountScene(rt, cfg) {
|
|
12273
13068
|
clear(rt);
|
|
12274
|
-
const c = cfg.canvas ?? document.createElement("canvas");
|
|
13069
|
+
const c = (cfg.canvas instanceof HTMLCanvasElement ? cfg.canvas : null) ?? document.createElement("canvas");
|
|
12275
13070
|
const dpr = effectiveDpr(rt, cfg);
|
|
12276
13071
|
const vw = c.clientWidth || window.innerWidth || 1;
|
|
12277
13072
|
const vh = c.clientHeight || window.innerHeight || 1;
|
|
12278
13073
|
c.width = Math.max(1, Math.round(vw * dpr));
|
|
12279
13074
|
c.height = Math.max(1, Math.round(vh * dpr));
|
|
12280
|
-
if (!cfg.canvas) {
|
|
13075
|
+
if (!(cfg.canvas instanceof HTMLCanvasElement)) {
|
|
12281
13076
|
c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
|
|
12282
13077
|
rt.wrap?.appendChild(c);
|
|
12283
13078
|
}
|
|
12284
13079
|
rt.canvas = c;
|
|
12285
13080
|
let disposed = false;
|
|
13081
|
+
const origWarn = console.warn.bind(console);
|
|
13082
|
+
console.warn = (...args) => {
|
|
13083
|
+
const s = args.map((a) => typeof a === "string" ? a : String(a?.message ?? a)).join(" ");
|
|
13084
|
+
if (s.includes("[we-scene]")) {
|
|
13085
|
+
try {
|
|
13086
|
+
reportDiag(rt, cfg, s.slice(0, 300));
|
|
13087
|
+
} catch {
|
|
13088
|
+
}
|
|
13089
|
+
}
|
|
13090
|
+
origWarn(...args);
|
|
13091
|
+
};
|
|
12286
13092
|
const pkgAbort = new AbortController();
|
|
12287
13093
|
let particleCleanup;
|
|
12288
13094
|
rt.sceneCleanup = () => {
|
|
12289
13095
|
disposed = true;
|
|
13096
|
+
console.warn = origWarn;
|
|
12290
13097
|
pkgAbort.abort();
|
|
12291
13098
|
rt.sceneTextUpdate = void 0;
|
|
12292
13099
|
if (particleCleanup) {
|
|
@@ -12343,6 +13150,11 @@ function mountScene(rt, cfg) {
|
|
|
12343
13150
|
const sceneEntry = pkg.getEntry(parsedPkg, "scene.json");
|
|
12344
13151
|
if (!sceneEntry) throw new Error("pkg 中没有 scene.json(不是场景壁纸?)");
|
|
12345
13152
|
const scene = scn.parseScene(JSON.parse(readText(sceneEntry)), project);
|
|
13153
|
+
if (cfg.clearColor) {
|
|
13154
|
+
const g = scene.general ??= {};
|
|
13155
|
+
g.clearcolor = cfg.clearColor;
|
|
13156
|
+
g.clearenabled = true;
|
|
13157
|
+
}
|
|
12346
13158
|
{
|
|
12347
13159
|
const zRaw = scene.general?.zoom;
|
|
12348
13160
|
const zVal = zRaw && typeof zRaw === "object" ? Number(zRaw.value) : Number(zRaw);
|
|
@@ -12427,9 +13239,10 @@ function mountScene(rt, cfg) {
|
|
|
12427
13239
|
const audioDriverRef = {
|
|
12428
13240
|
current: null
|
|
12429
13241
|
};
|
|
12430
|
-
let
|
|
13242
|
+
let liveMediaOverride = null;
|
|
13243
|
+
const currentMediaDriver = () => liveMediaOverride ?? rt.mediaSource ?? simMedia;
|
|
12431
13244
|
let windowDriver = simWindow;
|
|
12432
|
-
const audioSim = { enabled: supportsAudioProcessing };
|
|
13245
|
+
const audioSim = { enabled: supportsAudioProcessing && !rt.audioDisabled };
|
|
12433
13246
|
const zero = (n) => new Float32Array(n);
|
|
12434
13247
|
const SILENT_AUDIO = {
|
|
12435
13248
|
left16: zero(16),
|
|
@@ -12441,9 +13254,72 @@ function mountScene(rt, cfg) {
|
|
|
12441
13254
|
level: 0,
|
|
12442
13255
|
silent: true
|
|
12443
13256
|
};
|
|
13257
|
+
const hostAudio = (() => {
|
|
13258
|
+
const snapshot = {
|
|
13259
|
+
left64: zero(64),
|
|
13260
|
+
right64: zero(64),
|
|
13261
|
+
left32: zero(32),
|
|
13262
|
+
right32: zero(32),
|
|
13263
|
+
left16: zero(16),
|
|
13264
|
+
right16: zero(16),
|
|
13265
|
+
// 未钳位频谱:网页驱动会对它做 gamma 对比扩展。宿主给的已是 0..1
|
|
13266
|
+
// 归一化值,没有 pre-GAIN 概念,直接与 left64/right64 共用同一份数据
|
|
13267
|
+
preL64: zero(64),
|
|
13268
|
+
preR64: zero(64),
|
|
13269
|
+
level: 0,
|
|
13270
|
+
silent: true
|
|
13271
|
+
};
|
|
13272
|
+
const down = (dst, src) => {
|
|
13273
|
+
const g = src.length / dst.length;
|
|
13274
|
+
for (let i = 0; i < dst.length; i++) {
|
|
13275
|
+
let s = 0;
|
|
13276
|
+
const i0 = Math.floor(i * g);
|
|
13277
|
+
const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
|
|
13278
|
+
for (let j = i0; j < i1; j++) s += src[j];
|
|
13279
|
+
dst[i] = s / (i1 - i0);
|
|
13280
|
+
}
|
|
13281
|
+
};
|
|
13282
|
+
return {
|
|
13283
|
+
active: false,
|
|
13284
|
+
snapshot,
|
|
13285
|
+
/** 每帧从宿主拉一次。宿主返回 null(未采集/无权限)时置 active=false 回落模拟源 */
|
|
13286
|
+
pump() {
|
|
13287
|
+
const src = rt.audioBridge?.();
|
|
13288
|
+
if (!src || !src.left || !src.right) {
|
|
13289
|
+
this.active = false;
|
|
13290
|
+
return;
|
|
13291
|
+
}
|
|
13292
|
+
const n = Math.min(64, src.left.length, src.right.length);
|
|
13293
|
+
let sum = 0;
|
|
13294
|
+
for (let i = 0; i < n; i++) {
|
|
13295
|
+
const l = src.left[i] || 0;
|
|
13296
|
+
const r = src.right[i] || 0;
|
|
13297
|
+
snapshot.left64[i] = l;
|
|
13298
|
+
snapshot.right64[i] = r;
|
|
13299
|
+
snapshot.preL64[i] = l;
|
|
13300
|
+
snapshot.preR64[i] = r;
|
|
13301
|
+
if (i < 48) sum += l;
|
|
13302
|
+
}
|
|
13303
|
+
for (let i = n; i < 64; i++) {
|
|
13304
|
+
snapshot.left64[i] = 0;
|
|
13305
|
+
snapshot.right64[i] = 0;
|
|
13306
|
+
snapshot.preL64[i] = 0;
|
|
13307
|
+
snapshot.preR64[i] = 0;
|
|
13308
|
+
}
|
|
13309
|
+
down(snapshot.left32, snapshot.left64);
|
|
13310
|
+
down(snapshot.right32, snapshot.right64);
|
|
13311
|
+
down(snapshot.left16, snapshot.left64);
|
|
13312
|
+
down(snapshot.right16, snapshot.right64);
|
|
13313
|
+
snapshot.level = Math.min(1, sum / 48);
|
|
13314
|
+
snapshot.silent = snapshot.level < 0.02;
|
|
13315
|
+
this.active = true;
|
|
13316
|
+
}
|
|
13317
|
+
};
|
|
13318
|
+
})();
|
|
13319
|
+
const activeAudioSnapshot = () => hostAudio.active ? hostAudio.snapshot : audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
|
|
12444
13320
|
renderer.setAudioProvider(() => {
|
|
12445
13321
|
if (!audioSim.enabled) return SILENT_AUDIO;
|
|
12446
|
-
return
|
|
13322
|
+
return activeAudioSnapshot();
|
|
12447
13323
|
});
|
|
12448
13324
|
const audioViews = /* @__PURE__ */ new Map();
|
|
12449
13325
|
window.__audioStats = () => ({
|
|
@@ -12465,7 +13341,7 @@ function mountScene(rt, cfg) {
|
|
|
12465
13341
|
const shortcuts = system.createShortcutHandler((name) => {
|
|
12466
13342
|
reportDiag(rt, cfg, `openUserShortcut: ${name}`);
|
|
12467
13343
|
});
|
|
12468
|
-
const mediaSim = { enabled:
|
|
13344
|
+
const mediaSim = { enabled: !rt.mediaDisabled, override: null };
|
|
12469
13345
|
const mediaHooks = [];
|
|
12470
13346
|
let lastMediaSnap = null;
|
|
12471
13347
|
liveHold.lastSnap = {
|
|
@@ -12474,7 +13350,7 @@ function mountScene(rt, cfg) {
|
|
|
12474
13350
|
if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
|
|
12475
13351
|
}
|
|
12476
13352
|
};
|
|
12477
|
-
const mediaSnapshot = () =>
|
|
13353
|
+
const mediaSnapshot = () => currentMediaDriver().snapshot;
|
|
12478
13354
|
const registerMediaHook = (sb) => {
|
|
12479
13355
|
if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
|
|
12480
13356
|
mediaHooks.push(sb);
|
|
@@ -12519,36 +13395,30 @@ function mountScene(rt, cfg) {
|
|
|
12519
13395
|
}
|
|
12520
13396
|
lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
|
|
12521
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
|
+
};
|
|
12522
13411
|
const mediaControl = {
|
|
12523
13412
|
get snapshot() {
|
|
12524
13413
|
return mediaSnapshot();
|
|
12525
13414
|
},
|
|
12526
|
-
skipNext: () =>
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
skipPrevious: () => {
|
|
12532
|
-
mediaDriver.skipPrevious();
|
|
12533
|
-
dispatchMediaNow();
|
|
12534
|
-
return mediaSnapshot();
|
|
12535
|
-
},
|
|
12536
|
-
play: () => {
|
|
12537
|
-
mediaDriver.play();
|
|
12538
|
-
dispatchMediaNow();
|
|
12539
|
-
return mediaSnapshot();
|
|
12540
|
-
},
|
|
12541
|
-
pause: () => {
|
|
12542
|
-
mediaDriver.pause();
|
|
12543
|
-
dispatchMediaNow();
|
|
12544
|
-
return mediaSnapshot();
|
|
12545
|
-
},
|
|
12546
|
-
playPause: () => {
|
|
12547
|
-
mediaDriver.playPause();
|
|
12548
|
-
dispatchMediaNow();
|
|
12549
|
-
return mediaSnapshot();
|
|
12550
|
-
}
|
|
13415
|
+
skipNext: () => callDriver("skipNext"),
|
|
13416
|
+
skipPrevious: () => callDriver("skipPrevious"),
|
|
13417
|
+
play: () => callDriver("play"),
|
|
13418
|
+
pause: () => callDriver("pause"),
|
|
13419
|
+
playPause: () => callDriver("playPause")
|
|
12551
13420
|
};
|
|
13421
|
+
rt.mediaCtl = mediaControl;
|
|
12552
13422
|
window.__mediaControl = mediaControl;
|
|
12553
13423
|
window.__system = {
|
|
12554
13424
|
media: mediaControl,
|
|
@@ -12571,6 +13441,10 @@ function mountScene(rt, cfg) {
|
|
|
12571
13441
|
} : {}
|
|
12572
13442
|
);
|
|
12573
13443
|
renderer.setPointerProvider(() => pointerSrc);
|
|
13444
|
+
rt.pointerCtl = {
|
|
13445
|
+
push: (p) => pointerSrc.pushExternal(p),
|
|
13446
|
+
leave: () => pointerSrc.pushExternalLeave()
|
|
13447
|
+
};
|
|
12574
13448
|
{
|
|
12575
13449
|
const prevCleanup = particleCleanup;
|
|
12576
13450
|
particleCleanup = () => {
|
|
@@ -12665,6 +13539,18 @@ function mountScene(rt, cfg) {
|
|
|
12665
13539
|
}
|
|
12666
13540
|
}
|
|
12667
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
|
+
});
|
|
12668
13554
|
try {
|
|
12669
13555
|
const uploadLiveArtwork = async (info) => {
|
|
12670
13556
|
try {
|
|
@@ -12705,7 +13591,7 @@ function mountScene(rt, cfg) {
|
|
|
12705
13591
|
generated: true
|
|
12706
13592
|
});
|
|
12707
13593
|
}
|
|
12708
|
-
const snap =
|
|
13594
|
+
const snap = currentMediaDriver().snapshot;
|
|
12709
13595
|
if (palette) {
|
|
12710
13596
|
snap.primaryColor = media.mediaVec3(...palette.primary);
|
|
12711
13597
|
snap.secondaryColor = media.mediaVec3(...palette.secondary);
|
|
@@ -12730,39 +13616,48 @@ function mountScene(rt, cfg) {
|
|
|
12730
13616
|
void uploadLiveArtwork(info);
|
|
12731
13617
|
}
|
|
12732
13618
|
});
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
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
|
+
}
|
|
12743
13638
|
}
|
|
12744
13639
|
}
|
|
13640
|
+
lastMediaSnap = media.cloneMediaSnapshot(currentMediaDriver().snapshot);
|
|
12745
13641
|
}
|
|
12746
|
-
|
|
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();
|
|
12747
13660
|
}
|
|
12748
|
-
const st = live.status();
|
|
12749
|
-
reportDiag(
|
|
12750
|
-
rt,
|
|
12751
|
-
cfg,
|
|
12752
|
-
`liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
|
|
12753
|
-
);
|
|
12754
|
-
reportDiag(
|
|
12755
|
-
rt,
|
|
12756
|
-
cfg,
|
|
12757
|
-
`audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
|
|
12758
|
-
);
|
|
12759
|
-
window.__system = {
|
|
12760
|
-
media: mediaControl,
|
|
12761
|
-
windowTitle: windowDriver.snapshot,
|
|
12762
|
-
shortcuts: shortcuts.last,
|
|
12763
|
-
live: () => live.status()
|
|
12764
|
-
};
|
|
12765
|
-
window.__liveSystem = () => live.status();
|
|
12766
13661
|
} catch (e) {
|
|
12767
13662
|
reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
|
|
12768
13663
|
live = null;
|
|
@@ -13072,6 +13967,7 @@ function mountScene(rt, cfg) {
|
|
|
13072
13967
|
if (usedVisible && !rt.paused) texEntry.videoCtl.play();
|
|
13073
13968
|
}
|
|
13074
13969
|
const particleSystems = [];
|
|
13970
|
+
const particleDirty = [];
|
|
13075
13971
|
const particleSystemsByLayer = /* @__PURE__ */ new Map();
|
|
13076
13972
|
let builtinTexCount = 0;
|
|
13077
13973
|
const loadParticleTex = async (name) => {
|
|
@@ -13290,7 +14186,10 @@ function mountScene(rt, cfg) {
|
|
|
13290
14186
|
const py = originY != null ? originY : wy;
|
|
13291
14187
|
for (const ps of particleSystems) ps.setPointer(wx, py);
|
|
13292
14188
|
}
|
|
13293
|
-
|
|
14189
|
+
if (particleDirty.length) {
|
|
14190
|
+
for (const ps of particleDirty) ps.syncLayerTransform();
|
|
14191
|
+
}
|
|
14192
|
+
for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? activeAudioSnapshot() : null);
|
|
13294
14193
|
if (particleDiagFrame < 2) {
|
|
13295
14194
|
particleDiagFrame++;
|
|
13296
14195
|
if (particleDiagFrame === 2) {
|
|
@@ -13475,6 +14374,17 @@ function mountScene(rt, cfg) {
|
|
|
13475
14374
|
if (attachFollows.length) {
|
|
13476
14375
|
reportDiag(rt, cfg, `attachments: ${attachFollows.length} hanging layers`);
|
|
13477
14376
|
}
|
|
14377
|
+
const transformDirty = scn.collectTransformDirty(
|
|
14378
|
+
scene.layers,
|
|
14379
|
+
attachFollows.map((f) => f.layer)
|
|
14380
|
+
);
|
|
14381
|
+
if (transformDirty.size) {
|
|
14382
|
+
reportDiag(rt, cfg, `transform graph: ${transformDirty.size} live layers`);
|
|
14383
|
+
for (const [lid, list] of particleSystemsByLayer) {
|
|
14384
|
+
if (!transformDirty.has(lid)) continue;
|
|
14385
|
+
for (const ps of list) particleDirty.push(ps);
|
|
14386
|
+
}
|
|
14387
|
+
}
|
|
13478
14388
|
const textWidgets = [];
|
|
13479
14389
|
const textLayerText = /* @__PURE__ */ new Map();
|
|
13480
14390
|
const textShared = {};
|
|
@@ -13488,15 +14398,19 @@ function mountScene(rt, cfg) {
|
|
|
13488
14398
|
const win0 = fitWindow(normalizeFit(rt.cfg.fit), projW, projH, c.width, c.height);
|
|
13489
14399
|
const quality = Math.min(3, Math.max(0.5, c.width / Math.max(1, win0.viewW)));
|
|
13490
14400
|
const fontFamilies = /* @__PURE__ */ new Map();
|
|
14401
|
+
const usedFontKeys = [];
|
|
13491
14402
|
const fontPaths = /* @__PURE__ */ new Set();
|
|
13492
14403
|
for (const l of scene.layers) if (l.isText && l.textFont) fontPaths.add(l.textFont);
|
|
13493
14404
|
for (const e of parsedPkg.entries || []) {
|
|
13494
14405
|
if (typeof e.name === "string" && /^fonts\/.+\.(ttf|otf|woff2?)$/i.test(e.name)) fontPaths.add(e.name);
|
|
13495
14406
|
}
|
|
13496
14407
|
for (const fp of fontPaths) {
|
|
13497
|
-
const
|
|
14408
|
+
const key = `${cfg.src}|${fp}`;
|
|
14409
|
+
const cached = fontFaceCache.get(key);
|
|
13498
14410
|
if (cached) {
|
|
13499
|
-
|
|
14411
|
+
cached.refs++;
|
|
14412
|
+
usedFontKeys.push(key);
|
|
14413
|
+
fontFamilies.set(fp, cached.family);
|
|
13500
14414
|
continue;
|
|
13501
14415
|
}
|
|
13502
14416
|
const sys = SYSTEM_FONT_FAMILIES[fp.toLowerCase()];
|
|
@@ -13510,18 +14424,30 @@ function mountScene(rt, cfg) {
|
|
|
13510
14424
|
const bytes = sanitizeFontForBrowser(
|
|
13511
14425
|
fe instanceof Uint8Array ? fe : new Uint8Array(fe)
|
|
13512
14426
|
);
|
|
13513
|
-
const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
|
|
14427
|
+
const fam = "wefont_" + fontKeyHash(key) + "_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
|
|
13514
14428
|
const url = URL.createObjectURL(new Blob([bytes]));
|
|
13515
14429
|
const ff = new FontFace(fam, `url(${url})`);
|
|
13516
14430
|
await ff.load();
|
|
14431
|
+
if (disposed) {
|
|
14432
|
+
URL.revokeObjectURL(url);
|
|
14433
|
+
break;
|
|
14434
|
+
}
|
|
13517
14435
|
document.fonts.add(ff);
|
|
13518
14436
|
(rt.objectUrls ??= []).push(url);
|
|
13519
14437
|
fontFamilies.set(fp, fam);
|
|
13520
|
-
fontFaceCache.set(
|
|
14438
|
+
fontFaceCache.set(key, { family: fam, refs: 1 });
|
|
14439
|
+
usedFontKeys.push(key);
|
|
13521
14440
|
} catch (e) {
|
|
13522
14441
|
console.warn(`字体加载失败 ${fp}: ${e.message}`);
|
|
13523
14442
|
}
|
|
13524
14443
|
}
|
|
14444
|
+
if (usedFontKeys.length) {
|
|
14445
|
+
const prevCleanup = rt.sceneCleanup;
|
|
14446
|
+
rt.sceneCleanup = () => {
|
|
14447
|
+
releaseFontFaces(usedFontKeys);
|
|
14448
|
+
prevCleanup?.();
|
|
14449
|
+
};
|
|
14450
|
+
}
|
|
13525
14451
|
textCanvas = document.createElement("canvas");
|
|
13526
14452
|
textCtx = textCanvas.getContext("2d");
|
|
13527
14453
|
const MAX_TEX = 2048;
|
|
@@ -13574,10 +14500,18 @@ function mountScene(rt, cfg) {
|
|
|
13574
14500
|
const hw = layer.size[0] * (layer.scale[0] || 1) / 2;
|
|
13575
14501
|
const hh = layer.size[1] * (layer.scale[1] || 1) / 2;
|
|
13576
14502
|
const a = layer.textAnchor;
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
if (a.includes("
|
|
13580
|
-
if (a.includes("
|
|
14503
|
+
let adx = 0;
|
|
14504
|
+
let ady = 0;
|
|
14505
|
+
if (a.includes("left")) adx += hw;
|
|
14506
|
+
if (a.includes("right")) adx -= hw;
|
|
14507
|
+
if (a.includes("top")) ady -= hh;
|
|
14508
|
+
if (a.includes("bottom")) ady += hh;
|
|
14509
|
+
layer.origin[0] += adx;
|
|
14510
|
+
layer.origin[1] += ady;
|
|
14511
|
+
if (layer.localOrigin) {
|
|
14512
|
+
layer.localOrigin[0] += adx;
|
|
14513
|
+
layer.localOrigin[1] += ady;
|
|
14514
|
+
}
|
|
13581
14515
|
}
|
|
13582
14516
|
const em0 = TEXT_EM_SCALE * Math.max(1, layer.textPointsize);
|
|
13583
14517
|
const marginCap = wtext.textLayerHasTintMask(layer) ? 8 : 256;
|
|
@@ -13820,6 +14754,15 @@ function mountScene(rt, cfg) {
|
|
|
13820
14754
|
if (sb && sb.hasMediaHook) registerMediaHook(sb);
|
|
13821
14755
|
}
|
|
13822
14756
|
});
|
|
14757
|
+
const LOCAL_SLOT = {
|
|
14758
|
+
origin: "localOrigin",
|
|
14759
|
+
scale: "localScale",
|
|
14760
|
+
angles: "localAngles"
|
|
14761
|
+
};
|
|
14762
|
+
const fieldSlot = (layer, field) => {
|
|
14763
|
+
const slot = LOCAL_SLOT[field];
|
|
14764
|
+
return slot && layer && Array.isArray(layer[slot]) ? slot : field;
|
|
14765
|
+
};
|
|
13823
14766
|
for (const layer of scene.layers) {
|
|
13824
14767
|
const defs = layer.objectAnimations;
|
|
13825
14768
|
if (!defs) continue;
|
|
@@ -13830,11 +14773,13 @@ function mountScene(rt, cfg) {
|
|
|
13830
14773
|
const ctrl = anim.createAnimation(def.animation);
|
|
13831
14774
|
ctrl.field = field;
|
|
13832
14775
|
ctrl.baseValue = def.value;
|
|
13833
|
-
const
|
|
14776
|
+
const slot = fieldSlot(layer, field);
|
|
14777
|
+
const live2 = layer[slot];
|
|
13834
14778
|
ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
|
|
14779
|
+
ctrl.slot = slot;
|
|
13835
14780
|
layer.animationList.push(ctrl);
|
|
13836
14781
|
if (ctrl.name) layer.animations[ctrl.name] = ctrl;
|
|
13837
|
-
animRuns.push({ layer, field, ctrl });
|
|
14782
|
+
animRuns.push({ layer, field, slot, ctrl });
|
|
13838
14783
|
} catch (e) {
|
|
13839
14784
|
reportDiag(rt, cfg, `animation '${layer.name}.${field}' 建控制器失败: ${String(e.message).slice(0, 80)}`);
|
|
13840
14785
|
}
|
|
@@ -13899,7 +14844,8 @@ function mountScene(rt, cfg) {
|
|
|
13899
14844
|
});
|
|
13900
14845
|
if (sandbox) {
|
|
13901
14846
|
propSandboxes.push(sandbox);
|
|
13902
|
-
const
|
|
14847
|
+
const initSlot = fieldSlot(layer, field);
|
|
14848
|
+
const fieldVal = layer[initSlot];
|
|
13903
14849
|
const initArg = field === "angles" && Array.isArray(fieldVal) ? wtext.radToScriptAngles(fieldVal) : Array.isArray(fieldVal) ? { x: fieldVal[0] ?? 0, y: fieldVal[1] ?? 0, z: fieldVal[2] ?? 0 } : fieldVal;
|
|
13904
14850
|
sandbox.init(initArg);
|
|
13905
14851
|
sandbox.applyUserProperties(objUserProps);
|
|
@@ -13909,6 +14855,8 @@ function mountScene(rt, cfg) {
|
|
|
13909
14855
|
objectScriptRuns.push({
|
|
13910
14856
|
layer,
|
|
13911
14857
|
field,
|
|
14858
|
+
// 变换字段逐帧也在 local 槽上收发(与 init 同一空间)。
|
|
14859
|
+
slot: initSlot,
|
|
13912
14860
|
kind: field === "visible" ? "bool" : field === "alpha" || field === "brightness" ? "scalar" : "vec3",
|
|
13913
14861
|
sandbox
|
|
13914
14862
|
});
|
|
@@ -13972,6 +14920,7 @@ function mountScene(rt, cfg) {
|
|
|
13972
14920
|
window.__objScripts = objectScriptRuns;
|
|
13973
14921
|
}
|
|
13974
14922
|
window.__mediaHooks = mediaHooks;
|
|
14923
|
+
window.__sceneLayers = scene.layers;
|
|
13975
14924
|
window.__compositeStats = () => renderer.compositeStats?.() ?? null;
|
|
13976
14925
|
window.__compositeEnable = (on) => renderer.setCompositeEnabled?.(on);
|
|
13977
14926
|
window.__scene = scene;
|
|
@@ -14032,6 +14981,7 @@ function mountScene(rt, cfg) {
|
|
|
14032
14981
|
const playingVideos = [];
|
|
14033
14982
|
const playingAudios = [];
|
|
14034
14983
|
let lastRender = -Infinity;
|
|
14984
|
+
let lastAnimT = 0;
|
|
14035
14985
|
const renderLoop = (now) => {
|
|
14036
14986
|
if (disposed || rt.paused) return;
|
|
14037
14987
|
const fps = rt.cfg.sceneFps || 60;
|
|
@@ -14039,17 +14989,15 @@ function mountScene(rt, cfg) {
|
|
|
14039
14989
|
if (now - lastRender >= interval) {
|
|
14040
14990
|
lastRender = now;
|
|
14041
14991
|
markFrame(rt, now);
|
|
14042
|
-
if (rt.onFirstFrame) {
|
|
14043
|
-
const first = rt.onFirstFrame;
|
|
14044
|
-
rt.onFirstFrame = void 0;
|
|
14045
|
-
first();
|
|
14046
|
-
}
|
|
14047
14992
|
syncCanvasSize(rt, c, rt.cfg);
|
|
14048
14993
|
const t = (now - start - pauseAccum) / 1e3;
|
|
14049
14994
|
inputView.update(pointerSrc.state);
|
|
14050
14995
|
if (mediaSim.enabled) {
|
|
14051
14996
|
if (live?.media) live.media.pump();
|
|
14052
|
-
else
|
|
14997
|
+
else {
|
|
14998
|
+
const drv = currentMediaDriver();
|
|
14999
|
+
if (typeof drv?.update === "function") drv.update(t);
|
|
15000
|
+
}
|
|
14053
15001
|
const snap = mediaSnapshot();
|
|
14054
15002
|
const evts = media.diffMediaEvents(lastMediaSnap, snap);
|
|
14055
15003
|
if (evts.length) {
|
|
@@ -14064,34 +15012,37 @@ function mountScene(rt, cfg) {
|
|
|
14064
15012
|
}
|
|
14065
15013
|
if (live?.windowTitle) live.windowTitle.pump();
|
|
14066
15014
|
else simWindow.update(t);
|
|
15015
|
+
const animDt = Math.max(0, t - lastAnimT);
|
|
15016
|
+
lastAnimT = t;
|
|
14067
15017
|
for (const run of animRuns) {
|
|
14068
|
-
run.ctrl.advance(
|
|
15018
|
+
run.ctrl.advance(animDt);
|
|
14069
15019
|
const field = run.field;
|
|
15020
|
+
const slot = run.slot || field;
|
|
14070
15021
|
const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
|
|
14071
15022
|
if (Array.isArray(out)) {
|
|
14072
|
-
const cur = run.layer[
|
|
15023
|
+
const cur = run.layer[slot];
|
|
14073
15024
|
if (Array.isArray(cur)) for (let i = 0; i < out.length && i < cur.length; i++) cur[i] = out[i];
|
|
14074
15025
|
} else if (Number.isFinite(out)) {
|
|
14075
15026
|
if (field === "visible") run.layer[field] = !!out;
|
|
14076
|
-
else run.layer[
|
|
15027
|
+
else run.layer[slot] = out;
|
|
14077
15028
|
}
|
|
14078
15029
|
}
|
|
14079
15030
|
for (const run of generalAnimRuns) {
|
|
14080
|
-
run.ctrl.advance(
|
|
15031
|
+
run.ctrl.advance(animDt);
|
|
14081
15032
|
const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
|
|
14082
15033
|
if (typeof out === "number" && Number.isFinite(out)) run.write(out);
|
|
14083
15034
|
else if (Array.isArray(out) && Number.isFinite(out[0])) run.write(out[0]);
|
|
14084
15035
|
}
|
|
14085
15036
|
for (const run of effectVisibleRuns) {
|
|
14086
15037
|
if (run.sandbox.disabled) continue;
|
|
14087
|
-
run.sandbox.engine.frametime =
|
|
15038
|
+
run.sandbox.engine.frametime = animDt;
|
|
14088
15039
|
run.sandbox.engine.runtime = t;
|
|
14089
15040
|
const ret = run.sandbox.callUpdate(!!run.effect.visible);
|
|
14090
15041
|
if (typeof ret === "boolean") run.effect.visible = ret;
|
|
14091
15042
|
}
|
|
14092
15043
|
for (const run of generalScriptRuns) {
|
|
14093
15044
|
if (run.sandbox.disabled) continue;
|
|
14094
|
-
run.sandbox.engine.frametime =
|
|
15045
|
+
run.sandbox.engine.frametime = animDt;
|
|
14095
15046
|
run.sandbox.engine.runtime = t;
|
|
14096
15047
|
const g = scene.general || {};
|
|
14097
15048
|
const cur = g[run.field] && typeof g[run.field] === "object" && "value" in g[run.field] ? g[run.field].value : g[run.field];
|
|
@@ -14099,10 +15050,16 @@ function mountScene(rt, cfg) {
|
|
|
14099
15050
|
if (ret !== void 0) run.write(ret);
|
|
14100
15051
|
}
|
|
14101
15052
|
const screenRes = { x: c.clientWidth || window.innerWidth || 1, y: c.clientHeight || window.innerHeight || 1 };
|
|
15053
|
+
for (const sb of propSandboxes) {
|
|
15054
|
+
if (!sb || sb.disabled) continue;
|
|
15055
|
+
sb.engine.frametime = animDt;
|
|
15056
|
+
sb.engine.runtime = t;
|
|
15057
|
+
sb.engine.screenResolution = screenRes;
|
|
15058
|
+
}
|
|
14102
15059
|
let visibilityDirty = false;
|
|
14103
15060
|
for (const run of objectScriptRuns) {
|
|
14104
15061
|
if (run.sandbox.disabled) continue;
|
|
14105
|
-
run.sandbox.engine.frametime =
|
|
15062
|
+
run.sandbox.engine.frametime = animDt;
|
|
14106
15063
|
run.sandbox.engine.runtime = t;
|
|
14107
15064
|
run.sandbox.engine.screenResolution = screenRes;
|
|
14108
15065
|
const cur = run.layer[run.field];
|
|
@@ -14123,26 +15080,37 @@ function mountScene(rt, cfg) {
|
|
|
14123
15080
|
const n = Number(ret);
|
|
14124
15081
|
if (Number.isFinite(n)) run.layer[run.field] = n;
|
|
14125
15082
|
} else {
|
|
14126
|
-
const
|
|
15083
|
+
const slot = run.slot || run.field;
|
|
15084
|
+
const lcur = run.layer[slot];
|
|
15085
|
+
const v = run.field === "angles" ? wtext.radToScriptAngles(lcur) : { x: lcur[0] || 0, y: lcur[1] || 0, z: lcur[2] || 0 };
|
|
14127
15086
|
const ret = run.sandbox.callUpdate(v);
|
|
14128
15087
|
const o = ret && typeof ret === "object" && "x" in ret ? ret : v;
|
|
14129
|
-
run.layer[
|
|
15088
|
+
run.layer[slot] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
|
|
14130
15089
|
}
|
|
14131
15090
|
}
|
|
14132
15091
|
if (visibilityDirty) recomputeVisibility();
|
|
15092
|
+
if (transformDirty.size) scn.recomposeWorld(scene.layers, transformDirty);
|
|
14133
15093
|
if (audioSim.enabled) {
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
);
|
|
15094
|
+
hostAudio.pump();
|
|
15095
|
+
if (!hostAudio.active) {
|
|
15096
|
+
if (audioDriverRef.current) audioDriverRef.current.pump();
|
|
15097
|
+
else simAudio.update(t);
|
|
15098
|
+
}
|
|
15099
|
+
fillAudioBuffers(audioViews, activeAudioSnapshot());
|
|
14140
15100
|
}
|
|
14141
15101
|
if (attachFollows.length) {
|
|
14142
15102
|
mdl.followAttachments(attachFollows, t, getBoneOverrides);
|
|
14143
15103
|
}
|
|
14144
15104
|
const peek = rt.coverAlign;
|
|
14145
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
|
+
}
|
|
14146
15114
|
if (disposed || rt.paused) return;
|
|
14147
15115
|
try {
|
|
14148
15116
|
dispatchCursor();
|
|
@@ -14323,36 +15291,966 @@ function mountScene(rt, cfg) {
|
|
|
14323
15291
|
}
|
|
14324
15292
|
})();
|
|
14325
15293
|
}
|
|
15294
|
+
const SHIM_MARK = 'data-we-shim="1"';
|
|
15295
|
+
const SHIM_ATTR = "data-we-shim-src";
|
|
15296
|
+
function entryDirUrl(entryUrl) {
|
|
15297
|
+
try {
|
|
15298
|
+
const u = new URL(entryUrl);
|
|
15299
|
+
const path = u.pathname;
|
|
15300
|
+
const i = path.lastIndexOf("/");
|
|
15301
|
+
u.pathname = i < 0 ? "/" : path.slice(0, i + 1);
|
|
15302
|
+
u.hash = "";
|
|
15303
|
+
u.search = "";
|
|
15304
|
+
return u.href;
|
|
15305
|
+
} catch {
|
|
15306
|
+
const s = entryUrl.replace(/[#?].*$/, "");
|
|
15307
|
+
const i = s.lastIndexOf("/");
|
|
15308
|
+
return i < 0 ? s : s.slice(0, i + 1);
|
|
15309
|
+
}
|
|
15310
|
+
}
|
|
15311
|
+
function hasBlockingCsp(html) {
|
|
15312
|
+
const re = /<meta[^>]+http-equiv\s*=\s*["']?Content-Security-Policy["']?[^>]*>/gi;
|
|
15313
|
+
let m;
|
|
15314
|
+
while (m = re.exec(html)) {
|
|
15315
|
+
const tag = m[0];
|
|
15316
|
+
const content = /content\s*=\s*"([^"]*)"/i.exec(tag)?.[1] ?? /content\s*=\s*'([^']*)'/i.exec(tag)?.[1] ?? "";
|
|
15317
|
+
if (!/script-src/i.test(content)) continue;
|
|
15318
|
+
if (/script-src[^;]*'unsafe-inline'/i.test(content)) continue;
|
|
15319
|
+
if (/script-src[^;]*\*/i.test(content)) continue;
|
|
15320
|
+
return true;
|
|
15321
|
+
}
|
|
15322
|
+
return false;
|
|
15323
|
+
}
|
|
15324
|
+
function escapeScriptClose(js) {
|
|
15325
|
+
return js.replace(/<\/script/gi, "<\\/script");
|
|
15326
|
+
}
|
|
15327
|
+
function rewriteHtml(html, shimSource2, opts) {
|
|
15328
|
+
if (!html) html = "";
|
|
15329
|
+
if (html.includes(SHIM_MARK) || html.includes(SHIM_ATTR)) return html;
|
|
15330
|
+
const base = opts.baseHref && !/<base\b/i.test(html) ? `<base href="${opts.baseHref.replace(/"/g, """)}">` : "";
|
|
15331
|
+
const script = `<script ${SHIM_ATTR}="1">
|
|
15332
|
+
${escapeScriptClose(shimSource2)}
|
|
15333
|
+
<\/script>`;
|
|
15334
|
+
const seed = opts.seedScript && opts.seedScript.trim() ? `<script>
|
|
15335
|
+
${escapeScriptClose(opts.seedScript)}
|
|
15336
|
+
<\/script>` : "";
|
|
15337
|
+
const inject = `${base}${script}${seed}`;
|
|
15338
|
+
const headOpen = /<head(\s[^>]*)?>/i.exec(html);
|
|
15339
|
+
if (headOpen) {
|
|
15340
|
+
const at = headOpen.index + headOpen[0].length;
|
|
15341
|
+
return html.slice(0, at) + inject + html.slice(at);
|
|
15342
|
+
}
|
|
15343
|
+
const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
|
|
15344
|
+
if (htmlOpen) {
|
|
15345
|
+
const at = htmlOpen.index + htmlOpen[0].length;
|
|
15346
|
+
return html.slice(0, at) + `<head>${inject}</head>` + html.slice(at);
|
|
15347
|
+
}
|
|
15348
|
+
return `<!DOCTYPE html><html><head>${inject}</head><body>${html}</body></html>`;
|
|
15349
|
+
}
|
|
15350
|
+
const shimSource = '/**\n * WE 网页壁纸兼容 shim(注入到 iframe,必须在作者脚本之前执行)。\n *\n * 语料:本机库 42 张 web 壁纸扫描(2026-09)——\n * wallpaperPropertyListener 29 / RegisterAudioListener 22 /\n * RequestRandomFileForProperty 8 / userDirectoryFiles* 9 /\n * Media*Listener 2 / PluginListener 2\n *\n * 官方 CEF 在任何壁纸脚本前就把这些做成原生函数;工坊顶层直接注册。\n * 本文件作为 <head> 首个 classic script 插入。\n *\n * 父页控制面 __we*(main.ts weShimCall / web.ts 泵):\n * __weSetPaused / __weSetFps / __weSetVolume / __weApplyProps / __weSeedProps\n * __wePushAudio(arr128)\n * __wePushMedia(event) — {op, payload} 见下\n * __wePushDirectoryFiles(prop, files) / __weRemoveDirectoryFiles(prop, files)\n * __weRewriteFileUrl(s) — file:/// → 同源相对(HTTP 页);空 file:/// → ""\n * __wePushPointer(x, y, buttons) / __wePointerLeave() — 外部指针注入(见文末)\n */\n(function (w) {\n "use strict";\n try {\n if (w.document && w.document.documentElement) {\n w.document.documentElement.setAttribute("data-we-shim", "1");\n }\n } catch (_) {\n /* 忽略 */\n }\n\n var audioListener = null;\n var propertyListener = null;\n var paused = false;\n var fps = 60;\n var volume = 0;\n var pendingProps = null;\n var pendingGeneral = null;\n var rafMap = Object.create(null);\n var rafCounter = 0;\n var origRaf = w.requestAnimationFrame.bind(w);\n var origCaf = w.cancelAnimationFrame.bind(w);\n\n // 官方 CEF 以文件系统为源,作者普遍 `\'file:///\' + value`。HTTP 同源页里\n // file:///files/x.webm 加载失败;空 value 变成 file:///(1748506393)。\n // file: 协议页保持原样(真本地嵌入)。\n function rewriteBareFileUrl(url) {\n if (typeof url !== "string") return url;\n var s = url.trim();\n if (!/^file:/i.test(s)) return s;\n try {\n var loc = w.location;\n if (loc && loc.protocol === "file:") return s;\n } catch (_) {\n /* 忽略 */\n }\n var rest = s.replace(/^file:\\/\\//i, "").replace(/^\\/+/, "");\n if (!rest) return "";\n if (/^[a-zA-Z][:|]/.test(rest)) return "";\n if (/^(Users|home|tmp|var|etc|private|Volumes)\\//.test(rest)) return "";\n try {\n var href = w.location && w.location.href;\n if (href) return new URL(rest, href).href;\n } catch (_) {\n /* 忽略 */\n }\n return rest;\n }\n\n function rewriteWeFileUrl(input) {\n if (typeof input !== "string") return input;\n if (/url\\(/i.test(input)) {\n return input.replace(/url\\(\\s*([\'"]?)([^)\'"]*?)\\1\\s*\\)/gi, function (_m, q, inner) {\n var next = rewriteBareFileUrl(inner);\n if (!next) return "none";\n var quote = q || \'"\';\n return "url(" + quote + next + quote + ")";\n });\n }\n return rewriteBareFileUrl(input);\n }\n\n w.__weRewriteFileUrl = rewriteWeFileUrl;\n\n function installFileUrlHooks() {\n try {\n if (w.Element && w.Element.prototype && typeof w.Element.prototype.setAttribute === "function") {\n var origSetAttr = w.Element.prototype.setAttribute;\n w.Element.prototype.setAttribute = function (name, value) {\n var n = String(name || "").toLowerCase();\n if (n === "src" || n === "href" || n === "poster") value = rewriteWeFileUrl(value);\n return origSetAttr.call(this, name, value);\n };\n }\n } catch (_) {\n /* 无 DOM 的 verifier 跳过 */\n }\n var ctorNames = ["HTMLImageElement", "HTMLMediaElement", "HTMLSourceElement", "HTMLScriptElement"];\n for (var i = 0; i < ctorNames.length; i++) {\n try {\n var Ctor = w[ctorNames[i]];\n if (!Ctor || !Ctor.prototype) continue;\n var desc = Object.getOwnPropertyDescriptor(Ctor.prototype, "src");\n if (!desc || typeof desc.set !== "function") continue;\n (function (d) {\n Object.defineProperty(Ctor.prototype, "src", {\n configurable: true,\n enumerable: d.enumerable,\n get: d.get,\n set: function (v) {\n d.set.call(this, rewriteWeFileUrl(v));\n },\n });\n })(desc);\n } catch (_) {\n /* 忽略单个原型 */\n }\n }\n try {\n var styleDesc =\n w.HTMLElement && Object.getOwnPropertyDescriptor(w.HTMLElement.prototype, "style");\n // Chromium 的 backgroundImage 不是原型自有描述符,只能包 HTMLElement.style 的 Proxy。\n if (styleDesc && typeof styleDesc.get === "function" && w.Proxy && w.WeakMap) {\n var styleCache = new w.WeakMap();\n Object.defineProperty(w.HTMLElement.prototype, "style", {\n configurable: true,\n enumerable: styleDesc.enumerable,\n get: function () {\n var raw = styleDesc.get.call(this);\n if (!raw) return raw;\n var cached = styleCache.get(raw);\n if (cached) return cached;\n var proxy = new w.Proxy(raw, {\n set: function (target, prop, value) {\n if (typeof value === "string" && typeof prop === "string" && /background/i.test(prop)) {\n value = rewriteWeFileUrl(value);\n }\n target[prop] = value;\n return true;\n },\n get: function (target, prop) {\n var v = target[prop];\n if (typeof v === "function") return v.bind(target);\n return v;\n },\n });\n styleCache.set(raw, proxy);\n return proxy;\n },\n set: styleDesc.set,\n });\n }\n var styleProto = w.CSSStyleDeclaration && w.CSSStyleDeclaration.prototype;\n if (styleProto && typeof styleProto.setProperty === "function") {\n var origSetProp = styleProto.setProperty;\n styleProto.setProperty = function (name, value, priority) {\n if (typeof value === "string" && /background/i.test(String(name || ""))) {\n value = rewriteWeFileUrl(value);\n }\n return origSetProp.call(this, name, value, priority);\n };\n }\n } catch (_) {\n /* 忽略 */\n }\n }\n installFileUrlHooks();\n\n // propertyName → string[](绝对/相对路径;随机文件从此抽)\n var directoryFiles = Object.create(null);\n\n var mediaListeners = {\n properties: null,\n thumbnail: null,\n playback: null,\n timeline: null,\n status: null,\n };\n // 晚注册时回放最近一帧(作者脚本常在 DOMContentLoaded 后才 Register)\n var lastMedia = {\n properties: null,\n thumbnail: null,\n playback: null,\n timeline: null,\n status: null,\n };\n\n function callApplyUserProperties(props) {\n if (!propertyListener || typeof propertyListener.applyUserProperties !== "function") return;\n try {\n propertyListener.applyUserProperties(props || {});\n } catch (_) {\n /* 壁纸脚本抛错不打断宿主 */\n }\n }\n\n function callApplyGeneralProperties(props) {\n if (!propertyListener || typeof propertyListener.applyGeneralProperties !== "function") return;\n try {\n propertyListener.applyGeneralProperties(props || {});\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function callSetPaused(v) {\n if (!propertyListener || typeof propertyListener.setPaused !== "function") return;\n try {\n propertyListener.setPaused(!!v);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function callDirectoryAdded(prop, files) {\n if (!propertyListener || typeof propertyListener.userDirectoryFilesAddedOrChanged !== "function")\n return;\n try {\n propertyListener.userDirectoryFilesAddedOrChanged(prop, files);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function callDirectoryRemoved(prop, files) {\n if (!propertyListener || typeof propertyListener.userDirectoryFilesRemoved !== "function") return;\n try {\n propertyListener.userDirectoryFilesRemoved(prop, files);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function flushPending() {\n if (pendingProps) {\n var p = pendingProps;\n pendingProps = null;\n callApplyUserProperties(p);\n }\n if (pendingGeneral) {\n var g = pendingGeneral;\n pendingGeneral = null;\n callApplyGeneralProperties(g);\n }\n }\n\n /**\n * 工坊常在 React render 里写 `window.wallpaperPropertyListener = {…}`(2905017768)。\n * 官方 CEF 不会在赋值当下同步回调 setPaused/apply*;若我们同步 flush,\n * 等于 render 中 setState → React 熔断 → #root 空(一片黑)。\n */\n function afterAssign(fn) {\n try {\n if (typeof w.queueMicrotask === "function") w.queueMicrotask(fn);\n else w.setTimeout(fn, 0);\n } catch (_) {\n try {\n fn();\n } catch (_) {\n /* 忽略 */\n }\n }\n }\n\n function safeCall(fn, arg) {\n if (typeof fn !== "function") return;\n try {\n fn(arg);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n // —— 媒体集成枚举(3747222633:缺省时 PLAYBACK_PLAYING||0 会把「播放」当成 0)——\n w.wallpaperMediaIntegration = {\n PLAYBACK_STOPPED: 0,\n PLAYBACK_PLAYING: 1,\n PLAYBACK_PAUSED: 2,\n };\n\n // —— 官方 API:音频 ——\n w.wallpaperRegisterAudioListener = function (cb) {\n audioListener = typeof cb === "function" ? cb : null;\n };\n\n // —— 官方 API:媒体 ——\n w.wallpaperRegisterMediaPropertiesListener = function (cb) {\n mediaListeners.properties = typeof cb === "function" ? cb : null;\n if (mediaListeners.properties && lastMedia.properties) {\n safeCall(mediaListeners.properties, lastMedia.properties);\n }\n };\n w.wallpaperRegisterMediaThumbnailListener = function (cb) {\n mediaListeners.thumbnail = typeof cb === "function" ? cb : null;\n if (mediaListeners.thumbnail && lastMedia.thumbnail) {\n safeCall(mediaListeners.thumbnail, lastMedia.thumbnail);\n }\n };\n w.wallpaperRegisterMediaPlaybackListener = function (cb) {\n mediaListeners.playback = typeof cb === "function" ? cb : null;\n if (mediaListeners.playback && lastMedia.playback) {\n safeCall(mediaListeners.playback, lastMedia.playback);\n }\n };\n w.wallpaperRegisterMediaTimelineListener = function (cb) {\n mediaListeners.timeline = typeof cb === "function" ? cb : null;\n if (mediaListeners.timeline && lastMedia.timeline) {\n safeCall(mediaListeners.timeline, lastMedia.timeline);\n }\n };\n w.wallpaperRegisterMediaStatusListener = function (cb) {\n mediaListeners.status = typeof cb === "function" ? cb : null;\n if (mediaListeners.status && lastMedia.status) {\n safeCall(mediaListeners.status, lastMedia.status);\n }\n };\n\n // —— 官方 API:随机文件(slideshow)——\n // 回调签名:function(propertyName, filePath)。无库存文件时 filePath 为空串(语料 if(i) 守卫)。\n w.wallpaperRequestRandomFileForProperty = function (propertyName, callback) {\n if (typeof callback !== "function") return;\n var prop = String(propertyName || "");\n var list = directoryFiles[prop];\n var path = "";\n if (list && list.length) {\n path = String(list[(Math.random() * list.length) | 0] || "");\n }\n try {\n callback(prop, path);\n } catch (_) {\n /* 忽略 */\n }\n };\n\n // —— PropertyListener(getter/setter;回调延后到微任务,见 afterAssign)——\n // 官方在页面加载完成后才发全量属性/暂停状态;首屏脚本(body onLoad=init 等)常\n // 假设属性到达时 DOM/场景已初始化(827982449:applyUserProperties→cl() 在 load 前\n // 跑会撞上未创建的 scene/material)。未加载完成时等 window load + 一个宏任务\n // (保证排在 onLoad 属性处理器之后),已加载完成则微任务即发。\n function whenPageReady(fn) {\n var ready = "complete";\n try {\n ready = w.document.readyState;\n } catch (_) {\n /* 忽略 */\n }\n if (ready === "complete") {\n afterAssign(fn);\n return;\n }\n try {\n w.addEventListener("load", function () {\n // setTimeout 保证排在 load 同步链(onLoad 处理器)之后\n w.setTimeout(fn, 0);\n }, { once: true });\n } catch (_) {\n afterAssign(fn);\n }\n }\n Object.defineProperty(w, "wallpaperPropertyListener", {\n configurable: true,\n enumerable: true,\n get: function () {\n return propertyListener;\n },\n set: function (v) {\n var next = v && typeof v === "object" ? v : null;\n var prev = propertyListener;\n propertyListener = next;\n if (!next) return;\n // 仅首次注册补发挂载状态。官方 CEF 从不在赋值当下回调;2905017768 等 React 壁纸在\n // 渲染体里重新赋值(新对象字面量),若每次都补 setPaused 会形成\n // 渲染 → 赋值 → 补发 setState → 再渲染 的微任务死循环(点下一曲整页卡死)。\n if (prev) return;\n whenPageReady(function () {\n flushPending();\n callApplyGeneralProperties({ fps: fps });\n callSetPaused(paused);\n // 已缓存的目录文件补推一次(作者可能后挂 userDirectoryFilesAddedOrChanged)\n for (var prop in directoryFiles) {\n if (Object.prototype.hasOwnProperty.call(directoryFiles, prop) && directoryFiles[prop].length) {\n callDirectoryAdded(prop, directoryFiles[prop].slice());\n }\n }\n });\n },\n });\n\n // —— Plugin(iCUE 等;无硬件时空实现,避免 if 判断失败)——\n if (!w.wallpaperPluginListener) {\n w.wallpaperPluginListener = {\n onPluginLoaded: function () {},\n };\n }\n\n // —— 定时器冻结:官方暂停 = "fully freeze the process that renders the wallpaper",\n // rAF 已在节流层挂起,这里冻结定时器:暂停期间新建的挂起登记、恢复时按原延迟/间隔\n // 重新启动;**已启动**的真定时器到期由包装回调拦下——timeout 转挂起(恢复后立即补跑,\n // 近似官方的剩余等待),interval 直接跳过该周期(恢复后从下个周期继续)。\n var pendTimers = [];\n var tmSeq = 0;\n var TM_BASE = 0x40000000; // 假 id 段,避免与真实 timer id 混淆\n var origST = w.setTimeout;\n var origSI = w.setInterval;\n var origCTO = w.clearTimeout;\n var origCIT = w.clearInterval;\n function guardTimeout(fn) {\n if (typeof fn !== "function") return fn;\n return function () {\n if (paused) {\n pendTimers.push({ id: 0, kind: "t", fn: fn, ms: 1, extra: [] });\n return;\n }\n return fn.apply(this, arguments);\n };\n }\n function guardInterval(fn) {\n if (typeof fn !== "function") return fn;\n return function () {\n if (paused) return;\n return fn.apply(this, arguments);\n };\n }\n function startTimer(kind, fn, ms, extra) {\n if (paused) {\n var id = TM_BASE + ++tmSeq;\n pendTimers.push({ id: id, kind: kind, fn: fn, ms: ms, extra: extra });\n return id;\n }\n var args = [kind === "t" ? guardTimeout(fn) : guardInterval(fn), ms].concat(extra);\n return (kind === "t" ? origST : origSI).apply(w, args);\n }\n w.setTimeout = function (fn, ms) {\n return startTimer("t", fn, ms, Array.prototype.slice.call(arguments, 2));\n };\n w.setInterval = function (fn, ms) {\n return startTimer("i", fn, ms, Array.prototype.slice.call(arguments, 2));\n };\n function unpend(id) {\n for (var i = 0; i < pendTimers.length; i++) {\n if (pendTimers[i].id === id) {\n pendTimers.splice(i, 1);\n return true;\n }\n }\n return false;\n }\n w.clearTimeout = function (id) {\n if (unpend(id)) return;\n origCTO.call(w, id);\n };\n w.clearInterval = function (id) {\n if (unpend(id)) return;\n origCIT.call(w, id);\n };\n function resumeTimers() {\n var list = pendTimers;\n pendTimers = [];\n for (var i = 0; i < list.length; i++) {\n var t = list[i];\n (t.kind === "t" ? origST : origSI).call(w, t.fn, t.ms, t.extra);\n }\n }\n\n /**\n * 恢复暂停期间被挂起的 rAF 请求。\n *\n * **不补跑这些回调,主循环就永久断掉**(1278092907 Monstercat:`draw()` 在函数体\n * 开头就 `requestAnimationFrame(draw)` 再画,暂停期间那次请求被登记成 hold,\n * 恢复后没人跑它 → 整条链没有下一帧,画面永久定格,且没有任何报错)。\n * 这是 rAF 自递归的通用形态,不是这一张的特例。\n *\n * 走 `w.requestAnimationFrame` 而不是 `origRaf`:此时已 unpaused,要让它重新经过\n * 节流层(低 fps 时该走 setTimeout 路径),并照常发 we-frame 打点。\n */\n function resumeRafHolds() {\n var holds = [];\n for (var id in rafMap) {\n if (!Object.prototype.hasOwnProperty.call(rafMap, id)) continue;\n if (rafMap[id] && rafMap[id].kind === "hold") {\n holds.push(rafMap[id].cb);\n delete rafMap[id];\n }\n }\n for (var i = 0; i < holds.length; i++) {\n try {\n w.requestAnimationFrame(holds[i]);\n } catch (_) {\n /* 单个回调重挂失败不影响其它 */\n }\n }\n }\n\n // —— 媒体音量:对齐官方 CEF 语义(浏览器级主音量与作者页面内音量独立相乘)——\n // 作者常在播放前重设 a.volume = uiVolume(Bocchi),且音频多为 `new Audio()` 不进\n // DOM——querySelectorAll 找不到、直接覆盖 volume 又会被作者回写。因此 hook 原型:\n // setter 记作者值,元素实际音量 = 作者值 × 主音量;`__weSetVolume` 改系数并刷新\n // 全部活实例(DOM 内 + Audio 构造器登记的 WeakRef)。\n var hostVolume = 1;\n var liveMedia = []; // WeakRef<HTMLMediaElement>\n function trackMedia(el) {\n if (!w.WeakRef) return;\n liveMedia.push(new w.WeakRef(el));\n }\n function applyMediaVolume(el) {\n if (el.__weBaseVol != null) {\n mediaVolDesc.set.call(el, el.__weBaseVol * hostVolume);\n } else {\n mediaVolDesc.set.call(el, hostVolume);\n }\n var baseMuted = !!el.__weBaseMuted;\n mediaMutedDesc.set.call(el, baseMuted || hostVolume <= 0);\n }\n function refreshAllMediaVolume() {\n try {\n var nodes = w.document.querySelectorAll("audio,video");\n for (var i = 0; i < nodes.length; i++) applyMediaVolume(nodes[i]);\n } catch (_) {\n /* 忽略 */\n }\n for (var j = liveMedia.length - 1; j >= 0; j--) {\n var el = liveMedia[j].deref();\n if (!el) {\n liveMedia.splice(j, 1);\n continue;\n }\n applyMediaVolume(el);\n }\n }\n var mediaVolDesc = null;\n var mediaMutedDesc = null;\n function installMediaVolumeHooks() {\n try {\n if (!w.HTMLMediaElement || !w.HTMLMediaElement.prototype) return;\n var proto = w.HTMLMediaElement.prototype;\n mediaVolDesc = Object.getOwnPropertyDescriptor(proto, "volume");\n mediaMutedDesc = Object.getOwnPropertyDescriptor(proto, "muted");\n if (mediaVolDesc && typeof mediaVolDesc.set === "function") {\n Object.defineProperty(proto, "volume", {\n configurable: true,\n enumerable: mediaVolDesc.enumerable,\n get: function () {\n return this.__weBaseVol != null ? this.__weBaseVol : mediaVolDesc.get.call(this);\n },\n set: function (v) {\n this.__weBaseVol = Math.max(0, Math.min(1, Number(v) || 0));\n mediaVolDesc.set.call(this, this.__weBaseVol * hostVolume);\n },\n });\n }\n if (mediaMutedDesc && typeof mediaMutedDesc.set === "function") {\n Object.defineProperty(proto, "muted", {\n configurable: true,\n enumerable: mediaMutedDesc.enumerable,\n get: function () {\n return this.__weBaseMuted != null\n ? this.__weBaseMuted || hostVolume <= 0\n : mediaMutedDesc.get.call(this);\n },\n set: function (v) {\n this.__weBaseMuted = !!v;\n mediaMutedDesc.set.call(this, !!v || hostVolume <= 0);\n },\n });\n }\n // `new Audio()` 不进 DOM:构造器登记 WeakRef 以便主音量变化时刷新\n if (typeof w.Audio === "function" && w.WeakRef) {\n var OrigAudio = w.Audio;\n function WrappedAudio(src) {\n var a = new OrigAudio(src);\n trackMedia(a);\n applyMediaVolume(a);\n return a;\n }\n WrappedAudio.prototype = OrigAudio.prototype;\n w.Audio = WrappedAudio;\n }\n } catch (_) {\n /* 无媒体环境的 verifier 跳过 */\n }\n }\n installMediaVolumeHooks();\n\n // —— 父页控制面 ——\n // 官方 setPaused 只在暂停状态实际变化时调用一次;重复调用去重。\n // 暂停还要冻结页内媒体:官方是进程级冻结(无声、解码器可回收),作者的\n // setPaused 常只管自己的逻辑。只记录「我们代为暂停」的元素,恢复时仅还原这部分,\n // 不碰作者自己暂停的。\n var weFrozenMedia = [];\n function freezePageMedia() {\n weFrozenMedia.length = 0;\n try {\n var nodes = w.document.querySelectorAll("audio,video");\n for (var i = 0; i < nodes.length; i++) {\n if (!nodes[i].paused) {\n weFrozenMedia.push(nodes[i]);\n try {\n nodes[i].pause();\n } catch (_) {\n /* 忽略 */\n }\n }\n }\n } catch (_) {\n /* 忽略 */\n }\n }\n function thawPageMedia() {\n for (var i = 0; i < weFrozenMedia.length; i++) {\n try {\n var p = weFrozenMedia[i].play();\n if (p && p.catch) p.catch(function () {});\n } catch (_) {\n /* 忽略 */\n }\n }\n weFrozenMedia.length = 0;\n }\n\n /**\n * 暂停还要冻结 **CSS 动画 / 过渡**(Web Animations 时间轴)。\n *\n * rAF 与定时器冻结管不到它们:CSS `animation` 由浏览器**合成器**独立驱动,\n * 与 JS 主线程无关。1444432396 Glitch Clock 的整个视觉(背景移动、抖动、故障\n * 闪烁)是 10 处 `animation: … infinite`,只有时钟文字走 `setInterval` ——\n * 暂停后画面照旧动个不停,用户看到的就是「无法暂停」(实测暂停期间 6 个动画\n * 全为 `playState:"running"`,`currentTime` 700ms 推进整 700ms)。\n *\n * 官方暂停语义是「fully freeze the process that renders the wallpaper」,\n * 合成器动画自然也在冻结范围内。\n *\n * 与媒体冻结同一条纪律:**只记录我们代为暂停的**,恢复时仅还原这部分——\n * 作者自己用 `animation-play-state: paused` 停下的(常见于 hover 才播的装饰)\n * 不能被我们唤醒。`getAnimations()` 拿的是活动动画对象,`pause()`/`play()`\n * 直接作用在时间轴上,比改 `style.animationPlayState` 干净(后者会污染作者的\n * 内联样式,且被作者下一次样式写入覆盖)。\n */\n var weFrozenAnims = [];\n function freezePageAnimations() {\n weFrozenAnims.length = 0;\n try {\n if (typeof w.document.getAnimations !== "function") return;\n var anims = w.document.getAnimations();\n for (var i = 0; i < anims.length; i++) {\n var a = anims[i];\n if (a && a.playState === "running") {\n weFrozenAnims.push(a);\n try {\n a.pause();\n } catch (_) {\n /* 个别动画不可暂停时跳过 */\n }\n }\n }\n } catch (_) {\n /* 旧引擎无 getAnimations:退化为不冻结,不报错 */\n }\n }\n function thawPageAnimations() {\n for (var i = 0; i < weFrozenAnims.length; i++) {\n try {\n weFrozenAnims[i].play();\n } catch (_) {\n /* 已被作者移除的动画忽略 */\n }\n }\n weFrozenAnims.length = 0;\n }\n w.__weSetPaused = function (v) {\n var next = !!v;\n if (next === paused) return;\n paused = next;\n if (paused) {\n callSetPaused(true);\n freezePageMedia();\n freezePageAnimations();\n } else {\n callSetPaused(false);\n thawPageMedia();\n thawPageAnimations();\n resumeTimers();\n // rAF 挂起项必须补跑,否则自递归的主循环永久断链(1278092907)\n resumeRafHolds();\n }\n };\n\n w.__weSetFps = function (n) {\n var next = Number(n);\n if (!Number.isFinite(next) || next <= 0) return;\n fps = next;\n callApplyGeneralProperties({ fps: fps });\n };\n\n w.__weSetVolume = function (v) {\n var next = Math.max(0, Math.min(1, Number(v) || 0));\n volume = next;\n hostVolume = next;\n refreshAllMediaVolume();\n };\n\n w.__weApplyProps = function (props) {\n if (!props || typeof props !== "object") return;\n // file 属性:值是路径时登记进随机池(单文件 slideshow)\n try {\n for (var key in props) {\n if (!Object.prototype.hasOwnProperty.call(props, key)) continue;\n var ent = props[key];\n var val = ent && typeof ent === "object" && "value" in ent ? ent.value : ent;\n if (typeof val === "string" && val !== "" && /\\.(png|jpe?g|gif|webp|webm|mp4|bmp)$/i.test(val)) {\n directoryFiles[key] = [val];\n }\n }\n } catch (_) {\n /* 忽略 */\n }\n if (!propertyListener || typeof propertyListener.applyUserProperties !== "function") {\n pendingProps = props;\n return;\n }\n callApplyUserProperties(props);\n };\n\n w.__weSeedProps = function (props) {\n if (!props || typeof props !== "object") return;\n if (propertyListener && typeof propertyListener.applyUserProperties === "function") {\n w.__weApplyProps(props);\n } else {\n pendingProps = props;\n }\n };\n\n w.__wePushAudio = function (arr) {\n if (paused || !audioListener) return;\n try {\n audioListener(arr);\n } catch (_) {\n /* 忽略 */\n }\n };\n\n /**\n * 媒体事件泵。payload 形态对齐官方:\n * { op:"properties", title, artist, album, albumArtist }\n * { op:"thumbnail", thumbnail, primaryColor, textColor, ... }\n * { op:"playback", state } // 0/1/2\n * { op:"timeline", position, duration }\n * { op:"status", enabled }\n */\n w.__wePushMedia = function (payload) {\n if (!payload || typeof payload !== "object") return;\n var op = payload.op;\n if (op === "properties") {\n lastMedia.properties = payload;\n safeCall(mediaListeners.properties, payload);\n } else if (op === "thumbnail") {\n lastMedia.thumbnail = payload;\n safeCall(mediaListeners.thumbnail, payload);\n } else if (op === "playback") {\n lastMedia.playback = payload;\n safeCall(mediaListeners.playback, payload);\n } else if (op === "timeline") {\n lastMedia.timeline = payload;\n safeCall(mediaListeners.timeline, payload);\n } else if (op === "status") {\n lastMedia.status = payload;\n safeCall(mediaListeners.status, payload);\n }\n };\n\n /** 目录文件列表(首次或追加)。files: string[] */\n w.__wePushDirectoryFiles = function (propertyName, files) {\n var prop = String(propertyName || "");\n if (!prop || !Array.isArray(files)) return;\n var cleaned = [];\n for (var i = 0; i < files.length; i++) {\n if (files[i] != null && String(files[i]) !== "") cleaned.push(String(files[i]));\n }\n if (!directoryFiles[prop]) directoryFiles[prop] = [];\n // 首次全量替换语义由调用方决定;这里 concat 去重\n var seen = Object.create(null);\n for (var j = 0; j < directoryFiles[prop].length; j++) seen[directoryFiles[prop][j]] = 1;\n var added = [];\n for (var k = 0; k < cleaned.length; k++) {\n if (!seen[cleaned[k]]) {\n seen[cleaned[k]] = 1;\n directoryFiles[prop].push(cleaned[k]);\n added.push(cleaned[k]);\n }\n }\n if (added.length) callDirectoryAdded(prop, added);\n };\n\n w.__weRemoveDirectoryFiles = function (propertyName, files) {\n var prop = String(propertyName || "");\n if (!prop || !Array.isArray(files) || !directoryFiles[prop]) return;\n var removeSet = Object.create(null);\n for (var i = 0; i < files.length; i++) removeSet[String(files[i])] = 1;\n var kept = [];\n var removed = [];\n for (var j = 0; j < directoryFiles[prop].length; j++) {\n var f = directoryFiles[prop][j];\n if (removeSet[f]) removed.push(f);\n else kept.push(f);\n }\n directoryFiles[prop] = kept;\n if (removed.length) callDirectoryRemoved(prop, removed);\n };\n\n // —— 外部指针注入(桌面 underlay 层收不到鼠标事件,父页经 __wp.pushPointer 推入)——\n //\n // 场景壁纸那条通道是「写一个状态对象、渲染器每帧读」(render/pointer.js);网页壁纸\n // 没有这样的单一消费点 —— 作者代码就是**监听 DOM 事件**的,所以这里必须把推送\n // 还原成一串合成事件。语料(本机 49 张 web):mousemove 24 张、click 29 张、\n // mouseover/out 17 张、mouseenter/leave 8 张、pointer* 16 张(createjs 系一律走\n // pointerdown/move/up)、.button 18 张、.which 17 张、pointerId/relatedTarget 15 张。\n //\n // 三条要点(都有语料依据,改错了会静默失效):\n //\n // 1. **必须 elementFromPoint 按命中元素派发**,不能一律打 document。作者既有挂\n // document/window 的(15 张,靠冒泡收到),也有挂 canvas 上读 `event.offsetX`\n // 的(1748506393 流体 `pointers[0].dx = (e.offsetX - …)`)。offsetX/offsetY 由\n // 浏览器按 target 的 padding box 现算 —— target 打错就是错的偏移,且无任何报错。\n // pageX/pageY 同理由 clientX + 滚动量现算,不用我们填。\n //\n // 2. **over/out/enter/leave 链要按 W3C 语义补全**。1748506393 靠 canvas 的\n // `mouseenter` 把 `pointers[0].down` 置 true(不进这个分支则鼠标怎么动都不出染料)、\n // 靠 window 的 `mouseleave` 复位;1081733658 animatedGrid 靠 `document.body` 的\n // mouseover/mouseleave 起停整个网格动画。leave/enter 不冒泡,必须自己沿祖先链走到\n // 最近公共祖先,只发生变化的那一段。\n //\n // 3. **click 要靠 down/up 边缘合成**,且 down 与 up 的 target 不同(拖拽)时不发。\n // 29 张听 click 是最大的消费方;轮询推送里没有「点击」这个事件,只有按键掩码的\n // 跳变,边缘丢了就等于整类交互消失。\n //\n // 硬限制(写在这里避免反复试):CSS `:hover` 由浏览器自己的 hit-test 驱动,合成事件\n // 永远点不亮它(18 张含 `:hover`)—— 纯 CSS hover 动画的壁纸无法用注入通道响应,\n // 这不是实现缺陷,是合成事件的固有边界。\n var ptrHas = false; // 是否收到过推送(首帧 movement 归零用)\n var ptrX = 0;\n var ptrY = 0;\n var ptrButtons = 0;\n var ptrTarget = null; // 上次命中元素(over/out 链的旧端)\n var ptrDownTarget = null; // 按下时的命中元素(click 判定)\n var ptrLastClickTime = 0;\n var ptrLastClickTarget = null;\n /** 双击判定窗口(ms)。与主流浏览器一致,语料里 5 张听 dblclick。 */\n var PTR_DBLCLICK_MS = 500;\n\n function ptrRoot() {\n try {\n return w.document.body || w.document.documentElement || null;\n } catch (_) {\n return null;\n }\n }\n\n function ptrHitTest(x, y) {\n try {\n if (typeof w.document.elementFromPoint === "function") {\n var el = w.document.elementFromPoint(x, y);\n if (el) return el;\n }\n } catch (_) {\n /* 忽略 */\n }\n return ptrRoot();\n }\n\n /** node → [node, parent, …, root];用 parentNode 而非 parentElement,\n * 这样 document / documentElement 也在链里(作者挂 document 的 leave 要收到)。 */\n function ptrChain(node) {\n var out = [];\n var n = node;\n while (n) {\n out.push(n);\n try {\n n = n.parentNode || null;\n } catch (_) {\n n = null;\n }\n }\n return out;\n }\n\n function ptrCommonAncestor(a, b) {\n if (!a || !b) return null;\n var ca = ptrChain(a);\n var seen = [];\n for (var i = 0; i < ca.length; i++) seen.push(ca[i]);\n var cb = ptrChain(b);\n for (var j = 0; j < cb.length; j++) {\n for (var k = 0; k < seen.length; k++) {\n if (seen[k] === cb[j]) return cb[j];\n }\n }\n return null;\n }\n\n /**\n * 造一个合成鼠标/指针事件。\n *\n * `PointerEvent` 优先:createjs 一族(语料 7 张)只挂 pointerdown/move/up,\n * 且会读 `pointerId` / `pointerType` / `isPrimary`。环境没有 PointerEvent 时\n * 退回 MouseEvent(事件名照旧,作者的 addEventListener(\'pointermove\') 仍能收到)。\n */\n function ptrMakeEvent(type, x, y, opts) {\n var o = opts || {};\n var isPointer = type.indexOf("pointer") === 0;\n var init = {\n bubbles: o.bubbles !== false,\n cancelable: o.cancelable !== false,\n // composed:作者把 canvas 放进 shadow DOM 时事件要能穿出来\n composed: true,\n view: w,\n detail: o.detail || 0,\n clientX: x,\n clientY: y,\n // screenX/screenY 是 init 字段(不像 pageX/offsetX 那样现算)。iframe 里\n // 只能按外层窗口原点近似;16 张读 screenX,多用于算相对位移而非绝对定位。\n screenX: x + (Number(w.screenX) || 0),\n screenY: y + (Number(w.screenY) || 0),\n // button:**移动/悬停类事件必须是 -1**,只有 down/up/click 才是 0(左)/1(中)/2(右)。\n // 这条是 W3C 规定的「没有按键状态变化」哨兵值,不是可省的细节:GameMaker HTML5\n // 导出的运行时(2517518192 FNAF)在 pointermove 分支里照抄 `_tq = e.button` 再\n // `_mq |= (1 << _tq)`,而 _mq 只在 pointerup/out 才清零 —— 填 0 等于告诉游戏\n // 「左键一直按着」,鼠标只是移过去就永久卡在按下态(且没有任何报错)。\n button: o.button != null ? o.button : -1,\n buttons: o.buttons != null ? o.buttons : ptrButtons,\n movementX: o.movementX || 0,\n movementY: o.movementY || 0,\n ctrlKey: false,\n shiftKey: false,\n altKey: false,\n metaKey: false,\n };\n if ("relatedTarget" in o) init.relatedTarget = o.relatedTarget || null;\n // `button: -1` 无法经 MouseEvent 构造器表达:Chromium 把 -1 规范化成 0\n // (实测 `new MouseEvent("x", {button:-1}).button === 0`,而 -2 能原样通过 ——\n // 不是钳位,是对 -1 的特殊处理)。PointerEvent 构造器则保留 -1。\n // 所以 mouse 类事件必须在构造后把 -1 盖回去,否则「移动=左键按下」的坑\n // 只在 pointer 路径修好、mouse 路径依旧(2517518192 恰好走 pointer,\n // 光看它会误以为已经修完)。\n var needsButtonPatch = init.button < 0;\n var ev = null;\n if (isPointer) {\n init.pointerId = 1;\n init.pointerType = "mouse";\n init.isPrimary = true;\n init.width = 1;\n init.height = 1;\n init.pressure = init.buttons ? 0.5 : 0;\n try {\n if (typeof w.PointerEvent === "function") ev = new w.PointerEvent(type, init);\n } catch (_) {\n /* 退回 MouseEvent */\n }\n }\n if (!ev) {\n try {\n if (typeof w.MouseEvent === "function") ev = new w.MouseEvent(type, init);\n } catch (_) {\n /* 忽略 */\n }\n }\n if (ev && needsButtonPatch && ev.button !== init.button) {\n try {\n Object.defineProperty(ev, "button", { configurable: true, get: function () {\n return init.button;\n } });\n } catch (_) {\n /* 只读且不可重定义时保持构造值 */\n }\n }\n return ev;\n }\n\n function ptrDispatch(node, type, x, y, opts) {\n if (!node || typeof node.dispatchEvent !== "function") return;\n var ev = ptrMakeEvent(type, x, y, opts);\n if (!ev) return;\n try {\n node.dispatchEvent(ev);\n } catch (_) {\n /* 作者处理器抛错不打断后续事件(与官方 CEF 一致:一个坏 listener 不该\n 让整条链断掉,否则 leave 发不出去会留下永久 hover/按下态) */\n }\n }\n\n /** 命中元素变化时补 out/leave + over/enter 四段,顺序与浏览器一致。 */\n function ptrCrossBoundary(prev, next, x, y) {\n if (prev === next) return;\n var ancestor = ptrCommonAncestor(prev, next);\n if (prev) {\n ptrDispatch(prev, "pointerout", x, y, { relatedTarget: next });\n ptrDispatch(prev, "mouseout", x, y, { relatedTarget: next });\n var leaving = ptrChain(prev);\n for (var i = 0; i < leaving.length; i++) {\n if (leaving[i] === ancestor) break;\n // leave 不冒泡:必须逐个发,且 target 就是它自己\n ptrDispatch(leaving[i], "pointerleave", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: next,\n });\n ptrDispatch(leaving[i], "mouseleave", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: next,\n });\n }\n }\n if (next) {\n ptrDispatch(next, "pointerover", x, y, { relatedTarget: prev });\n ptrDispatch(next, "mouseover", x, y, { relatedTarget: prev });\n var entering = [];\n var chain = ptrChain(next);\n for (var j = 0; j < chain.length; j++) {\n if (chain[j] === ancestor) break;\n entering.push(chain[j]);\n }\n // enter 由外向内(祖先先收到),与浏览器一致\n for (var k = entering.length - 1; k >= 0; k--) {\n ptrDispatch(entering[k], "pointerenter", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: prev,\n });\n ptrDispatch(entering[k], "mouseenter", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: prev,\n });\n }\n }\n }\n\n /**\n * 外部指针注入入口。\n *\n * @param {number} x 相对 iframe 视口左边的 **CSS 像素**(= clientX 空间)\n * @param {number} y 同上,相对上边,Y 朝下\n * @param {number} [buttons] 按键位掩码,bit0 左键。与场景通道同一约定,\n * 当前只消费 bit0(右/中键位保留;桌面右键属于 Finder,不该被壁纸劫持)\n *\n * 接**像素**而不是归一化坐标:网页壁纸的 iframe 在 cover 露底自适配下可能比舞台大\n * 并带居中偏移(见 web.ts installLetterboxFix),换算需要 iframe 的几何 —— 那是父页\n * 才知道的信息,父页换算完再推进来,shim 不做二次除法。\n *\n * 暂停期间丢弃:官方暂停语义是「冻结渲染进程」,此时派发事件会让作者的动画状态\n * 在冻结中继续推进,恢复时画面跳一下。\n */\n w.__wePushPointer = function (x, y, buttons) {\n if (paused) return;\n var nx = Number(x);\n var ny = Number(y);\n // 非有限值直接丢弃(与场景通道同一约定):NaN 传进 clientX 会让 elementFromPoint\n // 返回 null、后续 offsetX 全成 NaN,作者的位移积分会一次性污染成 NaN 且不报错。\n if (!isFinite(nx) || !isFinite(ny)) return;\n var mask = Number(buttons) || 0;\n var moved = !ptrHas || nx !== ptrX || ny !== ptrY;\n var maskChanged = mask !== ptrButtons;\n // 位置与按键都没变就什么都不发:宿主按 ~90Hz 推送,静止时重复派发\n // mousemove 会让作者的「有没有在动」判定(1081733658 网格)永远认为在动。\n if (!moved && !maskChanged) return;\n\n var dx = ptrHas ? nx - ptrX : 0;\n var dy = ptrHas ? ny - ptrY : 0;\n ptrX = nx;\n ptrY = ny;\n ptrHas = true;\n\n var target = ptrHitTest(nx, ny);\n if (moved) {\n ptrCrossBoundary(ptrTarget, target, nx, ny);\n ptrTarget = target;\n ptrDispatch(target, "pointermove", nx, ny, { movementX: dx, movementY: dy });\n ptrDispatch(target, "mousemove", nx, ny, { movementX: dx, movementY: dy });\n } else {\n ptrTarget = target;\n }\n\n if (!maskChanged) return;\n var wasDown = (ptrButtons & 1) !== 0;\n var isDown = (mask & 1) !== 0;\n ptrButtons = mask;\n if (isDown === wasDown) return; // 只有高位变化:当前不消费\n if (isDown) {\n ptrDownTarget = target;\n ptrDispatch(target, "pointerdown", nx, ny, { button: 0, detail: 1 });\n ptrDispatch(target, "mousedown", nx, ny, { button: 0, detail: 1 });\n return;\n }\n ptrDispatch(target, "pointerup", nx, ny, { button: 0, detail: 1 });\n ptrDispatch(target, "mouseup", nx, ny, { button: 0, detail: 1 });\n // click 只在 down/up 落在同一元素上时发(否则是拖拽,浏览器也不发)\n if (ptrDownTarget && ptrDownTarget === target) {\n var now = Date.now();\n var isDouble =\n ptrLastClickTarget === target && now - ptrLastClickTime <= PTR_DBLCLICK_MS;\n ptrDispatch(target, "click", nx, ny, { button: 0, detail: isDouble ? 2 : 1 });\n if (isDouble) {\n ptrDispatch(target, "dblclick", nx, ny, { button: 0, detail: 2 });\n ptrLastClickTarget = null;\n ptrLastClickTime = 0;\n } else {\n ptrLastClickTarget = target;\n ptrLastClickTime = now;\n }\n }\n ptrDownTarget = null;\n };\n\n /**\n * 指针离开本窗口(鼠标去了别的显示器)。\n *\n * 与场景通道不同,这里**必须把 out/leave 链发出去**:场景侧只是清一个状态位,\n * 而网页作者的 hover 态是自己记的,不发 leave 就永久卡在「鼠标还在上面」\n * (1081733658 网格会一直跑、1748506393 的 `pointers[0].down` 一直为 true)。\n * 按下态也要补一次 up,否则拖拽逻辑永远不结束。\n */\n w.__wePointerLeave = function () {\n if ((ptrButtons & 1) !== 0 && ptrTarget) {\n ptrDispatch(ptrTarget, "pointerup", ptrX, ptrY, { button: 0, buttons: 0, detail: 1 });\n ptrDispatch(ptrTarget, "mouseup", ptrX, ptrY, { button: 0, buttons: 0, detail: 1 });\n }\n ptrButtons = 0;\n ptrDownTarget = null;\n if (ptrTarget) {\n ptrCrossBoundary(ptrTarget, null, ptrX, ptrY);\n ptrTarget = null;\n }\n // 位置(ptrX/ptrY)与 ptrHas 保留:下次进来时 movement 才是真实位移,\n // 而不是从 (0,0) 跳过来的一个巨大假 delta。\n };\n\n // —— rAF 节流(带 __weThrottled,避免父页 injectGpuThrottle 双层减半)——\n\n function installRafThrottle() {\n var throttled = function (cb) {\n if (typeof cb !== "function") return 0;\n if (paused) {\n var idHold = ++rafCounter;\n rafMap[idHold] = { kind: "hold", cb: cb };\n return idHold;\n }\n var limit = fps >= 60 ? 0 : 1000 / fps;\n if (limit <= 0) {\n var idNative = origRaf(function (now) {\n delete rafMap[idNative];\n try {\n cb(now);\n } catch (_) {\n /* 忽略 */\n }\n try {\n w.parent.postMessage({ op: "we-frame", t: now }, "*");\n } catch (_) {\n /* 忽略 */\n }\n });\n rafMap[idNative] = { kind: "native", id: idNative };\n return idNative;\n }\n var id = ++rafCounter;\n var to = w.setTimeout(function () {\n delete rafMap[id];\n origRaf(function (now) {\n try {\n cb(now);\n } catch (_) {\n /* 忽略 */\n }\n try {\n w.parent.postMessage({ op: "we-frame", t: now }, "*");\n } catch (_) {\n /* 忽略 */\n }\n });\n }, limit);\n rafMap[id] = { kind: "timeout", to: to };\n return id;\n };\n throttled.__weThrottled = true;\n w.requestAnimationFrame = throttled;\n w.cancelAnimationFrame = function (id) {\n var ent = rafMap[id];\n if (!ent) {\n try {\n origCaf(id);\n } catch (_) {\n /* 忽略 */\n }\n return;\n }\n delete rafMap[id];\n if (ent.kind === "timeout") w.clearTimeout(ent.to);\n else if (ent.kind === "native") origCaf(ent.id);\n };\n }\n\n installRafThrottle();\n})(window);\n\n';
|
|
15351
|
+
function weShimCall(rt, call) {
|
|
15352
|
+
try {
|
|
15353
|
+
const win = rt.iframe?.contentWindow;
|
|
15354
|
+
if (win) call(win);
|
|
15355
|
+
} catch {
|
|
15356
|
+
}
|
|
15357
|
+
}
|
|
15358
|
+
function injectGpuThrottle(rt, f, _doc) {
|
|
15359
|
+
const win = f.contentWindow;
|
|
15360
|
+
if (!win) return;
|
|
15361
|
+
if (win.requestAnimationFrame?.__weThrottled) return;
|
|
15362
|
+
const fps = rt.cfg.sceneFps || 30;
|
|
15363
|
+
if (fps >= 60) return;
|
|
15364
|
+
const interval = 1e3 / fps;
|
|
15365
|
+
try {
|
|
15366
|
+
const origRaf = win.requestAnimationFrame.bind(win);
|
|
15367
|
+
const rafMap = /* @__PURE__ */ new Map();
|
|
15368
|
+
let counter = 0;
|
|
15369
|
+
win.requestAnimationFrame = (cb) => {
|
|
15370
|
+
const id = ++counter;
|
|
15371
|
+
const to = win.setTimeout(() => {
|
|
15372
|
+
rafMap.delete(id);
|
|
15373
|
+
origRaf((now) => {
|
|
15374
|
+
try {
|
|
15375
|
+
cb(now);
|
|
15376
|
+
} catch {
|
|
15377
|
+
}
|
|
15378
|
+
});
|
|
15379
|
+
}, interval);
|
|
15380
|
+
rafMap.set(id, to);
|
|
15381
|
+
return id;
|
|
15382
|
+
};
|
|
15383
|
+
win.cancelAnimationFrame = (id) => {
|
|
15384
|
+
const to = rafMap.get(id);
|
|
15385
|
+
if (to !== void 0) {
|
|
15386
|
+
win.clearTimeout(to);
|
|
15387
|
+
rafMap.delete(id);
|
|
15388
|
+
}
|
|
15389
|
+
};
|
|
15390
|
+
} catch {
|
|
15391
|
+
}
|
|
15392
|
+
}
|
|
15393
|
+
const pumpBuffer = new Float32Array(128);
|
|
15394
|
+
function packWebAudioArrayInto(out, left, right) {
|
|
15395
|
+
const nL = Math.min(64, left.length);
|
|
15396
|
+
const nR = Math.min(64, right.length);
|
|
15397
|
+
for (let i = 0; i < nL; i++) out[i] = Number(left[i]) || 0;
|
|
15398
|
+
for (let i = 0; i < nR; i++) out[64 + i] = Number(right[i]) || 0;
|
|
15399
|
+
return out;
|
|
15400
|
+
}
|
|
15401
|
+
const WEB_SIM_AUDIO_GAIN = 1.8;
|
|
15402
|
+
const WEB_SIM_AUDIO_GAMMA = 1.8;
|
|
15403
|
+
const WEB_AUDIO_PUMP_HZ = 30;
|
|
15404
|
+
function shapeWebAudioBand(pre) {
|
|
15405
|
+
const v = Number(pre) || 0;
|
|
15406
|
+
if (v <= 0) return 0;
|
|
15407
|
+
return Math.min(1, Math.pow(v, WEB_SIM_AUDIO_GAMMA) * WEB_SIM_AUDIO_GAIN);
|
|
15408
|
+
}
|
|
15409
|
+
function defaultAudioDriver() {
|
|
15410
|
+
const sim = createSimulatedAudio();
|
|
15411
|
+
const left = new Float32Array(64);
|
|
15412
|
+
const right = new Float32Array(64);
|
|
15413
|
+
return {
|
|
15414
|
+
tick(nowMs) {
|
|
15415
|
+
sim.update(nowMs / 1e3);
|
|
15416
|
+
},
|
|
15417
|
+
snapshot() {
|
|
15418
|
+
const s = sim.snapshot;
|
|
15419
|
+
const preL = s.preL64;
|
|
15420
|
+
const preR = s.preR64;
|
|
15421
|
+
for (let i = 0; i < 64; i++) {
|
|
15422
|
+
if (preL && preR) {
|
|
15423
|
+
left[i] = shapeWebAudioBand(preL[i]);
|
|
15424
|
+
right[i] = shapeWebAudioBand(preR[i]);
|
|
15425
|
+
} else {
|
|
15426
|
+
left[i] = (Number(s.left64[i]) || 0) * 0.2;
|
|
15427
|
+
right[i] = (Number(s.right64[i]) || 0) * 0.2;
|
|
15428
|
+
}
|
|
15429
|
+
}
|
|
15430
|
+
return { left, right };
|
|
15431
|
+
}
|
|
15432
|
+
};
|
|
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
|
+
}
|
|
15472
|
+
function resolveContainer(rt, cfg) {
|
|
15473
|
+
if (rt.wrap) return rt.wrap;
|
|
15474
|
+
const el = cfg.canvas;
|
|
15475
|
+
if (!el) return null;
|
|
15476
|
+
if (el instanceof HTMLCanvasElement) {
|
|
15477
|
+
const parent = el.parentElement;
|
|
15478
|
+
if (parent) {
|
|
15479
|
+
reportDiag(rt, cfg, "网页壁纸挂在 canvas 父容器上(canvas 不能有子节点;更适合空 div)");
|
|
15480
|
+
return parent;
|
|
15481
|
+
}
|
|
15482
|
+
return null;
|
|
15483
|
+
}
|
|
15484
|
+
return el;
|
|
15485
|
+
}
|
|
15486
|
+
function buildSeedScript(props, fps, volume) {
|
|
15487
|
+
const parts = [];
|
|
15488
|
+
if (fps != null && Number.isFinite(fps)) parts.push(`window.__weSetFps(${Number(fps)});`);
|
|
15489
|
+
if (volume != null && Number.isFinite(volume)) {
|
|
15490
|
+
parts.push(`window.__weSetVolume(${Math.max(0, Math.min(1, Number(volume)))});`);
|
|
15491
|
+
}
|
|
15492
|
+
if (props && Object.keys(props).length) {
|
|
15493
|
+
parts.push(`window.__weSeedProps(${JSON.stringify(props)});`);
|
|
15494
|
+
}
|
|
15495
|
+
return parts.join("\n");
|
|
15496
|
+
}
|
|
15497
|
+
function installLetterboxFix(rt, f, container) {
|
|
15498
|
+
const BASE = "position:absolute;border:none;background:transparent;";
|
|
15499
|
+
const applyFull = () => {
|
|
15500
|
+
f.style.cssText = BASE + "inset:0;width:100%;height:100%;";
|
|
15501
|
+
};
|
|
15502
|
+
applyFull();
|
|
15503
|
+
let lastKey = "";
|
|
15504
|
+
const relayout = () => {
|
|
15505
|
+
if (!f.isConnected) return;
|
|
15506
|
+
let doc = null;
|
|
15507
|
+
try {
|
|
15508
|
+
doc = f.contentDocument;
|
|
15509
|
+
} catch {
|
|
15510
|
+
return;
|
|
15511
|
+
}
|
|
15512
|
+
if (!doc) return;
|
|
15513
|
+
const stageW = container.clientWidth || window.innerWidth || 0;
|
|
15514
|
+
const stageH = container.clientHeight || window.innerHeight || 0;
|
|
15515
|
+
const cover = normalizeFit(rt.cfg.fit) === "cover";
|
|
15516
|
+
applyFull();
|
|
15517
|
+
const box = cover && stageW > 0 && stageH > 0 ? measureWebLetterbox(doc) : null;
|
|
15518
|
+
const vp = box ? webCoverViewport(stageW, stageH, box.contentAspect) : null;
|
|
15519
|
+
const key = vp ? `${Math.round(vp.width)}x${Math.round(vp.height)}` : "full";
|
|
15520
|
+
if (!vp) {
|
|
15521
|
+
lastKey = "full";
|
|
15522
|
+
return;
|
|
15523
|
+
}
|
|
15524
|
+
f.style.cssText = BASE + `left:${vp.left}px;top:${vp.top}px;width:${vp.width}px;height:${vp.height}px;`;
|
|
15525
|
+
if (key !== lastKey) {
|
|
15526
|
+
lastKey = key;
|
|
15527
|
+
reportDiag(
|
|
15528
|
+
rt,
|
|
15529
|
+
rt.cfg,
|
|
15530
|
+
`网页壁纸露底自适配:视口按内容比例改为 ${Math.round(vp.width)}×${Math.round(vp.height)}(cover 居中裁切)`
|
|
15531
|
+
);
|
|
15532
|
+
}
|
|
15533
|
+
};
|
|
15534
|
+
const onResize = () => relayout();
|
|
15535
|
+
window.addEventListener("resize", onResize);
|
|
15536
|
+
let ro;
|
|
15537
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
15538
|
+
ro = new ResizeObserver(() => relayout());
|
|
15539
|
+
ro.observe(container);
|
|
15540
|
+
}
|
|
15541
|
+
const timers = [];
|
|
15542
|
+
const onLoad = () => {
|
|
15543
|
+
relayout();
|
|
15544
|
+
for (const d of [120, 400, 1200]) timers.push(window.setTimeout(relayout, d));
|
|
15545
|
+
try {
|
|
15546
|
+
const doc = f.contentDocument;
|
|
15547
|
+
if (doc) {
|
|
15548
|
+
for (const el of doc.querySelectorAll("video,img")) {
|
|
15549
|
+
el.addEventListener("loadedmetadata", relayout, { once: true });
|
|
15550
|
+
el.addEventListener("load", relayout, { once: true });
|
|
15551
|
+
}
|
|
15552
|
+
}
|
|
15553
|
+
} catch {
|
|
15554
|
+
}
|
|
15555
|
+
};
|
|
15556
|
+
f.addEventListener("load", onLoad);
|
|
15557
|
+
rt.webRelayout = relayout;
|
|
15558
|
+
const prev = rt.sceneCleanup;
|
|
15559
|
+
rt.sceneCleanup = () => {
|
|
15560
|
+
window.removeEventListener("resize", onResize);
|
|
15561
|
+
ro?.disconnect();
|
|
15562
|
+
for (const t of timers) clearTimeout(t);
|
|
15563
|
+
f.removeEventListener("load", onLoad);
|
|
15564
|
+
if (rt.webRelayout === relayout) rt.webRelayout = void 0;
|
|
15565
|
+
try {
|
|
15566
|
+
prev?.();
|
|
15567
|
+
} catch {
|
|
15568
|
+
}
|
|
15569
|
+
};
|
|
15570
|
+
}
|
|
15571
|
+
function webPointerToClient(u, v, stage, frame, client) {
|
|
15572
|
+
if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
|
|
15573
|
+
if (!(stage.width > 0) || !(stage.height > 0)) return null;
|
|
15574
|
+
const sx = frame.width > 0 && client.width > 0 ? frame.width / client.width : 1;
|
|
15575
|
+
const sy = frame.height > 0 && client.height > 0 ? frame.height / client.height : 1;
|
|
15576
|
+
return {
|
|
15577
|
+
x: (u * stage.width - (frame.left - stage.left)) / (sx || 1),
|
|
15578
|
+
y: (v * stage.height - (frame.top - stage.top)) / (sy || 1)
|
|
15579
|
+
};
|
|
15580
|
+
}
|
|
15581
|
+
function installWebPointerBridge(rt, f, container) {
|
|
15582
|
+
rt.pointerCtl = {
|
|
15583
|
+
push(p) {
|
|
15584
|
+
if (!f.isConnected) return;
|
|
15585
|
+
const cRect = container.getBoundingClientRect();
|
|
15586
|
+
const fRect = f.getBoundingClientRect();
|
|
15587
|
+
const pt = webPointerToClient(
|
|
15588
|
+
Number(p?.u),
|
|
15589
|
+
Number(p?.v),
|
|
15590
|
+
{
|
|
15591
|
+
left: cRect.left,
|
|
15592
|
+
top: cRect.top,
|
|
15593
|
+
width: cRect.width || container.clientWidth || window.innerWidth || 0,
|
|
15594
|
+
height: cRect.height || container.clientHeight || window.innerHeight || 0
|
|
15595
|
+
},
|
|
15596
|
+
{ left: fRect.left, top: fRect.top, width: fRect.width, height: fRect.height },
|
|
15597
|
+
{ width: f.clientWidth, height: f.clientHeight }
|
|
15598
|
+
);
|
|
15599
|
+
if (!pt) return;
|
|
15600
|
+
weShimCall(rt, (w) => w.__wePushPointer?.(pt.x, pt.y, Number(p.buttons) || 0));
|
|
15601
|
+
},
|
|
15602
|
+
leave() {
|
|
15603
|
+
weShimCall(rt, (w) => w.__wePointerLeave?.());
|
|
15604
|
+
}
|
|
15605
|
+
};
|
|
15606
|
+
}
|
|
15607
|
+
function attachIframe(rt, cfg, container, src, opts) {
|
|
15608
|
+
const f = document.createElement("iframe");
|
|
15609
|
+
f.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
15610
|
+
f.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:none;background:transparent;";
|
|
15611
|
+
if (!rt.wrap && getComputedStyle(container).position === "static") {
|
|
15612
|
+
container.style.position = "relative";
|
|
15613
|
+
}
|
|
15614
|
+
f.src = src;
|
|
15615
|
+
container.appendChild(f);
|
|
15616
|
+
rt.iframe = f;
|
|
15617
|
+
if (opts.blobUrl) {
|
|
15618
|
+
(rt.objectUrls ??= []).push(opts.blobUrl);
|
|
15619
|
+
}
|
|
15620
|
+
installLetterboxFix(rt, f, container);
|
|
15621
|
+
if (opts.injected) installWebPointerBridge(rt, f, container);
|
|
15622
|
+
const onFrameMsg = (ev) => {
|
|
15623
|
+
if (ev.source !== f.contentWindow) return;
|
|
15624
|
+
const data = ev.data;
|
|
15625
|
+
if (!data || data.op !== "we-frame") return;
|
|
15626
|
+
if (rt.paused) return;
|
|
15627
|
+
const t = typeof data.t === "number" ? data.t : performance.now();
|
|
15628
|
+
if (opts.frameClock) opts.frameClock.last = t;
|
|
15629
|
+
markFrame(rt, t);
|
|
15630
|
+
};
|
|
15631
|
+
window.addEventListener("message", onFrameMsg);
|
|
15632
|
+
const prevCleanup = rt.sceneCleanup;
|
|
15633
|
+
rt.sceneCleanup = () => {
|
|
15634
|
+
window.removeEventListener("message", onFrameMsg);
|
|
15635
|
+
try {
|
|
15636
|
+
prevCleanup?.();
|
|
15637
|
+
} catch {
|
|
15638
|
+
}
|
|
15639
|
+
};
|
|
15640
|
+
f.addEventListener("load", () => {
|
|
15641
|
+
try {
|
|
15642
|
+
const doc = f.contentDocument;
|
|
15643
|
+
if (doc) window.__blockContextMenu?.(doc);
|
|
15644
|
+
if (!opts.injected) injectGpuThrottle(rt, f, doc);
|
|
15645
|
+
} catch {
|
|
15646
|
+
}
|
|
15647
|
+
weShimCall(rt, (w2) => {
|
|
15648
|
+
const wire = {};
|
|
15649
|
+
for (const [k, v] of Object.entries(rt.liveUserProps ?? {})) wire[k] = { value: v };
|
|
15650
|
+
w2.__weApplyProps?.(wire);
|
|
15651
|
+
w2.__weSetFps?.(rt.cfg.sceneFps ?? 60);
|
|
15652
|
+
w2.__weSetVolume?.(rt.cfg.muted === false ? 1 : 0);
|
|
15653
|
+
if (rt.paused) w2.__weSetPaused?.(true);
|
|
15654
|
+
});
|
|
15655
|
+
try {
|
|
15656
|
+
rt.onFirstFrame?.();
|
|
15657
|
+
rt.onFirstFrame = void 0;
|
|
15658
|
+
} catch {
|
|
15659
|
+
}
|
|
15660
|
+
const w = container.clientWidth || window.innerWidth || 1;
|
|
15661
|
+
const h = container.clientHeight || window.innerHeight || 1;
|
|
15662
|
+
try {
|
|
15663
|
+
rt.onSceneInfo?.({
|
|
15664
|
+
width: w,
|
|
15665
|
+
height: h,
|
|
15666
|
+
layerCount: 0,
|
|
15667
|
+
hasModels: false,
|
|
15668
|
+
hasParticles: false,
|
|
15669
|
+
hasText: false
|
|
15670
|
+
});
|
|
15671
|
+
} catch {
|
|
15672
|
+
}
|
|
15673
|
+
});
|
|
15674
|
+
}
|
|
15675
|
+
const WEB_ASPECT_EPS = 5e-3;
|
|
15676
|
+
const WEB_LETTERBOX_MIN_RATIO = 0.01;
|
|
15677
|
+
const WEB_ASPECT_MIN = 0.2;
|
|
15678
|
+
const WEB_ASPECT_MAX = 6;
|
|
15679
|
+
function webCoverViewport(stageW, stageH, contentAspect) {
|
|
15680
|
+
if (!(stageW > 0) || !(stageH > 0) || !(contentAspect > 0)) return null;
|
|
15681
|
+
const stageAspect = stageW / stageH;
|
|
15682
|
+
if (Math.abs(stageAspect - contentAspect) <= WEB_ASPECT_EPS) return null;
|
|
15683
|
+
if (stageAspect < contentAspect) {
|
|
15684
|
+
const width = stageH * contentAspect;
|
|
15685
|
+
return { width, height: stageH, left: (stageW - width) / 2, top: 0 };
|
|
15686
|
+
}
|
|
15687
|
+
const height = stageW / contentAspect;
|
|
15688
|
+
return { width: stageW, height, left: 0, top: (stageH - height) / 2 };
|
|
15689
|
+
}
|
|
15690
|
+
function measureWebLetterbox(doc) {
|
|
15691
|
+
const win = doc.defaultView;
|
|
15692
|
+
if (!win) return null;
|
|
15693
|
+
const vw = win.innerWidth;
|
|
15694
|
+
const vh = win.innerHeight;
|
|
15695
|
+
if (!(vw > 0) || !(vh > 0)) return null;
|
|
15696
|
+
const cands = [...doc.querySelectorAll("video,img")];
|
|
15697
|
+
for (const el of cands) {
|
|
15698
|
+
const r = el.getBoundingClientRect();
|
|
15699
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
15700
|
+
if (r.width < vw * 0.98) continue;
|
|
15701
|
+
if (Math.abs(r.left) > vw * 0.02 || r.top > vh * 0.02) continue;
|
|
15702
|
+
if (vh - r.height < vh * WEB_LETTERBOX_MIN_RATIO) continue;
|
|
15703
|
+
const natW = el.videoWidth || el.naturalWidth || 0;
|
|
15704
|
+
const natH = el.videoHeight || el.naturalHeight || 0;
|
|
15705
|
+
if (!(natW > 0) || !(natH > 0)) continue;
|
|
15706
|
+
const aspect = natW / natH;
|
|
15707
|
+
if (!Number.isFinite(aspect) || aspect < WEB_ASPECT_MIN || aspect > WEB_ASPECT_MAX) continue;
|
|
15708
|
+
return { contentAspect: aspect };
|
|
15709
|
+
}
|
|
15710
|
+
return null;
|
|
15711
|
+
}
|
|
15712
|
+
function vecToCss(v) {
|
|
15713
|
+
if (!v) return "rgb(128,128,128)";
|
|
15714
|
+
const r = Math.round(Math.max(0, Math.min(1, Number(v.x) || 0)) * 255);
|
|
15715
|
+
const g = Math.round(Math.max(0, Math.min(1, Number(v.y) || 0)) * 255);
|
|
15716
|
+
const b = Math.round(Math.max(0, Math.min(1, Number(v.z) || 0)) * 255);
|
|
15717
|
+
return `rgb(${r},${g},${b})`;
|
|
15718
|
+
}
|
|
15719
|
+
function thumbDataUrlFromSnap(snap) {
|
|
15720
|
+
try {
|
|
15721
|
+
const c = document.createElement("canvas");
|
|
15722
|
+
c.width = c.height = 64;
|
|
15723
|
+
const ctx = c.getContext("2d");
|
|
15724
|
+
if (!ctx) return "";
|
|
15725
|
+
const p = snap.primaryColor;
|
|
15726
|
+
const s = snap.secondaryColor;
|
|
15727
|
+
const grd = ctx.createLinearGradient(0, 0, 64, 64);
|
|
15728
|
+
grd.addColorStop(0, vecToCss(p));
|
|
15729
|
+
grd.addColorStop(1, vecToCss(s));
|
|
15730
|
+
ctx.fillStyle = grd;
|
|
15731
|
+
ctx.fillRect(0, 0, 64, 64);
|
|
15732
|
+
return c.toDataURL("image/jpeg", 0.85);
|
|
15733
|
+
} catch {
|
|
15734
|
+
return "";
|
|
15735
|
+
}
|
|
15736
|
+
}
|
|
15737
|
+
function defaultMediaDriver() {
|
|
15738
|
+
return media.createSimulatedMedia();
|
|
15739
|
+
}
|
|
15740
|
+
function pushMediaDiff(rt, prev, snap) {
|
|
15741
|
+
const events = media.diffMediaEvents(prev, snap);
|
|
15742
|
+
for (const { name, event } of events) {
|
|
15743
|
+
if (name === "mediaStatusChanged") {
|
|
15744
|
+
weShimCall(rt, (w) => w.__wePushMedia?.({ op: "status", enabled: !!event.enabled }));
|
|
15745
|
+
} else if (name === "mediaPropertiesChanged") {
|
|
15746
|
+
weShimCall(
|
|
15747
|
+
rt,
|
|
15748
|
+
(w) => w.__wePushMedia?.({
|
|
15749
|
+
op: "properties",
|
|
15750
|
+
title: event.title ?? "",
|
|
15751
|
+
artist: event.artist ?? "",
|
|
15752
|
+
album: event.album ?? "",
|
|
15753
|
+
albumArtist: event.albumArtist ?? ""
|
|
15754
|
+
})
|
|
15755
|
+
);
|
|
15756
|
+
} else if (name === "mediaThumbnailChanged") {
|
|
15757
|
+
const thumb = thumbDataUrlFromSnap(snap);
|
|
15758
|
+
weShimCall(
|
|
15759
|
+
rt,
|
|
15760
|
+
(w) => w.__wePushMedia?.({
|
|
15761
|
+
op: "thumbnail",
|
|
15762
|
+
thumbnail: thumb,
|
|
15763
|
+
hasThumbnail: !!event.hasThumbnail || !!thumb,
|
|
15764
|
+
primaryColor: vecToCss(event.primaryColor),
|
|
15765
|
+
secondaryColor: vecToCss(event.secondaryColor),
|
|
15766
|
+
tertiaryColor: vecToCss(event.tertiaryColor),
|
|
15767
|
+
textColor: vecToCss(event.textColor),
|
|
15768
|
+
highContrastColor: vecToCss(event.highContrastColor)
|
|
15769
|
+
})
|
|
15770
|
+
);
|
|
15771
|
+
} else if (name === "mediaPlaybackChanged") {
|
|
15772
|
+
weShimCall(rt, (w) => w.__wePushMedia?.({ op: "playback", state: Number(event.state) || 0 }));
|
|
15773
|
+
} else if (name === "mediaTimelineChanged") {
|
|
15774
|
+
weShimCall(
|
|
15775
|
+
rt,
|
|
15776
|
+
(w) => w.__wePushMedia?.({
|
|
15777
|
+
op: "timeline",
|
|
15778
|
+
position: Number(event.position) || 0,
|
|
15779
|
+
duration: Number(event.duration) || 0
|
|
15780
|
+
})
|
|
15781
|
+
);
|
|
15782
|
+
}
|
|
15783
|
+
}
|
|
15784
|
+
return media.cloneMediaSnapshot(snap);
|
|
15785
|
+
}
|
|
15786
|
+
function startAudioPump(rt, driver, frameClock, liveHold = { driver: null }) {
|
|
15787
|
+
if (!driver) return;
|
|
15788
|
+
const bridged = bridgeAudioDriver(rt);
|
|
15789
|
+
const pick = () => rt.audioBridge ? bridged : liveHold.driver ?? driver;
|
|
15790
|
+
let raf = 0;
|
|
15791
|
+
let lastPush = 0;
|
|
15792
|
+
const tick = (now) => {
|
|
15793
|
+
raf = requestAnimationFrame(tick);
|
|
15794
|
+
if (rt.paused || !rt.iframe) return;
|
|
15795
|
+
const fps = rt.cfg.sceneFps || 60;
|
|
15796
|
+
const pumpFps = Math.min(Math.max(1, fps), WEB_AUDIO_PUMP_HZ);
|
|
15797
|
+
const interval = 1e3 / pumpFps;
|
|
15798
|
+
if (now - lastPush < interval * 0.85) return;
|
|
15799
|
+
lastPush = now;
|
|
15800
|
+
try {
|
|
15801
|
+
const cur = pick();
|
|
15802
|
+
cur.tick?.(now);
|
|
15803
|
+
const snap = cur.snapshot();
|
|
15804
|
+
const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
|
|
15805
|
+
weShimCall(rt, (w) => w.__wePushAudio?.(arr));
|
|
15806
|
+
if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
|
|
15807
|
+
} catch {
|
|
15808
|
+
}
|
|
15809
|
+
};
|
|
15810
|
+
raf = requestAnimationFrame(tick);
|
|
15811
|
+
const prev = rt.sceneCleanup;
|
|
15812
|
+
rt.sceneCleanup = () => {
|
|
15813
|
+
cancelAnimationFrame(raf);
|
|
15814
|
+
try {
|
|
15815
|
+
prev?.();
|
|
15816
|
+
} catch {
|
|
15817
|
+
}
|
|
15818
|
+
};
|
|
15819
|
+
}
|
|
15820
|
+
function startMediaPump(rt, driver) {
|
|
15821
|
+
if (!driver) return;
|
|
15822
|
+
const pick = () => rt.mediaSource ?? driver;
|
|
15823
|
+
let raf = 0;
|
|
15824
|
+
let lastMedia = null;
|
|
15825
|
+
let lastTick = 0;
|
|
15826
|
+
const tick = (now) => {
|
|
15827
|
+
raf = requestAnimationFrame(tick);
|
|
15828
|
+
if (rt.paused || !rt.iframe) return;
|
|
15829
|
+
if (now - lastTick < 200) return;
|
|
15830
|
+
lastTick = now;
|
|
15831
|
+
try {
|
|
15832
|
+
const cur = pick();
|
|
15833
|
+
cur.update?.(now / 1e3);
|
|
15834
|
+
lastMedia = pushMediaDiff(rt, lastMedia, cur.snapshot);
|
|
15835
|
+
} catch {
|
|
15836
|
+
}
|
|
15837
|
+
};
|
|
15838
|
+
raf = requestAnimationFrame(tick);
|
|
15839
|
+
const prev = rt.sceneCleanup;
|
|
15840
|
+
rt.sceneCleanup = () => {
|
|
15841
|
+
cancelAnimationFrame(raf);
|
|
15842
|
+
try {
|
|
15843
|
+
prev?.();
|
|
15844
|
+
} catch {
|
|
15845
|
+
}
|
|
15846
|
+
};
|
|
15847
|
+
}
|
|
15848
|
+
function installWebCtl(rt) {
|
|
15849
|
+
rt.sceneCtl = {
|
|
15850
|
+
pause() {
|
|
15851
|
+
rt.paused = true;
|
|
15852
|
+
weShimCall(rt, (w) => w.__weSetPaused?.(true));
|
|
15853
|
+
},
|
|
15854
|
+
resume() {
|
|
15855
|
+
rt.paused = false;
|
|
15856
|
+
weShimCall(rt, (w) => w.__weSetPaused?.(false));
|
|
15857
|
+
},
|
|
15858
|
+
applyUserProperties(props) {
|
|
15859
|
+
const flat = { ...rt.liveUserProps ?? {} };
|
|
15860
|
+
for (const [k, v] of Object.entries(props ?? {})) {
|
|
15861
|
+
const val = v && typeof v === "object" && "value" in v ? v.value : v;
|
|
15862
|
+
flat[k] = val;
|
|
15863
|
+
}
|
|
15864
|
+
rt.liveUserProps = flat;
|
|
15865
|
+
weShimCall(rt, (w) => w.__weApplyProps?.(props));
|
|
15866
|
+
}
|
|
15867
|
+
};
|
|
15868
|
+
}
|
|
15869
|
+
function isSameOriginUrl(url) {
|
|
15870
|
+
try {
|
|
15871
|
+
return new URL(url, location.href).origin === location.origin;
|
|
15872
|
+
} catch {
|
|
15873
|
+
return false;
|
|
15874
|
+
}
|
|
15875
|
+
}
|
|
15876
|
+
function projectPropertiesToWire(project) {
|
|
15877
|
+
const props = project?.general?.properties;
|
|
15878
|
+
if (!props || typeof props !== "object") return {};
|
|
15879
|
+
const out = {};
|
|
15880
|
+
for (const [name, def] of Object.entries(props)) {
|
|
15881
|
+
if (!def || typeof def !== "object" || typeof def.type !== "string") continue;
|
|
15882
|
+
const raw = def.value;
|
|
15883
|
+
const type = def.type.toLowerCase();
|
|
15884
|
+
if (raw === null || raw === void 0) {
|
|
15885
|
+
if (type === "file" || type === "directory") {
|
|
15886
|
+
out[name] = { value: "" };
|
|
15887
|
+
continue;
|
|
15888
|
+
}
|
|
15889
|
+
if (!("value" in def)) continue;
|
|
15890
|
+
}
|
|
15891
|
+
out[name] = { value: raw };
|
|
15892
|
+
}
|
|
15893
|
+
return out;
|
|
15894
|
+
}
|
|
15895
|
+
async function fetchProjectWire(entryUrl) {
|
|
15896
|
+
try {
|
|
15897
|
+
const projUrl = new URL("project.json", new URL(entryUrl, location.href));
|
|
15898
|
+
const r = await fetch(projUrl.href, { credentials: "same-origin" });
|
|
15899
|
+
if (!r.ok) return {};
|
|
15900
|
+
return projectPropertiesToWire(await r.json());
|
|
15901
|
+
} catch {
|
|
15902
|
+
return {};
|
|
15903
|
+
}
|
|
15904
|
+
}
|
|
15905
|
+
function mergeLiveIntoWire(defaults, live) {
|
|
15906
|
+
const out = { ...defaults };
|
|
15907
|
+
if (live) {
|
|
15908
|
+
for (const [k, v] of Object.entries(live)) out[k] = { value: v };
|
|
15909
|
+
}
|
|
15910
|
+
return out;
|
|
15911
|
+
}
|
|
15912
|
+
function mountWeb(rt, cfg) {
|
|
15913
|
+
clear(rt);
|
|
15914
|
+
rt.cfg = cfg;
|
|
15915
|
+
const container = resolveContainer(rt, cfg);
|
|
15916
|
+
if (!container) {
|
|
15917
|
+
reportDiag(rt, cfg, "网页壁纸:无可用容器");
|
|
15918
|
+
rt.onError?.(new Error("网页壁纸:无可用容器"));
|
|
15919
|
+
return;
|
|
15920
|
+
}
|
|
15921
|
+
const entry = cfg.src ?? "";
|
|
15922
|
+
if (!entry) {
|
|
15923
|
+
reportDiag(rt, cfg, "网页壁纸:缺少 src");
|
|
15924
|
+
rt.onError?.(new Error("网页壁纸:缺少 src"));
|
|
15925
|
+
return;
|
|
15926
|
+
}
|
|
15927
|
+
installWebCtl(rt);
|
|
15928
|
+
const cfgExt = cfg;
|
|
15929
|
+
const audioDriver = cfgExt._webAudio === null || rt.audioDisabled ? null : cfgExt._webAudio ?? defaultAudioDriver();
|
|
15930
|
+
const mediaDriver = cfgExt._webMedia === null || rt.mediaDisabled ? null : cfgExt._webMedia ?? defaultMediaDriver();
|
|
15931
|
+
const finishBare = (why) => {
|
|
15932
|
+
reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
|
|
15933
|
+
attachIframe(rt, cfg, container, entry, { injected: false });
|
|
15934
|
+
startAudioPump(rt, null);
|
|
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));
|
|
15945
|
+
};
|
|
15946
|
+
const frameClock = { last: 0 };
|
|
15947
|
+
const liveHold = { driver: null };
|
|
15948
|
+
const startPumps = () => {
|
|
15949
|
+
startAudioPump(rt, audioDriver, frameClock, liveHold);
|
|
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
|
+
}
|
|
15988
|
+
};
|
|
15989
|
+
void (async () => {
|
|
15990
|
+
const defaults = await fetchProjectWire(entry);
|
|
15991
|
+
const wire = mergeLiveIntoWire(defaults, rt.liveUserProps);
|
|
15992
|
+
rt.liveUserProps = Object.fromEntries(Object.entries(wire).map(([k, w]) => [k, w.value]));
|
|
15993
|
+
if (isSameOriginUrl(entry)) {
|
|
15994
|
+
attachIframe(rt, cfg, container, entry, { injected: true, frameClock });
|
|
15995
|
+
startPumps();
|
|
15996
|
+
const f = rt.iframe;
|
|
15997
|
+
f?.addEventListener(
|
|
15998
|
+
"load",
|
|
15999
|
+
() => {
|
|
16000
|
+
let hasShim = false;
|
|
16001
|
+
weShimCall(rt, (w) => {
|
|
16002
|
+
hasShim = typeof w.__weSetPaused === "function";
|
|
16003
|
+
});
|
|
16004
|
+
if (!hasShim) {
|
|
16005
|
+
reportDiag(
|
|
16006
|
+
rt,
|
|
16007
|
+
cfg,
|
|
16008
|
+
"网页壁纸:同源入口未检测到 WE shim(host 未注入?);Spine 类壁纸请确认 /web/ HTML 改写"
|
|
16009
|
+
);
|
|
16010
|
+
}
|
|
16011
|
+
},
|
|
16012
|
+
{ once: true }
|
|
16013
|
+
);
|
|
16014
|
+
return;
|
|
16015
|
+
}
|
|
16016
|
+
try {
|
|
16017
|
+
const res = await fetch(entry, { credentials: "same-origin" });
|
|
16018
|
+
if (!res.ok) {
|
|
16019
|
+
finishBare(`HTTP ${res.status}`);
|
|
16020
|
+
return;
|
|
16021
|
+
}
|
|
16022
|
+
const html = await res.text();
|
|
16023
|
+
if (hasBlockingCsp(html)) {
|
|
16024
|
+
finishBare("CSP 阻止 inline script");
|
|
16025
|
+
return;
|
|
16026
|
+
}
|
|
16027
|
+
const rewritten = rewriteHtml(html, shimSource, {
|
|
16028
|
+
baseHref: entryDirUrl(entry),
|
|
16029
|
+
seedScript: buildSeedScript(wire, cfg.sceneFps, cfg.muted === false ? 1 : 0)
|
|
16030
|
+
});
|
|
16031
|
+
const blob = new Blob([rewritten], { type: "text/html;charset=utf-8" });
|
|
16032
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
16033
|
+
attachIframe(rt, cfg, container, blobUrl, { blobUrl, injected: true, frameClock });
|
|
16034
|
+
startPumps();
|
|
16035
|
+
} catch (e) {
|
|
16036
|
+
finishBare(e instanceof Error ? e.message : String(e));
|
|
16037
|
+
}
|
|
16038
|
+
})();
|
|
16039
|
+
}
|
|
14326
16040
|
function mountWallpaper(rt, cfg) {
|
|
14327
|
-
|
|
16041
|
+
const type = String(cfg.type ?? "").toLowerCase();
|
|
16042
|
+
cfg = { ...cfg, type };
|
|
16043
|
+
rt.cfg = cfg;
|
|
16044
|
+
if ((type === "video" || type === "gif" || type === "image") && cfg.src) {
|
|
14328
16045
|
mountMedia(rt, cfg);
|
|
14329
|
-
} else if (
|
|
16046
|
+
} else if (type === "scene" && (cfg.source || cfg.src)) {
|
|
14330
16047
|
mountScene(rt, cfg);
|
|
16048
|
+
} else if (type === "web" && cfg.src) {
|
|
16049
|
+
mountWeb(rt, cfg);
|
|
14331
16050
|
} else {
|
|
14332
16051
|
rt.onUnhandledType?.(cfg);
|
|
14333
16052
|
}
|
|
14334
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
|
+
}
|
|
14335
16147
|
function normalizeFitOption(fit) {
|
|
14336
16148
|
if (fit === "fit") return "contain";
|
|
14337
16149
|
if (fit === "fill") return "cover";
|
|
14338
16150
|
return fit === "contain" || fit === "stretch" ? fit : "cover";
|
|
14339
16151
|
}
|
|
14340
|
-
function
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
16152
|
+
function isWebProject(project) {
|
|
16153
|
+
const t = project?.type;
|
|
16154
|
+
return typeof t === "string" && t.toLowerCase() === "web";
|
|
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
|
+
}
|
|
16163
|
+
function ensureSceneCanvas(el) {
|
|
16164
|
+
if (el instanceof HTMLCanvasElement) return el;
|
|
16165
|
+
const existing = el.querySelector(":scope > canvas[data-webwallgl]");
|
|
16166
|
+
if (existing instanceof HTMLCanvasElement) return existing;
|
|
16167
|
+
const c = document.createElement("canvas");
|
|
16168
|
+
c.setAttribute("data-webwallgl", "1");
|
|
16169
|
+
c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
|
|
16170
|
+
if (getComputedStyle(el).position === "static") el.style.position = "relative";
|
|
16171
|
+
el.appendChild(c);
|
|
16172
|
+
return c;
|
|
16173
|
+
}
|
|
16174
|
+
async function resolveMountConfig(el, o) {
|
|
16175
|
+
const base = {
|
|
14345
16176
|
fit: normalizeFitOption(o.fit),
|
|
14346
16177
|
renderDpr: o.renderDpr ?? 1,
|
|
14347
16178
|
sceneFps: o.fps ?? 60,
|
|
14348
16179
|
muted: (o.volume ?? 0) <= 0,
|
|
14349
|
-
loop: true
|
|
16180
|
+
loop: true,
|
|
16181
|
+
canvas: el,
|
|
16182
|
+
source: o.source
|
|
14350
16183
|
};
|
|
16184
|
+
let project = null;
|
|
16185
|
+
try {
|
|
16186
|
+
project = await o.source.project?.() ?? null;
|
|
16187
|
+
} catch {
|
|
16188
|
+
project = null;
|
|
16189
|
+
}
|
|
16190
|
+
if (isWebProject(project)) {
|
|
16191
|
+
let url;
|
|
16192
|
+
try {
|
|
16193
|
+
const entry = await o.source.webEntry?.();
|
|
16194
|
+
url = entry?.url;
|
|
16195
|
+
} catch {
|
|
16196
|
+
url = void 0;
|
|
16197
|
+
}
|
|
16198
|
+
if (!url && o.source.key) {
|
|
16199
|
+
const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") || "index.html" : "index.html";
|
|
16200
|
+
url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
|
|
16201
|
+
}
|
|
16202
|
+
if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
|
|
16203
|
+
return { ...base, type: "web", src: url, source: o.source };
|
|
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
|
+
}
|
|
16246
|
+
const canvas = ensureSceneCanvas(el);
|
|
16247
|
+
return { ...base, type: "scene", canvas, source: o.source };
|
|
14351
16248
|
}
|
|
14352
|
-
function createScene(
|
|
16249
|
+
function createScene(el, options) {
|
|
14353
16250
|
const rt = createRuntime();
|
|
14354
16251
|
const events = { ready: [], error: [], diagnostic: [] };
|
|
14355
16252
|
let currentOptions = { ...options ?? {}, source: null };
|
|
16253
|
+
let boundEl = el;
|
|
14356
16254
|
const emitError = (err) => {
|
|
14357
16255
|
for (const fn of events.error) {
|
|
14358
16256
|
try {
|
|
@@ -14384,6 +16282,26 @@ function createScene(canvas, options) {
|
|
|
14384
16282
|
rt.onSceneInfo = (info) => {
|
|
14385
16283
|
rt.info = info;
|
|
14386
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
|
+
};
|
|
14387
16305
|
};
|
|
14388
16306
|
const armFirstFrame = () => {
|
|
14389
16307
|
return new Promise((resolve) => {
|
|
@@ -14391,6 +16309,24 @@ function createScene(canvas, options) {
|
|
|
14391
16309
|
rt.onFirstFrame = () => {
|
|
14392
16310
|
rt.onFirstFrame = void 0;
|
|
14393
16311
|
prev?.();
|
|
16312
|
+
const info = rt.info ?? {
|
|
16313
|
+
width: 0,
|
|
16314
|
+
height: 0,
|
|
16315
|
+
layerCount: 0,
|
|
16316
|
+
hasModels: false,
|
|
16317
|
+
hasParticles: false,
|
|
16318
|
+
hasText: false
|
|
16319
|
+
};
|
|
16320
|
+
try {
|
|
16321
|
+
currentOptions.onReady?.(info);
|
|
16322
|
+
} catch {
|
|
16323
|
+
}
|
|
16324
|
+
for (const fn of events.ready) {
|
|
16325
|
+
try {
|
|
16326
|
+
fn(info);
|
|
16327
|
+
} catch {
|
|
16328
|
+
}
|
|
16329
|
+
}
|
|
14394
16330
|
resolve();
|
|
14395
16331
|
};
|
|
14396
16332
|
});
|
|
@@ -14404,7 +16340,9 @@ function createScene(canvas, options) {
|
|
|
14404
16340
|
return { promise, off };
|
|
14405
16341
|
};
|
|
14406
16342
|
const instance = {
|
|
14407
|
-
canvas
|
|
16343
|
+
get canvas() {
|
|
16344
|
+
return boundEl;
|
|
16345
|
+
},
|
|
14408
16346
|
pause() {
|
|
14409
16347
|
rt.paused = true;
|
|
14410
16348
|
rt.sceneCtl?.pause();
|
|
@@ -14420,14 +16358,17 @@ function createScene(canvas, options) {
|
|
|
14420
16358
|
setFit(fit) {
|
|
14421
16359
|
rt.cfg.fit = fit;
|
|
14422
16360
|
resetCoverAlign(rt);
|
|
16361
|
+
rt.webRelayout?.();
|
|
14423
16362
|
},
|
|
14424
16363
|
setFps(fps) {
|
|
14425
16364
|
rt.cfg.sceneFps = fps;
|
|
16365
|
+
weShimCall(rt, (w) => w.__weSetFps?.(fps));
|
|
14426
16366
|
},
|
|
14427
16367
|
setVolume(volume) {
|
|
14428
16368
|
const v = Math.max(0, Math.min(1, volume));
|
|
14429
16369
|
rt.cfg.muted = v <= 0;
|
|
14430
16370
|
rt.sceneAudio?.setVolume(v);
|
|
16371
|
+
weShimCall(rt, (w) => w.__weSetVolume?.(v));
|
|
14431
16372
|
},
|
|
14432
16373
|
setRenderDpr(dpr) {
|
|
14433
16374
|
rt.cfg.renderDpr = dpr;
|
|
@@ -14441,20 +16382,90 @@ function createScene(canvas, options) {
|
|
|
14441
16382
|
getProperties() {
|
|
14442
16383
|
return { ...rt.liveUserProps ?? {} };
|
|
14443
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
|
+
},
|
|
14444
16448
|
async load(source) {
|
|
16449
|
+
const prev = currentOptions.source;
|
|
16450
|
+
if (prev && prev !== source) {
|
|
16451
|
+
try {
|
|
16452
|
+
prev.dispose?.();
|
|
16453
|
+
} catch {
|
|
16454
|
+
}
|
|
16455
|
+
}
|
|
14445
16456
|
currentOptions = { ...currentOptions, source };
|
|
14446
16457
|
wireOptions(currentOptions);
|
|
14447
|
-
const cfg =
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
src: void 0,
|
|
14452
|
-
mediaBase: void 0
|
|
14453
|
-
};
|
|
16458
|
+
const cfg = await resolveMountConfig(boundEl, currentOptions);
|
|
16459
|
+
if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
|
|
16460
|
+
boundEl = cfg.canvas;
|
|
16461
|
+
}
|
|
14454
16462
|
rt.cfg = cfg;
|
|
14455
16463
|
rt.paused = false;
|
|
14456
16464
|
rt.info = void 0;
|
|
14457
16465
|
resetCoverAlign(rt);
|
|
16466
|
+
if (currentOptions.properties) {
|
|
16467
|
+
rt.liveUserProps = { ...currentOptions.properties };
|
|
16468
|
+
}
|
|
14458
16469
|
const firstFrame = armFirstFrame();
|
|
14459
16470
|
const failure = armFailure();
|
|
14460
16471
|
mountWallpaper(rt, cfg);
|
|
@@ -14472,6 +16483,10 @@ function createScene(canvas, options) {
|
|
|
14472
16483
|
},
|
|
14473
16484
|
destroy() {
|
|
14474
16485
|
destroyRuntime(rt);
|
|
16486
|
+
try {
|
|
16487
|
+
currentOptions.source?.dispose?.();
|
|
16488
|
+
} catch {
|
|
16489
|
+
}
|
|
14475
16490
|
rt.onDiagnostic = void 0;
|
|
14476
16491
|
rt.onError = void 0;
|
|
14477
16492
|
rt.onFirstFrame = void 0;
|
|
@@ -14498,10 +16513,17 @@ function createScene(canvas, options) {
|
|
|
14498
16513
|
const applyOptions = async (o) => {
|
|
14499
16514
|
currentOptions = o;
|
|
14500
16515
|
wireOptions(o);
|
|
14501
|
-
|
|
14502
|
-
|
|
16516
|
+
const cfg = await resolveMountConfig(el, o);
|
|
16517
|
+
if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
|
|
16518
|
+
boundEl = cfg.canvas;
|
|
16519
|
+
}
|
|
16520
|
+
rt.cfg = cfg;
|
|
16521
|
+
rt.paused = false;
|
|
14503
16522
|
rt.info = void 0;
|
|
14504
16523
|
resetCoverAlign(rt);
|
|
16524
|
+
if (o.properties && Object.keys(o.properties).length) {
|
|
16525
|
+
rt.liveUserProps = { ...o.properties };
|
|
16526
|
+
}
|
|
14505
16527
|
const firstFrame = armFirstFrame();
|
|
14506
16528
|
const failure = armFailure();
|
|
14507
16529
|
mountWallpaper(rt, rt.cfg);
|
|
@@ -14510,23 +16532,28 @@ function createScene(canvas, options) {
|
|
|
14510
16532
|
} finally {
|
|
14511
16533
|
failure.off();
|
|
14512
16534
|
}
|
|
16535
|
+
if (o.autoplay === false) instance.pause();
|
|
14513
16536
|
if ((o.volume ?? 0) > 0) instance.setVolume(o.volume);
|
|
14514
16537
|
if (o.properties && Object.keys(o.properties).length) instance.setProperties(o.properties);
|
|
14515
16538
|
};
|
|
14516
16539
|
instance.__applyOptions = applyOptions;
|
|
14517
16540
|
return instance;
|
|
14518
16541
|
}
|
|
14519
|
-
async function mount(
|
|
14520
|
-
const instance = createScene(
|
|
16542
|
+
async function mount(el, options) {
|
|
16543
|
+
const instance = createScene(el, options);
|
|
14521
16544
|
const withApply = instance;
|
|
14522
16545
|
await withApply.__applyOptions(options);
|
|
14523
16546
|
return instance;
|
|
14524
16547
|
}
|
|
14525
16548
|
export {
|
|
14526
16549
|
bytesSource,
|
|
16550
|
+
createMediaSource,
|
|
14527
16551
|
createScene,
|
|
14528
16552
|
fileSource,
|
|
14529
16553
|
httpSource,
|
|
14530
|
-
|
|
16554
|
+
mediaColor,
|
|
16555
|
+
mediaSource,
|
|
16556
|
+
mount,
|
|
16557
|
+
sniffMediaType
|
|
14531
16558
|
};
|
|
14532
16559
|
//# sourceMappingURL=webwallgl.mjs.map
|