webwallgl 1.0.0-beta1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/webwallgl.mjs CHANGED
@@ -29,7 +29,20 @@ function createRuntime(opts) {
29
29
  window.__blockContextMenu = (doc) => doc.addEventListener("contextmenu", block, true);
30
30
  })();
31
31
  function clear(rt) {
32
- if (rt.wrap) rt.wrap.innerHTML = "";
32
+ if (rt.iframe) {
33
+ try {
34
+ rt.iframe.contentWindow?.location.replace("about:blank");
35
+ } catch {
36
+ }
37
+ }
38
+ if (rt.wrap) {
39
+ rt.wrap.innerHTML = "";
40
+ } else if (rt.iframe?.isConnected) {
41
+ try {
42
+ rt.iframe.remove();
43
+ } catch {
44
+ }
45
+ }
33
46
  if (rt.raf !== void 0) cancelAnimationFrame(rt.raf);
34
47
  rt.raf = void 0;
35
48
  for (const p of rt.videoPairs ?? []) p.destroy();
@@ -37,6 +50,7 @@ function clear(rt) {
37
50
  if (rt.sceneCleanup) rt.sceneCleanup();
38
51
  rt.sceneCleanup = void 0;
39
52
  rt.sceneCtl = void 0;
53
+ rt.pointerCtl = void 0;
40
54
  if (rt.renderer) {
41
55
  rt.renderer.dispose?.();
42
56
  rt.renderer = void 0;
@@ -120,7 +134,7 @@ function reportDiag(rt, cfg, msg) {
120
134
  } catch {
121
135
  }
122
136
  try {
123
- const origin = cfg.mediaBase ? new URL(cfg.mediaBase).origin : "";
137
+ const origin = cfg.mediaBase ? new URL(cfg.mediaBase, window.location.href).origin : "";
124
138
  if (origin) {
125
139
  const img = new Image();
126
140
  img.src = `${origin}/diag?msg=${encodeURIComponent(`scene ${cfg.src ?? "?"}: ${msg.slice(0, 500)}`)}`;
@@ -863,6 +877,31 @@ function parseNum(v, dflt) {
863
877
  }
864
878
  return dflt;
865
879
  }
880
+ function isRenderInert(o) {
881
+ if (!o) return false;
882
+ return !o.image && !o.model && !o.particle && o.text == null && !o.size;
883
+ }
884
+ function composeChildTransform(parentWorld, childLocal, parentScalePropagates) {
885
+ const pscale = parentScalePropagates ? parentWorld.scale : [1, 1, 1];
886
+ const ca = (parentWorld.angles[2] || 0) * Math.PI / 180;
887
+ const cos = Math.cos(ca);
888
+ const sin = Math.sin(ca);
889
+ const ox = childLocal.origin[0] * pscale[0];
890
+ const oy = childLocal.origin[1] * pscale[1];
891
+ return {
892
+ origin: [
893
+ parentWorld.origin[0] + ox * cos - oy * sin,
894
+ parentWorld.origin[1] + ox * sin + oy * cos,
895
+ parentWorld.origin[2] + (childLocal.origin[2] || 0)
896
+ ],
897
+ scale: [
898
+ pscale[0] * childLocal.scale[0],
899
+ pscale[1] * childLocal.scale[1],
900
+ pscale[2] * childLocal.scale[2]
901
+ ],
902
+ angles: [childLocal.angles[0], childLocal.angles[1], (parentWorld.angles[2] || 0) + childLocal.angles[2]]
903
+ };
904
+ }
866
905
  function parseScene(sceneJson, project) {
867
906
  const properties = project && project.general && project.general.properties || {};
868
907
  const objects = sceneJson.objects || [];
@@ -879,6 +918,11 @@ function parseScene(sceneJson, project) {
879
918
  scale: parseVec3(o.scale || "1 1 1"),
880
919
  angles: parseVec3(o.angles || "0 0 0")
881
920
  }));
921
+ const localSnapshot = local.map((c) => ({
922
+ origin: c.origin.slice(),
923
+ scale: c.scale.slice(),
924
+ angles: c.angles.slice()
925
+ }));
882
926
  for (let pass = 0; pass < 8; pass++) {
883
927
  let changed = false;
884
928
  for (const c of local) {
@@ -892,20 +936,11 @@ function parseScene(sceneJson, project) {
892
936
  const pr = objects[pIdx];
893
937
  const prs = pr.scale;
894
938
  const runtimeBound = prs !== null && typeof prs === "object" && (typeof prs.script === "string" || prs.user !== void 0);
895
- const renderInert = !pr.image && !pr.model && !pr.particle && pr.text == null && !pr.size;
896
- const pscale = runtimeBound && renderInert ? [1, 1, 1] : pc.scale;
897
- const ca = pc.angles[2] * Math.PI / 180;
898
- const cos = Math.cos(ca);
899
- const sin = Math.sin(ca);
900
- const ox = c.origin[0] * pscale[0];
901
- const oy = c.origin[1] * pscale[1];
902
- c.origin[0] = pc.origin[0] + ox * cos - oy * sin;
903
- c.origin[1] = pc.origin[1] + ox * sin + oy * cos;
904
- c.origin[2] = pc.origin[2] + c.origin[2];
905
- c.angles[2] = pc.angles[2] + c.angles[2];
906
- c.scale[0] = pscale[0] * c.scale[0];
907
- c.scale[1] = pscale[1] * c.scale[1];
908
- c.scale[2] = pscale[2] * c.scale[2];
939
+ const propagateScale = !(runtimeBound && isRenderInert(pr));
940
+ const w = composeChildTransform(pc, c, propagateScale);
941
+ c.origin = w.origin;
942
+ c.scale = w.scale;
943
+ c.angles = w.angles;
909
944
  c.parent = null;
910
945
  changed = true;
911
946
  }
@@ -995,6 +1030,7 @@ function parseScene(sceneJson, project) {
995
1030
  // anchor 是盒子相对 origin 的锚点(同 image alignment 枚举,外加 "none")。
996
1031
  // 本机 563 个文字层:none 237 / 缺省 301 / center 20 —— none 与缺省都按 center 处理
997
1032
  // (WE 对象缺省对齐就是 center;显式 center 的挂件与时钟层行为一致)。
1033
+ // 2780710296 实验过默认改 top:竖直阶梯对了,但会平移其它壁纸文字相对图元的位置,已回滚。
998
1034
  textAnchor: typeof o.anchor === "string" && o.anchor !== "none" ? o.anchor : "center",
999
1035
  textMaxwidth: parseNum(o.maxwidth, 0),
1000
1036
  textMaxrows: parseNum(o.maxrows, 0),
@@ -1124,6 +1160,18 @@ function parseScene(sceneJson, project) {
1124
1160
  origin: layerOrigin,
1125
1161
  scale: world.scale,
1126
1162
  angles: world.angles,
1163
+ // [we-scene patch] 父级相对变换(WE 场景图的真实语义)。origin/scale/angles
1164
+ // 上的脚本与关键帧动画一律在这层空间收发,再由 recomposeWorld 合成回上面的
1165
+ // world 三件套。渲染 / hittest / getTransformMatrix 仍只读 world,不受影响。
1166
+ // isPostProcess 层的 world 被强制成整幅画布,local 对它无意义(recompose 跳过)。
1167
+ localOrigin: localSnapshot[i].origin,
1168
+ localScale: localSnapshot[i].scale,
1169
+ localAngles: localSnapshot[i].angles,
1170
+ // 「渲染惰性纯容器」:父 scale 是否传给子层由父级这个标志决定,
1171
+ // 判据与 parse 合并阶段逐字相同(见 isRenderInert)。
1172
+ renderInert: isRenderInert(o),
1173
+ // 父 scale 绑了脚本/用户属性(运行时可变)。与 renderInert 一起决定传播闸门。
1174
+ scaleRuntimeBound: !!(o.scale !== null && typeof o.scale === "object" && (typeof o.scale.script === "string" || o.scale.user !== void 0)),
1127
1175
  size: layerSize,
1128
1176
  alignment: o.alignment || "center",
1129
1177
  color: parseColor(o.color),
@@ -1234,15 +1282,110 @@ function resolveMaterial(modelJson) {
1234
1282
  cropoffset: modelJson.cropoffset ? parseVec2(modelJson.cropoffset) : null
1235
1283
  };
1236
1284
  }
1285
+ function recomposeWorld(layers, dirty) {
1286
+ if (!layers || layers.length === 0) return;
1287
+ const byId = /* @__PURE__ */ new Map();
1288
+ for (const l of layers) {
1289
+ if (l && l.id !== void 0 && l.id !== null) byId.set(l.id, l);
1290
+ }
1291
+ const depthOf = (l) => {
1292
+ let d = 0;
1293
+ let p = l.parentId;
1294
+ for (let guard = 0; p !== void 0 && p !== null && guard < 64; guard++) {
1295
+ const parent = byId.get(p);
1296
+ if (!parent) break;
1297
+ d++;
1298
+ p = parent.parentId;
1299
+ }
1300
+ return d;
1301
+ };
1302
+ const targets = [];
1303
+ for (const l of layers) {
1304
+ if (!l || !l.localOrigin) continue;
1305
+ if (l.isPostProcess) continue;
1306
+ if (dirty && !dirty.has(l.id)) continue;
1307
+ targets.push(l);
1308
+ }
1309
+ targets.sort((a, b) => depthOf(a) - depthOf(b));
1310
+ for (const l of targets) {
1311
+ const parent = l.parentId !== void 0 && l.parentId !== null ? byId.get(l.parentId) : null;
1312
+ let w;
1313
+ if (!parent) {
1314
+ w = { origin: l.localOrigin.slice(), scale: l.localScale.slice(), angles: l.localAngles.slice() };
1315
+ } else {
1316
+ const propagateScale = !(parent.scaleRuntimeBound && parent.renderInert);
1317
+ w = composeChildTransform(
1318
+ { origin: parent.origin, scale: parent.scale, angles: parent.angles },
1319
+ { origin: l.localOrigin, scale: l.localScale, angles: l.localAngles },
1320
+ propagateScale
1321
+ );
1322
+ }
1323
+ const d = l.attachBindDelta;
1324
+ if (d) {
1325
+ w.origin[0] += d[0];
1326
+ w.origin[1] += d[1];
1327
+ }
1328
+ l.origin[0] = w.origin[0];
1329
+ l.origin[1] = w.origin[1];
1330
+ l.origin[2] = w.origin[2];
1331
+ l.scale[0] = w.scale[0];
1332
+ l.scale[1] = w.scale[1];
1333
+ l.scale[2] = w.scale[2];
1334
+ l.angles[0] = w.angles[0];
1335
+ l.angles[1] = w.angles[1];
1336
+ l.angles[2] = w.angles[2];
1337
+ if (l.attachBase) {
1338
+ l.attachBase[0] = w.origin[0];
1339
+ l.attachBase[1] = w.origin[1];
1340
+ }
1341
+ }
1342
+ }
1343
+ function collectTransformDirty(layers, extraSeeds) {
1344
+ const dirty = /* @__PURE__ */ new Set();
1345
+ if (!layers || layers.length === 0) return dirty;
1346
+ const childrenOf = /* @__PURE__ */ new Map();
1347
+ for (const l of layers) {
1348
+ if (!l || l.parentId === void 0 || l.parentId === null) continue;
1349
+ const list = childrenOf.get(l.parentId);
1350
+ if (list) list.push(l);
1351
+ else childrenOf.set(l.parentId, [l]);
1352
+ }
1353
+ const TRANSFORM_FIELDS = ["origin", "scale", "angles"];
1354
+ const seeds = [];
1355
+ for (const l of layers) {
1356
+ if (!l || l.id === void 0 || l.id === null) continue;
1357
+ const scripts = l.objectScripts || null;
1358
+ const anims = l.objectAnimations || null;
1359
+ const bound = TRANSFORM_FIELDS.some((f) => scripts && scripts[f] || anims && anims[f]);
1360
+ if (bound) seeds.push(l);
1361
+ }
1362
+ if (extraSeeds) {
1363
+ for (const l of extraSeeds) if (l && l.id !== void 0 && l.id !== null) seeds.push(l);
1364
+ }
1365
+ const stack = seeds.slice();
1366
+ for (let guard = 0; stack.length > 0 && guard < 1e5; guard++) {
1367
+ const l = stack.pop();
1368
+ if (!l || l.id === void 0 || l.id === null) continue;
1369
+ if (dirty.has(l.id)) continue;
1370
+ dirty.add(l.id);
1371
+ const kids = childrenOf.get(l.id);
1372
+ if (kids) for (const c of kids) stack.push(c);
1373
+ }
1374
+ return dirty;
1375
+ }
1237
1376
  const sceneMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1238
1377
  __proto__: null,
1239
1378
  applySolidFromModel,
1379
+ collectTransformDirty,
1380
+ composeChildTransform,
1381
+ isRenderInert,
1240
1382
  parseBool,
1241
1383
  parseColor,
1242
1384
  parseNum,
1243
1385
  parseScene,
1244
1386
  parseVec2,
1245
1387
  parseVec3,
1388
+ recomposeWorld,
1246
1389
  recomputeLayerVisibility,
1247
1390
  resolveMaterial
1248
1391
  }, Symbol.toStringTag, { value: "Module" }));
@@ -1646,12 +1789,36 @@ function expandMacrosIn(text, depth) {
1646
1789
  const { defs, fns } = collectMacros(text);
1647
1790
  if (defs.size === 0 && fns.size === 0) break;
1648
1791
  const lines = text.split("\n");
1792
+ const defLine = /* @__PURE__ */ new Map();
1793
+ for (let i = 0; i < lines.length; i++) {
1794
+ const dm = /^[ \t]*#define[ \t]+([A-Za-z_][A-Za-z0-9_]*)/.exec(lines[i]);
1795
+ if (dm && !defLine.has(dm[1])) defLine.set(dm[1], i);
1796
+ }
1797
+ const declLine = /* @__PURE__ */ new Map();
1798
+ {
1799
+ const TYPES = "(?:float|int|bool|vec[234]|ivec[234]|bvec[234]|mat[234])";
1800
+ for (const name of defs.keys()) {
1801
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1802
+ const re = new RegExp(
1803
+ "^\\s*(?:const\\s+|uniform\\s+|varying\\s+|in\\s+|out\\s+|attribute\\s+)*" + TYPES + "\\s+" + esc + "\\s*[=;]"
1804
+ );
1805
+ for (let i = 0; i < lines.length; i++) {
1806
+ if (re.test(lines[i])) {
1807
+ declLine.set(name, i);
1808
+ break;
1809
+ }
1810
+ }
1811
+ }
1812
+ }
1649
1813
  let changed = false;
1650
1814
  for (let i = 0; i < lines.length; i++) {
1651
1815
  const line = lines[i];
1652
1816
  if (/^[ \t]*#/.test(line)) continue;
1653
1817
  let l = line;
1654
1818
  for (const [name, val] of defs) {
1819
+ const dl = defLine.get(name);
1820
+ if (dl !== void 0 && i < dl) continue;
1821
+ if (declLine.get(name) === i) continue;
1655
1822
  const re = new RegExp("\\b" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
1656
1823
  if (re.test(l)) {
1657
1824
  l = replaceWord(l, name, val);
@@ -1659,6 +1826,8 @@ function expandMacrosIn(text, depth) {
1659
1826
  }
1660
1827
  }
1661
1828
  for (const [name, info] of fns) {
1829
+ const dl = defLine.get(name);
1830
+ if (dl !== void 0 && i < dl) continue;
1662
1831
  if (l.includes(name)) {
1663
1832
  l = expandFunctionMacro(l, name, info, depth);
1664
1833
  changed = true;
@@ -1933,6 +2102,13 @@ function isDeclaration(text, idx) {
1933
2102
  const word = text.slice(p + 1, e);
1934
2103
  return GLSL_TYPES.has(word);
1935
2104
  }
2105
+ function collectIntNames(code) {
2106
+ const names = /* @__PURE__ */ new Set();
2107
+ let m;
2108
+ const declRe = /\b(?:const\s+)?int\s+([A-Za-z_]\w*)\s*[=;)\u0003]/g;
2109
+ while ((m = declRe.exec(code)) !== null) names.add(m[1]);
2110
+ return names;
2111
+ }
1936
2112
  function rewriteCall(text, callName, fn) {
1937
2113
  let out = "";
1938
2114
  let i = 0;
@@ -2017,23 +2193,73 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2017
2193
  const dim = sw.length;
2018
2194
  return fn + "(vec" + dim + "(" + num2 + "), " + expr + ")";
2019
2195
  });
2020
- code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
2021
- code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2022
- code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2023
- code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2024
- code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2025
- 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");
2026
- code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2027
- 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");
2028
- {
2029
- const floatNames = /* @__PURE__ */ new Set();
2030
- 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;
2031
- let dm;
2032
- while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
2033
- if (floatNames.size > 0) {
2034
- const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
2035
- code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
2196
+ code = code.replace(
2197
+ /(\.([xyzwrgba]{2,4})\s*=\s*)(max|min)\(\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*,\s*([^;]+?)\s*\)\s*;/g,
2198
+ (all, lead, sw, fn, scalar, vecExpr) => {
2199
+ if (!/\.[xyzwrgba]{2,4}\b|\bvec[234]\s*\(/.test(vecExpr)) return all;
2200
+ return `${lead}${fn}(vec${sw.length}(${scalar}), ${vecExpr});`;
2201
+ }
2202
+ );
2203
+ code = code.replace(
2204
+ /(^|[^\w)\]])([+-])([+-])(?=[\d.])/g,
2205
+ (all, pre, s1, s2) => pre + (s1 === s2 ? "+" : "-")
2206
+ );
2207
+ const sciHoles = [];
2208
+ code = code.replace(/\b\d+(?:\.\d+)?[eE][+-]?\d+\b/g, (m) => {
2209
+ sciHoles.push(m);
2210
+ return "" + "".repeat(sciHoles.length) + "";
2211
+ });
2212
+ const forHoles = [];
2213
+ code = code.replace(/\bfor\s*\(\s*int\s+([A-Za-z_]\w*)([^)]*)\)/g, (m, name, rest) => {
2214
+ forHoles.push(rest + ")");
2215
+ return `for (int ${name}${"".repeat(forHoles.length)}`;
2216
+ });
2217
+ for (let pass = 0; pass < 8; pass++) {
2218
+ const before = code;
2219
+ code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
2220
+ code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2221
+ code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2222
+ code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2223
+ code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2224
+ 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");
2225
+ code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2226
+ 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");
2227
+ code = code.replace(
2228
+ /(^|[^\w.])(\d+)\s*([*/+-])\s*(\()/g,
2229
+ (all, pre, num2, op, open, offset, whole) => {
2230
+ let depth = 0;
2231
+ let end = offset + all.length - 1;
2232
+ for (; end < whole.length; end++) {
2233
+ const ch = whole[end];
2234
+ if (ch === "(") depth++;
2235
+ else if (ch === ")") {
2236
+ depth--;
2237
+ if (depth === 0) {
2238
+ end++;
2239
+ break;
2240
+ }
2241
+ } else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
2242
+ }
2243
+ for (; end < whole.length; end++) {
2244
+ const ch = whole[end];
2245
+ if (ch === ";" || ch === "," || ch === "\n" || ch === ")") break;
2246
+ }
2247
+ const seg = whole.slice(offset, end);
2248
+ return /\d\.\d/.test(seg) ? `${pre}${num2}.0 ${op} ${open}` : all;
2249
+ }
2250
+ );
2251
+ {
2252
+ const floatNames = /* @__PURE__ */ new Set();
2253
+ 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;
2254
+ let dm;
2255
+ while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
2256
+ if (floatNames.size > 0) {
2257
+ const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
2258
+ code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
2259
+ code = code.replace(new RegExp("\\b(" + alt + ")\\s*([+-])\\s*(\\d+)(?![\\d.])", "g"), "$1 $2 $3.0");
2260
+ }
2036
2261
  }
2262
+ if (code === before) break;
2037
2263
  }
2038
2264
  {
2039
2265
  const FLOAT_BUILTINS = [
@@ -2169,6 +2395,67 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2169
2395
  return pre + lhs + " = " + rhs + "." + SW[lw] + ";";
2170
2396
  });
2171
2397
  }
2398
+ {
2399
+ const floatDecl = /* @__PURE__ */ new Set();
2400
+ {
2401
+ const fdre = /\b(?:uniform|varying|attribute|in|out|const)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
2402
+ let fd;
2403
+ while ((fd = fdre.exec(code)) !== null) floatDecl.add(fd[1]);
2404
+ }
2405
+ const vecW = (expr) => {
2406
+ const e = expr.trim();
2407
+ {
2408
+ const c = /^vec([234])\s*\(/.exec(e);
2409
+ if (c) {
2410
+ let depth = 0;
2411
+ for (let i = e.indexOf("("); i < e.length; i++) {
2412
+ if (e[i] === "(") depth++;
2413
+ else if (e[i] === ")") {
2414
+ depth--;
2415
+ if (depth === 0) return i === e.length - 1 ? Number(c[1]) : 0;
2416
+ }
2417
+ }
2418
+ return 0;
2419
+ }
2420
+ }
2421
+ if (/^texture(?:Lod)?\s*\(/.test(e)) {
2422
+ let depth = 0;
2423
+ for (let i = e.indexOf("("); i < e.length; i++) {
2424
+ if (e[i] === "(") depth++;
2425
+ else if (e[i] === ")") {
2426
+ depth--;
2427
+ if (depth === 0) return i === e.length - 1 ? 4 : 0;
2428
+ }
2429
+ }
2430
+ return 0;
2431
+ }
2432
+ const m = /^([A-Za-z_]\w*)(?:\.([xyzwrgba]{2,4}))?\s*[*/]\s*([^*/]+)$/.exec(e);
2433
+ if (!m) return 0;
2434
+ const rhsPart = m[3];
2435
+ if (/\bvec[234]\s*\(|\.[xyzwrgba]{2,4}\b/.test(rhsPart)) return 0;
2436
+ if (m[2]) return m[2].length;
2437
+ if (floatDecl.has(m[1])) return 0;
2438
+ return width.get(m[1]) || 0;
2439
+ };
2440
+ code = code.replace(
2441
+ /(^|[;{}\n]\s*)float\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
2442
+ (all, pre, name, rhs) => {
2443
+ const w = vecW(rhs);
2444
+ if (w < 2) return all;
2445
+ return `${pre}float ${name} = (${rhs.trim()}).x;`;
2446
+ }
2447
+ );
2448
+ const SWN = { 2: "xy", 3: "xyz" };
2449
+ code = code.replace(
2450
+ /(^|[;{}\n]\s*)vec([23])\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
2451
+ (all, pre, dim, name, rhs) => {
2452
+ const lw = Number(dim);
2453
+ const rw = vecW(rhs);
2454
+ if (rw <= lw) return all;
2455
+ return `${pre}vec${dim} ${name} = (${rhs.trim()}).${SWN[lw]};`;
2456
+ }
2457
+ );
2458
+ }
2172
2459
  if (width.size > 0) {
2173
2460
  const floatNames = /* @__PURE__ */ new Set();
2174
2461
  const fre = /\b(?:uniform|varying|attribute|in|out)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
@@ -2210,6 +2497,7 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2210
2497
  code = code.replace(/(^|[;{}\n]\s*)([A-Za-z_]\w*)\s*=\s*([^;]+);/g, (all, pre, lhs, rhs) => {
2211
2498
  const lw = width.get(lhs);
2212
2499
  if (!lw) return all;
2500
+ if (floatNames.has(lhs)) return all;
2213
2501
  const r = rhs.trim();
2214
2502
  if (new RegExp("^vec" + lw + "\\s*\\(").test(r)) return all;
2215
2503
  if (width.has(r)) return all;
@@ -2253,6 +2541,52 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2253
2541
  new RegExp(`\\bint\\s+([A-Za-z_]\\w*)\\s*=\\s*((?:${FLOAT_FNS})\\s*\\()`, "g"),
2254
2542
  "float $1 = $2"
2255
2543
  );
2544
+ code = code.replace(
2545
+ /\bfloat\s+([A-Za-z_]\w*)\s*=\s*(int\s*\([^;]*\))\s*;/g,
2546
+ "float $1 = float($2);"
2547
+ );
2548
+ {
2549
+ const intNames = collectIntNames(code);
2550
+ if (intNames.size > 0) {
2551
+ code = code.replace(
2552
+ /\b(const\s+)?float\s+([A-Za-z_]\w*)\s*=\s*([^;{}]+);/g,
2553
+ (all, cst, name, rhs) => {
2554
+ const body = rhs.trim();
2555
+ if (/\./.test(body)) return all;
2556
+ if (/[A-Za-z_]\w*\s*\(/.test(body)) return all;
2557
+ const ids = body.match(/[A-Za-z_]\w*/g);
2558
+ if (!ids || !ids.length) return all;
2559
+ if (!ids.every((x) => intNames.has(x))) return all;
2560
+ return `${cst || ""}float ${name} = float(${body});`;
2561
+ }
2562
+ );
2563
+ }
2564
+ }
2565
+ {
2566
+ const intNames = collectIntNames(code);
2567
+ const floatNames = /* @__PURE__ */ new Set();
2568
+ let fm;
2569
+ const fDeclRe = /\b(?:const\s+|uniform\s+|varying\s+|in\s+|out\s+)*float\s+([A-Za-z_]\w*)/g;
2570
+ while ((fm = fDeclRe.exec(code)) !== null) floatNames.add(fm[1]);
2571
+ for (const n of [...intNames]) {
2572
+ if (floatNames.has(n)) {
2573
+ intNames.delete(n);
2574
+ floatNames.delete(n);
2575
+ }
2576
+ }
2577
+ if (intNames.size > 0 && floatNames.size > 0) {
2578
+ const iAlt = [...intNames].sort((a, b) => b.length - a.length).join("|");
2579
+ const fAlt = [...floatNames].sort((a, b) => b.length - a.length).join("|");
2580
+ code = code.replace(
2581
+ new RegExp(`\\b(${iAlt})\\s*([*/+-])\\s*(${fAlt})\\b`, "g"),
2582
+ (all, a, op, b) => `float(${a}) ${op} ${b}`
2583
+ );
2584
+ code = code.replace(
2585
+ new RegExp(`\\b(${fAlt})\\s*([*/+-])\\s*(${iAlt})\\b`, "g"),
2586
+ (all, a, op, b) => `${a} ${op} float(${b})`
2587
+ );
2588
+ }
2589
+ }
2256
2590
  {
2257
2591
  const boolNames = /* @__PURE__ */ new Set();
2258
2592
  const boolRe = /\bbool\s+([A-Za-z_]\w*)\s*=/g;
@@ -2266,6 +2600,17 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2266
2600
  );
2267
2601
  }
2268
2602
  }
2603
+ {
2604
+ const CMP = /\(\s*([^()&|]+?)\s*(<=|>=|<|>|==|!=)\s*([^()&|]+?)\s*\)/g;
2605
+ code = code.replace(
2606
+ new RegExp(CMP.source + "\\s*([*/])", "g"),
2607
+ (all, lhs, op, rhs, mulOp) => `float(${lhs.trim()} ${op} ${rhs.trim()}) ${mulOp}`
2608
+ );
2609
+ code = code.replace(
2610
+ new RegExp("([-+*/]=\\s*)" + CMP.source, "g"),
2611
+ (all, assign, lhs, op, rhs) => `${assign}float(${lhs.trim()} ${op} ${rhs.trim()})`
2612
+ );
2613
+ }
2269
2614
  {
2270
2615
  const names = /* @__PURE__ */ new Set();
2271
2616
  for (const fm of code.matchAll(/\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?float\s+([A-Za-z_]\w*)\s*\[/g)) {
@@ -2284,6 +2629,17 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2284
2629
  let p = close1 + 1;
2285
2630
  while (p < code.length && /[ \t]/.test(code[p])) p++;
2286
2631
  if (code[p] !== "[") {
2632
+ const e12 = code.slice(open1 + 1, close1);
2633
+ const t = e12.trim();
2634
+ const isFloatish = /\d\.\d/.test(t) || /^[A-Za-z_]\w*$/.test(t) && new RegExp("\\bfloat\\s+" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b").test(code);
2635
+ const alreadyInt = /^\s*int\s*\(/.test(t) || /^-?\d+$/.test(t);
2636
+ if (isFloatish && !alreadyInt) {
2637
+ out += code.slice(last, fm.index);
2638
+ out += fm[1] + "[int(" + t + ")]";
2639
+ last = close1 + 1;
2640
+ re.lastIndex = last;
2641
+ continue;
2642
+ }
2287
2643
  re.lastIndex = close1 + 1;
2288
2644
  continue;
2289
2645
  }
@@ -2424,6 +2780,59 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2424
2780
  }).join("\n");
2425
2781
  }
2426
2782
  }
2783
+ code = code.replace(
2784
+ /^(\s*in\s+(?:highp|mediump|lowp\s+)?)(vec[234]|float)(\s+)([A-Za-z_]\w*)(\s*;)/gm,
2785
+ (all, pre, ty, sp, name, tail) => {
2786
+ const vt = vertTypes.get(name);
2787
+ if (!vt || RANK[vt] >= RANK[ty]) return all;
2788
+ const CH = "xyzw";
2789
+ const RG = "rgba";
2790
+ const over = new RegExp(
2791
+ "\\b" + name + "\\s*\\.\\s*[" + CH + RG + "]*[" + CH.slice(RANK[vt]) + RG.slice(RANK[vt]) + "]"
2792
+ );
2793
+ if (over.test(code)) return all;
2794
+ return pre + vt + sp + name + tail;
2795
+ }
2796
+ );
2797
+ }
2798
+ {
2799
+ const inVecN = /* @__PURE__ */ new Map();
2800
+ const declRe = /^\s*in\s+(?:highp|mediump|lowp\s+)?(vec[34])\s+([A-Za-z_]\w*)\s*;/gm;
2801
+ let dm;
2802
+ while ((dm = declRe.exec(code)) !== null) inVecN.set(dm[2], Number(dm[1].slice(3)));
2803
+ const localVecN = new Map(inVecN);
2804
+ const locRe = /\b(vec[34])\s+([A-Za-z_]\w*)\s*[=;]/g;
2805
+ while ((dm = locRe.exec(code)) !== null) {
2806
+ if (!localVecN.has(dm[2])) localVecN.set(dm[2], Number(dm[1].slice(3)));
2807
+ }
2808
+ if (localVecN.size > 0) {
2809
+ const swizzleUvArg = (arg) => {
2810
+ const t = arg.trim();
2811
+ if (!t) return arg;
2812
+ const bare = /^([A-Za-z_]\w*)$/.exec(t);
2813
+ if (bare && localVecN.has(bare[1])) return bare[1] + ".xy";
2814
+ const bin = /^([A-Za-z_]\w*)(\s*[+\-].+)$/.exec(t);
2815
+ if (bin && localVecN.has(bin[1])) return "(" + bin[1] + ".xy" + bin[2] + ")";
2816
+ return arg;
2817
+ };
2818
+ for (const fn of ["textureLod", "texture"]) {
2819
+ code = rewriteCall(code, fn, (inner) => {
2820
+ const args = splitArgs(inner);
2821
+ if (args.length >= 2) args[1] = swizzleUvArg(args[1]);
2822
+ return fn + "(" + args.join(", ") + ")";
2823
+ });
2824
+ }
2825
+ }
2826
+ if (inVecN.size > 0) {
2827
+ code = code.split("\n").map((line) => {
2828
+ if (!/\bvec2\s+[A-Za-z_]\w*\s*=/.test(line)) return line;
2829
+ let out = line;
2830
+ for (const name of inVecN.keys()) {
2831
+ out = out.replace(new RegExp("\\b" + name + "\\b(?!\\s*[.\\w])", "g"), name + ".xy");
2832
+ }
2833
+ return out;
2834
+ }).join("\n");
2835
+ }
2427
2836
  }
2428
2837
  const written = /* @__PURE__ */ new Set();
2429
2838
  const inNames = /* @__PURE__ */ new Set();
@@ -2444,18 +2853,27 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2444
2853
  let body = code.slice(braceIdx + 1);
2445
2854
  const decls = [];
2446
2855
  for (const name of written) {
2856
+ const shadowed = new RegExp(
2857
+ "(?:^|[;{}\\n])\\s*(?:highp|mediump|lowp\\s+)?(?:vec[234]|float|int|bool)\\s+" + name + "\\s*[=;]"
2858
+ ).test(body);
2859
+ if (shadowed) continue;
2447
2860
  const tm = new RegExp("^\\s*in\\s+(?:highp|mediump|lowp\\s+)?(vec[234]|float)\\s+" + name + "\\s*;", "m").exec(code);
2448
2861
  const ty = tm ? tm[1] : "vec4";
2449
2862
  decls.push(" " + ty + " " + name + "_rw = " + name + ";");
2450
2863
  body = replaceWord(body, name, name + "_rw");
2451
2864
  }
2452
- body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
2865
+ if (decls.length > 0) {
2866
+ body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
2867
+ }
2453
2868
  code = head + body;
2454
2869
  }
2455
2870
  }
2456
2871
  }
2457
2872
  code = code.replace(/\[(?:unroll|loop|branch|flatten)\]\s*/g, "");
2458
2873
  code = code.replace(/\bstatic\s+/g, "");
2874
+ if (forHoles.length > 0) {
2875
+ code = code.replace(/\u0003(\u0004+)\u0003/g, (m, marks) => forHoles[marks.length - 1]);
2876
+ }
2459
2877
  {
2460
2878
  const floatNames = /* @__PURE__ */ new Set();
2461
2879
  for (const m of code.matchAll(/\b(?:uniform[ \t]+)?(?:highp|mediump|lowp)?[ \t]*float[ \t]+([A-Za-z_]\w*)[ \t]*[;=]/g)) {
@@ -2485,6 +2903,9 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2485
2903
  return line;
2486
2904
  }).join("\n");
2487
2905
  }
2906
+ if (sciHoles.length > 0) {
2907
+ code = code.replace(/\u0001(\u0002+)\u0001/g, (m, marks) => sciHoles[marks.length - 1]);
2908
+ }
2488
2909
  let prologue = "#version 300 es\n";
2489
2910
  if (stage === "vert") {
2490
2911
  prologue += "precision highp float;\n";
@@ -2720,7 +3141,8 @@ function compile(gl, type, src) {
2720
3141
  return s;
2721
3142
  }
2722
3143
  function parseVec3Local(s) {
2723
- const p = String(s).trim().split(/\s+/).map(Number);
3144
+ if (s !== null && typeof s === "object" && "value" in s) s = s.value;
3145
+ const p = String(s ?? "").trim().split(/\s+/).map(Number);
2724
3146
  return [p[0] || 0, p[1] || 0, p[2] || 0];
2725
3147
  }
2726
3148
  function makeTexture(gl, rgba, width, height, bitmap = null) {
@@ -3269,7 +3691,11 @@ function createRenderer(canvas, opts = {}) {
3269
3691
  }
3270
3692
  }
3271
3693
  const key = shaderName + "|" + JSON.stringify(effectiveCombos);
3272
- if (progCache.has(key)) return progCache.get(key);
3694
+ if (progCache.has(key)) {
3695
+ const hit = progCache.get(key);
3696
+ if (hit === null) throw new Error("shader=" + shaderName + " 编译失败(已缓存)");
3697
+ return hit;
3698
+ }
3273
3699
  for (let attempt = 0; attempt < 4; attempt++) {
3274
3700
  const missing = /* @__PURE__ */ new Set();
3275
3701
  const resolver = (file) => {
@@ -3284,6 +3710,7 @@ function createRenderer(canvas, opts = {}) {
3284
3710
  try {
3285
3711
  prog = linkProgram(gl, vertGlsl, fragGlsl);
3286
3712
  } catch (e) {
3713
+ progCache.set(key, null);
3287
3714
  throw new Error("shader=" + shaderName + " " + (e && e.message));
3288
3715
  }
3289
3716
  const uni = /* @__PURE__ */ new Map();
@@ -3294,8 +3721,9 @@ function createRenderer(canvas, opts = {}) {
3294
3721
  uni.set(base, { loc: gl.getUniformLocation(prog, info.name), type: GL_TYPES[info.type] || "unknown", size: info.size });
3295
3722
  }
3296
3723
  const matMeta = { ...parseMaterialMeta(src.vert), ...parseMaterialMeta(src.frag) };
3724
+ 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);
3297
3725
  const samplerDefaults = new Map([...parseSamplerDefaults(src.vert), ...parseSamplerDefaults(src.frag)]);
3298
- const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl };
3726
+ const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl, ndcDirect };
3299
3727
  progCache.set(key, entry);
3300
3728
  return entry;
3301
3729
  }
@@ -4047,7 +4475,13 @@ function createRenderer(canvas, opts = {}) {
4047
4475
  }
4048
4476
  const drawLayers = cam.perspective ? scene.layers.slice().sort((a, b) => Number(!!b.isSkybox) - Number(!!a.isSkybox)) : scene.layers;
4049
4477
  for (const layer of drawLayers) {
4050
- if (!layer.visible || layer.destroyed) continue;
4478
+ if (layer.destroyed) continue;
4479
+ if (!layer.visible) {
4480
+ if (pendingEmptyCompose.has(layer.id)) {
4481
+ captureEmptyComposeAtZOrder(layer, cam, viewProj, width, height);
4482
+ }
4483
+ continue;
4484
+ }
4051
4485
  if (layer.isPostProcess && !(layer.effects || []).some((e) => e.visible)) continue;
4052
4486
  if (groupChildIds.has(layer.id)) continue;
4053
4487
  if (layer.isContainer) {
@@ -4477,7 +4911,10 @@ function createRenderer(canvas, opts = {}) {
4477
4911
  try {
4478
4912
  progEntry = await getEffectProgram(mp.shader, combos, mergedTex);
4479
4913
  } catch (e) {
4480
- console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, e && e.message || e);
4914
+ const msg = e && e.message || String(e);
4915
+ if (!/已缓存/.test(msg)) {
4916
+ console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, msg);
4917
+ }
4481
4918
  failedEffects.add(eff2);
4482
4919
  continue;
4483
4920
  }
@@ -4503,7 +4940,13 @@ function createRenderer(canvas, opts = {}) {
4503
4940
  gl.bindFramebuffer(gl.FRAMEBUFFER, outFBO.fbo);
4504
4941
  gl.viewport(0, 0, outFBO.width, outFBO.height);
4505
4942
  gl.bindVertexArray(vao);
4506
- uploadQuad("pass", PASS_QUAD);
4943
+ const usePixelQuad = !progEntry.ndcDirect;
4944
+ if (usePixelQuad) {
4945
+ uploadQuad("passPx" + outFBO.width + "x" + outFBO.height, layerQuad(outFBO.width, outFBO.height));
4946
+ } else {
4947
+ uploadQuad("pass", PASS_QUAD);
4948
+ }
4949
+ const passMVP = usePixelQuad ? mat4Transpose(mat4Ortho(0, outFBO.width, 0, outFBO.height, -1e4, 1e4)) : IDENT_M4;
4507
4950
  const texNames = mp.textures || [];
4508
4951
  const maxTex = Math.max(texNames.length, 8);
4509
4952
  const resolutions = /* @__PURE__ */ new Map();
@@ -4532,7 +4975,7 @@ function createRenderer(canvas, opts = {}) {
4532
4975
  usedUnits.add(ti);
4533
4976
  resolutions.set(ti, [t.width, t.height, t.width, t.height]);
4534
4977
  }
4535
- bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, IDENT_M4, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
4978
+ bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, passMVP, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
4536
4979
  bindConstants(
4537
4980
  uni,
4538
4981
  animatedConstants(
@@ -4827,6 +5270,7 @@ layout(location=1) in vec3 a_pos; // 实例中心(投影空间世界
4827
5270
  layout(location=2) in vec2 a_sizeRot; // x=size(像素) y=rot(弧度)
4828
5271
  layout(location=3) in vec4 a_color; // rgb + alpha
4829
5272
  layout(location=4) in vec3 a_stretchFrame; // xy=非等比拉伸 z=帧序号
5273
+ layout(location=5) in vec2 a_vrange; // 段两端沿贴图 v 的取值(rope 连线用;普通精灵 0..1)
4830
5274
  uniform mat4 u_mvp;
4831
5275
  // 序列帧 uv 变换表(TEXS 帧矩形归一化后的 offset/scale),最多 128 帧
4832
5276
  // (matrix spritesheet 72 有 71 帧,旧上限 64 会丢末尾字符)
@@ -4844,7 +5288,8 @@ void main(){
4844
5288
  gl_Position = u_mvp * vec4(a_pos.xy + rotated, a_pos.z, 1.0);
4845
5289
  // quad 角 → 贴图 uv。世界 y 已翻转到投影空间(y 向下),故 quad 的 +y 角
4846
5290
  // 对应屏幕上方,应采样纹理顶行 v=1(与 renderer.js 的 layerQuadVerts 同约定)。
4847
- vec2 uv = a_corner + 0.5;
5291
+ // a_vrange rope 段两端各取自己的 v(沿绳连续渐变);普通精灵是 (0,1) 恒等。
5292
+ vec2 uv = vec2(a_corner.x + 0.5, mix(a_vrange.x, a_vrange.y, a_corner.y + 0.5));
4848
5293
  if (u_frameCount > 0) {
4849
5294
  // 帧矩形以左上为原点(TEXS 是 top-down 像素坐标),故先把 v 翻成 top-down
4850
5295
  int fi = int(a_stretchFrame.z);
@@ -4918,7 +5363,7 @@ void main(){
4918
5363
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
4919
5364
  gl.vertexAttribDivisor(0, 0);
4920
5365
  gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
4921
- const S = 48;
5366
+ const S = 56;
4922
5367
  gl.enableVertexAttribArray(1);
4923
5368
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, S, 0);
4924
5369
  gl.vertexAttribDivisor(1, 1);
@@ -4931,6 +5376,9 @@ void main(){
4931
5376
  gl.enableVertexAttribArray(4);
4932
5377
  gl.vertexAttribPointer(4, 3, gl.FLOAT, false, S, 36);
4933
5378
  gl.vertexAttribDivisor(4, 1);
5379
+ gl.enableVertexAttribArray(5);
5380
+ gl.vertexAttribPointer(5, 2, gl.FLOAT, false, S, 48);
5381
+ gl.vertexAttribDivisor(5, 1);
4934
5382
  gl.bindVertexArray(null);
4935
5383
  return {
4936
5384
  prog: {
@@ -4982,6 +5430,20 @@ function spriteTrailRotation(dx, dy) {
4982
5430
  function particleInstanceSegs(trailCfg, trailSegments) {
4983
5431
  return trailCfg && trailCfg.kind === "ropetrail" ? Math.max(1, trailSegments || 1) : 1;
4984
5432
  }
5433
+ function ropeTrailHistoryCount(cfg) {
5434
+ if (!cfg || cfg.kind !== "ropetrail") return 1;
5435
+ const segs = Math.round(Number(cfg.segments) || 0);
5436
+ if (segs >= 2) return Math.min(32, segs);
5437
+ return 8;
5438
+ }
5439
+ function ropeTrailDuration(cfg) {
5440
+ if (!cfg || cfg.kind !== "ropetrail") return 0;
5441
+ const L = Number(cfg.length);
5442
+ return Number.isFinite(L) && L > 0 ? L : 0.2;
5443
+ }
5444
+ function ropeParticleV(p) {
5445
+ return p.life > 0 ? p.age / p.life : 0;
5446
+ }
4985
5447
  class Particle {
4986
5448
  constructor() {
4987
5449
  this.alive = false;
@@ -5010,6 +5472,8 @@ class Particle {
5010
5472
  this.turbSpeed = 0;
5011
5473
  this.turbPhase = 0;
5012
5474
  this.trail = null;
5475
+ this.trailClock = 0;
5476
+ this.seq = 0;
5013
5477
  }
5014
5478
  }
5015
5479
  class ParticleSystem {
@@ -5018,20 +5482,7 @@ class ParticleSystem {
5018
5482
  this.model = model || {};
5019
5483
  this.override = override || {};
5020
5484
  this.layer = layer || null;
5021
- const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
5022
- const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
5023
- const la = layer && layer.angles ? layer.angles : [0, 0, 0];
5024
- this.originX = lo[0] || 0;
5025
- this.originY = lo[1] || 0;
5026
- this.originZ = lo[2] || 0;
5027
- this.scaleX = ls[0] === 0 ? 1 : ls[0];
5028
- this.scaleY = ls[1] === 0 ? 1 : ls[1];
5029
- this.angleZ = (la[2] || 0) * Math.PI / 180;
5030
- const asx = Math.abs(this.scaleX);
5031
- const asy = Math.abs(this.scaleY);
5032
- this.sysScale = Math.min(asx, asy) || 1;
5033
- this.spriteStretchX = asx / this.sysScale;
5034
- this.spriteStretchY = asy / this.sysScale;
5485
+ this.syncLayerTransform();
5035
5486
  this.maxCount = Math.max(1, Math.min(2e4, num(this.model.maxcount, 100)));
5036
5487
  this.simTime = 0;
5037
5488
  this.paused = false;
@@ -5070,6 +5521,9 @@ class ParticleSystem {
5070
5521
  this._followParent = null;
5071
5522
  this._followMode = null;
5072
5523
  this._followOffset = [0, 0, 0];
5524
+ this.ropeRenderer = null;
5525
+ this._seq = 0;
5526
+ this._ropeOrder = [];
5073
5527
  this._ov = {};
5074
5528
  this._applyOverride();
5075
5529
  this.startTime = Math.max(0, Math.min(30, num(this.model.starttime, 0)));
@@ -5346,24 +5800,34 @@ class ParticleSystem {
5346
5800
  const kind = r && r.name || "sprite";
5347
5801
  return {
5348
5802
  kind,
5349
- length: num(r && r.length, kind === "spritetrail" ? 0.1 : 0),
5803
+ length: num(r && r.length, kind === "spritetrail" ? 0.1 : kind === "ropetrail" ? 0.2 : 0),
5350
5804
  maxLength: num(r && r.maxlength, 0),
5351
5805
  minLength: num(r && r.minlength, 0),
5352
5806
  subdivision: num(r && r.subdivision, 1),
5807
+ // Rope Trail 段数(官方 `segments`);与 spritetrail 的 maxlength 无关
5808
+ segments: num(r && r.segments, 0),
5353
5809
  orientation: r && r.orientation || null
5354
5810
  };
5355
5811
  });
5356
5812
  if (omitted && !this.renderers.length) {
5357
- this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, orientation: null }];
5813
+ this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, segments: 0, orientation: null }];
5358
5814
  }
5359
5815
  const tr = this.renderers.find((r) => r.kind === "spritetrail" || r.kind === "ropetrail");
5360
5816
  this.trailCfg = tr || null;
5361
5817
  this.trailSegments = 1;
5818
+ this.trailDuration = 0;
5819
+ this.trailSampleDt = 0;
5362
5820
  if (tr && tr.kind === "ropetrail") {
5363
- const segs = Math.max(2, Math.min(16, Math.round(tr.maxLength || tr.subdivision || 6)));
5821
+ const segs = ropeTrailHistoryCount(tr);
5364
5822
  this.trailSegments = segs;
5365
- for (const p of this.pool) p.trail = new Float32Array(segs * 3);
5823
+ this.trailDuration = ropeTrailDuration(tr);
5824
+ this.trailSampleDt = this.trailDuration / Math.max(1, segs - 1);
5825
+ for (const p of this.pool) {
5826
+ p.trail = new Float32Array(segs * 3);
5827
+ p.trailClock = 0;
5828
+ }
5366
5829
  }
5830
+ this.ropeRenderer = this.renderers.find((r) => r.kind === "rope") || null;
5367
5831
  }
5368
5832
  setModel(model) {
5369
5833
  this.model = model;
@@ -5423,6 +5887,31 @@ class ParticleSystem {
5423
5887
  setVisible(v) {
5424
5888
  this.visible = v;
5425
5889
  }
5890
+ /**
5891
+ * [we-scene patch] 从图层重新读取变换(构造时也走这里)。
5892
+ *
5893
+ * 发射器变换原先只在构造时缓存一次、之后**从不刷新**。父组一旦带脚本/动画
5894
+ * 变换(全库 17 个粒子层有脚本化祖先),图层被 recomposeWorld 挪走了,
5895
+ * 粒子却仍从旧位置喷出来 —— 画面上是「人物滑走了、他的火焰留在原地」。
5896
+ * 宿主在 recompose 之后对脏子树里的粒子层调用本方法。
5897
+ */
5898
+ syncLayerTransform() {
5899
+ const layer = this.layer;
5900
+ const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
5901
+ const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
5902
+ const la = layer && layer.angles ? layer.angles : [0, 0, 0];
5903
+ this.originX = lo[0] || 0;
5904
+ this.originY = lo[1] || 0;
5905
+ this.originZ = lo[2] || 0;
5906
+ this.scaleX = ls[0] === 0 ? 1 : ls[0];
5907
+ this.scaleY = ls[1] === 0 ? 1 : ls[1];
5908
+ this.angleZ = (la[2] || 0) * Math.PI / 180;
5909
+ const asx = Math.abs(this.scaleX);
5910
+ const asy = Math.abs(this.scaleY);
5911
+ this.sysScale = Math.min(asx, asy) || 1;
5912
+ this.spriteStretchX = asx / this.sysScale;
5913
+ this.spriteStretchY = asy / this.sysScale;
5914
+ }
5426
5915
  // 宿主每帧提供鼠标位置(世界像素);转到局部空间供控制点使用
5427
5916
  setPointer(worldX, worldY) {
5428
5917
  const dx = worldX - this.originX;
@@ -5494,6 +5983,7 @@ class ParticleSystem {
5494
5983
  p.rotVel = 0;
5495
5984
  p.vx = p.vy = p.vz = 0;
5496
5985
  p.frame = 0;
5986
+ p.seq = this._seq++;
5497
5987
  const o = em.origin;
5498
5988
  if (em.kind === "box") {
5499
5989
  const d = em.distanceMax || [0, 0, 0];
@@ -5640,6 +6130,7 @@ class ParticleSystem {
5640
6130
  p.trail[i + 1] = p.y;
5641
6131
  p.trail[i + 2] = p.z;
5642
6132
  }
6133
+ p.trailClock = 0;
5643
6134
  }
5644
6135
  }
5645
6136
  // ---------- 每粒子更新 ----------
@@ -5790,10 +6281,20 @@ class ParticleSystem {
5790
6281
  }
5791
6282
  if (p.trail) {
5792
6283
  const tr = p.trail;
5793
- for (let i = tr.length - 3; i >= 3; i -= 3) {
5794
- tr[i] = tr[i - 3];
5795
- tr[i + 1] = tr[i - 2];
5796
- tr[i + 2] = tr[i - 1];
6284
+ const step = this.trailSampleDt;
6285
+ if (step > 0) {
6286
+ p.trailClock = (p.trailClock || 0) + dt;
6287
+ let shifts = 0;
6288
+ const cap = this.trailSegments || 8;
6289
+ while (p.trailClock >= step && shifts < cap) {
6290
+ p.trailClock -= step;
6291
+ shifts++;
6292
+ for (let i = tr.length - 3; i >= 3; i -= 3) {
6293
+ tr[i] = tr[i - 3];
6294
+ tr[i + 1] = tr[i - 2];
6295
+ tr[i + 2] = tr[i - 1];
6296
+ }
6297
+ }
5797
6298
  }
5798
6299
  tr[0] = p.x;
5799
6300
  tr[1] = p.y;
@@ -5888,13 +6389,22 @@ class ParticleSystem {
5888
6389
  if (!this._prog) this._buildProgram(gl);
5889
6390
  const trail = this.trailCfg && this.trailCfg.kind === "ropetrail" ? this.trailCfg : null;
5890
6391
  const spriteTrail = this.trailCfg && this.trailCfg.kind === "spritetrail" ? this.trailCfg : null;
6392
+ const rope = this.ropeRenderer;
5891
6393
  const segs = particleInstanceSegs(this.trailCfg, this.trailSegments);
5892
- const STRIDE = 12;
6394
+ const STRIDE = 14;
5893
6395
  const pool = this.pool;
6396
+ let order = null;
6397
+ if (rope) {
6398
+ order = this._ropeOrder;
6399
+ order.length = 0;
6400
+ for (let i = 0; i < pool.length; i++) if (pool[i].alive) order.push(pool[i]);
6401
+ order.sort((a, b) => a.seq - b.seq);
6402
+ }
5894
6403
  let live = 0;
5895
- for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
5896
- if (live === 0) return;
5897
- const instCount = live * segs;
6404
+ if (order) live = order.length;
6405
+ else for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
6406
+ if (live === 0 || rope && live < 2) return;
6407
+ const instCount = rope ? live - 1 : live * segs;
5898
6408
  const need = instCount * STRIDE;
5899
6409
  if (!this._data || this._data.length < need) this._data = new Float32Array(Math.max(need, 1024));
5900
6410
  const data = this._data;
@@ -5914,7 +6424,34 @@ class ParticleSystem {
5914
6424
  const py = ly * sy;
5915
6425
  return [ox + px * cos - py * sin, projH - (oy + px * sin + py * cos)];
5916
6426
  };
5917
- for (let i = 0; i < pool.length; i++) {
6427
+ if (rope) {
6428
+ for (let i = 0; i + 1 < live; i++) {
6429
+ const a = order[i];
6430
+ const b = order[i + 1];
6431
+ const wa = toWorld(a.x, a.y);
6432
+ const wb = toWorld(b.x, b.y);
6433
+ const dx = wb[0] - wa[0];
6434
+ const dy = wb[1] - wa[1];
6435
+ const dist = Math.hypot(dx, dy);
6436
+ const width2 = (Math.abs(a.size) + Math.abs(b.size)) * 0.5 * sysScale;
6437
+ if (!(width2 > 0)) continue;
6438
+ data[k++] = (wa[0] + wb[0]) * 0.5;
6439
+ data[k++] = (wa[1] + wb[1]) * 0.5;
6440
+ data[k++] = 0;
6441
+ data[k++] = width2;
6442
+ data[k++] = Math.atan2(-dx, dy);
6443
+ data[k++] = (a.r + b.r) * 0.5 * bright;
6444
+ data[k++] = (a.g + b.g) * 0.5 * bright;
6445
+ data[k++] = (a.b + b.b) * 0.5 * bright;
6446
+ data[k++] = (a.alpha + b.alpha) * 0.5;
6447
+ data[k++] = 1;
6448
+ data[k++] = dist / width2;
6449
+ data[k++] = 0;
6450
+ data[k++] = ropeParticleV(a);
6451
+ data[k++] = ropeParticleV(b);
6452
+ }
6453
+ }
6454
+ for (let i = 0; i < pool.length && !rope; i++) {
5918
6455
  const p = pool[i];
5919
6456
  if (!p.alive) continue;
5920
6457
  for (let s = 0; s < segs; s++) {
@@ -5922,18 +6459,53 @@ class ParticleSystem {
5922
6459
  let ly = p.y;
5923
6460
  let segAlpha = 1;
5924
6461
  let segSize = 1;
6462
+ let rot = p.rot;
6463
+ let instStretchX = stretchX;
6464
+ let instStretchY = stretchY;
6465
+ let wx;
6466
+ let wy;
5925
6467
  if (trail && p.trail) {
5926
6468
  lx = p.trail[s * 3];
5927
6469
  ly = p.trail[s * 3 + 1];
5928
6470
  const t = segs > 1 ? s / (segs - 1) : 0;
5929
6471
  segAlpha = 1 - t;
5930
6472
  segSize = 1 - t * 0.55;
6473
+ let tdx = 0;
6474
+ let tdy = 0;
6475
+ if (s + 1 < segs) {
6476
+ tdx = p.trail[s * 3] - p.trail[(s + 1) * 3];
6477
+ tdy = p.trail[s * 3 + 1] - p.trail[(s + 1) * 3 + 1];
6478
+ } else if (s > 0) {
6479
+ tdx = p.trail[(s - 1) * 3] - p.trail[s * 3];
6480
+ tdy = p.trail[(s - 1) * 3 + 1] - p.trail[s * 3 + 1];
6481
+ } else {
6482
+ tdx = p.vx;
6483
+ tdy = p.vy;
6484
+ }
6485
+ const w0 = toWorld(lx, ly);
6486
+ const w1 = toWorld(lx - tdx, ly - tdy);
6487
+ const dx = w0[0] - w1[0];
6488
+ const dy = w0[1] - w1[1];
6489
+ const dist = Math.hypot(dx, dy);
6490
+ const base = Math.max(1e-3, Math.abs(p.size) * sysScale * segSize);
6491
+ if (dist > 1e-3) {
6492
+ rot = spriteTrailRotation(dx, dy);
6493
+ wx = (w0[0] + w1[0]) * 0.5;
6494
+ wy = (w0[1] + w1[1]) * 0.5;
6495
+ instStretchY = Math.max(stretchY, dist / base);
6496
+ } else {
6497
+ wx = w0[0];
6498
+ wy = w0[1];
6499
+ }
6500
+ } else {
6501
+ const w = toWorld(lx, ly);
6502
+ wx = w[0];
6503
+ wy = w[1];
5931
6504
  }
5932
- const w = toWorld(lx, ly);
5933
- let rot = p.rot;
5934
- let instStretchX = stretchX;
5935
- let instStretchY = stretchY;
5936
6505
  if (spriteTrail) {
6506
+ const w = toWorld(p.x, p.y);
6507
+ wx = w[0];
6508
+ wy = w[1];
5937
6509
  const w1 = toWorld(p.x + p.vx, p.y + p.vy);
5938
6510
  rot = spriteTrailRotation(w1[0] - w[0], w1[1] - w[1]);
5939
6511
  const factor = spriteTrailLengthFactor(
@@ -5944,8 +6516,8 @@ class ParticleSystem {
5944
6516
  );
5945
6517
  instStretchY = stretchY * factor;
5946
6518
  }
5947
- data[k++] = w[0];
5948
- data[k++] = w[1];
6519
+ data[k++] = wx;
6520
+ data[k++] = wy;
5949
6521
  data[k++] = 0;
5950
6522
  data[k++] = Math.abs(p.size) * sysScale * segSize;
5951
6523
  data[k++] = rot;
@@ -5956,6 +6528,8 @@ class ParticleSystem {
5956
6528
  data[k++] = instStretchX;
5957
6529
  data[k++] = instStretchY;
5958
6530
  data[k++] = p.frame;
6531
+ data[k++] = 0;
6532
+ data[k++] = 1;
5959
6533
  }
5960
6534
  }
5961
6535
  const prog = this._prog;
@@ -6089,6 +6663,9 @@ const particlesMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.define
6089
6663
  particleInstanceSegs,
6090
6664
  particlePassRefract,
6091
6665
  rgbaIsBlankWhite,
6666
+ ropeParticleV,
6667
+ ropeTrailDuration,
6668
+ ropeTrailHistoryCount,
6092
6669
  spriteTrailLengthFactor,
6093
6670
  spriteTrailRotation
6094
6671
  }, Symbol.toStringTag, { value: "Module" }));
@@ -6194,7 +6771,7 @@ function spikyStar(size, points, len, sharp, coreR) {
6194
6771
  return { width: size, height: size, rgba };
6195
6772
  }
6196
6773
  function beam(w, h, coreWidth, fadeBoth, peak) {
6197
- const pk = peak === void 0 ? 0.55 : peak;
6774
+ const pk = 0.55;
6198
6775
  const rgba = new Uint8Array(w * h * 4);
6199
6776
  for (let y = 0; y < h; y++) {
6200
6777
  const ty = y / (h - 1);
@@ -6207,6 +6784,41 @@ function beam(w, h, coreWidth, fadeBoth, peak) {
6207
6784
  }
6208
6785
  return { width: w, height: h, rgba };
6209
6786
  }
6787
+ const RAIN_FRAMES = 4;
6788
+ function rainStreak(w, h, tiltDeg, corePx, peak) {
6789
+ const pk = peak === void 0 ? 0.85 : peak;
6790
+ const rgba = new Uint8Array(w * h * 4);
6791
+ const fh = h / RAIN_FRAMES;
6792
+ const rng = mulberry32$1(335009);
6793
+ const tilt = Math.tan(tiltDeg * Math.PI / 180);
6794
+ const cx = w / 2;
6795
+ for (let f = 0; f < RAIN_FRAMES; f++) {
6796
+ const peakF = pk * (0.8 + rng() * 0.35);
6797
+ const coreF = corePx * (0.85 + rng() * 0.5);
6798
+ const ph = (rng() - 0.5) * w * 0.12;
6799
+ for (let y = 0; y < fh; y++) {
6800
+ const ty = y / (fh - 1);
6801
+ const lineX = cx + ph + (fh - 1) * tilt / 2 - (fh - 1) * tilt * ty;
6802
+ const vy = gauss(ty - 0.5, 0.27);
6803
+ for (let x = 0; x < w; x++) {
6804
+ const d = Math.abs(x + 0.5 - lineX);
6805
+ const g = Math.exp(-(d * d) / (coreF * coreF));
6806
+ const a = g * vy * peakF;
6807
+ if (a < 4e-3) continue;
6808
+ writeWhite(rgba, ((f * fh + y) * w + x) * 4, a);
6809
+ }
6810
+ }
6811
+ }
6812
+ return { width: w, height: h, rgba };
6813
+ }
6814
+ function builtinParticleFrames(name) {
6815
+ if (name === "particle/nature/rain1" || name === "particle/nature/rain2") {
6816
+ const list = [];
6817
+ for (let i = 0; i < RAIN_FRAMES; i++) list.push({ ou: 0, ov: i / RAIN_FRAMES, su: 1, sv: 1 / RAIN_FRAMES });
6818
+ return list;
6819
+ }
6820
+ return null;
6821
+ }
6210
6822
  function ring(size, radius, thickness) {
6211
6823
  const rgba = new Uint8Array(size * size * 4);
6212
6824
  const half = size / 2;
@@ -6887,8 +7499,8 @@ const BUILDERS = {
6887
7499
  // 水滴(原生 64×256 → 128×512):竖长泪滴
6888
7500
  "particle/drop": () => teardrop(128, 512),
6889
7501
  // 雨丝(原生 64×256 → 128×512):细长条,两端渐隐
6890
- "particle/nature/rain1": () => beam(128, 512, 0.16, true, 0.6),
6891
- "particle/nature/rain2": () => beam(128, 512, 0.24, true, 0.55),
7502
+ "particle/nature/rain1": () => rainStreak(128, 512, 10, 1, 0.78),
7503
+ "particle/nature/rain2": () => rainStreak(128, 512, 10, 1.6, 0.66),
6892
7504
  // 雨滴 sheet(原生 128×256 → 256×512,2×4 格):每格一颗上圆下尖小水滴
6893
7505
  "particle/water/rain_drops_sheet": () => dropSheet(256, 512, 2, 4),
6894
7506
  // 雾(原生 256 → 512):絮状 fBm,弱遮罩铺满;三张不同尺度/种子
@@ -7006,6 +7618,7 @@ function isBuiltinParticleTextureName(name) {
7006
7618
  const particleTexMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
7007
7619
  __proto__: null,
7008
7620
  buildBuiltinParticleTexture,
7621
+ builtinParticleFrames,
7009
7622
  isBuiltinParticleTextureName,
7010
7623
  listBuiltinParticleTextureNames,
7011
7624
  particleNormalNameForAlbedo,
@@ -7623,6 +8236,11 @@ function applyAttachmentBindOrigins(layers) {
7623
8236
  c.origin[0] += d[0];
7624
8237
  c.origin[1] += d[1];
7625
8238
  }
8239
+ layer.parallaxDepth = parent.parallaxDepth ? parent.parallaxDepth.slice() : null;
8240
+ for (const c of desc) {
8241
+ c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
8242
+ }
8243
+ layer.attachBindDelta = [d[0], d[1]];
7626
8244
  follows.push({
7627
8245
  layer,
7628
8246
  parent,
@@ -7637,6 +8255,9 @@ function applyAttachmentBindOrigins(layers) {
7637
8255
  f.baseY = f.layer.origin[1];
7638
8256
  f.subtree = [{ layer: f.layer, x: f.layer.origin[0], y: f.layer.origin[1] }];
7639
8257
  for (const c of f.desc) f.subtree.push({ layer: c, x: c.origin[0], y: c.origin[1] });
8258
+ for (const s of f.subtree) {
8259
+ if (!s.layer.attachBase) s.layer.attachBase = [s.x, s.y];
8260
+ }
7640
8261
  }
7641
8262
  return follows;
7642
8263
  }
@@ -7662,14 +8283,20 @@ function followAttachments(follows, time, getBoneOverrides) {
7662
8283
  }
7663
8284
  deltas.push(parentMeshToWorldDelta(f.parent, cur[12] - f.bindX, cur[13] - f.bindY));
7664
8285
  }
8286
+ const baseOf = (s) => {
8287
+ const ab = s.layer.attachBase;
8288
+ if (ab) return ab;
8289
+ return [s.x, s.y];
8290
+ };
7665
8291
  const seen = /* @__PURE__ */ new Set();
7666
8292
  for (const f of follows) {
7667
8293
  const tree = f.subtree || [{ layer: f.layer, x: f.baseX, y: f.baseY }];
7668
8294
  for (const s of tree) {
7669
8295
  if (seen.has(s.layer)) continue;
7670
8296
  seen.add(s.layer);
7671
- s.layer.origin[0] = s.x;
7672
- s.layer.origin[1] = s.y;
8297
+ const b = baseOf(s);
8298
+ s.layer.origin[0] = b[0];
8299
+ s.layer.origin[1] = b[1];
7673
8300
  }
7674
8301
  }
7675
8302
  for (let i = 0; i < follows.length; i++) {
@@ -7930,10 +8557,12 @@ function layoutText(content, opts, measure) {
7930
8557
  const totalH = lines.length * lineHeight;
7931
8558
  const halign = opts.halign || "center";
7932
8559
  const valign = opts.valign || "center";
7933
- const y0 = valign === "top" ? pad : valign === "bottom" ? Math.max(pad, boxH - pad - totalH) : (boxH - totalH) / 2;
8560
+ const midX = boxW / 2;
8561
+ const midY = boxH / 2;
8562
+ const y0 = valign === "top" ? midY : valign === "bottom" ? midY - totalH : (boxH - totalH) / 2;
7934
8563
  const out = lines.map((text, i) => {
7935
8564
  const w = widths[i];
7936
- const x = halign === "left" ? pad : halign === "right" ? Math.max(pad, boxW - pad - w) : (boxW - w) / 2;
8565
+ const x = halign === "left" ? midX : halign === "right" ? midX - w : (boxW - w) / 2;
7937
8566
  return { text, width: w, x, y: y0 + i * lineHeight };
7938
8567
  });
7939
8568
  return { lines: out, lineHeight, totalH, truncated, boxW, boxH };
@@ -8308,6 +8937,11 @@ function propValue(v) {
8308
8937
  if (v !== null && typeof v === "object" && "value" in v) return v.value;
8309
8938
  return v;
8310
8939
  }
8940
+ function engineCanvasSize(cs) {
8941
+ const src = cs || { width: 1920, height: 1080 };
8942
+ if (src.x !== void 0 && src.y !== void 0) return src;
8943
+ return { x: src.width, y: src.height, width: src.width, height: src.height };
8944
+ }
8311
8945
  function evalTextScript(script, scriptprops, opts = {}) {
8312
8946
  if (typeof script !== "string" || script.length === 0) return null;
8313
8947
  const body = scriptToFunctionBody(script);
@@ -8349,7 +8983,7 @@ function evalTextScript(script, scriptprops, opts = {}) {
8349
8983
  },
8350
8984
  frametime: 1 / 60,
8351
8985
  runtime: 0,
8352
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
8986
+ canvasSize: engineCanvasSize(opts.canvasSize),
8353
8987
  // [we-scene patch] engine.screenResolution(全库 10 处 / 2 壁纸):
8354
8988
  // 屏幕**像素**尺寸,脚本用它把 input.cursorScreenPosition 归一化
8355
8989
  // (3791967416 除它得 [0,1];3509243656 减半屏得 [-1,1])。
@@ -8549,6 +9183,19 @@ function evalTextScript(script, scriptprops, opts = {}) {
8549
9183
  if (opts.onError) opts.onError(e, "applyUserProperties");
8550
9184
  }
8551
9185
  },
9186
+ /** 直调 update 不做文本加工:层可见性脚本(visible.script)要拿原始返回值
9187
+ * ——布尔控可见,callUpdate 会把它吞成 null(防画到画面上的文字版语义)。 */
9188
+ callUpdateRaw(value) {
9189
+ if (!fns.update || sandbox.disabled) return void 0;
9190
+ try {
9191
+ return fns.update(value);
9192
+ } catch (e) {
9193
+ sandbox.errCount++;
9194
+ if (opts.onError) opts.onError(e, "update");
9195
+ if (sandbox.errCount >= 3) sandbox.disabled = true;
9196
+ return void 0;
9197
+ }
9198
+ },
8552
9199
  /** 求值当前文本:返回新文本;undefined/null 保留原值;连续出错 3 次熔断回退静态文本 */
8553
9200
  callUpdate(value) {
8554
9201
  if (!fns.update || sandbox.disabled) return null;
@@ -9240,13 +9887,8 @@ function makeObjectLayerProxy(layer, opts) {
9240
9887
  Object.defineProperty(proxy, key, {
9241
9888
  enumerable: true,
9242
9889
  get() {
9243
- if (layer && Array.isArray(layer[key])) {
9244
- const a = layer[key];
9245
- store[key].x = a[0] || 0;
9246
- store[key].y = a[1] || 0;
9247
- store[key].z = a[2] || 0;
9248
- }
9249
- return store[key];
9890
+ const a = layer && Array.isArray(layer[key]) ? layer[key] : null;
9891
+ return makeVec3(a || [0, 0, 0]);
9250
9892
  },
9251
9893
  set(v) {
9252
9894
  const a = normVec(v);
@@ -9299,7 +9941,7 @@ function evalObjectScript(script, scriptprops, opts = {}) {
9299
9941
  },
9300
9942
  frametime: 1 / 60,
9301
9943
  runtime: 0,
9302
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
9944
+ canvasSize: engineCanvasSize(opts.canvasSize),
9303
9945
  screenResolution: opts.screenResolution || { x: 1920, y: 1080 },
9304
9946
  timeOfDay: typeof opts.timeOfDay === "number" ? opts.timeOfDay : 0,
9305
9947
  userProperties: opts.userProperties || {},
@@ -9442,7 +10084,8 @@ function evalObjectScript(script, scriptprops, opts = {}) {
9442
10084
  const hasCursorHook = !!(fns && (fns.cursorClick || fns.cursorEnter || fns.cursorLeave || fns.cursorDown || fns.cursorUp || fns.cursorMove));
9443
10085
  const hasMediaHook = !!(fns && MEDIA_CALLBACKS.some((n) => fns[n]));
9444
10086
  const hasApplyHook = !!(fns && typeof fns.applyUserProperties === "function");
9445
- if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook) return null;
10087
+ const usesEngineClock = /\bengine\s*\.\s*(runtime|frametime)\b/.test(body);
10088
+ if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook && !usesEngineClock) return null;
9446
10089
  const sandbox = {
9447
10090
  engine,
9448
10091
  scriptProperties: spValues,
@@ -9734,7 +10377,7 @@ function createEngineTimers(host = {}, opts = {}) {
9734
10377
  cancel.handle = state.handle;
9735
10378
  return cancel;
9736
10379
  }
9737
- function setInterval(fn, ms) {
10380
+ function setInterval2(fn, ms) {
9738
10381
  if (typeof fn !== "function") return makeCancel({ fired: true, handle: null }, null);
9739
10382
  const state = { fired: false, handle: null };
9740
10383
  const cancel = makeCancel(state, clearI);
@@ -9743,14 +10386,14 @@ function createEngineTimers(host = {}, opts = {}) {
9743
10386
  cancel.handle = state.handle;
9744
10387
  return cancel;
9745
10388
  }
9746
- function clearTimeout(h) {
10389
+ function clearTimeout2(h) {
9747
10390
  if (typeof h === "function") {
9748
10391
  h();
9749
10392
  return;
9750
10393
  }
9751
10394
  if (clearT && h != null) clearT(h);
9752
10395
  }
9753
- function clearInterval(h) {
10396
+ function clearInterval2(h) {
9754
10397
  if (typeof h === "function") {
9755
10398
  h();
9756
10399
  return;
@@ -9763,9 +10406,9 @@ function createEngineTimers(host = {}, opts = {}) {
9763
10406
  }
9764
10407
  return {
9765
10408
  setTimeout,
9766
- setInterval,
9767
- clearTimeout,
9768
- clearInterval,
10409
+ setInterval: setInterval2,
10410
+ clearTimeout: clearTimeout2,
10411
+ clearInterval: clearInterval2,
9769
10412
  dispose,
9770
10413
  /** 测试与诊断用:尚未触发且未取消的定时器数 */
9771
10414
  pendingCount: () => pending.size
@@ -9815,11 +10458,13 @@ function createSimulatedAudio(seed = 20260830) {
9815
10458
  const patterns = makePatterns(rand2);
9816
10459
  const phases = new Float32Array(64);
9817
10460
  for (let i = 0; i < 64; i++) phases[i] = rand2() * 64;
9818
- const BANDS = 64;
9819
- const rawL = new Float32Array(BANDS);
9820
- const rawR = new Float32Array(BANDS);
9821
- const left64 = new Float32Array(BANDS);
9822
- const right64 = new Float32Array(BANDS);
10461
+ const BANDS2 = 64;
10462
+ const rawL = new Float32Array(BANDS2);
10463
+ const rawR = new Float32Array(BANDS2);
10464
+ const preL64 = new Float32Array(BANDS2);
10465
+ const preR64 = new Float32Array(BANDS2);
10466
+ const left64 = new Float32Array(BANDS2);
10467
+ const right64 = new Float32Array(BANDS2);
9823
10468
  const left32 = new Float32Array(32);
9824
10469
  const right32 = new Float32Array(32);
9825
10470
  const left16 = new Float32Array(16);
@@ -9831,12 +10476,21 @@ function createSimulatedAudio(seed = 20260830) {
9831
10476
  right32,
9832
10477
  left16,
9833
10478
  right16,
10479
+ /**
10480
+ * 未钳位(pre-GAIN、pre-clamp)的 64 band,含左右声道 pan。
10481
+ * left64/right64 是 `min(1, v*GAIN)` 之后的值:底鼓段基底就已到 ~0.6、峰值贴 1,
10482
+ * 波峰因数被压平——网页作者按「峰值过阈值」判定敲击时(1520828134 猫爪
10483
+ * `audioArray[i] > 0.5`),事后再乘任何标量都无法把基底与峰值分开。
10484
+ * 网页驱动改对本数组做 gamma 对比扩展,音条墙仍走已标定的 left64/right64。
10485
+ */
10486
+ preL64,
10487
+ preR64,
9834
10488
  /** vumeter:整体响度 0..1(粒子 audioprocessing / 文字脚本 average 用) */
9835
10489
  level: 0,
9836
10490
  /** 渲染器诊断:当前是否处于「静音段」 */
9837
10491
  silent: false
9838
10492
  };
9839
- function downsample(dst, src) {
10493
+ function downsample2(dst, src) {
9840
10494
  const g = src.length / dst.length;
9841
10495
  for (let i = 0; i < dst.length; i++) {
9842
10496
  let s = 0;
@@ -9863,8 +10517,8 @@ function createSimulatedAudio(seed = 20260830) {
9863
10517
  const hatV = patterns.hat[i16] * hitEnv * drumGate;
9864
10518
  const riser = buildup > 0 ? Math.pow(buildup, 3) * (0.4 + 0.6 * Math.abs(vnoise(step * 2, 7))) : 0;
9865
10519
  let levelSum = 0;
9866
- for (let i = 0; i < BANDS; i++) {
9867
- const fq = i / BANDS;
10520
+ for (let i = 0; i < BANDS2; i++) {
10521
+ const fq = i / BANDS2;
9868
10522
  const tilt = Math.pow(1 - fq * 0.85, 1.6);
9869
10523
  let v = midGate * (0.5 + 0.3 * vnoise(beat * 0.5 + phases[i] * 0.05, i % 8));
9870
10524
  v *= 0.35 + 0.65 * fq;
@@ -9876,6 +10530,7 @@ function createSimulatedAudio(seed = 20260830) {
9876
10530
  if (fq >= 0.45) v += hatV * 0.5 * ((fq - 0.45) / 0.55);
9877
10531
  v += riser * 0.5;
9878
10532
  v *= tilt;
10533
+ const vPre = v;
9879
10534
  v = Math.min(1, v * GAIN);
9880
10535
  const width = 0.06 + fq * 0.2;
9881
10536
  const pan = vnoise(beat * 0.13 + i * 0.35, 11) * width;
@@ -9884,14 +10539,16 @@ function createSimulatedAudio(seed = 20260830) {
9884
10539
  const floorV = silent ? 0.012 : 0;
9885
10540
  rawL[i] = Math.max(floorV, l);
9886
10541
  rawR[i] = Math.max(floorV, r);
10542
+ preL64[i] = Math.max(floorV, Math.max(0, vPre * (1 - pan)));
10543
+ preR64[i] = Math.max(floorV, Math.max(0, vPre * (1 + pan)));
9887
10544
  if (i < 48) levelSum += (rawL[i] + rawR[i]) * 0.5;
9888
10545
  }
9889
10546
  left64.set(rawL);
9890
10547
  right64.set(rawR);
9891
- downsample(left32, rawL);
9892
- downsample(right32, rawR);
9893
- downsample(left16, rawL);
9894
- downsample(right16, rawR);
10548
+ downsample2(left32, rawL);
10549
+ downsample2(right32, rawR);
10550
+ downsample2(left16, rawL);
10551
+ downsample2(right16, rawR);
9895
10552
  snapshot.level = Math.min(1, levelSum / (48 * 1.2));
9896
10553
  snapshot.silent = silent;
9897
10554
  return snapshot;
@@ -9900,7 +10557,7 @@ function createSimulatedAudio(seed = 20260830) {
9900
10557
  update,
9901
10558
  snapshot,
9902
10559
  /** 频段基数 */
9903
- bands: BANDS
10560
+ bands: BANDS2
9904
10561
  };
9905
10562
  }
9906
10563
  function fillAudioBuffers(views, snapshot) {
@@ -9922,7 +10579,7 @@ function fillOne(dst, src64) {
9922
10579
  dst[i] = s / (i1 - i0);
9923
10580
  }
9924
10581
  }
9925
- const MEDIA_PLAYBACK = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
10582
+ const MEDIA_PLAYBACK$1 = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
9926
10583
  class MediaVec3 {
9927
10584
  constructor(x, y, z) {
9928
10585
  this.x = Number(x) || 0;
@@ -10047,7 +10704,7 @@ function createSimulatedMedia(seed = 20260901) {
10047
10704
  const cycle = tracks.reduce((s, t) => s + t.duration + GAP, 0);
10048
10705
  const snapshot = {
10049
10706
  hasMedia: false,
10050
- state: MEDIA_PLAYBACK.STOPPED,
10707
+ state: MEDIA_PLAYBACK$1.STOPPED,
10051
10708
  title: "",
10052
10709
  artist: "",
10053
10710
  album: "",
@@ -10114,10 +10771,10 @@ function createSimulatedMedia(seed = 20260901) {
10114
10771
  snapshot.duration = tr.duration;
10115
10772
  snapshot.position = pos;
10116
10773
  const frac = tr.duration > 0 ? pos / tr.duration : 0;
10117
- if (held) snapshot.state = MEDIA_PLAYBACK.PAUSED;
10118
- else if (inGap) snapshot.state = MEDIA_PLAYBACK.STOPPED;
10119
- else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK.PAUSED;
10120
- else snapshot.state = MEDIA_PLAYBACK.PLAYING;
10774
+ if (held) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10775
+ else if (inGap) snapshot.state = MEDIA_PLAYBACK$1.STOPPED;
10776
+ else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10777
+ else snapshot.state = MEDIA_PLAYBACK$1.PLAYING;
10121
10778
  snapshot.hasThumbnail = !inGap;
10122
10779
  const c = tr.colors;
10123
10780
  snapshot.primaryColor = new MediaVec3(c.primary[0], c.primary[1], c.primary[2]);
@@ -10156,7 +10813,7 @@ function createSimulatedMedia(seed = 20260901) {
10156
10813
  if (held) return snapshot;
10157
10814
  holdT = lastWall + seekOffset;
10158
10815
  held = true;
10159
- snapshot.state = MEDIA_PLAYBACK.PAUSED;
10816
+ snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10160
10817
  return snapshot;
10161
10818
  }
10162
10819
  function play() {
@@ -10173,7 +10830,7 @@ function createSimulatedMedia(seed = 20260901) {
10173
10830
  snapshot,
10174
10831
  tracks,
10175
10832
  cycle,
10176
- MEDIA_PLAYBACK,
10833
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10177
10834
  skipNext,
10178
10835
  skipPrevious,
10179
10836
  play,
@@ -10281,7 +10938,7 @@ function cloneMediaSnapshot(s) {
10281
10938
  }
10282
10939
  const mediaMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10283
10940
  __proto__: null,
10284
- MEDIA_PLAYBACK,
10941
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10285
10942
  cloneMediaSnapshot,
10286
10943
  createSimulatedMedia,
10287
10944
  diffMediaEvents,
@@ -10381,15 +11038,7 @@ function createPointerSource(opts = {}) {
10381
11038
  state.screenH = v.h || 1;
10382
11039
  }
10383
11040
  readViewport();
10384
- function onMove(ev) {
10385
- readViewport();
10386
- let x = ev.clientX;
10387
- let y = ev.clientY;
10388
- if (target && typeof target.getBoundingClientRect === "function") {
10389
- const r = target.getBoundingClientRect();
10390
- x -= r.left;
10391
- y -= r.top;
10392
- }
11041
+ function applyMove(x, y) {
10393
11042
  const u = x / state.screenW;
10394
11043
  const v = y / state.screenH;
10395
11044
  if (!state.has) {
@@ -10406,18 +11055,35 @@ function createPointerSource(opts = {}) {
10406
11055
  state.moveCount++;
10407
11056
  state.lastEventTime = Date.now();
10408
11057
  }
11058
+ function applyButtons(mask) {
11059
+ const left = (mask & 1) !== 0;
11060
+ if (left === state.leftDown) return;
11061
+ state.leftDown = left;
11062
+ if (left) state.downCount++;
11063
+ else state.upCount++;
11064
+ state.lastEventTime = Date.now();
11065
+ }
11066
+ function onMove(ev) {
11067
+ readViewport();
11068
+ let x = ev.clientX;
11069
+ let y = ev.clientY;
11070
+ if (target && typeof target.getBoundingClientRect === "function") {
11071
+ const r = target.getBoundingClientRect();
11072
+ x -= r.left;
11073
+ y -= r.top;
11074
+ }
11075
+ applyMove(x, y);
11076
+ }
10409
11077
  function onDown(ev) {
10410
11078
  if (ev.button !== void 0 && ev.button !== 0) return;
10411
- state.leftDown = true;
10412
- state.downCount++;
11079
+ applyButtons(1);
10413
11080
  }
10414
11081
  function onUp(ev) {
10415
11082
  if (ev.button !== void 0 && ev.button !== 0) return;
10416
- state.leftDown = false;
10417
- state.upCount++;
11083
+ applyButtons(0);
10418
11084
  }
10419
11085
  function onLeaveWindow() {
10420
- state.leftDown = false;
11086
+ applyButtons(0);
10421
11087
  }
10422
11088
  let attached = false;
10423
11089
  if (target && target.addEventListener) {
@@ -10433,9 +11099,45 @@ function createPointerSource(opts = {}) {
10433
11099
  return {
10434
11100
  state,
10435
11101
  /**
10436
- * 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current
10437
- * 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
10438
- * 帧间位移抹成 0(见文件头注释)。
11102
+ * 外部注入指针状态(宿主轮询系统鼠标后推入)。协议见 docs/INTEGRATION.md
11103
+ *
11104
+ * 接**归一化**坐标而不是像素:宿主知道自己那块屏的 points 尺寸,除法在它那边
11105
+ * 做更准(混合 DPI 多显示器下无需任何 DPR 折算);这里再乘回 screenW/H 得到
11106
+ * input.cursorScreenPosition 要的像素。
11107
+ *
11108
+ * u/v 是 [0,1]、原点左上、**Y 朝下** —— 与 DOM 路径的 state.u/v 同一空间
11109
+ * (见文件头坐标约定)。宿主不要替 shader 翻 Y。
11110
+ *
11111
+ * 不在这里推进 last:外部推送频率(~90Hz)高于帧率,若在推送里推进 last,
11112
+ * `length(g_PointerPosition - g_PointerPositionLast)` 会恒接近 0,
11113
+ * cursorripple 完全不起波且无报错(与 DOM 路径同一个坑,见文件头)。
11114
+ *
11115
+ * @param {{u:number, v:number, buttons?:number}} p 归一化位置 + 按键位掩码(bit0 左)
11116
+ */
11117
+ pushExternal(p) {
11118
+ if (!p) return;
11119
+ readViewport();
11120
+ const u = Number(p.u);
11121
+ const v = Number(p.v);
11122
+ if (Number.isFinite(u) && Number.isFinite(v)) {
11123
+ applyMove(u * state.screenW, v * state.screenH);
11124
+ }
11125
+ applyButtons(Number(p.buttons) || 0);
11126
+ },
11127
+ /**
11128
+ * 外部指针离开本窗口(鼠标移到了别的显示器)。
11129
+ *
11130
+ * **只清按键,保留位置与 has** —— 清 has 会让 xray 开窗突然跳到相机外
11131
+ * (renderer.js 的 XRAY_IDLE_SCREEN_UV)、视差弹回中心,画面会明显抽一下。
11132
+ * 语义与 DOM 的 onLeaveWindow 一致:位置停在最后已知点,只是不再按着键。
11133
+ */
11134
+ pushExternalLeave() {
11135
+ applyButtons(0);
11136
+ },
11137
+ /**
11138
+ * 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current。
11139
+ * 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
11140
+ * 帧间位移抹成 0(见文件头注释)。
10439
11141
  */
10440
11142
  beginFrame() {
10441
11143
  readViewport();
@@ -10958,6 +11660,21 @@ function httpSource(baseUrl, init) {
10958
11660
  } catch {
10959
11661
  return null;
10960
11662
  }
11663
+ },
11664
+ async webEntry(signal) {
11665
+ let file = "index.html";
11666
+ try {
11667
+ const r = await fetch(`${base}/project.json`, { ...init, signal });
11668
+ if (r.ok) {
11669
+ const project = await r.json();
11670
+ if (project && typeof project.file === "string" && project.file.trim()) {
11671
+ file = project.file.trim().replace(/^\/+/, "");
11672
+ }
11673
+ }
11674
+ } catch {
11675
+ if (signal?.aborted) throw new Error("aborted");
11676
+ }
11677
+ return { url: `${base}/${file}` };
10961
11678
  }
10962
11679
  };
10963
11680
  }
@@ -11145,6 +11862,382 @@ const SKIP_TEXT = false;
11145
11862
  const SKIP_PARTICLES = false;
11146
11863
  const SKIP_SCENE_EFFECTS = false;
11147
11864
  const TEXT_EM_SCALE = 4;
11865
+ const BANDS = 64;
11866
+ const MEDIA_PLAYBACK = { STOPPED: 0 };
11867
+ function zeroBands() {
11868
+ return {
11869
+ left64: new Float32Array(BANDS),
11870
+ right64: new Float32Array(BANDS),
11871
+ left32: new Float32Array(32),
11872
+ right32: new Float32Array(32),
11873
+ left16: new Float32Array(16),
11874
+ right16: new Float32Array(16),
11875
+ level: 0,
11876
+ silent: true
11877
+ };
11878
+ }
11879
+ function downsample(dst, src64) {
11880
+ const g = src64.length / dst.length;
11881
+ for (let i = 0; i < dst.length; i++) {
11882
+ let s = 0;
11883
+ const i0 = Math.floor(i * g);
11884
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
11885
+ for (let j = i0; j < i1; j++) s += src64[j];
11886
+ dst[i] = s / (i1 - i0);
11887
+ }
11888
+ }
11889
+ function fillFromByteFreq(out, bytes, sampleRate) {
11890
+ const n = bytes.length;
11891
+ const nyquist = sampleRate * 0.5;
11892
+ const fMin = 20;
11893
+ const fMax = Math.min(2e4, nyquist);
11894
+ let levelSum = 0;
11895
+ for (let b = 0; b < BANDS; b++) {
11896
+ const t0 = b / BANDS;
11897
+ const t1 = (b + 1) / BANDS;
11898
+ const loHz = fMin * Math.pow(fMax / fMin, t0);
11899
+ const hiHz = fMin * Math.pow(fMax / fMin, t1);
11900
+ const i0 = Math.max(0, Math.floor(loHz / nyquist * n));
11901
+ const i1 = Math.min(n, Math.max(i0 + 1, Math.ceil(hiHz / nyquist * n)));
11902
+ let s = 0;
11903
+ for (let i = i0; i < i1; i++) s += bytes[i] / 255;
11904
+ const v = Math.min(1, s / (i1 - i0) * 1.35);
11905
+ out.left64[b] = v;
11906
+ out.right64[b] = v;
11907
+ if (b < 48) levelSum += v;
11908
+ }
11909
+ downsample(out.left32, out.left64);
11910
+ downsample(out.right32, out.right64);
11911
+ downsample(out.left16, out.left64);
11912
+ downsample(out.right16, out.right64);
11913
+ out.level = Math.min(1, levelSum / (48 * 1.2));
11914
+ out.silent = out.level < 0.02;
11915
+ }
11916
+ function hashHue(s) {
11917
+ let h = 2166136261;
11918
+ for (let i = 0; i < s.length; i++) {
11919
+ h ^= s.charCodeAt(i);
11920
+ h = Math.imul(h, 16777619);
11921
+ }
11922
+ return (h >>> 0) % 360;
11923
+ }
11924
+ function hslToRgb(h, sat, light) {
11925
+ const s = sat / 100;
11926
+ const l = light / 100;
11927
+ const c = (1 - Math.abs(2 * l - 1)) * s;
11928
+ const hp = h / 60;
11929
+ const x = c * (1 - Math.abs(hp % 2 - 1));
11930
+ let r = 0, g = 0, b = 0;
11931
+ if (hp < 1) [r, g, b] = [c, x, 0];
11932
+ else if (hp < 2) [r, g, b] = [x, c, 0];
11933
+ else if (hp < 3) [r, g, b] = [0, c, x];
11934
+ else if (hp < 4) [r, g, b] = [0, x, c];
11935
+ else if (hp < 5) [r, g, b] = [x, 0, c];
11936
+ else [r, g, b] = [c, 0, x];
11937
+ const m = l - c / 2;
11938
+ return [r + m, g + m, b + m];
11939
+ }
11940
+ function applyPalette(snap, seed) {
11941
+ const hue = hashHue(seed || "empty");
11942
+ const [pr, pg, pb] = hslToRgb(hue, 72, 48);
11943
+ const [sr, sg, sb] = hslToRgb((hue + 40) % 360, 55, 28);
11944
+ const [tr, tg, tb] = hslToRgb((hue + 20) % 360, 70, 72);
11945
+ snap.primaryColor = media.mediaVec3(pr, pg, pb);
11946
+ snap.secondaryColor = media.mediaVec3(sr, sg, sb);
11947
+ snap.tertiaryColor = media.mediaVec3(tr, tg, tb);
11948
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
11949
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
11950
+ snap.hasThumbnail = !!seed;
11951
+ }
11952
+ function sampleArtworkPalette(img, w, h) {
11953
+ const c = document.createElement("canvas");
11954
+ c.width = 32;
11955
+ c.height = 32;
11956
+ const ctx = c.getContext("2d", { willReadFrequently: true });
11957
+ if (!ctx) return null;
11958
+ ctx.drawImage(img, 0, 0, w, h, 0, 0, 32, 32);
11959
+ const data = ctx.getImageData(0, 0, 32, 32).data;
11960
+ let r = 0, g = 0, b = 0, n = 0;
11961
+ let br = 0, bg = 0, bb = 0, best = -1;
11962
+ for (let i = 0; i < data.length; i += 4) {
11963
+ const pr = data[i] / 255, pg = data[i + 1] / 255, pb = data[i + 2] / 255;
11964
+ r += pr;
11965
+ g += pg;
11966
+ b += pb;
11967
+ n++;
11968
+ const mx = Math.max(pr, pg, pb), mn = Math.min(pr, pg, pb);
11969
+ const sat = mx - mn;
11970
+ const lum = 0.2126 * pr + 0.7152 * pg + 0.0722 * pb;
11971
+ const score = sat * 1.4 + (lum > 0.15 && lum < 0.85 ? 0.3 : 0);
11972
+ if (score > best) {
11973
+ best = score;
11974
+ br = pr;
11975
+ bg = pg;
11976
+ bb = pb;
11977
+ }
11978
+ }
11979
+ if (!n) return null;
11980
+ const primary = [br, bg, bb];
11981
+ const secondary = [r / n * 0.55, g / n * 0.55, b / n * 0.55];
11982
+ const tertiary = [
11983
+ Math.min(1, primary[0] * 0.45 + 0.55),
11984
+ Math.min(1, primary[1] * 0.45 + 0.55),
11985
+ Math.min(1, primary[2] * 0.45 + 0.55)
11986
+ ];
11987
+ return { primary, secondary, tertiary };
11988
+ }
11989
+ function rasterizeArtwork(img, srcW, srcH, size = 512) {
11990
+ const c = document.createElement("canvas");
11991
+ c.width = size;
11992
+ c.height = size;
11993
+ const ctx = c.getContext("2d");
11994
+ const scale = Math.max(size / Math.max(1, srcW), size / Math.max(1, srcH));
11995
+ const dw = srcW * scale;
11996
+ const dh = srcH * scale;
11997
+ ctx.fillStyle = "#000";
11998
+ ctx.fillRect(0, 0, size, size);
11999
+ ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
12000
+ const id = ctx.getImageData(0, 0, size, size);
12001
+ return { width: size, height: size, rgba: new Uint8Array(id.data) };
12002
+ }
12003
+ function emptyMediaSnapshot() {
12004
+ return {
12005
+ hasMedia: false,
12006
+ state: MEDIA_PLAYBACK.STOPPED,
12007
+ title: "",
12008
+ artist: "",
12009
+ album: "",
12010
+ albumArtist: "",
12011
+ position: 0,
12012
+ duration: 0,
12013
+ hasThumbnail: false,
12014
+ primaryColor: media.mediaVec3(0, 0, 0),
12015
+ secondaryColor: media.mediaVec3(0, 0, 0),
12016
+ tertiaryColor: media.mediaVec3(0, 0, 0),
12017
+ textColor: media.mediaVec3(1, 1, 1),
12018
+ highContrastColor: media.mediaVec3(1, 1, 1),
12019
+ trackIndex: -1,
12020
+ lyrics: [],
12021
+ lyricLine: "",
12022
+ lyricIndex: -1
12023
+ };
12024
+ }
12025
+ async function openMicAnalyser() {
12026
+ if (!navigator.mediaDevices?.getUserMedia) return null;
12027
+ try {
12028
+ const stream = await navigator.mediaDevices.getUserMedia({
12029
+ audio: {
12030
+ echoCancellation: false,
12031
+ noiseSuppression: false,
12032
+ autoGainControl: false
12033
+ },
12034
+ video: false
12035
+ });
12036
+ const ctx = new AudioContext();
12037
+ const src = ctx.createMediaStreamSource(stream);
12038
+ const analyser = ctx.createAnalyser();
12039
+ analyser.fftSize = 2048;
12040
+ analyser.smoothingTimeConstant = 0.8;
12041
+ src.connect(analyser);
12042
+ if (ctx.state === "suspended") await ctx.resume().catch(() => {
12043
+ });
12044
+ return { ctx, stream, analyser, buf: new Uint8Array(analyser.frequencyBinCount) };
12045
+ } catch {
12046
+ return null;
12047
+ }
12048
+ }
12049
+ async function startLiveSystem(opts) {
12050
+ const origin = opts?.origin ?? (typeof location !== "undefined" ? location.origin : "");
12051
+ const onArtwork = opts?.onArtwork;
12052
+ const audioSnap = zeroBands();
12053
+ const mediaSnap = emptyMediaSnapshot();
12054
+ const winSnap = { app: "", title: "", url: "", index: 0 };
12055
+ let audioMode = "off";
12056
+ let mediaMode = "offline";
12057
+ let windowMode = "offline";
12058
+ let trackKey = "";
12059
+ let hasArtwork = false;
12060
+ let lastArtworkKey = "";
12061
+ const mic = await openMicAnalyser();
12062
+ if (mic) audioMode = "mic";
12063
+ else if (!navigator.mediaDevices?.getUserMedia) audioMode = "unavailable";
12064
+ else audioMode = "denied";
12065
+ let es = null;
12066
+ let pollTimer = null;
12067
+ let disposed = false;
12068
+ const requestArtwork = (key, title, artist) => {
12069
+ if (!origin || !onArtwork || !hasArtwork) return;
12070
+ if (lastArtworkKey === key) return;
12071
+ lastArtworkKey = key;
12072
+ onArtwork({
12073
+ url: `${origin}/api/system/artwork?k=${encodeURIComponent(key)}&_=${Date.now()}`,
12074
+ trackKey: key,
12075
+ title,
12076
+ artist
12077
+ });
12078
+ };
12079
+ const applyMediaPayload = (m) => {
12080
+ if (!m || !m.hasMedia) {
12081
+ mediaSnap.hasMedia = false;
12082
+ mediaSnap.state = MEDIA_PLAYBACK.STOPPED;
12083
+ mediaSnap.title = "";
12084
+ mediaSnap.artist = "";
12085
+ mediaSnap.album = "";
12086
+ mediaSnap.albumArtist = "";
12087
+ mediaSnap.position = 0;
12088
+ mediaSnap.duration = 0;
12089
+ mediaSnap.hasThumbnail = false;
12090
+ mediaSnap.trackIndex = -1;
12091
+ mediaMode = m ? "empty" : "offline";
12092
+ trackKey = "";
12093
+ hasArtwork = false;
12094
+ lastArtworkKey = "";
12095
+ return;
12096
+ }
12097
+ mediaMode = "live";
12098
+ mediaSnap.hasMedia = true;
12099
+ mediaSnap.state = Number(m.state) === 2 ? 2 : Number(m.state) === 1 ? 1 : 0;
12100
+ mediaSnap.title = String(m.title ?? "");
12101
+ mediaSnap.artist = String(m.artist ?? "");
12102
+ mediaSnap.album = String(m.album ?? "");
12103
+ mediaSnap.albumArtist = String(m.albumArtist ?? m.artist ?? "");
12104
+ mediaSnap.position = Number(m.position) || 0;
12105
+ mediaSnap.duration = Number(m.duration) || 0;
12106
+ hasArtwork = m.hasArtwork === true;
12107
+ const key = `${mediaSnap.title}|${mediaSnap.artist}|${mediaSnap.album}`;
12108
+ if (key !== trackKey) {
12109
+ trackKey = key;
12110
+ lastArtworkKey = "";
12111
+ mediaSnap.trackIndex = mediaSnap.trackIndex + 1 | 0;
12112
+ applyPalette(mediaSnap, key);
12113
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
12114
+ } else {
12115
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
12116
+ }
12117
+ };
12118
+ const applyWindowPayload = (w) => {
12119
+ if (!w) {
12120
+ windowMode = "offline";
12121
+ return;
12122
+ }
12123
+ winSnap.app = String(w.app ?? "");
12124
+ winSnap.title = String(w.title ?? "");
12125
+ winSnap.url = String(w.url ?? "");
12126
+ windowMode = winSnap.app || winSnap.title ? "live" : "empty";
12127
+ };
12128
+ const pollOnce = async () => {
12129
+ if (!origin || disposed) return;
12130
+ try {
12131
+ const [mr, wr] = await Promise.all([
12132
+ fetch(`${origin}/api/system/media`, { cache: "no-store" }),
12133
+ fetch(`${origin}/api/system/window`, { cache: "no-store" })
12134
+ ]);
12135
+ if (mr.ok) {
12136
+ const j = await mr.json();
12137
+ applyMediaPayload(j);
12138
+ } else {
12139
+ mediaMode = "offline";
12140
+ }
12141
+ if (wr.ok) {
12142
+ applyWindowPayload(await wr.json());
12143
+ }
12144
+ } catch {
12145
+ mediaMode = mediaMode === "live" ? "live" : "offline";
12146
+ windowMode = windowMode === "live" ? "live" : "offline";
12147
+ }
12148
+ };
12149
+ if (origin) {
12150
+ await pollOnce();
12151
+ pollTimer = setInterval(() => void pollOnce(), 1e3);
12152
+ try {
12153
+ es = new EventSource(`${origin}/api/system/stream`);
12154
+ es.onmessage = (ev) => {
12155
+ if (disposed) return;
12156
+ try {
12157
+ const data = JSON.parse(ev.data);
12158
+ applyMediaPayload(data.media);
12159
+ applyWindowPayload(data.window);
12160
+ } catch {
12161
+ }
12162
+ };
12163
+ } catch {
12164
+ }
12165
+ }
12166
+ const postControl = (action) => {
12167
+ if (!origin || disposed) return;
12168
+ void fetch(`${origin}/api/system/media-control`, {
12169
+ method: "POST",
12170
+ headers: { "Content-Type": "application/json" },
12171
+ body: JSON.stringify({ action })
12172
+ }).then(async (r) => {
12173
+ if (!r.ok) return null;
12174
+ return r.json();
12175
+ }).then((j) => {
12176
+ if (j && typeof j === "object") applyMediaPayload(j);
12177
+ void pollOnce();
12178
+ }).catch(() => {
12179
+ void pollOnce();
12180
+ });
12181
+ };
12182
+ return {
12183
+ audio: {
12184
+ snapshot: audioSnap,
12185
+ pump: () => {
12186
+ if (!mic || disposed) {
12187
+ audioSnap.level = 0;
12188
+ audioSnap.silent = true;
12189
+ return;
12190
+ }
12191
+ mic.analyser.getByteFrequencyData(mic.buf);
12192
+ fillFromByteFreq(audioSnap, mic.buf, mic.ctx.sampleRate || 48e3);
12193
+ }
12194
+ },
12195
+ media: {
12196
+ snapshot: mediaSnap,
12197
+ pump: () => {
12198
+ },
12199
+ skipNext: () => postControl("skipNext"),
12200
+ skipPrevious: () => postControl("skipPrevious"),
12201
+ play: () => postControl("play"),
12202
+ pause: () => postControl("pause"),
12203
+ playPause: () => postControl("playPause")
12204
+ },
12205
+ windowTitle: {
12206
+ snapshot: winSnap,
12207
+ pump: () => {
12208
+ }
12209
+ },
12210
+ status: () => ({
12211
+ audio: audioMode,
12212
+ media: mediaMode,
12213
+ window: windowMode,
12214
+ title: mediaSnap.title,
12215
+ artist: mediaSnap.artist,
12216
+ app: winSnap.app,
12217
+ windowTitle: winSnap.title,
12218
+ hasArtwork
12219
+ }),
12220
+ dispose: () => {
12221
+ disposed = true;
12222
+ if (pollTimer) {
12223
+ clearInterval(pollTimer);
12224
+ pollTimer = null;
12225
+ }
12226
+ try {
12227
+ es?.close();
12228
+ } catch {
12229
+ }
12230
+ es = null;
12231
+ if (mic) {
12232
+ try {
12233
+ mic.stream.getTracks().forEach((t) => t.stop());
12234
+ void mic.ctx.close();
12235
+ } catch {
12236
+ }
12237
+ }
12238
+ }
12239
+ };
12240
+ }
11148
12241
  const WE_SHADER_HEADERS = {
11149
12242
  "common.h": `// WE common.h(重建子集,供 we-scene 浏览器渲染)
11150
12243
  #define M_PI 3.14159265359
@@ -11417,10 +12510,18 @@ mat3 squareToQuad(vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
11417
12510
  float d = p1.y - p0.y + g * p1.y;
11418
12511
  float e = p3.y - p0.y + h * p3.y;
11419
12512
  float f = p0.y;
11420
- // 行向量约定:[u v 1] * M
11421
- return mat3(a, d, g,
11422
- b, e, h,
11423
- c, f, 1.0);
12513
+ // 调用点一律是 mul(vec3(uv,1), inverse(本函数结果)),hlsl2glsl 把它转写成
12514
+ // transpose(xform) * vec3(uv,1)。要让屏幕点 s 得到 texCoord = S⁻¹·s(S 为
12515
+ // Heckbert 正向矩阵 [[a,b,c],[d,e,f],[g,h,1]],单位方→四边形),必须
12516
+ // xform = inverse(本函数结果) 满足 transpose(xform)·s = S⁻¹·s,
12517
+ // 即本函数返回 S 的**转置**:mat3 列主序构造为 (a,d,g)(b,e,h)(c,f,1) 的转置
12518
+ // = (a,b,c)(d,e,f)(g,h,1)。排布差一个转置,perspective/水波等全部错位——
12519
+ // 3174556087 的音谱柱被贴到窗户侧边竖排(应为贴下窗沿横排)实测确认。
12520
+ // 2026-09-05 数值模拟:旧排布 transpose(S⁻¹)·corner 与正确 S⁻¹·corner 逐项不同,
12521
+ // 可见区塌缩成一条斜带;新排布后中心 (0.5,0.5) → (0.478,0.511) ∈ [0,1]²。
12522
+ return mat3(a, b, c,
12523
+ d, e, f,
12524
+ g, h, 1.0);
11424
12525
  }
11425
12526
  `,
11426
12527
  // WE common_blur.h(重建)。blurNa 的权重不是估算的 —— 壁纸 1444077782 里存着
@@ -11550,6 +12651,66 @@ vec3 DecompressNormal(vec4 tex) {
11550
12651
  "common_vertex.h": `// WE common_vertex.h(重建:空占位,见 headers.ts 注释)
11551
12652
  `
11552
12653
  };
12654
+ function sanitizeFontForBrowser(src) {
12655
+ if (!src || src.length < 12) return src;
12656
+ const b0 = src[0], b1 = src[1], b2 = src[2], b3 = src[3];
12657
+ const isTtf = b0 === 0 && b1 === 1 && b2 === 0 && b3 === 0;
12658
+ const isOtto = b0 === 79 && b1 === 84 && b2 === 84 && b3 === 79;
12659
+ if (!isTtf && !isOtto) return src;
12660
+ const out = Uint8Array.from(src);
12661
+ const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
12662
+ const numTables = dv.getUint16(4);
12663
+ if (numTables <= 0 || 12 + numTables * 16 > out.length) return src;
12664
+ let cmapEntry = -1;
12665
+ let cmapOffset = 0;
12666
+ let cmapLength = 0;
12667
+ for (let i = 0; i < numTables; i++) {
12668
+ const e = 12 + i * 16;
12669
+ const tag = String.fromCharCode(out[e], out[e + 1], out[e + 2], out[e + 3]);
12670
+ if (tag === "cmap") {
12671
+ cmapEntry = e;
12672
+ cmapOffset = dv.getUint32(e + 8);
12673
+ cmapLength = dv.getUint32(e + 12);
12674
+ break;
12675
+ }
12676
+ }
12677
+ if (cmapEntry < 0 || cmapOffset + cmapLength > out.length) return src;
12678
+ let changed = false;
12679
+ const numEnc = dv.getUint16(cmapOffset + 2);
12680
+ for (let i = 0; i < numEnc; i++) {
12681
+ const rec = cmapOffset + 4 + i * 8;
12682
+ const soff = dv.getUint32(rec + 4);
12683
+ const abs = cmapOffset + soff;
12684
+ if (abs + 14 > out.length) continue;
12685
+ if (dv.getUint16(abs) !== 4) continue;
12686
+ const segCountX2 = dv.getUint16(abs + 6);
12687
+ const segCount = segCountX2 >>> 1;
12688
+ if (segCount < 1) continue;
12689
+ const expSearch = 2 * Math.pow(2, Math.floor(Math.log2(segCount)));
12690
+ const expSel = Math.floor(Math.log2(segCount));
12691
+ const expShift = segCountX2 - expSearch;
12692
+ const curSearch = dv.getUint16(abs + 8);
12693
+ const curSel = dv.getUint16(abs + 10);
12694
+ const curShift = dv.getUint16(abs + 12);
12695
+ if (curSearch === expSearch && curSel === expSel && curShift === expShift) continue;
12696
+ dv.setUint16(abs + 8, expSearch);
12697
+ dv.setUint16(abs + 10, expSel);
12698
+ dv.setUint16(abs + 12, expShift);
12699
+ changed = true;
12700
+ }
12701
+ if (!changed) return src;
12702
+ let sum = 0;
12703
+ const end = cmapOffset + cmapLength;
12704
+ for (let p = cmapOffset; p < end; p += 4) {
12705
+ const b02 = out[p] || 0;
12706
+ const b12 = p + 1 < end ? out[p + 1] : 0;
12707
+ const b22 = p + 2 < end ? out[p + 2] : 0;
12708
+ const b32 = p + 3 < end ? out[p + 3] : 0;
12709
+ sum = sum + (b02 << 24 | b12 << 16 | b22 << 8 | b32) >>> 0;
12710
+ }
12711
+ dv.setUint32(cmapEntry + 4, sum);
12712
+ return out;
12713
+ }
11553
12714
  const SYSTEM_FONT_FAMILIES = {
11554
12715
  systemfont_segoe: "'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
11555
12716
  systemfont_arial: "Arial, 'Helvetica Neue', sans-serif",
@@ -11570,7 +12731,48 @@ const SYSTEM_FONT_FAMILIES = {
11570
12731
  systemfont_simhei: "SimHei, 'Heiti SC', sans-serif"
11571
12732
  };
11572
12733
  const fontFaceCache = /* @__PURE__ */ new Map();
12734
+ function fontKeyHash(key) {
12735
+ let h = 5381;
12736
+ for (let i = 0; i < key.length; i++) h = (h << 5) + h + key.charCodeAt(i) | 0;
12737
+ return (h >>> 0).toString(36);
12738
+ }
12739
+ function releaseFontFaces(keys) {
12740
+ for (const key of keys) {
12741
+ const entry = fontFaceCache.get(key);
12742
+ if (!entry) continue;
12743
+ entry.refs--;
12744
+ if (entry.refs > 0) continue;
12745
+ fontFaceCache.delete(key);
12746
+ try {
12747
+ const dead = [];
12748
+ document.fonts.forEach((f) => {
12749
+ if (f.family === entry.family) dead.push(f);
12750
+ });
12751
+ for (const f of dead) document.fonts.delete(f);
12752
+ } catch {
12753
+ }
12754
+ }
12755
+ }
11573
12756
  const pkgCache = /* @__PURE__ */ new Map();
12757
+ const PKG_CACHE_MAX_BYTES = 512 * 1024 * 1024;
12758
+ let pkgCacheBytes = 0;
12759
+ function pkgCacheEvict(currentKey) {
12760
+ while (pkgCache.size > 0 && (pkgCache.size > 2 || pkgCacheBytes > PKG_CACHE_MAX_BYTES)) {
12761
+ let oldestKey = null;
12762
+ let oldestAt = Infinity;
12763
+ for (const [k, v] of pkgCache) {
12764
+ if (k === currentKey) continue;
12765
+ if (v.at < oldestAt) {
12766
+ oldestAt = v.at;
12767
+ oldestKey = k;
12768
+ }
12769
+ }
12770
+ if (!oldestKey) break;
12771
+ const victim = pkgCache.get(oldestKey);
12772
+ pkgCacheBytes -= victim.parsed.fileSize || 0;
12773
+ pkgCache.delete(oldestKey);
12774
+ }
12775
+ }
11574
12776
  async function loadParsedPkg(rt, cfg, source, signal) {
11575
12777
  const cacheKey = source.key;
11576
12778
  if (cacheKey) {
@@ -11593,38 +12795,40 @@ async function loadParsedPkg(rt, cfg, source, signal) {
11593
12795
  const parsed = pkg.parsePkg(bytes);
11594
12796
  if (!cacheKey) return parsed;
11595
12797
  pkgCache.set(cacheKey, { parsed, at: Date.now() });
11596
- if (pkgCache.size > 2) {
11597
- let oldestKey = null;
11598
- let oldestAt = Infinity;
11599
- for (const [k, v] of pkgCache) {
11600
- if (k === cacheKey) continue;
11601
- if (v.at < oldestAt) {
11602
- oldestAt = v.at;
11603
- oldestKey = k;
11604
- }
11605
- }
11606
- if (oldestKey) pkgCache.delete(oldestKey);
11607
- }
12798
+ pkgCacheBytes += parsed.fileSize || 0;
12799
+ pkgCacheEvict(cacheKey);
11608
12800
  return parsed;
11609
12801
  }
11610
12802
  function mountScene(rt, cfg) {
11611
12803
  clear(rt);
11612
- const c = cfg.canvas ?? document.createElement("canvas");
12804
+ const c = (cfg.canvas instanceof HTMLCanvasElement ? cfg.canvas : null) ?? document.createElement("canvas");
11613
12805
  const dpr = effectiveDpr(rt, cfg);
11614
12806
  const vw = c.clientWidth || window.innerWidth || 1;
11615
12807
  const vh = c.clientHeight || window.innerHeight || 1;
11616
12808
  c.width = Math.max(1, Math.round(vw * dpr));
11617
12809
  c.height = Math.max(1, Math.round(vh * dpr));
11618
- if (!cfg.canvas) {
12810
+ if (!(cfg.canvas instanceof HTMLCanvasElement)) {
11619
12811
  c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11620
12812
  rt.wrap?.appendChild(c);
11621
12813
  }
11622
12814
  rt.canvas = c;
11623
12815
  let disposed = false;
12816
+ const origWarn = console.warn.bind(console);
12817
+ console.warn = (...args) => {
12818
+ const s = args.map((a) => typeof a === "string" ? a : String(a?.message ?? a)).join(" ");
12819
+ if (s.includes("[we-scene]")) {
12820
+ try {
12821
+ reportDiag(rt, cfg, s.slice(0, 300));
12822
+ } catch {
12823
+ }
12824
+ }
12825
+ origWarn(...args);
12826
+ };
11624
12827
  const pkgAbort = new AbortController();
11625
12828
  let particleCleanup;
11626
12829
  rt.sceneCleanup = () => {
11627
12830
  disposed = true;
12831
+ console.warn = origWarn;
11628
12832
  pkgAbort.abort();
11629
12833
  rt.sceneTextUpdate = void 0;
11630
12834
  if (particleCleanup) {
@@ -11681,6 +12885,11 @@ function mountScene(rt, cfg) {
11681
12885
  const sceneEntry = pkg.getEntry(parsedPkg, "scene.json");
11682
12886
  if (!sceneEntry) throw new Error("pkg 中没有 scene.json(不是场景壁纸?)");
11683
12887
  const scene = scn.parseScene(JSON.parse(readText(sceneEntry)), project);
12888
+ if (cfg.clearColor) {
12889
+ const g = scene.general ??= {};
12890
+ g.clearcolor = cfg.clearColor;
12891
+ g.clearenabled = true;
12892
+ }
11684
12893
  {
11685
12894
  const zRaw = scene.general?.zoom;
11686
12895
  const zVal = zRaw && typeof zRaw === "object" ? Number(zRaw.value) : Number(zRaw);
@@ -11751,6 +12960,22 @@ function mountScene(rt, cfg) {
11751
12960
  if (disposed) return;
11752
12961
  const supportsAudioProcessing = project?.general?.supportsaudioprocessing !== false;
11753
12962
  const simAudio = createSimulatedAudio();
12963
+ const simMedia = media.createSimulatedMedia();
12964
+ const simWindow = system.createSimulatedWindowTitle();
12965
+ let live = null;
12966
+ const liveHold = {
12967
+ mediaDriver: null,
12968
+ lastSnap: {
12969
+ get: () => null,
12970
+ setHasThumbnail: () => {
12971
+ }
12972
+ }
12973
+ };
12974
+ const audioDriverRef = {
12975
+ current: null
12976
+ };
12977
+ let mediaDriver = simMedia;
12978
+ let windowDriver = simWindow;
11754
12979
  const audioSim = { enabled: supportsAudioProcessing };
11755
12980
  const zero = (n) => new Float32Array(n);
11756
12981
  const SILENT_AUDIO = {
@@ -11763,32 +12988,108 @@ function mountScene(rt, cfg) {
11763
12988
  level: 0,
11764
12989
  silent: true
11765
12990
  };
11766
- renderer.setAudioProvider(() => audioSim.enabled ? simAudio.snapshot : SILENT_AUDIO);
12991
+ const hostAudio = (() => {
12992
+ const snapshot = {
12993
+ left64: zero(64),
12994
+ right64: zero(64),
12995
+ left32: zero(32),
12996
+ right32: zero(32),
12997
+ left16: zero(16),
12998
+ right16: zero(16),
12999
+ // 未钳位频谱:网页驱动会对它做 gamma 对比扩展。宿主给的已是 0..1
13000
+ // 归一化值,没有 pre-GAIN 概念,直接与 left64/right64 共用同一份数据
13001
+ preL64: zero(64),
13002
+ preR64: zero(64),
13003
+ level: 0,
13004
+ silent: true
13005
+ };
13006
+ const down = (dst, src) => {
13007
+ const g = src.length / dst.length;
13008
+ for (let i = 0; i < dst.length; i++) {
13009
+ let s = 0;
13010
+ const i0 = Math.floor(i * g);
13011
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
13012
+ for (let j = i0; j < i1; j++) s += src[j];
13013
+ dst[i] = s / (i1 - i0);
13014
+ }
13015
+ };
13016
+ return {
13017
+ active: false,
13018
+ snapshot,
13019
+ /** 每帧从宿主拉一次。宿主返回 null(未采集/无权限)时置 active=false 回落模拟源 */
13020
+ pump() {
13021
+ const src = rt.audioBridge?.();
13022
+ if (!src || !src.left || !src.right) {
13023
+ this.active = false;
13024
+ return;
13025
+ }
13026
+ const n = Math.min(64, src.left.length, src.right.length);
13027
+ let sum = 0;
13028
+ for (let i = 0; i < n; i++) {
13029
+ const l = src.left[i] || 0;
13030
+ const r = src.right[i] || 0;
13031
+ snapshot.left64[i] = l;
13032
+ snapshot.right64[i] = r;
13033
+ snapshot.preL64[i] = l;
13034
+ snapshot.preR64[i] = r;
13035
+ if (i < 48) sum += l;
13036
+ }
13037
+ for (let i = n; i < 64; i++) {
13038
+ snapshot.left64[i] = 0;
13039
+ snapshot.right64[i] = 0;
13040
+ snapshot.preL64[i] = 0;
13041
+ snapshot.preR64[i] = 0;
13042
+ }
13043
+ down(snapshot.left32, snapshot.left64);
13044
+ down(snapshot.right32, snapshot.right64);
13045
+ down(snapshot.left16, snapshot.left64);
13046
+ down(snapshot.right16, snapshot.right64);
13047
+ snapshot.level = Math.min(1, sum / 48);
13048
+ snapshot.silent = snapshot.level < 0.02;
13049
+ this.active = true;
13050
+ }
13051
+ };
13052
+ })();
13053
+ const activeAudioSnapshot = () => hostAudio.active ? hostAudio.snapshot : audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
13054
+ renderer.setAudioProvider(() => {
13055
+ if (!audioSim.enabled) return SILENT_AUDIO;
13056
+ return activeAudioSnapshot();
13057
+ });
11767
13058
  const audioViews = /* @__PURE__ */ new Map();
11768
13059
  window.__audioStats = () => ({
11769
13060
  enabled: audioSim.enabled,
11770
- level: audioSim.enabled ? Math.round(simAudio.snapshot.level * 1e3) / 1e3 : 0,
11771
- silent: audioSim.enabled ? simAudio.snapshot.silent : true,
11772
- bass: audioSim.enabled ? Math.round(simAudio.snapshot.left64[2] * 1e3) / 1e3 : 0
13061
+ live: !!audioDriverRef.current,
13062
+ level: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).level * 1e3) / 1e3 : 0,
13063
+ silent: audioSim.enabled ? (audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).silent : true,
13064
+ bass: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).left64[2] * 1e3) / 1e3 : 0
11773
13065
  });
11774
13066
  window.__audioMute = (on) => {
11775
13067
  audioSim.enabled = !on;
11776
13068
  return audioSim.enabled;
11777
13069
  };
11778
- reportDiag(rt, cfg, `audio: simulated stream, supportsaudioprocessing=${supportsAudioProcessing}`);
11779
- const simMedia = media.createSimulatedMedia();
11780
- const simWindow = system.createSimulatedWindowTitle();
13070
+ reportDiag(
13071
+ rt,
13072
+ cfg,
13073
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13074
+ );
11781
13075
  const shortcuts = system.createShortcutHandler((name) => {
11782
13076
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
11783
13077
  });
11784
13078
  const mediaSim = { enabled: true, override: null };
11785
13079
  const mediaHooks = [];
11786
13080
  let lastMediaSnap = null;
13081
+ liveHold.lastSnap = {
13082
+ get: () => lastMediaSnap,
13083
+ setHasThumbnail: (v) => {
13084
+ if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
13085
+ }
13086
+ };
13087
+ const mediaSnapshot = () => mediaDriver.snapshot;
11787
13088
  const registerMediaHook = (sb) => {
11788
13089
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
11789
13090
  mediaHooks.push(sb);
11790
- if (!simMedia.snapshot.hasMedia) return;
11791
- for (const { name, event } of media.diffMediaEvents(null, simMedia.snapshot)) {
13091
+ if (!mediaSnapshot().hasMedia) return;
13092
+ for (const { name, event } of media.diffMediaEvents(null, mediaSnapshot())) {
11792
13093
  try {
11793
13094
  sb.callMedia(name, event);
11794
13095
  } catch {
@@ -11797,26 +13098,27 @@ function mountScene(rt, cfg) {
11797
13098
  };
11798
13099
  window.__mediaStats = () => ({
11799
13100
  enabled: mediaSim.enabled,
13101
+ live: !!live,
11800
13102
  hooks: mediaHooks.length,
11801
- title: simMedia.snapshot.title,
11802
- artist: simMedia.snapshot.artist,
11803
- album: simMedia.snapshot.album,
11804
- state: simMedia.snapshot.state,
11805
- position: Math.round(simMedia.snapshot.position),
11806
- duration: simMedia.snapshot.duration,
11807
- hasThumbnail: simMedia.snapshot.hasThumbnail,
11808
- lyric: simMedia.snapshot.lyricLine,
11809
- primaryColor: simMedia.snapshot.primaryColor ? [simMedia.snapshot.primaryColor.x, simMedia.snapshot.primaryColor.y, simMedia.snapshot.primaryColor.z] : null
13103
+ title: mediaSnapshot().title,
13104
+ artist: mediaSnapshot().artist,
13105
+ album: mediaSnapshot().album,
13106
+ state: mediaSnapshot().state,
13107
+ position: Math.round(mediaSnapshot().position),
13108
+ duration: mediaSnapshot().duration,
13109
+ hasThumbnail: mediaSnapshot().hasThumbnail,
13110
+ lyric: mediaSnapshot().lyricLine,
13111
+ primaryColor: mediaSnapshot().primaryColor ? [mediaSnapshot().primaryColor.x, mediaSnapshot().primaryColor.y, mediaSnapshot().primaryColor.z] : null
11810
13112
  });
11811
13113
  window.__mediaSet = (patch) => {
11812
- Object.assign(simMedia.snapshot, patch || {});
11813
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
13114
+ Object.assign(mediaSnapshot(), patch || {});
13115
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11814
13116
  for (const { name, event } of evts) for (const sb of mediaHooks) sb.callMedia(name, event);
11815
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
13117
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11816
13118
  return window.__mediaStats();
11817
13119
  };
11818
13120
  const dispatchMediaNow = () => {
11819
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
13121
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11820
13122
  for (const { name, event } of evts) {
11821
13123
  for (const sb of mediaHooks) {
11822
13124
  try {
@@ -11825,42 +13127,50 @@ function mountScene(rt, cfg) {
11825
13127
  }
11826
13128
  }
11827
13129
  }
11828
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
13130
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11829
13131
  };
11830
13132
  const mediaControl = {
11831
- snapshot: simMedia.snapshot,
13133
+ get snapshot() {
13134
+ return mediaSnapshot();
13135
+ },
11832
13136
  skipNext: () => {
11833
- simMedia.skipNext();
13137
+ mediaDriver.skipNext();
11834
13138
  dispatchMediaNow();
11835
- return simMedia.snapshot;
13139
+ return mediaSnapshot();
11836
13140
  },
11837
13141
  skipPrevious: () => {
11838
- simMedia.skipPrevious();
13142
+ mediaDriver.skipPrevious();
11839
13143
  dispatchMediaNow();
11840
- return simMedia.snapshot;
13144
+ return mediaSnapshot();
11841
13145
  },
11842
13146
  play: () => {
11843
- simMedia.play();
13147
+ mediaDriver.play();
11844
13148
  dispatchMediaNow();
11845
- return simMedia.snapshot;
13149
+ return mediaSnapshot();
11846
13150
  },
11847
13151
  pause: () => {
11848
- simMedia.pause();
13152
+ mediaDriver.pause();
11849
13153
  dispatchMediaNow();
11850
- return simMedia.snapshot;
13154
+ return mediaSnapshot();
11851
13155
  },
11852
13156
  playPause: () => {
11853
- simMedia.playPause();
13157
+ mediaDriver.playPause();
11854
13158
  dispatchMediaNow();
11855
- return simMedia.snapshot;
13159
+ return mediaSnapshot();
11856
13160
  }
11857
13161
  };
11858
13162
  window.__mediaControl = mediaControl;
11859
13163
  window.__system = {
11860
13164
  media: mediaControl,
11861
- windowTitle: simWindow.snapshot,
11862
- shortcuts: shortcuts.last
13165
+ windowTitle: windowDriver.snapshot,
13166
+ shortcuts: shortcuts.last,
13167
+ live: null
11863
13168
  };
13169
+ window.__liveSystem = () => ({
13170
+ audio: "off",
13171
+ media: "offline",
13172
+ window: "offline"
13173
+ });
11864
13174
  const pointerSrc = pointerLib.createPointerSource(
11865
13175
  cfg.canvas ? {
11866
13176
  target: cfg.canvas,
@@ -11871,6 +13181,10 @@ function mountScene(rt, cfg) {
11871
13181
  } : {}
11872
13182
  );
11873
13183
  renderer.setPointerProvider(() => pointerSrc);
13184
+ rt.pointerCtl = {
13185
+ push: (p) => pointerSrc.pushExternal(p),
13186
+ leave: () => pointerSrc.pushExternalLeave()
13187
+ };
11874
13188
  {
11875
13189
  const prevCleanup = particleCleanup;
11876
13190
  particleCleanup = () => {
@@ -11964,6 +13278,118 @@ function mountScene(rt, cfg) {
11964
13278
  textures.set("$mediaPreviousThumbnail", mkThumb(tracks[tracks.length - 1]));
11965
13279
  }
11966
13280
  }
13281
+ if (cfg.liveSystem) {
13282
+ try {
13283
+ const uploadLiveArtwork = async (info) => {
13284
+ try {
13285
+ const res = await fetch(info.url, { cache: "no-store" });
13286
+ if (!res.ok) return;
13287
+ const blob = await res.blob();
13288
+ const bmp = await createImageBitmap(blob);
13289
+ const raster = rasterizeArtwork(bmp, bmp.width, bmp.height, 512);
13290
+ const palette = sampleArtworkPalette(bmp, bmp.width, bmp.height);
13291
+ bmp.close?.();
13292
+ const cur = textures.get("$mediaThumbnail");
13293
+ if (cur) textures.set("$mediaPreviousThumbnail", cur);
13294
+ const gl = renderer.gl;
13295
+ const existing = textures.get("$mediaThumbnail");
13296
+ if (existing?.glTex) {
13297
+ gl.bindTexture(gl.TEXTURE_2D, existing.glTex);
13298
+ gl.texImage2D(
13299
+ gl.TEXTURE_2D,
13300
+ 0,
13301
+ gl.RGBA,
13302
+ raster.width,
13303
+ raster.height,
13304
+ 0,
13305
+ gl.RGBA,
13306
+ gl.UNSIGNED_BYTE,
13307
+ raster.rgba
13308
+ );
13309
+ existing.width = raster.width;
13310
+ existing.height = raster.height;
13311
+ existing.mips = [raster];
13312
+ } else {
13313
+ textures.set("$mediaThumbnail", {
13314
+ glTex: rnd.makeTextureMip(gl, [raster], false),
13315
+ width: raster.width,
13316
+ height: raster.height,
13317
+ rg88: false,
13318
+ mips: [raster],
13319
+ generated: true
13320
+ });
13321
+ }
13322
+ const snap = mediaDriver.snapshot;
13323
+ if (palette) {
13324
+ snap.primaryColor = media.mediaVec3(...palette.primary);
13325
+ snap.secondaryColor = media.mediaVec3(...palette.secondary);
13326
+ snap.tertiaryColor = media.mediaVec3(...palette.tertiary);
13327
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
13328
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
13329
+ }
13330
+ snap.hasThumbnail = true;
13331
+ liveHold.lastSnap.setHasThumbnail(false);
13332
+ reportDiag(rt, cfg, `liveSystem: artwork ${info.title || info.trackKey}`);
13333
+ } catch (e) {
13334
+ reportDiag(
13335
+ rt,
13336
+ cfg,
13337
+ `liveSystem: artwork 失败 (${e instanceof Error ? e.message : e})`
13338
+ );
13339
+ }
13340
+ };
13341
+ live = await startLiveSystem({
13342
+ origin: location.origin,
13343
+ onArtwork: (info) => {
13344
+ void uploadLiveArtwork(info);
13345
+ }
13346
+ });
13347
+ mediaDriver = live.media;
13348
+ windowDriver = live.windowTitle;
13349
+ liveHold.mediaDriver = live.media;
13350
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13351
+ if (mediaDriver.snapshot.hasMedia) {
13352
+ for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
13353
+ for (const sb of mediaHooks) {
13354
+ try {
13355
+ sb.callMedia(name, event);
13356
+ } catch {
13357
+ }
13358
+ }
13359
+ }
13360
+ lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
13361
+ }
13362
+ const st = live.status();
13363
+ reportDiag(
13364
+ rt,
13365
+ cfg,
13366
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13367
+ );
13368
+ reportDiag(
13369
+ rt,
13370
+ cfg,
13371
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13372
+ );
13373
+ window.__system = {
13374
+ media: mediaControl,
13375
+ windowTitle: windowDriver.snapshot,
13376
+ shortcuts: shortcuts.last,
13377
+ live: () => live.status()
13378
+ };
13379
+ window.__liveSystem = () => live.status();
13380
+ } catch (e) {
13381
+ reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
13382
+ live = null;
13383
+ }
13384
+ }
13385
+ {
13386
+ const prevCleanup = particleCleanup;
13387
+ particleCleanup = () => {
13388
+ prevCleanup?.();
13389
+ live?.dispose();
13390
+ live = null;
13391
+ };
13392
+ }
11967
13393
  const texInflight = /* @__PURE__ */ new Map();
11968
13394
  const loadTexInner = async (name) => {
11969
13395
  if (textures.has(name)) return textures.get(name);
@@ -12155,6 +13581,13 @@ function mountScene(rt, cfg) {
12155
13581
  model = JSON.parse(readText(modelEntry));
12156
13582
  }
12157
13583
  scn.applySolidFromModel(layer, model);
13584
+ const instUt = layer.srcObject?.instance?.usertextures;
13585
+ const instUtName = instUt?.[0] && typeof instUt[0].name === "string" && instUt[0].name.startsWith("$") ? instUt[0].name : null;
13586
+ const instBoundTex = instUtName && textures.has(instUtName) ? instUtName : null;
13587
+ if (instBoundTex) {
13588
+ layer.textureName = instBoundTex;
13589
+ layer.solid = false;
13590
+ }
12158
13591
  if (model && typeof model === "object" && "width" in model && "height" in model) {
12159
13592
  const m = model;
12160
13593
  if ((layer.size?.[0] || 0) === 0 && (layer.size?.[1] || 0) === 0 && m.width > 0 && m.height > 0) {
@@ -12195,7 +13628,7 @@ function mountScene(rt, cfg) {
12195
13628
  texJobs.push(
12196
13629
  loadTex(tn).then((entry) => {
12197
13630
  if (!entry) return;
12198
- if (si === 0) {
13631
+ if (si === 0 && !instBoundTex) {
12199
13632
  layer.textureName = tn;
12200
13633
  loadedTex++;
12201
13634
  if (entry.videoCtl) layer.videoCtl = entry.videoCtl;
@@ -12253,6 +13686,7 @@ function mountScene(rt, cfg) {
12253
13686
  if (usedVisible && !rt.paused) texEntry.videoCtl.play();
12254
13687
  }
12255
13688
  const particleSystems = [];
13689
+ const particleDirty = [];
12256
13690
  const particleSystemsByLayer = /* @__PURE__ */ new Map();
12257
13691
  let builtinTexCount = 0;
12258
13692
  const loadParticleTex = async (name) => {
@@ -12266,7 +13700,10 @@ function mountScene(rt, cfg) {
12266
13700
  height: gen.height,
12267
13701
  rg88: false,
12268
13702
  mips: [gen],
12269
- generated: true
13703
+ generated: true,
13704
+ // 内置贴图的帧表(rain1/rain2 的 1×4 图集):randomframe 预设依赖它
13705
+ // 随机取帧,缺了就整图采样画出超长丝(1823900922)。
13706
+ frames: ptex.builtinParticleFrames(name) ?? void 0
12270
13707
  };
12271
13708
  textures.set(name, entry);
12272
13709
  builtinTexCount++;
@@ -12468,12 +13905,15 @@ function mountScene(rt, cfg) {
12468
13905
  const py = originY != null ? originY : wy;
12469
13906
  for (const ps of particleSystems) ps.setPointer(wx, py);
12470
13907
  }
12471
- for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? simAudio.snapshot : null);
13908
+ if (particleDirty.length) {
13909
+ for (const ps of particleDirty) ps.syncLayerTransform();
13910
+ }
13911
+ for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? activeAudioSnapshot() : null);
12472
13912
  if (particleDiagFrame < 2) {
12473
13913
  particleDiagFrame++;
12474
13914
  if (particleDiagFrame === 2) {
12475
- const live = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
12476
- reportDiag(rt, cfg, `particles live: ${live} across ${particleSystems.length} systems`);
13915
+ const live2 = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
13916
+ reportDiag(rt, cfg, `particles live: ${live2} across ${particleSystems.length} systems`);
12477
13917
  }
12478
13918
  }
12479
13919
  },
@@ -12492,7 +13932,7 @@ function mountScene(rt, cfg) {
12492
13932
  `particles: ${particleSystems.length} systems, ${builtinTexCount} builtin tex generated`
12493
13933
  );
12494
13934
  window.__particleStats = () => particleSystems.map((ps) => {
12495
- let live = 0;
13935
+ let live2 = 0;
12496
13936
  let minX = Infinity;
12497
13937
  let maxX = -Infinity;
12498
13938
  let minY = Infinity;
@@ -12501,7 +13941,7 @@ function mountScene(rt, cfg) {
12501
13941
  let maxS = -Infinity;
12502
13942
  for (const p of ps.pool) {
12503
13943
  if (!p.alive) continue;
12504
- live++;
13944
+ live2++;
12505
13945
  const px = ps.originX + p.x * ps.scaleX;
12506
13946
  const py = ps.originY + p.y * ps.scaleY;
12507
13947
  if (px < minX) minX = px;
@@ -12513,13 +13953,13 @@ function mountScene(rt, cfg) {
12513
13953
  if (s > maxS) maxS = s;
12514
13954
  }
12515
13955
  return {
12516
- live,
13956
+ live: live2,
12517
13957
  max: ps.maxCount,
12518
13958
  blend: ps.blend,
12519
13959
  renderer: ps.renderers.map((r) => r.kind).join("+"),
12520
13960
  origin: [Math.round(ps.originX), Math.round(ps.originY)],
12521
- bbox: live ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
12522
- size: live ? [Math.round(minS), Math.round(maxS)] : null
13961
+ bbox: live2 ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
13962
+ size: live2 ? [Math.round(minS), Math.round(maxS)] : null
12523
13963
  };
12524
13964
  });
12525
13965
  window.__particleToggle = (on, onlyIndex) => {
@@ -12653,6 +14093,17 @@ function mountScene(rt, cfg) {
12653
14093
  if (attachFollows.length) {
12654
14094
  reportDiag(rt, cfg, `attachments: ${attachFollows.length} hanging layers`);
12655
14095
  }
14096
+ const transformDirty = scn.collectTransformDirty(
14097
+ scene.layers,
14098
+ attachFollows.map((f) => f.layer)
14099
+ );
14100
+ if (transformDirty.size) {
14101
+ reportDiag(rt, cfg, `transform graph: ${transformDirty.size} live layers`);
14102
+ for (const [lid, list] of particleSystemsByLayer) {
14103
+ if (!transformDirty.has(lid)) continue;
14104
+ for (const ps of list) particleDirty.push(ps);
14105
+ }
14106
+ }
12656
14107
  const textWidgets = [];
12657
14108
  const textLayerText = /* @__PURE__ */ new Map();
12658
14109
  const textShared = {};
@@ -12666,15 +14117,19 @@ function mountScene(rt, cfg) {
12666
14117
  const win0 = fitWindow(normalizeFit(rt.cfg.fit), projW, projH, c.width, c.height);
12667
14118
  const quality = Math.min(3, Math.max(0.5, c.width / Math.max(1, win0.viewW)));
12668
14119
  const fontFamilies = /* @__PURE__ */ new Map();
14120
+ const usedFontKeys = [];
12669
14121
  const fontPaths = /* @__PURE__ */ new Set();
12670
14122
  for (const l of scene.layers) if (l.isText && l.textFont) fontPaths.add(l.textFont);
12671
14123
  for (const e of parsedPkg.entries || []) {
12672
14124
  if (typeof e.name === "string" && /^fonts\/.+\.(ttf|otf|woff2?)$/i.test(e.name)) fontPaths.add(e.name);
12673
14125
  }
12674
14126
  for (const fp of fontPaths) {
12675
- const cached = fontFaceCache.get(`${cfg.src}|${fp}`);
14127
+ const key = `${cfg.src}|${fp}`;
14128
+ const cached = fontFaceCache.get(key);
12676
14129
  if (cached) {
12677
- fontFamilies.set(fp, cached);
14130
+ cached.refs++;
14131
+ usedFontKeys.push(key);
14132
+ fontFamilies.set(fp, cached.family);
12678
14133
  continue;
12679
14134
  }
12680
14135
  const sys = SYSTEM_FONT_FAMILIES[fp.toLowerCase()];
@@ -12685,18 +14140,33 @@ function mountScene(rt, cfg) {
12685
14140
  try {
12686
14141
  const fe = pkg.getEntry(parsedPkg, fp);
12687
14142
  if (!fe) continue;
12688
- const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
12689
- const url = URL.createObjectURL(new Blob([fe]));
14143
+ const bytes = sanitizeFontForBrowser(
14144
+ fe instanceof Uint8Array ? fe : new Uint8Array(fe)
14145
+ );
14146
+ const fam = "wefont_" + fontKeyHash(key) + "_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
14147
+ const url = URL.createObjectURL(new Blob([bytes]));
12690
14148
  const ff = new FontFace(fam, `url(${url})`);
12691
14149
  await ff.load();
14150
+ if (disposed) {
14151
+ URL.revokeObjectURL(url);
14152
+ break;
14153
+ }
12692
14154
  document.fonts.add(ff);
12693
14155
  (rt.objectUrls ??= []).push(url);
12694
14156
  fontFamilies.set(fp, fam);
12695
- fontFaceCache.set(`${cfg.src}|${fp}`, fam);
14157
+ fontFaceCache.set(key, { family: fam, refs: 1 });
14158
+ usedFontKeys.push(key);
12696
14159
  } catch (e) {
12697
14160
  console.warn(`字体加载失败 ${fp}: ${e.message}`);
12698
14161
  }
12699
14162
  }
14163
+ if (usedFontKeys.length) {
14164
+ const prevCleanup = rt.sceneCleanup;
14165
+ rt.sceneCleanup = () => {
14166
+ releaseFontFaces(usedFontKeys);
14167
+ prevCleanup?.();
14168
+ };
14169
+ }
12700
14170
  textCanvas = document.createElement("canvas");
12701
14171
  textCtx = textCanvas.getContext("2d");
12702
14172
  const MAX_TEX = 2048;
@@ -12730,7 +14200,7 @@ function mountScene(rt, cfg) {
12730
14200
  // 与对象/效果开关/常量/general 统一走 engineTimers(P1-1)。
12731
14201
  ...timerOpts,
12732
14202
  mediaControl,
12733
- windowTitle: simWindow.snapshot,
14203
+ windowTitle: windowDriver.snapshot,
12734
14204
  openUserShortcut: shortcuts.openUserShortcut,
12735
14205
  getLayerText: (name) => textLayerText.get(name),
12736
14206
  onError: (e) => {
@@ -12749,10 +14219,18 @@ function mountScene(rt, cfg) {
12749
14219
  const hw = layer.size[0] * (layer.scale[0] || 1) / 2;
12750
14220
  const hh = layer.size[1] * (layer.scale[1] || 1) / 2;
12751
14221
  const a = layer.textAnchor;
12752
- if (a.includes("left")) layer.origin[0] += hw;
12753
- if (a.includes("right")) layer.origin[0] -= hw;
12754
- if (a.includes("top")) layer.origin[1] -= hh;
12755
- if (a.includes("bottom")) layer.origin[1] += hh;
14222
+ let adx = 0;
14223
+ let ady = 0;
14224
+ if (a.includes("left")) adx += hw;
14225
+ if (a.includes("right")) adx -= hw;
14226
+ if (a.includes("top")) ady -= hh;
14227
+ if (a.includes("bottom")) ady += hh;
14228
+ layer.origin[0] += adx;
14229
+ layer.origin[1] += ady;
14230
+ if (layer.localOrigin) {
14231
+ layer.localOrigin[0] += adx;
14232
+ layer.localOrigin[1] += ady;
14233
+ }
12756
14234
  }
12757
14235
  const em0 = TEXT_EM_SCALE * Math.max(1, layer.textPointsize);
12758
14236
  const marginCap = wtext.textLayerHasTintMask(layer) ? 8 : 256;
@@ -12978,7 +14456,7 @@ function mountScene(rt, cfg) {
12978
14456
  return clone;
12979
14457
  },
12980
14458
  mediaControl,
12981
- windowTitle: simWindow.snapshot,
14459
+ windowTitle: windowDriver.snapshot,
12982
14460
  openUserShortcut: shortcuts.openUserShortcut,
12983
14461
  isScreensaver: false
12984
14462
  };
@@ -12995,6 +14473,15 @@ function mountScene(rt, cfg) {
12995
14473
  if (sb && sb.hasMediaHook) registerMediaHook(sb);
12996
14474
  }
12997
14475
  });
14476
+ const LOCAL_SLOT = {
14477
+ origin: "localOrigin",
14478
+ scale: "localScale",
14479
+ angles: "localAngles"
14480
+ };
14481
+ const fieldSlot = (layer, field) => {
14482
+ const slot = LOCAL_SLOT[field];
14483
+ return slot && layer && Array.isArray(layer[slot]) ? slot : field;
14484
+ };
12998
14485
  for (const layer of scene.layers) {
12999
14486
  const defs = layer.objectAnimations;
13000
14487
  if (!defs) continue;
@@ -13005,11 +14492,13 @@ function mountScene(rt, cfg) {
13005
14492
  const ctrl = anim.createAnimation(def.animation);
13006
14493
  ctrl.field = field;
13007
14494
  ctrl.baseValue = def.value;
13008
- const live = layer[field];
13009
- ctrl.baseNumeric = Array.isArray(live) ? live.slice() : live;
14495
+ const slot = fieldSlot(layer, field);
14496
+ const live2 = layer[slot];
14497
+ ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
14498
+ ctrl.slot = slot;
13010
14499
  layer.animationList.push(ctrl);
13011
14500
  if (ctrl.name) layer.animations[ctrl.name] = ctrl;
13012
- animRuns.push({ layer, field, ctrl });
14501
+ animRuns.push({ layer, field, slot, ctrl });
13013
14502
  } catch (e) {
13014
14503
  reportDiag(rt, cfg, `animation '${layer.name}.${field}' 建控制器失败: ${String(e.message).slice(0, 80)}`);
13015
14504
  }
@@ -13074,7 +14563,8 @@ function mountScene(rt, cfg) {
13074
14563
  });
13075
14564
  if (sandbox) {
13076
14565
  propSandboxes.push(sandbox);
13077
- const fieldVal = layer[field];
14566
+ const initSlot = fieldSlot(layer, field);
14567
+ const fieldVal = layer[initSlot];
13078
14568
  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;
13079
14569
  sandbox.init(initArg);
13080
14570
  sandbox.applyUserProperties(objUserProps);
@@ -13084,6 +14574,8 @@ function mountScene(rt, cfg) {
13084
14574
  objectScriptRuns.push({
13085
14575
  layer,
13086
14576
  field,
14577
+ // 变换字段逐帧也在 local 槽上收发(与 init 同一空间)。
14578
+ slot: initSlot,
13087
14579
  kind: field === "visible" ? "bool" : field === "alpha" || field === "brightness" ? "scalar" : "vec3",
13088
14580
  sandbox
13089
14581
  });
@@ -13147,6 +14639,7 @@ function mountScene(rt, cfg) {
13147
14639
  window.__objScripts = objectScriptRuns;
13148
14640
  }
13149
14641
  window.__mediaHooks = mediaHooks;
14642
+ window.__sceneLayers = scene.layers;
13150
14643
  window.__compositeStats = () => renderer.compositeStats?.() ?? null;
13151
14644
  window.__compositeEnable = (on) => renderer.setCompositeEnabled?.(on);
13152
14645
  window.__scene = scene;
@@ -13207,6 +14700,7 @@ function mountScene(rt, cfg) {
13207
14700
  const playingVideos = [];
13208
14701
  const playingAudios = [];
13209
14702
  let lastRender = -Infinity;
14703
+ let lastAnimT = 0;
13210
14704
  const renderLoop = (now) => {
13211
14705
  if (disposed || rt.paused) return;
13212
14706
  const fps = rt.cfg.sceneFps || 60;
@@ -13223,8 +14717,10 @@ function mountScene(rt, cfg) {
13223
14717
  const t = (now - start - pauseAccum) / 1e3;
13224
14718
  inputView.update(pointerSrc.state);
13225
14719
  if (mediaSim.enabled) {
13226
- simMedia.update(t);
13227
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
14720
+ if (live?.media) live.media.pump();
14721
+ else simMedia.update(t);
14722
+ const snap = mediaSnapshot();
14723
+ const evts = media.diffMediaEvents(lastMediaSnap, snap);
13228
14724
  if (evts.length) {
13229
14725
  for (const { name, event } of evts) {
13230
14726
  for (const sb of mediaHooks) {
@@ -13232,38 +14728,42 @@ function mountScene(rt, cfg) {
13232
14728
  sb.callMedia(name, event);
13233
14729
  }
13234
14730
  }
13235
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
14731
+ lastMediaSnap = media.cloneMediaSnapshot(snap);
13236
14732
  }
13237
14733
  }
13238
- simWindow.update(t);
14734
+ if (live?.windowTitle) live.windowTitle.pump();
14735
+ else simWindow.update(t);
14736
+ const animDt = Math.max(0, t - lastAnimT);
14737
+ lastAnimT = t;
13239
14738
  for (const run of animRuns) {
13240
- run.ctrl.advance(interval / 1e3);
14739
+ run.ctrl.advance(animDt);
13241
14740
  const field = run.field;
14741
+ const slot = run.slot || field;
13242
14742
  const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
13243
14743
  if (Array.isArray(out)) {
13244
- const cur = run.layer[field];
14744
+ const cur = run.layer[slot];
13245
14745
  if (Array.isArray(cur)) for (let i = 0; i < out.length && i < cur.length; i++) cur[i] = out[i];
13246
14746
  } else if (Number.isFinite(out)) {
13247
14747
  if (field === "visible") run.layer[field] = !!out;
13248
- else run.layer[field] = out;
14748
+ else run.layer[slot] = out;
13249
14749
  }
13250
14750
  }
13251
14751
  for (const run of generalAnimRuns) {
13252
- run.ctrl.advance(interval / 1e3);
14752
+ run.ctrl.advance(animDt);
13253
14753
  const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
13254
14754
  if (typeof out === "number" && Number.isFinite(out)) run.write(out);
13255
14755
  else if (Array.isArray(out) && Number.isFinite(out[0])) run.write(out[0]);
13256
14756
  }
13257
14757
  for (const run of effectVisibleRuns) {
13258
14758
  if (run.sandbox.disabled) continue;
13259
- run.sandbox.engine.frametime = interval / 1e3;
14759
+ run.sandbox.engine.frametime = animDt;
13260
14760
  run.sandbox.engine.runtime = t;
13261
14761
  const ret = run.sandbox.callUpdate(!!run.effect.visible);
13262
14762
  if (typeof ret === "boolean") run.effect.visible = ret;
13263
14763
  }
13264
14764
  for (const run of generalScriptRuns) {
13265
14765
  if (run.sandbox.disabled) continue;
13266
- run.sandbox.engine.frametime = interval / 1e3;
14766
+ run.sandbox.engine.frametime = animDt;
13267
14767
  run.sandbox.engine.runtime = t;
13268
14768
  const g = scene.general || {};
13269
14769
  const cur = g[run.field] && typeof g[run.field] === "object" && "value" in g[run.field] ? g[run.field].value : g[run.field];
@@ -13271,10 +14771,16 @@ function mountScene(rt, cfg) {
13271
14771
  if (ret !== void 0) run.write(ret);
13272
14772
  }
13273
14773
  const screenRes = { x: c.clientWidth || window.innerWidth || 1, y: c.clientHeight || window.innerHeight || 1 };
14774
+ for (const sb of propSandboxes) {
14775
+ if (!sb || sb.disabled) continue;
14776
+ sb.engine.frametime = animDt;
14777
+ sb.engine.runtime = t;
14778
+ sb.engine.screenResolution = screenRes;
14779
+ }
13274
14780
  let visibilityDirty = false;
13275
14781
  for (const run of objectScriptRuns) {
13276
14782
  if (run.sandbox.disabled) continue;
13277
- run.sandbox.engine.frametime = interval / 1e3;
14783
+ run.sandbox.engine.frametime = animDt;
13278
14784
  run.sandbox.engine.runtime = t;
13279
14785
  run.sandbox.engine.screenResolution = screenRes;
13280
14786
  const cur = run.layer[run.field];
@@ -13295,16 +14801,23 @@ function mountScene(rt, cfg) {
13295
14801
  const n = Number(ret);
13296
14802
  if (Number.isFinite(n)) run.layer[run.field] = n;
13297
14803
  } else {
13298
- const v = run.field === "angles" ? wtext.radToScriptAngles(cur) : { x: cur[0] || 0, y: cur[1] || 0, z: cur[2] || 0 };
14804
+ const slot = run.slot || run.field;
14805
+ const lcur = run.layer[slot];
14806
+ const v = run.field === "angles" ? wtext.radToScriptAngles(lcur) : { x: lcur[0] || 0, y: lcur[1] || 0, z: lcur[2] || 0 };
13299
14807
  const ret = run.sandbox.callUpdate(v);
13300
14808
  const o = ret && typeof ret === "object" && "x" in ret ? ret : v;
13301
- run.layer[run.field] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
14809
+ run.layer[slot] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
13302
14810
  }
13303
14811
  }
13304
14812
  if (visibilityDirty) recomputeVisibility();
14813
+ if (transformDirty.size) scn.recomposeWorld(scene.layers, transformDirty);
13305
14814
  if (audioSim.enabled) {
13306
- simAudio.update(t);
13307
- fillAudioBuffers(audioViews, simAudio.snapshot);
14815
+ hostAudio.pump();
14816
+ if (!hostAudio.active) {
14817
+ if (audioDriverRef.current) audioDriverRef.current.pump();
14818
+ else simAudio.update(t);
14819
+ }
14820
+ fillAudioBuffers(audioViews, activeAudioSnapshot());
13308
14821
  }
13309
14822
  if (attachFollows.length) {
13310
14823
  mdl.followAttachments(attachFollows, t, getBoneOverrides);
@@ -13491,11 +15004,672 @@ function mountScene(rt, cfg) {
13491
15004
  }
13492
15005
  })();
13493
15006
  }
15007
+ const SHIM_MARK = 'data-we-shim="1"';
15008
+ const SHIM_ATTR = "data-we-shim-src";
15009
+ function entryDirUrl(entryUrl) {
15010
+ try {
15011
+ const u = new URL(entryUrl);
15012
+ const path = u.pathname;
15013
+ const i = path.lastIndexOf("/");
15014
+ u.pathname = i < 0 ? "/" : path.slice(0, i + 1);
15015
+ u.hash = "";
15016
+ u.search = "";
15017
+ return u.href;
15018
+ } catch {
15019
+ const s = entryUrl.replace(/[#?].*$/, "");
15020
+ const i = s.lastIndexOf("/");
15021
+ return i < 0 ? s : s.slice(0, i + 1);
15022
+ }
15023
+ }
15024
+ function hasBlockingCsp(html) {
15025
+ const re = /<meta[^>]+http-equiv\s*=\s*["']?Content-Security-Policy["']?[^>]*>/gi;
15026
+ let m;
15027
+ while (m = re.exec(html)) {
15028
+ const tag = m[0];
15029
+ const content = /content\s*=\s*"([^"]*)"/i.exec(tag)?.[1] ?? /content\s*=\s*'([^']*)'/i.exec(tag)?.[1] ?? "";
15030
+ if (!/script-src/i.test(content)) continue;
15031
+ if (/script-src[^;]*'unsafe-inline'/i.test(content)) continue;
15032
+ if (/script-src[^;]*\*/i.test(content)) continue;
15033
+ return true;
15034
+ }
15035
+ return false;
15036
+ }
15037
+ function escapeScriptClose(js) {
15038
+ return js.replace(/<\/script/gi, "<\\/script");
15039
+ }
15040
+ function rewriteHtml(html, shimSource2, opts) {
15041
+ if (!html) html = "";
15042
+ if (html.includes(SHIM_MARK) || html.includes(SHIM_ATTR)) return html;
15043
+ const base = opts.baseHref && !/<base\b/i.test(html) ? `<base href="${opts.baseHref.replace(/"/g, "&quot;")}">` : "";
15044
+ const script = `<script ${SHIM_ATTR}="1">
15045
+ ${escapeScriptClose(shimSource2)}
15046
+ <\/script>`;
15047
+ const seed = opts.seedScript && opts.seedScript.trim() ? `<script>
15048
+ ${escapeScriptClose(opts.seedScript)}
15049
+ <\/script>` : "";
15050
+ const inject = `${base}${script}${seed}`;
15051
+ const headOpen = /<head(\s[^>]*)?>/i.exec(html);
15052
+ if (headOpen) {
15053
+ const at = headOpen.index + headOpen[0].length;
15054
+ return html.slice(0, at) + inject + html.slice(at);
15055
+ }
15056
+ const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
15057
+ if (htmlOpen) {
15058
+ const at = htmlOpen.index + htmlOpen[0].length;
15059
+ return html.slice(0, at) + `<head>${inject}</head>` + html.slice(at);
15060
+ }
15061
+ return `<!DOCTYPE html><html><head>${inject}</head><body>${html}</body></html>`;
15062
+ }
15063
+ 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';
15064
+ function weShimCall(rt, call) {
15065
+ try {
15066
+ const win = rt.iframe?.contentWindow;
15067
+ if (win) call(win);
15068
+ } catch {
15069
+ }
15070
+ }
15071
+ function injectGpuThrottle(rt, f, _doc) {
15072
+ const win = f.contentWindow;
15073
+ if (!win) return;
15074
+ if (win.requestAnimationFrame?.__weThrottled) return;
15075
+ const fps = rt.cfg.sceneFps || 30;
15076
+ if (fps >= 60) return;
15077
+ const interval = 1e3 / fps;
15078
+ try {
15079
+ const origRaf = win.requestAnimationFrame.bind(win);
15080
+ const rafMap = /* @__PURE__ */ new Map();
15081
+ let counter = 0;
15082
+ win.requestAnimationFrame = (cb) => {
15083
+ const id = ++counter;
15084
+ const to = win.setTimeout(() => {
15085
+ rafMap.delete(id);
15086
+ origRaf((now) => {
15087
+ try {
15088
+ cb(now);
15089
+ } catch {
15090
+ }
15091
+ });
15092
+ }, interval);
15093
+ rafMap.set(id, to);
15094
+ return id;
15095
+ };
15096
+ win.cancelAnimationFrame = (id) => {
15097
+ const to = rafMap.get(id);
15098
+ if (to !== void 0) {
15099
+ win.clearTimeout(to);
15100
+ rafMap.delete(id);
15101
+ }
15102
+ };
15103
+ } catch {
15104
+ }
15105
+ }
15106
+ const pumpBuffer = new Float32Array(128);
15107
+ function packWebAudioArrayInto(out, left, right) {
15108
+ const nL = Math.min(64, left.length);
15109
+ const nR = Math.min(64, right.length);
15110
+ for (let i = 0; i < nL; i++) out[i] = Number(left[i]) || 0;
15111
+ for (let i = 0; i < nR; i++) out[64 + i] = Number(right[i]) || 0;
15112
+ return out;
15113
+ }
15114
+ const WEB_SIM_AUDIO_GAIN = 1.8;
15115
+ const WEB_SIM_AUDIO_GAMMA = 1.8;
15116
+ const WEB_AUDIO_PUMP_HZ = 30;
15117
+ function shapeWebAudioBand(pre) {
15118
+ const v = Number(pre) || 0;
15119
+ if (v <= 0) return 0;
15120
+ return Math.min(1, Math.pow(v, WEB_SIM_AUDIO_GAMMA) * WEB_SIM_AUDIO_GAIN);
15121
+ }
15122
+ function defaultAudioDriver() {
15123
+ const sim = createSimulatedAudio();
15124
+ const left = new Float32Array(64);
15125
+ const right = new Float32Array(64);
15126
+ return {
15127
+ tick(nowMs) {
15128
+ sim.update(nowMs / 1e3);
15129
+ },
15130
+ snapshot() {
15131
+ const s = sim.snapshot;
15132
+ const preL = s.preL64;
15133
+ const preR = s.preR64;
15134
+ for (let i = 0; i < 64; i++) {
15135
+ if (preL && preR) {
15136
+ left[i] = shapeWebAudioBand(preL[i]);
15137
+ right[i] = shapeWebAudioBand(preR[i]);
15138
+ } else {
15139
+ left[i] = (Number(s.left64[i]) || 0) * 0.2;
15140
+ right[i] = (Number(s.right64[i]) || 0) * 0.2;
15141
+ }
15142
+ }
15143
+ return { left, right };
15144
+ }
15145
+ };
15146
+ }
15147
+ function resolveContainer(rt, cfg) {
15148
+ if (rt.wrap) return rt.wrap;
15149
+ const el = cfg.canvas;
15150
+ if (!el) return null;
15151
+ if (el instanceof HTMLCanvasElement) {
15152
+ const parent = el.parentElement;
15153
+ if (parent) {
15154
+ reportDiag(rt, cfg, "网页壁纸挂在 canvas 父容器上(canvas 不能有子节点;更适合空 div)");
15155
+ return parent;
15156
+ }
15157
+ return null;
15158
+ }
15159
+ return el;
15160
+ }
15161
+ function buildSeedScript(props, fps, volume) {
15162
+ const parts = [];
15163
+ if (fps != null && Number.isFinite(fps)) parts.push(`window.__weSetFps(${Number(fps)});`);
15164
+ if (volume != null && Number.isFinite(volume)) {
15165
+ parts.push(`window.__weSetVolume(${Math.max(0, Math.min(1, Number(volume)))});`);
15166
+ }
15167
+ if (props && Object.keys(props).length) {
15168
+ parts.push(`window.__weSeedProps(${JSON.stringify(props)});`);
15169
+ }
15170
+ return parts.join("\n");
15171
+ }
15172
+ function installLetterboxFix(rt, f, container) {
15173
+ const BASE = "position:absolute;border:none;background:transparent;";
15174
+ const applyFull = () => {
15175
+ f.style.cssText = BASE + "inset:0;width:100%;height:100%;";
15176
+ };
15177
+ applyFull();
15178
+ let lastKey = "";
15179
+ const relayout = () => {
15180
+ if (!f.isConnected) return;
15181
+ let doc = null;
15182
+ try {
15183
+ doc = f.contentDocument;
15184
+ } catch {
15185
+ return;
15186
+ }
15187
+ if (!doc) return;
15188
+ const stageW = container.clientWidth || window.innerWidth || 0;
15189
+ const stageH = container.clientHeight || window.innerHeight || 0;
15190
+ const cover = normalizeFit(rt.cfg.fit) === "cover";
15191
+ applyFull();
15192
+ const box = cover && stageW > 0 && stageH > 0 ? measureWebLetterbox(doc) : null;
15193
+ const vp = box ? webCoverViewport(stageW, stageH, box.contentAspect) : null;
15194
+ const key = vp ? `${Math.round(vp.width)}x${Math.round(vp.height)}` : "full";
15195
+ if (!vp) {
15196
+ lastKey = "full";
15197
+ return;
15198
+ }
15199
+ f.style.cssText = BASE + `left:${vp.left}px;top:${vp.top}px;width:${vp.width}px;height:${vp.height}px;`;
15200
+ if (key !== lastKey) {
15201
+ lastKey = key;
15202
+ reportDiag(
15203
+ rt,
15204
+ rt.cfg,
15205
+ `网页壁纸露底自适配:视口按内容比例改为 ${Math.round(vp.width)}×${Math.round(vp.height)}(cover 居中裁切)`
15206
+ );
15207
+ }
15208
+ };
15209
+ const onResize = () => relayout();
15210
+ window.addEventListener("resize", onResize);
15211
+ let ro;
15212
+ if (typeof ResizeObserver !== "undefined") {
15213
+ ro = new ResizeObserver(() => relayout());
15214
+ ro.observe(container);
15215
+ }
15216
+ const timers = [];
15217
+ const onLoad = () => {
15218
+ relayout();
15219
+ for (const d of [120, 400, 1200]) timers.push(window.setTimeout(relayout, d));
15220
+ try {
15221
+ const doc = f.contentDocument;
15222
+ if (doc) {
15223
+ for (const el of doc.querySelectorAll("video,img")) {
15224
+ el.addEventListener("loadedmetadata", relayout, { once: true });
15225
+ el.addEventListener("load", relayout, { once: true });
15226
+ }
15227
+ }
15228
+ } catch {
15229
+ }
15230
+ };
15231
+ f.addEventListener("load", onLoad);
15232
+ rt.webRelayout = relayout;
15233
+ const prev = rt.sceneCleanup;
15234
+ rt.sceneCleanup = () => {
15235
+ window.removeEventListener("resize", onResize);
15236
+ ro?.disconnect();
15237
+ for (const t of timers) clearTimeout(t);
15238
+ f.removeEventListener("load", onLoad);
15239
+ if (rt.webRelayout === relayout) rt.webRelayout = void 0;
15240
+ try {
15241
+ prev?.();
15242
+ } catch {
15243
+ }
15244
+ };
15245
+ }
15246
+ function webPointerToClient(u, v, stage, frame, client) {
15247
+ if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
15248
+ if (!(stage.width > 0) || !(stage.height > 0)) return null;
15249
+ const sx = frame.width > 0 && client.width > 0 ? frame.width / client.width : 1;
15250
+ const sy = frame.height > 0 && client.height > 0 ? frame.height / client.height : 1;
15251
+ return {
15252
+ x: (u * stage.width - (frame.left - stage.left)) / (sx || 1),
15253
+ y: (v * stage.height - (frame.top - stage.top)) / (sy || 1)
15254
+ };
15255
+ }
15256
+ function installWebPointerBridge(rt, f, container) {
15257
+ rt.pointerCtl = {
15258
+ push(p) {
15259
+ if (!f.isConnected) return;
15260
+ const cRect = container.getBoundingClientRect();
15261
+ const fRect = f.getBoundingClientRect();
15262
+ const pt = webPointerToClient(
15263
+ Number(p?.u),
15264
+ Number(p?.v),
15265
+ {
15266
+ left: cRect.left,
15267
+ top: cRect.top,
15268
+ width: cRect.width || container.clientWidth || window.innerWidth || 0,
15269
+ height: cRect.height || container.clientHeight || window.innerHeight || 0
15270
+ },
15271
+ { left: fRect.left, top: fRect.top, width: fRect.width, height: fRect.height },
15272
+ { width: f.clientWidth, height: f.clientHeight }
15273
+ );
15274
+ if (!pt) return;
15275
+ weShimCall(rt, (w) => w.__wePushPointer?.(pt.x, pt.y, Number(p.buttons) || 0));
15276
+ },
15277
+ leave() {
15278
+ weShimCall(rt, (w) => w.__wePointerLeave?.());
15279
+ }
15280
+ };
15281
+ }
15282
+ function attachIframe(rt, cfg, container, src, opts) {
15283
+ const f = document.createElement("iframe");
15284
+ f.setAttribute("sandbox", "allow-scripts allow-same-origin");
15285
+ f.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:none;background:transparent;";
15286
+ if (!rt.wrap && getComputedStyle(container).position === "static") {
15287
+ container.style.position = "relative";
15288
+ }
15289
+ f.src = src;
15290
+ container.appendChild(f);
15291
+ rt.iframe = f;
15292
+ if (opts.blobUrl) {
15293
+ (rt.objectUrls ??= []).push(opts.blobUrl);
15294
+ }
15295
+ installLetterboxFix(rt, f, container);
15296
+ if (opts.injected) installWebPointerBridge(rt, f, container);
15297
+ const onFrameMsg = (ev) => {
15298
+ if (ev.source !== f.contentWindow) return;
15299
+ const data = ev.data;
15300
+ if (!data || data.op !== "we-frame") return;
15301
+ if (rt.paused) return;
15302
+ const t = typeof data.t === "number" ? data.t : performance.now();
15303
+ if (opts.frameClock) opts.frameClock.last = t;
15304
+ markFrame(rt, t);
15305
+ };
15306
+ window.addEventListener("message", onFrameMsg);
15307
+ const prevCleanup = rt.sceneCleanup;
15308
+ rt.sceneCleanup = () => {
15309
+ window.removeEventListener("message", onFrameMsg);
15310
+ try {
15311
+ prevCleanup?.();
15312
+ } catch {
15313
+ }
15314
+ };
15315
+ f.addEventListener("load", () => {
15316
+ try {
15317
+ const doc = f.contentDocument;
15318
+ if (doc) window.__blockContextMenu?.(doc);
15319
+ if (!opts.injected) injectGpuThrottle(rt, f, doc);
15320
+ } catch {
15321
+ }
15322
+ weShimCall(rt, (w2) => {
15323
+ const wire = {};
15324
+ for (const [k, v] of Object.entries(rt.liveUserProps ?? {})) wire[k] = { value: v };
15325
+ w2.__weApplyProps?.(wire);
15326
+ w2.__weSetFps?.(rt.cfg.sceneFps ?? 60);
15327
+ w2.__weSetVolume?.(rt.cfg.muted === false ? 1 : 0);
15328
+ if (rt.paused) w2.__weSetPaused?.(true);
15329
+ });
15330
+ try {
15331
+ rt.onFirstFrame?.();
15332
+ rt.onFirstFrame = void 0;
15333
+ } catch {
15334
+ }
15335
+ const w = container.clientWidth || window.innerWidth || 1;
15336
+ const h = container.clientHeight || window.innerHeight || 1;
15337
+ try {
15338
+ rt.onSceneInfo?.({
15339
+ width: w,
15340
+ height: h,
15341
+ layerCount: 0,
15342
+ hasModels: false,
15343
+ hasParticles: false,
15344
+ hasText: false
15345
+ });
15346
+ } catch {
15347
+ }
15348
+ });
15349
+ }
15350
+ const WEB_ASPECT_EPS = 5e-3;
15351
+ const WEB_LETTERBOX_MIN_RATIO = 0.01;
15352
+ const WEB_ASPECT_MIN = 0.2;
15353
+ const WEB_ASPECT_MAX = 6;
15354
+ function webCoverViewport(stageW, stageH, contentAspect) {
15355
+ if (!(stageW > 0) || !(stageH > 0) || !(contentAspect > 0)) return null;
15356
+ const stageAspect = stageW / stageH;
15357
+ if (Math.abs(stageAspect - contentAspect) <= WEB_ASPECT_EPS) return null;
15358
+ if (stageAspect < contentAspect) {
15359
+ const width = stageH * contentAspect;
15360
+ return { width, height: stageH, left: (stageW - width) / 2, top: 0 };
15361
+ }
15362
+ const height = stageW / contentAspect;
15363
+ return { width: stageW, height, left: 0, top: (stageH - height) / 2 };
15364
+ }
15365
+ function measureWebLetterbox(doc) {
15366
+ const win = doc.defaultView;
15367
+ if (!win) return null;
15368
+ const vw = win.innerWidth;
15369
+ const vh = win.innerHeight;
15370
+ if (!(vw > 0) || !(vh > 0)) return null;
15371
+ const cands = [...doc.querySelectorAll("video,img")];
15372
+ for (const el of cands) {
15373
+ const r = el.getBoundingClientRect();
15374
+ if (r.width <= 0 || r.height <= 0) continue;
15375
+ if (r.width < vw * 0.98) continue;
15376
+ if (Math.abs(r.left) > vw * 0.02 || r.top > vh * 0.02) continue;
15377
+ if (vh - r.height < vh * WEB_LETTERBOX_MIN_RATIO) continue;
15378
+ const natW = el.videoWidth || el.naturalWidth || 0;
15379
+ const natH = el.videoHeight || el.naturalHeight || 0;
15380
+ if (!(natW > 0) || !(natH > 0)) continue;
15381
+ const aspect = natW / natH;
15382
+ if (!Number.isFinite(aspect) || aspect < WEB_ASPECT_MIN || aspect > WEB_ASPECT_MAX) continue;
15383
+ return { contentAspect: aspect };
15384
+ }
15385
+ return null;
15386
+ }
15387
+ function vecToCss(v) {
15388
+ if (!v) return "rgb(128,128,128)";
15389
+ const r = Math.round(Math.max(0, Math.min(1, Number(v.x) || 0)) * 255);
15390
+ const g = Math.round(Math.max(0, Math.min(1, Number(v.y) || 0)) * 255);
15391
+ const b = Math.round(Math.max(0, Math.min(1, Number(v.z) || 0)) * 255);
15392
+ return `rgb(${r},${g},${b})`;
15393
+ }
15394
+ function thumbDataUrlFromSnap(snap) {
15395
+ try {
15396
+ const c = document.createElement("canvas");
15397
+ c.width = c.height = 64;
15398
+ const ctx = c.getContext("2d");
15399
+ if (!ctx) return "";
15400
+ const p = snap.primaryColor;
15401
+ const s = snap.secondaryColor;
15402
+ const grd = ctx.createLinearGradient(0, 0, 64, 64);
15403
+ grd.addColorStop(0, vecToCss(p));
15404
+ grd.addColorStop(1, vecToCss(s));
15405
+ ctx.fillStyle = grd;
15406
+ ctx.fillRect(0, 0, 64, 64);
15407
+ return c.toDataURL("image/jpeg", 0.85);
15408
+ } catch {
15409
+ return "";
15410
+ }
15411
+ }
15412
+ function defaultMediaDriver() {
15413
+ return media.createSimulatedMedia();
15414
+ }
15415
+ function pushMediaDiff(rt, prev, snap) {
15416
+ const events = media.diffMediaEvents(prev, snap);
15417
+ for (const { name, event } of events) {
15418
+ if (name === "mediaStatusChanged") {
15419
+ weShimCall(rt, (w) => w.__wePushMedia?.({ op: "status", enabled: !!event.enabled }));
15420
+ } else if (name === "mediaPropertiesChanged") {
15421
+ weShimCall(
15422
+ rt,
15423
+ (w) => w.__wePushMedia?.({
15424
+ op: "properties",
15425
+ title: event.title ?? "",
15426
+ artist: event.artist ?? "",
15427
+ album: event.album ?? "",
15428
+ albumArtist: event.albumArtist ?? ""
15429
+ })
15430
+ );
15431
+ } else if (name === "mediaThumbnailChanged") {
15432
+ const thumb = thumbDataUrlFromSnap(snap);
15433
+ weShimCall(
15434
+ rt,
15435
+ (w) => w.__wePushMedia?.({
15436
+ op: "thumbnail",
15437
+ thumbnail: thumb,
15438
+ hasThumbnail: !!event.hasThumbnail || !!thumb,
15439
+ primaryColor: vecToCss(event.primaryColor),
15440
+ secondaryColor: vecToCss(event.secondaryColor),
15441
+ tertiaryColor: vecToCss(event.tertiaryColor),
15442
+ textColor: vecToCss(event.textColor),
15443
+ highContrastColor: vecToCss(event.highContrastColor)
15444
+ })
15445
+ );
15446
+ } else if (name === "mediaPlaybackChanged") {
15447
+ weShimCall(rt, (w) => w.__wePushMedia?.({ op: "playback", state: Number(event.state) || 0 }));
15448
+ } else if (name === "mediaTimelineChanged") {
15449
+ weShimCall(
15450
+ rt,
15451
+ (w) => w.__wePushMedia?.({
15452
+ op: "timeline",
15453
+ position: Number(event.position) || 0,
15454
+ duration: Number(event.duration) || 0
15455
+ })
15456
+ );
15457
+ }
15458
+ }
15459
+ return media.cloneMediaSnapshot(snap);
15460
+ }
15461
+ function startAudioPump(rt, driver, frameClock) {
15462
+ if (!driver) return;
15463
+ let raf = 0;
15464
+ let lastPush = 0;
15465
+ const tick = (now) => {
15466
+ raf = requestAnimationFrame(tick);
15467
+ if (rt.paused || !rt.iframe) return;
15468
+ const fps = rt.cfg.sceneFps || 60;
15469
+ const pumpFps = Math.min(Math.max(1, fps), WEB_AUDIO_PUMP_HZ);
15470
+ const interval = 1e3 / pumpFps;
15471
+ if (now - lastPush < interval * 0.85) return;
15472
+ lastPush = now;
15473
+ try {
15474
+ driver.tick?.(now);
15475
+ const snap = driver.snapshot();
15476
+ const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
15477
+ weShimCall(rt, (w) => w.__wePushAudio?.(arr));
15478
+ if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
15479
+ } catch {
15480
+ }
15481
+ };
15482
+ raf = requestAnimationFrame(tick);
15483
+ const prev = rt.sceneCleanup;
15484
+ rt.sceneCleanup = () => {
15485
+ cancelAnimationFrame(raf);
15486
+ try {
15487
+ prev?.();
15488
+ } catch {
15489
+ }
15490
+ };
15491
+ }
15492
+ function startMediaPump(rt, driver) {
15493
+ if (!driver) return;
15494
+ let raf = 0;
15495
+ let lastMedia = null;
15496
+ let lastTick = 0;
15497
+ const tick = (now) => {
15498
+ raf = requestAnimationFrame(tick);
15499
+ if (rt.paused || !rt.iframe) return;
15500
+ if (now - lastTick < 200) return;
15501
+ lastTick = now;
15502
+ try {
15503
+ driver.update(now / 1e3);
15504
+ lastMedia = pushMediaDiff(rt, lastMedia, driver.snapshot);
15505
+ } catch {
15506
+ }
15507
+ };
15508
+ raf = requestAnimationFrame(tick);
15509
+ const prev = rt.sceneCleanup;
15510
+ rt.sceneCleanup = () => {
15511
+ cancelAnimationFrame(raf);
15512
+ try {
15513
+ prev?.();
15514
+ } catch {
15515
+ }
15516
+ };
15517
+ }
15518
+ function installWebCtl(rt) {
15519
+ rt.sceneCtl = {
15520
+ pause() {
15521
+ rt.paused = true;
15522
+ weShimCall(rt, (w) => w.__weSetPaused?.(true));
15523
+ },
15524
+ resume() {
15525
+ rt.paused = false;
15526
+ weShimCall(rt, (w) => w.__weSetPaused?.(false));
15527
+ },
15528
+ applyUserProperties(props) {
15529
+ const flat = { ...rt.liveUserProps ?? {} };
15530
+ for (const [k, v] of Object.entries(props ?? {})) {
15531
+ const val = v && typeof v === "object" && "value" in v ? v.value : v;
15532
+ flat[k] = val;
15533
+ }
15534
+ rt.liveUserProps = flat;
15535
+ weShimCall(rt, (w) => w.__weApplyProps?.(props));
15536
+ }
15537
+ };
15538
+ }
15539
+ function isSameOriginUrl(url) {
15540
+ try {
15541
+ return new URL(url, location.href).origin === location.origin;
15542
+ } catch {
15543
+ return false;
15544
+ }
15545
+ }
15546
+ function projectPropertiesToWire(project) {
15547
+ const props = project?.general?.properties;
15548
+ if (!props || typeof props !== "object") return {};
15549
+ const out = {};
15550
+ for (const [name, def] of Object.entries(props)) {
15551
+ if (!def || typeof def !== "object" || typeof def.type !== "string") continue;
15552
+ const raw = def.value;
15553
+ const type = def.type.toLowerCase();
15554
+ if (raw === null || raw === void 0) {
15555
+ if (type === "file" || type === "directory") {
15556
+ out[name] = { value: "" };
15557
+ continue;
15558
+ }
15559
+ if (!("value" in def)) continue;
15560
+ }
15561
+ out[name] = { value: raw };
15562
+ }
15563
+ return out;
15564
+ }
15565
+ async function fetchProjectWire(entryUrl) {
15566
+ try {
15567
+ const projUrl = new URL("project.json", new URL(entryUrl, location.href));
15568
+ const r = await fetch(projUrl.href, { credentials: "same-origin" });
15569
+ if (!r.ok) return {};
15570
+ return projectPropertiesToWire(await r.json());
15571
+ } catch {
15572
+ return {};
15573
+ }
15574
+ }
15575
+ function mergeLiveIntoWire(defaults, live) {
15576
+ const out = { ...defaults };
15577
+ if (live) {
15578
+ for (const [k, v] of Object.entries(live)) out[k] = { value: v };
15579
+ }
15580
+ return out;
15581
+ }
15582
+ function mountWeb(rt, cfg) {
15583
+ clear(rt);
15584
+ rt.cfg = cfg;
15585
+ const container = resolveContainer(rt, cfg);
15586
+ if (!container) {
15587
+ reportDiag(rt, cfg, "网页壁纸:无可用容器");
15588
+ rt.onError?.(new Error("网页壁纸:无可用容器"));
15589
+ return;
15590
+ }
15591
+ const entry = cfg.src ?? "";
15592
+ if (!entry) {
15593
+ reportDiag(rt, cfg, "网页壁纸:缺少 src");
15594
+ rt.onError?.(new Error("网页壁纸:缺少 src"));
15595
+ return;
15596
+ }
15597
+ installWebCtl(rt);
15598
+ const cfgExt = cfg;
15599
+ const audioDriver = cfgExt._webAudio === null ? null : cfgExt._webAudio ?? defaultAudioDriver();
15600
+ const mediaDriver = cfgExt._webMedia === null ? null : cfgExt._webMedia ?? defaultMediaDriver();
15601
+ const finishBare = (why) => {
15602
+ reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
15603
+ attachIframe(rt, cfg, container, entry, { injected: false });
15604
+ startAudioPump(rt, null);
15605
+ startMediaPump(rt, null);
15606
+ };
15607
+ const frameClock = { last: 0 };
15608
+ const startPumps = () => {
15609
+ startAudioPump(rt, audioDriver, frameClock);
15610
+ startMediaPump(rt, mediaDriver);
15611
+ };
15612
+ void (async () => {
15613
+ const defaults = await fetchProjectWire(entry);
15614
+ const wire = mergeLiveIntoWire(defaults, rt.liveUserProps);
15615
+ rt.liveUserProps = Object.fromEntries(Object.entries(wire).map(([k, w]) => [k, w.value]));
15616
+ if (isSameOriginUrl(entry)) {
15617
+ attachIframe(rt, cfg, container, entry, { injected: true, frameClock });
15618
+ startPumps();
15619
+ const f = rt.iframe;
15620
+ f?.addEventListener(
15621
+ "load",
15622
+ () => {
15623
+ let hasShim = false;
15624
+ weShimCall(rt, (w) => {
15625
+ hasShim = typeof w.__weSetPaused === "function";
15626
+ });
15627
+ if (!hasShim) {
15628
+ reportDiag(
15629
+ rt,
15630
+ cfg,
15631
+ "网页壁纸:同源入口未检测到 WE shim(host 未注入?);Spine 类壁纸请确认 /web/ HTML 改写"
15632
+ );
15633
+ }
15634
+ },
15635
+ { once: true }
15636
+ );
15637
+ return;
15638
+ }
15639
+ try {
15640
+ const res = await fetch(entry, { credentials: "same-origin" });
15641
+ if (!res.ok) {
15642
+ finishBare(`HTTP ${res.status}`);
15643
+ return;
15644
+ }
15645
+ const html = await res.text();
15646
+ if (hasBlockingCsp(html)) {
15647
+ finishBare("CSP 阻止 inline script");
15648
+ return;
15649
+ }
15650
+ const rewritten = rewriteHtml(html, shimSource, {
15651
+ baseHref: entryDirUrl(entry),
15652
+ seedScript: buildSeedScript(wire, cfg.sceneFps, cfg.muted === false ? 1 : 0)
15653
+ });
15654
+ const blob = new Blob([rewritten], { type: "text/html;charset=utf-8" });
15655
+ const blobUrl = URL.createObjectURL(blob);
15656
+ attachIframe(rt, cfg, container, blobUrl, { blobUrl, injected: true, frameClock });
15657
+ startPumps();
15658
+ } catch (e) {
15659
+ finishBare(e instanceof Error ? e.message : String(e));
15660
+ }
15661
+ })();
15662
+ }
13494
15663
  function mountWallpaper(rt, cfg) {
13495
- if ((cfg.type === "video" || cfg.type === "gif" || cfg.type === "image") && cfg.src) {
15664
+ const type = String(cfg.type ?? "").toLowerCase();
15665
+ cfg = { ...cfg, type };
15666
+ rt.cfg = cfg;
15667
+ if ((type === "video" || type === "gif" || type === "image") && cfg.src) {
13496
15668
  mountMedia(rt, cfg);
13497
- } else if (cfg.type === "scene" && (cfg.source || cfg.src)) {
15669
+ } else if (type === "scene" && (cfg.source || cfg.src)) {
13498
15670
  mountScene(rt, cfg);
15671
+ } else if (type === "web" && cfg.src) {
15672
+ mountWeb(rt, cfg);
13499
15673
  } else {
13500
15674
  rt.onUnhandledType?.(cfg);
13501
15675
  }
@@ -13505,22 +15679,60 @@ function normalizeFitOption(fit) {
13505
15679
  if (fit === "fill") return "cover";
13506
15680
  return fit === "contain" || fit === "stretch" ? fit : "cover";
13507
15681
  }
13508
- function toConfig(canvas, o) {
13509
- return {
13510
- type: "scene",
13511
- canvas,
13512
- source: o.source,
15682
+ function isWebProject(project) {
15683
+ const t = project?.type;
15684
+ return typeof t === "string" && t.toLowerCase() === "web";
15685
+ }
15686
+ function ensureSceneCanvas(el) {
15687
+ if (el instanceof HTMLCanvasElement) return el;
15688
+ const existing = el.querySelector(":scope > canvas[data-webwallgl]");
15689
+ if (existing instanceof HTMLCanvasElement) return existing;
15690
+ const c = document.createElement("canvas");
15691
+ c.setAttribute("data-webwallgl", "1");
15692
+ c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
15693
+ if (getComputedStyle(el).position === "static") el.style.position = "relative";
15694
+ el.appendChild(c);
15695
+ return c;
15696
+ }
15697
+ async function resolveMountConfig(el, o) {
15698
+ const base = {
13513
15699
  fit: normalizeFitOption(o.fit),
13514
15700
  renderDpr: o.renderDpr ?? 1,
13515
15701
  sceneFps: o.fps ?? 60,
13516
15702
  muted: (o.volume ?? 0) <= 0,
13517
- loop: true
15703
+ loop: true,
15704
+ canvas: el,
15705
+ source: o.source
13518
15706
  };
15707
+ let project = null;
15708
+ try {
15709
+ project = await o.source.project?.() ?? null;
15710
+ } catch {
15711
+ project = null;
15712
+ }
15713
+ if (isWebProject(project)) {
15714
+ let url;
15715
+ try {
15716
+ const entry = await o.source.webEntry?.();
15717
+ url = entry?.url;
15718
+ } catch {
15719
+ url = void 0;
15720
+ }
15721
+ if (!url && o.source.key) {
15722
+ const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") || "index.html" : "index.html";
15723
+ url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
15724
+ }
15725
+ if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
15726
+ return { ...base, type: "web", src: url, source: o.source };
15727
+ }
15728
+ const canvas = ensureSceneCanvas(el);
15729
+ return { ...base, type: "scene", canvas, source: o.source };
13519
15730
  }
13520
- function createScene(canvas, options) {
15731
+ function createScene(el, options) {
13521
15732
  const rt = createRuntime();
13522
15733
  const events = { ready: [], error: [], diagnostic: [] };
13523
15734
  let currentOptions = { ...options ?? {}, source: null };
15735
+ let boundEl = el;
13524
15736
  const emitError = (err) => {
13525
15737
  for (const fn of events.error) {
13526
15738
  try {
@@ -13559,6 +15771,24 @@ function createScene(canvas, options) {
13559
15771
  rt.onFirstFrame = () => {
13560
15772
  rt.onFirstFrame = void 0;
13561
15773
  prev?.();
15774
+ const info = rt.info ?? {
15775
+ width: 0,
15776
+ height: 0,
15777
+ layerCount: 0,
15778
+ hasModels: false,
15779
+ hasParticles: false,
15780
+ hasText: false
15781
+ };
15782
+ try {
15783
+ currentOptions.onReady?.(info);
15784
+ } catch {
15785
+ }
15786
+ for (const fn of events.ready) {
15787
+ try {
15788
+ fn(info);
15789
+ } catch {
15790
+ }
15791
+ }
13562
15792
  resolve();
13563
15793
  };
13564
15794
  });
@@ -13572,7 +15802,9 @@ function createScene(canvas, options) {
13572
15802
  return { promise, off };
13573
15803
  };
13574
15804
  const instance = {
13575
- canvas,
15805
+ get canvas() {
15806
+ return boundEl;
15807
+ },
13576
15808
  pause() {
13577
15809
  rt.paused = true;
13578
15810
  rt.sceneCtl?.pause();
@@ -13591,11 +15823,13 @@ function createScene(canvas, options) {
13591
15823
  },
13592
15824
  setFps(fps) {
13593
15825
  rt.cfg.sceneFps = fps;
15826
+ weShimCall(rt, (w) => w.__weSetFps?.(fps));
13594
15827
  },
13595
15828
  setVolume(volume) {
13596
15829
  const v = Math.max(0, Math.min(1, volume));
13597
15830
  rt.cfg.muted = v <= 0;
13598
15831
  rt.sceneAudio?.setVolume(v);
15832
+ weShimCall(rt, (w) => w.__weSetVolume?.(v));
13599
15833
  },
13600
15834
  setRenderDpr(dpr) {
13601
15835
  rt.cfg.renderDpr = dpr;
@@ -13612,17 +15846,17 @@ function createScene(canvas, options) {
13612
15846
  async load(source) {
13613
15847
  currentOptions = { ...currentOptions, source };
13614
15848
  wireOptions(currentOptions);
13615
- const cfg = {
13616
- ...rt.cfg,
13617
- type: "scene",
13618
- source,
13619
- src: void 0,
13620
- mediaBase: void 0
13621
- };
15849
+ const cfg = await resolveMountConfig(boundEl, currentOptions);
15850
+ if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
15851
+ boundEl = cfg.canvas;
15852
+ }
13622
15853
  rt.cfg = cfg;
13623
15854
  rt.paused = false;
13624
15855
  rt.info = void 0;
13625
15856
  resetCoverAlign(rt);
15857
+ if (currentOptions.properties) {
15858
+ rt.liveUserProps = { ...currentOptions.properties };
15859
+ }
13626
15860
  const firstFrame = armFirstFrame();
13627
15861
  const failure = armFailure();
13628
15862
  mountWallpaper(rt, cfg);
@@ -13666,10 +15900,17 @@ function createScene(canvas, options) {
13666
15900
  const applyOptions = async (o) => {
13667
15901
  currentOptions = o;
13668
15902
  wireOptions(o);
13669
- rt.cfg = toConfig(canvas, o);
15903
+ const cfg = await resolveMountConfig(el, o);
15904
+ if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
15905
+ boundEl = cfg.canvas;
15906
+ }
15907
+ rt.cfg = cfg;
13670
15908
  rt.paused = o.autoplay === false;
13671
15909
  rt.info = void 0;
13672
15910
  resetCoverAlign(rt);
15911
+ if (o.properties && Object.keys(o.properties).length) {
15912
+ rt.liveUserProps = { ...o.properties };
15913
+ }
13673
15914
  const firstFrame = armFirstFrame();
13674
15915
  const failure = armFailure();
13675
15916
  mountWallpaper(rt, rt.cfg);
@@ -13684,8 +15925,8 @@ function createScene(canvas, options) {
13684
15925
  instance.__applyOptions = applyOptions;
13685
15926
  return instance;
13686
15927
  }
13687
- async function mount(canvas, options) {
13688
- const instance = createScene(canvas, options);
15928
+ async function mount(el, options) {
15929
+ const instance = createScene(el, options);
13689
15930
  const withApply = instance;
13690
15931
  await withApply.__applyOptions(options);
13691
15932
  return instance;