webwallgl 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +78 -3
- package/README.md +77 -3
- package/package.json +1 -1
- package/types.d.ts +9 -1
- package/webwallgl.d.ts +2 -2
- package/webwallgl.global.js +1548 -139
- 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 +1548 -139
- package/webwallgl.mjs.map +1 -1
package/webwallgl.global.js
CHANGED
|
@@ -33,7 +33,20 @@
|
|
|
33
33
|
window.__blockContextMenu = (doc) => doc.addEventListener("contextmenu", block, true);
|
|
34
34
|
})();
|
|
35
35
|
function clear(rt) {
|
|
36
|
-
if (rt.
|
|
36
|
+
if (rt.iframe) {
|
|
37
|
+
try {
|
|
38
|
+
rt.iframe.contentWindow?.location.replace("about:blank");
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (rt.wrap) {
|
|
43
|
+
rt.wrap.innerHTML = "";
|
|
44
|
+
} else if (rt.iframe?.isConnected) {
|
|
45
|
+
try {
|
|
46
|
+
rt.iframe.remove();
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
37
50
|
if (rt.raf !== void 0) cancelAnimationFrame(rt.raf);
|
|
38
51
|
rt.raf = void 0;
|
|
39
52
|
for (const p of rt.videoPairs ?? []) p.destroy();
|
|
@@ -41,6 +54,7 @@
|
|
|
41
54
|
if (rt.sceneCleanup) rt.sceneCleanup();
|
|
42
55
|
rt.sceneCleanup = void 0;
|
|
43
56
|
rt.sceneCtl = void 0;
|
|
57
|
+
rt.pointerCtl = void 0;
|
|
44
58
|
if (rt.renderer) {
|
|
45
59
|
rt.renderer.dispose?.();
|
|
46
60
|
rt.renderer = void 0;
|
|
@@ -124,7 +138,7 @@
|
|
|
124
138
|
} catch {
|
|
125
139
|
}
|
|
126
140
|
try {
|
|
127
|
-
const origin = cfg.mediaBase ? new URL(cfg.mediaBase).origin : "";
|
|
141
|
+
const origin = cfg.mediaBase ? new URL(cfg.mediaBase, window.location.href).origin : "";
|
|
128
142
|
if (origin) {
|
|
129
143
|
const img = new Image();
|
|
130
144
|
img.src = `${origin}/diag?msg=${encodeURIComponent(`scene ${cfg.src ?? "?"}: ${msg.slice(0, 500)}`)}`;
|
|
@@ -867,6 +881,31 @@
|
|
|
867
881
|
}
|
|
868
882
|
return dflt;
|
|
869
883
|
}
|
|
884
|
+
function isRenderInert(o) {
|
|
885
|
+
if (!o) return false;
|
|
886
|
+
return !o.image && !o.model && !o.particle && o.text == null && !o.size;
|
|
887
|
+
}
|
|
888
|
+
function composeChildTransform(parentWorld, childLocal, parentScalePropagates) {
|
|
889
|
+
const pscale = parentScalePropagates ? parentWorld.scale : [1, 1, 1];
|
|
890
|
+
const ca = (parentWorld.angles[2] || 0) * Math.PI / 180;
|
|
891
|
+
const cos = Math.cos(ca);
|
|
892
|
+
const sin = Math.sin(ca);
|
|
893
|
+
const ox = childLocal.origin[0] * pscale[0];
|
|
894
|
+
const oy = childLocal.origin[1] * pscale[1];
|
|
895
|
+
return {
|
|
896
|
+
origin: [
|
|
897
|
+
parentWorld.origin[0] + ox * cos - oy * sin,
|
|
898
|
+
parentWorld.origin[1] + ox * sin + oy * cos,
|
|
899
|
+
parentWorld.origin[2] + (childLocal.origin[2] || 0)
|
|
900
|
+
],
|
|
901
|
+
scale: [
|
|
902
|
+
pscale[0] * childLocal.scale[0],
|
|
903
|
+
pscale[1] * childLocal.scale[1],
|
|
904
|
+
pscale[2] * childLocal.scale[2]
|
|
905
|
+
],
|
|
906
|
+
angles: [childLocal.angles[0], childLocal.angles[1], (parentWorld.angles[2] || 0) + childLocal.angles[2]]
|
|
907
|
+
};
|
|
908
|
+
}
|
|
870
909
|
function parseScene(sceneJson, project) {
|
|
871
910
|
const properties = project && project.general && project.general.properties || {};
|
|
872
911
|
const objects = sceneJson.objects || [];
|
|
@@ -883,6 +922,11 @@
|
|
|
883
922
|
scale: parseVec3(o.scale || "1 1 1"),
|
|
884
923
|
angles: parseVec3(o.angles || "0 0 0")
|
|
885
924
|
}));
|
|
925
|
+
const localSnapshot = local.map((c) => ({
|
|
926
|
+
origin: c.origin.slice(),
|
|
927
|
+
scale: c.scale.slice(),
|
|
928
|
+
angles: c.angles.slice()
|
|
929
|
+
}));
|
|
886
930
|
for (let pass = 0; pass < 8; pass++) {
|
|
887
931
|
let changed = false;
|
|
888
932
|
for (const c of local) {
|
|
@@ -896,20 +940,11 @@
|
|
|
896
940
|
const pr = objects[pIdx];
|
|
897
941
|
const prs = pr.scale;
|
|
898
942
|
const runtimeBound = prs !== null && typeof prs === "object" && (typeof prs.script === "string" || prs.user !== void 0);
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const ox = c.origin[0] * pscale[0];
|
|
905
|
-
const oy = c.origin[1] * pscale[1];
|
|
906
|
-
c.origin[0] = pc.origin[0] + ox * cos - oy * sin;
|
|
907
|
-
c.origin[1] = pc.origin[1] + ox * sin + oy * cos;
|
|
908
|
-
c.origin[2] = pc.origin[2] + c.origin[2];
|
|
909
|
-
c.angles[2] = pc.angles[2] + c.angles[2];
|
|
910
|
-
c.scale[0] = pscale[0] * c.scale[0];
|
|
911
|
-
c.scale[1] = pscale[1] * c.scale[1];
|
|
912
|
-
c.scale[2] = pscale[2] * c.scale[2];
|
|
943
|
+
const propagateScale = !(runtimeBound && isRenderInert(pr));
|
|
944
|
+
const w = composeChildTransform(pc, c, propagateScale);
|
|
945
|
+
c.origin = w.origin;
|
|
946
|
+
c.scale = w.scale;
|
|
947
|
+
c.angles = w.angles;
|
|
913
948
|
c.parent = null;
|
|
914
949
|
changed = true;
|
|
915
950
|
}
|
|
@@ -1129,6 +1164,18 @@
|
|
|
1129
1164
|
origin: layerOrigin,
|
|
1130
1165
|
scale: world.scale,
|
|
1131
1166
|
angles: world.angles,
|
|
1167
|
+
// [we-scene patch] 父级相对变换(WE 场景图的真实语义)。origin/scale/angles
|
|
1168
|
+
// 上的脚本与关键帧动画一律在这层空间收发,再由 recomposeWorld 合成回上面的
|
|
1169
|
+
// world 三件套。渲染 / hittest / getTransformMatrix 仍只读 world,不受影响。
|
|
1170
|
+
// isPostProcess 层的 world 被强制成整幅画布,local 对它无意义(recompose 跳过)。
|
|
1171
|
+
localOrigin: localSnapshot[i].origin,
|
|
1172
|
+
localScale: localSnapshot[i].scale,
|
|
1173
|
+
localAngles: localSnapshot[i].angles,
|
|
1174
|
+
// 「渲染惰性纯容器」:父 scale 是否传给子层由父级这个标志决定,
|
|
1175
|
+
// 判据与 parse 合并阶段逐字相同(见 isRenderInert)。
|
|
1176
|
+
renderInert: isRenderInert(o),
|
|
1177
|
+
// 父 scale 绑了脚本/用户属性(运行时可变)。与 renderInert 一起决定传播闸门。
|
|
1178
|
+
scaleRuntimeBound: !!(o.scale !== null && typeof o.scale === "object" && (typeof o.scale.script === "string" || o.scale.user !== void 0)),
|
|
1132
1179
|
size: layerSize,
|
|
1133
1180
|
alignment: o.alignment || "center",
|
|
1134
1181
|
color: parseColor(o.color),
|
|
@@ -1239,15 +1286,110 @@
|
|
|
1239
1286
|
cropoffset: modelJson.cropoffset ? parseVec2(modelJson.cropoffset) : null
|
|
1240
1287
|
};
|
|
1241
1288
|
}
|
|
1289
|
+
function recomposeWorld(layers, dirty) {
|
|
1290
|
+
if (!layers || layers.length === 0) return;
|
|
1291
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1292
|
+
for (const l of layers) {
|
|
1293
|
+
if (l && l.id !== void 0 && l.id !== null) byId.set(l.id, l);
|
|
1294
|
+
}
|
|
1295
|
+
const depthOf = (l) => {
|
|
1296
|
+
let d = 0;
|
|
1297
|
+
let p = l.parentId;
|
|
1298
|
+
for (let guard = 0; p !== void 0 && p !== null && guard < 64; guard++) {
|
|
1299
|
+
const parent = byId.get(p);
|
|
1300
|
+
if (!parent) break;
|
|
1301
|
+
d++;
|
|
1302
|
+
p = parent.parentId;
|
|
1303
|
+
}
|
|
1304
|
+
return d;
|
|
1305
|
+
};
|
|
1306
|
+
const targets = [];
|
|
1307
|
+
for (const l of layers) {
|
|
1308
|
+
if (!l || !l.localOrigin) continue;
|
|
1309
|
+
if (l.isPostProcess) continue;
|
|
1310
|
+
if (dirty && !dirty.has(l.id)) continue;
|
|
1311
|
+
targets.push(l);
|
|
1312
|
+
}
|
|
1313
|
+
targets.sort((a, b) => depthOf(a) - depthOf(b));
|
|
1314
|
+
for (const l of targets) {
|
|
1315
|
+
const parent = l.parentId !== void 0 && l.parentId !== null ? byId.get(l.parentId) : null;
|
|
1316
|
+
let w;
|
|
1317
|
+
if (!parent) {
|
|
1318
|
+
w = { origin: l.localOrigin.slice(), scale: l.localScale.slice(), angles: l.localAngles.slice() };
|
|
1319
|
+
} else {
|
|
1320
|
+
const propagateScale = !(parent.scaleRuntimeBound && parent.renderInert);
|
|
1321
|
+
w = composeChildTransform(
|
|
1322
|
+
{ origin: parent.origin, scale: parent.scale, angles: parent.angles },
|
|
1323
|
+
{ origin: l.localOrigin, scale: l.localScale, angles: l.localAngles },
|
|
1324
|
+
propagateScale
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
const d = l.attachBindDelta;
|
|
1328
|
+
if (d) {
|
|
1329
|
+
w.origin[0] += d[0];
|
|
1330
|
+
w.origin[1] += d[1];
|
|
1331
|
+
}
|
|
1332
|
+
l.origin[0] = w.origin[0];
|
|
1333
|
+
l.origin[1] = w.origin[1];
|
|
1334
|
+
l.origin[2] = w.origin[2];
|
|
1335
|
+
l.scale[0] = w.scale[0];
|
|
1336
|
+
l.scale[1] = w.scale[1];
|
|
1337
|
+
l.scale[2] = w.scale[2];
|
|
1338
|
+
l.angles[0] = w.angles[0];
|
|
1339
|
+
l.angles[1] = w.angles[1];
|
|
1340
|
+
l.angles[2] = w.angles[2];
|
|
1341
|
+
if (l.attachBase) {
|
|
1342
|
+
l.attachBase[0] = w.origin[0];
|
|
1343
|
+
l.attachBase[1] = w.origin[1];
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function collectTransformDirty(layers, extraSeeds) {
|
|
1348
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
1349
|
+
if (!layers || layers.length === 0) return dirty;
|
|
1350
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1351
|
+
for (const l of layers) {
|
|
1352
|
+
if (!l || l.parentId === void 0 || l.parentId === null) continue;
|
|
1353
|
+
const list = childrenOf.get(l.parentId);
|
|
1354
|
+
if (list) list.push(l);
|
|
1355
|
+
else childrenOf.set(l.parentId, [l]);
|
|
1356
|
+
}
|
|
1357
|
+
const TRANSFORM_FIELDS = ["origin", "scale", "angles"];
|
|
1358
|
+
const seeds = [];
|
|
1359
|
+
for (const l of layers) {
|
|
1360
|
+
if (!l || l.id === void 0 || l.id === null) continue;
|
|
1361
|
+
const scripts = l.objectScripts || null;
|
|
1362
|
+
const anims = l.objectAnimations || null;
|
|
1363
|
+
const bound = TRANSFORM_FIELDS.some((f) => scripts && scripts[f] || anims && anims[f]);
|
|
1364
|
+
if (bound) seeds.push(l);
|
|
1365
|
+
}
|
|
1366
|
+
if (extraSeeds) {
|
|
1367
|
+
for (const l of extraSeeds) if (l && l.id !== void 0 && l.id !== null) seeds.push(l);
|
|
1368
|
+
}
|
|
1369
|
+
const stack = seeds.slice();
|
|
1370
|
+
for (let guard = 0; stack.length > 0 && guard < 1e5; guard++) {
|
|
1371
|
+
const l = stack.pop();
|
|
1372
|
+
if (!l || l.id === void 0 || l.id === null) continue;
|
|
1373
|
+
if (dirty.has(l.id)) continue;
|
|
1374
|
+
dirty.add(l.id);
|
|
1375
|
+
const kids = childrenOf.get(l.id);
|
|
1376
|
+
if (kids) for (const c of kids) stack.push(c);
|
|
1377
|
+
}
|
|
1378
|
+
return dirty;
|
|
1379
|
+
}
|
|
1242
1380
|
const sceneMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1243
1381
|
__proto__: null,
|
|
1244
1382
|
applySolidFromModel,
|
|
1383
|
+
collectTransformDirty,
|
|
1384
|
+
composeChildTransform,
|
|
1385
|
+
isRenderInert,
|
|
1245
1386
|
parseBool,
|
|
1246
1387
|
parseColor,
|
|
1247
1388
|
parseNum,
|
|
1248
1389
|
parseScene,
|
|
1249
1390
|
parseVec2,
|
|
1250
1391
|
parseVec3,
|
|
1392
|
+
recomposeWorld,
|
|
1251
1393
|
recomputeLayerVisibility,
|
|
1252
1394
|
resolveMaterial
|
|
1253
1395
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
@@ -1651,12 +1793,36 @@
|
|
|
1651
1793
|
const { defs, fns } = collectMacros(text);
|
|
1652
1794
|
if (defs.size === 0 && fns.size === 0) break;
|
|
1653
1795
|
const lines = text.split("\n");
|
|
1796
|
+
const defLine = /* @__PURE__ */ new Map();
|
|
1797
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1798
|
+
const dm = /^[ \t]*#define[ \t]+([A-Za-z_][A-Za-z0-9_]*)/.exec(lines[i]);
|
|
1799
|
+
if (dm && !defLine.has(dm[1])) defLine.set(dm[1], i);
|
|
1800
|
+
}
|
|
1801
|
+
const declLine = /* @__PURE__ */ new Map();
|
|
1802
|
+
{
|
|
1803
|
+
const TYPES = "(?:float|int|bool|vec[234]|ivec[234]|bvec[234]|mat[234])";
|
|
1804
|
+
for (const name of defs.keys()) {
|
|
1805
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1806
|
+
const re = new RegExp(
|
|
1807
|
+
"^\\s*(?:const\\s+|uniform\\s+|varying\\s+|in\\s+|out\\s+|attribute\\s+)*" + TYPES + "\\s+" + esc + "\\s*[=;]"
|
|
1808
|
+
);
|
|
1809
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1810
|
+
if (re.test(lines[i])) {
|
|
1811
|
+
declLine.set(name, i);
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1654
1817
|
let changed = false;
|
|
1655
1818
|
for (let i = 0; i < lines.length; i++) {
|
|
1656
1819
|
const line = lines[i];
|
|
1657
1820
|
if (/^[ \t]*#/.test(line)) continue;
|
|
1658
1821
|
let l = line;
|
|
1659
1822
|
for (const [name, val] of defs) {
|
|
1823
|
+
const dl = defLine.get(name);
|
|
1824
|
+
if (dl !== void 0 && i < dl) continue;
|
|
1825
|
+
if (declLine.get(name) === i) continue;
|
|
1660
1826
|
const re = new RegExp("\\b" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
|
|
1661
1827
|
if (re.test(l)) {
|
|
1662
1828
|
l = replaceWord(l, name, val);
|
|
@@ -1664,6 +1830,8 @@
|
|
|
1664
1830
|
}
|
|
1665
1831
|
}
|
|
1666
1832
|
for (const [name, info] of fns) {
|
|
1833
|
+
const dl = defLine.get(name);
|
|
1834
|
+
if (dl !== void 0 && i < dl) continue;
|
|
1667
1835
|
if (l.includes(name)) {
|
|
1668
1836
|
l = expandFunctionMacro(l, name, info, depth);
|
|
1669
1837
|
changed = true;
|
|
@@ -1938,6 +2106,13 @@
|
|
|
1938
2106
|
const word = text.slice(p + 1, e);
|
|
1939
2107
|
return GLSL_TYPES.has(word);
|
|
1940
2108
|
}
|
|
2109
|
+
function collectIntNames(code) {
|
|
2110
|
+
const names = /* @__PURE__ */ new Set();
|
|
2111
|
+
let m;
|
|
2112
|
+
const declRe = /\b(?:const\s+)?int\s+([A-Za-z_]\w*)\s*[=;)\u0003]/g;
|
|
2113
|
+
while ((m = declRe.exec(code)) !== null) names.add(m[1]);
|
|
2114
|
+
return names;
|
|
2115
|
+
}
|
|
1941
2116
|
function rewriteCall(text, callName, fn) {
|
|
1942
2117
|
let out = "";
|
|
1943
2118
|
let i = 0;
|
|
@@ -2022,23 +2197,73 @@
|
|
|
2022
2197
|
const dim = sw.length;
|
|
2023
2198
|
return fn + "(vec" + dim + "(" + num2 + "), " + expr + ")";
|
|
2024
2199
|
});
|
|
2025
|
-
code = code.replace(
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
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");
|
|
2031
|
-
code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
|
|
2032
|
-
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");
|
|
2033
|
-
{
|
|
2034
|
-
const floatNames = /* @__PURE__ */ new Set();
|
|
2035
|
-
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;
|
|
2036
|
-
let dm;
|
|
2037
|
-
while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
|
|
2038
|
-
if (floatNames.size > 0) {
|
|
2039
|
-
const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
|
|
2040
|
-
code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
|
|
2200
|
+
code = code.replace(
|
|
2201
|
+
/(\.([xyzwrgba]{2,4})\s*=\s*)(max|min)\(\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*,\s*([^;]+?)\s*\)\s*;/g,
|
|
2202
|
+
(all, lead, sw, fn, scalar, vecExpr) => {
|
|
2203
|
+
if (!/\.[xyzwrgba]{2,4}\b|\bvec[234]\s*\(/.test(vecExpr)) return all;
|
|
2204
|
+
return `${lead}${fn}(vec${sw.length}(${scalar}), ${vecExpr});`;
|
|
2041
2205
|
}
|
|
2206
|
+
);
|
|
2207
|
+
code = code.replace(
|
|
2208
|
+
/(^|[^\w)\]])([+-])([+-])(?=[\d.])/g,
|
|
2209
|
+
(all, pre, s1, s2) => pre + (s1 === s2 ? "+" : "-")
|
|
2210
|
+
);
|
|
2211
|
+
const sciHoles = [];
|
|
2212
|
+
code = code.replace(/\b\d+(?:\.\d+)?[eE][+-]?\d+\b/g, (m) => {
|
|
2213
|
+
sciHoles.push(m);
|
|
2214
|
+
return "" + "".repeat(sciHoles.length) + "";
|
|
2215
|
+
});
|
|
2216
|
+
const forHoles = [];
|
|
2217
|
+
code = code.replace(/\bfor\s*\(\s*int\s+([A-Za-z_]\w*)([^)]*)\)/g, (m, name, rest) => {
|
|
2218
|
+
forHoles.push(rest + ")");
|
|
2219
|
+
return `for (int ${name}${"".repeat(forHoles.length)}`;
|
|
2220
|
+
});
|
|
2221
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
2222
|
+
const before = code;
|
|
2223
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
|
|
2224
|
+
code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2225
|
+
code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2226
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
|
|
2227
|
+
code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
|
|
2228
|
+
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");
|
|
2229
|
+
code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
|
|
2230
|
+
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");
|
|
2231
|
+
code = code.replace(
|
|
2232
|
+
/(^|[^\w.])(\d+)\s*([*/+-])\s*(\()/g,
|
|
2233
|
+
(all, pre, num2, op, open, offset, whole) => {
|
|
2234
|
+
let depth = 0;
|
|
2235
|
+
let end = offset + all.length - 1;
|
|
2236
|
+
for (; end < whole.length; end++) {
|
|
2237
|
+
const ch = whole[end];
|
|
2238
|
+
if (ch === "(") depth++;
|
|
2239
|
+
else if (ch === ")") {
|
|
2240
|
+
depth--;
|
|
2241
|
+
if (depth === 0) {
|
|
2242
|
+
end++;
|
|
2243
|
+
break;
|
|
2244
|
+
}
|
|
2245
|
+
} else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
|
|
2246
|
+
}
|
|
2247
|
+
for (; end < whole.length; end++) {
|
|
2248
|
+
const ch = whole[end];
|
|
2249
|
+
if (ch === ";" || ch === "," || ch === "\n" || ch === ")") break;
|
|
2250
|
+
}
|
|
2251
|
+
const seg = whole.slice(offset, end);
|
|
2252
|
+
return /\d\.\d/.test(seg) ? `${pre}${num2}.0 ${op} ${open}` : all;
|
|
2253
|
+
}
|
|
2254
|
+
);
|
|
2255
|
+
{
|
|
2256
|
+
const floatNames = /* @__PURE__ */ new Set();
|
|
2257
|
+
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;
|
|
2258
|
+
let dm;
|
|
2259
|
+
while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
|
|
2260
|
+
if (floatNames.size > 0) {
|
|
2261
|
+
const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
|
|
2262
|
+
code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
|
|
2263
|
+
code = code.replace(new RegExp("\\b(" + alt + ")\\s*([+-])\\s*(\\d+)(?![\\d.])", "g"), "$1 $2 $3.0");
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (code === before) break;
|
|
2042
2267
|
}
|
|
2043
2268
|
{
|
|
2044
2269
|
const FLOAT_BUILTINS = [
|
|
@@ -2174,6 +2399,67 @@
|
|
|
2174
2399
|
return pre + lhs + " = " + rhs + "." + SW[lw] + ";";
|
|
2175
2400
|
});
|
|
2176
2401
|
}
|
|
2402
|
+
{
|
|
2403
|
+
const floatDecl = /* @__PURE__ */ new Set();
|
|
2404
|
+
{
|
|
2405
|
+
const fdre = /\b(?:uniform|varying|attribute|in|out|const)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
|
|
2406
|
+
let fd;
|
|
2407
|
+
while ((fd = fdre.exec(code)) !== null) floatDecl.add(fd[1]);
|
|
2408
|
+
}
|
|
2409
|
+
const vecW = (expr) => {
|
|
2410
|
+
const e = expr.trim();
|
|
2411
|
+
{
|
|
2412
|
+
const c = /^vec([234])\s*\(/.exec(e);
|
|
2413
|
+
if (c) {
|
|
2414
|
+
let depth = 0;
|
|
2415
|
+
for (let i = e.indexOf("("); i < e.length; i++) {
|
|
2416
|
+
if (e[i] === "(") depth++;
|
|
2417
|
+
else if (e[i] === ")") {
|
|
2418
|
+
depth--;
|
|
2419
|
+
if (depth === 0) return i === e.length - 1 ? Number(c[1]) : 0;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return 0;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
if (/^texture(?:Lod)?\s*\(/.test(e)) {
|
|
2426
|
+
let depth = 0;
|
|
2427
|
+
for (let i = e.indexOf("("); i < e.length; i++) {
|
|
2428
|
+
if (e[i] === "(") depth++;
|
|
2429
|
+
else if (e[i] === ")") {
|
|
2430
|
+
depth--;
|
|
2431
|
+
if (depth === 0) return i === e.length - 1 ? 4 : 0;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return 0;
|
|
2435
|
+
}
|
|
2436
|
+
const m = /^([A-Za-z_]\w*)(?:\.([xyzwrgba]{2,4}))?\s*[*/]\s*([^*/]+)$/.exec(e);
|
|
2437
|
+
if (!m) return 0;
|
|
2438
|
+
const rhsPart = m[3];
|
|
2439
|
+
if (/\bvec[234]\s*\(|\.[xyzwrgba]{2,4}\b/.test(rhsPart)) return 0;
|
|
2440
|
+
if (m[2]) return m[2].length;
|
|
2441
|
+
if (floatDecl.has(m[1])) return 0;
|
|
2442
|
+
return width.get(m[1]) || 0;
|
|
2443
|
+
};
|
|
2444
|
+
code = code.replace(
|
|
2445
|
+
/(^|[;{}\n]\s*)float\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
|
|
2446
|
+
(all, pre, name, rhs) => {
|
|
2447
|
+
const w = vecW(rhs);
|
|
2448
|
+
if (w < 2) return all;
|
|
2449
|
+
return `${pre}float ${name} = (${rhs.trim()}).x;`;
|
|
2450
|
+
}
|
|
2451
|
+
);
|
|
2452
|
+
const SWN = { 2: "xy", 3: "xyz" };
|
|
2453
|
+
code = code.replace(
|
|
2454
|
+
/(^|[;{}\n]\s*)vec([23])\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
|
|
2455
|
+
(all, pre, dim, name, rhs) => {
|
|
2456
|
+
const lw = Number(dim);
|
|
2457
|
+
const rw = vecW(rhs);
|
|
2458
|
+
if (rw <= lw) return all;
|
|
2459
|
+
return `${pre}vec${dim} ${name} = (${rhs.trim()}).${SWN[lw]};`;
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2177
2463
|
if (width.size > 0) {
|
|
2178
2464
|
const floatNames = /* @__PURE__ */ new Set();
|
|
2179
2465
|
const fre = /\b(?:uniform|varying|attribute|in|out)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
|
|
@@ -2215,6 +2501,7 @@
|
|
|
2215
2501
|
code = code.replace(/(^|[;{}\n]\s*)([A-Za-z_]\w*)\s*=\s*([^;]+);/g, (all, pre, lhs, rhs) => {
|
|
2216
2502
|
const lw = width.get(lhs);
|
|
2217
2503
|
if (!lw) return all;
|
|
2504
|
+
if (floatNames.has(lhs)) return all;
|
|
2218
2505
|
const r = rhs.trim();
|
|
2219
2506
|
if (new RegExp("^vec" + lw + "\\s*\\(").test(r)) return all;
|
|
2220
2507
|
if (width.has(r)) return all;
|
|
@@ -2258,6 +2545,52 @@
|
|
|
2258
2545
|
new RegExp(`\\bint\\s+([A-Za-z_]\\w*)\\s*=\\s*((?:${FLOAT_FNS})\\s*\\()`, "g"),
|
|
2259
2546
|
"float $1 = $2"
|
|
2260
2547
|
);
|
|
2548
|
+
code = code.replace(
|
|
2549
|
+
/\bfloat\s+([A-Za-z_]\w*)\s*=\s*(int\s*\([^;]*\))\s*;/g,
|
|
2550
|
+
"float $1 = float($2);"
|
|
2551
|
+
);
|
|
2552
|
+
{
|
|
2553
|
+
const intNames = collectIntNames(code);
|
|
2554
|
+
if (intNames.size > 0) {
|
|
2555
|
+
code = code.replace(
|
|
2556
|
+
/\b(const\s+)?float\s+([A-Za-z_]\w*)\s*=\s*([^;{}]+);/g,
|
|
2557
|
+
(all, cst, name, rhs) => {
|
|
2558
|
+
const body = rhs.trim();
|
|
2559
|
+
if (/\./.test(body)) return all;
|
|
2560
|
+
if (/[A-Za-z_]\w*\s*\(/.test(body)) return all;
|
|
2561
|
+
const ids = body.match(/[A-Za-z_]\w*/g);
|
|
2562
|
+
if (!ids || !ids.length) return all;
|
|
2563
|
+
if (!ids.every((x) => intNames.has(x))) return all;
|
|
2564
|
+
return `${cst || ""}float ${name} = float(${body});`;
|
|
2565
|
+
}
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
{
|
|
2570
|
+
const intNames = collectIntNames(code);
|
|
2571
|
+
const floatNames = /* @__PURE__ */ new Set();
|
|
2572
|
+
let fm;
|
|
2573
|
+
const fDeclRe = /\b(?:const\s+|uniform\s+|varying\s+|in\s+|out\s+)*float\s+([A-Za-z_]\w*)/g;
|
|
2574
|
+
while ((fm = fDeclRe.exec(code)) !== null) floatNames.add(fm[1]);
|
|
2575
|
+
for (const n of [...intNames]) {
|
|
2576
|
+
if (floatNames.has(n)) {
|
|
2577
|
+
intNames.delete(n);
|
|
2578
|
+
floatNames.delete(n);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
if (intNames.size > 0 && floatNames.size > 0) {
|
|
2582
|
+
const iAlt = [...intNames].sort((a, b) => b.length - a.length).join("|");
|
|
2583
|
+
const fAlt = [...floatNames].sort((a, b) => b.length - a.length).join("|");
|
|
2584
|
+
code = code.replace(
|
|
2585
|
+
new RegExp(`\\b(${iAlt})\\s*([*/+-])\\s*(${fAlt})\\b`, "g"),
|
|
2586
|
+
(all, a, op, b) => `float(${a}) ${op} ${b}`
|
|
2587
|
+
);
|
|
2588
|
+
code = code.replace(
|
|
2589
|
+
new RegExp(`\\b(${fAlt})\\s*([*/+-])\\s*(${iAlt})\\b`, "g"),
|
|
2590
|
+
(all, a, op, b) => `${a} ${op} float(${b})`
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2261
2594
|
{
|
|
2262
2595
|
const boolNames = /* @__PURE__ */ new Set();
|
|
2263
2596
|
const boolRe = /\bbool\s+([A-Za-z_]\w*)\s*=/g;
|
|
@@ -2271,6 +2604,17 @@
|
|
|
2271
2604
|
);
|
|
2272
2605
|
}
|
|
2273
2606
|
}
|
|
2607
|
+
{
|
|
2608
|
+
const CMP = /\(\s*([^()&|]+?)\s*(<=|>=|<|>|==|!=)\s*([^()&|]+?)\s*\)/g;
|
|
2609
|
+
code = code.replace(
|
|
2610
|
+
new RegExp(CMP.source + "\\s*([*/])", "g"),
|
|
2611
|
+
(all, lhs, op, rhs, mulOp) => `float(${lhs.trim()} ${op} ${rhs.trim()}) ${mulOp}`
|
|
2612
|
+
);
|
|
2613
|
+
code = code.replace(
|
|
2614
|
+
new RegExp("([-+*/]=\\s*)" + CMP.source, "g"),
|
|
2615
|
+
(all, assign, lhs, op, rhs) => `${assign}float(${lhs.trim()} ${op} ${rhs.trim()})`
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2274
2618
|
{
|
|
2275
2619
|
const names = /* @__PURE__ */ new Set();
|
|
2276
2620
|
for (const fm of code.matchAll(/\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?float\s+([A-Za-z_]\w*)\s*\[/g)) {
|
|
@@ -2289,6 +2633,17 @@
|
|
|
2289
2633
|
let p = close1 + 1;
|
|
2290
2634
|
while (p < code.length && /[ \t]/.test(code[p])) p++;
|
|
2291
2635
|
if (code[p] !== "[") {
|
|
2636
|
+
const e12 = code.slice(open1 + 1, close1);
|
|
2637
|
+
const t = e12.trim();
|
|
2638
|
+
const isFloatish = /\d\.\d/.test(t) || /^[A-Za-z_]\w*$/.test(t) && new RegExp("\\bfloat\\s+" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b").test(code);
|
|
2639
|
+
const alreadyInt = /^\s*int\s*\(/.test(t) || /^-?\d+$/.test(t);
|
|
2640
|
+
if (isFloatish && !alreadyInt) {
|
|
2641
|
+
out += code.slice(last, fm.index);
|
|
2642
|
+
out += fm[1] + "[int(" + t + ")]";
|
|
2643
|
+
last = close1 + 1;
|
|
2644
|
+
re.lastIndex = last;
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2292
2647
|
re.lastIndex = close1 + 1;
|
|
2293
2648
|
continue;
|
|
2294
2649
|
}
|
|
@@ -2429,6 +2784,20 @@
|
|
|
2429
2784
|
}).join("\n");
|
|
2430
2785
|
}
|
|
2431
2786
|
}
|
|
2787
|
+
code = code.replace(
|
|
2788
|
+
/^(\s*in\s+(?:highp|mediump|lowp\s+)?)(vec[234]|float)(\s+)([A-Za-z_]\w*)(\s*;)/gm,
|
|
2789
|
+
(all, pre, ty, sp, name, tail) => {
|
|
2790
|
+
const vt = vertTypes.get(name);
|
|
2791
|
+
if (!vt || RANK[vt] >= RANK[ty]) return all;
|
|
2792
|
+
const CH = "xyzw";
|
|
2793
|
+
const RG = "rgba";
|
|
2794
|
+
const over = new RegExp(
|
|
2795
|
+
"\\b" + name + "\\s*\\.\\s*[" + CH + RG + "]*[" + CH.slice(RANK[vt]) + RG.slice(RANK[vt]) + "]"
|
|
2796
|
+
);
|
|
2797
|
+
if (over.test(code)) return all;
|
|
2798
|
+
return pre + vt + sp + name + tail;
|
|
2799
|
+
}
|
|
2800
|
+
);
|
|
2432
2801
|
}
|
|
2433
2802
|
{
|
|
2434
2803
|
const inVecN = /* @__PURE__ */ new Map();
|
|
@@ -2488,18 +2857,27 @@
|
|
|
2488
2857
|
let body = code.slice(braceIdx + 1);
|
|
2489
2858
|
const decls = [];
|
|
2490
2859
|
for (const name of written) {
|
|
2860
|
+
const shadowed = new RegExp(
|
|
2861
|
+
"(?:^|[;{}\\n])\\s*(?:highp|mediump|lowp\\s+)?(?:vec[234]|float|int|bool)\\s+" + name + "\\s*[=;]"
|
|
2862
|
+
).test(body);
|
|
2863
|
+
if (shadowed) continue;
|
|
2491
2864
|
const tm = new RegExp("^\\s*in\\s+(?:highp|mediump|lowp\\s+)?(vec[234]|float)\\s+" + name + "\\s*;", "m").exec(code);
|
|
2492
2865
|
const ty = tm ? tm[1] : "vec4";
|
|
2493
2866
|
decls.push(" " + ty + " " + name + "_rw = " + name + ";");
|
|
2494
2867
|
body = replaceWord(body, name, name + "_rw");
|
|
2495
2868
|
}
|
|
2496
|
-
|
|
2869
|
+
if (decls.length > 0) {
|
|
2870
|
+
body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
|
|
2871
|
+
}
|
|
2497
2872
|
code = head + body;
|
|
2498
2873
|
}
|
|
2499
2874
|
}
|
|
2500
2875
|
}
|
|
2501
2876
|
code = code.replace(/\[(?:unroll|loop|branch|flatten)\]\s*/g, "");
|
|
2502
2877
|
code = code.replace(/\bstatic\s+/g, "");
|
|
2878
|
+
if (forHoles.length > 0) {
|
|
2879
|
+
code = code.replace(/\u0003(\u0004+)\u0003/g, (m, marks) => forHoles[marks.length - 1]);
|
|
2880
|
+
}
|
|
2503
2881
|
{
|
|
2504
2882
|
const floatNames = /* @__PURE__ */ new Set();
|
|
2505
2883
|
for (const m of code.matchAll(/\b(?:uniform[ \t]+)?(?:highp|mediump|lowp)?[ \t]*float[ \t]+([A-Za-z_]\w*)[ \t]*[;=]/g)) {
|
|
@@ -2529,6 +2907,9 @@
|
|
|
2529
2907
|
return line;
|
|
2530
2908
|
}).join("\n");
|
|
2531
2909
|
}
|
|
2910
|
+
if (sciHoles.length > 0) {
|
|
2911
|
+
code = code.replace(/\u0001(\u0002+)\u0001/g, (m, marks) => sciHoles[marks.length - 1]);
|
|
2912
|
+
}
|
|
2532
2913
|
let prologue = "#version 300 es\n";
|
|
2533
2914
|
if (stage === "vert") {
|
|
2534
2915
|
prologue += "precision highp float;\n";
|
|
@@ -3344,8 +3725,9 @@ void main() {
|
|
|
3344
3725
|
uni.set(base, { loc: gl.getUniformLocation(prog, info.name), type: GL_TYPES[info.type] || "unknown", size: info.size });
|
|
3345
3726
|
}
|
|
3346
3727
|
const matMeta = { ...parseMaterialMeta(src.vert), ...parseMaterialMeta(src.frag) };
|
|
3728
|
+
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);
|
|
3347
3729
|
const samplerDefaults = new Map([...parseSamplerDefaults(src.vert), ...parseSamplerDefaults(src.frag)]);
|
|
3348
|
-
const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl };
|
|
3730
|
+
const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl, ndcDirect };
|
|
3349
3731
|
progCache.set(key, entry);
|
|
3350
3732
|
return entry;
|
|
3351
3733
|
}
|
|
@@ -4097,7 +4479,13 @@ void main() {
|
|
|
4097
4479
|
}
|
|
4098
4480
|
const drawLayers = cam.perspective ? scene.layers.slice().sort((a, b) => Number(!!b.isSkybox) - Number(!!a.isSkybox)) : scene.layers;
|
|
4099
4481
|
for (const layer of drawLayers) {
|
|
4100
|
-
if (
|
|
4482
|
+
if (layer.destroyed) continue;
|
|
4483
|
+
if (!layer.visible) {
|
|
4484
|
+
if (pendingEmptyCompose.has(layer.id)) {
|
|
4485
|
+
captureEmptyComposeAtZOrder(layer, cam, viewProj, width, height);
|
|
4486
|
+
}
|
|
4487
|
+
continue;
|
|
4488
|
+
}
|
|
4101
4489
|
if (layer.isPostProcess && !(layer.effects || []).some((e) => e.visible)) continue;
|
|
4102
4490
|
if (groupChildIds.has(layer.id)) continue;
|
|
4103
4491
|
if (layer.isContainer) {
|
|
@@ -4556,7 +4944,13 @@ void main() {
|
|
|
4556
4944
|
gl.bindFramebuffer(gl.FRAMEBUFFER, outFBO.fbo);
|
|
4557
4945
|
gl.viewport(0, 0, outFBO.width, outFBO.height);
|
|
4558
4946
|
gl.bindVertexArray(vao);
|
|
4559
|
-
|
|
4947
|
+
const usePixelQuad = !progEntry.ndcDirect;
|
|
4948
|
+
if (usePixelQuad) {
|
|
4949
|
+
uploadQuad("passPx" + outFBO.width + "x" + outFBO.height, layerQuad(outFBO.width, outFBO.height));
|
|
4950
|
+
} else {
|
|
4951
|
+
uploadQuad("pass", PASS_QUAD);
|
|
4952
|
+
}
|
|
4953
|
+
const passMVP = usePixelQuad ? mat4Transpose(mat4Ortho(0, outFBO.width, 0, outFBO.height, -1e4, 1e4)) : IDENT_M4;
|
|
4560
4954
|
const texNames = mp.textures || [];
|
|
4561
4955
|
const maxTex = Math.max(texNames.length, 8);
|
|
4562
4956
|
const resolutions = /* @__PURE__ */ new Map();
|
|
@@ -4585,7 +4979,7 @@ void main() {
|
|
|
4585
4979
|
usedUnits.add(ti);
|
|
4586
4980
|
resolutions.set(ti, [t.width, t.height, t.width, t.height]);
|
|
4587
4981
|
}
|
|
4588
|
-
bindSystemUniforms(uni, layer, time, cam.projW, cam.projH,
|
|
4982
|
+
bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, passMVP, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
|
|
4589
4983
|
bindConstants(
|
|
4590
4984
|
uni,
|
|
4591
4985
|
animatedConstants(
|
|
@@ -5092,20 +5486,7 @@ void main(){
|
|
|
5092
5486
|
this.model = model || {};
|
|
5093
5487
|
this.override = override || {};
|
|
5094
5488
|
this.layer = layer || null;
|
|
5095
|
-
|
|
5096
|
-
const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
|
|
5097
|
-
const la = layer && layer.angles ? layer.angles : [0, 0, 0];
|
|
5098
|
-
this.originX = lo[0] || 0;
|
|
5099
|
-
this.originY = lo[1] || 0;
|
|
5100
|
-
this.originZ = lo[2] || 0;
|
|
5101
|
-
this.scaleX = ls[0] === 0 ? 1 : ls[0];
|
|
5102
|
-
this.scaleY = ls[1] === 0 ? 1 : ls[1];
|
|
5103
|
-
this.angleZ = (la[2] || 0) * Math.PI / 180;
|
|
5104
|
-
const asx = Math.abs(this.scaleX);
|
|
5105
|
-
const asy = Math.abs(this.scaleY);
|
|
5106
|
-
this.sysScale = Math.min(asx, asy) || 1;
|
|
5107
|
-
this.spriteStretchX = asx / this.sysScale;
|
|
5108
|
-
this.spriteStretchY = asy / this.sysScale;
|
|
5489
|
+
this.syncLayerTransform();
|
|
5109
5490
|
this.maxCount = Math.max(1, Math.min(2e4, num(this.model.maxcount, 100)));
|
|
5110
5491
|
this.simTime = 0;
|
|
5111
5492
|
this.paused = false;
|
|
@@ -5510,6 +5891,31 @@ void main(){
|
|
|
5510
5891
|
setVisible(v) {
|
|
5511
5892
|
this.visible = v;
|
|
5512
5893
|
}
|
|
5894
|
+
/**
|
|
5895
|
+
* [we-scene patch] 从图层重新读取变换(构造时也走这里)。
|
|
5896
|
+
*
|
|
5897
|
+
* 发射器变换原先只在构造时缓存一次、之后**从不刷新**。父组一旦带脚本/动画
|
|
5898
|
+
* 变换(全库 17 个粒子层有脚本化祖先),图层被 recomposeWorld 挪走了,
|
|
5899
|
+
* 粒子却仍从旧位置喷出来 —— 画面上是「人物滑走了、他的火焰留在原地」。
|
|
5900
|
+
* 宿主在 recompose 之后对脏子树里的粒子层调用本方法。
|
|
5901
|
+
*/
|
|
5902
|
+
syncLayerTransform() {
|
|
5903
|
+
const layer = this.layer;
|
|
5904
|
+
const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
|
|
5905
|
+
const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
|
|
5906
|
+
const la = layer && layer.angles ? layer.angles : [0, 0, 0];
|
|
5907
|
+
this.originX = lo[0] || 0;
|
|
5908
|
+
this.originY = lo[1] || 0;
|
|
5909
|
+
this.originZ = lo[2] || 0;
|
|
5910
|
+
this.scaleX = ls[0] === 0 ? 1 : ls[0];
|
|
5911
|
+
this.scaleY = ls[1] === 0 ? 1 : ls[1];
|
|
5912
|
+
this.angleZ = (la[2] || 0) * Math.PI / 180;
|
|
5913
|
+
const asx = Math.abs(this.scaleX);
|
|
5914
|
+
const asy = Math.abs(this.scaleY);
|
|
5915
|
+
this.sysScale = Math.min(asx, asy) || 1;
|
|
5916
|
+
this.spriteStretchX = asx / this.sysScale;
|
|
5917
|
+
this.spriteStretchY = asy / this.sysScale;
|
|
5918
|
+
}
|
|
5513
5919
|
// 宿主每帧提供鼠标位置(世界像素);转到局部空间供控制点使用
|
|
5514
5920
|
setPointer(worldX, worldY) {
|
|
5515
5921
|
const dx = worldX - this.originX;
|
|
@@ -7838,6 +8244,7 @@ void main(){
|
|
|
7838
8244
|
for (const c of desc) {
|
|
7839
8245
|
c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
|
|
7840
8246
|
}
|
|
8247
|
+
layer.attachBindDelta = [d[0], d[1]];
|
|
7841
8248
|
follows.push({
|
|
7842
8249
|
layer,
|
|
7843
8250
|
parent,
|
|
@@ -7852,6 +8259,9 @@ void main(){
|
|
|
7852
8259
|
f.baseY = f.layer.origin[1];
|
|
7853
8260
|
f.subtree = [{ layer: f.layer, x: f.layer.origin[0], y: f.layer.origin[1] }];
|
|
7854
8261
|
for (const c of f.desc) f.subtree.push({ layer: c, x: c.origin[0], y: c.origin[1] });
|
|
8262
|
+
for (const s of f.subtree) {
|
|
8263
|
+
if (!s.layer.attachBase) s.layer.attachBase = [s.x, s.y];
|
|
8264
|
+
}
|
|
7855
8265
|
}
|
|
7856
8266
|
return follows;
|
|
7857
8267
|
}
|
|
@@ -7877,14 +8287,20 @@ void main(){
|
|
|
7877
8287
|
}
|
|
7878
8288
|
deltas.push(parentMeshToWorldDelta(f.parent, cur[12] - f.bindX, cur[13] - f.bindY));
|
|
7879
8289
|
}
|
|
8290
|
+
const baseOf = (s) => {
|
|
8291
|
+
const ab = s.layer.attachBase;
|
|
8292
|
+
if (ab) return ab;
|
|
8293
|
+
return [s.x, s.y];
|
|
8294
|
+
};
|
|
7880
8295
|
const seen = /* @__PURE__ */ new Set();
|
|
7881
8296
|
for (const f of follows) {
|
|
7882
8297
|
const tree = f.subtree || [{ layer: f.layer, x: f.baseX, y: f.baseY }];
|
|
7883
8298
|
for (const s of tree) {
|
|
7884
8299
|
if (seen.has(s.layer)) continue;
|
|
7885
8300
|
seen.add(s.layer);
|
|
7886
|
-
|
|
7887
|
-
s.layer.origin[
|
|
8301
|
+
const b = baseOf(s);
|
|
8302
|
+
s.layer.origin[0] = b[0];
|
|
8303
|
+
s.layer.origin[1] = b[1];
|
|
7888
8304
|
}
|
|
7889
8305
|
}
|
|
7890
8306
|
for (let i = 0; i < follows.length; i++) {
|
|
@@ -8771,6 +9187,19 @@ void main() {
|
|
|
8771
9187
|
if (opts.onError) opts.onError(e, "applyUserProperties");
|
|
8772
9188
|
}
|
|
8773
9189
|
},
|
|
9190
|
+
/** 直调 update 不做文本加工:层可见性脚本(visible.script)要拿原始返回值
|
|
9191
|
+
* ——布尔控可见,callUpdate 会把它吞成 null(防画到画面上的文字版语义)。 */
|
|
9192
|
+
callUpdateRaw(value) {
|
|
9193
|
+
if (!fns.update || sandbox.disabled) return void 0;
|
|
9194
|
+
try {
|
|
9195
|
+
return fns.update(value);
|
|
9196
|
+
} catch (e) {
|
|
9197
|
+
sandbox.errCount++;
|
|
9198
|
+
if (opts.onError) opts.onError(e, "update");
|
|
9199
|
+
if (sandbox.errCount >= 3) sandbox.disabled = true;
|
|
9200
|
+
return void 0;
|
|
9201
|
+
}
|
|
9202
|
+
},
|
|
8774
9203
|
/** 求值当前文本:返回新文本;undefined/null 保留原值;连续出错 3 次熔断回退静态文本 */
|
|
8775
9204
|
callUpdate(value) {
|
|
8776
9205
|
if (!fns.update || sandbox.disabled) return null;
|
|
@@ -9462,13 +9891,8 @@ void main() {
|
|
|
9462
9891
|
Object.defineProperty(proxy, key, {
|
|
9463
9892
|
enumerable: true,
|
|
9464
9893
|
get() {
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
store[key].x = a[0] || 0;
|
|
9468
|
-
store[key].y = a[1] || 0;
|
|
9469
|
-
store[key].z = a[2] || 0;
|
|
9470
|
-
}
|
|
9471
|
-
return store[key];
|
|
9894
|
+
const a = layer && Array.isArray(layer[key]) ? layer[key] : null;
|
|
9895
|
+
return makeVec3(a || [0, 0, 0]);
|
|
9472
9896
|
},
|
|
9473
9897
|
set(v) {
|
|
9474
9898
|
const a = normVec(v);
|
|
@@ -9664,7 +10088,8 @@ void main() {
|
|
|
9664
10088
|
const hasCursorHook = !!(fns && (fns.cursorClick || fns.cursorEnter || fns.cursorLeave || fns.cursorDown || fns.cursorUp || fns.cursorMove));
|
|
9665
10089
|
const hasMediaHook = !!(fns && MEDIA_CALLBACKS.some((n) => fns[n]));
|
|
9666
10090
|
const hasApplyHook = !!(fns && typeof fns.applyUserProperties === "function");
|
|
9667
|
-
|
|
10091
|
+
const usesEngineClock = /\bengine\s*\.\s*(runtime|frametime)\b/.test(body);
|
|
10092
|
+
if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook && !usesEngineClock) return null;
|
|
9668
10093
|
const sandbox = {
|
|
9669
10094
|
engine,
|
|
9670
10095
|
scriptProperties: spValues,
|
|
@@ -9965,7 +10390,7 @@ void main() {
|
|
|
9965
10390
|
cancel.handle = state.handle;
|
|
9966
10391
|
return cancel;
|
|
9967
10392
|
}
|
|
9968
|
-
function
|
|
10393
|
+
function clearTimeout2(h) {
|
|
9969
10394
|
if (typeof h === "function") {
|
|
9970
10395
|
h();
|
|
9971
10396
|
return;
|
|
@@ -9986,7 +10411,7 @@ void main() {
|
|
|
9986
10411
|
return {
|
|
9987
10412
|
setTimeout,
|
|
9988
10413
|
setInterval: setInterval2,
|
|
9989
|
-
clearTimeout,
|
|
10414
|
+
clearTimeout: clearTimeout2,
|
|
9990
10415
|
clearInterval: clearInterval2,
|
|
9991
10416
|
dispose,
|
|
9992
10417
|
/** 测试与诊断用:尚未触发且未取消的定时器数 */
|
|
@@ -10040,6 +10465,8 @@ void main() {
|
|
|
10040
10465
|
const BANDS2 = 64;
|
|
10041
10466
|
const rawL = new Float32Array(BANDS2);
|
|
10042
10467
|
const rawR = new Float32Array(BANDS2);
|
|
10468
|
+
const preL64 = new Float32Array(BANDS2);
|
|
10469
|
+
const preR64 = new Float32Array(BANDS2);
|
|
10043
10470
|
const left64 = new Float32Array(BANDS2);
|
|
10044
10471
|
const right64 = new Float32Array(BANDS2);
|
|
10045
10472
|
const left32 = new Float32Array(32);
|
|
@@ -10053,6 +10480,15 @@ void main() {
|
|
|
10053
10480
|
right32,
|
|
10054
10481
|
left16,
|
|
10055
10482
|
right16,
|
|
10483
|
+
/**
|
|
10484
|
+
* 未钳位(pre-GAIN、pre-clamp)的 64 band,含左右声道 pan。
|
|
10485
|
+
* left64/right64 是 `min(1, v*GAIN)` 之后的值:底鼓段基底就已到 ~0.6、峰值贴 1,
|
|
10486
|
+
* 波峰因数被压平——网页作者按「峰值过阈值」判定敲击时(1520828134 猫爪
|
|
10487
|
+
* `audioArray[i] > 0.5`),事后再乘任何标量都无法把基底与峰值分开。
|
|
10488
|
+
* 网页驱动改对本数组做 gamma 对比扩展,音条墙仍走已标定的 left64/right64。
|
|
10489
|
+
*/
|
|
10490
|
+
preL64,
|
|
10491
|
+
preR64,
|
|
10056
10492
|
/** vumeter:整体响度 0..1(粒子 audioprocessing / 文字脚本 average 用) */
|
|
10057
10493
|
level: 0,
|
|
10058
10494
|
/** 渲染器诊断:当前是否处于「静音段」 */
|
|
@@ -10098,6 +10534,7 @@ void main() {
|
|
|
10098
10534
|
if (fq >= 0.45) v += hatV * 0.5 * ((fq - 0.45) / 0.55);
|
|
10099
10535
|
v += riser * 0.5;
|
|
10100
10536
|
v *= tilt;
|
|
10537
|
+
const vPre = v;
|
|
10101
10538
|
v = Math.min(1, v * GAIN);
|
|
10102
10539
|
const width = 0.06 + fq * 0.2;
|
|
10103
10540
|
const pan = vnoise(beat * 0.13 + i * 0.35, 11) * width;
|
|
@@ -10106,6 +10543,8 @@ void main() {
|
|
|
10106
10543
|
const floorV = silent ? 0.012 : 0;
|
|
10107
10544
|
rawL[i] = Math.max(floorV, l);
|
|
10108
10545
|
rawR[i] = Math.max(floorV, r);
|
|
10546
|
+
preL64[i] = Math.max(floorV, Math.max(0, vPre * (1 - pan)));
|
|
10547
|
+
preR64[i] = Math.max(floorV, Math.max(0, vPre * (1 + pan)));
|
|
10109
10548
|
if (i < 48) levelSum += (rawL[i] + rawR[i]) * 0.5;
|
|
10110
10549
|
}
|
|
10111
10550
|
left64.set(rawL);
|
|
@@ -10603,15 +11042,7 @@ void main() {
|
|
|
10603
11042
|
state.screenH = v.h || 1;
|
|
10604
11043
|
}
|
|
10605
11044
|
readViewport();
|
|
10606
|
-
function
|
|
10607
|
-
readViewport();
|
|
10608
|
-
let x = ev.clientX;
|
|
10609
|
-
let y = ev.clientY;
|
|
10610
|
-
if (target && typeof target.getBoundingClientRect === "function") {
|
|
10611
|
-
const r = target.getBoundingClientRect();
|
|
10612
|
-
x -= r.left;
|
|
10613
|
-
y -= r.top;
|
|
10614
|
-
}
|
|
11045
|
+
function applyMove(x, y) {
|
|
10615
11046
|
const u = x / state.screenW;
|
|
10616
11047
|
const v = y / state.screenH;
|
|
10617
11048
|
if (!state.has) {
|
|
@@ -10628,18 +11059,35 @@ void main() {
|
|
|
10628
11059
|
state.moveCount++;
|
|
10629
11060
|
state.lastEventTime = Date.now();
|
|
10630
11061
|
}
|
|
11062
|
+
function applyButtons(mask) {
|
|
11063
|
+
const left = (mask & 1) !== 0;
|
|
11064
|
+
if (left === state.leftDown) return;
|
|
11065
|
+
state.leftDown = left;
|
|
11066
|
+
if (left) state.downCount++;
|
|
11067
|
+
else state.upCount++;
|
|
11068
|
+
state.lastEventTime = Date.now();
|
|
11069
|
+
}
|
|
11070
|
+
function onMove(ev) {
|
|
11071
|
+
readViewport();
|
|
11072
|
+
let x = ev.clientX;
|
|
11073
|
+
let y = ev.clientY;
|
|
11074
|
+
if (target && typeof target.getBoundingClientRect === "function") {
|
|
11075
|
+
const r = target.getBoundingClientRect();
|
|
11076
|
+
x -= r.left;
|
|
11077
|
+
y -= r.top;
|
|
11078
|
+
}
|
|
11079
|
+
applyMove(x, y);
|
|
11080
|
+
}
|
|
10631
11081
|
function onDown(ev) {
|
|
10632
11082
|
if (ev.button !== void 0 && ev.button !== 0) return;
|
|
10633
|
-
|
|
10634
|
-
state.downCount++;
|
|
11083
|
+
applyButtons(1);
|
|
10635
11084
|
}
|
|
10636
11085
|
function onUp(ev) {
|
|
10637
11086
|
if (ev.button !== void 0 && ev.button !== 0) return;
|
|
10638
|
-
|
|
10639
|
-
state.upCount++;
|
|
11087
|
+
applyButtons(0);
|
|
10640
11088
|
}
|
|
10641
11089
|
function onLeaveWindow() {
|
|
10642
|
-
|
|
11090
|
+
applyButtons(0);
|
|
10643
11091
|
}
|
|
10644
11092
|
let attached = false;
|
|
10645
11093
|
if (target && target.addEventListener) {
|
|
@@ -10654,6 +11102,42 @@ void main() {
|
|
|
10654
11102
|
}
|
|
10655
11103
|
return {
|
|
10656
11104
|
state,
|
|
11105
|
+
/**
|
|
11106
|
+
* 外部注入指针状态(宿主轮询系统鼠标后推入)。协议见 docs/INTEGRATION.md。
|
|
11107
|
+
*
|
|
11108
|
+
* 接**归一化**坐标而不是像素:宿主知道自己那块屏的 points 尺寸,除法在它那边
|
|
11109
|
+
* 做更准(混合 DPI 多显示器下无需任何 DPR 折算);这里再乘回 screenW/H 得到
|
|
11110
|
+
* input.cursorScreenPosition 要的像素。
|
|
11111
|
+
*
|
|
11112
|
+
* u/v 是 [0,1]、原点左上、**Y 朝下** —— 与 DOM 路径的 state.u/v 同一空间
|
|
11113
|
+
* (见文件头坐标约定)。宿主不要替 shader 翻 Y。
|
|
11114
|
+
*
|
|
11115
|
+
* 不在这里推进 last:外部推送频率(~90Hz)高于帧率,若在推送里推进 last,
|
|
11116
|
+
* `length(g_PointerPosition - g_PointerPositionLast)` 会恒接近 0,
|
|
11117
|
+
* cursorripple 完全不起波且无报错(与 DOM 路径同一个坑,见文件头)。
|
|
11118
|
+
*
|
|
11119
|
+
* @param {{u:number, v:number, buttons?:number}} p 归一化位置 + 按键位掩码(bit0 左)
|
|
11120
|
+
*/
|
|
11121
|
+
pushExternal(p) {
|
|
11122
|
+
if (!p) return;
|
|
11123
|
+
readViewport();
|
|
11124
|
+
const u = Number(p.u);
|
|
11125
|
+
const v = Number(p.v);
|
|
11126
|
+
if (Number.isFinite(u) && Number.isFinite(v)) {
|
|
11127
|
+
applyMove(u * state.screenW, v * state.screenH);
|
|
11128
|
+
}
|
|
11129
|
+
applyButtons(Number(p.buttons) || 0);
|
|
11130
|
+
},
|
|
11131
|
+
/**
|
|
11132
|
+
* 外部指针离开本窗口(鼠标移到了别的显示器)。
|
|
11133
|
+
*
|
|
11134
|
+
* **只清按键,保留位置与 has** —— 清 has 会让 xray 开窗突然跳到相机外
|
|
11135
|
+
* (renderer.js 的 XRAY_IDLE_SCREEN_UV)、视差弹回中心,画面会明显抽一下。
|
|
11136
|
+
* 语义与 DOM 的 onLeaveWindow 一致:位置停在最后已知点,只是不再按着键。
|
|
11137
|
+
*/
|
|
11138
|
+
pushExternalLeave() {
|
|
11139
|
+
applyButtons(0);
|
|
11140
|
+
},
|
|
10657
11141
|
/**
|
|
10658
11142
|
* 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current。
|
|
10659
11143
|
* 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
|
|
@@ -11180,6 +11664,21 @@ void main() {
|
|
|
11180
11664
|
} catch {
|
|
11181
11665
|
return null;
|
|
11182
11666
|
}
|
|
11667
|
+
},
|
|
11668
|
+
async webEntry(signal) {
|
|
11669
|
+
let file = "index.html";
|
|
11670
|
+
try {
|
|
11671
|
+
const r = await fetch(`${base}/project.json`, { ...init, signal });
|
|
11672
|
+
if (r.ok) {
|
|
11673
|
+
const project = await r.json();
|
|
11674
|
+
if (project && typeof project.file === "string" && project.file.trim()) {
|
|
11675
|
+
file = project.file.trim().replace(/^\/+/, "");
|
|
11676
|
+
}
|
|
11677
|
+
}
|
|
11678
|
+
} catch {
|
|
11679
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
11680
|
+
}
|
|
11681
|
+
return { url: `${base}/${file}` };
|
|
11183
11682
|
}
|
|
11184
11683
|
};
|
|
11185
11684
|
}
|
|
@@ -12236,7 +12735,48 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
12236
12735
|
systemfont_simhei: "SimHei, 'Heiti SC', sans-serif"
|
|
12237
12736
|
};
|
|
12238
12737
|
const fontFaceCache = /* @__PURE__ */ new Map();
|
|
12738
|
+
function fontKeyHash(key) {
|
|
12739
|
+
let h = 5381;
|
|
12740
|
+
for (let i = 0; i < key.length; i++) h = (h << 5) + h + key.charCodeAt(i) | 0;
|
|
12741
|
+
return (h >>> 0).toString(36);
|
|
12742
|
+
}
|
|
12743
|
+
function releaseFontFaces(keys) {
|
|
12744
|
+
for (const key of keys) {
|
|
12745
|
+
const entry = fontFaceCache.get(key);
|
|
12746
|
+
if (!entry) continue;
|
|
12747
|
+
entry.refs--;
|
|
12748
|
+
if (entry.refs > 0) continue;
|
|
12749
|
+
fontFaceCache.delete(key);
|
|
12750
|
+
try {
|
|
12751
|
+
const dead = [];
|
|
12752
|
+
document.fonts.forEach((f) => {
|
|
12753
|
+
if (f.family === entry.family) dead.push(f);
|
|
12754
|
+
});
|
|
12755
|
+
for (const f of dead) document.fonts.delete(f);
|
|
12756
|
+
} catch {
|
|
12757
|
+
}
|
|
12758
|
+
}
|
|
12759
|
+
}
|
|
12239
12760
|
const pkgCache = /* @__PURE__ */ new Map();
|
|
12761
|
+
const PKG_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
12762
|
+
let pkgCacheBytes = 0;
|
|
12763
|
+
function pkgCacheEvict(currentKey) {
|
|
12764
|
+
while (pkgCache.size > 0 && (pkgCache.size > 2 || pkgCacheBytes > PKG_CACHE_MAX_BYTES)) {
|
|
12765
|
+
let oldestKey = null;
|
|
12766
|
+
let oldestAt = Infinity;
|
|
12767
|
+
for (const [k, v] of pkgCache) {
|
|
12768
|
+
if (k === currentKey) continue;
|
|
12769
|
+
if (v.at < oldestAt) {
|
|
12770
|
+
oldestAt = v.at;
|
|
12771
|
+
oldestKey = k;
|
|
12772
|
+
}
|
|
12773
|
+
}
|
|
12774
|
+
if (!oldestKey) break;
|
|
12775
|
+
const victim = pkgCache.get(oldestKey);
|
|
12776
|
+
pkgCacheBytes -= victim.parsed.fileSize || 0;
|
|
12777
|
+
pkgCache.delete(oldestKey);
|
|
12778
|
+
}
|
|
12779
|
+
}
|
|
12240
12780
|
async function loadParsedPkg(rt, cfg, source, signal) {
|
|
12241
12781
|
const cacheKey = source.key;
|
|
12242
12782
|
if (cacheKey) {
|
|
@@ -12259,38 +12799,40 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
12259
12799
|
const parsed = pkg.parsePkg(bytes);
|
|
12260
12800
|
if (!cacheKey) return parsed;
|
|
12261
12801
|
pkgCache.set(cacheKey, { parsed, at: Date.now() });
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
let oldestAt = Infinity;
|
|
12265
|
-
for (const [k, v] of pkgCache) {
|
|
12266
|
-
if (k === cacheKey) continue;
|
|
12267
|
-
if (v.at < oldestAt) {
|
|
12268
|
-
oldestAt = v.at;
|
|
12269
|
-
oldestKey = k;
|
|
12270
|
-
}
|
|
12271
|
-
}
|
|
12272
|
-
if (oldestKey) pkgCache.delete(oldestKey);
|
|
12273
|
-
}
|
|
12802
|
+
pkgCacheBytes += parsed.fileSize || 0;
|
|
12803
|
+
pkgCacheEvict(cacheKey);
|
|
12274
12804
|
return parsed;
|
|
12275
12805
|
}
|
|
12276
12806
|
function mountScene(rt, cfg) {
|
|
12277
12807
|
clear(rt);
|
|
12278
|
-
const c = cfg.canvas ?? document.createElement("canvas");
|
|
12808
|
+
const c = (cfg.canvas instanceof HTMLCanvasElement ? cfg.canvas : null) ?? document.createElement("canvas");
|
|
12279
12809
|
const dpr = effectiveDpr(rt, cfg);
|
|
12280
12810
|
const vw = c.clientWidth || window.innerWidth || 1;
|
|
12281
12811
|
const vh = c.clientHeight || window.innerHeight || 1;
|
|
12282
12812
|
c.width = Math.max(1, Math.round(vw * dpr));
|
|
12283
12813
|
c.height = Math.max(1, Math.round(vh * dpr));
|
|
12284
|
-
if (!cfg.canvas) {
|
|
12814
|
+
if (!(cfg.canvas instanceof HTMLCanvasElement)) {
|
|
12285
12815
|
c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
|
|
12286
12816
|
rt.wrap?.appendChild(c);
|
|
12287
12817
|
}
|
|
12288
12818
|
rt.canvas = c;
|
|
12289
12819
|
let disposed = false;
|
|
12820
|
+
const origWarn = console.warn.bind(console);
|
|
12821
|
+
console.warn = (...args) => {
|
|
12822
|
+
const s = args.map((a) => typeof a === "string" ? a : String(a?.message ?? a)).join(" ");
|
|
12823
|
+
if (s.includes("[we-scene]")) {
|
|
12824
|
+
try {
|
|
12825
|
+
reportDiag(rt, cfg, s.slice(0, 300));
|
|
12826
|
+
} catch {
|
|
12827
|
+
}
|
|
12828
|
+
}
|
|
12829
|
+
origWarn(...args);
|
|
12830
|
+
};
|
|
12290
12831
|
const pkgAbort = new AbortController();
|
|
12291
12832
|
let particleCleanup;
|
|
12292
12833
|
rt.sceneCleanup = () => {
|
|
12293
12834
|
disposed = true;
|
|
12835
|
+
console.warn = origWarn;
|
|
12294
12836
|
pkgAbort.abort();
|
|
12295
12837
|
rt.sceneTextUpdate = void 0;
|
|
12296
12838
|
if (particleCleanup) {
|
|
@@ -12347,6 +12889,11 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
12347
12889
|
const sceneEntry = pkg.getEntry(parsedPkg, "scene.json");
|
|
12348
12890
|
if (!sceneEntry) throw new Error("pkg 中没有 scene.json(不是场景壁纸?)");
|
|
12349
12891
|
const scene = scn.parseScene(JSON.parse(readText(sceneEntry)), project);
|
|
12892
|
+
if (cfg.clearColor) {
|
|
12893
|
+
const g = scene.general ??= {};
|
|
12894
|
+
g.clearcolor = cfg.clearColor;
|
|
12895
|
+
g.clearenabled = true;
|
|
12896
|
+
}
|
|
12350
12897
|
{
|
|
12351
12898
|
const zRaw = scene.general?.zoom;
|
|
12352
12899
|
const zVal = zRaw && typeof zRaw === "object" ? Number(zRaw.value) : Number(zRaw);
|
|
@@ -12445,9 +12992,72 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
12445
12992
|
level: 0,
|
|
12446
12993
|
silent: true
|
|
12447
12994
|
};
|
|
12995
|
+
const hostAudio = (() => {
|
|
12996
|
+
const snapshot = {
|
|
12997
|
+
left64: zero(64),
|
|
12998
|
+
right64: zero(64),
|
|
12999
|
+
left32: zero(32),
|
|
13000
|
+
right32: zero(32),
|
|
13001
|
+
left16: zero(16),
|
|
13002
|
+
right16: zero(16),
|
|
13003
|
+
// 未钳位频谱:网页驱动会对它做 gamma 对比扩展。宿主给的已是 0..1
|
|
13004
|
+
// 归一化值,没有 pre-GAIN 概念,直接与 left64/right64 共用同一份数据
|
|
13005
|
+
preL64: zero(64),
|
|
13006
|
+
preR64: zero(64),
|
|
13007
|
+
level: 0,
|
|
13008
|
+
silent: true
|
|
13009
|
+
};
|
|
13010
|
+
const down = (dst, src) => {
|
|
13011
|
+
const g = src.length / dst.length;
|
|
13012
|
+
for (let i = 0; i < dst.length; i++) {
|
|
13013
|
+
let s = 0;
|
|
13014
|
+
const i0 = Math.floor(i * g);
|
|
13015
|
+
const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
|
|
13016
|
+
for (let j = i0; j < i1; j++) s += src[j];
|
|
13017
|
+
dst[i] = s / (i1 - i0);
|
|
13018
|
+
}
|
|
13019
|
+
};
|
|
13020
|
+
return {
|
|
13021
|
+
active: false,
|
|
13022
|
+
snapshot,
|
|
13023
|
+
/** 每帧从宿主拉一次。宿主返回 null(未采集/无权限)时置 active=false 回落模拟源 */
|
|
13024
|
+
pump() {
|
|
13025
|
+
const src = rt.audioBridge?.();
|
|
13026
|
+
if (!src || !src.left || !src.right) {
|
|
13027
|
+
this.active = false;
|
|
13028
|
+
return;
|
|
13029
|
+
}
|
|
13030
|
+
const n = Math.min(64, src.left.length, src.right.length);
|
|
13031
|
+
let sum = 0;
|
|
13032
|
+
for (let i = 0; i < n; i++) {
|
|
13033
|
+
const l = src.left[i] || 0;
|
|
13034
|
+
const r = src.right[i] || 0;
|
|
13035
|
+
snapshot.left64[i] = l;
|
|
13036
|
+
snapshot.right64[i] = r;
|
|
13037
|
+
snapshot.preL64[i] = l;
|
|
13038
|
+
snapshot.preR64[i] = r;
|
|
13039
|
+
if (i < 48) sum += l;
|
|
13040
|
+
}
|
|
13041
|
+
for (let i = n; i < 64; i++) {
|
|
13042
|
+
snapshot.left64[i] = 0;
|
|
13043
|
+
snapshot.right64[i] = 0;
|
|
13044
|
+
snapshot.preL64[i] = 0;
|
|
13045
|
+
snapshot.preR64[i] = 0;
|
|
13046
|
+
}
|
|
13047
|
+
down(snapshot.left32, snapshot.left64);
|
|
13048
|
+
down(snapshot.right32, snapshot.right64);
|
|
13049
|
+
down(snapshot.left16, snapshot.left64);
|
|
13050
|
+
down(snapshot.right16, snapshot.right64);
|
|
13051
|
+
snapshot.level = Math.min(1, sum / 48);
|
|
13052
|
+
snapshot.silent = snapshot.level < 0.02;
|
|
13053
|
+
this.active = true;
|
|
13054
|
+
}
|
|
13055
|
+
};
|
|
13056
|
+
})();
|
|
13057
|
+
const activeAudioSnapshot = () => hostAudio.active ? hostAudio.snapshot : audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
|
|
12448
13058
|
renderer.setAudioProvider(() => {
|
|
12449
13059
|
if (!audioSim.enabled) return SILENT_AUDIO;
|
|
12450
|
-
return
|
|
13060
|
+
return activeAudioSnapshot();
|
|
12451
13061
|
});
|
|
12452
13062
|
const audioViews = /* @__PURE__ */ new Map();
|
|
12453
13063
|
window.__audioStats = () => ({
|
|
@@ -12575,6 +13185,10 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
12575
13185
|
} : {}
|
|
12576
13186
|
);
|
|
12577
13187
|
renderer.setPointerProvider(() => pointerSrc);
|
|
13188
|
+
rt.pointerCtl = {
|
|
13189
|
+
push: (p) => pointerSrc.pushExternal(p),
|
|
13190
|
+
leave: () => pointerSrc.pushExternalLeave()
|
|
13191
|
+
};
|
|
12578
13192
|
{
|
|
12579
13193
|
const prevCleanup = particleCleanup;
|
|
12580
13194
|
particleCleanup = () => {
|
|
@@ -13076,6 +13690,7 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13076
13690
|
if (usedVisible && !rt.paused) texEntry.videoCtl.play();
|
|
13077
13691
|
}
|
|
13078
13692
|
const particleSystems = [];
|
|
13693
|
+
const particleDirty = [];
|
|
13079
13694
|
const particleSystemsByLayer = /* @__PURE__ */ new Map();
|
|
13080
13695
|
let builtinTexCount = 0;
|
|
13081
13696
|
const loadParticleTex = async (name) => {
|
|
@@ -13294,7 +13909,10 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13294
13909
|
const py = originY != null ? originY : wy;
|
|
13295
13910
|
for (const ps of particleSystems) ps.setPointer(wx, py);
|
|
13296
13911
|
}
|
|
13297
|
-
|
|
13912
|
+
if (particleDirty.length) {
|
|
13913
|
+
for (const ps of particleDirty) ps.syncLayerTransform();
|
|
13914
|
+
}
|
|
13915
|
+
for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? activeAudioSnapshot() : null);
|
|
13298
13916
|
if (particleDiagFrame < 2) {
|
|
13299
13917
|
particleDiagFrame++;
|
|
13300
13918
|
if (particleDiagFrame === 2) {
|
|
@@ -13479,6 +14097,17 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13479
14097
|
if (attachFollows.length) {
|
|
13480
14098
|
reportDiag(rt, cfg, `attachments: ${attachFollows.length} hanging layers`);
|
|
13481
14099
|
}
|
|
14100
|
+
const transformDirty = scn.collectTransformDirty(
|
|
14101
|
+
scene.layers,
|
|
14102
|
+
attachFollows.map((f) => f.layer)
|
|
14103
|
+
);
|
|
14104
|
+
if (transformDirty.size) {
|
|
14105
|
+
reportDiag(rt, cfg, `transform graph: ${transformDirty.size} live layers`);
|
|
14106
|
+
for (const [lid, list] of particleSystemsByLayer) {
|
|
14107
|
+
if (!transformDirty.has(lid)) continue;
|
|
14108
|
+
for (const ps of list) particleDirty.push(ps);
|
|
14109
|
+
}
|
|
14110
|
+
}
|
|
13482
14111
|
const textWidgets = [];
|
|
13483
14112
|
const textLayerText = /* @__PURE__ */ new Map();
|
|
13484
14113
|
const textShared = {};
|
|
@@ -13492,15 +14121,19 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13492
14121
|
const win0 = fitWindow(normalizeFit(rt.cfg.fit), projW, projH, c.width, c.height);
|
|
13493
14122
|
const quality = Math.min(3, Math.max(0.5, c.width / Math.max(1, win0.viewW)));
|
|
13494
14123
|
const fontFamilies = /* @__PURE__ */ new Map();
|
|
14124
|
+
const usedFontKeys = [];
|
|
13495
14125
|
const fontPaths = /* @__PURE__ */ new Set();
|
|
13496
14126
|
for (const l of scene.layers) if (l.isText && l.textFont) fontPaths.add(l.textFont);
|
|
13497
14127
|
for (const e of parsedPkg.entries || []) {
|
|
13498
14128
|
if (typeof e.name === "string" && /^fonts\/.+\.(ttf|otf|woff2?)$/i.test(e.name)) fontPaths.add(e.name);
|
|
13499
14129
|
}
|
|
13500
14130
|
for (const fp of fontPaths) {
|
|
13501
|
-
const
|
|
14131
|
+
const key = `${cfg.src}|${fp}`;
|
|
14132
|
+
const cached = fontFaceCache.get(key);
|
|
13502
14133
|
if (cached) {
|
|
13503
|
-
|
|
14134
|
+
cached.refs++;
|
|
14135
|
+
usedFontKeys.push(key);
|
|
14136
|
+
fontFamilies.set(fp, cached.family);
|
|
13504
14137
|
continue;
|
|
13505
14138
|
}
|
|
13506
14139
|
const sys = SYSTEM_FONT_FAMILIES[fp.toLowerCase()];
|
|
@@ -13514,18 +14147,30 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13514
14147
|
const bytes = sanitizeFontForBrowser(
|
|
13515
14148
|
fe instanceof Uint8Array ? fe : new Uint8Array(fe)
|
|
13516
14149
|
);
|
|
13517
|
-
const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
|
|
14150
|
+
const fam = "wefont_" + fontKeyHash(key) + "_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
|
|
13518
14151
|
const url = URL.createObjectURL(new Blob([bytes]));
|
|
13519
14152
|
const ff = new FontFace(fam, `url(${url})`);
|
|
13520
14153
|
await ff.load();
|
|
14154
|
+
if (disposed) {
|
|
14155
|
+
URL.revokeObjectURL(url);
|
|
14156
|
+
break;
|
|
14157
|
+
}
|
|
13521
14158
|
document.fonts.add(ff);
|
|
13522
14159
|
(rt.objectUrls ??= []).push(url);
|
|
13523
14160
|
fontFamilies.set(fp, fam);
|
|
13524
|
-
fontFaceCache.set(
|
|
14161
|
+
fontFaceCache.set(key, { family: fam, refs: 1 });
|
|
14162
|
+
usedFontKeys.push(key);
|
|
13525
14163
|
} catch (e) {
|
|
13526
14164
|
console.warn(`字体加载失败 ${fp}: ${e.message}`);
|
|
13527
14165
|
}
|
|
13528
14166
|
}
|
|
14167
|
+
if (usedFontKeys.length) {
|
|
14168
|
+
const prevCleanup = rt.sceneCleanup;
|
|
14169
|
+
rt.sceneCleanup = () => {
|
|
14170
|
+
releaseFontFaces(usedFontKeys);
|
|
14171
|
+
prevCleanup?.();
|
|
14172
|
+
};
|
|
14173
|
+
}
|
|
13529
14174
|
textCanvas = document.createElement("canvas");
|
|
13530
14175
|
textCtx = textCanvas.getContext("2d");
|
|
13531
14176
|
const MAX_TEX = 2048;
|
|
@@ -13578,10 +14223,18 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13578
14223
|
const hw = layer.size[0] * (layer.scale[0] || 1) / 2;
|
|
13579
14224
|
const hh = layer.size[1] * (layer.scale[1] || 1) / 2;
|
|
13580
14225
|
const a = layer.textAnchor;
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
if (a.includes("
|
|
13584
|
-
if (a.includes("
|
|
14226
|
+
let adx = 0;
|
|
14227
|
+
let ady = 0;
|
|
14228
|
+
if (a.includes("left")) adx += hw;
|
|
14229
|
+
if (a.includes("right")) adx -= hw;
|
|
14230
|
+
if (a.includes("top")) ady -= hh;
|
|
14231
|
+
if (a.includes("bottom")) ady += hh;
|
|
14232
|
+
layer.origin[0] += adx;
|
|
14233
|
+
layer.origin[1] += ady;
|
|
14234
|
+
if (layer.localOrigin) {
|
|
14235
|
+
layer.localOrigin[0] += adx;
|
|
14236
|
+
layer.localOrigin[1] += ady;
|
|
14237
|
+
}
|
|
13585
14238
|
}
|
|
13586
14239
|
const em0 = TEXT_EM_SCALE * Math.max(1, layer.textPointsize);
|
|
13587
14240
|
const marginCap = wtext.textLayerHasTintMask(layer) ? 8 : 256;
|
|
@@ -13824,6 +14477,15 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13824
14477
|
if (sb && sb.hasMediaHook) registerMediaHook(sb);
|
|
13825
14478
|
}
|
|
13826
14479
|
});
|
|
14480
|
+
const LOCAL_SLOT = {
|
|
14481
|
+
origin: "localOrigin",
|
|
14482
|
+
scale: "localScale",
|
|
14483
|
+
angles: "localAngles"
|
|
14484
|
+
};
|
|
14485
|
+
const fieldSlot = (layer, field) => {
|
|
14486
|
+
const slot = LOCAL_SLOT[field];
|
|
14487
|
+
return slot && layer && Array.isArray(layer[slot]) ? slot : field;
|
|
14488
|
+
};
|
|
13827
14489
|
for (const layer of scene.layers) {
|
|
13828
14490
|
const defs = layer.objectAnimations;
|
|
13829
14491
|
if (!defs) continue;
|
|
@@ -13834,11 +14496,13 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13834
14496
|
const ctrl = anim.createAnimation(def.animation);
|
|
13835
14497
|
ctrl.field = field;
|
|
13836
14498
|
ctrl.baseValue = def.value;
|
|
13837
|
-
const
|
|
14499
|
+
const slot = fieldSlot(layer, field);
|
|
14500
|
+
const live2 = layer[slot];
|
|
13838
14501
|
ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
|
|
14502
|
+
ctrl.slot = slot;
|
|
13839
14503
|
layer.animationList.push(ctrl);
|
|
13840
14504
|
if (ctrl.name) layer.animations[ctrl.name] = ctrl;
|
|
13841
|
-
animRuns.push({ layer, field, ctrl });
|
|
14505
|
+
animRuns.push({ layer, field, slot, ctrl });
|
|
13842
14506
|
} catch (e) {
|
|
13843
14507
|
reportDiag(rt, cfg, `animation '${layer.name}.${field}' 建控制器失败: ${String(e.message).slice(0, 80)}`);
|
|
13844
14508
|
}
|
|
@@ -13903,7 +14567,8 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13903
14567
|
});
|
|
13904
14568
|
if (sandbox) {
|
|
13905
14569
|
propSandboxes.push(sandbox);
|
|
13906
|
-
const
|
|
14570
|
+
const initSlot = fieldSlot(layer, field);
|
|
14571
|
+
const fieldVal = layer[initSlot];
|
|
13907
14572
|
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;
|
|
13908
14573
|
sandbox.init(initArg);
|
|
13909
14574
|
sandbox.applyUserProperties(objUserProps);
|
|
@@ -13913,6 +14578,8 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13913
14578
|
objectScriptRuns.push({
|
|
13914
14579
|
layer,
|
|
13915
14580
|
field,
|
|
14581
|
+
// 变换字段逐帧也在 local 槽上收发(与 init 同一空间)。
|
|
14582
|
+
slot: initSlot,
|
|
13916
14583
|
kind: field === "visible" ? "bool" : field === "alpha" || field === "brightness" ? "scalar" : "vec3",
|
|
13917
14584
|
sandbox
|
|
13918
14585
|
});
|
|
@@ -13976,6 +14643,7 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
13976
14643
|
window.__objScripts = objectScriptRuns;
|
|
13977
14644
|
}
|
|
13978
14645
|
window.__mediaHooks = mediaHooks;
|
|
14646
|
+
window.__sceneLayers = scene.layers;
|
|
13979
14647
|
window.__compositeStats = () => renderer.compositeStats?.() ?? null;
|
|
13980
14648
|
window.__compositeEnable = (on) => renderer.setCompositeEnabled?.(on);
|
|
13981
14649
|
window.__scene = scene;
|
|
@@ -14036,6 +14704,7 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14036
14704
|
const playingVideos = [];
|
|
14037
14705
|
const playingAudios = [];
|
|
14038
14706
|
let lastRender = -Infinity;
|
|
14707
|
+
let lastAnimT = 0;
|
|
14039
14708
|
const renderLoop = (now) => {
|
|
14040
14709
|
if (disposed || rt.paused) return;
|
|
14041
14710
|
const fps = rt.cfg.sceneFps || 60;
|
|
@@ -14068,34 +14737,37 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14068
14737
|
}
|
|
14069
14738
|
if (live?.windowTitle) live.windowTitle.pump();
|
|
14070
14739
|
else simWindow.update(t);
|
|
14740
|
+
const animDt = Math.max(0, t - lastAnimT);
|
|
14741
|
+
lastAnimT = t;
|
|
14071
14742
|
for (const run of animRuns) {
|
|
14072
|
-
run.ctrl.advance(
|
|
14743
|
+
run.ctrl.advance(animDt);
|
|
14073
14744
|
const field = run.field;
|
|
14745
|
+
const slot = run.slot || field;
|
|
14074
14746
|
const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
|
|
14075
14747
|
if (Array.isArray(out)) {
|
|
14076
|
-
const cur = run.layer[
|
|
14748
|
+
const cur = run.layer[slot];
|
|
14077
14749
|
if (Array.isArray(cur)) for (let i = 0; i < out.length && i < cur.length; i++) cur[i] = out[i];
|
|
14078
14750
|
} else if (Number.isFinite(out)) {
|
|
14079
14751
|
if (field === "visible") run.layer[field] = !!out;
|
|
14080
|
-
else run.layer[
|
|
14752
|
+
else run.layer[slot] = out;
|
|
14081
14753
|
}
|
|
14082
14754
|
}
|
|
14083
14755
|
for (const run of generalAnimRuns) {
|
|
14084
|
-
run.ctrl.advance(
|
|
14756
|
+
run.ctrl.advance(animDt);
|
|
14085
14757
|
const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
|
|
14086
14758
|
if (typeof out === "number" && Number.isFinite(out)) run.write(out);
|
|
14087
14759
|
else if (Array.isArray(out) && Number.isFinite(out[0])) run.write(out[0]);
|
|
14088
14760
|
}
|
|
14089
14761
|
for (const run of effectVisibleRuns) {
|
|
14090
14762
|
if (run.sandbox.disabled) continue;
|
|
14091
|
-
run.sandbox.engine.frametime =
|
|
14763
|
+
run.sandbox.engine.frametime = animDt;
|
|
14092
14764
|
run.sandbox.engine.runtime = t;
|
|
14093
14765
|
const ret = run.sandbox.callUpdate(!!run.effect.visible);
|
|
14094
14766
|
if (typeof ret === "boolean") run.effect.visible = ret;
|
|
14095
14767
|
}
|
|
14096
14768
|
for (const run of generalScriptRuns) {
|
|
14097
14769
|
if (run.sandbox.disabled) continue;
|
|
14098
|
-
run.sandbox.engine.frametime =
|
|
14770
|
+
run.sandbox.engine.frametime = animDt;
|
|
14099
14771
|
run.sandbox.engine.runtime = t;
|
|
14100
14772
|
const g = scene.general || {};
|
|
14101
14773
|
const cur = g[run.field] && typeof g[run.field] === "object" && "value" in g[run.field] ? g[run.field].value : g[run.field];
|
|
@@ -14103,10 +14775,16 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14103
14775
|
if (ret !== void 0) run.write(ret);
|
|
14104
14776
|
}
|
|
14105
14777
|
const screenRes = { x: c.clientWidth || window.innerWidth || 1, y: c.clientHeight || window.innerHeight || 1 };
|
|
14778
|
+
for (const sb of propSandboxes) {
|
|
14779
|
+
if (!sb || sb.disabled) continue;
|
|
14780
|
+
sb.engine.frametime = animDt;
|
|
14781
|
+
sb.engine.runtime = t;
|
|
14782
|
+
sb.engine.screenResolution = screenRes;
|
|
14783
|
+
}
|
|
14106
14784
|
let visibilityDirty = false;
|
|
14107
14785
|
for (const run of objectScriptRuns) {
|
|
14108
14786
|
if (run.sandbox.disabled) continue;
|
|
14109
|
-
run.sandbox.engine.frametime =
|
|
14787
|
+
run.sandbox.engine.frametime = animDt;
|
|
14110
14788
|
run.sandbox.engine.runtime = t;
|
|
14111
14789
|
run.sandbox.engine.screenResolution = screenRes;
|
|
14112
14790
|
const cur = run.layer[run.field];
|
|
@@ -14127,20 +14805,23 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14127
14805
|
const n = Number(ret);
|
|
14128
14806
|
if (Number.isFinite(n)) run.layer[run.field] = n;
|
|
14129
14807
|
} else {
|
|
14130
|
-
const
|
|
14808
|
+
const slot = run.slot || run.field;
|
|
14809
|
+
const lcur = run.layer[slot];
|
|
14810
|
+
const v = run.field === "angles" ? wtext.radToScriptAngles(lcur) : { x: lcur[0] || 0, y: lcur[1] || 0, z: lcur[2] || 0 };
|
|
14131
14811
|
const ret = run.sandbox.callUpdate(v);
|
|
14132
14812
|
const o = ret && typeof ret === "object" && "x" in ret ? ret : v;
|
|
14133
|
-
run.layer[
|
|
14813
|
+
run.layer[slot] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
|
|
14134
14814
|
}
|
|
14135
14815
|
}
|
|
14136
14816
|
if (visibilityDirty) recomputeVisibility();
|
|
14817
|
+
if (transformDirty.size) scn.recomposeWorld(scene.layers, transformDirty);
|
|
14137
14818
|
if (audioSim.enabled) {
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14143
|
-
);
|
|
14819
|
+
hostAudio.pump();
|
|
14820
|
+
if (!hostAudio.active) {
|
|
14821
|
+
if (audioDriverRef.current) audioDriverRef.current.pump();
|
|
14822
|
+
else simAudio.update(t);
|
|
14823
|
+
}
|
|
14824
|
+
fillAudioBuffers(audioViews, activeAudioSnapshot());
|
|
14144
14825
|
}
|
|
14145
14826
|
if (attachFollows.length) {
|
|
14146
14827
|
mdl.followAttachments(attachFollows, t, getBoneOverrides);
|
|
@@ -14327,11 +15008,672 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14327
15008
|
}
|
|
14328
15009
|
})();
|
|
14329
15010
|
}
|
|
15011
|
+
const SHIM_MARK = 'data-we-shim="1"';
|
|
15012
|
+
const SHIM_ATTR = "data-we-shim-src";
|
|
15013
|
+
function entryDirUrl(entryUrl) {
|
|
15014
|
+
try {
|
|
15015
|
+
const u = new URL(entryUrl);
|
|
15016
|
+
const path = u.pathname;
|
|
15017
|
+
const i = path.lastIndexOf("/");
|
|
15018
|
+
u.pathname = i < 0 ? "/" : path.slice(0, i + 1);
|
|
15019
|
+
u.hash = "";
|
|
15020
|
+
u.search = "";
|
|
15021
|
+
return u.href;
|
|
15022
|
+
} catch {
|
|
15023
|
+
const s = entryUrl.replace(/[#?].*$/, "");
|
|
15024
|
+
const i = s.lastIndexOf("/");
|
|
15025
|
+
return i < 0 ? s : s.slice(0, i + 1);
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
function hasBlockingCsp(html) {
|
|
15029
|
+
const re = /<meta[^>]+http-equiv\s*=\s*["']?Content-Security-Policy["']?[^>]*>/gi;
|
|
15030
|
+
let m;
|
|
15031
|
+
while (m = re.exec(html)) {
|
|
15032
|
+
const tag = m[0];
|
|
15033
|
+
const content = /content\s*=\s*"([^"]*)"/i.exec(tag)?.[1] ?? /content\s*=\s*'([^']*)'/i.exec(tag)?.[1] ?? "";
|
|
15034
|
+
if (!/script-src/i.test(content)) continue;
|
|
15035
|
+
if (/script-src[^;]*'unsafe-inline'/i.test(content)) continue;
|
|
15036
|
+
if (/script-src[^;]*\*/i.test(content)) continue;
|
|
15037
|
+
return true;
|
|
15038
|
+
}
|
|
15039
|
+
return false;
|
|
15040
|
+
}
|
|
15041
|
+
function escapeScriptClose(js) {
|
|
15042
|
+
return js.replace(/<\/script/gi, "<\\/script");
|
|
15043
|
+
}
|
|
15044
|
+
function rewriteHtml(html, shimSource2, opts) {
|
|
15045
|
+
if (!html) html = "";
|
|
15046
|
+
if (html.includes(SHIM_MARK) || html.includes(SHIM_ATTR)) return html;
|
|
15047
|
+
const base = opts.baseHref && !/<base\b/i.test(html) ? `<base href="${opts.baseHref.replace(/"/g, """)}">` : "";
|
|
15048
|
+
const script = `<script ${SHIM_ATTR}="1">
|
|
15049
|
+
${escapeScriptClose(shimSource2)}
|
|
15050
|
+
<\/script>`;
|
|
15051
|
+
const seed = opts.seedScript && opts.seedScript.trim() ? `<script>
|
|
15052
|
+
${escapeScriptClose(opts.seedScript)}
|
|
15053
|
+
<\/script>` : "";
|
|
15054
|
+
const inject = `${base}${script}${seed}`;
|
|
15055
|
+
const headOpen = /<head(\s[^>]*)?>/i.exec(html);
|
|
15056
|
+
if (headOpen) {
|
|
15057
|
+
const at = headOpen.index + headOpen[0].length;
|
|
15058
|
+
return html.slice(0, at) + inject + html.slice(at);
|
|
15059
|
+
}
|
|
15060
|
+
const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
|
|
15061
|
+
if (htmlOpen) {
|
|
15062
|
+
const at = htmlOpen.index + htmlOpen[0].length;
|
|
15063
|
+
return html.slice(0, at) + `<head>${inject}</head>` + html.slice(at);
|
|
15064
|
+
}
|
|
15065
|
+
return `<!DOCTYPE html><html><head>${inject}</head><body>${html}</body></html>`;
|
|
15066
|
+
}
|
|
15067
|
+
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';
|
|
15068
|
+
function weShimCall(rt, call) {
|
|
15069
|
+
try {
|
|
15070
|
+
const win = rt.iframe?.contentWindow;
|
|
15071
|
+
if (win) call(win);
|
|
15072
|
+
} catch {
|
|
15073
|
+
}
|
|
15074
|
+
}
|
|
15075
|
+
function injectGpuThrottle(rt, f, _doc) {
|
|
15076
|
+
const win = f.contentWindow;
|
|
15077
|
+
if (!win) return;
|
|
15078
|
+
if (win.requestAnimationFrame?.__weThrottled) return;
|
|
15079
|
+
const fps = rt.cfg.sceneFps || 30;
|
|
15080
|
+
if (fps >= 60) return;
|
|
15081
|
+
const interval = 1e3 / fps;
|
|
15082
|
+
try {
|
|
15083
|
+
const origRaf = win.requestAnimationFrame.bind(win);
|
|
15084
|
+
const rafMap = /* @__PURE__ */ new Map();
|
|
15085
|
+
let counter = 0;
|
|
15086
|
+
win.requestAnimationFrame = (cb) => {
|
|
15087
|
+
const id = ++counter;
|
|
15088
|
+
const to = win.setTimeout(() => {
|
|
15089
|
+
rafMap.delete(id);
|
|
15090
|
+
origRaf((now) => {
|
|
15091
|
+
try {
|
|
15092
|
+
cb(now);
|
|
15093
|
+
} catch {
|
|
15094
|
+
}
|
|
15095
|
+
});
|
|
15096
|
+
}, interval);
|
|
15097
|
+
rafMap.set(id, to);
|
|
15098
|
+
return id;
|
|
15099
|
+
};
|
|
15100
|
+
win.cancelAnimationFrame = (id) => {
|
|
15101
|
+
const to = rafMap.get(id);
|
|
15102
|
+
if (to !== void 0) {
|
|
15103
|
+
win.clearTimeout(to);
|
|
15104
|
+
rafMap.delete(id);
|
|
15105
|
+
}
|
|
15106
|
+
};
|
|
15107
|
+
} catch {
|
|
15108
|
+
}
|
|
15109
|
+
}
|
|
15110
|
+
const pumpBuffer = new Float32Array(128);
|
|
15111
|
+
function packWebAudioArrayInto(out, left, right) {
|
|
15112
|
+
const nL = Math.min(64, left.length);
|
|
15113
|
+
const nR = Math.min(64, right.length);
|
|
15114
|
+
for (let i = 0; i < nL; i++) out[i] = Number(left[i]) || 0;
|
|
15115
|
+
for (let i = 0; i < nR; i++) out[64 + i] = Number(right[i]) || 0;
|
|
15116
|
+
return out;
|
|
15117
|
+
}
|
|
15118
|
+
const WEB_SIM_AUDIO_GAIN = 1.8;
|
|
15119
|
+
const WEB_SIM_AUDIO_GAMMA = 1.8;
|
|
15120
|
+
const WEB_AUDIO_PUMP_HZ = 30;
|
|
15121
|
+
function shapeWebAudioBand(pre) {
|
|
15122
|
+
const v = Number(pre) || 0;
|
|
15123
|
+
if (v <= 0) return 0;
|
|
15124
|
+
return Math.min(1, Math.pow(v, WEB_SIM_AUDIO_GAMMA) * WEB_SIM_AUDIO_GAIN);
|
|
15125
|
+
}
|
|
15126
|
+
function defaultAudioDriver() {
|
|
15127
|
+
const sim = createSimulatedAudio();
|
|
15128
|
+
const left = new Float32Array(64);
|
|
15129
|
+
const right = new Float32Array(64);
|
|
15130
|
+
return {
|
|
15131
|
+
tick(nowMs) {
|
|
15132
|
+
sim.update(nowMs / 1e3);
|
|
15133
|
+
},
|
|
15134
|
+
snapshot() {
|
|
15135
|
+
const s = sim.snapshot;
|
|
15136
|
+
const preL = s.preL64;
|
|
15137
|
+
const preR = s.preR64;
|
|
15138
|
+
for (let i = 0; i < 64; i++) {
|
|
15139
|
+
if (preL && preR) {
|
|
15140
|
+
left[i] = shapeWebAudioBand(preL[i]);
|
|
15141
|
+
right[i] = shapeWebAudioBand(preR[i]);
|
|
15142
|
+
} else {
|
|
15143
|
+
left[i] = (Number(s.left64[i]) || 0) * 0.2;
|
|
15144
|
+
right[i] = (Number(s.right64[i]) || 0) * 0.2;
|
|
15145
|
+
}
|
|
15146
|
+
}
|
|
15147
|
+
return { left, right };
|
|
15148
|
+
}
|
|
15149
|
+
};
|
|
15150
|
+
}
|
|
15151
|
+
function resolveContainer(rt, cfg) {
|
|
15152
|
+
if (rt.wrap) return rt.wrap;
|
|
15153
|
+
const el = cfg.canvas;
|
|
15154
|
+
if (!el) return null;
|
|
15155
|
+
if (el instanceof HTMLCanvasElement) {
|
|
15156
|
+
const parent = el.parentElement;
|
|
15157
|
+
if (parent) {
|
|
15158
|
+
reportDiag(rt, cfg, "网页壁纸挂在 canvas 父容器上(canvas 不能有子节点;更适合空 div)");
|
|
15159
|
+
return parent;
|
|
15160
|
+
}
|
|
15161
|
+
return null;
|
|
15162
|
+
}
|
|
15163
|
+
return el;
|
|
15164
|
+
}
|
|
15165
|
+
function buildSeedScript(props, fps, volume) {
|
|
15166
|
+
const parts = [];
|
|
15167
|
+
if (fps != null && Number.isFinite(fps)) parts.push(`window.__weSetFps(${Number(fps)});`);
|
|
15168
|
+
if (volume != null && Number.isFinite(volume)) {
|
|
15169
|
+
parts.push(`window.__weSetVolume(${Math.max(0, Math.min(1, Number(volume)))});`);
|
|
15170
|
+
}
|
|
15171
|
+
if (props && Object.keys(props).length) {
|
|
15172
|
+
parts.push(`window.__weSeedProps(${JSON.stringify(props)});`);
|
|
15173
|
+
}
|
|
15174
|
+
return parts.join("\n");
|
|
15175
|
+
}
|
|
15176
|
+
function installLetterboxFix(rt, f, container) {
|
|
15177
|
+
const BASE = "position:absolute;border:none;background:transparent;";
|
|
15178
|
+
const applyFull = () => {
|
|
15179
|
+
f.style.cssText = BASE + "inset:0;width:100%;height:100%;";
|
|
15180
|
+
};
|
|
15181
|
+
applyFull();
|
|
15182
|
+
let lastKey = "";
|
|
15183
|
+
const relayout = () => {
|
|
15184
|
+
if (!f.isConnected) return;
|
|
15185
|
+
let doc = null;
|
|
15186
|
+
try {
|
|
15187
|
+
doc = f.contentDocument;
|
|
15188
|
+
} catch {
|
|
15189
|
+
return;
|
|
15190
|
+
}
|
|
15191
|
+
if (!doc) return;
|
|
15192
|
+
const stageW = container.clientWidth || window.innerWidth || 0;
|
|
15193
|
+
const stageH = container.clientHeight || window.innerHeight || 0;
|
|
15194
|
+
const cover = normalizeFit(rt.cfg.fit) === "cover";
|
|
15195
|
+
applyFull();
|
|
15196
|
+
const box = cover && stageW > 0 && stageH > 0 ? measureWebLetterbox(doc) : null;
|
|
15197
|
+
const vp = box ? webCoverViewport(stageW, stageH, box.contentAspect) : null;
|
|
15198
|
+
const key = vp ? `${Math.round(vp.width)}x${Math.round(vp.height)}` : "full";
|
|
15199
|
+
if (!vp) {
|
|
15200
|
+
lastKey = "full";
|
|
15201
|
+
return;
|
|
15202
|
+
}
|
|
15203
|
+
f.style.cssText = BASE + `left:${vp.left}px;top:${vp.top}px;width:${vp.width}px;height:${vp.height}px;`;
|
|
15204
|
+
if (key !== lastKey) {
|
|
15205
|
+
lastKey = key;
|
|
15206
|
+
reportDiag(
|
|
15207
|
+
rt,
|
|
15208
|
+
rt.cfg,
|
|
15209
|
+
`网页壁纸露底自适配:视口按内容比例改为 ${Math.round(vp.width)}×${Math.round(vp.height)}(cover 居中裁切)`
|
|
15210
|
+
);
|
|
15211
|
+
}
|
|
15212
|
+
};
|
|
15213
|
+
const onResize = () => relayout();
|
|
15214
|
+
window.addEventListener("resize", onResize);
|
|
15215
|
+
let ro;
|
|
15216
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
15217
|
+
ro = new ResizeObserver(() => relayout());
|
|
15218
|
+
ro.observe(container);
|
|
15219
|
+
}
|
|
15220
|
+
const timers = [];
|
|
15221
|
+
const onLoad = () => {
|
|
15222
|
+
relayout();
|
|
15223
|
+
for (const d of [120, 400, 1200]) timers.push(window.setTimeout(relayout, d));
|
|
15224
|
+
try {
|
|
15225
|
+
const doc = f.contentDocument;
|
|
15226
|
+
if (doc) {
|
|
15227
|
+
for (const el of doc.querySelectorAll("video,img")) {
|
|
15228
|
+
el.addEventListener("loadedmetadata", relayout, { once: true });
|
|
15229
|
+
el.addEventListener("load", relayout, { once: true });
|
|
15230
|
+
}
|
|
15231
|
+
}
|
|
15232
|
+
} catch {
|
|
15233
|
+
}
|
|
15234
|
+
};
|
|
15235
|
+
f.addEventListener("load", onLoad);
|
|
15236
|
+
rt.webRelayout = relayout;
|
|
15237
|
+
const prev = rt.sceneCleanup;
|
|
15238
|
+
rt.sceneCleanup = () => {
|
|
15239
|
+
window.removeEventListener("resize", onResize);
|
|
15240
|
+
ro?.disconnect();
|
|
15241
|
+
for (const t of timers) clearTimeout(t);
|
|
15242
|
+
f.removeEventListener("load", onLoad);
|
|
15243
|
+
if (rt.webRelayout === relayout) rt.webRelayout = void 0;
|
|
15244
|
+
try {
|
|
15245
|
+
prev?.();
|
|
15246
|
+
} catch {
|
|
15247
|
+
}
|
|
15248
|
+
};
|
|
15249
|
+
}
|
|
15250
|
+
function webPointerToClient(u, v, stage, frame, client) {
|
|
15251
|
+
if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
|
|
15252
|
+
if (!(stage.width > 0) || !(stage.height > 0)) return null;
|
|
15253
|
+
const sx = frame.width > 0 && client.width > 0 ? frame.width / client.width : 1;
|
|
15254
|
+
const sy = frame.height > 0 && client.height > 0 ? frame.height / client.height : 1;
|
|
15255
|
+
return {
|
|
15256
|
+
x: (u * stage.width - (frame.left - stage.left)) / (sx || 1),
|
|
15257
|
+
y: (v * stage.height - (frame.top - stage.top)) / (sy || 1)
|
|
15258
|
+
};
|
|
15259
|
+
}
|
|
15260
|
+
function installWebPointerBridge(rt, f, container) {
|
|
15261
|
+
rt.pointerCtl = {
|
|
15262
|
+
push(p) {
|
|
15263
|
+
if (!f.isConnected) return;
|
|
15264
|
+
const cRect = container.getBoundingClientRect();
|
|
15265
|
+
const fRect = f.getBoundingClientRect();
|
|
15266
|
+
const pt = webPointerToClient(
|
|
15267
|
+
Number(p?.u),
|
|
15268
|
+
Number(p?.v),
|
|
15269
|
+
{
|
|
15270
|
+
left: cRect.left,
|
|
15271
|
+
top: cRect.top,
|
|
15272
|
+
width: cRect.width || container.clientWidth || window.innerWidth || 0,
|
|
15273
|
+
height: cRect.height || container.clientHeight || window.innerHeight || 0
|
|
15274
|
+
},
|
|
15275
|
+
{ left: fRect.left, top: fRect.top, width: fRect.width, height: fRect.height },
|
|
15276
|
+
{ width: f.clientWidth, height: f.clientHeight }
|
|
15277
|
+
);
|
|
15278
|
+
if (!pt) return;
|
|
15279
|
+
weShimCall(rt, (w) => w.__wePushPointer?.(pt.x, pt.y, Number(p.buttons) || 0));
|
|
15280
|
+
},
|
|
15281
|
+
leave() {
|
|
15282
|
+
weShimCall(rt, (w) => w.__wePointerLeave?.());
|
|
15283
|
+
}
|
|
15284
|
+
};
|
|
15285
|
+
}
|
|
15286
|
+
function attachIframe(rt, cfg, container, src, opts) {
|
|
15287
|
+
const f = document.createElement("iframe");
|
|
15288
|
+
f.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
15289
|
+
f.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:none;background:transparent;";
|
|
15290
|
+
if (!rt.wrap && getComputedStyle(container).position === "static") {
|
|
15291
|
+
container.style.position = "relative";
|
|
15292
|
+
}
|
|
15293
|
+
f.src = src;
|
|
15294
|
+
container.appendChild(f);
|
|
15295
|
+
rt.iframe = f;
|
|
15296
|
+
if (opts.blobUrl) {
|
|
15297
|
+
(rt.objectUrls ??= []).push(opts.blobUrl);
|
|
15298
|
+
}
|
|
15299
|
+
installLetterboxFix(rt, f, container);
|
|
15300
|
+
if (opts.injected) installWebPointerBridge(rt, f, container);
|
|
15301
|
+
const onFrameMsg = (ev) => {
|
|
15302
|
+
if (ev.source !== f.contentWindow) return;
|
|
15303
|
+
const data = ev.data;
|
|
15304
|
+
if (!data || data.op !== "we-frame") return;
|
|
15305
|
+
if (rt.paused) return;
|
|
15306
|
+
const t = typeof data.t === "number" ? data.t : performance.now();
|
|
15307
|
+
if (opts.frameClock) opts.frameClock.last = t;
|
|
15308
|
+
markFrame(rt, t);
|
|
15309
|
+
};
|
|
15310
|
+
window.addEventListener("message", onFrameMsg);
|
|
15311
|
+
const prevCleanup = rt.sceneCleanup;
|
|
15312
|
+
rt.sceneCleanup = () => {
|
|
15313
|
+
window.removeEventListener("message", onFrameMsg);
|
|
15314
|
+
try {
|
|
15315
|
+
prevCleanup?.();
|
|
15316
|
+
} catch {
|
|
15317
|
+
}
|
|
15318
|
+
};
|
|
15319
|
+
f.addEventListener("load", () => {
|
|
15320
|
+
try {
|
|
15321
|
+
const doc = f.contentDocument;
|
|
15322
|
+
if (doc) window.__blockContextMenu?.(doc);
|
|
15323
|
+
if (!opts.injected) injectGpuThrottle(rt, f, doc);
|
|
15324
|
+
} catch {
|
|
15325
|
+
}
|
|
15326
|
+
weShimCall(rt, (w2) => {
|
|
15327
|
+
const wire = {};
|
|
15328
|
+
for (const [k, v] of Object.entries(rt.liveUserProps ?? {})) wire[k] = { value: v };
|
|
15329
|
+
w2.__weApplyProps?.(wire);
|
|
15330
|
+
w2.__weSetFps?.(rt.cfg.sceneFps ?? 60);
|
|
15331
|
+
w2.__weSetVolume?.(rt.cfg.muted === false ? 1 : 0);
|
|
15332
|
+
if (rt.paused) w2.__weSetPaused?.(true);
|
|
15333
|
+
});
|
|
15334
|
+
try {
|
|
15335
|
+
rt.onFirstFrame?.();
|
|
15336
|
+
rt.onFirstFrame = void 0;
|
|
15337
|
+
} catch {
|
|
15338
|
+
}
|
|
15339
|
+
const w = container.clientWidth || window.innerWidth || 1;
|
|
15340
|
+
const h = container.clientHeight || window.innerHeight || 1;
|
|
15341
|
+
try {
|
|
15342
|
+
rt.onSceneInfo?.({
|
|
15343
|
+
width: w,
|
|
15344
|
+
height: h,
|
|
15345
|
+
layerCount: 0,
|
|
15346
|
+
hasModels: false,
|
|
15347
|
+
hasParticles: false,
|
|
15348
|
+
hasText: false
|
|
15349
|
+
});
|
|
15350
|
+
} catch {
|
|
15351
|
+
}
|
|
15352
|
+
});
|
|
15353
|
+
}
|
|
15354
|
+
const WEB_ASPECT_EPS = 5e-3;
|
|
15355
|
+
const WEB_LETTERBOX_MIN_RATIO = 0.01;
|
|
15356
|
+
const WEB_ASPECT_MIN = 0.2;
|
|
15357
|
+
const WEB_ASPECT_MAX = 6;
|
|
15358
|
+
function webCoverViewport(stageW, stageH, contentAspect) {
|
|
15359
|
+
if (!(stageW > 0) || !(stageH > 0) || !(contentAspect > 0)) return null;
|
|
15360
|
+
const stageAspect = stageW / stageH;
|
|
15361
|
+
if (Math.abs(stageAspect - contentAspect) <= WEB_ASPECT_EPS) return null;
|
|
15362
|
+
if (stageAspect < contentAspect) {
|
|
15363
|
+
const width = stageH * contentAspect;
|
|
15364
|
+
return { width, height: stageH, left: (stageW - width) / 2, top: 0 };
|
|
15365
|
+
}
|
|
15366
|
+
const height = stageW / contentAspect;
|
|
15367
|
+
return { width: stageW, height, left: 0, top: (stageH - height) / 2 };
|
|
15368
|
+
}
|
|
15369
|
+
function measureWebLetterbox(doc) {
|
|
15370
|
+
const win = doc.defaultView;
|
|
15371
|
+
if (!win) return null;
|
|
15372
|
+
const vw = win.innerWidth;
|
|
15373
|
+
const vh = win.innerHeight;
|
|
15374
|
+
if (!(vw > 0) || !(vh > 0)) return null;
|
|
15375
|
+
const cands = [...doc.querySelectorAll("video,img")];
|
|
15376
|
+
for (const el of cands) {
|
|
15377
|
+
const r = el.getBoundingClientRect();
|
|
15378
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
15379
|
+
if (r.width < vw * 0.98) continue;
|
|
15380
|
+
if (Math.abs(r.left) > vw * 0.02 || r.top > vh * 0.02) continue;
|
|
15381
|
+
if (vh - r.height < vh * WEB_LETTERBOX_MIN_RATIO) continue;
|
|
15382
|
+
const natW = el.videoWidth || el.naturalWidth || 0;
|
|
15383
|
+
const natH = el.videoHeight || el.naturalHeight || 0;
|
|
15384
|
+
if (!(natW > 0) || !(natH > 0)) continue;
|
|
15385
|
+
const aspect = natW / natH;
|
|
15386
|
+
if (!Number.isFinite(aspect) || aspect < WEB_ASPECT_MIN || aspect > WEB_ASPECT_MAX) continue;
|
|
15387
|
+
return { contentAspect: aspect };
|
|
15388
|
+
}
|
|
15389
|
+
return null;
|
|
15390
|
+
}
|
|
15391
|
+
function vecToCss(v) {
|
|
15392
|
+
if (!v) return "rgb(128,128,128)";
|
|
15393
|
+
const r = Math.round(Math.max(0, Math.min(1, Number(v.x) || 0)) * 255);
|
|
15394
|
+
const g = Math.round(Math.max(0, Math.min(1, Number(v.y) || 0)) * 255);
|
|
15395
|
+
const b = Math.round(Math.max(0, Math.min(1, Number(v.z) || 0)) * 255);
|
|
15396
|
+
return `rgb(${r},${g},${b})`;
|
|
15397
|
+
}
|
|
15398
|
+
function thumbDataUrlFromSnap(snap) {
|
|
15399
|
+
try {
|
|
15400
|
+
const c = document.createElement("canvas");
|
|
15401
|
+
c.width = c.height = 64;
|
|
15402
|
+
const ctx = c.getContext("2d");
|
|
15403
|
+
if (!ctx) return "";
|
|
15404
|
+
const p = snap.primaryColor;
|
|
15405
|
+
const s = snap.secondaryColor;
|
|
15406
|
+
const grd = ctx.createLinearGradient(0, 0, 64, 64);
|
|
15407
|
+
grd.addColorStop(0, vecToCss(p));
|
|
15408
|
+
grd.addColorStop(1, vecToCss(s));
|
|
15409
|
+
ctx.fillStyle = grd;
|
|
15410
|
+
ctx.fillRect(0, 0, 64, 64);
|
|
15411
|
+
return c.toDataURL("image/jpeg", 0.85);
|
|
15412
|
+
} catch {
|
|
15413
|
+
return "";
|
|
15414
|
+
}
|
|
15415
|
+
}
|
|
15416
|
+
function defaultMediaDriver() {
|
|
15417
|
+
return media.createSimulatedMedia();
|
|
15418
|
+
}
|
|
15419
|
+
function pushMediaDiff(rt, prev, snap) {
|
|
15420
|
+
const events = media.diffMediaEvents(prev, snap);
|
|
15421
|
+
for (const { name, event } of events) {
|
|
15422
|
+
if (name === "mediaStatusChanged") {
|
|
15423
|
+
weShimCall(rt, (w) => w.__wePushMedia?.({ op: "status", enabled: !!event.enabled }));
|
|
15424
|
+
} else if (name === "mediaPropertiesChanged") {
|
|
15425
|
+
weShimCall(
|
|
15426
|
+
rt,
|
|
15427
|
+
(w) => w.__wePushMedia?.({
|
|
15428
|
+
op: "properties",
|
|
15429
|
+
title: event.title ?? "",
|
|
15430
|
+
artist: event.artist ?? "",
|
|
15431
|
+
album: event.album ?? "",
|
|
15432
|
+
albumArtist: event.albumArtist ?? ""
|
|
15433
|
+
})
|
|
15434
|
+
);
|
|
15435
|
+
} else if (name === "mediaThumbnailChanged") {
|
|
15436
|
+
const thumb = thumbDataUrlFromSnap(snap);
|
|
15437
|
+
weShimCall(
|
|
15438
|
+
rt,
|
|
15439
|
+
(w) => w.__wePushMedia?.({
|
|
15440
|
+
op: "thumbnail",
|
|
15441
|
+
thumbnail: thumb,
|
|
15442
|
+
hasThumbnail: !!event.hasThumbnail || !!thumb,
|
|
15443
|
+
primaryColor: vecToCss(event.primaryColor),
|
|
15444
|
+
secondaryColor: vecToCss(event.secondaryColor),
|
|
15445
|
+
tertiaryColor: vecToCss(event.tertiaryColor),
|
|
15446
|
+
textColor: vecToCss(event.textColor),
|
|
15447
|
+
highContrastColor: vecToCss(event.highContrastColor)
|
|
15448
|
+
})
|
|
15449
|
+
);
|
|
15450
|
+
} else if (name === "mediaPlaybackChanged") {
|
|
15451
|
+
weShimCall(rt, (w) => w.__wePushMedia?.({ op: "playback", state: Number(event.state) || 0 }));
|
|
15452
|
+
} else if (name === "mediaTimelineChanged") {
|
|
15453
|
+
weShimCall(
|
|
15454
|
+
rt,
|
|
15455
|
+
(w) => w.__wePushMedia?.({
|
|
15456
|
+
op: "timeline",
|
|
15457
|
+
position: Number(event.position) || 0,
|
|
15458
|
+
duration: Number(event.duration) || 0
|
|
15459
|
+
})
|
|
15460
|
+
);
|
|
15461
|
+
}
|
|
15462
|
+
}
|
|
15463
|
+
return media.cloneMediaSnapshot(snap);
|
|
15464
|
+
}
|
|
15465
|
+
function startAudioPump(rt, driver, frameClock) {
|
|
15466
|
+
if (!driver) return;
|
|
15467
|
+
let raf = 0;
|
|
15468
|
+
let lastPush = 0;
|
|
15469
|
+
const tick = (now) => {
|
|
15470
|
+
raf = requestAnimationFrame(tick);
|
|
15471
|
+
if (rt.paused || !rt.iframe) return;
|
|
15472
|
+
const fps = rt.cfg.sceneFps || 60;
|
|
15473
|
+
const pumpFps = Math.min(Math.max(1, fps), WEB_AUDIO_PUMP_HZ);
|
|
15474
|
+
const interval = 1e3 / pumpFps;
|
|
15475
|
+
if (now - lastPush < interval * 0.85) return;
|
|
15476
|
+
lastPush = now;
|
|
15477
|
+
try {
|
|
15478
|
+
driver.tick?.(now);
|
|
15479
|
+
const snap = driver.snapshot();
|
|
15480
|
+
const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
|
|
15481
|
+
weShimCall(rt, (w) => w.__wePushAudio?.(arr));
|
|
15482
|
+
if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
|
|
15483
|
+
} catch {
|
|
15484
|
+
}
|
|
15485
|
+
};
|
|
15486
|
+
raf = requestAnimationFrame(tick);
|
|
15487
|
+
const prev = rt.sceneCleanup;
|
|
15488
|
+
rt.sceneCleanup = () => {
|
|
15489
|
+
cancelAnimationFrame(raf);
|
|
15490
|
+
try {
|
|
15491
|
+
prev?.();
|
|
15492
|
+
} catch {
|
|
15493
|
+
}
|
|
15494
|
+
};
|
|
15495
|
+
}
|
|
15496
|
+
function startMediaPump(rt, driver) {
|
|
15497
|
+
if (!driver) return;
|
|
15498
|
+
let raf = 0;
|
|
15499
|
+
let lastMedia = null;
|
|
15500
|
+
let lastTick = 0;
|
|
15501
|
+
const tick = (now) => {
|
|
15502
|
+
raf = requestAnimationFrame(tick);
|
|
15503
|
+
if (rt.paused || !rt.iframe) return;
|
|
15504
|
+
if (now - lastTick < 200) return;
|
|
15505
|
+
lastTick = now;
|
|
15506
|
+
try {
|
|
15507
|
+
driver.update(now / 1e3);
|
|
15508
|
+
lastMedia = pushMediaDiff(rt, lastMedia, driver.snapshot);
|
|
15509
|
+
} catch {
|
|
15510
|
+
}
|
|
15511
|
+
};
|
|
15512
|
+
raf = requestAnimationFrame(tick);
|
|
15513
|
+
const prev = rt.sceneCleanup;
|
|
15514
|
+
rt.sceneCleanup = () => {
|
|
15515
|
+
cancelAnimationFrame(raf);
|
|
15516
|
+
try {
|
|
15517
|
+
prev?.();
|
|
15518
|
+
} catch {
|
|
15519
|
+
}
|
|
15520
|
+
};
|
|
15521
|
+
}
|
|
15522
|
+
function installWebCtl(rt) {
|
|
15523
|
+
rt.sceneCtl = {
|
|
15524
|
+
pause() {
|
|
15525
|
+
rt.paused = true;
|
|
15526
|
+
weShimCall(rt, (w) => w.__weSetPaused?.(true));
|
|
15527
|
+
},
|
|
15528
|
+
resume() {
|
|
15529
|
+
rt.paused = false;
|
|
15530
|
+
weShimCall(rt, (w) => w.__weSetPaused?.(false));
|
|
15531
|
+
},
|
|
15532
|
+
applyUserProperties(props) {
|
|
15533
|
+
const flat = { ...rt.liveUserProps ?? {} };
|
|
15534
|
+
for (const [k, v] of Object.entries(props ?? {})) {
|
|
15535
|
+
const val = v && typeof v === "object" && "value" in v ? v.value : v;
|
|
15536
|
+
flat[k] = val;
|
|
15537
|
+
}
|
|
15538
|
+
rt.liveUserProps = flat;
|
|
15539
|
+
weShimCall(rt, (w) => w.__weApplyProps?.(props));
|
|
15540
|
+
}
|
|
15541
|
+
};
|
|
15542
|
+
}
|
|
15543
|
+
function isSameOriginUrl(url) {
|
|
15544
|
+
try {
|
|
15545
|
+
return new URL(url, location.href).origin === location.origin;
|
|
15546
|
+
} catch {
|
|
15547
|
+
return false;
|
|
15548
|
+
}
|
|
15549
|
+
}
|
|
15550
|
+
function projectPropertiesToWire(project) {
|
|
15551
|
+
const props = project?.general?.properties;
|
|
15552
|
+
if (!props || typeof props !== "object") return {};
|
|
15553
|
+
const out = {};
|
|
15554
|
+
for (const [name, def] of Object.entries(props)) {
|
|
15555
|
+
if (!def || typeof def !== "object" || typeof def.type !== "string") continue;
|
|
15556
|
+
const raw = def.value;
|
|
15557
|
+
const type = def.type.toLowerCase();
|
|
15558
|
+
if (raw === null || raw === void 0) {
|
|
15559
|
+
if (type === "file" || type === "directory") {
|
|
15560
|
+
out[name] = { value: "" };
|
|
15561
|
+
continue;
|
|
15562
|
+
}
|
|
15563
|
+
if (!("value" in def)) continue;
|
|
15564
|
+
}
|
|
15565
|
+
out[name] = { value: raw };
|
|
15566
|
+
}
|
|
15567
|
+
return out;
|
|
15568
|
+
}
|
|
15569
|
+
async function fetchProjectWire(entryUrl) {
|
|
15570
|
+
try {
|
|
15571
|
+
const projUrl = new URL("project.json", new URL(entryUrl, location.href));
|
|
15572
|
+
const r = await fetch(projUrl.href, { credentials: "same-origin" });
|
|
15573
|
+
if (!r.ok) return {};
|
|
15574
|
+
return projectPropertiesToWire(await r.json());
|
|
15575
|
+
} catch {
|
|
15576
|
+
return {};
|
|
15577
|
+
}
|
|
15578
|
+
}
|
|
15579
|
+
function mergeLiveIntoWire(defaults, live) {
|
|
15580
|
+
const out = { ...defaults };
|
|
15581
|
+
if (live) {
|
|
15582
|
+
for (const [k, v] of Object.entries(live)) out[k] = { value: v };
|
|
15583
|
+
}
|
|
15584
|
+
return out;
|
|
15585
|
+
}
|
|
15586
|
+
function mountWeb(rt, cfg) {
|
|
15587
|
+
clear(rt);
|
|
15588
|
+
rt.cfg = cfg;
|
|
15589
|
+
const container = resolveContainer(rt, cfg);
|
|
15590
|
+
if (!container) {
|
|
15591
|
+
reportDiag(rt, cfg, "网页壁纸:无可用容器");
|
|
15592
|
+
rt.onError?.(new Error("网页壁纸:无可用容器"));
|
|
15593
|
+
return;
|
|
15594
|
+
}
|
|
15595
|
+
const entry = cfg.src ?? "";
|
|
15596
|
+
if (!entry) {
|
|
15597
|
+
reportDiag(rt, cfg, "网页壁纸:缺少 src");
|
|
15598
|
+
rt.onError?.(new Error("网页壁纸:缺少 src"));
|
|
15599
|
+
return;
|
|
15600
|
+
}
|
|
15601
|
+
installWebCtl(rt);
|
|
15602
|
+
const cfgExt = cfg;
|
|
15603
|
+
const audioDriver = cfgExt._webAudio === null ? null : cfgExt._webAudio ?? defaultAudioDriver();
|
|
15604
|
+
const mediaDriver = cfgExt._webMedia === null ? null : cfgExt._webMedia ?? defaultMediaDriver();
|
|
15605
|
+
const finishBare = (why) => {
|
|
15606
|
+
reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
|
|
15607
|
+
attachIframe(rt, cfg, container, entry, { injected: false });
|
|
15608
|
+
startAudioPump(rt, null);
|
|
15609
|
+
startMediaPump(rt, null);
|
|
15610
|
+
};
|
|
15611
|
+
const frameClock = { last: 0 };
|
|
15612
|
+
const startPumps = () => {
|
|
15613
|
+
startAudioPump(rt, audioDriver, frameClock);
|
|
15614
|
+
startMediaPump(rt, mediaDriver);
|
|
15615
|
+
};
|
|
15616
|
+
void (async () => {
|
|
15617
|
+
const defaults = await fetchProjectWire(entry);
|
|
15618
|
+
const wire = mergeLiveIntoWire(defaults, rt.liveUserProps);
|
|
15619
|
+
rt.liveUserProps = Object.fromEntries(Object.entries(wire).map(([k, w]) => [k, w.value]));
|
|
15620
|
+
if (isSameOriginUrl(entry)) {
|
|
15621
|
+
attachIframe(rt, cfg, container, entry, { injected: true, frameClock });
|
|
15622
|
+
startPumps();
|
|
15623
|
+
const f = rt.iframe;
|
|
15624
|
+
f?.addEventListener(
|
|
15625
|
+
"load",
|
|
15626
|
+
() => {
|
|
15627
|
+
let hasShim = false;
|
|
15628
|
+
weShimCall(rt, (w) => {
|
|
15629
|
+
hasShim = typeof w.__weSetPaused === "function";
|
|
15630
|
+
});
|
|
15631
|
+
if (!hasShim) {
|
|
15632
|
+
reportDiag(
|
|
15633
|
+
rt,
|
|
15634
|
+
cfg,
|
|
15635
|
+
"网页壁纸:同源入口未检测到 WE shim(host 未注入?);Spine 类壁纸请确认 /web/ HTML 改写"
|
|
15636
|
+
);
|
|
15637
|
+
}
|
|
15638
|
+
},
|
|
15639
|
+
{ once: true }
|
|
15640
|
+
);
|
|
15641
|
+
return;
|
|
15642
|
+
}
|
|
15643
|
+
try {
|
|
15644
|
+
const res = await fetch(entry, { credentials: "same-origin" });
|
|
15645
|
+
if (!res.ok) {
|
|
15646
|
+
finishBare(`HTTP ${res.status}`);
|
|
15647
|
+
return;
|
|
15648
|
+
}
|
|
15649
|
+
const html = await res.text();
|
|
15650
|
+
if (hasBlockingCsp(html)) {
|
|
15651
|
+
finishBare("CSP 阻止 inline script");
|
|
15652
|
+
return;
|
|
15653
|
+
}
|
|
15654
|
+
const rewritten = rewriteHtml(html, shimSource, {
|
|
15655
|
+
baseHref: entryDirUrl(entry),
|
|
15656
|
+
seedScript: buildSeedScript(wire, cfg.sceneFps, cfg.muted === false ? 1 : 0)
|
|
15657
|
+
});
|
|
15658
|
+
const blob = new Blob([rewritten], { type: "text/html;charset=utf-8" });
|
|
15659
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
15660
|
+
attachIframe(rt, cfg, container, blobUrl, { blobUrl, injected: true, frameClock });
|
|
15661
|
+
startPumps();
|
|
15662
|
+
} catch (e) {
|
|
15663
|
+
finishBare(e instanceof Error ? e.message : String(e));
|
|
15664
|
+
}
|
|
15665
|
+
})();
|
|
15666
|
+
}
|
|
14330
15667
|
function mountWallpaper(rt, cfg) {
|
|
14331
|
-
|
|
15668
|
+
const type = String(cfg.type ?? "").toLowerCase();
|
|
15669
|
+
cfg = { ...cfg, type };
|
|
15670
|
+
rt.cfg = cfg;
|
|
15671
|
+
if ((type === "video" || type === "gif" || type === "image") && cfg.src) {
|
|
14332
15672
|
mountMedia(rt, cfg);
|
|
14333
|
-
} else if (
|
|
15673
|
+
} else if (type === "scene" && (cfg.source || cfg.src)) {
|
|
14334
15674
|
mountScene(rt, cfg);
|
|
15675
|
+
} else if (type === "web" && cfg.src) {
|
|
15676
|
+
mountWeb(rt, cfg);
|
|
14335
15677
|
} else {
|
|
14336
15678
|
rt.onUnhandledType?.(cfg);
|
|
14337
15679
|
}
|
|
@@ -14341,22 +15683,60 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14341
15683
|
if (fit === "fill") return "cover";
|
|
14342
15684
|
return fit === "contain" || fit === "stretch" ? fit : "cover";
|
|
14343
15685
|
}
|
|
14344
|
-
function
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
15686
|
+
function isWebProject(project) {
|
|
15687
|
+
const t = project?.type;
|
|
15688
|
+
return typeof t === "string" && t.toLowerCase() === "web";
|
|
15689
|
+
}
|
|
15690
|
+
function ensureSceneCanvas(el) {
|
|
15691
|
+
if (el instanceof HTMLCanvasElement) return el;
|
|
15692
|
+
const existing = el.querySelector(":scope > canvas[data-webwallgl]");
|
|
15693
|
+
if (existing instanceof HTMLCanvasElement) return existing;
|
|
15694
|
+
const c = document.createElement("canvas");
|
|
15695
|
+
c.setAttribute("data-webwallgl", "1");
|
|
15696
|
+
c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
|
|
15697
|
+
if (getComputedStyle(el).position === "static") el.style.position = "relative";
|
|
15698
|
+
el.appendChild(c);
|
|
15699
|
+
return c;
|
|
15700
|
+
}
|
|
15701
|
+
async function resolveMountConfig(el, o) {
|
|
15702
|
+
const base = {
|
|
14349
15703
|
fit: normalizeFitOption(o.fit),
|
|
14350
15704
|
renderDpr: o.renderDpr ?? 1,
|
|
14351
15705
|
sceneFps: o.fps ?? 60,
|
|
14352
15706
|
muted: (o.volume ?? 0) <= 0,
|
|
14353
|
-
loop: true
|
|
15707
|
+
loop: true,
|
|
15708
|
+
canvas: el,
|
|
15709
|
+
source: o.source
|
|
14354
15710
|
};
|
|
15711
|
+
let project = null;
|
|
15712
|
+
try {
|
|
15713
|
+
project = await o.source.project?.() ?? null;
|
|
15714
|
+
} catch {
|
|
15715
|
+
project = null;
|
|
15716
|
+
}
|
|
15717
|
+
if (isWebProject(project)) {
|
|
15718
|
+
let url;
|
|
15719
|
+
try {
|
|
15720
|
+
const entry = await o.source.webEntry?.();
|
|
15721
|
+
url = entry?.url;
|
|
15722
|
+
} catch {
|
|
15723
|
+
url = void 0;
|
|
15724
|
+
}
|
|
15725
|
+
if (!url && o.source.key) {
|
|
15726
|
+
const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") || "index.html" : "index.html";
|
|
15727
|
+
url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
|
|
15728
|
+
}
|
|
15729
|
+
if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
|
|
15730
|
+
return { ...base, type: "web", src: url, source: o.source };
|
|
15731
|
+
}
|
|
15732
|
+
const canvas = ensureSceneCanvas(el);
|
|
15733
|
+
return { ...base, type: "scene", canvas, source: o.source };
|
|
14355
15734
|
}
|
|
14356
|
-
function createScene(
|
|
15735
|
+
function createScene(el, options) {
|
|
14357
15736
|
const rt = createRuntime();
|
|
14358
15737
|
const events = { ready: [], error: [], diagnostic: [] };
|
|
14359
15738
|
let currentOptions = { ...options ?? {}, source: null };
|
|
15739
|
+
let boundEl = el;
|
|
14360
15740
|
const emitError = (err) => {
|
|
14361
15741
|
for (const fn of events.error) {
|
|
14362
15742
|
try {
|
|
@@ -14395,6 +15775,24 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14395
15775
|
rt.onFirstFrame = () => {
|
|
14396
15776
|
rt.onFirstFrame = void 0;
|
|
14397
15777
|
prev?.();
|
|
15778
|
+
const info = rt.info ?? {
|
|
15779
|
+
width: 0,
|
|
15780
|
+
height: 0,
|
|
15781
|
+
layerCount: 0,
|
|
15782
|
+
hasModels: false,
|
|
15783
|
+
hasParticles: false,
|
|
15784
|
+
hasText: false
|
|
15785
|
+
};
|
|
15786
|
+
try {
|
|
15787
|
+
currentOptions.onReady?.(info);
|
|
15788
|
+
} catch {
|
|
15789
|
+
}
|
|
15790
|
+
for (const fn of events.ready) {
|
|
15791
|
+
try {
|
|
15792
|
+
fn(info);
|
|
15793
|
+
} catch {
|
|
15794
|
+
}
|
|
15795
|
+
}
|
|
14398
15796
|
resolve();
|
|
14399
15797
|
};
|
|
14400
15798
|
});
|
|
@@ -14408,7 +15806,9 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14408
15806
|
return { promise, off };
|
|
14409
15807
|
};
|
|
14410
15808
|
const instance = {
|
|
14411
|
-
canvas
|
|
15809
|
+
get canvas() {
|
|
15810
|
+
return boundEl;
|
|
15811
|
+
},
|
|
14412
15812
|
pause() {
|
|
14413
15813
|
rt.paused = true;
|
|
14414
15814
|
rt.sceneCtl?.pause();
|
|
@@ -14427,11 +15827,13 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14427
15827
|
},
|
|
14428
15828
|
setFps(fps) {
|
|
14429
15829
|
rt.cfg.sceneFps = fps;
|
|
15830
|
+
weShimCall(rt, (w) => w.__weSetFps?.(fps));
|
|
14430
15831
|
},
|
|
14431
15832
|
setVolume(volume) {
|
|
14432
15833
|
const v = Math.max(0, Math.min(1, volume));
|
|
14433
15834
|
rt.cfg.muted = v <= 0;
|
|
14434
15835
|
rt.sceneAudio?.setVolume(v);
|
|
15836
|
+
weShimCall(rt, (w) => w.__weSetVolume?.(v));
|
|
14435
15837
|
},
|
|
14436
15838
|
setRenderDpr(dpr) {
|
|
14437
15839
|
rt.cfg.renderDpr = dpr;
|
|
@@ -14448,17 +15850,17 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14448
15850
|
async load(source) {
|
|
14449
15851
|
currentOptions = { ...currentOptions, source };
|
|
14450
15852
|
wireOptions(currentOptions);
|
|
14451
|
-
const cfg =
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
src: void 0,
|
|
14456
|
-
mediaBase: void 0
|
|
14457
|
-
};
|
|
15853
|
+
const cfg = await resolveMountConfig(boundEl, currentOptions);
|
|
15854
|
+
if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
|
|
15855
|
+
boundEl = cfg.canvas;
|
|
15856
|
+
}
|
|
14458
15857
|
rt.cfg = cfg;
|
|
14459
15858
|
rt.paused = false;
|
|
14460
15859
|
rt.info = void 0;
|
|
14461
15860
|
resetCoverAlign(rt);
|
|
15861
|
+
if (currentOptions.properties) {
|
|
15862
|
+
rt.liveUserProps = { ...currentOptions.properties };
|
|
15863
|
+
}
|
|
14462
15864
|
const firstFrame = armFirstFrame();
|
|
14463
15865
|
const failure = armFailure();
|
|
14464
15866
|
mountWallpaper(rt, cfg);
|
|
@@ -14502,10 +15904,17 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14502
15904
|
const applyOptions = async (o) => {
|
|
14503
15905
|
currentOptions = o;
|
|
14504
15906
|
wireOptions(o);
|
|
14505
|
-
|
|
15907
|
+
const cfg = await resolveMountConfig(el, o);
|
|
15908
|
+
if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
|
|
15909
|
+
boundEl = cfg.canvas;
|
|
15910
|
+
}
|
|
15911
|
+
rt.cfg = cfg;
|
|
14506
15912
|
rt.paused = o.autoplay === false;
|
|
14507
15913
|
rt.info = void 0;
|
|
14508
15914
|
resetCoverAlign(rt);
|
|
15915
|
+
if (o.properties && Object.keys(o.properties).length) {
|
|
15916
|
+
rt.liveUserProps = { ...o.properties };
|
|
15917
|
+
}
|
|
14509
15918
|
const firstFrame = armFirstFrame();
|
|
14510
15919
|
const failure = armFailure();
|
|
14511
15920
|
mountWallpaper(rt, rt.cfg);
|
|
@@ -14520,8 +15929,8 @@ vec3 DecompressNormal(vec4 tex) {
|
|
|
14520
15929
|
instance.__applyOptions = applyOptions;
|
|
14521
15930
|
return instance;
|
|
14522
15931
|
}
|
|
14523
|
-
async function mount(
|
|
14524
|
-
const instance = createScene(
|
|
15932
|
+
async function mount(el, options) {
|
|
15933
|
+
const instance = createScene(el, options);
|
|
14525
15934
|
const withApply = instance;
|
|
14526
15935
|
await withApply.__applyOptions(options);
|
|
14527
15936
|
return instance;
|