webwallgl 1.0.0 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,14 +33,42 @@
33
33
  window.__blockContextMenu = (doc) => doc.addEventListener("contextmenu", block, true);
34
34
  })();
35
35
  function clear(rt) {
36
- if (rt.wrap) rt.wrap.innerHTML = "";
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();
40
53
  rt.videoPairs = void 0;
41
- if (rt.sceneCleanup) rt.sceneCleanup();
54
+ if (rt.sceneCleanup) {
55
+ try {
56
+ rt.sceneCleanup();
57
+ } catch {
58
+ }
59
+ }
42
60
  rt.sceneCleanup = void 0;
43
61
  rt.sceneCtl = void 0;
62
+ rt.pointerCtl = void 0;
63
+ rt.mediaCtl = void 0;
64
+ const ds = rt.wallpaperDisposers ?? [];
65
+ rt.wallpaperDisposers = [];
66
+ for (const d of ds) {
67
+ try {
68
+ d();
69
+ } catch {
70
+ }
71
+ }
44
72
  if (rt.renderer) {
45
73
  rt.renderer.dispose?.();
46
74
  rt.renderer = void 0;
@@ -82,10 +110,18 @@
82
110
  rt.canvas = void 0;
83
111
  rt.ctx = void 0;
84
112
  rt.info = void 0;
113
+ clearSceneDebugGlobals();
85
114
  resetFrameMeter(rt);
86
115
  }
87
116
  function destroyRuntime(rt) {
88
- clear(rt);
117
+ try {
118
+ clear(rt);
119
+ } catch {
120
+ }
121
+ if (rt.peekRaf !== void 0) {
122
+ cancelAnimationFrame(rt.peekRaf);
123
+ rt.peekRaf = void 0;
124
+ }
89
125
  for (const off of rt.disposers.splice(0)) {
90
126
  try {
91
127
  off();
@@ -124,7 +160,7 @@
124
160
  } catch {
125
161
  }
126
162
  try {
127
- const origin = cfg.mediaBase ? new URL(cfg.mediaBase).origin : "";
163
+ const origin = cfg.mediaBase ? new URL(cfg.mediaBase, window.location.href).origin : "";
128
164
  if (origin) {
129
165
  const img = new Image();
130
166
  img.src = `${origin}/diag?msg=${encodeURIComponent(`scene ${cfg.src ?? "?"}: ${msg.slice(0, 500)}`)}`;
@@ -141,6 +177,32 @@
141
177
  const obj = rt.video ?? rt.img;
142
178
  if (obj && obj.isConnected) obj.style.objectPosition = pos;
143
179
  }
180
+ const SCENE_DEBUG_GLOBALS = [
181
+ "__scene",
182
+ "__sceneLayers",
183
+ "__textures",
184
+ "__objScripts",
185
+ "__textWidgets",
186
+ "__mediaHooks",
187
+ "__mediaControl",
188
+ "__mediaStats",
189
+ "__mediaSet",
190
+ "__system",
191
+ "__liveSystem",
192
+ "__audioStats",
193
+ "__audioMute",
194
+ "__particleStats",
195
+ "__particleToggle",
196
+ "__pointerStats",
197
+ "__compositeStats",
198
+ "__compositeEnable"
199
+ ];
200
+ function clearSceneDebugGlobals() {
201
+ const w = window;
202
+ for (const k of SCENE_DEBUG_GLOBALS) {
203
+ if (k in w) delete w[k];
204
+ }
205
+ }
144
206
  function syncCanvasSize(rt, canvas, cfg) {
145
207
  const dpr = effectiveDpr(rt, cfg);
146
208
  const w = Math.max(1, Math.round((canvas.clientWidth || window.innerWidth || 1) * dpr));
@@ -867,6 +929,31 @@
867
929
  }
868
930
  return dflt;
869
931
  }
932
+ function isRenderInert(o) {
933
+ if (!o) return false;
934
+ return !o.image && !o.model && !o.particle && o.text == null && !o.size;
935
+ }
936
+ function composeChildTransform(parentWorld, childLocal, parentScalePropagates) {
937
+ const pscale = parentScalePropagates ? parentWorld.scale : [1, 1, 1];
938
+ const ca = (parentWorld.angles[2] || 0) * Math.PI / 180;
939
+ const cos = Math.cos(ca);
940
+ const sin = Math.sin(ca);
941
+ const ox = childLocal.origin[0] * pscale[0];
942
+ const oy = childLocal.origin[1] * pscale[1];
943
+ return {
944
+ origin: [
945
+ parentWorld.origin[0] + ox * cos - oy * sin,
946
+ parentWorld.origin[1] + ox * sin + oy * cos,
947
+ parentWorld.origin[2] + (childLocal.origin[2] || 0)
948
+ ],
949
+ scale: [
950
+ pscale[0] * childLocal.scale[0],
951
+ pscale[1] * childLocal.scale[1],
952
+ pscale[2] * childLocal.scale[2]
953
+ ],
954
+ angles: [childLocal.angles[0], childLocal.angles[1], (parentWorld.angles[2] || 0) + childLocal.angles[2]]
955
+ };
956
+ }
870
957
  function parseScene(sceneJson, project) {
871
958
  const properties = project && project.general && project.general.properties || {};
872
959
  const objects = sceneJson.objects || [];
@@ -883,6 +970,11 @@
883
970
  scale: parseVec3(o.scale || "1 1 1"),
884
971
  angles: parseVec3(o.angles || "0 0 0")
885
972
  }));
973
+ const localSnapshot = local.map((c) => ({
974
+ origin: c.origin.slice(),
975
+ scale: c.scale.slice(),
976
+ angles: c.angles.slice()
977
+ }));
886
978
  for (let pass = 0; pass < 8; pass++) {
887
979
  let changed = false;
888
980
  for (const c of local) {
@@ -896,20 +988,11 @@
896
988
  const pr = objects[pIdx];
897
989
  const prs = pr.scale;
898
990
  const runtimeBound = prs !== null && typeof prs === "object" && (typeof prs.script === "string" || prs.user !== void 0);
899
- const renderInert = !pr.image && !pr.model && !pr.particle && pr.text == null && !pr.size;
900
- const pscale = runtimeBound && renderInert ? [1, 1, 1] : pc.scale;
901
- const ca = pc.angles[2] * Math.PI / 180;
902
- const cos = Math.cos(ca);
903
- const sin = Math.sin(ca);
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];
991
+ const propagateScale = !(runtimeBound && isRenderInert(pr));
992
+ const w = composeChildTransform(pc, c, propagateScale);
993
+ c.origin = w.origin;
994
+ c.scale = w.scale;
995
+ c.angles = w.angles;
913
996
  c.parent = null;
914
997
  changed = true;
915
998
  }
@@ -1129,6 +1212,18 @@
1129
1212
  origin: layerOrigin,
1130
1213
  scale: world.scale,
1131
1214
  angles: world.angles,
1215
+ // [we-scene patch] 父级相对变换(WE 场景图的真实语义)。origin/scale/angles
1216
+ // 上的脚本与关键帧动画一律在这层空间收发,再由 recomposeWorld 合成回上面的
1217
+ // world 三件套。渲染 / hittest / getTransformMatrix 仍只读 world,不受影响。
1218
+ // isPostProcess 层的 world 被强制成整幅画布,local 对它无意义(recompose 跳过)。
1219
+ localOrigin: localSnapshot[i].origin,
1220
+ localScale: localSnapshot[i].scale,
1221
+ localAngles: localSnapshot[i].angles,
1222
+ // 「渲染惰性纯容器」:父 scale 是否传给子层由父级这个标志决定,
1223
+ // 判据与 parse 合并阶段逐字相同(见 isRenderInert)。
1224
+ renderInert: isRenderInert(o),
1225
+ // 父 scale 绑了脚本/用户属性(运行时可变)。与 renderInert 一起决定传播闸门。
1226
+ scaleRuntimeBound: !!(o.scale !== null && typeof o.scale === "object" && (typeof o.scale.script === "string" || o.scale.user !== void 0)),
1132
1227
  size: layerSize,
1133
1228
  alignment: o.alignment || "center",
1134
1229
  color: parseColor(o.color),
@@ -1239,15 +1334,110 @@
1239
1334
  cropoffset: modelJson.cropoffset ? parseVec2(modelJson.cropoffset) : null
1240
1335
  };
1241
1336
  }
1337
+ function recomposeWorld(layers, dirty) {
1338
+ if (!layers || layers.length === 0) return;
1339
+ const byId = /* @__PURE__ */ new Map();
1340
+ for (const l of layers) {
1341
+ if (l && l.id !== void 0 && l.id !== null) byId.set(l.id, l);
1342
+ }
1343
+ const depthOf = (l) => {
1344
+ let d = 0;
1345
+ let p = l.parentId;
1346
+ for (let guard = 0; p !== void 0 && p !== null && guard < 64; guard++) {
1347
+ const parent = byId.get(p);
1348
+ if (!parent) break;
1349
+ d++;
1350
+ p = parent.parentId;
1351
+ }
1352
+ return d;
1353
+ };
1354
+ const targets = [];
1355
+ for (const l of layers) {
1356
+ if (!l || !l.localOrigin) continue;
1357
+ if (l.isPostProcess) continue;
1358
+ if (dirty && !dirty.has(l.id)) continue;
1359
+ targets.push(l);
1360
+ }
1361
+ targets.sort((a, b) => depthOf(a) - depthOf(b));
1362
+ for (const l of targets) {
1363
+ const parent = l.parentId !== void 0 && l.parentId !== null ? byId.get(l.parentId) : null;
1364
+ let w;
1365
+ if (!parent) {
1366
+ w = { origin: l.localOrigin.slice(), scale: l.localScale.slice(), angles: l.localAngles.slice() };
1367
+ } else {
1368
+ const propagateScale = !(parent.scaleRuntimeBound && parent.renderInert);
1369
+ w = composeChildTransform(
1370
+ { origin: parent.origin, scale: parent.scale, angles: parent.angles },
1371
+ { origin: l.localOrigin, scale: l.localScale, angles: l.localAngles },
1372
+ propagateScale
1373
+ );
1374
+ }
1375
+ const d = l.attachBindDelta;
1376
+ if (d) {
1377
+ w.origin[0] += d[0];
1378
+ w.origin[1] += d[1];
1379
+ }
1380
+ l.origin[0] = w.origin[0];
1381
+ l.origin[1] = w.origin[1];
1382
+ l.origin[2] = w.origin[2];
1383
+ l.scale[0] = w.scale[0];
1384
+ l.scale[1] = w.scale[1];
1385
+ l.scale[2] = w.scale[2];
1386
+ l.angles[0] = w.angles[0];
1387
+ l.angles[1] = w.angles[1];
1388
+ l.angles[2] = w.angles[2];
1389
+ if (l.attachBase) {
1390
+ l.attachBase[0] = w.origin[0];
1391
+ l.attachBase[1] = w.origin[1];
1392
+ }
1393
+ }
1394
+ }
1395
+ function collectTransformDirty(layers, extraSeeds) {
1396
+ const dirty = /* @__PURE__ */ new Set();
1397
+ if (!layers || layers.length === 0) return dirty;
1398
+ const childrenOf = /* @__PURE__ */ new Map();
1399
+ for (const l of layers) {
1400
+ if (!l || l.parentId === void 0 || l.parentId === null) continue;
1401
+ const list = childrenOf.get(l.parentId);
1402
+ if (list) list.push(l);
1403
+ else childrenOf.set(l.parentId, [l]);
1404
+ }
1405
+ const TRANSFORM_FIELDS = ["origin", "scale", "angles"];
1406
+ const seeds = [];
1407
+ for (const l of layers) {
1408
+ if (!l || l.id === void 0 || l.id === null) continue;
1409
+ const scripts = l.objectScripts || null;
1410
+ const anims = l.objectAnimations || null;
1411
+ const bound = TRANSFORM_FIELDS.some((f) => scripts && scripts[f] || anims && anims[f]);
1412
+ if (bound) seeds.push(l);
1413
+ }
1414
+ if (extraSeeds) {
1415
+ for (const l of extraSeeds) if (l && l.id !== void 0 && l.id !== null) seeds.push(l);
1416
+ }
1417
+ const stack = seeds.slice();
1418
+ for (let guard = 0; stack.length > 0 && guard < 1e5; guard++) {
1419
+ const l = stack.pop();
1420
+ if (!l || l.id === void 0 || l.id === null) continue;
1421
+ if (dirty.has(l.id)) continue;
1422
+ dirty.add(l.id);
1423
+ const kids = childrenOf.get(l.id);
1424
+ if (kids) for (const c of kids) stack.push(c);
1425
+ }
1426
+ return dirty;
1427
+ }
1242
1428
  const sceneMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1243
1429
  __proto__: null,
1244
1430
  applySolidFromModel,
1431
+ collectTransformDirty,
1432
+ composeChildTransform,
1433
+ isRenderInert,
1245
1434
  parseBool,
1246
1435
  parseColor,
1247
1436
  parseNum,
1248
1437
  parseScene,
1249
1438
  parseVec2,
1250
1439
  parseVec3,
1440
+ recomposeWorld,
1251
1441
  recomputeLayerVisibility,
1252
1442
  resolveMaterial
1253
1443
  }, Symbol.toStringTag, { value: "Module" }));
@@ -1651,12 +1841,36 @@
1651
1841
  const { defs, fns } = collectMacros(text);
1652
1842
  if (defs.size === 0 && fns.size === 0) break;
1653
1843
  const lines = text.split("\n");
1844
+ const defLine = /* @__PURE__ */ new Map();
1845
+ for (let i = 0; i < lines.length; i++) {
1846
+ const dm = /^[ \t]*#define[ \t]+([A-Za-z_][A-Za-z0-9_]*)/.exec(lines[i]);
1847
+ if (dm && !defLine.has(dm[1])) defLine.set(dm[1], i);
1848
+ }
1849
+ const declLine = /* @__PURE__ */ new Map();
1850
+ {
1851
+ const TYPES = "(?:float|int|bool|vec[234]|ivec[234]|bvec[234]|mat[234])";
1852
+ for (const name of defs.keys()) {
1853
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1854
+ const re = new RegExp(
1855
+ "^\\s*(?:const\\s+|uniform\\s+|varying\\s+|in\\s+|out\\s+|attribute\\s+)*" + TYPES + "\\s+" + esc + "\\s*[=;]"
1856
+ );
1857
+ for (let i = 0; i < lines.length; i++) {
1858
+ if (re.test(lines[i])) {
1859
+ declLine.set(name, i);
1860
+ break;
1861
+ }
1862
+ }
1863
+ }
1864
+ }
1654
1865
  let changed = false;
1655
1866
  for (let i = 0; i < lines.length; i++) {
1656
1867
  const line = lines[i];
1657
1868
  if (/^[ \t]*#/.test(line)) continue;
1658
1869
  let l = line;
1659
1870
  for (const [name, val] of defs) {
1871
+ const dl = defLine.get(name);
1872
+ if (dl !== void 0 && i < dl) continue;
1873
+ if (declLine.get(name) === i) continue;
1660
1874
  const re = new RegExp("\\b" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
1661
1875
  if (re.test(l)) {
1662
1876
  l = replaceWord(l, name, val);
@@ -1664,6 +1878,8 @@
1664
1878
  }
1665
1879
  }
1666
1880
  for (const [name, info] of fns) {
1881
+ const dl = defLine.get(name);
1882
+ if (dl !== void 0 && i < dl) continue;
1667
1883
  if (l.includes(name)) {
1668
1884
  l = expandFunctionMacro(l, name, info, depth);
1669
1885
  changed = true;
@@ -1938,6 +2154,13 @@
1938
2154
  const word = text.slice(p + 1, e);
1939
2155
  return GLSL_TYPES.has(word);
1940
2156
  }
2157
+ function collectIntNames(code) {
2158
+ const names = /* @__PURE__ */ new Set();
2159
+ let m;
2160
+ const declRe = /\b(?:const\s+)?int\s+([A-Za-z_]\w*)\s*[=;)\u0003]/g;
2161
+ while ((m = declRe.exec(code)) !== null) names.add(m[1]);
2162
+ return names;
2163
+ }
1941
2164
  function rewriteCall(text, callName, fn) {
1942
2165
  let out = "";
1943
2166
  let i = 0;
@@ -2022,23 +2245,73 @@
2022
2245
  const dim = sw.length;
2023
2246
  return fn + "(vec" + dim + "(" + num2 + "), " + expr + ")";
2024
2247
  });
2025
- code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
2026
- code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2027
- code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2028
- code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2029
- code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
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");
2248
+ code = code.replace(
2249
+ /(\.([xyzwrgba]{2,4})\s*=\s*)(max|min)\(\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*,\s*([^;]+?)\s*\)\s*;/g,
2250
+ (all, lead, sw, fn, scalar, vecExpr) => {
2251
+ if (!/\.[xyzwrgba]{2,4}\b|\bvec[234]\s*\(/.test(vecExpr)) return all;
2252
+ return `${lead}${fn}(vec${sw.length}(${scalar}), ${vecExpr});`;
2253
+ }
2254
+ );
2255
+ code = code.replace(
2256
+ /(^|[^\w)\]])([+-])([+-])(?=[\d.])/g,
2257
+ (all, pre, s1, s2) => pre + (s1 === s2 ? "+" : "-")
2258
+ );
2259
+ const sciHoles = [];
2260
+ code = code.replace(/\b\d+(?:\.\d+)?[eE][+-]?\d+\b/g, (m) => {
2261
+ sciHoles.push(m);
2262
+ return "" + "".repeat(sciHoles.length) + "";
2263
+ });
2264
+ const forHoles = [];
2265
+ code = code.replace(/\bfor\s*\(\s*int\s+([A-Za-z_]\w*)([^)]*)\)/g, (m, name, rest) => {
2266
+ forHoles.push(rest + ")");
2267
+ return `for (int ${name}${"".repeat(forHoles.length)}`;
2268
+ });
2269
+ for (let pass = 0; pass < 8; pass++) {
2270
+ const before = code;
2271
+ code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
2272
+ code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2273
+ code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2274
+ code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2275
+ code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2276
+ 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");
2277
+ code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2278
+ 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");
2279
+ code = code.replace(
2280
+ /(^|[^\w.])(\d+)\s*([*/+-])\s*(\()/g,
2281
+ (all, pre, num2, op, open, offset, whole) => {
2282
+ let depth = 0;
2283
+ let end = offset + all.length - 1;
2284
+ for (; end < whole.length; end++) {
2285
+ const ch = whole[end];
2286
+ if (ch === "(") depth++;
2287
+ else if (ch === ")") {
2288
+ depth--;
2289
+ if (depth === 0) {
2290
+ end++;
2291
+ break;
2292
+ }
2293
+ } else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
2294
+ }
2295
+ for (; end < whole.length; end++) {
2296
+ const ch = whole[end];
2297
+ if (ch === ";" || ch === "," || ch === "\n" || ch === ")") break;
2298
+ }
2299
+ const seg = whole.slice(offset, end);
2300
+ return /\d\.\d/.test(seg) ? `${pre}${num2}.0 ${op} ${open}` : all;
2301
+ }
2302
+ );
2303
+ {
2304
+ const floatNames = /* @__PURE__ */ new Set();
2305
+ 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;
2306
+ let dm;
2307
+ while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
2308
+ if (floatNames.size > 0) {
2309
+ const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
2310
+ code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
2311
+ code = code.replace(new RegExp("\\b(" + alt + ")\\s*([+-])\\s*(\\d+)(?![\\d.])", "g"), "$1 $2 $3.0");
2312
+ }
2041
2313
  }
2314
+ if (code === before) break;
2042
2315
  }
2043
2316
  {
2044
2317
  const FLOAT_BUILTINS = [
@@ -2174,6 +2447,67 @@
2174
2447
  return pre + lhs + " = " + rhs + "." + SW[lw] + ";";
2175
2448
  });
2176
2449
  }
2450
+ {
2451
+ const floatDecl = /* @__PURE__ */ new Set();
2452
+ {
2453
+ const fdre = /\b(?:uniform|varying|attribute|in|out|const)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
2454
+ let fd;
2455
+ while ((fd = fdre.exec(code)) !== null) floatDecl.add(fd[1]);
2456
+ }
2457
+ const vecW = (expr) => {
2458
+ const e = expr.trim();
2459
+ {
2460
+ const c = /^vec([234])\s*\(/.exec(e);
2461
+ if (c) {
2462
+ let depth = 0;
2463
+ for (let i = e.indexOf("("); i < e.length; i++) {
2464
+ if (e[i] === "(") depth++;
2465
+ else if (e[i] === ")") {
2466
+ depth--;
2467
+ if (depth === 0) return i === e.length - 1 ? Number(c[1]) : 0;
2468
+ }
2469
+ }
2470
+ return 0;
2471
+ }
2472
+ }
2473
+ if (/^texture(?:Lod)?\s*\(/.test(e)) {
2474
+ let depth = 0;
2475
+ for (let i = e.indexOf("("); i < e.length; i++) {
2476
+ if (e[i] === "(") depth++;
2477
+ else if (e[i] === ")") {
2478
+ depth--;
2479
+ if (depth === 0) return i === e.length - 1 ? 4 : 0;
2480
+ }
2481
+ }
2482
+ return 0;
2483
+ }
2484
+ const m = /^([A-Za-z_]\w*)(?:\.([xyzwrgba]{2,4}))?\s*[*/]\s*([^*/]+)$/.exec(e);
2485
+ if (!m) return 0;
2486
+ const rhsPart = m[3];
2487
+ if (/\bvec[234]\s*\(|\.[xyzwrgba]{2,4}\b/.test(rhsPart)) return 0;
2488
+ if (m[2]) return m[2].length;
2489
+ if (floatDecl.has(m[1])) return 0;
2490
+ return width.get(m[1]) || 0;
2491
+ };
2492
+ code = code.replace(
2493
+ /(^|[;{}\n]\s*)float\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
2494
+ (all, pre, name, rhs) => {
2495
+ const w = vecW(rhs);
2496
+ if (w < 2) return all;
2497
+ return `${pre}float ${name} = (${rhs.trim()}).x;`;
2498
+ }
2499
+ );
2500
+ const SWN = { 2: "xy", 3: "xyz" };
2501
+ code = code.replace(
2502
+ /(^|[;{}\n]\s*)vec([23])\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
2503
+ (all, pre, dim, name, rhs) => {
2504
+ const lw = Number(dim);
2505
+ const rw = vecW(rhs);
2506
+ if (rw <= lw) return all;
2507
+ return `${pre}vec${dim} ${name} = (${rhs.trim()}).${SWN[lw]};`;
2508
+ }
2509
+ );
2510
+ }
2177
2511
  if (width.size > 0) {
2178
2512
  const floatNames = /* @__PURE__ */ new Set();
2179
2513
  const fre = /\b(?:uniform|varying|attribute|in|out)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
@@ -2215,6 +2549,7 @@
2215
2549
  code = code.replace(/(^|[;{}\n]\s*)([A-Za-z_]\w*)\s*=\s*([^;]+);/g, (all, pre, lhs, rhs) => {
2216
2550
  const lw = width.get(lhs);
2217
2551
  if (!lw) return all;
2552
+ if (floatNames.has(lhs)) return all;
2218
2553
  const r = rhs.trim();
2219
2554
  if (new RegExp("^vec" + lw + "\\s*\\(").test(r)) return all;
2220
2555
  if (width.has(r)) return all;
@@ -2258,6 +2593,52 @@
2258
2593
  new RegExp(`\\bint\\s+([A-Za-z_]\\w*)\\s*=\\s*((?:${FLOAT_FNS})\\s*\\()`, "g"),
2259
2594
  "float $1 = $2"
2260
2595
  );
2596
+ code = code.replace(
2597
+ /\bfloat\s+([A-Za-z_]\w*)\s*=\s*(int\s*\([^;]*\))\s*;/g,
2598
+ "float $1 = float($2);"
2599
+ );
2600
+ {
2601
+ const intNames = collectIntNames(code);
2602
+ if (intNames.size > 0) {
2603
+ code = code.replace(
2604
+ /\b(const\s+)?float\s+([A-Za-z_]\w*)\s*=\s*([^;{}]+);/g,
2605
+ (all, cst, name, rhs) => {
2606
+ const body = rhs.trim();
2607
+ if (/\./.test(body)) return all;
2608
+ if (/[A-Za-z_]\w*\s*\(/.test(body)) return all;
2609
+ const ids = body.match(/[A-Za-z_]\w*/g);
2610
+ if (!ids || !ids.length) return all;
2611
+ if (!ids.every((x) => intNames.has(x))) return all;
2612
+ return `${cst || ""}float ${name} = float(${body});`;
2613
+ }
2614
+ );
2615
+ }
2616
+ }
2617
+ {
2618
+ const intNames = collectIntNames(code);
2619
+ const floatNames = /* @__PURE__ */ new Set();
2620
+ let fm;
2621
+ const fDeclRe = /\b(?:const\s+|uniform\s+|varying\s+|in\s+|out\s+)*float\s+([A-Za-z_]\w*)/g;
2622
+ while ((fm = fDeclRe.exec(code)) !== null) floatNames.add(fm[1]);
2623
+ for (const n of [...intNames]) {
2624
+ if (floatNames.has(n)) {
2625
+ intNames.delete(n);
2626
+ floatNames.delete(n);
2627
+ }
2628
+ }
2629
+ if (intNames.size > 0 && floatNames.size > 0) {
2630
+ const iAlt = [...intNames].sort((a, b) => b.length - a.length).join("|");
2631
+ const fAlt = [...floatNames].sort((a, b) => b.length - a.length).join("|");
2632
+ code = code.replace(
2633
+ new RegExp(`\\b(${iAlt})\\s*([*/+-])\\s*(${fAlt})\\b`, "g"),
2634
+ (all, a, op, b) => `float(${a}) ${op} ${b}`
2635
+ );
2636
+ code = code.replace(
2637
+ new RegExp(`\\b(${fAlt})\\s*([*/+-])\\s*(${iAlt})\\b`, "g"),
2638
+ (all, a, op, b) => `${a} ${op} float(${b})`
2639
+ );
2640
+ }
2641
+ }
2261
2642
  {
2262
2643
  const boolNames = /* @__PURE__ */ new Set();
2263
2644
  const boolRe = /\bbool\s+([A-Za-z_]\w*)\s*=/g;
@@ -2271,6 +2652,17 @@
2271
2652
  );
2272
2653
  }
2273
2654
  }
2655
+ {
2656
+ const CMP = /\(\s*([^()&|]+?)\s*(<=|>=|<|>|==|!=)\s*([^()&|]+?)\s*\)/g;
2657
+ code = code.replace(
2658
+ new RegExp(CMP.source + "\\s*([*/])", "g"),
2659
+ (all, lhs, op, rhs, mulOp) => `float(${lhs.trim()} ${op} ${rhs.trim()}) ${mulOp}`
2660
+ );
2661
+ code = code.replace(
2662
+ new RegExp("([-+*/]=\\s*)" + CMP.source, "g"),
2663
+ (all, assign, lhs, op, rhs) => `${assign}float(${lhs.trim()} ${op} ${rhs.trim()})`
2664
+ );
2665
+ }
2274
2666
  {
2275
2667
  const names = /* @__PURE__ */ new Set();
2276
2668
  for (const fm of code.matchAll(/\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?float\s+([A-Za-z_]\w*)\s*\[/g)) {
@@ -2289,6 +2681,17 @@
2289
2681
  let p = close1 + 1;
2290
2682
  while (p < code.length && /[ \t]/.test(code[p])) p++;
2291
2683
  if (code[p] !== "[") {
2684
+ const e12 = code.slice(open1 + 1, close1);
2685
+ const t = e12.trim();
2686
+ const isFloatish = /\d\.\d/.test(t) || /^[A-Za-z_]\w*$/.test(t) && new RegExp("\\bfloat\\s+" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b").test(code);
2687
+ const alreadyInt = /^\s*int\s*\(/.test(t) || /^-?\d+$/.test(t);
2688
+ if (isFloatish && !alreadyInt) {
2689
+ out += code.slice(last, fm.index);
2690
+ out += fm[1] + "[int(" + t + ")]";
2691
+ last = close1 + 1;
2692
+ re.lastIndex = last;
2693
+ continue;
2694
+ }
2292
2695
  re.lastIndex = close1 + 1;
2293
2696
  continue;
2294
2697
  }
@@ -2429,6 +2832,20 @@
2429
2832
  }).join("\n");
2430
2833
  }
2431
2834
  }
2835
+ code = code.replace(
2836
+ /^(\s*in\s+(?:highp|mediump|lowp\s+)?)(vec[234]|float)(\s+)([A-Za-z_]\w*)(\s*;)/gm,
2837
+ (all, pre, ty, sp, name, tail) => {
2838
+ const vt = vertTypes.get(name);
2839
+ if (!vt || RANK[vt] >= RANK[ty]) return all;
2840
+ const CH = "xyzw";
2841
+ const RG = "rgba";
2842
+ const over = new RegExp(
2843
+ "\\b" + name + "\\s*\\.\\s*[" + CH + RG + "]*[" + CH.slice(RANK[vt]) + RG.slice(RANK[vt]) + "]"
2844
+ );
2845
+ if (over.test(code)) return all;
2846
+ return pre + vt + sp + name + tail;
2847
+ }
2848
+ );
2432
2849
  }
2433
2850
  {
2434
2851
  const inVecN = /* @__PURE__ */ new Map();
@@ -2488,18 +2905,27 @@
2488
2905
  let body = code.slice(braceIdx + 1);
2489
2906
  const decls = [];
2490
2907
  for (const name of written) {
2908
+ const shadowed = new RegExp(
2909
+ "(?:^|[;{}\\n])\\s*(?:highp|mediump|lowp\\s+)?(?:vec[234]|float|int|bool)\\s+" + name + "\\s*[=;]"
2910
+ ).test(body);
2911
+ if (shadowed) continue;
2491
2912
  const tm = new RegExp("^\\s*in\\s+(?:highp|mediump|lowp\\s+)?(vec[234]|float)\\s+" + name + "\\s*;", "m").exec(code);
2492
2913
  const ty = tm ? tm[1] : "vec4";
2493
2914
  decls.push(" " + ty + " " + name + "_rw = " + name + ";");
2494
2915
  body = replaceWord(body, name, name + "_rw");
2495
2916
  }
2496
- body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
2917
+ if (decls.length > 0) {
2918
+ body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
2919
+ }
2497
2920
  code = head + body;
2498
2921
  }
2499
2922
  }
2500
2923
  }
2501
2924
  code = code.replace(/\[(?:unroll|loop|branch|flatten)\]\s*/g, "");
2502
2925
  code = code.replace(/\bstatic\s+/g, "");
2926
+ if (forHoles.length > 0) {
2927
+ code = code.replace(/\u0003(\u0004+)\u0003/g, (m, marks) => forHoles[marks.length - 1]);
2928
+ }
2503
2929
  {
2504
2930
  const floatNames = /* @__PURE__ */ new Set();
2505
2931
  for (const m of code.matchAll(/\b(?:uniform[ \t]+)?(?:highp|mediump|lowp)?[ \t]*float[ \t]+([A-Za-z_]\w*)[ \t]*[;=]/g)) {
@@ -2529,6 +2955,9 @@
2529
2955
  return line;
2530
2956
  }).join("\n");
2531
2957
  }
2958
+ if (sciHoles.length > 0) {
2959
+ code = code.replace(/\u0001(\u0002+)\u0001/g, (m, marks) => sciHoles[marks.length - 1]);
2960
+ }
2532
2961
  let prologue = "#version 300 es\n";
2533
2962
  if (stage === "vert") {
2534
2963
  prologue += "precision highp float;\n";
@@ -3162,6 +3591,11 @@ void main() {
3162
3591
  const from = usesVarying ? /unprojectedUVs\s*\*=\s*v_PointerScale\s*\*/ : /unprojectedUVs\s*\*=\s*g_PointerScale\s*\*/;
3163
3592
  return out.replace(from, "unprojectedUVs *= " + XRAY_FRAG_UV_SCALE + " *");
3164
3593
  }
3594
+ const XRAY_SIZE_FALLBACK = 1;
3595
+ function constantFallback(uniformName, declaredDefault) {
3596
+ if (uniformName === "g_PointerScale") return XRAY_SIZE_FALLBACK;
3597
+ return declaredDefault;
3598
+ }
3165
3599
  function createRenderer(canvas, opts = {}) {
3166
3600
  const gl = canvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false, alpha: false, preserveDrawingBuffer: true });
3167
3601
  if (!gl) throw new Error("当前浏览器不支持 WebGL2");
@@ -3344,8 +3778,9 @@ void main() {
3344
3778
  uni.set(base, { loc: gl.getUniformLocation(prog, info.name), type: GL_TYPES[info.type] || "unknown", size: info.size });
3345
3779
  }
3346
3780
  const matMeta = { ...parseMaterialMeta(src.vert), ...parseMaterialMeta(src.frag) };
3781
+ 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
3782
  const samplerDefaults = new Map([...parseSamplerDefaults(src.vert), ...parseSamplerDefaults(src.frag)]);
3348
- const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl };
3783
+ const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl, ndcDirect };
3349
3784
  progCache.set(key, entry);
3350
3785
  return entry;
3351
3786
  }
@@ -3517,7 +3952,8 @@ void main() {
3517
3952
  }
3518
3953
  for (const [matKey, entry] of Object.entries(matMeta || {})) {
3519
3954
  if (!constants || !(matKey in constants)) {
3520
- if (entry.default !== void 0) setConstant(uni, entry.uniform, entry.default);
3955
+ const dflt = constantFallback(entry.uniform, entry.default);
3956
+ if (dflt !== void 0) setConstant(uni, entry.uniform, dflt);
3521
3957
  }
3522
3958
  }
3523
3959
  }
@@ -4097,7 +4533,13 @@ void main() {
4097
4533
  }
4098
4534
  const drawLayers = cam.perspective ? scene.layers.slice().sort((a, b) => Number(!!b.isSkybox) - Number(!!a.isSkybox)) : scene.layers;
4099
4535
  for (const layer of drawLayers) {
4100
- if (!layer.visible || layer.destroyed) continue;
4536
+ if (layer.destroyed) continue;
4537
+ if (!layer.visible) {
4538
+ if (pendingEmptyCompose.has(layer.id)) {
4539
+ captureEmptyComposeAtZOrder(layer, cam, viewProj, width, height);
4540
+ }
4541
+ continue;
4542
+ }
4101
4543
  if (layer.isPostProcess && !(layer.effects || []).some((e) => e.visible)) continue;
4102
4544
  if (groupChildIds.has(layer.id)) continue;
4103
4545
  if (layer.isContainer) {
@@ -4556,7 +4998,13 @@ void main() {
4556
4998
  gl.bindFramebuffer(gl.FRAMEBUFFER, outFBO.fbo);
4557
4999
  gl.viewport(0, 0, outFBO.width, outFBO.height);
4558
5000
  gl.bindVertexArray(vao);
4559
- uploadQuad("pass", PASS_QUAD);
5001
+ const usePixelQuad = !progEntry.ndcDirect;
5002
+ if (usePixelQuad) {
5003
+ uploadQuad("passPx" + outFBO.width + "x" + outFBO.height, layerQuad(outFBO.width, outFBO.height));
5004
+ } else {
5005
+ uploadQuad("pass", PASS_QUAD);
5006
+ }
5007
+ const passMVP = usePixelQuad ? mat4Transpose(mat4Ortho(0, outFBO.width, 0, outFBO.height, -1e4, 1e4)) : IDENT_M4;
4560
5008
  const texNames = mp.textures || [];
4561
5009
  const maxTex = Math.max(texNames.length, 8);
4562
5010
  const resolutions = /* @__PURE__ */ new Map();
@@ -4585,7 +5033,7 @@ void main() {
4585
5033
  usedUnits.add(ti);
4586
5034
  resolutions.set(ti, [t.width, t.height, t.width, t.height]);
4587
5035
  }
4588
- bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, IDENT_M4, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
5036
+ bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, passMVP, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
4589
5037
  bindConstants(
4590
5038
  uni,
4591
5039
  animatedConstants(
@@ -4724,9 +5172,11 @@ void main() {
4724
5172
  __proto__: null,
4725
5173
  ALIGN,
4726
5174
  XRAY_IDLE_SCREEN_UV,
5175
+ XRAY_SIZE_FALLBACK,
4727
5176
  applyColorBlendCPU,
4728
5177
  collectGroupDescendantIds,
4729
5178
  colorBlendPlan,
5179
+ constantFallback,
4730
5180
  createRenderer,
4731
5181
  effectFboSize,
4732
5182
  layerWantsPreserveBackdrop,
@@ -5092,20 +5542,7 @@ void main(){
5092
5542
  this.model = model || {};
5093
5543
  this.override = override || {};
5094
5544
  this.layer = layer || null;
5095
- const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
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;
5545
+ this.syncLayerTransform();
5109
5546
  this.maxCount = Math.max(1, Math.min(2e4, num(this.model.maxcount, 100)));
5110
5547
  this.simTime = 0;
5111
5548
  this.paused = false;
@@ -5510,6 +5947,31 @@ void main(){
5510
5947
  setVisible(v) {
5511
5948
  this.visible = v;
5512
5949
  }
5950
+ /**
5951
+ * [we-scene patch] 从图层重新读取变换(构造时也走这里)。
5952
+ *
5953
+ * 发射器变换原先只在构造时缓存一次、之后**从不刷新**。父组一旦带脚本/动画
5954
+ * 变换(全库 17 个粒子层有脚本化祖先),图层被 recomposeWorld 挪走了,
5955
+ * 粒子却仍从旧位置喷出来 —— 画面上是「人物滑走了、他的火焰留在原地」。
5956
+ * 宿主在 recompose 之后对脏子树里的粒子层调用本方法。
5957
+ */
5958
+ syncLayerTransform() {
5959
+ const layer = this.layer;
5960
+ const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
5961
+ const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
5962
+ const la = layer && layer.angles ? layer.angles : [0, 0, 0];
5963
+ this.originX = lo[0] || 0;
5964
+ this.originY = lo[1] || 0;
5965
+ this.originZ = lo[2] || 0;
5966
+ this.scaleX = ls[0] === 0 ? 1 : ls[0];
5967
+ this.scaleY = ls[1] === 0 ? 1 : ls[1];
5968
+ this.angleZ = (la[2] || 0) * Math.PI / 180;
5969
+ const asx = Math.abs(this.scaleX);
5970
+ const asy = Math.abs(this.scaleY);
5971
+ this.sysScale = Math.min(asx, asy) || 1;
5972
+ this.spriteStretchX = asx / this.sysScale;
5973
+ this.spriteStretchY = asy / this.sysScale;
5974
+ }
5513
5975
  // 宿主每帧提供鼠标位置(世界像素);转到局部空间供控制点使用
5514
5976
  setPointer(worldX, worldY) {
5515
5977
  const dx = worldX - this.originX;
@@ -7838,6 +8300,7 @@ void main(){
7838
8300
  for (const c of desc) {
7839
8301
  c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
7840
8302
  }
8303
+ layer.attachBindDelta = [d[0], d[1]];
7841
8304
  follows.push({
7842
8305
  layer,
7843
8306
  parent,
@@ -7852,6 +8315,9 @@ void main(){
7852
8315
  f.baseY = f.layer.origin[1];
7853
8316
  f.subtree = [{ layer: f.layer, x: f.layer.origin[0], y: f.layer.origin[1] }];
7854
8317
  for (const c of f.desc) f.subtree.push({ layer: c, x: c.origin[0], y: c.origin[1] });
8318
+ for (const s of f.subtree) {
8319
+ if (!s.layer.attachBase) s.layer.attachBase = [s.x, s.y];
8320
+ }
7855
8321
  }
7856
8322
  return follows;
7857
8323
  }
@@ -7877,14 +8343,20 @@ void main(){
7877
8343
  }
7878
8344
  deltas.push(parentMeshToWorldDelta(f.parent, cur[12] - f.bindX, cur[13] - f.bindY));
7879
8345
  }
8346
+ const baseOf = (s) => {
8347
+ const ab = s.layer.attachBase;
8348
+ if (ab) return ab;
8349
+ return [s.x, s.y];
8350
+ };
7880
8351
  const seen = /* @__PURE__ */ new Set();
7881
8352
  for (const f of follows) {
7882
8353
  const tree = f.subtree || [{ layer: f.layer, x: f.baseX, y: f.baseY }];
7883
8354
  for (const s of tree) {
7884
8355
  if (seen.has(s.layer)) continue;
7885
8356
  seen.add(s.layer);
7886
- s.layer.origin[0] = s.x;
7887
- s.layer.origin[1] = s.y;
8357
+ const b = baseOf(s);
8358
+ s.layer.origin[0] = b[0];
8359
+ s.layer.origin[1] = b[1];
7888
8360
  }
7889
8361
  }
7890
8362
  for (let i = 0; i < follows.length; i++) {
@@ -8771,6 +9243,19 @@ void main() {
8771
9243
  if (opts.onError) opts.onError(e, "applyUserProperties");
8772
9244
  }
8773
9245
  },
9246
+ /** 直调 update 不做文本加工:层可见性脚本(visible.script)要拿原始返回值
9247
+ * ——布尔控可见,callUpdate 会把它吞成 null(防画到画面上的文字版语义)。 */
9248
+ callUpdateRaw(value) {
9249
+ if (!fns.update || sandbox.disabled) return void 0;
9250
+ try {
9251
+ return fns.update(value);
9252
+ } catch (e) {
9253
+ sandbox.errCount++;
9254
+ if (opts.onError) opts.onError(e, "update");
9255
+ if (sandbox.errCount >= 3) sandbox.disabled = true;
9256
+ return void 0;
9257
+ }
9258
+ },
8774
9259
  /** 求值当前文本:返回新文本;undefined/null 保留原值;连续出错 3 次熔断回退静态文本 */
8775
9260
  callUpdate(value) {
8776
9261
  if (!fns.update || sandbox.disabled) return null;
@@ -9462,13 +9947,8 @@ void main() {
9462
9947
  Object.defineProperty(proxy, key, {
9463
9948
  enumerable: true,
9464
9949
  get() {
9465
- if (layer && Array.isArray(layer[key])) {
9466
- const a = layer[key];
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];
9950
+ const a = layer && Array.isArray(layer[key]) ? layer[key] : null;
9951
+ return makeVec3(a || [0, 0, 0]);
9472
9952
  },
9473
9953
  set(v) {
9474
9954
  const a = normVec(v);
@@ -9664,7 +10144,8 @@ void main() {
9664
10144
  const hasCursorHook = !!(fns && (fns.cursorClick || fns.cursorEnter || fns.cursorLeave || fns.cursorDown || fns.cursorUp || fns.cursorMove));
9665
10145
  const hasMediaHook = !!(fns && MEDIA_CALLBACKS.some((n) => fns[n]));
9666
10146
  const hasApplyHook = !!(fns && typeof fns.applyUserProperties === "function");
9667
- if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook) return null;
10147
+ const usesEngineClock = /\bengine\s*\.\s*(runtime|frametime)\b/.test(body);
10148
+ if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook && !usesEngineClock) return null;
9668
10149
  const sandbox = {
9669
10150
  engine,
9670
10151
  scriptProperties: spValues,
@@ -9965,7 +10446,7 @@ void main() {
9965
10446
  cancel.handle = state.handle;
9966
10447
  return cancel;
9967
10448
  }
9968
- function clearTimeout(h) {
10449
+ function clearTimeout2(h) {
9969
10450
  if (typeof h === "function") {
9970
10451
  h();
9971
10452
  return;
@@ -9986,7 +10467,7 @@ void main() {
9986
10467
  return {
9987
10468
  setTimeout,
9988
10469
  setInterval: setInterval2,
9989
- clearTimeout,
10470
+ clearTimeout: clearTimeout2,
9990
10471
  clearInterval: clearInterval2,
9991
10472
  dispose,
9992
10473
  /** 测试与诊断用:尚未触发且未取消的定时器数 */
@@ -10040,6 +10521,8 @@ void main() {
10040
10521
  const BANDS2 = 64;
10041
10522
  const rawL = new Float32Array(BANDS2);
10042
10523
  const rawR = new Float32Array(BANDS2);
10524
+ const preL64 = new Float32Array(BANDS2);
10525
+ const preR64 = new Float32Array(BANDS2);
10043
10526
  const left64 = new Float32Array(BANDS2);
10044
10527
  const right64 = new Float32Array(BANDS2);
10045
10528
  const left32 = new Float32Array(32);
@@ -10053,6 +10536,15 @@ void main() {
10053
10536
  right32,
10054
10537
  left16,
10055
10538
  right16,
10539
+ /**
10540
+ * 未钳位(pre-GAIN、pre-clamp)的 64 band,含左右声道 pan。
10541
+ * left64/right64 是 `min(1, v*GAIN)` 之后的值:底鼓段基底就已到 ~0.6、峰值贴 1,
10542
+ * 波峰因数被压平——网页作者按「峰值过阈值」判定敲击时(1520828134 猫爪
10543
+ * `audioArray[i] > 0.5`),事后再乘任何标量都无法把基底与峰值分开。
10544
+ * 网页驱动改对本数组做 gamma 对比扩展,音条墙仍走已标定的 left64/right64。
10545
+ */
10546
+ preL64,
10547
+ preR64,
10056
10548
  /** vumeter:整体响度 0..1(粒子 audioprocessing / 文字脚本 average 用) */
10057
10549
  level: 0,
10058
10550
  /** 渲染器诊断:当前是否处于「静音段」 */
@@ -10098,6 +10590,7 @@ void main() {
10098
10590
  if (fq >= 0.45) v += hatV * 0.5 * ((fq - 0.45) / 0.55);
10099
10591
  v += riser * 0.5;
10100
10592
  v *= tilt;
10593
+ const vPre = v;
10101
10594
  v = Math.min(1, v * GAIN);
10102
10595
  const width = 0.06 + fq * 0.2;
10103
10596
  const pan = vnoise(beat * 0.13 + i * 0.35, 11) * width;
@@ -10106,6 +10599,8 @@ void main() {
10106
10599
  const floorV = silent ? 0.012 : 0;
10107
10600
  rawL[i] = Math.max(floorV, l);
10108
10601
  rawR[i] = Math.max(floorV, r);
10602
+ preL64[i] = Math.max(floorV, Math.max(0, vPre * (1 - pan)));
10603
+ preR64[i] = Math.max(floorV, Math.max(0, vPre * (1 + pan)));
10109
10604
  if (i < 48) levelSum += (rawL[i] + rawR[i]) * 0.5;
10110
10605
  }
10111
10606
  left64.set(rawL);
@@ -10603,15 +11098,7 @@ void main() {
10603
11098
  state.screenH = v.h || 1;
10604
11099
  }
10605
11100
  readViewport();
10606
- function onMove(ev) {
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
- }
11101
+ function applyMove(x, y) {
10615
11102
  const u = x / state.screenW;
10616
11103
  const v = y / state.screenH;
10617
11104
  if (!state.has) {
@@ -10628,18 +11115,35 @@ void main() {
10628
11115
  state.moveCount++;
10629
11116
  state.lastEventTime = Date.now();
10630
11117
  }
11118
+ function applyButtons(mask) {
11119
+ const left = (mask & 1) !== 0;
11120
+ if (left === state.leftDown) return;
11121
+ state.leftDown = left;
11122
+ if (left) state.downCount++;
11123
+ else state.upCount++;
11124
+ state.lastEventTime = Date.now();
11125
+ }
11126
+ function onMove(ev) {
11127
+ readViewport();
11128
+ let x = ev.clientX;
11129
+ let y = ev.clientY;
11130
+ if (target && typeof target.getBoundingClientRect === "function") {
11131
+ const r = target.getBoundingClientRect();
11132
+ x -= r.left;
11133
+ y -= r.top;
11134
+ }
11135
+ applyMove(x, y);
11136
+ }
10631
11137
  function onDown(ev) {
10632
11138
  if (ev.button !== void 0 && ev.button !== 0) return;
10633
- state.leftDown = true;
10634
- state.downCount++;
11139
+ applyButtons(1);
10635
11140
  }
10636
11141
  function onUp(ev) {
10637
11142
  if (ev.button !== void 0 && ev.button !== 0) return;
10638
- state.leftDown = false;
10639
- state.upCount++;
11143
+ applyButtons(0);
10640
11144
  }
10641
11145
  function onLeaveWindow() {
10642
- state.leftDown = false;
11146
+ applyButtons(0);
10643
11147
  }
10644
11148
  let attached = false;
10645
11149
  if (target && target.addEventListener) {
@@ -10654,6 +11158,42 @@ void main() {
10654
11158
  }
10655
11159
  return {
10656
11160
  state,
11161
+ /**
11162
+ * 外部注入指针状态(宿主轮询系统鼠标后推入)。协议见 docs/INTEGRATION.md。
11163
+ *
11164
+ * 接**归一化**坐标而不是像素:宿主知道自己那块屏的 points 尺寸,除法在它那边
11165
+ * 做更准(混合 DPI 多显示器下无需任何 DPR 折算);这里再乘回 screenW/H 得到
11166
+ * input.cursorScreenPosition 要的像素。
11167
+ *
11168
+ * u/v 是 [0,1]、原点左上、**Y 朝下** —— 与 DOM 路径的 state.u/v 同一空间
11169
+ * (见文件头坐标约定)。宿主不要替 shader 翻 Y。
11170
+ *
11171
+ * 不在这里推进 last:外部推送频率(~90Hz)高于帧率,若在推送里推进 last,
11172
+ * `length(g_PointerPosition - g_PointerPositionLast)` 会恒接近 0,
11173
+ * cursorripple 完全不起波且无报错(与 DOM 路径同一个坑,见文件头)。
11174
+ *
11175
+ * @param {{u:number, v:number, buttons?:number}} p 归一化位置 + 按键位掩码(bit0 左)
11176
+ */
11177
+ pushExternal(p) {
11178
+ if (!p) return;
11179
+ readViewport();
11180
+ const u = Number(p.u);
11181
+ const v = Number(p.v);
11182
+ if (Number.isFinite(u) && Number.isFinite(v)) {
11183
+ applyMove(u * state.screenW, v * state.screenH);
11184
+ }
11185
+ applyButtons(Number(p.buttons) || 0);
11186
+ },
11187
+ /**
11188
+ * 外部指针离开本窗口(鼠标移到了别的显示器)。
11189
+ *
11190
+ * **只清按键,保留位置与 has** —— 清 has 会让 xray 开窗突然跳到相机外
11191
+ * (renderer.js 的 XRAY_IDLE_SCREEN_UV)、视差弹回中心,画面会明显抽一下。
11192
+ * 语义与 DOM 的 onLeaveWindow 一致:位置停在最后已知点,只是不再按着键。
11193
+ */
11194
+ pushExternalLeave() {
11195
+ applyButtons(0);
11196
+ },
10657
11197
  /**
10658
11198
  * 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current。
10659
11199
  * 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
@@ -10860,19 +11400,84 @@ void main() {
10860
11400
  dec.close?.();
10861
11401
  return { width, height, frames };
10862
11402
  }
11403
+ function attachVideoSpectrum(rt, cfg, v) {
11404
+ if (rt.audioBridge) return;
11405
+ const AC = window.AudioContext || window.webkitAudioContext;
11406
+ if (!AC) return;
11407
+ let ctx;
11408
+ let analyser;
11409
+ try {
11410
+ ctx = new AC();
11411
+ const srcNode = ctx.createMediaElementSource(v);
11412
+ analyser = ctx.createAnalyser();
11413
+ analyser.fftSize = 256;
11414
+ analyser.smoothingTimeConstant = 0.75;
11415
+ srcNode.connect(analyser);
11416
+ analyser.connect(ctx.destination);
11417
+ } catch (e) {
11418
+ reportDiag(rt, cfg, `media 频谱接管跳过: ${e?.message ?? e}`);
11419
+ return;
11420
+ }
11421
+ const bins = new Uint8Array(analyser.frequencyBinCount);
11422
+ const left = new Float32Array(64);
11423
+ const right = new Float32Array(64);
11424
+ const bridge = () => {
11425
+ if (ctx.state !== "running") return null;
11426
+ analyser.getByteFrequencyData(bins);
11427
+ const n = Math.min(64, bins.length);
11428
+ for (let i = 0; i < n; i++) {
11429
+ const x = bins[i] / 255;
11430
+ left[i] = x;
11431
+ right[i] = x;
11432
+ }
11433
+ for (let i = n; i < 64; i++) {
11434
+ left[i] = 0;
11435
+ right[i] = 0;
11436
+ }
11437
+ return { left, right };
11438
+ };
11439
+ rt.audioBridge = bridge;
11440
+ if (ctx.state === "suspended") {
11441
+ const kick = () => {
11442
+ void ctx.resume().catch(() => {
11443
+ });
11444
+ window.removeEventListener("pointerdown", kick);
11445
+ window.removeEventListener("keydown", kick);
11446
+ };
11447
+ window.addEventListener("pointerdown", kick, { once: true });
11448
+ window.addEventListener("keydown", kick, { once: true });
11449
+ (rt.wallpaperDisposers ??= []).push(() => {
11450
+ window.removeEventListener("pointerdown", kick);
11451
+ window.removeEventListener("keydown", kick);
11452
+ });
11453
+ }
11454
+ (rt.wallpaperDisposers ??= []).push(() => {
11455
+ if (rt.audioBridge === bridge) rt.audioBridge = null;
11456
+ void ctx.close().catch(() => {
11457
+ });
11458
+ });
11459
+ }
10863
11460
  function mountMedia(rt, cfg) {
10864
11461
  clear(rt);
10865
- if (!cfg.src) {
11462
+ const failHard = (why) => {
11463
+ reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
11464
+ rt.onError?.(new Error(`媒体壁纸(${cfg.type})${why}`));
10866
11465
  rt.fallbackPage?.();
11466
+ };
11467
+ if (!cfg.src) {
11468
+ failHard("缺少资源 URL(cfg.src 为空)");
10867
11469
  return;
10868
11470
  }
10869
11471
  const isVideo = cfg.type === "video";
10870
11472
  const isGif = cfg.type === "gif";
10871
- const c = document.createElement("canvas");
11473
+ const embedded = cfg.canvas instanceof HTMLCanvasElement;
11474
+ const c = embedded ? cfg.canvas : document.createElement("canvas");
10872
11475
  const dpr = effectiveDpr(rt, cfg);
10873
- c.width = Math.max(1, Math.round(innerWidth * dpr));
10874
- c.height = Math.max(1, Math.round(innerHeight * dpr));
10875
- c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11476
+ const vw = c.clientWidth || window.innerWidth || 1;
11477
+ const vh = c.clientHeight || window.innerHeight || 1;
11478
+ c.width = Math.max(1, Math.round(vw * dpr));
11479
+ c.height = Math.max(1, Math.round(vh * dpr));
11480
+ if (!embedded) c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
10876
11481
  const gl2 = c.getContext("webgl2", {
10877
11482
  premultipliedAlpha: false,
10878
11483
  antialias: false,
@@ -10880,13 +11485,32 @@ void main() {
10880
11485
  preserveDrawingBuffer: true
10881
11486
  });
10882
11487
  if (!gl2) {
10883
- reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE,回退 DOM 渲染`);
11488
+ reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE`);
11489
+ if (embedded) {
11490
+ rt.onError?.(new Error("WEBGL2_UNAVAILABLE"));
11491
+ return;
11492
+ }
10884
11493
  if (isVideo) mountVideoDom(rt, cfg);
10885
11494
  else mountGifDom(rt, cfg);
10886
11495
  return;
10887
11496
  }
10888
- rt.wrap?.appendChild(c);
11497
+ if (!embedded) rt.wrap?.appendChild(c);
10889
11498
  rt.canvas = c;
11499
+ if (rt.onSceneInfo) {
11500
+ const hook = rt.onSceneInfo;
11501
+ rt.onSceneInfo = void 0;
11502
+ try {
11503
+ hook({
11504
+ width: vw,
11505
+ height: vh,
11506
+ layerCount: 0,
11507
+ hasModels: false,
11508
+ hasParticles: false,
11509
+ hasText: false
11510
+ });
11511
+ } catch {
11512
+ }
11513
+ }
10890
11514
  let disposed = false;
10891
11515
  rt.sceneCleanup = () => {
10892
11516
  disposed = true;
@@ -10898,6 +11522,21 @@ void main() {
10898
11522
  let pauseImpl;
10899
11523
  let resumeImpl;
10900
11524
  let videoWasPlaying = false;
11525
+ rt.sceneAudio = {
11526
+ setVolume(v) {
11527
+ const vid = rt.video;
11528
+ if (!vid) return;
11529
+ const vol = Math.max(0, Math.min(1, Number(v) || 0));
11530
+ vid.volume = vol;
11531
+ vid.muted = vol <= 0;
11532
+ if (vol > 0 && vid.paused && !rt.paused) {
11533
+ void vid.play().catch((e) => {
11534
+ reportDiag(rt, cfg, `media 取消静音后自动播放被拒绝: ${e?.message ?? e}`);
11535
+ });
11536
+ }
11537
+ },
11538
+ audios: []
11539
+ };
10901
11540
  rt.sceneCtl = {
10902
11541
  pause() {
10903
11542
  pauseImpl?.();
@@ -10911,8 +11550,7 @@ void main() {
10911
11550
  const fail = (why) => {
10912
11551
  if (disposed) return;
10913
11552
  disposed = true;
10914
- reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
10915
- rt.fallbackPage?.();
11553
+ failHard(why);
10916
11554
  };
10917
11555
  void (async () => {
10918
11556
  try {
@@ -10960,6 +11598,7 @@ void main() {
10960
11598
  });
10961
11599
  if (!rt.paused) void v.play().catch(() => {
10962
11600
  });
11601
+ attachVideoSpectrum(rt, cfg, v);
10963
11602
  reportDiag(rt, cfg, `media video ${mediaW}x${mediaH} → scene 渲染`);
10964
11603
  } else {
10965
11604
  let decoded = false;
@@ -11059,6 +11698,14 @@ void main() {
11059
11698
  peek.x,
11060
11699
  peek.y
11061
11700
  ).then(() => {
11701
+ if (rt.onFirstFrame) {
11702
+ const first = rt.onFirstFrame;
11703
+ rt.onFirstFrame = void 0;
11704
+ try {
11705
+ first();
11706
+ } catch {
11707
+ }
11708
+ }
11062
11709
  if (disposed || rt.paused) return;
11063
11710
  rt.raf = requestAnimationFrame(renderLoop);
11064
11711
  }).catch((e) => fail(String(e.message || e).slice(0, 200)));
@@ -11141,6 +11788,48 @@ void main() {
11141
11788
  rt.img = img;
11142
11789
  }
11143
11790
  const PKG_PATHS = ["scene.pkg", "scenes/scene.pkg", "gifscene.pkg"];
11791
+ const EXT_TYPES = {
11792
+ mp4: "video",
11793
+ webm: "video",
11794
+ mov: "video",
11795
+ m4v: "video",
11796
+ ogv: "video",
11797
+ gif: "gif",
11798
+ png: "image",
11799
+ jpg: "image",
11800
+ jpeg: "image",
11801
+ webp: "image",
11802
+ avif: "image",
11803
+ bmp: "image"
11804
+ };
11805
+ function typeFromMime(mime) {
11806
+ const m = mime.toLowerCase().split(";")[0].trim();
11807
+ if (m === "image/gif") return "gif";
11808
+ if (m.startsWith("video/")) return "video";
11809
+ if (m.startsWith("image/")) return "image";
11810
+ return null;
11811
+ }
11812
+ function sniffMediaType(url) {
11813
+ if (typeof url !== "string" || !url) return null;
11814
+ let path = url;
11815
+ const hash = path.indexOf("#");
11816
+ if (hash >= 0) path = path.slice(0, hash);
11817
+ const q = path.indexOf("?");
11818
+ if (q >= 0) path = path.slice(0, q);
11819
+ const seg = path.split("/").pop() || "";
11820
+ const dot = seg.lastIndexOf(".");
11821
+ if (dot < 0) return null;
11822
+ return EXT_TYPES[seg.slice(dot + 1).toLowerCase()] ?? null;
11823
+ }
11824
+ async function sniffMediaTypeByHead(url, init, signal) {
11825
+ try {
11826
+ const r = await fetch(url, { ...init, method: "HEAD", signal });
11827
+ if (!r.ok) return null;
11828
+ return typeFromMime(r.headers.get("content-type") || "");
11829
+ } catch {
11830
+ return null;
11831
+ }
11832
+ }
11144
11833
  function httpSource(baseUrl, init) {
11145
11834
  const base = baseUrl.replace(/\/+$/, "");
11146
11835
  return {
@@ -11180,6 +11869,44 @@ void main() {
11180
11869
  } catch {
11181
11870
  return null;
11182
11871
  }
11872
+ },
11873
+ async webEntry(signal) {
11874
+ let file = "index.html";
11875
+ try {
11876
+ const r = await fetch(`${base}/project.json`, { ...init, signal });
11877
+ if (r.ok) {
11878
+ const project = await r.json();
11879
+ if (project && typeof project.file === "string" && project.file.trim()) {
11880
+ file = project.file.trim().replace(/^\/+/, "");
11881
+ }
11882
+ }
11883
+ } catch {
11884
+ if (signal?.aborted) throw new Error("aborted");
11885
+ }
11886
+ return { url: `${base}/${file}` };
11887
+ },
11888
+ /**
11889
+ * 媒体壁纸(video/gif/image)的资源地址:`{base}/{project.file}`。
11890
+ *
11891
+ * 与 webEntry 的区别是**没有默认文件名可兜底**:网页壁纸缺 file 时
11892
+ * index.html 是行业惯例,媒体壁纸的文件名(scene.mp4 / xxx.gif)完全由作者定,
11893
+ * 猜一个只会 404。拿不到 file 就返回 null,让 mount 报「无法解析媒体 URL」,
11894
+ * 而不是发一个必然失败的请求、再把那个 404 当成根因写进错误里。
11895
+ */
11896
+ async mediaEntry(signal) {
11897
+ let file = "";
11898
+ try {
11899
+ const r = await fetch(`${base}/project.json`, { ...init, signal });
11900
+ if (r.ok) {
11901
+ const project = await r.json();
11902
+ if (project && typeof project.file === "string" && project.file.trim()) {
11903
+ file = project.file.trim().replace(/^\/+/, "");
11904
+ }
11905
+ }
11906
+ } catch {
11907
+ if (signal?.aborted) throw new Error("aborted");
11908
+ }
11909
+ return file ? { url: `${base}/${file}` } : null;
11183
11910
  }
11184
11911
  };
11185
11912
  }
@@ -11199,6 +11926,43 @@ void main() {
11199
11926
  project: async () => project ?? null
11200
11927
  };
11201
11928
  }
11929
+ function mediaSource(urlOrFile, options) {
11930
+ const isBlob = typeof urlOrFile !== "string";
11931
+ const named = urlOrFile;
11932
+ let type = options?.type ?? (isBlob ? typeFromMime(urlOrFile.type || "") : null) ?? sniffMediaType(isBlob ? String(named.name ?? "") : urlOrFile);
11933
+ let headTried = false;
11934
+ let objectUrl = null;
11935
+ const url = () => {
11936
+ if (!isBlob) return urlOrFile;
11937
+ if (!objectUrl) objectUrl = URL.createObjectURL(urlOrFile);
11938
+ return objectUrl;
11939
+ };
11940
+ const key = options?.key ?? (isBlob ? typeof named.name === "string" ? `media:${named.name}:${named.size}:${named.lastModified ?? 0}` : void 0 : `media:${urlOrFile}`);
11941
+ return {
11942
+ key,
11943
+ async scenePkg() {
11944
+ throw new Error("mediaSource 是纯媒体来源,没有 scene.pkg(请改用 httpSource/fileSource/bytesSource)");
11945
+ },
11946
+ // type 为 null 时也如实返回:resolveMountConfig 会再按 mediaEntry 的 URL
11947
+ // 嗅探一次(含 HEAD 兜底),仍认不出才落回 scene 并报错
11948
+ async project() {
11949
+ return type ? { type } : null;
11950
+ },
11951
+ async mediaEntry(signal) {
11952
+ if (!type && !isBlob && !headTried) {
11953
+ headTried = true;
11954
+ type = await sniffMediaTypeByHead(urlOrFile, void 0, signal);
11955
+ }
11956
+ return { url: url(), type: type ?? void 0 };
11957
+ },
11958
+ dispose() {
11959
+ if (objectUrl) {
11960
+ URL.revokeObjectURL(objectUrl);
11961
+ objectUrl = null;
11962
+ }
11963
+ }
11964
+ };
11965
+ }
11202
11966
  const LOOP_PREROLL_SEC = 0.5;
11203
11967
  const LOOP_HOLD_SEC = 0.04;
11204
11968
  const LOOP_SWAP_EPS = 0.08;
@@ -12236,7 +13000,48 @@ vec3 DecompressNormal(vec4 tex) {
12236
13000
  systemfont_simhei: "SimHei, 'Heiti SC', sans-serif"
12237
13001
  };
12238
13002
  const fontFaceCache = /* @__PURE__ */ new Map();
13003
+ function fontKeyHash(key) {
13004
+ let h = 5381;
13005
+ for (let i = 0; i < key.length; i++) h = (h << 5) + h + key.charCodeAt(i) | 0;
13006
+ return (h >>> 0).toString(36);
13007
+ }
13008
+ function releaseFontFaces(keys) {
13009
+ for (const key of keys) {
13010
+ const entry = fontFaceCache.get(key);
13011
+ if (!entry) continue;
13012
+ entry.refs--;
13013
+ if (entry.refs > 0) continue;
13014
+ fontFaceCache.delete(key);
13015
+ try {
13016
+ const dead = [];
13017
+ document.fonts.forEach((f) => {
13018
+ if (f.family === entry.family) dead.push(f);
13019
+ });
13020
+ for (const f of dead) document.fonts.delete(f);
13021
+ } catch {
13022
+ }
13023
+ }
13024
+ }
12239
13025
  const pkgCache = /* @__PURE__ */ new Map();
13026
+ const PKG_CACHE_MAX_BYTES = 512 * 1024 * 1024;
13027
+ let pkgCacheBytes = 0;
13028
+ function pkgCacheEvict(currentKey) {
13029
+ while (pkgCache.size > 0 && (pkgCache.size > 2 || pkgCacheBytes > PKG_CACHE_MAX_BYTES)) {
13030
+ let oldestKey = null;
13031
+ let oldestAt = Infinity;
13032
+ for (const [k, v] of pkgCache) {
13033
+ if (k === currentKey) continue;
13034
+ if (v.at < oldestAt) {
13035
+ oldestAt = v.at;
13036
+ oldestKey = k;
13037
+ }
13038
+ }
13039
+ if (!oldestKey) break;
13040
+ const victim = pkgCache.get(oldestKey);
13041
+ pkgCacheBytes -= victim.parsed.fileSize || 0;
13042
+ pkgCache.delete(oldestKey);
13043
+ }
13044
+ }
12240
13045
  async function loadParsedPkg(rt, cfg, source, signal) {
12241
13046
  const cacheKey = source.key;
12242
13047
  if (cacheKey) {
@@ -12259,38 +13064,40 @@ vec3 DecompressNormal(vec4 tex) {
12259
13064
  const parsed = pkg.parsePkg(bytes);
12260
13065
  if (!cacheKey) return parsed;
12261
13066
  pkgCache.set(cacheKey, { parsed, at: Date.now() });
12262
- if (pkgCache.size > 2) {
12263
- let oldestKey = null;
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
- }
13067
+ pkgCacheBytes += parsed.fileSize || 0;
13068
+ pkgCacheEvict(cacheKey);
12274
13069
  return parsed;
12275
13070
  }
12276
13071
  function mountScene(rt, cfg) {
12277
13072
  clear(rt);
12278
- const c = cfg.canvas ?? document.createElement("canvas");
13073
+ const c = (cfg.canvas instanceof HTMLCanvasElement ? cfg.canvas : null) ?? document.createElement("canvas");
12279
13074
  const dpr = effectiveDpr(rt, cfg);
12280
13075
  const vw = c.clientWidth || window.innerWidth || 1;
12281
13076
  const vh = c.clientHeight || window.innerHeight || 1;
12282
13077
  c.width = Math.max(1, Math.round(vw * dpr));
12283
13078
  c.height = Math.max(1, Math.round(vh * dpr));
12284
- if (!cfg.canvas) {
13079
+ if (!(cfg.canvas instanceof HTMLCanvasElement)) {
12285
13080
  c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
12286
13081
  rt.wrap?.appendChild(c);
12287
13082
  }
12288
13083
  rt.canvas = c;
12289
13084
  let disposed = false;
13085
+ const origWarn = console.warn.bind(console);
13086
+ console.warn = (...args) => {
13087
+ const s = args.map((a) => typeof a === "string" ? a : String(a?.message ?? a)).join(" ");
13088
+ if (s.includes("[we-scene]")) {
13089
+ try {
13090
+ reportDiag(rt, cfg, s.slice(0, 300));
13091
+ } catch {
13092
+ }
13093
+ }
13094
+ origWarn(...args);
13095
+ };
12290
13096
  const pkgAbort = new AbortController();
12291
13097
  let particleCleanup;
12292
13098
  rt.sceneCleanup = () => {
12293
13099
  disposed = true;
13100
+ console.warn = origWarn;
12294
13101
  pkgAbort.abort();
12295
13102
  rt.sceneTextUpdate = void 0;
12296
13103
  if (particleCleanup) {
@@ -12347,6 +13154,11 @@ vec3 DecompressNormal(vec4 tex) {
12347
13154
  const sceneEntry = pkg.getEntry(parsedPkg, "scene.json");
12348
13155
  if (!sceneEntry) throw new Error("pkg 中没有 scene.json(不是场景壁纸?)");
12349
13156
  const scene = scn.parseScene(JSON.parse(readText(sceneEntry)), project);
13157
+ if (cfg.clearColor) {
13158
+ const g = scene.general ??= {};
13159
+ g.clearcolor = cfg.clearColor;
13160
+ g.clearenabled = true;
13161
+ }
12350
13162
  {
12351
13163
  const zRaw = scene.general?.zoom;
12352
13164
  const zVal = zRaw && typeof zRaw === "object" ? Number(zRaw.value) : Number(zRaw);
@@ -12431,9 +13243,10 @@ vec3 DecompressNormal(vec4 tex) {
12431
13243
  const audioDriverRef = {
12432
13244
  current: null
12433
13245
  };
12434
- let mediaDriver = simMedia;
13246
+ let liveMediaOverride = null;
13247
+ const currentMediaDriver = () => liveMediaOverride ?? rt.mediaSource ?? simMedia;
12435
13248
  let windowDriver = simWindow;
12436
- const audioSim = { enabled: supportsAudioProcessing };
13249
+ const audioSim = { enabled: supportsAudioProcessing && !rt.audioDisabled };
12437
13250
  const zero = (n) => new Float32Array(n);
12438
13251
  const SILENT_AUDIO = {
12439
13252
  left16: zero(16),
@@ -12445,9 +13258,72 @@ vec3 DecompressNormal(vec4 tex) {
12445
13258
  level: 0,
12446
13259
  silent: true
12447
13260
  };
13261
+ const hostAudio = (() => {
13262
+ const snapshot = {
13263
+ left64: zero(64),
13264
+ right64: zero(64),
13265
+ left32: zero(32),
13266
+ right32: zero(32),
13267
+ left16: zero(16),
13268
+ right16: zero(16),
13269
+ // 未钳位频谱:网页驱动会对它做 gamma 对比扩展。宿主给的已是 0..1
13270
+ // 归一化值,没有 pre-GAIN 概念,直接与 left64/right64 共用同一份数据
13271
+ preL64: zero(64),
13272
+ preR64: zero(64),
13273
+ level: 0,
13274
+ silent: true
13275
+ };
13276
+ const down = (dst, src) => {
13277
+ const g = src.length / dst.length;
13278
+ for (let i = 0; i < dst.length; i++) {
13279
+ let s = 0;
13280
+ const i0 = Math.floor(i * g);
13281
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
13282
+ for (let j = i0; j < i1; j++) s += src[j];
13283
+ dst[i] = s / (i1 - i0);
13284
+ }
13285
+ };
13286
+ return {
13287
+ active: false,
13288
+ snapshot,
13289
+ /** 每帧从宿主拉一次。宿主返回 null(未采集/无权限)时置 active=false 回落模拟源 */
13290
+ pump() {
13291
+ const src = rt.audioBridge?.();
13292
+ if (!src || !src.left || !src.right) {
13293
+ this.active = false;
13294
+ return;
13295
+ }
13296
+ const n = Math.min(64, src.left.length, src.right.length);
13297
+ let sum = 0;
13298
+ for (let i = 0; i < n; i++) {
13299
+ const l = src.left[i] || 0;
13300
+ const r = src.right[i] || 0;
13301
+ snapshot.left64[i] = l;
13302
+ snapshot.right64[i] = r;
13303
+ snapshot.preL64[i] = l;
13304
+ snapshot.preR64[i] = r;
13305
+ if (i < 48) sum += l;
13306
+ }
13307
+ for (let i = n; i < 64; i++) {
13308
+ snapshot.left64[i] = 0;
13309
+ snapshot.right64[i] = 0;
13310
+ snapshot.preL64[i] = 0;
13311
+ snapshot.preR64[i] = 0;
13312
+ }
13313
+ down(snapshot.left32, snapshot.left64);
13314
+ down(snapshot.right32, snapshot.right64);
13315
+ down(snapshot.left16, snapshot.left64);
13316
+ down(snapshot.right16, snapshot.right64);
13317
+ snapshot.level = Math.min(1, sum / 48);
13318
+ snapshot.silent = snapshot.level < 0.02;
13319
+ this.active = true;
13320
+ }
13321
+ };
13322
+ })();
13323
+ const activeAudioSnapshot = () => hostAudio.active ? hostAudio.snapshot : audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
12448
13324
  renderer.setAudioProvider(() => {
12449
13325
  if (!audioSim.enabled) return SILENT_AUDIO;
12450
- return audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
13326
+ return activeAudioSnapshot();
12451
13327
  });
12452
13328
  const audioViews = /* @__PURE__ */ new Map();
12453
13329
  window.__audioStats = () => ({
@@ -12469,7 +13345,7 @@ vec3 DecompressNormal(vec4 tex) {
12469
13345
  const shortcuts = system.createShortcutHandler((name) => {
12470
13346
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
12471
13347
  });
12472
- const mediaSim = { enabled: true, override: null };
13348
+ const mediaSim = { enabled: !rt.mediaDisabled, override: null };
12473
13349
  const mediaHooks = [];
12474
13350
  let lastMediaSnap = null;
12475
13351
  liveHold.lastSnap = {
@@ -12478,7 +13354,7 @@ vec3 DecompressNormal(vec4 tex) {
12478
13354
  if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
12479
13355
  }
12480
13356
  };
12481
- const mediaSnapshot = () => mediaDriver.snapshot;
13357
+ const mediaSnapshot = () => currentMediaDriver().snapshot;
12482
13358
  const registerMediaHook = (sb) => {
12483
13359
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
12484
13360
  mediaHooks.push(sb);
@@ -12523,36 +13399,30 @@ vec3 DecompressNormal(vec4 tex) {
12523
13399
  }
12524
13400
  lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
12525
13401
  };
13402
+ const callDriver = (name) => {
13403
+ const drv = currentMediaDriver();
13404
+ const fn = drv?.[name];
13405
+ if (typeof fn === "function") {
13406
+ try {
13407
+ fn.call(drv);
13408
+ } catch (e) {
13409
+ reportDiag(rt, cfg, `media ${name} 失败: ${e?.message}`);
13410
+ }
13411
+ }
13412
+ dispatchMediaNow();
13413
+ return mediaSnapshot();
13414
+ };
12526
13415
  const mediaControl = {
12527
13416
  get snapshot() {
12528
13417
  return mediaSnapshot();
12529
13418
  },
12530
- skipNext: () => {
12531
- mediaDriver.skipNext();
12532
- dispatchMediaNow();
12533
- return mediaSnapshot();
12534
- },
12535
- skipPrevious: () => {
12536
- mediaDriver.skipPrevious();
12537
- dispatchMediaNow();
12538
- return mediaSnapshot();
12539
- },
12540
- play: () => {
12541
- mediaDriver.play();
12542
- dispatchMediaNow();
12543
- return mediaSnapshot();
12544
- },
12545
- pause: () => {
12546
- mediaDriver.pause();
12547
- dispatchMediaNow();
12548
- return mediaSnapshot();
12549
- },
12550
- playPause: () => {
12551
- mediaDriver.playPause();
12552
- dispatchMediaNow();
12553
- return mediaSnapshot();
12554
- }
13419
+ skipNext: () => callDriver("skipNext"),
13420
+ skipPrevious: () => callDriver("skipPrevious"),
13421
+ play: () => callDriver("play"),
13422
+ pause: () => callDriver("pause"),
13423
+ playPause: () => callDriver("playPause")
12555
13424
  };
13425
+ rt.mediaCtl = mediaControl;
12556
13426
  window.__mediaControl = mediaControl;
12557
13427
  window.__system = {
12558
13428
  media: mediaControl,
@@ -12575,6 +13445,10 @@ vec3 DecompressNormal(vec4 tex) {
12575
13445
  } : {}
12576
13446
  );
12577
13447
  renderer.setPointerProvider(() => pointerSrc);
13448
+ rt.pointerCtl = {
13449
+ push: (p) => pointerSrc.pushExternal(p),
13450
+ leave: () => pointerSrc.pushExternalLeave()
13451
+ };
12578
13452
  {
12579
13453
  const prevCleanup = particleCleanup;
12580
13454
  particleCleanup = () => {
@@ -12669,6 +13543,18 @@ vec3 DecompressNormal(vec4 tex) {
12669
13543
  }
12670
13544
  }
12671
13545
  if (cfg.liveSystem) {
13546
+ const liveSlot = {
13547
+ handle: null,
13548
+ dead: false
13549
+ };
13550
+ (rt.wallpaperDisposers ??= []).push(() => {
13551
+ liveSlot.dead = true;
13552
+ try {
13553
+ liveSlot.handle?.dispose();
13554
+ } catch {
13555
+ }
13556
+ liveSlot.handle = null;
13557
+ });
12672
13558
  try {
12673
13559
  const uploadLiveArtwork = async (info) => {
12674
13560
  try {
@@ -12709,7 +13595,7 @@ vec3 DecompressNormal(vec4 tex) {
12709
13595
  generated: true
12710
13596
  });
12711
13597
  }
12712
- const snap = mediaDriver.snapshot;
13598
+ const snap = currentMediaDriver().snapshot;
12713
13599
  if (palette) {
12714
13600
  snap.primaryColor = media.mediaVec3(...palette.primary);
12715
13601
  snap.secondaryColor = media.mediaVec3(...palette.secondary);
@@ -12734,39 +13620,48 @@ vec3 DecompressNormal(vec4 tex) {
12734
13620
  void uploadLiveArtwork(info);
12735
13621
  }
12736
13622
  });
12737
- mediaDriver = live.media;
12738
- windowDriver = live.windowTitle;
12739
- liveHold.mediaDriver = live.media;
12740
- if (live.status().audio === "mic") audioDriverRef.current = live.audio;
12741
- if (mediaDriver.snapshot.hasMedia) {
12742
- for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
12743
- for (const sb of mediaHooks) {
12744
- try {
12745
- sb.callMedia(name, event);
12746
- } catch {
13623
+ if (liveSlot.dead) {
13624
+ try {
13625
+ live.dispose();
13626
+ } catch {
13627
+ }
13628
+ live = null;
13629
+ } else {
13630
+ liveSlot.handle = live;
13631
+ liveMediaOverride = live.media;
13632
+ windowDriver = live.windowTitle;
13633
+ liveHold.mediaDriver = live.media;
13634
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13635
+ if (currentMediaDriver().snapshot.hasMedia) {
13636
+ for (const { name, event } of media.diffMediaEvents(null, currentMediaDriver().snapshot)) {
13637
+ for (const sb of mediaHooks) {
13638
+ try {
13639
+ sb.callMedia(name, event);
13640
+ } catch {
13641
+ }
12747
13642
  }
12748
13643
  }
13644
+ lastMediaSnap = media.cloneMediaSnapshot(currentMediaDriver().snapshot);
12749
13645
  }
12750
- lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
13646
+ const st = live.status();
13647
+ reportDiag(
13648
+ rt,
13649
+ cfg,
13650
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13651
+ );
13652
+ reportDiag(
13653
+ rt,
13654
+ cfg,
13655
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13656
+ );
13657
+ window.__system = {
13658
+ media: mediaControl,
13659
+ windowTitle: windowDriver.snapshot,
13660
+ shortcuts: shortcuts.last,
13661
+ live: () => live.status()
13662
+ };
13663
+ window.__liveSystem = () => live.status();
12751
13664
  }
12752
- const st = live.status();
12753
- reportDiag(
12754
- rt,
12755
- cfg,
12756
- `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
12757
- );
12758
- reportDiag(
12759
- rt,
12760
- cfg,
12761
- `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
12762
- );
12763
- window.__system = {
12764
- media: mediaControl,
12765
- windowTitle: windowDriver.snapshot,
12766
- shortcuts: shortcuts.last,
12767
- live: () => live.status()
12768
- };
12769
- window.__liveSystem = () => live.status();
12770
13665
  } catch (e) {
12771
13666
  reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
12772
13667
  live = null;
@@ -13076,6 +13971,7 @@ vec3 DecompressNormal(vec4 tex) {
13076
13971
  if (usedVisible && !rt.paused) texEntry.videoCtl.play();
13077
13972
  }
13078
13973
  const particleSystems = [];
13974
+ const particleDirty = [];
13079
13975
  const particleSystemsByLayer = /* @__PURE__ */ new Map();
13080
13976
  let builtinTexCount = 0;
13081
13977
  const loadParticleTex = async (name) => {
@@ -13294,7 +14190,10 @@ vec3 DecompressNormal(vec4 tex) {
13294
14190
  const py = originY != null ? originY : wy;
13295
14191
  for (const ps of particleSystems) ps.setPointer(wx, py);
13296
14192
  }
13297
- for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? simAudio.snapshot : null);
14193
+ if (particleDirty.length) {
14194
+ for (const ps of particleDirty) ps.syncLayerTransform();
14195
+ }
14196
+ for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? activeAudioSnapshot() : null);
13298
14197
  if (particleDiagFrame < 2) {
13299
14198
  particleDiagFrame++;
13300
14199
  if (particleDiagFrame === 2) {
@@ -13479,6 +14378,17 @@ vec3 DecompressNormal(vec4 tex) {
13479
14378
  if (attachFollows.length) {
13480
14379
  reportDiag(rt, cfg, `attachments: ${attachFollows.length} hanging layers`);
13481
14380
  }
14381
+ const transformDirty = scn.collectTransformDirty(
14382
+ scene.layers,
14383
+ attachFollows.map((f) => f.layer)
14384
+ );
14385
+ if (transformDirty.size) {
14386
+ reportDiag(rt, cfg, `transform graph: ${transformDirty.size} live layers`);
14387
+ for (const [lid, list] of particleSystemsByLayer) {
14388
+ if (!transformDirty.has(lid)) continue;
14389
+ for (const ps of list) particleDirty.push(ps);
14390
+ }
14391
+ }
13482
14392
  const textWidgets = [];
13483
14393
  const textLayerText = /* @__PURE__ */ new Map();
13484
14394
  const textShared = {};
@@ -13492,15 +14402,19 @@ vec3 DecompressNormal(vec4 tex) {
13492
14402
  const win0 = fitWindow(normalizeFit(rt.cfg.fit), projW, projH, c.width, c.height);
13493
14403
  const quality = Math.min(3, Math.max(0.5, c.width / Math.max(1, win0.viewW)));
13494
14404
  const fontFamilies = /* @__PURE__ */ new Map();
14405
+ const usedFontKeys = [];
13495
14406
  const fontPaths = /* @__PURE__ */ new Set();
13496
14407
  for (const l of scene.layers) if (l.isText && l.textFont) fontPaths.add(l.textFont);
13497
14408
  for (const e of parsedPkg.entries || []) {
13498
14409
  if (typeof e.name === "string" && /^fonts\/.+\.(ttf|otf|woff2?)$/i.test(e.name)) fontPaths.add(e.name);
13499
14410
  }
13500
14411
  for (const fp of fontPaths) {
13501
- const cached = fontFaceCache.get(`${cfg.src}|${fp}`);
14412
+ const key = `${cfg.src}|${fp}`;
14413
+ const cached = fontFaceCache.get(key);
13502
14414
  if (cached) {
13503
- fontFamilies.set(fp, cached);
14415
+ cached.refs++;
14416
+ usedFontKeys.push(key);
14417
+ fontFamilies.set(fp, cached.family);
13504
14418
  continue;
13505
14419
  }
13506
14420
  const sys = SYSTEM_FONT_FAMILIES[fp.toLowerCase()];
@@ -13514,18 +14428,30 @@ vec3 DecompressNormal(vec4 tex) {
13514
14428
  const bytes = sanitizeFontForBrowser(
13515
14429
  fe instanceof Uint8Array ? fe : new Uint8Array(fe)
13516
14430
  );
13517
- const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
14431
+ const fam = "wefont_" + fontKeyHash(key) + "_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
13518
14432
  const url = URL.createObjectURL(new Blob([bytes]));
13519
14433
  const ff = new FontFace(fam, `url(${url})`);
13520
14434
  await ff.load();
14435
+ if (disposed) {
14436
+ URL.revokeObjectURL(url);
14437
+ break;
14438
+ }
13521
14439
  document.fonts.add(ff);
13522
14440
  (rt.objectUrls ??= []).push(url);
13523
14441
  fontFamilies.set(fp, fam);
13524
- fontFaceCache.set(`${cfg.src}|${fp}`, fam);
14442
+ fontFaceCache.set(key, { family: fam, refs: 1 });
14443
+ usedFontKeys.push(key);
13525
14444
  } catch (e) {
13526
14445
  console.warn(`字体加载失败 ${fp}: ${e.message}`);
13527
14446
  }
13528
14447
  }
14448
+ if (usedFontKeys.length) {
14449
+ const prevCleanup = rt.sceneCleanup;
14450
+ rt.sceneCleanup = () => {
14451
+ releaseFontFaces(usedFontKeys);
14452
+ prevCleanup?.();
14453
+ };
14454
+ }
13529
14455
  textCanvas = document.createElement("canvas");
13530
14456
  textCtx = textCanvas.getContext("2d");
13531
14457
  const MAX_TEX = 2048;
@@ -13578,10 +14504,18 @@ vec3 DecompressNormal(vec4 tex) {
13578
14504
  const hw = layer.size[0] * (layer.scale[0] || 1) / 2;
13579
14505
  const hh = layer.size[1] * (layer.scale[1] || 1) / 2;
13580
14506
  const a = layer.textAnchor;
13581
- if (a.includes("left")) layer.origin[0] += hw;
13582
- if (a.includes("right")) layer.origin[0] -= hw;
13583
- if (a.includes("top")) layer.origin[1] -= hh;
13584
- if (a.includes("bottom")) layer.origin[1] += hh;
14507
+ let adx = 0;
14508
+ let ady = 0;
14509
+ if (a.includes("left")) adx += hw;
14510
+ if (a.includes("right")) adx -= hw;
14511
+ if (a.includes("top")) ady -= hh;
14512
+ if (a.includes("bottom")) ady += hh;
14513
+ layer.origin[0] += adx;
14514
+ layer.origin[1] += ady;
14515
+ if (layer.localOrigin) {
14516
+ layer.localOrigin[0] += adx;
14517
+ layer.localOrigin[1] += ady;
14518
+ }
13585
14519
  }
13586
14520
  const em0 = TEXT_EM_SCALE * Math.max(1, layer.textPointsize);
13587
14521
  const marginCap = wtext.textLayerHasTintMask(layer) ? 8 : 256;
@@ -13824,6 +14758,15 @@ vec3 DecompressNormal(vec4 tex) {
13824
14758
  if (sb && sb.hasMediaHook) registerMediaHook(sb);
13825
14759
  }
13826
14760
  });
14761
+ const LOCAL_SLOT = {
14762
+ origin: "localOrigin",
14763
+ scale: "localScale",
14764
+ angles: "localAngles"
14765
+ };
14766
+ const fieldSlot = (layer, field) => {
14767
+ const slot = LOCAL_SLOT[field];
14768
+ return slot && layer && Array.isArray(layer[slot]) ? slot : field;
14769
+ };
13827
14770
  for (const layer of scene.layers) {
13828
14771
  const defs = layer.objectAnimations;
13829
14772
  if (!defs) continue;
@@ -13834,11 +14777,13 @@ vec3 DecompressNormal(vec4 tex) {
13834
14777
  const ctrl = anim.createAnimation(def.animation);
13835
14778
  ctrl.field = field;
13836
14779
  ctrl.baseValue = def.value;
13837
- const live2 = layer[field];
14780
+ const slot = fieldSlot(layer, field);
14781
+ const live2 = layer[slot];
13838
14782
  ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
14783
+ ctrl.slot = slot;
13839
14784
  layer.animationList.push(ctrl);
13840
14785
  if (ctrl.name) layer.animations[ctrl.name] = ctrl;
13841
- animRuns.push({ layer, field, ctrl });
14786
+ animRuns.push({ layer, field, slot, ctrl });
13842
14787
  } catch (e) {
13843
14788
  reportDiag(rt, cfg, `animation '${layer.name}.${field}' 建控制器失败: ${String(e.message).slice(0, 80)}`);
13844
14789
  }
@@ -13903,7 +14848,8 @@ vec3 DecompressNormal(vec4 tex) {
13903
14848
  });
13904
14849
  if (sandbox) {
13905
14850
  propSandboxes.push(sandbox);
13906
- const fieldVal = layer[field];
14851
+ const initSlot = fieldSlot(layer, field);
14852
+ const fieldVal = layer[initSlot];
13907
14853
  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
14854
  sandbox.init(initArg);
13909
14855
  sandbox.applyUserProperties(objUserProps);
@@ -13913,6 +14859,8 @@ vec3 DecompressNormal(vec4 tex) {
13913
14859
  objectScriptRuns.push({
13914
14860
  layer,
13915
14861
  field,
14862
+ // 变换字段逐帧也在 local 槽上收发(与 init 同一空间)。
14863
+ slot: initSlot,
13916
14864
  kind: field === "visible" ? "bool" : field === "alpha" || field === "brightness" ? "scalar" : "vec3",
13917
14865
  sandbox
13918
14866
  });
@@ -13976,6 +14924,7 @@ vec3 DecompressNormal(vec4 tex) {
13976
14924
  window.__objScripts = objectScriptRuns;
13977
14925
  }
13978
14926
  window.__mediaHooks = mediaHooks;
14927
+ window.__sceneLayers = scene.layers;
13979
14928
  window.__compositeStats = () => renderer.compositeStats?.() ?? null;
13980
14929
  window.__compositeEnable = (on) => renderer.setCompositeEnabled?.(on);
13981
14930
  window.__scene = scene;
@@ -14036,6 +14985,7 @@ vec3 DecompressNormal(vec4 tex) {
14036
14985
  const playingVideos = [];
14037
14986
  const playingAudios = [];
14038
14987
  let lastRender = -Infinity;
14988
+ let lastAnimT = 0;
14039
14989
  const renderLoop = (now) => {
14040
14990
  if (disposed || rt.paused) return;
14041
14991
  const fps = rt.cfg.sceneFps || 60;
@@ -14043,17 +14993,15 @@ vec3 DecompressNormal(vec4 tex) {
14043
14993
  if (now - lastRender >= interval) {
14044
14994
  lastRender = now;
14045
14995
  markFrame(rt, now);
14046
- if (rt.onFirstFrame) {
14047
- const first = rt.onFirstFrame;
14048
- rt.onFirstFrame = void 0;
14049
- first();
14050
- }
14051
14996
  syncCanvasSize(rt, c, rt.cfg);
14052
14997
  const t = (now - start - pauseAccum) / 1e3;
14053
14998
  inputView.update(pointerSrc.state);
14054
14999
  if (mediaSim.enabled) {
14055
15000
  if (live?.media) live.media.pump();
14056
- else simMedia.update(t);
15001
+ else {
15002
+ const drv = currentMediaDriver();
15003
+ if (typeof drv?.update === "function") drv.update(t);
15004
+ }
14057
15005
  const snap = mediaSnapshot();
14058
15006
  const evts = media.diffMediaEvents(lastMediaSnap, snap);
14059
15007
  if (evts.length) {
@@ -14068,34 +15016,37 @@ vec3 DecompressNormal(vec4 tex) {
14068
15016
  }
14069
15017
  if (live?.windowTitle) live.windowTitle.pump();
14070
15018
  else simWindow.update(t);
15019
+ const animDt = Math.max(0, t - lastAnimT);
15020
+ lastAnimT = t;
14071
15021
  for (const run of animRuns) {
14072
- run.ctrl.advance(interval / 1e3);
15022
+ run.ctrl.advance(animDt);
14073
15023
  const field = run.field;
15024
+ const slot = run.slot || field;
14074
15025
  const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
14075
15026
  if (Array.isArray(out)) {
14076
- const cur = run.layer[field];
15027
+ const cur = run.layer[slot];
14077
15028
  if (Array.isArray(cur)) for (let i = 0; i < out.length && i < cur.length; i++) cur[i] = out[i];
14078
15029
  } else if (Number.isFinite(out)) {
14079
15030
  if (field === "visible") run.layer[field] = !!out;
14080
- else run.layer[field] = out;
15031
+ else run.layer[slot] = out;
14081
15032
  }
14082
15033
  }
14083
15034
  for (const run of generalAnimRuns) {
14084
- run.ctrl.advance(interval / 1e3);
15035
+ run.ctrl.advance(animDt);
14085
15036
  const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
14086
15037
  if (typeof out === "number" && Number.isFinite(out)) run.write(out);
14087
15038
  else if (Array.isArray(out) && Number.isFinite(out[0])) run.write(out[0]);
14088
15039
  }
14089
15040
  for (const run of effectVisibleRuns) {
14090
15041
  if (run.sandbox.disabled) continue;
14091
- run.sandbox.engine.frametime = interval / 1e3;
15042
+ run.sandbox.engine.frametime = animDt;
14092
15043
  run.sandbox.engine.runtime = t;
14093
15044
  const ret = run.sandbox.callUpdate(!!run.effect.visible);
14094
15045
  if (typeof ret === "boolean") run.effect.visible = ret;
14095
15046
  }
14096
15047
  for (const run of generalScriptRuns) {
14097
15048
  if (run.sandbox.disabled) continue;
14098
- run.sandbox.engine.frametime = interval / 1e3;
15049
+ run.sandbox.engine.frametime = animDt;
14099
15050
  run.sandbox.engine.runtime = t;
14100
15051
  const g = scene.general || {};
14101
15052
  const cur = g[run.field] && typeof g[run.field] === "object" && "value" in g[run.field] ? g[run.field].value : g[run.field];
@@ -14103,10 +15054,16 @@ vec3 DecompressNormal(vec4 tex) {
14103
15054
  if (ret !== void 0) run.write(ret);
14104
15055
  }
14105
15056
  const screenRes = { x: c.clientWidth || window.innerWidth || 1, y: c.clientHeight || window.innerHeight || 1 };
15057
+ for (const sb of propSandboxes) {
15058
+ if (!sb || sb.disabled) continue;
15059
+ sb.engine.frametime = animDt;
15060
+ sb.engine.runtime = t;
15061
+ sb.engine.screenResolution = screenRes;
15062
+ }
14106
15063
  let visibilityDirty = false;
14107
15064
  for (const run of objectScriptRuns) {
14108
15065
  if (run.sandbox.disabled) continue;
14109
- run.sandbox.engine.frametime = interval / 1e3;
15066
+ run.sandbox.engine.frametime = animDt;
14110
15067
  run.sandbox.engine.runtime = t;
14111
15068
  run.sandbox.engine.screenResolution = screenRes;
14112
15069
  const cur = run.layer[run.field];
@@ -14127,26 +15084,37 @@ vec3 DecompressNormal(vec4 tex) {
14127
15084
  const n = Number(ret);
14128
15085
  if (Number.isFinite(n)) run.layer[run.field] = n;
14129
15086
  } else {
14130
- const v = run.field === "angles" ? wtext.radToScriptAngles(cur) : { x: cur[0] || 0, y: cur[1] || 0, z: cur[2] || 0 };
15087
+ const slot = run.slot || run.field;
15088
+ const lcur = run.layer[slot];
15089
+ const v = run.field === "angles" ? wtext.radToScriptAngles(lcur) : { x: lcur[0] || 0, y: lcur[1] || 0, z: lcur[2] || 0 };
14131
15090
  const ret = run.sandbox.callUpdate(v);
14132
15091
  const o = ret && typeof ret === "object" && "x" in ret ? ret : v;
14133
- run.layer[run.field] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
15092
+ run.layer[slot] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
14134
15093
  }
14135
15094
  }
14136
15095
  if (visibilityDirty) recomputeVisibility();
15096
+ if (transformDirty.size) scn.recomposeWorld(scene.layers, transformDirty);
14137
15097
  if (audioSim.enabled) {
14138
- if (audioDriverRef.current) audioDriverRef.current.pump();
14139
- else simAudio.update(t);
14140
- fillAudioBuffers(
14141
- audioViews,
14142
- audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot
14143
- );
15098
+ hostAudio.pump();
15099
+ if (!hostAudio.active) {
15100
+ if (audioDriverRef.current) audioDriverRef.current.pump();
15101
+ else simAudio.update(t);
15102
+ }
15103
+ fillAudioBuffers(audioViews, activeAudioSnapshot());
14144
15104
  }
14145
15105
  if (attachFollows.length) {
14146
15106
  mdl.followAttachments(attachFollows, t, getBoneOverrides);
14147
15107
  }
14148
15108
  const peek = rt.coverAlign;
14149
15109
  void renderer.render(scene, textures, c.width, c.height, t, normalizeFit(rt.cfg.fit), peek.x, peek.y).then(() => {
15110
+ if (rt.onFirstFrame) {
15111
+ const first = rt.onFirstFrame;
15112
+ rt.onFirstFrame = void 0;
15113
+ try {
15114
+ first();
15115
+ } catch {
15116
+ }
15117
+ }
14150
15118
  if (disposed || rt.paused) return;
14151
15119
  try {
14152
15120
  dispatchCursor();
@@ -14327,36 +15295,966 @@ vec3 DecompressNormal(vec4 tex) {
14327
15295
  }
14328
15296
  })();
14329
15297
  }
15298
+ const SHIM_MARK = 'data-we-shim="1"';
15299
+ const SHIM_ATTR = "data-we-shim-src";
15300
+ function entryDirUrl(entryUrl) {
15301
+ try {
15302
+ const u = new URL(entryUrl);
15303
+ const path = u.pathname;
15304
+ const i = path.lastIndexOf("/");
15305
+ u.pathname = i < 0 ? "/" : path.slice(0, i + 1);
15306
+ u.hash = "";
15307
+ u.search = "";
15308
+ return u.href;
15309
+ } catch {
15310
+ const s = entryUrl.replace(/[#?].*$/, "");
15311
+ const i = s.lastIndexOf("/");
15312
+ return i < 0 ? s : s.slice(0, i + 1);
15313
+ }
15314
+ }
15315
+ function hasBlockingCsp(html) {
15316
+ const re = /<meta[^>]+http-equiv\s*=\s*["']?Content-Security-Policy["']?[^>]*>/gi;
15317
+ let m;
15318
+ while (m = re.exec(html)) {
15319
+ const tag = m[0];
15320
+ const content = /content\s*=\s*"([^"]*)"/i.exec(tag)?.[1] ?? /content\s*=\s*'([^']*)'/i.exec(tag)?.[1] ?? "";
15321
+ if (!/script-src/i.test(content)) continue;
15322
+ if (/script-src[^;]*'unsafe-inline'/i.test(content)) continue;
15323
+ if (/script-src[^;]*\*/i.test(content)) continue;
15324
+ return true;
15325
+ }
15326
+ return false;
15327
+ }
15328
+ function escapeScriptClose(js) {
15329
+ return js.replace(/<\/script/gi, "<\\/script");
15330
+ }
15331
+ function rewriteHtml(html, shimSource2, opts) {
15332
+ if (!html) html = "";
15333
+ if (html.includes(SHIM_MARK) || html.includes(SHIM_ATTR)) return html;
15334
+ const base = opts.baseHref && !/<base\b/i.test(html) ? `<base href="${opts.baseHref.replace(/"/g, "&quot;")}">` : "";
15335
+ const script = `<script ${SHIM_ATTR}="1">
15336
+ ${escapeScriptClose(shimSource2)}
15337
+ <\/script>`;
15338
+ const seed = opts.seedScript && opts.seedScript.trim() ? `<script>
15339
+ ${escapeScriptClose(opts.seedScript)}
15340
+ <\/script>` : "";
15341
+ const inject = `${base}${script}${seed}`;
15342
+ const headOpen = /<head(\s[^>]*)?>/i.exec(html);
15343
+ if (headOpen) {
15344
+ const at = headOpen.index + headOpen[0].length;
15345
+ return html.slice(0, at) + inject + html.slice(at);
15346
+ }
15347
+ const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
15348
+ if (htmlOpen) {
15349
+ const at = htmlOpen.index + htmlOpen[0].length;
15350
+ return html.slice(0, at) + `<head>${inject}</head>` + html.slice(at);
15351
+ }
15352
+ return `<!DOCTYPE html><html><head>${inject}</head><body>${html}</body></html>`;
15353
+ }
15354
+ 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';
15355
+ function weShimCall(rt, call) {
15356
+ try {
15357
+ const win = rt.iframe?.contentWindow;
15358
+ if (win) call(win);
15359
+ } catch {
15360
+ }
15361
+ }
15362
+ function injectGpuThrottle(rt, f, _doc) {
15363
+ const win = f.contentWindow;
15364
+ if (!win) return;
15365
+ if (win.requestAnimationFrame?.__weThrottled) return;
15366
+ const fps = rt.cfg.sceneFps || 30;
15367
+ if (fps >= 60) return;
15368
+ const interval = 1e3 / fps;
15369
+ try {
15370
+ const origRaf = win.requestAnimationFrame.bind(win);
15371
+ const rafMap = /* @__PURE__ */ new Map();
15372
+ let counter = 0;
15373
+ win.requestAnimationFrame = (cb) => {
15374
+ const id = ++counter;
15375
+ const to = win.setTimeout(() => {
15376
+ rafMap.delete(id);
15377
+ origRaf((now) => {
15378
+ try {
15379
+ cb(now);
15380
+ } catch {
15381
+ }
15382
+ });
15383
+ }, interval);
15384
+ rafMap.set(id, to);
15385
+ return id;
15386
+ };
15387
+ win.cancelAnimationFrame = (id) => {
15388
+ const to = rafMap.get(id);
15389
+ if (to !== void 0) {
15390
+ win.clearTimeout(to);
15391
+ rafMap.delete(id);
15392
+ }
15393
+ };
15394
+ } catch {
15395
+ }
15396
+ }
15397
+ const pumpBuffer = new Float32Array(128);
15398
+ function packWebAudioArrayInto(out, left, right) {
15399
+ const nL = Math.min(64, left.length);
15400
+ const nR = Math.min(64, right.length);
15401
+ for (let i = 0; i < nL; i++) out[i] = Number(left[i]) || 0;
15402
+ for (let i = 0; i < nR; i++) out[64 + i] = Number(right[i]) || 0;
15403
+ return out;
15404
+ }
15405
+ const WEB_SIM_AUDIO_GAIN = 1.8;
15406
+ const WEB_SIM_AUDIO_GAMMA = 1.8;
15407
+ const WEB_AUDIO_PUMP_HZ = 30;
15408
+ function shapeWebAudioBand(pre) {
15409
+ const v = Number(pre) || 0;
15410
+ if (v <= 0) return 0;
15411
+ return Math.min(1, Math.pow(v, WEB_SIM_AUDIO_GAMMA) * WEB_SIM_AUDIO_GAIN);
15412
+ }
15413
+ function defaultAudioDriver() {
15414
+ const sim = createSimulatedAudio();
15415
+ const left = new Float32Array(64);
15416
+ const right = new Float32Array(64);
15417
+ return {
15418
+ tick(nowMs) {
15419
+ sim.update(nowMs / 1e3);
15420
+ },
15421
+ snapshot() {
15422
+ const s = sim.snapshot;
15423
+ const preL = s.preL64;
15424
+ const preR = s.preR64;
15425
+ for (let i = 0; i < 64; i++) {
15426
+ if (preL && preR) {
15427
+ left[i] = shapeWebAudioBand(preL[i]);
15428
+ right[i] = shapeWebAudioBand(preR[i]);
15429
+ } else {
15430
+ left[i] = (Number(s.left64[i]) || 0) * 0.2;
15431
+ right[i] = (Number(s.right64[i]) || 0) * 0.2;
15432
+ }
15433
+ }
15434
+ return { left, right };
15435
+ }
15436
+ };
15437
+ }
15438
+ function bridgeAudioDriver(rt) {
15439
+ const left = new Float32Array(64);
15440
+ const right = new Float32Array(64);
15441
+ return {
15442
+ snapshot() {
15443
+ const src = rt.audioBridge?.();
15444
+ const sl = src?.left;
15445
+ const sr = src?.right;
15446
+ const n = sl && sr ? Math.min(64, sl.length, sr.length) : 0;
15447
+ for (let i = 0; i < n; i++) {
15448
+ left[i] = Math.max(0, Math.min(1, Number(sl[i]) || 0));
15449
+ right[i] = Math.max(0, Math.min(1, Number(sr[i]) || 0));
15450
+ }
15451
+ for (let i = n; i < 64; i++) {
15452
+ left[i] = 0;
15453
+ right[i] = 0;
15454
+ }
15455
+ return { left, right };
15456
+ }
15457
+ };
15458
+ }
15459
+ function liveAudioDriver(handle) {
15460
+ const left = new Float32Array(64);
15461
+ const right = new Float32Array(64);
15462
+ return {
15463
+ tick() {
15464
+ handle.audio.pump();
15465
+ },
15466
+ snapshot() {
15467
+ const s = handle.audio.snapshot;
15468
+ for (let i = 0; i < 64; i++) {
15469
+ left[i] = Math.max(0, Math.min(1, Number(s.left64[i]) || 0));
15470
+ right[i] = Math.max(0, Math.min(1, Number(s.right64[i]) || 0));
15471
+ }
15472
+ return { left, right };
15473
+ }
15474
+ };
15475
+ }
15476
+ function resolveContainer(rt, cfg) {
15477
+ if (rt.wrap) return rt.wrap;
15478
+ const el = cfg.canvas;
15479
+ if (!el) return null;
15480
+ if (el instanceof HTMLCanvasElement) {
15481
+ const parent = el.parentElement;
15482
+ if (parent) {
15483
+ reportDiag(rt, cfg, "网页壁纸挂在 canvas 父容器上(canvas 不能有子节点;更适合空 div)");
15484
+ return parent;
15485
+ }
15486
+ return null;
15487
+ }
15488
+ return el;
15489
+ }
15490
+ function buildSeedScript(props, fps, volume) {
15491
+ const parts = [];
15492
+ if (fps != null && Number.isFinite(fps)) parts.push(`window.__weSetFps(${Number(fps)});`);
15493
+ if (volume != null && Number.isFinite(volume)) {
15494
+ parts.push(`window.__weSetVolume(${Math.max(0, Math.min(1, Number(volume)))});`);
15495
+ }
15496
+ if (props && Object.keys(props).length) {
15497
+ parts.push(`window.__weSeedProps(${JSON.stringify(props)});`);
15498
+ }
15499
+ return parts.join("\n");
15500
+ }
15501
+ function installLetterboxFix(rt, f, container) {
15502
+ const BASE = "position:absolute;border:none;background:transparent;";
15503
+ const applyFull = () => {
15504
+ f.style.cssText = BASE + "inset:0;width:100%;height:100%;";
15505
+ };
15506
+ applyFull();
15507
+ let lastKey = "";
15508
+ const relayout = () => {
15509
+ if (!f.isConnected) return;
15510
+ let doc = null;
15511
+ try {
15512
+ doc = f.contentDocument;
15513
+ } catch {
15514
+ return;
15515
+ }
15516
+ if (!doc) return;
15517
+ const stageW = container.clientWidth || window.innerWidth || 0;
15518
+ const stageH = container.clientHeight || window.innerHeight || 0;
15519
+ const cover = normalizeFit(rt.cfg.fit) === "cover";
15520
+ applyFull();
15521
+ const box = cover && stageW > 0 && stageH > 0 ? measureWebLetterbox(doc) : null;
15522
+ const vp = box ? webCoverViewport(stageW, stageH, box.contentAspect) : null;
15523
+ const key = vp ? `${Math.round(vp.width)}x${Math.round(vp.height)}` : "full";
15524
+ if (!vp) {
15525
+ lastKey = "full";
15526
+ return;
15527
+ }
15528
+ f.style.cssText = BASE + `left:${vp.left}px;top:${vp.top}px;width:${vp.width}px;height:${vp.height}px;`;
15529
+ if (key !== lastKey) {
15530
+ lastKey = key;
15531
+ reportDiag(
15532
+ rt,
15533
+ rt.cfg,
15534
+ `网页壁纸露底自适配:视口按内容比例改为 ${Math.round(vp.width)}×${Math.round(vp.height)}(cover 居中裁切)`
15535
+ );
15536
+ }
15537
+ };
15538
+ const onResize = () => relayout();
15539
+ window.addEventListener("resize", onResize);
15540
+ let ro;
15541
+ if (typeof ResizeObserver !== "undefined") {
15542
+ ro = new ResizeObserver(() => relayout());
15543
+ ro.observe(container);
15544
+ }
15545
+ const timers = [];
15546
+ const onLoad = () => {
15547
+ relayout();
15548
+ for (const d of [120, 400, 1200]) timers.push(window.setTimeout(relayout, d));
15549
+ try {
15550
+ const doc = f.contentDocument;
15551
+ if (doc) {
15552
+ for (const el of doc.querySelectorAll("video,img")) {
15553
+ el.addEventListener("loadedmetadata", relayout, { once: true });
15554
+ el.addEventListener("load", relayout, { once: true });
15555
+ }
15556
+ }
15557
+ } catch {
15558
+ }
15559
+ };
15560
+ f.addEventListener("load", onLoad);
15561
+ rt.webRelayout = relayout;
15562
+ const prev = rt.sceneCleanup;
15563
+ rt.sceneCleanup = () => {
15564
+ window.removeEventListener("resize", onResize);
15565
+ ro?.disconnect();
15566
+ for (const t of timers) clearTimeout(t);
15567
+ f.removeEventListener("load", onLoad);
15568
+ if (rt.webRelayout === relayout) rt.webRelayout = void 0;
15569
+ try {
15570
+ prev?.();
15571
+ } catch {
15572
+ }
15573
+ };
15574
+ }
15575
+ function webPointerToClient(u, v, stage, frame, client) {
15576
+ if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
15577
+ if (!(stage.width > 0) || !(stage.height > 0)) return null;
15578
+ const sx = frame.width > 0 && client.width > 0 ? frame.width / client.width : 1;
15579
+ const sy = frame.height > 0 && client.height > 0 ? frame.height / client.height : 1;
15580
+ return {
15581
+ x: (u * stage.width - (frame.left - stage.left)) / (sx || 1),
15582
+ y: (v * stage.height - (frame.top - stage.top)) / (sy || 1)
15583
+ };
15584
+ }
15585
+ function installWebPointerBridge(rt, f, container) {
15586
+ rt.pointerCtl = {
15587
+ push(p) {
15588
+ if (!f.isConnected) return;
15589
+ const cRect = container.getBoundingClientRect();
15590
+ const fRect = f.getBoundingClientRect();
15591
+ const pt = webPointerToClient(
15592
+ Number(p?.u),
15593
+ Number(p?.v),
15594
+ {
15595
+ left: cRect.left,
15596
+ top: cRect.top,
15597
+ width: cRect.width || container.clientWidth || window.innerWidth || 0,
15598
+ height: cRect.height || container.clientHeight || window.innerHeight || 0
15599
+ },
15600
+ { left: fRect.left, top: fRect.top, width: fRect.width, height: fRect.height },
15601
+ { width: f.clientWidth, height: f.clientHeight }
15602
+ );
15603
+ if (!pt) return;
15604
+ weShimCall(rt, (w) => w.__wePushPointer?.(pt.x, pt.y, Number(p.buttons) || 0));
15605
+ },
15606
+ leave() {
15607
+ weShimCall(rt, (w) => w.__wePointerLeave?.());
15608
+ }
15609
+ };
15610
+ }
15611
+ function attachIframe(rt, cfg, container, src, opts) {
15612
+ const f = document.createElement("iframe");
15613
+ f.setAttribute("sandbox", "allow-scripts allow-same-origin");
15614
+ f.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:none;background:transparent;";
15615
+ if (!rt.wrap && getComputedStyle(container).position === "static") {
15616
+ container.style.position = "relative";
15617
+ }
15618
+ f.src = src;
15619
+ container.appendChild(f);
15620
+ rt.iframe = f;
15621
+ if (opts.blobUrl) {
15622
+ (rt.objectUrls ??= []).push(opts.blobUrl);
15623
+ }
15624
+ installLetterboxFix(rt, f, container);
15625
+ if (opts.injected) installWebPointerBridge(rt, f, container);
15626
+ const onFrameMsg = (ev) => {
15627
+ if (ev.source !== f.contentWindow) return;
15628
+ const data = ev.data;
15629
+ if (!data || data.op !== "we-frame") return;
15630
+ if (rt.paused) return;
15631
+ const t = typeof data.t === "number" ? data.t : performance.now();
15632
+ if (opts.frameClock) opts.frameClock.last = t;
15633
+ markFrame(rt, t);
15634
+ };
15635
+ window.addEventListener("message", onFrameMsg);
15636
+ const prevCleanup = rt.sceneCleanup;
15637
+ rt.sceneCleanup = () => {
15638
+ window.removeEventListener("message", onFrameMsg);
15639
+ try {
15640
+ prevCleanup?.();
15641
+ } catch {
15642
+ }
15643
+ };
15644
+ f.addEventListener("load", () => {
15645
+ try {
15646
+ const doc = f.contentDocument;
15647
+ if (doc) window.__blockContextMenu?.(doc);
15648
+ if (!opts.injected) injectGpuThrottle(rt, f, doc);
15649
+ } catch {
15650
+ }
15651
+ weShimCall(rt, (w2) => {
15652
+ const wire = {};
15653
+ for (const [k, v] of Object.entries(rt.liveUserProps ?? {})) wire[k] = { value: v };
15654
+ w2.__weApplyProps?.(wire);
15655
+ w2.__weSetFps?.(rt.cfg.sceneFps ?? 60);
15656
+ w2.__weSetVolume?.(rt.cfg.muted === false ? 1 : 0);
15657
+ if (rt.paused) w2.__weSetPaused?.(true);
15658
+ });
15659
+ try {
15660
+ rt.onFirstFrame?.();
15661
+ rt.onFirstFrame = void 0;
15662
+ } catch {
15663
+ }
15664
+ const w = container.clientWidth || window.innerWidth || 1;
15665
+ const h = container.clientHeight || window.innerHeight || 1;
15666
+ try {
15667
+ rt.onSceneInfo?.({
15668
+ width: w,
15669
+ height: h,
15670
+ layerCount: 0,
15671
+ hasModels: false,
15672
+ hasParticles: false,
15673
+ hasText: false
15674
+ });
15675
+ } catch {
15676
+ }
15677
+ });
15678
+ }
15679
+ const WEB_ASPECT_EPS = 5e-3;
15680
+ const WEB_LETTERBOX_MIN_RATIO = 0.01;
15681
+ const WEB_ASPECT_MIN = 0.2;
15682
+ const WEB_ASPECT_MAX = 6;
15683
+ function webCoverViewport(stageW, stageH, contentAspect) {
15684
+ if (!(stageW > 0) || !(stageH > 0) || !(contentAspect > 0)) return null;
15685
+ const stageAspect = stageW / stageH;
15686
+ if (Math.abs(stageAspect - contentAspect) <= WEB_ASPECT_EPS) return null;
15687
+ if (stageAspect < contentAspect) {
15688
+ const width = stageH * contentAspect;
15689
+ return { width, height: stageH, left: (stageW - width) / 2, top: 0 };
15690
+ }
15691
+ const height = stageW / contentAspect;
15692
+ return { width: stageW, height, left: 0, top: (stageH - height) / 2 };
15693
+ }
15694
+ function measureWebLetterbox(doc) {
15695
+ const win = doc.defaultView;
15696
+ if (!win) return null;
15697
+ const vw = win.innerWidth;
15698
+ const vh = win.innerHeight;
15699
+ if (!(vw > 0) || !(vh > 0)) return null;
15700
+ const cands = [...doc.querySelectorAll("video,img")];
15701
+ for (const el of cands) {
15702
+ const r = el.getBoundingClientRect();
15703
+ if (r.width <= 0 || r.height <= 0) continue;
15704
+ if (r.width < vw * 0.98) continue;
15705
+ if (Math.abs(r.left) > vw * 0.02 || r.top > vh * 0.02) continue;
15706
+ if (vh - r.height < vh * WEB_LETTERBOX_MIN_RATIO) continue;
15707
+ const natW = el.videoWidth || el.naturalWidth || 0;
15708
+ const natH = el.videoHeight || el.naturalHeight || 0;
15709
+ if (!(natW > 0) || !(natH > 0)) continue;
15710
+ const aspect = natW / natH;
15711
+ if (!Number.isFinite(aspect) || aspect < WEB_ASPECT_MIN || aspect > WEB_ASPECT_MAX) continue;
15712
+ return { contentAspect: aspect };
15713
+ }
15714
+ return null;
15715
+ }
15716
+ function vecToCss(v) {
15717
+ if (!v) return "rgb(128,128,128)";
15718
+ const r = Math.round(Math.max(0, Math.min(1, Number(v.x) || 0)) * 255);
15719
+ const g = Math.round(Math.max(0, Math.min(1, Number(v.y) || 0)) * 255);
15720
+ const b = Math.round(Math.max(0, Math.min(1, Number(v.z) || 0)) * 255);
15721
+ return `rgb(${r},${g},${b})`;
15722
+ }
15723
+ function thumbDataUrlFromSnap(snap) {
15724
+ try {
15725
+ const c = document.createElement("canvas");
15726
+ c.width = c.height = 64;
15727
+ const ctx = c.getContext("2d");
15728
+ if (!ctx) return "";
15729
+ const p = snap.primaryColor;
15730
+ const s = snap.secondaryColor;
15731
+ const grd = ctx.createLinearGradient(0, 0, 64, 64);
15732
+ grd.addColorStop(0, vecToCss(p));
15733
+ grd.addColorStop(1, vecToCss(s));
15734
+ ctx.fillStyle = grd;
15735
+ ctx.fillRect(0, 0, 64, 64);
15736
+ return c.toDataURL("image/jpeg", 0.85);
15737
+ } catch {
15738
+ return "";
15739
+ }
15740
+ }
15741
+ function defaultMediaDriver() {
15742
+ return media.createSimulatedMedia();
15743
+ }
15744
+ function pushMediaDiff(rt, prev, snap) {
15745
+ const events = media.diffMediaEvents(prev, snap);
15746
+ for (const { name, event } of events) {
15747
+ if (name === "mediaStatusChanged") {
15748
+ weShimCall(rt, (w) => w.__wePushMedia?.({ op: "status", enabled: !!event.enabled }));
15749
+ } else if (name === "mediaPropertiesChanged") {
15750
+ weShimCall(
15751
+ rt,
15752
+ (w) => w.__wePushMedia?.({
15753
+ op: "properties",
15754
+ title: event.title ?? "",
15755
+ artist: event.artist ?? "",
15756
+ album: event.album ?? "",
15757
+ albumArtist: event.albumArtist ?? ""
15758
+ })
15759
+ );
15760
+ } else if (name === "mediaThumbnailChanged") {
15761
+ const thumb = thumbDataUrlFromSnap(snap);
15762
+ weShimCall(
15763
+ rt,
15764
+ (w) => w.__wePushMedia?.({
15765
+ op: "thumbnail",
15766
+ thumbnail: thumb,
15767
+ hasThumbnail: !!event.hasThumbnail || !!thumb,
15768
+ primaryColor: vecToCss(event.primaryColor),
15769
+ secondaryColor: vecToCss(event.secondaryColor),
15770
+ tertiaryColor: vecToCss(event.tertiaryColor),
15771
+ textColor: vecToCss(event.textColor),
15772
+ highContrastColor: vecToCss(event.highContrastColor)
15773
+ })
15774
+ );
15775
+ } else if (name === "mediaPlaybackChanged") {
15776
+ weShimCall(rt, (w) => w.__wePushMedia?.({ op: "playback", state: Number(event.state) || 0 }));
15777
+ } else if (name === "mediaTimelineChanged") {
15778
+ weShimCall(
15779
+ rt,
15780
+ (w) => w.__wePushMedia?.({
15781
+ op: "timeline",
15782
+ position: Number(event.position) || 0,
15783
+ duration: Number(event.duration) || 0
15784
+ })
15785
+ );
15786
+ }
15787
+ }
15788
+ return media.cloneMediaSnapshot(snap);
15789
+ }
15790
+ function startAudioPump(rt, driver, frameClock, liveHold = { driver: null }) {
15791
+ if (!driver) return;
15792
+ const bridged = bridgeAudioDriver(rt);
15793
+ const pick = () => rt.audioBridge ? bridged : liveHold.driver ?? driver;
15794
+ let raf = 0;
15795
+ let lastPush = 0;
15796
+ const tick = (now) => {
15797
+ raf = requestAnimationFrame(tick);
15798
+ if (rt.paused || !rt.iframe) return;
15799
+ const fps = rt.cfg.sceneFps || 60;
15800
+ const pumpFps = Math.min(Math.max(1, fps), WEB_AUDIO_PUMP_HZ);
15801
+ const interval = 1e3 / pumpFps;
15802
+ if (now - lastPush < interval * 0.85) return;
15803
+ lastPush = now;
15804
+ try {
15805
+ const cur = pick();
15806
+ cur.tick?.(now);
15807
+ const snap = cur.snapshot();
15808
+ const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
15809
+ weShimCall(rt, (w) => w.__wePushAudio?.(arr));
15810
+ if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
15811
+ } catch {
15812
+ }
15813
+ };
15814
+ raf = requestAnimationFrame(tick);
15815
+ const prev = rt.sceneCleanup;
15816
+ rt.sceneCleanup = () => {
15817
+ cancelAnimationFrame(raf);
15818
+ try {
15819
+ prev?.();
15820
+ } catch {
15821
+ }
15822
+ };
15823
+ }
15824
+ function startMediaPump(rt, driver) {
15825
+ if (!driver) return;
15826
+ const pick = () => rt.mediaSource ?? driver;
15827
+ let raf = 0;
15828
+ let lastMedia = null;
15829
+ let lastTick = 0;
15830
+ const tick = (now) => {
15831
+ raf = requestAnimationFrame(tick);
15832
+ if (rt.paused || !rt.iframe) return;
15833
+ if (now - lastTick < 200) return;
15834
+ lastTick = now;
15835
+ try {
15836
+ const cur = pick();
15837
+ cur.update?.(now / 1e3);
15838
+ lastMedia = pushMediaDiff(rt, lastMedia, cur.snapshot);
15839
+ } catch {
15840
+ }
15841
+ };
15842
+ raf = requestAnimationFrame(tick);
15843
+ const prev = rt.sceneCleanup;
15844
+ rt.sceneCleanup = () => {
15845
+ cancelAnimationFrame(raf);
15846
+ try {
15847
+ prev?.();
15848
+ } catch {
15849
+ }
15850
+ };
15851
+ }
15852
+ function installWebCtl(rt) {
15853
+ rt.sceneCtl = {
15854
+ pause() {
15855
+ rt.paused = true;
15856
+ weShimCall(rt, (w) => w.__weSetPaused?.(true));
15857
+ },
15858
+ resume() {
15859
+ rt.paused = false;
15860
+ weShimCall(rt, (w) => w.__weSetPaused?.(false));
15861
+ },
15862
+ applyUserProperties(props) {
15863
+ const flat = { ...rt.liveUserProps ?? {} };
15864
+ for (const [k, v] of Object.entries(props ?? {})) {
15865
+ const val = v && typeof v === "object" && "value" in v ? v.value : v;
15866
+ flat[k] = val;
15867
+ }
15868
+ rt.liveUserProps = flat;
15869
+ weShimCall(rt, (w) => w.__weApplyProps?.(props));
15870
+ }
15871
+ };
15872
+ }
15873
+ function isSameOriginUrl(url) {
15874
+ try {
15875
+ return new URL(url, location.href).origin === location.origin;
15876
+ } catch {
15877
+ return false;
15878
+ }
15879
+ }
15880
+ function projectPropertiesToWire(project) {
15881
+ const props = project?.general?.properties;
15882
+ if (!props || typeof props !== "object") return {};
15883
+ const out = {};
15884
+ for (const [name, def] of Object.entries(props)) {
15885
+ if (!def || typeof def !== "object" || typeof def.type !== "string") continue;
15886
+ const raw = def.value;
15887
+ const type = def.type.toLowerCase();
15888
+ if (raw === null || raw === void 0) {
15889
+ if (type === "file" || type === "directory") {
15890
+ out[name] = { value: "" };
15891
+ continue;
15892
+ }
15893
+ if (!("value" in def)) continue;
15894
+ }
15895
+ out[name] = { value: raw };
15896
+ }
15897
+ return out;
15898
+ }
15899
+ async function fetchProjectWire(entryUrl) {
15900
+ try {
15901
+ const projUrl = new URL("project.json", new URL(entryUrl, location.href));
15902
+ const r = await fetch(projUrl.href, { credentials: "same-origin" });
15903
+ if (!r.ok) return {};
15904
+ return projectPropertiesToWire(await r.json());
15905
+ } catch {
15906
+ return {};
15907
+ }
15908
+ }
15909
+ function mergeLiveIntoWire(defaults, live) {
15910
+ const out = { ...defaults };
15911
+ if (live) {
15912
+ for (const [k, v] of Object.entries(live)) out[k] = { value: v };
15913
+ }
15914
+ return out;
15915
+ }
15916
+ function mountWeb(rt, cfg) {
15917
+ clear(rt);
15918
+ rt.cfg = cfg;
15919
+ const container = resolveContainer(rt, cfg);
15920
+ if (!container) {
15921
+ reportDiag(rt, cfg, "网页壁纸:无可用容器");
15922
+ rt.onError?.(new Error("网页壁纸:无可用容器"));
15923
+ return;
15924
+ }
15925
+ const entry = cfg.src ?? "";
15926
+ if (!entry) {
15927
+ reportDiag(rt, cfg, "网页壁纸:缺少 src");
15928
+ rt.onError?.(new Error("网页壁纸:缺少 src"));
15929
+ return;
15930
+ }
15931
+ installWebCtl(rt);
15932
+ const cfgExt = cfg;
15933
+ const audioDriver = cfgExt._webAudio === null || rt.audioDisabled ? null : cfgExt._webAudio ?? defaultAudioDriver();
15934
+ const mediaDriver = cfgExt._webMedia === null || rt.mediaDisabled ? null : cfgExt._webMedia ?? defaultMediaDriver();
15935
+ const finishBare = (why) => {
15936
+ reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
15937
+ attachIframe(rt, cfg, container, entry, { injected: false });
15938
+ startAudioPump(rt, null);
15939
+ startMediaPump(rt, null);
15940
+ let beat = 0;
15941
+ const tick = (now) => {
15942
+ if (!rt.iframe) return;
15943
+ beat = requestAnimationFrame(tick);
15944
+ if (rt.paused) return;
15945
+ markFrame(rt, now);
15946
+ };
15947
+ beat = requestAnimationFrame(tick);
15948
+ (rt.wallpaperDisposers ??= []).push(() => cancelAnimationFrame(beat));
15949
+ };
15950
+ const frameClock = { last: 0 };
15951
+ const liveHold = { driver: null };
15952
+ const startPumps = () => {
15953
+ startAudioPump(rt, audioDriver, frameClock, liveHold);
15954
+ startMediaPump(rt, mediaDriver);
15955
+ if (cfg.liveSystem && audioDriver) {
15956
+ const liveSlot = {
15957
+ handle: null,
15958
+ dead: false
15959
+ };
15960
+ (rt.wallpaperDisposers ??= []).push(() => {
15961
+ liveSlot.dead = true;
15962
+ liveHold.driver = null;
15963
+ try {
15964
+ liveSlot.handle?.dispose();
15965
+ } catch {
15966
+ }
15967
+ liveSlot.handle = null;
15968
+ });
15969
+ void (async () => {
15970
+ try {
15971
+ const live = await startLiveSystem({ origin: location.origin });
15972
+ if (liveSlot.dead) {
15973
+ try {
15974
+ live.dispose();
15975
+ } catch {
15976
+ }
15977
+ return;
15978
+ }
15979
+ liveSlot.handle = live;
15980
+ const st = live.status();
15981
+ if (st.audio === "mic") {
15982
+ liveHold.driver = liveAudioDriver(live);
15983
+ reportDiag(rt, cfg, "liveSystem: 网页壁纸音频改用麦克风");
15984
+ } else {
15985
+ reportDiag(rt, cfg, `liveSystem: 麦克风不可用(${st.audio}),网页壁纸沿用模拟源`);
15986
+ }
15987
+ } catch (e) {
15988
+ reportDiag(rt, cfg, `liveSystem: 启动失败,网页壁纸沿用模拟源 (${e?.message ?? e})`);
15989
+ }
15990
+ })();
15991
+ }
15992
+ };
15993
+ void (async () => {
15994
+ const defaults = await fetchProjectWire(entry);
15995
+ const wire = mergeLiveIntoWire(defaults, rt.liveUserProps);
15996
+ rt.liveUserProps = Object.fromEntries(Object.entries(wire).map(([k, w]) => [k, w.value]));
15997
+ if (isSameOriginUrl(entry)) {
15998
+ attachIframe(rt, cfg, container, entry, { injected: true, frameClock });
15999
+ startPumps();
16000
+ const f = rt.iframe;
16001
+ f?.addEventListener(
16002
+ "load",
16003
+ () => {
16004
+ let hasShim = false;
16005
+ weShimCall(rt, (w) => {
16006
+ hasShim = typeof w.__weSetPaused === "function";
16007
+ });
16008
+ if (!hasShim) {
16009
+ reportDiag(
16010
+ rt,
16011
+ cfg,
16012
+ "网页壁纸:同源入口未检测到 WE shim(host 未注入?);Spine 类壁纸请确认 /web/ HTML 改写"
16013
+ );
16014
+ }
16015
+ },
16016
+ { once: true }
16017
+ );
16018
+ return;
16019
+ }
16020
+ try {
16021
+ const res = await fetch(entry, { credentials: "same-origin" });
16022
+ if (!res.ok) {
16023
+ finishBare(`HTTP ${res.status}`);
16024
+ return;
16025
+ }
16026
+ const html = await res.text();
16027
+ if (hasBlockingCsp(html)) {
16028
+ finishBare("CSP 阻止 inline script");
16029
+ return;
16030
+ }
16031
+ const rewritten = rewriteHtml(html, shimSource, {
16032
+ baseHref: entryDirUrl(entry),
16033
+ seedScript: buildSeedScript(wire, cfg.sceneFps, cfg.muted === false ? 1 : 0)
16034
+ });
16035
+ const blob = new Blob([rewritten], { type: "text/html;charset=utf-8" });
16036
+ const blobUrl = URL.createObjectURL(blob);
16037
+ attachIframe(rt, cfg, container, blobUrl, { blobUrl, injected: true, frameClock });
16038
+ startPumps();
16039
+ } catch (e) {
16040
+ finishBare(e instanceof Error ? e.message : String(e));
16041
+ }
16042
+ })();
16043
+ }
14330
16044
  function mountWallpaper(rt, cfg) {
14331
- if ((cfg.type === "video" || cfg.type === "gif" || cfg.type === "image") && cfg.src) {
16045
+ const type = String(cfg.type ?? "").toLowerCase();
16046
+ cfg = { ...cfg, type };
16047
+ rt.cfg = cfg;
16048
+ if ((type === "video" || type === "gif" || type === "image") && cfg.src) {
14332
16049
  mountMedia(rt, cfg);
14333
- } else if (cfg.type === "scene" && (cfg.source || cfg.src)) {
16050
+ } else if (type === "scene" && (cfg.source || cfg.src)) {
14334
16051
  mountScene(rt, cfg);
16052
+ } else if (type === "web" && cfg.src) {
16053
+ mountWeb(rt, cfg);
14335
16054
  } else {
14336
16055
  rt.onUnhandledType?.(cfg);
14337
16056
  }
14338
16057
  }
16058
+ class Color {
16059
+ x;
16060
+ y;
16061
+ z;
16062
+ constructor(x, y, z) {
16063
+ this.x = Number(x) || 0;
16064
+ this.y = Number(y) || 0;
16065
+ this.z = Number(z) || 0;
16066
+ }
16067
+ add(o) {
16068
+ return new Color(this.x + o.x, this.y + o.y, this.z + o.z);
16069
+ }
16070
+ subtract(o) {
16071
+ return new Color(this.x - o.x, this.y - o.y, this.z - o.z);
16072
+ }
16073
+ multiply(k) {
16074
+ if (typeof k === "number") return new Color(this.x * k, this.y * k, this.z * k);
16075
+ return new Color(this.x * k.x, this.y * k.y, this.z * k.z);
16076
+ }
16077
+ toString() {
16078
+ return `${this.x} ${this.y} ${this.z}`;
16079
+ }
16080
+ }
16081
+ function mediaColor(r, g, b) {
16082
+ if (r instanceof Color) return r;
16083
+ if (Array.isArray(r)) return new Color(r[0] ?? 0, r[1] ?? 0, r[2] ?? 0);
16084
+ if (typeof r === "object" && r !== null) {
16085
+ const o = r;
16086
+ return new Color(o.x ?? 0, o.y ?? 0, o.z ?? 0);
16087
+ }
16088
+ return new Color(r, g ?? 0, b ?? 0);
16089
+ }
16090
+ const DEF_PRIMARY = [0.35, 0.38, 0.45];
16091
+ const DEF_SECONDARY = [0.12, 0.13, 0.17];
16092
+ const DEF_TERTIARY = [0.72, 0.76, 0.84];
16093
+ const DEF_TEXT = [0.95, 0.96, 0.98];
16094
+ function locateLyric(lyrics, position) {
16095
+ if (!Array.isArray(lyrics) || lyrics.length === 0) return { line: "", index: -1 };
16096
+ let idx = -1;
16097
+ for (let i = 0; i < lyrics.length; i++) {
16098
+ const at = Number(lyrics[i]?.[0]);
16099
+ if (Number.isFinite(at) && at <= position) idx = i;
16100
+ else break;
16101
+ }
16102
+ return { line: idx >= 0 ? String(lyrics[idx][1] ?? "") : "", index: idx };
16103
+ }
16104
+ function buildSnapshot(init) {
16105
+ const position = Number(init.position) || 0;
16106
+ const lyrics = Array.isArray(init.lyrics) ? init.lyrics : [];
16107
+ const { line, index } = locateLyric(lyrics, position);
16108
+ const state = init.state !== void 0 ? init.state : init.playing === false ? 2 : init.playing ? 1 : 0;
16109
+ return {
16110
+ hasMedia: init.hasMedia ?? (init.title != null || init.artist != null || !!init.playing),
16111
+ state,
16112
+ title: String(init.title ?? ""),
16113
+ artist: String(init.artist ?? ""),
16114
+ album: String(init.album ?? ""),
16115
+ albumArtist: String(init.albumArtist ?? init.artist ?? ""),
16116
+ position,
16117
+ duration: Number(init.duration) || 0,
16118
+ hasThumbnail: init.hasThumbnail ?? false,
16119
+ primaryColor: mediaColor(init.primaryColor ?? DEF_PRIMARY),
16120
+ secondaryColor: mediaColor(init.secondaryColor ?? DEF_SECONDARY),
16121
+ tertiaryColor: mediaColor(init.tertiaryColor ?? DEF_TERTIARY),
16122
+ textColor: mediaColor(init.textColor ?? DEF_TEXT),
16123
+ highContrastColor: mediaColor(init.highContrastColor ?? init.textColor ?? DEF_TEXT),
16124
+ trackIndex: Number(init.trackIndex) || 0,
16125
+ lyrics,
16126
+ lyricLine: line,
16127
+ lyricIndex: index
16128
+ };
16129
+ }
16130
+ function createMediaSource(init = {}, controls = {}) {
16131
+ let cur = { ...init };
16132
+ let snap = buildSnapshot(cur);
16133
+ return {
16134
+ get snapshot() {
16135
+ return snap;
16136
+ },
16137
+ set(patch) {
16138
+ cur = { ...cur, ...patch };
16139
+ snap = buildSnapshot(cur);
16140
+ },
16141
+ // 宿主的快照由外部事件驱动,不需要按帧自行推进;留空实现满足接口即可
16142
+ update() {
16143
+ },
16144
+ skipNext: controls.skipNext,
16145
+ skipPrevious: controls.skipPrevious,
16146
+ play: controls.play,
16147
+ pause: controls.pause,
16148
+ playPause: controls.playPause
16149
+ };
16150
+ }
14339
16151
  function normalizeFitOption(fit) {
14340
16152
  if (fit === "fit") return "contain";
14341
16153
  if (fit === "fill") return "cover";
14342
16154
  return fit === "contain" || fit === "stretch" ? fit : "cover";
14343
16155
  }
14344
- function toConfig(canvas, o) {
14345
- return {
14346
- type: "scene",
14347
- canvas,
14348
- source: o.source,
16156
+ function isWebProject(project) {
16157
+ const t = project?.type;
16158
+ return typeof t === "string" && t.toLowerCase() === "web";
16159
+ }
16160
+ const MEDIA_TYPES = /* @__PURE__ */ new Set(["video", "gif", "image"]);
16161
+ function mediaProjectType(project) {
16162
+ const t = project?.type;
16163
+ if (typeof t !== "string") return null;
16164
+ const lower = t.toLowerCase();
16165
+ return MEDIA_TYPES.has(lower) ? lower : null;
16166
+ }
16167
+ function ensureSceneCanvas(el) {
16168
+ if (el instanceof HTMLCanvasElement) return el;
16169
+ const existing = el.querySelector(":scope > canvas[data-webwallgl]");
16170
+ if (existing instanceof HTMLCanvasElement) return existing;
16171
+ const c = document.createElement("canvas");
16172
+ c.setAttribute("data-webwallgl", "1");
16173
+ c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
16174
+ if (getComputedStyle(el).position === "static") el.style.position = "relative";
16175
+ el.appendChild(c);
16176
+ return c;
16177
+ }
16178
+ async function resolveMountConfig(el, o) {
16179
+ const base = {
14349
16180
  fit: normalizeFitOption(o.fit),
14350
16181
  renderDpr: o.renderDpr ?? 1,
14351
16182
  sceneFps: o.fps ?? 60,
14352
16183
  muted: (o.volume ?? 0) <= 0,
14353
- loop: true
16184
+ loop: true,
16185
+ canvas: el,
16186
+ source: o.source
14354
16187
  };
16188
+ let project = null;
16189
+ try {
16190
+ project = await o.source.project?.() ?? null;
16191
+ } catch {
16192
+ project = null;
16193
+ }
16194
+ if (isWebProject(project)) {
16195
+ let url;
16196
+ try {
16197
+ const entry = await o.source.webEntry?.();
16198
+ url = entry?.url;
16199
+ } catch {
16200
+ url = void 0;
16201
+ }
16202
+ if (!url && o.source.key) {
16203
+ const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") || "index.html" : "index.html";
16204
+ url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
16205
+ }
16206
+ if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
16207
+ return { ...base, type: "web", src: url, source: o.source };
16208
+ }
16209
+ const mediaType = mediaProjectType(project);
16210
+ if (mediaType) {
16211
+ let url;
16212
+ let entryType;
16213
+ try {
16214
+ const entry = await o.source.mediaEntry?.();
16215
+ url = entry?.url;
16216
+ entryType = entry?.type;
16217
+ } catch {
16218
+ url = void 0;
16219
+ }
16220
+ if (!url && o.source.key) {
16221
+ const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") : "";
16222
+ if (file) url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
16223
+ }
16224
+ if (!url) {
16225
+ throw new Error(
16226
+ `媒体壁纸(${mediaType}):无法解析资源 URL(需要 Source.mediaEntry 或 project.file + httpSource)`
16227
+ );
16228
+ }
16229
+ const canvas2 = ensureSceneCanvas(el);
16230
+ const finalType = MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : mediaType;
16231
+ return { ...base, type: finalType, src: url, canvas: canvas2, source: o.source };
16232
+ }
16233
+ const declaredType = typeof project?.type === "string" ? String(project.type).trim() : "";
16234
+ if (!declaredType && typeof o.source.mediaEntry === "function") {
16235
+ let url;
16236
+ let entryType;
16237
+ try {
16238
+ const entry = await o.source.mediaEntry();
16239
+ url = entry?.url;
16240
+ entryType = entry?.type;
16241
+ } catch {
16242
+ url = void 0;
16243
+ }
16244
+ const sniffed = (MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : null) ?? (url ? sniffMediaType(url) : null);
16245
+ if (url && sniffed) {
16246
+ const canvas2 = ensureSceneCanvas(el);
16247
+ return { ...base, type: sniffed, src: url, canvas: canvas2, source: o.source };
16248
+ }
16249
+ }
16250
+ const canvas = ensureSceneCanvas(el);
16251
+ return { ...base, type: "scene", canvas, source: o.source };
14355
16252
  }
14356
- function createScene(canvas, options) {
16253
+ function createScene(el, options) {
14357
16254
  const rt = createRuntime();
14358
16255
  const events = { ready: [], error: [], diagnostic: [] };
14359
16256
  let currentOptions = { ...options ?? {}, source: null };
16257
+ let boundEl = el;
14360
16258
  const emitError = (err) => {
14361
16259
  for (const fn of events.error) {
14362
16260
  try {
@@ -14388,6 +16286,26 @@ vec3 DecompressNormal(vec4 tex) {
14388
16286
  rt.onSceneInfo = (info) => {
14389
16287
  rt.info = info;
14390
16288
  };
16289
+ if ("audio" in o) applyAudio(o.audio ?? null);
16290
+ if ("media" in o) {
16291
+ rt.mediaSource = o.media ?? null;
16292
+ rt.mediaDisabled = o.media === null;
16293
+ }
16294
+ };
16295
+ const applyAudio = (src) => {
16296
+ rt.audioDisabled = src === null;
16297
+ if (!src) {
16298
+ rt.audioBridge = null;
16299
+ return;
16300
+ }
16301
+ rt.audioBridge = () => {
16302
+ try {
16303
+ const s = src.snapshot();
16304
+ return s && s.left && s.right ? s : null;
16305
+ } catch {
16306
+ return null;
16307
+ }
16308
+ };
14391
16309
  };
14392
16310
  const armFirstFrame = () => {
14393
16311
  return new Promise((resolve) => {
@@ -14395,6 +16313,24 @@ vec3 DecompressNormal(vec4 tex) {
14395
16313
  rt.onFirstFrame = () => {
14396
16314
  rt.onFirstFrame = void 0;
14397
16315
  prev?.();
16316
+ const info = rt.info ?? {
16317
+ width: 0,
16318
+ height: 0,
16319
+ layerCount: 0,
16320
+ hasModels: false,
16321
+ hasParticles: false,
16322
+ hasText: false
16323
+ };
16324
+ try {
16325
+ currentOptions.onReady?.(info);
16326
+ } catch {
16327
+ }
16328
+ for (const fn of events.ready) {
16329
+ try {
16330
+ fn(info);
16331
+ } catch {
16332
+ }
16333
+ }
14398
16334
  resolve();
14399
16335
  };
14400
16336
  });
@@ -14408,7 +16344,9 @@ vec3 DecompressNormal(vec4 tex) {
14408
16344
  return { promise, off };
14409
16345
  };
14410
16346
  const instance = {
14411
- canvas,
16347
+ get canvas() {
16348
+ return boundEl;
16349
+ },
14412
16350
  pause() {
14413
16351
  rt.paused = true;
14414
16352
  rt.sceneCtl?.pause();
@@ -14424,14 +16362,17 @@ vec3 DecompressNormal(vec4 tex) {
14424
16362
  setFit(fit) {
14425
16363
  rt.cfg.fit = fit;
14426
16364
  resetCoverAlign(rt);
16365
+ rt.webRelayout?.();
14427
16366
  },
14428
16367
  setFps(fps) {
14429
16368
  rt.cfg.sceneFps = fps;
16369
+ weShimCall(rt, (w) => w.__weSetFps?.(fps));
14430
16370
  },
14431
16371
  setVolume(volume) {
14432
16372
  const v = Math.max(0, Math.min(1, volume));
14433
16373
  rt.cfg.muted = v <= 0;
14434
16374
  rt.sceneAudio?.setVolume(v);
16375
+ weShimCall(rt, (w) => w.__weSetVolume?.(v));
14435
16376
  },
14436
16377
  setRenderDpr(dpr) {
14437
16378
  rt.cfg.renderDpr = dpr;
@@ -14445,20 +16386,90 @@ vec3 DecompressNormal(vec4 tex) {
14445
16386
  getProperties() {
14446
16387
  return { ...rt.liveUserProps ?? {} };
14447
16388
  },
16389
+ // 宿主频谱源。只存引用,实际每帧拉取在 scene-mount 的渲染循环里。
16390
+ // 与 __wp.setAudioBridge 同纪律:换场景不清空,装一次对之后所有场景生效
16391
+ // (wireOptions 也只在选项里出现 audio 时才覆盖,见那里的说明)。
16392
+ setAudio(src) {
16393
+ currentOptions = { ...currentOptions, audio: src };
16394
+ applyAudio(src);
16395
+ rt.audioDisabled = false;
16396
+ },
16397
+ // 系统媒体源。与 setAudio 同纪律:只存引用、换场景不清空,
16398
+ // scene 与 web 两条装配路径读同一个 rt.mediaSource。
16399
+ // 注意语义与 MountOptions.media:null 不同:这里的 null 按文档是
16400
+ // 「回落内置模拟源」,不是禁用(禁用只在挂载选项里表达)。
16401
+ setMedia(src) {
16402
+ currentOptions = { ...currentOptions, media: src };
16403
+ rt.mediaSource = src ?? null;
16404
+ rt.mediaDisabled = false;
16405
+ },
16406
+ // 媒体控制面。装配后由 mountScene 写入 rt.mediaCtl;未装配(或媒体/网页
16407
+ // 壁纸尚无控制面)时给一个惰性替身,读快照得空、控制方法静默无效 ——
16408
+ // 让调用方能无条件 `wp.media.playPause()` 而不必先判空。
16409
+ get media() {
16410
+ const ctl = rt.mediaCtl;
16411
+ if (ctl) return ctl;
16412
+ const empty = {
16413
+ hasMedia: false,
16414
+ state: 0,
16415
+ title: "",
16416
+ artist: "",
16417
+ album: "",
16418
+ albumArtist: "",
16419
+ position: 0,
16420
+ duration: 0,
16421
+ hasThumbnail: false,
16422
+ primaryColor: mediaColor(0, 0, 0),
16423
+ secondaryColor: mediaColor(0, 0, 0),
16424
+ tertiaryColor: mediaColor(0, 0, 0),
16425
+ textColor: mediaColor(1, 1, 1),
16426
+ highContrastColor: mediaColor(1, 1, 1),
16427
+ trackIndex: 0,
16428
+ lyrics: [],
16429
+ lyricLine: "",
16430
+ lyricIndex: -1
16431
+ };
16432
+ const noop = () => empty;
16433
+ return {
16434
+ get snapshot() {
16435
+ return empty;
16436
+ },
16437
+ skipNext: noop,
16438
+ skipPrevious: noop,
16439
+ play: noop,
16440
+ pause: noop,
16441
+ playPause: noop
16442
+ };
16443
+ },
16444
+ // 外部指针注入。pointerCtl 由 mountScene / mountWeb 各自装配时设置,
16445
+ // 媒体壁纸不设 —— 那时这里静默无效,与整页渲染器的 __wp.pushPointer 一致。
16446
+ pushPointer(u, v, buttons) {
16447
+ rt.pointerCtl?.push({ u, v, buttons });
16448
+ },
16449
+ pointerLeave() {
16450
+ rt.pointerCtl?.leave();
16451
+ },
14448
16452
  async load(source) {
16453
+ const prev = currentOptions.source;
16454
+ if (prev && prev !== source) {
16455
+ try {
16456
+ prev.dispose?.();
16457
+ } catch {
16458
+ }
16459
+ }
14449
16460
  currentOptions = { ...currentOptions, source };
14450
16461
  wireOptions(currentOptions);
14451
- const cfg = {
14452
- ...rt.cfg,
14453
- type: "scene",
14454
- source,
14455
- src: void 0,
14456
- mediaBase: void 0
14457
- };
16462
+ const cfg = await resolveMountConfig(boundEl, currentOptions);
16463
+ if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
16464
+ boundEl = cfg.canvas;
16465
+ }
14458
16466
  rt.cfg = cfg;
14459
16467
  rt.paused = false;
14460
16468
  rt.info = void 0;
14461
16469
  resetCoverAlign(rt);
16470
+ if (currentOptions.properties) {
16471
+ rt.liveUserProps = { ...currentOptions.properties };
16472
+ }
14462
16473
  const firstFrame = armFirstFrame();
14463
16474
  const failure = armFailure();
14464
16475
  mountWallpaper(rt, cfg);
@@ -14476,6 +16487,10 @@ vec3 DecompressNormal(vec4 tex) {
14476
16487
  },
14477
16488
  destroy() {
14478
16489
  destroyRuntime(rt);
16490
+ try {
16491
+ currentOptions.source?.dispose?.();
16492
+ } catch {
16493
+ }
14479
16494
  rt.onDiagnostic = void 0;
14480
16495
  rt.onError = void 0;
14481
16496
  rt.onFirstFrame = void 0;
@@ -14502,10 +16517,17 @@ vec3 DecompressNormal(vec4 tex) {
14502
16517
  const applyOptions = async (o) => {
14503
16518
  currentOptions = o;
14504
16519
  wireOptions(o);
14505
- rt.cfg = toConfig(canvas, o);
14506
- rt.paused = o.autoplay === false;
16520
+ const cfg = await resolveMountConfig(el, o);
16521
+ if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
16522
+ boundEl = cfg.canvas;
16523
+ }
16524
+ rt.cfg = cfg;
16525
+ rt.paused = false;
14507
16526
  rt.info = void 0;
14508
16527
  resetCoverAlign(rt);
16528
+ if (o.properties && Object.keys(o.properties).length) {
16529
+ rt.liveUserProps = { ...o.properties };
16530
+ }
14509
16531
  const firstFrame = armFirstFrame();
14510
16532
  const failure = armFailure();
14511
16533
  mountWallpaper(rt, rt.cfg);
@@ -14514,23 +16536,28 @@ vec3 DecompressNormal(vec4 tex) {
14514
16536
  } finally {
14515
16537
  failure.off();
14516
16538
  }
16539
+ if (o.autoplay === false) instance.pause();
14517
16540
  if ((o.volume ?? 0) > 0) instance.setVolume(o.volume);
14518
16541
  if (o.properties && Object.keys(o.properties).length) instance.setProperties(o.properties);
14519
16542
  };
14520
16543
  instance.__applyOptions = applyOptions;
14521
16544
  return instance;
14522
16545
  }
14523
- async function mount(canvas, options) {
14524
- const instance = createScene(canvas, options);
16546
+ async function mount(el, options) {
16547
+ const instance = createScene(el, options);
14525
16548
  const withApply = instance;
14526
16549
  await withApply.__applyOptions(options);
14527
16550
  return instance;
14528
16551
  }
14529
16552
  exports2.bytesSource = bytesSource;
16553
+ exports2.createMediaSource = createMediaSource;
14530
16554
  exports2.createScene = createScene;
14531
16555
  exports2.fileSource = fileSource;
14532
16556
  exports2.httpSource = httpSource;
16557
+ exports2.mediaColor = mediaColor;
16558
+ exports2.mediaSource = mediaSource;
14533
16559
  exports2.mount = mount;
16560
+ exports2.sniffMediaType = sniffMediaType;
14534
16561
  Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
14535
16562
  });
14536
16563
  //# sourceMappingURL=webwallgl.global.js.map