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.
@@ -33,7 +33,20 @@
33
33
  window.__blockContextMenu = (doc) => doc.addEventListener("contextmenu", block, true);
34
34
  })();
35
35
  function clear(rt) {
36
- if (rt.wrap) rt.wrap.innerHTML = "";
36
+ if (rt.iframe) {
37
+ try {
38
+ rt.iframe.contentWindow?.location.replace("about:blank");
39
+ } catch {
40
+ }
41
+ }
42
+ if (rt.wrap) {
43
+ rt.wrap.innerHTML = "";
44
+ } else if (rt.iframe?.isConnected) {
45
+ try {
46
+ rt.iframe.remove();
47
+ } catch {
48
+ }
49
+ }
37
50
  if (rt.raf !== void 0) cancelAnimationFrame(rt.raf);
38
51
  rt.raf = void 0;
39
52
  for (const p of rt.videoPairs ?? []) p.destroy();
@@ -41,6 +54,7 @@
41
54
  if (rt.sceneCleanup) rt.sceneCleanup();
42
55
  rt.sceneCleanup = void 0;
43
56
  rt.sceneCtl = void 0;
57
+ rt.pointerCtl = void 0;
44
58
  if (rt.renderer) {
45
59
  rt.renderer.dispose?.();
46
60
  rt.renderer = void 0;
@@ -124,7 +138,7 @@
124
138
  } catch {
125
139
  }
126
140
  try {
127
- const origin = cfg.mediaBase ? new URL(cfg.mediaBase).origin : "";
141
+ const origin = cfg.mediaBase ? new URL(cfg.mediaBase, window.location.href).origin : "";
128
142
  if (origin) {
129
143
  const img = new Image();
130
144
  img.src = `${origin}/diag?msg=${encodeURIComponent(`scene ${cfg.src ?? "?"}: ${msg.slice(0, 500)}`)}`;
@@ -867,6 +881,31 @@
867
881
  }
868
882
  return dflt;
869
883
  }
884
+ function isRenderInert(o) {
885
+ if (!o) return false;
886
+ return !o.image && !o.model && !o.particle && o.text == null && !o.size;
887
+ }
888
+ function composeChildTransform(parentWorld, childLocal, parentScalePropagates) {
889
+ const pscale = parentScalePropagates ? parentWorld.scale : [1, 1, 1];
890
+ const ca = (parentWorld.angles[2] || 0) * Math.PI / 180;
891
+ const cos = Math.cos(ca);
892
+ const sin = Math.sin(ca);
893
+ const ox = childLocal.origin[0] * pscale[0];
894
+ const oy = childLocal.origin[1] * pscale[1];
895
+ return {
896
+ origin: [
897
+ parentWorld.origin[0] + ox * cos - oy * sin,
898
+ parentWorld.origin[1] + ox * sin + oy * cos,
899
+ parentWorld.origin[2] + (childLocal.origin[2] || 0)
900
+ ],
901
+ scale: [
902
+ pscale[0] * childLocal.scale[0],
903
+ pscale[1] * childLocal.scale[1],
904
+ pscale[2] * childLocal.scale[2]
905
+ ],
906
+ angles: [childLocal.angles[0], childLocal.angles[1], (parentWorld.angles[2] || 0) + childLocal.angles[2]]
907
+ };
908
+ }
870
909
  function parseScene(sceneJson, project) {
871
910
  const properties = project && project.general && project.general.properties || {};
872
911
  const objects = sceneJson.objects || [];
@@ -883,6 +922,11 @@
883
922
  scale: parseVec3(o.scale || "1 1 1"),
884
923
  angles: parseVec3(o.angles || "0 0 0")
885
924
  }));
925
+ const localSnapshot = local.map((c) => ({
926
+ origin: c.origin.slice(),
927
+ scale: c.scale.slice(),
928
+ angles: c.angles.slice()
929
+ }));
886
930
  for (let pass = 0; pass < 8; pass++) {
887
931
  let changed = false;
888
932
  for (const c of local) {
@@ -896,20 +940,11 @@
896
940
  const pr = objects[pIdx];
897
941
  const prs = pr.scale;
898
942
  const runtimeBound = prs !== null && typeof prs === "object" && (typeof prs.script === "string" || prs.user !== void 0);
899
- const renderInert = !pr.image && !pr.model && !pr.particle && pr.text == null && !pr.size;
900
- const pscale = runtimeBound && renderInert ? [1, 1, 1] : pc.scale;
901
- const ca = pc.angles[2] * Math.PI / 180;
902
- const cos = Math.cos(ca);
903
- const sin = Math.sin(ca);
904
- const ox = c.origin[0] * pscale[0];
905
- const oy = c.origin[1] * pscale[1];
906
- c.origin[0] = pc.origin[0] + ox * cos - oy * sin;
907
- c.origin[1] = pc.origin[1] + ox * sin + oy * cos;
908
- c.origin[2] = pc.origin[2] + c.origin[2];
909
- c.angles[2] = pc.angles[2] + c.angles[2];
910
- c.scale[0] = pscale[0] * c.scale[0];
911
- c.scale[1] = pscale[1] * c.scale[1];
912
- c.scale[2] = pscale[2] * c.scale[2];
943
+ const propagateScale = !(runtimeBound && isRenderInert(pr));
944
+ const w = composeChildTransform(pc, c, propagateScale);
945
+ c.origin = w.origin;
946
+ c.scale = w.scale;
947
+ c.angles = w.angles;
913
948
  c.parent = null;
914
949
  changed = true;
915
950
  }
@@ -999,6 +1034,7 @@
999
1034
  // anchor 是盒子相对 origin 的锚点(同 image alignment 枚举,外加 "none")。
1000
1035
  // 本机 563 个文字层:none 237 / 缺省 301 / center 20 —— none 与缺省都按 center 处理
1001
1036
  // (WE 对象缺省对齐就是 center;显式 center 的挂件与时钟层行为一致)。
1037
+ // 2780710296 实验过默认改 top:竖直阶梯对了,但会平移其它壁纸文字相对图元的位置,已回滚。
1002
1038
  textAnchor: typeof o.anchor === "string" && o.anchor !== "none" ? o.anchor : "center",
1003
1039
  textMaxwidth: parseNum(o.maxwidth, 0),
1004
1040
  textMaxrows: parseNum(o.maxrows, 0),
@@ -1128,6 +1164,18 @@
1128
1164
  origin: layerOrigin,
1129
1165
  scale: world.scale,
1130
1166
  angles: world.angles,
1167
+ // [we-scene patch] 父级相对变换(WE 场景图的真实语义)。origin/scale/angles
1168
+ // 上的脚本与关键帧动画一律在这层空间收发,再由 recomposeWorld 合成回上面的
1169
+ // world 三件套。渲染 / hittest / getTransformMatrix 仍只读 world,不受影响。
1170
+ // isPostProcess 层的 world 被强制成整幅画布,local 对它无意义(recompose 跳过)。
1171
+ localOrigin: localSnapshot[i].origin,
1172
+ localScale: localSnapshot[i].scale,
1173
+ localAngles: localSnapshot[i].angles,
1174
+ // 「渲染惰性纯容器」:父 scale 是否传给子层由父级这个标志决定,
1175
+ // 判据与 parse 合并阶段逐字相同(见 isRenderInert)。
1176
+ renderInert: isRenderInert(o),
1177
+ // 父 scale 绑了脚本/用户属性(运行时可变)。与 renderInert 一起决定传播闸门。
1178
+ scaleRuntimeBound: !!(o.scale !== null && typeof o.scale === "object" && (typeof o.scale.script === "string" || o.scale.user !== void 0)),
1131
1179
  size: layerSize,
1132
1180
  alignment: o.alignment || "center",
1133
1181
  color: parseColor(o.color),
@@ -1238,15 +1286,110 @@
1238
1286
  cropoffset: modelJson.cropoffset ? parseVec2(modelJson.cropoffset) : null
1239
1287
  };
1240
1288
  }
1289
+ function recomposeWorld(layers, dirty) {
1290
+ if (!layers || layers.length === 0) return;
1291
+ const byId = /* @__PURE__ */ new Map();
1292
+ for (const l of layers) {
1293
+ if (l && l.id !== void 0 && l.id !== null) byId.set(l.id, l);
1294
+ }
1295
+ const depthOf = (l) => {
1296
+ let d = 0;
1297
+ let p = l.parentId;
1298
+ for (let guard = 0; p !== void 0 && p !== null && guard < 64; guard++) {
1299
+ const parent = byId.get(p);
1300
+ if (!parent) break;
1301
+ d++;
1302
+ p = parent.parentId;
1303
+ }
1304
+ return d;
1305
+ };
1306
+ const targets = [];
1307
+ for (const l of layers) {
1308
+ if (!l || !l.localOrigin) continue;
1309
+ if (l.isPostProcess) continue;
1310
+ if (dirty && !dirty.has(l.id)) continue;
1311
+ targets.push(l);
1312
+ }
1313
+ targets.sort((a, b) => depthOf(a) - depthOf(b));
1314
+ for (const l of targets) {
1315
+ const parent = l.parentId !== void 0 && l.parentId !== null ? byId.get(l.parentId) : null;
1316
+ let w;
1317
+ if (!parent) {
1318
+ w = { origin: l.localOrigin.slice(), scale: l.localScale.slice(), angles: l.localAngles.slice() };
1319
+ } else {
1320
+ const propagateScale = !(parent.scaleRuntimeBound && parent.renderInert);
1321
+ w = composeChildTransform(
1322
+ { origin: parent.origin, scale: parent.scale, angles: parent.angles },
1323
+ { origin: l.localOrigin, scale: l.localScale, angles: l.localAngles },
1324
+ propagateScale
1325
+ );
1326
+ }
1327
+ const d = l.attachBindDelta;
1328
+ if (d) {
1329
+ w.origin[0] += d[0];
1330
+ w.origin[1] += d[1];
1331
+ }
1332
+ l.origin[0] = w.origin[0];
1333
+ l.origin[1] = w.origin[1];
1334
+ l.origin[2] = w.origin[2];
1335
+ l.scale[0] = w.scale[0];
1336
+ l.scale[1] = w.scale[1];
1337
+ l.scale[2] = w.scale[2];
1338
+ l.angles[0] = w.angles[0];
1339
+ l.angles[1] = w.angles[1];
1340
+ l.angles[2] = w.angles[2];
1341
+ if (l.attachBase) {
1342
+ l.attachBase[0] = w.origin[0];
1343
+ l.attachBase[1] = w.origin[1];
1344
+ }
1345
+ }
1346
+ }
1347
+ function collectTransformDirty(layers, extraSeeds) {
1348
+ const dirty = /* @__PURE__ */ new Set();
1349
+ if (!layers || layers.length === 0) return dirty;
1350
+ const childrenOf = /* @__PURE__ */ new Map();
1351
+ for (const l of layers) {
1352
+ if (!l || l.parentId === void 0 || l.parentId === null) continue;
1353
+ const list = childrenOf.get(l.parentId);
1354
+ if (list) list.push(l);
1355
+ else childrenOf.set(l.parentId, [l]);
1356
+ }
1357
+ const TRANSFORM_FIELDS = ["origin", "scale", "angles"];
1358
+ const seeds = [];
1359
+ for (const l of layers) {
1360
+ if (!l || l.id === void 0 || l.id === null) continue;
1361
+ const scripts = l.objectScripts || null;
1362
+ const anims = l.objectAnimations || null;
1363
+ const bound = TRANSFORM_FIELDS.some((f) => scripts && scripts[f] || anims && anims[f]);
1364
+ if (bound) seeds.push(l);
1365
+ }
1366
+ if (extraSeeds) {
1367
+ for (const l of extraSeeds) if (l && l.id !== void 0 && l.id !== null) seeds.push(l);
1368
+ }
1369
+ const stack = seeds.slice();
1370
+ for (let guard = 0; stack.length > 0 && guard < 1e5; guard++) {
1371
+ const l = stack.pop();
1372
+ if (!l || l.id === void 0 || l.id === null) continue;
1373
+ if (dirty.has(l.id)) continue;
1374
+ dirty.add(l.id);
1375
+ const kids = childrenOf.get(l.id);
1376
+ if (kids) for (const c of kids) stack.push(c);
1377
+ }
1378
+ return dirty;
1379
+ }
1241
1380
  const sceneMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1242
1381
  __proto__: null,
1243
1382
  applySolidFromModel,
1383
+ collectTransformDirty,
1384
+ composeChildTransform,
1385
+ isRenderInert,
1244
1386
  parseBool,
1245
1387
  parseColor,
1246
1388
  parseNum,
1247
1389
  parseScene,
1248
1390
  parseVec2,
1249
1391
  parseVec3,
1392
+ recomposeWorld,
1250
1393
  recomputeLayerVisibility,
1251
1394
  resolveMaterial
1252
1395
  }, Symbol.toStringTag, { value: "Module" }));
@@ -1650,12 +1793,36 @@
1650
1793
  const { defs, fns } = collectMacros(text);
1651
1794
  if (defs.size === 0 && fns.size === 0) break;
1652
1795
  const lines = text.split("\n");
1796
+ const defLine = /* @__PURE__ */ new Map();
1797
+ for (let i = 0; i < lines.length; i++) {
1798
+ const dm = /^[ \t]*#define[ \t]+([A-Za-z_][A-Za-z0-9_]*)/.exec(lines[i]);
1799
+ if (dm && !defLine.has(dm[1])) defLine.set(dm[1], i);
1800
+ }
1801
+ const declLine = /* @__PURE__ */ new Map();
1802
+ {
1803
+ const TYPES = "(?:float|int|bool|vec[234]|ivec[234]|bvec[234]|mat[234])";
1804
+ for (const name of defs.keys()) {
1805
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1806
+ const re = new RegExp(
1807
+ "^\\s*(?:const\\s+|uniform\\s+|varying\\s+|in\\s+|out\\s+|attribute\\s+)*" + TYPES + "\\s+" + esc + "\\s*[=;]"
1808
+ );
1809
+ for (let i = 0; i < lines.length; i++) {
1810
+ if (re.test(lines[i])) {
1811
+ declLine.set(name, i);
1812
+ break;
1813
+ }
1814
+ }
1815
+ }
1816
+ }
1653
1817
  let changed = false;
1654
1818
  for (let i = 0; i < lines.length; i++) {
1655
1819
  const line = lines[i];
1656
1820
  if (/^[ \t]*#/.test(line)) continue;
1657
1821
  let l = line;
1658
1822
  for (const [name, val] of defs) {
1823
+ const dl = defLine.get(name);
1824
+ if (dl !== void 0 && i < dl) continue;
1825
+ if (declLine.get(name) === i) continue;
1659
1826
  const re = new RegExp("\\b" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
1660
1827
  if (re.test(l)) {
1661
1828
  l = replaceWord(l, name, val);
@@ -1663,6 +1830,8 @@
1663
1830
  }
1664
1831
  }
1665
1832
  for (const [name, info] of fns) {
1833
+ const dl = defLine.get(name);
1834
+ if (dl !== void 0 && i < dl) continue;
1666
1835
  if (l.includes(name)) {
1667
1836
  l = expandFunctionMacro(l, name, info, depth);
1668
1837
  changed = true;
@@ -1937,6 +2106,13 @@
1937
2106
  const word = text.slice(p + 1, e);
1938
2107
  return GLSL_TYPES.has(word);
1939
2108
  }
2109
+ function collectIntNames(code) {
2110
+ const names = /* @__PURE__ */ new Set();
2111
+ let m;
2112
+ const declRe = /\b(?:const\s+)?int\s+([A-Za-z_]\w*)\s*[=;)\u0003]/g;
2113
+ while ((m = declRe.exec(code)) !== null) names.add(m[1]);
2114
+ return names;
2115
+ }
1940
2116
  function rewriteCall(text, callName, fn) {
1941
2117
  let out = "";
1942
2118
  let i = 0;
@@ -2021,23 +2197,73 @@
2021
2197
  const dim = sw.length;
2022
2198
  return fn + "(vec" + dim + "(" + num2 + "), " + expr + ")";
2023
2199
  });
2024
- code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
2025
- code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2026
- code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2027
- code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2028
- code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2029
- 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");
2030
- code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2031
- 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");
2032
- {
2033
- const floatNames = /* @__PURE__ */ new Set();
2034
- 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;
2035
- let dm;
2036
- while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
2037
- if (floatNames.size > 0) {
2038
- const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
2039
- code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
2200
+ code = code.replace(
2201
+ /(\.([xyzwrgba]{2,4})\s*=\s*)(max|min)\(\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*,\s*([^;]+?)\s*\)\s*;/g,
2202
+ (all, lead, sw, fn, scalar, vecExpr) => {
2203
+ if (!/\.[xyzwrgba]{2,4}\b|\bvec[234]\s*\(/.test(vecExpr)) return all;
2204
+ return `${lead}${fn}(vec${sw.length}(${scalar}), ${vecExpr});`;
2205
+ }
2206
+ );
2207
+ code = code.replace(
2208
+ /(^|[^\w)\]])([+-])([+-])(?=[\d.])/g,
2209
+ (all, pre, s1, s2) => pre + (s1 === s2 ? "+" : "-")
2210
+ );
2211
+ const sciHoles = [];
2212
+ code = code.replace(/\b\d+(?:\.\d+)?[eE][+-]?\d+\b/g, (m) => {
2213
+ sciHoles.push(m);
2214
+ return "" + "".repeat(sciHoles.length) + "";
2215
+ });
2216
+ const forHoles = [];
2217
+ code = code.replace(/\bfor\s*\(\s*int\s+([A-Za-z_]\w*)([^)]*)\)/g, (m, name, rest) => {
2218
+ forHoles.push(rest + ")");
2219
+ return `for (int ${name}${"".repeat(forHoles.length)}`;
2220
+ });
2221
+ for (let pass = 0; pass < 8; pass++) {
2222
+ const before = code;
2223
+ code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*([A-Za-z_][A-Za-z0-9_]*)/g, "$1$2.0 $3 $4");
2224
+ code = code.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2225
+ code = code.replace(/(\d+\.\d+)\s*([*/])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2226
+ code = code.replace(/(^|[^\w.])(\d+)\s*([*/])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2227
+ code = code.replace(/(\.\d+)\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2228
+ code = code.replace(/([A-Za-z_]\w*\.(?:xyzw|xyz|xy|zw|rgba|rgb|rg|x|y|z|w|r|g|b|a))\s*([+-])\s*(\d+)(?![\d.])/g, "$1 $2 $3.0");
2229
+ code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*(\d+\.\d+)/g, "$1$2.0 $3 $4");
2230
+ code = code.replace(/(^|[^\w.])(\d+)\s*([+-])\s*([A-Za-z_]\w*\.(?:xyzw|xyz|xy|zw|rgba|rgb|rg|x|y|z|w|r|g|b|a))/g, "$1$2.0 $3 $4");
2231
+ code = code.replace(
2232
+ /(^|[^\w.])(\d+)\s*([*/+-])\s*(\()/g,
2233
+ (all, pre, num2, op, open, offset, whole) => {
2234
+ let depth = 0;
2235
+ let end = offset + all.length - 1;
2236
+ for (; end < whole.length; end++) {
2237
+ const ch = whole[end];
2238
+ if (ch === "(") depth++;
2239
+ else if (ch === ")") {
2240
+ depth--;
2241
+ if (depth === 0) {
2242
+ end++;
2243
+ break;
2244
+ }
2245
+ } else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
2246
+ }
2247
+ for (; end < whole.length; end++) {
2248
+ const ch = whole[end];
2249
+ if (ch === ";" || ch === "," || ch === "\n" || ch === ")") break;
2250
+ }
2251
+ const seg = whole.slice(offset, end);
2252
+ return /\d\.\d/.test(seg) ? `${pre}${num2}.0 ${op} ${open}` : all;
2253
+ }
2254
+ );
2255
+ {
2256
+ const floatNames = /* @__PURE__ */ new Set();
2257
+ const declRe = /\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?(?:float|vec2|vec3|vec4|mat2|mat3|mat4)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
2258
+ let dm;
2259
+ while ((dm = declRe.exec(code)) !== null) floatNames.add(dm[1]);
2260
+ if (floatNames.size > 0) {
2261
+ const alt = Array.from(floatNames).sort((a, b) => b.length - a.length).join("|");
2262
+ code = code.replace(new RegExp("(^|[^\\w.])(\\d+)\\s*([+-])\\s*(" + alt + ")(?![A-Za-z0-9_])", "g"), "$1$2.0 $3 $4");
2263
+ code = code.replace(new RegExp("\\b(" + alt + ")\\s*([+-])\\s*(\\d+)(?![\\d.])", "g"), "$1 $2 $3.0");
2264
+ }
2040
2265
  }
2266
+ if (code === before) break;
2041
2267
  }
2042
2268
  {
2043
2269
  const FLOAT_BUILTINS = [
@@ -2173,6 +2399,67 @@
2173
2399
  return pre + lhs + " = " + rhs + "." + SW[lw] + ";";
2174
2400
  });
2175
2401
  }
2402
+ {
2403
+ const floatDecl = /* @__PURE__ */ new Set();
2404
+ {
2405
+ const fdre = /\b(?:uniform|varying|attribute|in|out|const)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
2406
+ let fd;
2407
+ while ((fd = fdre.exec(code)) !== null) floatDecl.add(fd[1]);
2408
+ }
2409
+ const vecW = (expr) => {
2410
+ const e = expr.trim();
2411
+ {
2412
+ const c = /^vec([234])\s*\(/.exec(e);
2413
+ if (c) {
2414
+ let depth = 0;
2415
+ for (let i = e.indexOf("("); i < e.length; i++) {
2416
+ if (e[i] === "(") depth++;
2417
+ else if (e[i] === ")") {
2418
+ depth--;
2419
+ if (depth === 0) return i === e.length - 1 ? Number(c[1]) : 0;
2420
+ }
2421
+ }
2422
+ return 0;
2423
+ }
2424
+ }
2425
+ if (/^texture(?:Lod)?\s*\(/.test(e)) {
2426
+ let depth = 0;
2427
+ for (let i = e.indexOf("("); i < e.length; i++) {
2428
+ if (e[i] === "(") depth++;
2429
+ else if (e[i] === ")") {
2430
+ depth--;
2431
+ if (depth === 0) return i === e.length - 1 ? 4 : 0;
2432
+ }
2433
+ }
2434
+ return 0;
2435
+ }
2436
+ const m = /^([A-Za-z_]\w*)(?:\.([xyzwrgba]{2,4}))?\s*[*/]\s*([^*/]+)$/.exec(e);
2437
+ if (!m) return 0;
2438
+ const rhsPart = m[3];
2439
+ if (/\bvec[234]\s*\(|\.[xyzwrgba]{2,4}\b/.test(rhsPart)) return 0;
2440
+ if (m[2]) return m[2].length;
2441
+ if (floatDecl.has(m[1])) return 0;
2442
+ return width.get(m[1]) || 0;
2443
+ };
2444
+ code = code.replace(
2445
+ /(^|[;{}\n]\s*)float\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
2446
+ (all, pre, name, rhs) => {
2447
+ const w = vecW(rhs);
2448
+ if (w < 2) return all;
2449
+ return `${pre}float ${name} = (${rhs.trim()}).x;`;
2450
+ }
2451
+ );
2452
+ const SWN = { 2: "xy", 3: "xyz" };
2453
+ code = code.replace(
2454
+ /(^|[;{}\n]\s*)vec([23])\s+([A-Za-z_]\w*)\s*=\s*([^;]+);/g,
2455
+ (all, pre, dim, name, rhs) => {
2456
+ const lw = Number(dim);
2457
+ const rw = vecW(rhs);
2458
+ if (rw <= lw) return all;
2459
+ return `${pre}vec${dim} ${name} = (${rhs.trim()}).${SWN[lw]};`;
2460
+ }
2461
+ );
2462
+ }
2176
2463
  if (width.size > 0) {
2177
2464
  const floatNames = /* @__PURE__ */ new Set();
2178
2465
  const fre = /\b(?:uniform|varying|attribute|in|out)?\s*\bfloat\s+([A-Za-z_]\w*)/g;
@@ -2214,6 +2501,7 @@
2214
2501
  code = code.replace(/(^|[;{}\n]\s*)([A-Za-z_]\w*)\s*=\s*([^;]+);/g, (all, pre, lhs, rhs) => {
2215
2502
  const lw = width.get(lhs);
2216
2503
  if (!lw) return all;
2504
+ if (floatNames.has(lhs)) return all;
2217
2505
  const r = rhs.trim();
2218
2506
  if (new RegExp("^vec" + lw + "\\s*\\(").test(r)) return all;
2219
2507
  if (width.has(r)) return all;
@@ -2257,6 +2545,52 @@
2257
2545
  new RegExp(`\\bint\\s+([A-Za-z_]\\w*)\\s*=\\s*((?:${FLOAT_FNS})\\s*\\()`, "g"),
2258
2546
  "float $1 = $2"
2259
2547
  );
2548
+ code = code.replace(
2549
+ /\bfloat\s+([A-Za-z_]\w*)\s*=\s*(int\s*\([^;]*\))\s*;/g,
2550
+ "float $1 = float($2);"
2551
+ );
2552
+ {
2553
+ const intNames = collectIntNames(code);
2554
+ if (intNames.size > 0) {
2555
+ code = code.replace(
2556
+ /\b(const\s+)?float\s+([A-Za-z_]\w*)\s*=\s*([^;{}]+);/g,
2557
+ (all, cst, name, rhs) => {
2558
+ const body = rhs.trim();
2559
+ if (/\./.test(body)) return all;
2560
+ if (/[A-Za-z_]\w*\s*\(/.test(body)) return all;
2561
+ const ids = body.match(/[A-Za-z_]\w*/g);
2562
+ if (!ids || !ids.length) return all;
2563
+ if (!ids.every((x) => intNames.has(x))) return all;
2564
+ return `${cst || ""}float ${name} = float(${body});`;
2565
+ }
2566
+ );
2567
+ }
2568
+ }
2569
+ {
2570
+ const intNames = collectIntNames(code);
2571
+ const floatNames = /* @__PURE__ */ new Set();
2572
+ let fm;
2573
+ const fDeclRe = /\b(?:const\s+|uniform\s+|varying\s+|in\s+|out\s+)*float\s+([A-Za-z_]\w*)/g;
2574
+ while ((fm = fDeclRe.exec(code)) !== null) floatNames.add(fm[1]);
2575
+ for (const n of [...intNames]) {
2576
+ if (floatNames.has(n)) {
2577
+ intNames.delete(n);
2578
+ floatNames.delete(n);
2579
+ }
2580
+ }
2581
+ if (intNames.size > 0 && floatNames.size > 0) {
2582
+ const iAlt = [...intNames].sort((a, b) => b.length - a.length).join("|");
2583
+ const fAlt = [...floatNames].sort((a, b) => b.length - a.length).join("|");
2584
+ code = code.replace(
2585
+ new RegExp(`\\b(${iAlt})\\s*([*/+-])\\s*(${fAlt})\\b`, "g"),
2586
+ (all, a, op, b) => `float(${a}) ${op} ${b}`
2587
+ );
2588
+ code = code.replace(
2589
+ new RegExp(`\\b(${fAlt})\\s*([*/+-])\\s*(${iAlt})\\b`, "g"),
2590
+ (all, a, op, b) => `${a} ${op} float(${b})`
2591
+ );
2592
+ }
2593
+ }
2260
2594
  {
2261
2595
  const boolNames = /* @__PURE__ */ new Set();
2262
2596
  const boolRe = /\bbool\s+([A-Za-z_]\w*)\s*=/g;
@@ -2270,6 +2604,17 @@
2270
2604
  );
2271
2605
  }
2272
2606
  }
2607
+ {
2608
+ const CMP = /\(\s*([^()&|]+?)\s*(<=|>=|<|>|==|!=)\s*([^()&|]+?)\s*\)/g;
2609
+ code = code.replace(
2610
+ new RegExp(CMP.source + "\\s*([*/])", "g"),
2611
+ (all, lhs, op, rhs, mulOp) => `float(${lhs.trim()} ${op} ${rhs.trim()}) ${mulOp}`
2612
+ );
2613
+ code = code.replace(
2614
+ new RegExp("([-+*/]=\\s*)" + CMP.source, "g"),
2615
+ (all, assign, lhs, op, rhs) => `${assign}float(${lhs.trim()} ${op} ${rhs.trim()})`
2616
+ );
2617
+ }
2273
2618
  {
2274
2619
  const names = /* @__PURE__ */ new Set();
2275
2620
  for (const fm of code.matchAll(/\b(?:uniform\s+)?(?:highp|mediump|lowp\s+)?float\s+([A-Za-z_]\w*)\s*\[/g)) {
@@ -2288,6 +2633,17 @@
2288
2633
  let p = close1 + 1;
2289
2634
  while (p < code.length && /[ \t]/.test(code[p])) p++;
2290
2635
  if (code[p] !== "[") {
2636
+ const e12 = code.slice(open1 + 1, close1);
2637
+ const t = e12.trim();
2638
+ const isFloatish = /\d\.\d/.test(t) || /^[A-Za-z_]\w*$/.test(t) && new RegExp("\\bfloat\\s+" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b").test(code);
2639
+ const alreadyInt = /^\s*int\s*\(/.test(t) || /^-?\d+$/.test(t);
2640
+ if (isFloatish && !alreadyInt) {
2641
+ out += code.slice(last, fm.index);
2642
+ out += fm[1] + "[int(" + t + ")]";
2643
+ last = close1 + 1;
2644
+ re.lastIndex = last;
2645
+ continue;
2646
+ }
2291
2647
  re.lastIndex = close1 + 1;
2292
2648
  continue;
2293
2649
  }
@@ -2428,6 +2784,59 @@
2428
2784
  }).join("\n");
2429
2785
  }
2430
2786
  }
2787
+ code = code.replace(
2788
+ /^(\s*in\s+(?:highp|mediump|lowp\s+)?)(vec[234]|float)(\s+)([A-Za-z_]\w*)(\s*;)/gm,
2789
+ (all, pre, ty, sp, name, tail) => {
2790
+ const vt = vertTypes.get(name);
2791
+ if (!vt || RANK[vt] >= RANK[ty]) return all;
2792
+ const CH = "xyzw";
2793
+ const RG = "rgba";
2794
+ const over = new RegExp(
2795
+ "\\b" + name + "\\s*\\.\\s*[" + CH + RG + "]*[" + CH.slice(RANK[vt]) + RG.slice(RANK[vt]) + "]"
2796
+ );
2797
+ if (over.test(code)) return all;
2798
+ return pre + vt + sp + name + tail;
2799
+ }
2800
+ );
2801
+ }
2802
+ {
2803
+ const inVecN = /* @__PURE__ */ new Map();
2804
+ const declRe = /^\s*in\s+(?:highp|mediump|lowp\s+)?(vec[34])\s+([A-Za-z_]\w*)\s*;/gm;
2805
+ let dm;
2806
+ while ((dm = declRe.exec(code)) !== null) inVecN.set(dm[2], Number(dm[1].slice(3)));
2807
+ const localVecN = new Map(inVecN);
2808
+ const locRe = /\b(vec[34])\s+([A-Za-z_]\w*)\s*[=;]/g;
2809
+ while ((dm = locRe.exec(code)) !== null) {
2810
+ if (!localVecN.has(dm[2])) localVecN.set(dm[2], Number(dm[1].slice(3)));
2811
+ }
2812
+ if (localVecN.size > 0) {
2813
+ const swizzleUvArg = (arg) => {
2814
+ const t = arg.trim();
2815
+ if (!t) return arg;
2816
+ const bare = /^([A-Za-z_]\w*)$/.exec(t);
2817
+ if (bare && localVecN.has(bare[1])) return bare[1] + ".xy";
2818
+ const bin = /^([A-Za-z_]\w*)(\s*[+\-].+)$/.exec(t);
2819
+ if (bin && localVecN.has(bin[1])) return "(" + bin[1] + ".xy" + bin[2] + ")";
2820
+ return arg;
2821
+ };
2822
+ for (const fn of ["textureLod", "texture"]) {
2823
+ code = rewriteCall(code, fn, (inner) => {
2824
+ const args = splitArgs(inner);
2825
+ if (args.length >= 2) args[1] = swizzleUvArg(args[1]);
2826
+ return fn + "(" + args.join(", ") + ")";
2827
+ });
2828
+ }
2829
+ }
2830
+ if (inVecN.size > 0) {
2831
+ code = code.split("\n").map((line) => {
2832
+ if (!/\bvec2\s+[A-Za-z_]\w*\s*=/.test(line)) return line;
2833
+ let out = line;
2834
+ for (const name of inVecN.keys()) {
2835
+ out = out.replace(new RegExp("\\b" + name + "\\b(?!\\s*[.\\w])", "g"), name + ".xy");
2836
+ }
2837
+ return out;
2838
+ }).join("\n");
2839
+ }
2431
2840
  }
2432
2841
  const written = /* @__PURE__ */ new Set();
2433
2842
  const inNames = /* @__PURE__ */ new Set();
@@ -2448,18 +2857,27 @@
2448
2857
  let body = code.slice(braceIdx + 1);
2449
2858
  const decls = [];
2450
2859
  for (const name of written) {
2860
+ const shadowed = new RegExp(
2861
+ "(?:^|[;{}\\n])\\s*(?:highp|mediump|lowp\\s+)?(?:vec[234]|float|int|bool)\\s+" + name + "\\s*[=;]"
2862
+ ).test(body);
2863
+ if (shadowed) continue;
2451
2864
  const tm = new RegExp("^\\s*in\\s+(?:highp|mediump|lowp\\s+)?(vec[234]|float)\\s+" + name + "\\s*;", "m").exec(code);
2452
2865
  const ty = tm ? tm[1] : "vec4";
2453
2866
  decls.push(" " + ty + " " + name + "_rw = " + name + ";");
2454
2867
  body = replaceWord(body, name, name + "_rw");
2455
2868
  }
2456
- body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
2869
+ if (decls.length > 0) {
2870
+ body = "\n" + decls.map((d) => d.replace(/= (\w+)_rw;/, "= $1;")).join("\n") + "\n" + body;
2871
+ }
2457
2872
  code = head + body;
2458
2873
  }
2459
2874
  }
2460
2875
  }
2461
2876
  code = code.replace(/\[(?:unroll|loop|branch|flatten)\]\s*/g, "");
2462
2877
  code = code.replace(/\bstatic\s+/g, "");
2878
+ if (forHoles.length > 0) {
2879
+ code = code.replace(/\u0003(\u0004+)\u0003/g, (m, marks) => forHoles[marks.length - 1]);
2880
+ }
2463
2881
  {
2464
2882
  const floatNames = /* @__PURE__ */ new Set();
2465
2883
  for (const m of code.matchAll(/\b(?:uniform[ \t]+)?(?:highp|mediump|lowp)?[ \t]*float[ \t]+([A-Za-z_]\w*)[ \t]*[;=]/g)) {
@@ -2489,6 +2907,9 @@
2489
2907
  return line;
2490
2908
  }).join("\n");
2491
2909
  }
2910
+ if (sciHoles.length > 0) {
2911
+ code = code.replace(/\u0001(\u0002+)\u0001/g, (m, marks) => sciHoles[marks.length - 1]);
2912
+ }
2492
2913
  let prologue = "#version 300 es\n";
2493
2914
  if (stage === "vert") {
2494
2915
  prologue += "precision highp float;\n";
@@ -2724,7 +3145,8 @@ void main() {
2724
3145
  return s;
2725
3146
  }
2726
3147
  function parseVec3Local(s) {
2727
- const p = String(s).trim().split(/\s+/).map(Number);
3148
+ if (s !== null && typeof s === "object" && "value" in s) s = s.value;
3149
+ const p = String(s ?? "").trim().split(/\s+/).map(Number);
2728
3150
  return [p[0] || 0, p[1] || 0, p[2] || 0];
2729
3151
  }
2730
3152
  function makeTexture(gl, rgba, width, height, bitmap = null) {
@@ -3273,7 +3695,11 @@ void main() {
3273
3695
  }
3274
3696
  }
3275
3697
  const key = shaderName + "|" + JSON.stringify(effectiveCombos);
3276
- if (progCache.has(key)) return progCache.get(key);
3698
+ if (progCache.has(key)) {
3699
+ const hit = progCache.get(key);
3700
+ if (hit === null) throw new Error("shader=" + shaderName + " 编译失败(已缓存)");
3701
+ return hit;
3702
+ }
3277
3703
  for (let attempt = 0; attempt < 4; attempt++) {
3278
3704
  const missing = /* @__PURE__ */ new Set();
3279
3705
  const resolver = (file) => {
@@ -3288,6 +3714,7 @@ void main() {
3288
3714
  try {
3289
3715
  prog = linkProgram(gl, vertGlsl, fragGlsl);
3290
3716
  } catch (e) {
3717
+ progCache.set(key, null);
3291
3718
  throw new Error("shader=" + shaderName + " " + (e && e.message));
3292
3719
  }
3293
3720
  const uni = /* @__PURE__ */ new Map();
@@ -3298,8 +3725,9 @@ void main() {
3298
3725
  uni.set(base, { loc: gl.getUniformLocation(prog, info.name), type: GL_TYPES[info.type] || "unknown", size: info.size });
3299
3726
  }
3300
3727
  const matMeta = { ...parseMaterialMeta(src.vert), ...parseMaterialMeta(src.frag) };
3728
+ const ndcDirect = /gl_Position\s*=\s*vec4\s*\(\s*a_Position/.test(src.vert) && !/[aA]_Position[\s\S]{0,40}mul\s*\(/.test(src.vert);
3301
3729
  const samplerDefaults = new Map([...parseSamplerDefaults(src.vert), ...parseSamplerDefaults(src.frag)]);
3302
- const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl };
3730
+ const entry = { prog, uni, matMeta, samplerDefaults, fragGlsl, vertGlsl, ndcDirect };
3303
3731
  progCache.set(key, entry);
3304
3732
  return entry;
3305
3733
  }
@@ -4051,7 +4479,13 @@ void main() {
4051
4479
  }
4052
4480
  const drawLayers = cam.perspective ? scene.layers.slice().sort((a, b) => Number(!!b.isSkybox) - Number(!!a.isSkybox)) : scene.layers;
4053
4481
  for (const layer of drawLayers) {
4054
- if (!layer.visible || layer.destroyed) continue;
4482
+ if (layer.destroyed) continue;
4483
+ if (!layer.visible) {
4484
+ if (pendingEmptyCompose.has(layer.id)) {
4485
+ captureEmptyComposeAtZOrder(layer, cam, viewProj, width, height);
4486
+ }
4487
+ continue;
4488
+ }
4055
4489
  if (layer.isPostProcess && !(layer.effects || []).some((e) => e.visible)) continue;
4056
4490
  if (groupChildIds.has(layer.id)) continue;
4057
4491
  if (layer.isContainer) {
@@ -4481,7 +4915,10 @@ void main() {
4481
4915
  try {
4482
4916
  progEntry = await getEffectProgram(mp.shader, combos, mergedTex);
4483
4917
  } catch (e) {
4484
- console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, e && e.message || e);
4918
+ const msg = e && e.message || String(e);
4919
+ if (!/已缓存/.test(msg)) {
4920
+ console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, msg);
4921
+ }
4485
4922
  failedEffects.add(eff2);
4486
4923
  continue;
4487
4924
  }
@@ -4507,7 +4944,13 @@ void main() {
4507
4944
  gl.bindFramebuffer(gl.FRAMEBUFFER, outFBO.fbo);
4508
4945
  gl.viewport(0, 0, outFBO.width, outFBO.height);
4509
4946
  gl.bindVertexArray(vao);
4510
- uploadQuad("pass", PASS_QUAD);
4947
+ const usePixelQuad = !progEntry.ndcDirect;
4948
+ if (usePixelQuad) {
4949
+ uploadQuad("passPx" + outFBO.width + "x" + outFBO.height, layerQuad(outFBO.width, outFBO.height));
4950
+ } else {
4951
+ uploadQuad("pass", PASS_QUAD);
4952
+ }
4953
+ const passMVP = usePixelQuad ? mat4Transpose(mat4Ortho(0, outFBO.width, 0, outFBO.height, -1e4, 1e4)) : IDENT_M4;
4511
4954
  const texNames = mp.textures || [];
4512
4955
  const maxTex = Math.max(texNames.length, 8);
4513
4956
  const resolutions = /* @__PURE__ */ new Map();
@@ -4536,7 +4979,7 @@ void main() {
4536
4979
  usedUnits.add(ti);
4537
4980
  resolutions.set(ti, [t.width, t.height, t.width, t.height]);
4538
4981
  }
4539
- bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, IDENT_M4, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
4982
+ bindSystemUniforms(uni, layer, time, cam.projW, cam.projH, passMVP, layerOrtho, IDENT_M4, resolutions, layerOrtho, cam);
4540
4983
  bindConstants(
4541
4984
  uni,
4542
4985
  animatedConstants(
@@ -4831,6 +5274,7 @@ layout(location=1) in vec3 a_pos; // 实例中心(投影空间世界
4831
5274
  layout(location=2) in vec2 a_sizeRot; // x=size(像素) y=rot(弧度)
4832
5275
  layout(location=3) in vec4 a_color; // rgb + alpha
4833
5276
  layout(location=4) in vec3 a_stretchFrame; // xy=非等比拉伸 z=帧序号
5277
+ layout(location=5) in vec2 a_vrange; // 段两端沿贴图 v 的取值(rope 连线用;普通精灵 0..1)
4834
5278
  uniform mat4 u_mvp;
4835
5279
  // 序列帧 uv 变换表(TEXS 帧矩形归一化后的 offset/scale),最多 128 帧
4836
5280
  // (matrix spritesheet 72 有 71 帧,旧上限 64 会丢末尾字符)
@@ -4848,7 +5292,8 @@ void main(){
4848
5292
  gl_Position = u_mvp * vec4(a_pos.xy + rotated, a_pos.z, 1.0);
4849
5293
  // quad 角 → 贴图 uv。世界 y 已翻转到投影空间(y 向下),故 quad 的 +y 角
4850
5294
  // 对应屏幕上方,应采样纹理顶行 v=1(与 renderer.js 的 layerQuadVerts 同约定)。
4851
- vec2 uv = a_corner + 0.5;
5295
+ // a_vrange rope 段两端各取自己的 v(沿绳连续渐变);普通精灵是 (0,1) 恒等。
5296
+ vec2 uv = vec2(a_corner.x + 0.5, mix(a_vrange.x, a_vrange.y, a_corner.y + 0.5));
4852
5297
  if (u_frameCount > 0) {
4853
5298
  // 帧矩形以左上为原点(TEXS 是 top-down 像素坐标),故先把 v 翻成 top-down
4854
5299
  int fi = int(a_stretchFrame.z);
@@ -4922,7 +5367,7 @@ void main(){
4922
5367
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
4923
5368
  gl.vertexAttribDivisor(0, 0);
4924
5369
  gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
4925
- const S = 48;
5370
+ const S = 56;
4926
5371
  gl.enableVertexAttribArray(1);
4927
5372
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, S, 0);
4928
5373
  gl.vertexAttribDivisor(1, 1);
@@ -4935,6 +5380,9 @@ void main(){
4935
5380
  gl.enableVertexAttribArray(4);
4936
5381
  gl.vertexAttribPointer(4, 3, gl.FLOAT, false, S, 36);
4937
5382
  gl.vertexAttribDivisor(4, 1);
5383
+ gl.enableVertexAttribArray(5);
5384
+ gl.vertexAttribPointer(5, 2, gl.FLOAT, false, S, 48);
5385
+ gl.vertexAttribDivisor(5, 1);
4938
5386
  gl.bindVertexArray(null);
4939
5387
  return {
4940
5388
  prog: {
@@ -4986,6 +5434,20 @@ void main(){
4986
5434
  function particleInstanceSegs(trailCfg, trailSegments) {
4987
5435
  return trailCfg && trailCfg.kind === "ropetrail" ? Math.max(1, trailSegments || 1) : 1;
4988
5436
  }
5437
+ function ropeTrailHistoryCount(cfg) {
5438
+ if (!cfg || cfg.kind !== "ropetrail") return 1;
5439
+ const segs = Math.round(Number(cfg.segments) || 0);
5440
+ if (segs >= 2) return Math.min(32, segs);
5441
+ return 8;
5442
+ }
5443
+ function ropeTrailDuration(cfg) {
5444
+ if (!cfg || cfg.kind !== "ropetrail") return 0;
5445
+ const L = Number(cfg.length);
5446
+ return Number.isFinite(L) && L > 0 ? L : 0.2;
5447
+ }
5448
+ function ropeParticleV(p) {
5449
+ return p.life > 0 ? p.age / p.life : 0;
5450
+ }
4989
5451
  class Particle {
4990
5452
  constructor() {
4991
5453
  this.alive = false;
@@ -5014,6 +5476,8 @@ void main(){
5014
5476
  this.turbSpeed = 0;
5015
5477
  this.turbPhase = 0;
5016
5478
  this.trail = null;
5479
+ this.trailClock = 0;
5480
+ this.seq = 0;
5017
5481
  }
5018
5482
  }
5019
5483
  class ParticleSystem {
@@ -5022,20 +5486,7 @@ void main(){
5022
5486
  this.model = model || {};
5023
5487
  this.override = override || {};
5024
5488
  this.layer = layer || null;
5025
- const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
5026
- const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
5027
- const la = layer && layer.angles ? layer.angles : [0, 0, 0];
5028
- this.originX = lo[0] || 0;
5029
- this.originY = lo[1] || 0;
5030
- this.originZ = lo[2] || 0;
5031
- this.scaleX = ls[0] === 0 ? 1 : ls[0];
5032
- this.scaleY = ls[1] === 0 ? 1 : ls[1];
5033
- this.angleZ = (la[2] || 0) * Math.PI / 180;
5034
- const asx = Math.abs(this.scaleX);
5035
- const asy = Math.abs(this.scaleY);
5036
- this.sysScale = Math.min(asx, asy) || 1;
5037
- this.spriteStretchX = asx / this.sysScale;
5038
- this.spriteStretchY = asy / this.sysScale;
5489
+ this.syncLayerTransform();
5039
5490
  this.maxCount = Math.max(1, Math.min(2e4, num(this.model.maxcount, 100)));
5040
5491
  this.simTime = 0;
5041
5492
  this.paused = false;
@@ -5074,6 +5525,9 @@ void main(){
5074
5525
  this._followParent = null;
5075
5526
  this._followMode = null;
5076
5527
  this._followOffset = [0, 0, 0];
5528
+ this.ropeRenderer = null;
5529
+ this._seq = 0;
5530
+ this._ropeOrder = [];
5077
5531
  this._ov = {};
5078
5532
  this._applyOverride();
5079
5533
  this.startTime = Math.max(0, Math.min(30, num(this.model.starttime, 0)));
@@ -5350,24 +5804,34 @@ void main(){
5350
5804
  const kind = r && r.name || "sprite";
5351
5805
  return {
5352
5806
  kind,
5353
- length: num(r && r.length, kind === "spritetrail" ? 0.1 : 0),
5807
+ length: num(r && r.length, kind === "spritetrail" ? 0.1 : kind === "ropetrail" ? 0.2 : 0),
5354
5808
  maxLength: num(r && r.maxlength, 0),
5355
5809
  minLength: num(r && r.minlength, 0),
5356
5810
  subdivision: num(r && r.subdivision, 1),
5811
+ // Rope Trail 段数(官方 `segments`);与 spritetrail 的 maxlength 无关
5812
+ segments: num(r && r.segments, 0),
5357
5813
  orientation: r && r.orientation || null
5358
5814
  };
5359
5815
  });
5360
5816
  if (omitted && !this.renderers.length) {
5361
- this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, orientation: null }];
5817
+ this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, segments: 0, orientation: null }];
5362
5818
  }
5363
5819
  const tr = this.renderers.find((r) => r.kind === "spritetrail" || r.kind === "ropetrail");
5364
5820
  this.trailCfg = tr || null;
5365
5821
  this.trailSegments = 1;
5822
+ this.trailDuration = 0;
5823
+ this.trailSampleDt = 0;
5366
5824
  if (tr && tr.kind === "ropetrail") {
5367
- const segs = Math.max(2, Math.min(16, Math.round(tr.maxLength || tr.subdivision || 6)));
5825
+ const segs = ropeTrailHistoryCount(tr);
5368
5826
  this.trailSegments = segs;
5369
- for (const p of this.pool) p.trail = new Float32Array(segs * 3);
5827
+ this.trailDuration = ropeTrailDuration(tr);
5828
+ this.trailSampleDt = this.trailDuration / Math.max(1, segs - 1);
5829
+ for (const p of this.pool) {
5830
+ p.trail = new Float32Array(segs * 3);
5831
+ p.trailClock = 0;
5832
+ }
5370
5833
  }
5834
+ this.ropeRenderer = this.renderers.find((r) => r.kind === "rope") || null;
5371
5835
  }
5372
5836
  setModel(model) {
5373
5837
  this.model = model;
@@ -5427,6 +5891,31 @@ void main(){
5427
5891
  setVisible(v) {
5428
5892
  this.visible = v;
5429
5893
  }
5894
+ /**
5895
+ * [we-scene patch] 从图层重新读取变换(构造时也走这里)。
5896
+ *
5897
+ * 发射器变换原先只在构造时缓存一次、之后**从不刷新**。父组一旦带脚本/动画
5898
+ * 变换(全库 17 个粒子层有脚本化祖先),图层被 recomposeWorld 挪走了,
5899
+ * 粒子却仍从旧位置喷出来 —— 画面上是「人物滑走了、他的火焰留在原地」。
5900
+ * 宿主在 recompose 之后对脏子树里的粒子层调用本方法。
5901
+ */
5902
+ syncLayerTransform() {
5903
+ const layer = this.layer;
5904
+ const lo = layer && layer.origin ? layer.origin : [0, 0, 0];
5905
+ const ls = layer && layer.scale ? layer.scale : [1, 1, 1];
5906
+ const la = layer && layer.angles ? layer.angles : [0, 0, 0];
5907
+ this.originX = lo[0] || 0;
5908
+ this.originY = lo[1] || 0;
5909
+ this.originZ = lo[2] || 0;
5910
+ this.scaleX = ls[0] === 0 ? 1 : ls[0];
5911
+ this.scaleY = ls[1] === 0 ? 1 : ls[1];
5912
+ this.angleZ = (la[2] || 0) * Math.PI / 180;
5913
+ const asx = Math.abs(this.scaleX);
5914
+ const asy = Math.abs(this.scaleY);
5915
+ this.sysScale = Math.min(asx, asy) || 1;
5916
+ this.spriteStretchX = asx / this.sysScale;
5917
+ this.spriteStretchY = asy / this.sysScale;
5918
+ }
5430
5919
  // 宿主每帧提供鼠标位置(世界像素);转到局部空间供控制点使用
5431
5920
  setPointer(worldX, worldY) {
5432
5921
  const dx = worldX - this.originX;
@@ -5498,6 +5987,7 @@ void main(){
5498
5987
  p.rotVel = 0;
5499
5988
  p.vx = p.vy = p.vz = 0;
5500
5989
  p.frame = 0;
5990
+ p.seq = this._seq++;
5501
5991
  const o = em.origin;
5502
5992
  if (em.kind === "box") {
5503
5993
  const d = em.distanceMax || [0, 0, 0];
@@ -5644,6 +6134,7 @@ void main(){
5644
6134
  p.trail[i + 1] = p.y;
5645
6135
  p.trail[i + 2] = p.z;
5646
6136
  }
6137
+ p.trailClock = 0;
5647
6138
  }
5648
6139
  }
5649
6140
  // ---------- 每粒子更新 ----------
@@ -5794,10 +6285,20 @@ void main(){
5794
6285
  }
5795
6286
  if (p.trail) {
5796
6287
  const tr = p.trail;
5797
- for (let i = tr.length - 3; i >= 3; i -= 3) {
5798
- tr[i] = tr[i - 3];
5799
- tr[i + 1] = tr[i - 2];
5800
- tr[i + 2] = tr[i - 1];
6288
+ const step = this.trailSampleDt;
6289
+ if (step > 0) {
6290
+ p.trailClock = (p.trailClock || 0) + dt;
6291
+ let shifts = 0;
6292
+ const cap = this.trailSegments || 8;
6293
+ while (p.trailClock >= step && shifts < cap) {
6294
+ p.trailClock -= step;
6295
+ shifts++;
6296
+ for (let i = tr.length - 3; i >= 3; i -= 3) {
6297
+ tr[i] = tr[i - 3];
6298
+ tr[i + 1] = tr[i - 2];
6299
+ tr[i + 2] = tr[i - 1];
6300
+ }
6301
+ }
5801
6302
  }
5802
6303
  tr[0] = p.x;
5803
6304
  tr[1] = p.y;
@@ -5892,13 +6393,22 @@ void main(){
5892
6393
  if (!this._prog) this._buildProgram(gl);
5893
6394
  const trail = this.trailCfg && this.trailCfg.kind === "ropetrail" ? this.trailCfg : null;
5894
6395
  const spriteTrail = this.trailCfg && this.trailCfg.kind === "spritetrail" ? this.trailCfg : null;
6396
+ const rope = this.ropeRenderer;
5895
6397
  const segs = particleInstanceSegs(this.trailCfg, this.trailSegments);
5896
- const STRIDE = 12;
6398
+ const STRIDE = 14;
5897
6399
  const pool = this.pool;
6400
+ let order = null;
6401
+ if (rope) {
6402
+ order = this._ropeOrder;
6403
+ order.length = 0;
6404
+ for (let i = 0; i < pool.length; i++) if (pool[i].alive) order.push(pool[i]);
6405
+ order.sort((a, b) => a.seq - b.seq);
6406
+ }
5898
6407
  let live = 0;
5899
- for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
5900
- if (live === 0) return;
5901
- const instCount = live * segs;
6408
+ if (order) live = order.length;
6409
+ else for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
6410
+ if (live === 0 || rope && live < 2) return;
6411
+ const instCount = rope ? live - 1 : live * segs;
5902
6412
  const need = instCount * STRIDE;
5903
6413
  if (!this._data || this._data.length < need) this._data = new Float32Array(Math.max(need, 1024));
5904
6414
  const data = this._data;
@@ -5918,7 +6428,34 @@ void main(){
5918
6428
  const py = ly * sy;
5919
6429
  return [ox + px * cos - py * sin, projH - (oy + px * sin + py * cos)];
5920
6430
  };
5921
- for (let i = 0; i < pool.length; i++) {
6431
+ if (rope) {
6432
+ for (let i = 0; i + 1 < live; i++) {
6433
+ const a = order[i];
6434
+ const b = order[i + 1];
6435
+ const wa = toWorld(a.x, a.y);
6436
+ const wb = toWorld(b.x, b.y);
6437
+ const dx = wb[0] - wa[0];
6438
+ const dy = wb[1] - wa[1];
6439
+ const dist = Math.hypot(dx, dy);
6440
+ const width2 = (Math.abs(a.size) + Math.abs(b.size)) * 0.5 * sysScale;
6441
+ if (!(width2 > 0)) continue;
6442
+ data[k++] = (wa[0] + wb[0]) * 0.5;
6443
+ data[k++] = (wa[1] + wb[1]) * 0.5;
6444
+ data[k++] = 0;
6445
+ data[k++] = width2;
6446
+ data[k++] = Math.atan2(-dx, dy);
6447
+ data[k++] = (a.r + b.r) * 0.5 * bright;
6448
+ data[k++] = (a.g + b.g) * 0.5 * bright;
6449
+ data[k++] = (a.b + b.b) * 0.5 * bright;
6450
+ data[k++] = (a.alpha + b.alpha) * 0.5;
6451
+ data[k++] = 1;
6452
+ data[k++] = dist / width2;
6453
+ data[k++] = 0;
6454
+ data[k++] = ropeParticleV(a);
6455
+ data[k++] = ropeParticleV(b);
6456
+ }
6457
+ }
6458
+ for (let i = 0; i < pool.length && !rope; i++) {
5922
6459
  const p = pool[i];
5923
6460
  if (!p.alive) continue;
5924
6461
  for (let s = 0; s < segs; s++) {
@@ -5926,18 +6463,53 @@ void main(){
5926
6463
  let ly = p.y;
5927
6464
  let segAlpha = 1;
5928
6465
  let segSize = 1;
6466
+ let rot = p.rot;
6467
+ let instStretchX = stretchX;
6468
+ let instStretchY = stretchY;
6469
+ let wx;
6470
+ let wy;
5929
6471
  if (trail && p.trail) {
5930
6472
  lx = p.trail[s * 3];
5931
6473
  ly = p.trail[s * 3 + 1];
5932
6474
  const t = segs > 1 ? s / (segs - 1) : 0;
5933
6475
  segAlpha = 1 - t;
5934
6476
  segSize = 1 - t * 0.55;
6477
+ let tdx = 0;
6478
+ let tdy = 0;
6479
+ if (s + 1 < segs) {
6480
+ tdx = p.trail[s * 3] - p.trail[(s + 1) * 3];
6481
+ tdy = p.trail[s * 3 + 1] - p.trail[(s + 1) * 3 + 1];
6482
+ } else if (s > 0) {
6483
+ tdx = p.trail[(s - 1) * 3] - p.trail[s * 3];
6484
+ tdy = p.trail[(s - 1) * 3 + 1] - p.trail[s * 3 + 1];
6485
+ } else {
6486
+ tdx = p.vx;
6487
+ tdy = p.vy;
6488
+ }
6489
+ const w0 = toWorld(lx, ly);
6490
+ const w1 = toWorld(lx - tdx, ly - tdy);
6491
+ const dx = w0[0] - w1[0];
6492
+ const dy = w0[1] - w1[1];
6493
+ const dist = Math.hypot(dx, dy);
6494
+ const base = Math.max(1e-3, Math.abs(p.size) * sysScale * segSize);
6495
+ if (dist > 1e-3) {
6496
+ rot = spriteTrailRotation(dx, dy);
6497
+ wx = (w0[0] + w1[0]) * 0.5;
6498
+ wy = (w0[1] + w1[1]) * 0.5;
6499
+ instStretchY = Math.max(stretchY, dist / base);
6500
+ } else {
6501
+ wx = w0[0];
6502
+ wy = w0[1];
6503
+ }
6504
+ } else {
6505
+ const w = toWorld(lx, ly);
6506
+ wx = w[0];
6507
+ wy = w[1];
5935
6508
  }
5936
- const w = toWorld(lx, ly);
5937
- let rot = p.rot;
5938
- let instStretchX = stretchX;
5939
- let instStretchY = stretchY;
5940
6509
  if (spriteTrail) {
6510
+ const w = toWorld(p.x, p.y);
6511
+ wx = w[0];
6512
+ wy = w[1];
5941
6513
  const w1 = toWorld(p.x + p.vx, p.y + p.vy);
5942
6514
  rot = spriteTrailRotation(w1[0] - w[0], w1[1] - w[1]);
5943
6515
  const factor = spriteTrailLengthFactor(
@@ -5948,8 +6520,8 @@ void main(){
5948
6520
  );
5949
6521
  instStretchY = stretchY * factor;
5950
6522
  }
5951
- data[k++] = w[0];
5952
- data[k++] = w[1];
6523
+ data[k++] = wx;
6524
+ data[k++] = wy;
5953
6525
  data[k++] = 0;
5954
6526
  data[k++] = Math.abs(p.size) * sysScale * segSize;
5955
6527
  data[k++] = rot;
@@ -5960,6 +6532,8 @@ void main(){
5960
6532
  data[k++] = instStretchX;
5961
6533
  data[k++] = instStretchY;
5962
6534
  data[k++] = p.frame;
6535
+ data[k++] = 0;
6536
+ data[k++] = 1;
5963
6537
  }
5964
6538
  }
5965
6539
  const prog = this._prog;
@@ -6093,6 +6667,9 @@ void main(){
6093
6667
  particleInstanceSegs,
6094
6668
  particlePassRefract,
6095
6669
  rgbaIsBlankWhite,
6670
+ ropeParticleV,
6671
+ ropeTrailDuration,
6672
+ ropeTrailHistoryCount,
6096
6673
  spriteTrailLengthFactor,
6097
6674
  spriteTrailRotation
6098
6675
  }, Symbol.toStringTag, { value: "Module" }));
@@ -6198,7 +6775,7 @@ void main(){
6198
6775
  return { width: size, height: size, rgba };
6199
6776
  }
6200
6777
  function beam(w, h, coreWidth, fadeBoth, peak) {
6201
- const pk = peak === void 0 ? 0.55 : peak;
6778
+ const pk = 0.55;
6202
6779
  const rgba = new Uint8Array(w * h * 4);
6203
6780
  for (let y = 0; y < h; y++) {
6204
6781
  const ty = y / (h - 1);
@@ -6211,6 +6788,41 @@ void main(){
6211
6788
  }
6212
6789
  return { width: w, height: h, rgba };
6213
6790
  }
6791
+ const RAIN_FRAMES = 4;
6792
+ function rainStreak(w, h, tiltDeg, corePx, peak) {
6793
+ const pk = peak === void 0 ? 0.85 : peak;
6794
+ const rgba = new Uint8Array(w * h * 4);
6795
+ const fh = h / RAIN_FRAMES;
6796
+ const rng = mulberry32$1(335009);
6797
+ const tilt = Math.tan(tiltDeg * Math.PI / 180);
6798
+ const cx = w / 2;
6799
+ for (let f = 0; f < RAIN_FRAMES; f++) {
6800
+ const peakF = pk * (0.8 + rng() * 0.35);
6801
+ const coreF = corePx * (0.85 + rng() * 0.5);
6802
+ const ph = (rng() - 0.5) * w * 0.12;
6803
+ for (let y = 0; y < fh; y++) {
6804
+ const ty = y / (fh - 1);
6805
+ const lineX = cx + ph + (fh - 1) * tilt / 2 - (fh - 1) * tilt * ty;
6806
+ const vy = gauss(ty - 0.5, 0.27);
6807
+ for (let x = 0; x < w; x++) {
6808
+ const d = Math.abs(x + 0.5 - lineX);
6809
+ const g = Math.exp(-(d * d) / (coreF * coreF));
6810
+ const a = g * vy * peakF;
6811
+ if (a < 4e-3) continue;
6812
+ writeWhite(rgba, ((f * fh + y) * w + x) * 4, a);
6813
+ }
6814
+ }
6815
+ }
6816
+ return { width: w, height: h, rgba };
6817
+ }
6818
+ function builtinParticleFrames(name) {
6819
+ if (name === "particle/nature/rain1" || name === "particle/nature/rain2") {
6820
+ const list = [];
6821
+ for (let i = 0; i < RAIN_FRAMES; i++) list.push({ ou: 0, ov: i / RAIN_FRAMES, su: 1, sv: 1 / RAIN_FRAMES });
6822
+ return list;
6823
+ }
6824
+ return null;
6825
+ }
6214
6826
  function ring(size, radius, thickness) {
6215
6827
  const rgba = new Uint8Array(size * size * 4);
6216
6828
  const half = size / 2;
@@ -6891,8 +7503,8 @@ void main(){
6891
7503
  // 水滴(原生 64×256 → 128×512):竖长泪滴
6892
7504
  "particle/drop": () => teardrop(128, 512),
6893
7505
  // 雨丝(原生 64×256 → 128×512):细长条,两端渐隐
6894
- "particle/nature/rain1": () => beam(128, 512, 0.16, true, 0.6),
6895
- "particle/nature/rain2": () => beam(128, 512, 0.24, true, 0.55),
7506
+ "particle/nature/rain1": () => rainStreak(128, 512, 10, 1, 0.78),
7507
+ "particle/nature/rain2": () => rainStreak(128, 512, 10, 1.6, 0.66),
6896
7508
  // 雨滴 sheet(原生 128×256 → 256×512,2×4 格):每格一颗上圆下尖小水滴
6897
7509
  "particle/water/rain_drops_sheet": () => dropSheet(256, 512, 2, 4),
6898
7510
  // 雾(原生 256 → 512):絮状 fBm,弱遮罩铺满;三张不同尺度/种子
@@ -7010,6 +7622,7 @@ void main(){
7010
7622
  const particleTexMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
7011
7623
  __proto__: null,
7012
7624
  buildBuiltinParticleTexture,
7625
+ builtinParticleFrames,
7013
7626
  isBuiltinParticleTextureName,
7014
7627
  listBuiltinParticleTextureNames,
7015
7628
  particleNormalNameForAlbedo,
@@ -7627,6 +8240,11 @@ void main(){
7627
8240
  c.origin[0] += d[0];
7628
8241
  c.origin[1] += d[1];
7629
8242
  }
8243
+ layer.parallaxDepth = parent.parallaxDepth ? parent.parallaxDepth.slice() : null;
8244
+ for (const c of desc) {
8245
+ c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
8246
+ }
8247
+ layer.attachBindDelta = [d[0], d[1]];
7630
8248
  follows.push({
7631
8249
  layer,
7632
8250
  parent,
@@ -7641,6 +8259,9 @@ void main(){
7641
8259
  f.baseY = f.layer.origin[1];
7642
8260
  f.subtree = [{ layer: f.layer, x: f.layer.origin[0], y: f.layer.origin[1] }];
7643
8261
  for (const c of f.desc) f.subtree.push({ layer: c, x: c.origin[0], y: c.origin[1] });
8262
+ for (const s of f.subtree) {
8263
+ if (!s.layer.attachBase) s.layer.attachBase = [s.x, s.y];
8264
+ }
7644
8265
  }
7645
8266
  return follows;
7646
8267
  }
@@ -7666,14 +8287,20 @@ void main(){
7666
8287
  }
7667
8288
  deltas.push(parentMeshToWorldDelta(f.parent, cur[12] - f.bindX, cur[13] - f.bindY));
7668
8289
  }
8290
+ const baseOf = (s) => {
8291
+ const ab = s.layer.attachBase;
8292
+ if (ab) return ab;
8293
+ return [s.x, s.y];
8294
+ };
7669
8295
  const seen = /* @__PURE__ */ new Set();
7670
8296
  for (const f of follows) {
7671
8297
  const tree = f.subtree || [{ layer: f.layer, x: f.baseX, y: f.baseY }];
7672
8298
  for (const s of tree) {
7673
8299
  if (seen.has(s.layer)) continue;
7674
8300
  seen.add(s.layer);
7675
- s.layer.origin[0] = s.x;
7676
- s.layer.origin[1] = s.y;
8301
+ const b = baseOf(s);
8302
+ s.layer.origin[0] = b[0];
8303
+ s.layer.origin[1] = b[1];
7677
8304
  }
7678
8305
  }
7679
8306
  for (let i = 0; i < follows.length; i++) {
@@ -7934,10 +8561,12 @@ void main() {
7934
8561
  const totalH = lines.length * lineHeight;
7935
8562
  const halign = opts.halign || "center";
7936
8563
  const valign = opts.valign || "center";
7937
- const y0 = valign === "top" ? pad : valign === "bottom" ? Math.max(pad, boxH - pad - totalH) : (boxH - totalH) / 2;
8564
+ const midX = boxW / 2;
8565
+ const midY = boxH / 2;
8566
+ const y0 = valign === "top" ? midY : valign === "bottom" ? midY - totalH : (boxH - totalH) / 2;
7938
8567
  const out = lines.map((text, i) => {
7939
8568
  const w = widths[i];
7940
- const x = halign === "left" ? pad : halign === "right" ? Math.max(pad, boxW - pad - w) : (boxW - w) / 2;
8569
+ const x = halign === "left" ? midX : halign === "right" ? midX - w : (boxW - w) / 2;
7941
8570
  return { text, width: w, x, y: y0 + i * lineHeight };
7942
8571
  });
7943
8572
  return { lines: out, lineHeight, totalH, truncated, boxW, boxH };
@@ -8312,6 +8941,11 @@ void main() {
8312
8941
  if (v !== null && typeof v === "object" && "value" in v) return v.value;
8313
8942
  return v;
8314
8943
  }
8944
+ function engineCanvasSize(cs) {
8945
+ const src = cs || { width: 1920, height: 1080 };
8946
+ if (src.x !== void 0 && src.y !== void 0) return src;
8947
+ return { x: src.width, y: src.height, width: src.width, height: src.height };
8948
+ }
8315
8949
  function evalTextScript(script, scriptprops, opts = {}) {
8316
8950
  if (typeof script !== "string" || script.length === 0) return null;
8317
8951
  const body = scriptToFunctionBody(script);
@@ -8353,7 +8987,7 @@ void main() {
8353
8987
  },
8354
8988
  frametime: 1 / 60,
8355
8989
  runtime: 0,
8356
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
8990
+ canvasSize: engineCanvasSize(opts.canvasSize),
8357
8991
  // [we-scene patch] engine.screenResolution(全库 10 处 / 2 壁纸):
8358
8992
  // 屏幕**像素**尺寸,脚本用它把 input.cursorScreenPosition 归一化
8359
8993
  // (3791967416 除它得 [0,1];3509243656 减半屏得 [-1,1])。
@@ -8553,6 +9187,19 @@ void main() {
8553
9187
  if (opts.onError) opts.onError(e, "applyUserProperties");
8554
9188
  }
8555
9189
  },
9190
+ /** 直调 update 不做文本加工:层可见性脚本(visible.script)要拿原始返回值
9191
+ * ——布尔控可见,callUpdate 会把它吞成 null(防画到画面上的文字版语义)。 */
9192
+ callUpdateRaw(value) {
9193
+ if (!fns.update || sandbox.disabled) return void 0;
9194
+ try {
9195
+ return fns.update(value);
9196
+ } catch (e) {
9197
+ sandbox.errCount++;
9198
+ if (opts.onError) opts.onError(e, "update");
9199
+ if (sandbox.errCount >= 3) sandbox.disabled = true;
9200
+ return void 0;
9201
+ }
9202
+ },
8556
9203
  /** 求值当前文本:返回新文本;undefined/null 保留原值;连续出错 3 次熔断回退静态文本 */
8557
9204
  callUpdate(value) {
8558
9205
  if (!fns.update || sandbox.disabled) return null;
@@ -9244,13 +9891,8 @@ void main() {
9244
9891
  Object.defineProperty(proxy, key, {
9245
9892
  enumerable: true,
9246
9893
  get() {
9247
- if (layer && Array.isArray(layer[key])) {
9248
- const a = layer[key];
9249
- store[key].x = a[0] || 0;
9250
- store[key].y = a[1] || 0;
9251
- store[key].z = a[2] || 0;
9252
- }
9253
- return store[key];
9894
+ const a = layer && Array.isArray(layer[key]) ? layer[key] : null;
9895
+ return makeVec3(a || [0, 0, 0]);
9254
9896
  },
9255
9897
  set(v) {
9256
9898
  const a = normVec(v);
@@ -9303,7 +9945,7 @@ void main() {
9303
9945
  },
9304
9946
  frametime: 1 / 60,
9305
9947
  runtime: 0,
9306
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
9948
+ canvasSize: engineCanvasSize(opts.canvasSize),
9307
9949
  screenResolution: opts.screenResolution || { x: 1920, y: 1080 },
9308
9950
  timeOfDay: typeof opts.timeOfDay === "number" ? opts.timeOfDay : 0,
9309
9951
  userProperties: opts.userProperties || {},
@@ -9446,7 +10088,8 @@ void main() {
9446
10088
  const hasCursorHook = !!(fns && (fns.cursorClick || fns.cursorEnter || fns.cursorLeave || fns.cursorDown || fns.cursorUp || fns.cursorMove));
9447
10089
  const hasMediaHook = !!(fns && MEDIA_CALLBACKS.some((n) => fns[n]));
9448
10090
  const hasApplyHook = !!(fns && typeof fns.applyUserProperties === "function");
9449
- if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook) return null;
10091
+ const usesEngineClock = /\bengine\s*\.\s*(runtime|frametime)\b/.test(body);
10092
+ if (!fns || !fns.update && !hasCursorHook && !hasMediaHook && !hasApplyHook && !usesEngineClock) return null;
9450
10093
  const sandbox = {
9451
10094
  engine,
9452
10095
  scriptProperties: spValues,
@@ -9738,7 +10381,7 @@ void main() {
9738
10381
  cancel.handle = state.handle;
9739
10382
  return cancel;
9740
10383
  }
9741
- function setInterval(fn, ms) {
10384
+ function setInterval2(fn, ms) {
9742
10385
  if (typeof fn !== "function") return makeCancel({ fired: true, handle: null }, null);
9743
10386
  const state = { fired: false, handle: null };
9744
10387
  const cancel = makeCancel(state, clearI);
@@ -9747,14 +10390,14 @@ void main() {
9747
10390
  cancel.handle = state.handle;
9748
10391
  return cancel;
9749
10392
  }
9750
- function clearTimeout(h) {
10393
+ function clearTimeout2(h) {
9751
10394
  if (typeof h === "function") {
9752
10395
  h();
9753
10396
  return;
9754
10397
  }
9755
10398
  if (clearT && h != null) clearT(h);
9756
10399
  }
9757
- function clearInterval(h) {
10400
+ function clearInterval2(h) {
9758
10401
  if (typeof h === "function") {
9759
10402
  h();
9760
10403
  return;
@@ -9767,9 +10410,9 @@ void main() {
9767
10410
  }
9768
10411
  return {
9769
10412
  setTimeout,
9770
- setInterval,
9771
- clearTimeout,
9772
- clearInterval,
10413
+ setInterval: setInterval2,
10414
+ clearTimeout: clearTimeout2,
10415
+ clearInterval: clearInterval2,
9773
10416
  dispose,
9774
10417
  /** 测试与诊断用:尚未触发且未取消的定时器数 */
9775
10418
  pendingCount: () => pending.size
@@ -9819,11 +10462,13 @@ void main() {
9819
10462
  const patterns = makePatterns(rand2);
9820
10463
  const phases = new Float32Array(64);
9821
10464
  for (let i = 0; i < 64; i++) phases[i] = rand2() * 64;
9822
- const BANDS = 64;
9823
- const rawL = new Float32Array(BANDS);
9824
- const rawR = new Float32Array(BANDS);
9825
- const left64 = new Float32Array(BANDS);
9826
- const right64 = new Float32Array(BANDS);
10465
+ const BANDS2 = 64;
10466
+ const rawL = new Float32Array(BANDS2);
10467
+ const rawR = new Float32Array(BANDS2);
10468
+ const preL64 = new Float32Array(BANDS2);
10469
+ const preR64 = new Float32Array(BANDS2);
10470
+ const left64 = new Float32Array(BANDS2);
10471
+ const right64 = new Float32Array(BANDS2);
9827
10472
  const left32 = new Float32Array(32);
9828
10473
  const right32 = new Float32Array(32);
9829
10474
  const left16 = new Float32Array(16);
@@ -9835,12 +10480,21 @@ void main() {
9835
10480
  right32,
9836
10481
  left16,
9837
10482
  right16,
10483
+ /**
10484
+ * 未钳位(pre-GAIN、pre-clamp)的 64 band,含左右声道 pan。
10485
+ * left64/right64 是 `min(1, v*GAIN)` 之后的值:底鼓段基底就已到 ~0.6、峰值贴 1,
10486
+ * 波峰因数被压平——网页作者按「峰值过阈值」判定敲击时(1520828134 猫爪
10487
+ * `audioArray[i] > 0.5`),事后再乘任何标量都无法把基底与峰值分开。
10488
+ * 网页驱动改对本数组做 gamma 对比扩展,音条墙仍走已标定的 left64/right64。
10489
+ */
10490
+ preL64,
10491
+ preR64,
9838
10492
  /** vumeter:整体响度 0..1(粒子 audioprocessing / 文字脚本 average 用) */
9839
10493
  level: 0,
9840
10494
  /** 渲染器诊断:当前是否处于「静音段」 */
9841
10495
  silent: false
9842
10496
  };
9843
- function downsample(dst, src) {
10497
+ function downsample2(dst, src) {
9844
10498
  const g = src.length / dst.length;
9845
10499
  for (let i = 0; i < dst.length; i++) {
9846
10500
  let s = 0;
@@ -9867,8 +10521,8 @@ void main() {
9867
10521
  const hatV = patterns.hat[i16] * hitEnv * drumGate;
9868
10522
  const riser = buildup > 0 ? Math.pow(buildup, 3) * (0.4 + 0.6 * Math.abs(vnoise(step * 2, 7))) : 0;
9869
10523
  let levelSum = 0;
9870
- for (let i = 0; i < BANDS; i++) {
9871
- const fq = i / BANDS;
10524
+ for (let i = 0; i < BANDS2; i++) {
10525
+ const fq = i / BANDS2;
9872
10526
  const tilt = Math.pow(1 - fq * 0.85, 1.6);
9873
10527
  let v = midGate * (0.5 + 0.3 * vnoise(beat * 0.5 + phases[i] * 0.05, i % 8));
9874
10528
  v *= 0.35 + 0.65 * fq;
@@ -9880,6 +10534,7 @@ void main() {
9880
10534
  if (fq >= 0.45) v += hatV * 0.5 * ((fq - 0.45) / 0.55);
9881
10535
  v += riser * 0.5;
9882
10536
  v *= tilt;
10537
+ const vPre = v;
9883
10538
  v = Math.min(1, v * GAIN);
9884
10539
  const width = 0.06 + fq * 0.2;
9885
10540
  const pan = vnoise(beat * 0.13 + i * 0.35, 11) * width;
@@ -9888,14 +10543,16 @@ void main() {
9888
10543
  const floorV = silent ? 0.012 : 0;
9889
10544
  rawL[i] = Math.max(floorV, l);
9890
10545
  rawR[i] = Math.max(floorV, r);
10546
+ preL64[i] = Math.max(floorV, Math.max(0, vPre * (1 - pan)));
10547
+ preR64[i] = Math.max(floorV, Math.max(0, vPre * (1 + pan)));
9891
10548
  if (i < 48) levelSum += (rawL[i] + rawR[i]) * 0.5;
9892
10549
  }
9893
10550
  left64.set(rawL);
9894
10551
  right64.set(rawR);
9895
- downsample(left32, rawL);
9896
- downsample(right32, rawR);
9897
- downsample(left16, rawL);
9898
- downsample(right16, rawR);
10552
+ downsample2(left32, rawL);
10553
+ downsample2(right32, rawR);
10554
+ downsample2(left16, rawL);
10555
+ downsample2(right16, rawR);
9899
10556
  snapshot.level = Math.min(1, levelSum / (48 * 1.2));
9900
10557
  snapshot.silent = silent;
9901
10558
  return snapshot;
@@ -9904,7 +10561,7 @@ void main() {
9904
10561
  update,
9905
10562
  snapshot,
9906
10563
  /** 频段基数 */
9907
- bands: BANDS
10564
+ bands: BANDS2
9908
10565
  };
9909
10566
  }
9910
10567
  function fillAudioBuffers(views, snapshot) {
@@ -9926,7 +10583,7 @@ void main() {
9926
10583
  dst[i] = s / (i1 - i0);
9927
10584
  }
9928
10585
  }
9929
- const MEDIA_PLAYBACK = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
10586
+ const MEDIA_PLAYBACK$1 = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
9930
10587
  class MediaVec3 {
9931
10588
  constructor(x, y, z) {
9932
10589
  this.x = Number(x) || 0;
@@ -10051,7 +10708,7 @@ void main() {
10051
10708
  const cycle = tracks.reduce((s, t) => s + t.duration + GAP, 0);
10052
10709
  const snapshot = {
10053
10710
  hasMedia: false,
10054
- state: MEDIA_PLAYBACK.STOPPED,
10711
+ state: MEDIA_PLAYBACK$1.STOPPED,
10055
10712
  title: "",
10056
10713
  artist: "",
10057
10714
  album: "",
@@ -10118,10 +10775,10 @@ void main() {
10118
10775
  snapshot.duration = tr.duration;
10119
10776
  snapshot.position = pos;
10120
10777
  const frac = tr.duration > 0 ? pos / tr.duration : 0;
10121
- if (held) snapshot.state = MEDIA_PLAYBACK.PAUSED;
10122
- else if (inGap) snapshot.state = MEDIA_PLAYBACK.STOPPED;
10123
- else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK.PAUSED;
10124
- else snapshot.state = MEDIA_PLAYBACK.PLAYING;
10778
+ if (held) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10779
+ else if (inGap) snapshot.state = MEDIA_PLAYBACK$1.STOPPED;
10780
+ else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10781
+ else snapshot.state = MEDIA_PLAYBACK$1.PLAYING;
10125
10782
  snapshot.hasThumbnail = !inGap;
10126
10783
  const c = tr.colors;
10127
10784
  snapshot.primaryColor = new MediaVec3(c.primary[0], c.primary[1], c.primary[2]);
@@ -10160,7 +10817,7 @@ void main() {
10160
10817
  if (held) return snapshot;
10161
10818
  holdT = lastWall + seekOffset;
10162
10819
  held = true;
10163
- snapshot.state = MEDIA_PLAYBACK.PAUSED;
10820
+ snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10164
10821
  return snapshot;
10165
10822
  }
10166
10823
  function play() {
@@ -10177,7 +10834,7 @@ void main() {
10177
10834
  snapshot,
10178
10835
  tracks,
10179
10836
  cycle,
10180
- MEDIA_PLAYBACK,
10837
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10181
10838
  skipNext,
10182
10839
  skipPrevious,
10183
10840
  play,
@@ -10285,7 +10942,7 @@ void main() {
10285
10942
  }
10286
10943
  const mediaMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10287
10944
  __proto__: null,
10288
- MEDIA_PLAYBACK,
10945
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10289
10946
  cloneMediaSnapshot,
10290
10947
  createSimulatedMedia,
10291
10948
  diffMediaEvents,
@@ -10385,15 +11042,7 @@ void main() {
10385
11042
  state.screenH = v.h || 1;
10386
11043
  }
10387
11044
  readViewport();
10388
- function onMove(ev) {
10389
- readViewport();
10390
- let x = ev.clientX;
10391
- let y = ev.clientY;
10392
- if (target && typeof target.getBoundingClientRect === "function") {
10393
- const r = target.getBoundingClientRect();
10394
- x -= r.left;
10395
- y -= r.top;
10396
- }
11045
+ function applyMove(x, y) {
10397
11046
  const u = x / state.screenW;
10398
11047
  const v = y / state.screenH;
10399
11048
  if (!state.has) {
@@ -10410,18 +11059,35 @@ void main() {
10410
11059
  state.moveCount++;
10411
11060
  state.lastEventTime = Date.now();
10412
11061
  }
11062
+ function applyButtons(mask) {
11063
+ const left = (mask & 1) !== 0;
11064
+ if (left === state.leftDown) return;
11065
+ state.leftDown = left;
11066
+ if (left) state.downCount++;
11067
+ else state.upCount++;
11068
+ state.lastEventTime = Date.now();
11069
+ }
11070
+ function onMove(ev) {
11071
+ readViewport();
11072
+ let x = ev.clientX;
11073
+ let y = ev.clientY;
11074
+ if (target && typeof target.getBoundingClientRect === "function") {
11075
+ const r = target.getBoundingClientRect();
11076
+ x -= r.left;
11077
+ y -= r.top;
11078
+ }
11079
+ applyMove(x, y);
11080
+ }
10413
11081
  function onDown(ev) {
10414
11082
  if (ev.button !== void 0 && ev.button !== 0) return;
10415
- state.leftDown = true;
10416
- state.downCount++;
11083
+ applyButtons(1);
10417
11084
  }
10418
11085
  function onUp(ev) {
10419
11086
  if (ev.button !== void 0 && ev.button !== 0) return;
10420
- state.leftDown = false;
10421
- state.upCount++;
11087
+ applyButtons(0);
10422
11088
  }
10423
11089
  function onLeaveWindow() {
10424
- state.leftDown = false;
11090
+ applyButtons(0);
10425
11091
  }
10426
11092
  let attached = false;
10427
11093
  if (target && target.addEventListener) {
@@ -10437,9 +11103,45 @@ void main() {
10437
11103
  return {
10438
11104
  state,
10439
11105
  /**
10440
- * 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current
10441
- * 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
10442
- * 帧间位移抹成 0(见文件头注释)。
11106
+ * 外部注入指针状态(宿主轮询系统鼠标后推入)。协议见 docs/INTEGRATION.md
11107
+ *
11108
+ * 接**归一化**坐标而不是像素:宿主知道自己那块屏的 points 尺寸,除法在它那边
11109
+ * 做更准(混合 DPI 多显示器下无需任何 DPR 折算);这里再乘回 screenW/H 得到
11110
+ * input.cursorScreenPosition 要的像素。
11111
+ *
11112
+ * u/v 是 [0,1]、原点左上、**Y 朝下** —— 与 DOM 路径的 state.u/v 同一空间
11113
+ * (见文件头坐标约定)。宿主不要替 shader 翻 Y。
11114
+ *
11115
+ * 不在这里推进 last:外部推送频率(~90Hz)高于帧率,若在推送里推进 last,
11116
+ * `length(g_PointerPosition - g_PointerPositionLast)` 会恒接近 0,
11117
+ * cursorripple 完全不起波且无报错(与 DOM 路径同一个坑,见文件头)。
11118
+ *
11119
+ * @param {{u:number, v:number, buttons?:number}} p 归一化位置 + 按键位掩码(bit0 左)
11120
+ */
11121
+ pushExternal(p) {
11122
+ if (!p) return;
11123
+ readViewport();
11124
+ const u = Number(p.u);
11125
+ const v = Number(p.v);
11126
+ if (Number.isFinite(u) && Number.isFinite(v)) {
11127
+ applyMove(u * state.screenW, v * state.screenH);
11128
+ }
11129
+ applyButtons(Number(p.buttons) || 0);
11130
+ },
11131
+ /**
11132
+ * 外部指针离开本窗口(鼠标移到了别的显示器)。
11133
+ *
11134
+ * **只清按键,保留位置与 has** —— 清 has 会让 xray 开窗突然跳到相机外
11135
+ * (renderer.js 的 XRAY_IDLE_SCREEN_UV)、视差弹回中心,画面会明显抽一下。
11136
+ * 语义与 DOM 的 onLeaveWindow 一致:位置停在最后已知点,只是不再按着键。
11137
+ */
11138
+ pushExternalLeave() {
11139
+ applyButtons(0);
11140
+ },
11141
+ /**
11142
+ * 每帧所有消费方读完 current/last **之后**调用一次:把 last 推到 current。
11143
+ * 事件驱动下 current 在 rAF 之间已被 mousemove 更新;消费前调用会把
11144
+ * 帧间位移抹成 0(见文件头注释)。
10443
11145
  */
10444
11146
  beginFrame() {
10445
11147
  readViewport();
@@ -10962,6 +11664,21 @@ void main() {
10962
11664
  } catch {
10963
11665
  return null;
10964
11666
  }
11667
+ },
11668
+ async webEntry(signal) {
11669
+ let file = "index.html";
11670
+ try {
11671
+ const r = await fetch(`${base}/project.json`, { ...init, signal });
11672
+ if (r.ok) {
11673
+ const project = await r.json();
11674
+ if (project && typeof project.file === "string" && project.file.trim()) {
11675
+ file = project.file.trim().replace(/^\/+/, "");
11676
+ }
11677
+ }
11678
+ } catch {
11679
+ if (signal?.aborted) throw new Error("aborted");
11680
+ }
11681
+ return { url: `${base}/${file}` };
10965
11682
  }
10966
11683
  };
10967
11684
  }
@@ -11149,6 +11866,382 @@ void main() {
11149
11866
  const SKIP_PARTICLES = false;
11150
11867
  const SKIP_SCENE_EFFECTS = false;
11151
11868
  const TEXT_EM_SCALE = 4;
11869
+ const BANDS = 64;
11870
+ const MEDIA_PLAYBACK = { STOPPED: 0 };
11871
+ function zeroBands() {
11872
+ return {
11873
+ left64: new Float32Array(BANDS),
11874
+ right64: new Float32Array(BANDS),
11875
+ left32: new Float32Array(32),
11876
+ right32: new Float32Array(32),
11877
+ left16: new Float32Array(16),
11878
+ right16: new Float32Array(16),
11879
+ level: 0,
11880
+ silent: true
11881
+ };
11882
+ }
11883
+ function downsample(dst, src64) {
11884
+ const g = src64.length / dst.length;
11885
+ for (let i = 0; i < dst.length; i++) {
11886
+ let s = 0;
11887
+ const i0 = Math.floor(i * g);
11888
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
11889
+ for (let j = i0; j < i1; j++) s += src64[j];
11890
+ dst[i] = s / (i1 - i0);
11891
+ }
11892
+ }
11893
+ function fillFromByteFreq(out, bytes, sampleRate) {
11894
+ const n = bytes.length;
11895
+ const nyquist = sampleRate * 0.5;
11896
+ const fMin = 20;
11897
+ const fMax = Math.min(2e4, nyquist);
11898
+ let levelSum = 0;
11899
+ for (let b = 0; b < BANDS; b++) {
11900
+ const t0 = b / BANDS;
11901
+ const t1 = (b + 1) / BANDS;
11902
+ const loHz = fMin * Math.pow(fMax / fMin, t0);
11903
+ const hiHz = fMin * Math.pow(fMax / fMin, t1);
11904
+ const i0 = Math.max(0, Math.floor(loHz / nyquist * n));
11905
+ const i1 = Math.min(n, Math.max(i0 + 1, Math.ceil(hiHz / nyquist * n)));
11906
+ let s = 0;
11907
+ for (let i = i0; i < i1; i++) s += bytes[i] / 255;
11908
+ const v = Math.min(1, s / (i1 - i0) * 1.35);
11909
+ out.left64[b] = v;
11910
+ out.right64[b] = v;
11911
+ if (b < 48) levelSum += v;
11912
+ }
11913
+ downsample(out.left32, out.left64);
11914
+ downsample(out.right32, out.right64);
11915
+ downsample(out.left16, out.left64);
11916
+ downsample(out.right16, out.right64);
11917
+ out.level = Math.min(1, levelSum / (48 * 1.2));
11918
+ out.silent = out.level < 0.02;
11919
+ }
11920
+ function hashHue(s) {
11921
+ let h = 2166136261;
11922
+ for (let i = 0; i < s.length; i++) {
11923
+ h ^= s.charCodeAt(i);
11924
+ h = Math.imul(h, 16777619);
11925
+ }
11926
+ return (h >>> 0) % 360;
11927
+ }
11928
+ function hslToRgb(h, sat, light) {
11929
+ const s = sat / 100;
11930
+ const l = light / 100;
11931
+ const c = (1 - Math.abs(2 * l - 1)) * s;
11932
+ const hp = h / 60;
11933
+ const x = c * (1 - Math.abs(hp % 2 - 1));
11934
+ let r = 0, g = 0, b = 0;
11935
+ if (hp < 1) [r, g, b] = [c, x, 0];
11936
+ else if (hp < 2) [r, g, b] = [x, c, 0];
11937
+ else if (hp < 3) [r, g, b] = [0, c, x];
11938
+ else if (hp < 4) [r, g, b] = [0, x, c];
11939
+ else if (hp < 5) [r, g, b] = [x, 0, c];
11940
+ else [r, g, b] = [c, 0, x];
11941
+ const m = l - c / 2;
11942
+ return [r + m, g + m, b + m];
11943
+ }
11944
+ function applyPalette(snap, seed) {
11945
+ const hue = hashHue(seed || "empty");
11946
+ const [pr, pg, pb] = hslToRgb(hue, 72, 48);
11947
+ const [sr, sg, sb] = hslToRgb((hue + 40) % 360, 55, 28);
11948
+ const [tr, tg, tb] = hslToRgb((hue + 20) % 360, 70, 72);
11949
+ snap.primaryColor = media.mediaVec3(pr, pg, pb);
11950
+ snap.secondaryColor = media.mediaVec3(sr, sg, sb);
11951
+ snap.tertiaryColor = media.mediaVec3(tr, tg, tb);
11952
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
11953
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
11954
+ snap.hasThumbnail = !!seed;
11955
+ }
11956
+ function sampleArtworkPalette(img, w, h) {
11957
+ const c = document.createElement("canvas");
11958
+ c.width = 32;
11959
+ c.height = 32;
11960
+ const ctx = c.getContext("2d", { willReadFrequently: true });
11961
+ if (!ctx) return null;
11962
+ ctx.drawImage(img, 0, 0, w, h, 0, 0, 32, 32);
11963
+ const data = ctx.getImageData(0, 0, 32, 32).data;
11964
+ let r = 0, g = 0, b = 0, n = 0;
11965
+ let br = 0, bg = 0, bb = 0, best = -1;
11966
+ for (let i = 0; i < data.length; i += 4) {
11967
+ const pr = data[i] / 255, pg = data[i + 1] / 255, pb = data[i + 2] / 255;
11968
+ r += pr;
11969
+ g += pg;
11970
+ b += pb;
11971
+ n++;
11972
+ const mx = Math.max(pr, pg, pb), mn = Math.min(pr, pg, pb);
11973
+ const sat = mx - mn;
11974
+ const lum = 0.2126 * pr + 0.7152 * pg + 0.0722 * pb;
11975
+ const score = sat * 1.4 + (lum > 0.15 && lum < 0.85 ? 0.3 : 0);
11976
+ if (score > best) {
11977
+ best = score;
11978
+ br = pr;
11979
+ bg = pg;
11980
+ bb = pb;
11981
+ }
11982
+ }
11983
+ if (!n) return null;
11984
+ const primary = [br, bg, bb];
11985
+ const secondary = [r / n * 0.55, g / n * 0.55, b / n * 0.55];
11986
+ const tertiary = [
11987
+ Math.min(1, primary[0] * 0.45 + 0.55),
11988
+ Math.min(1, primary[1] * 0.45 + 0.55),
11989
+ Math.min(1, primary[2] * 0.45 + 0.55)
11990
+ ];
11991
+ return { primary, secondary, tertiary };
11992
+ }
11993
+ function rasterizeArtwork(img, srcW, srcH, size = 512) {
11994
+ const c = document.createElement("canvas");
11995
+ c.width = size;
11996
+ c.height = size;
11997
+ const ctx = c.getContext("2d");
11998
+ const scale = Math.max(size / Math.max(1, srcW), size / Math.max(1, srcH));
11999
+ const dw = srcW * scale;
12000
+ const dh = srcH * scale;
12001
+ ctx.fillStyle = "#000";
12002
+ ctx.fillRect(0, 0, size, size);
12003
+ ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
12004
+ const id = ctx.getImageData(0, 0, size, size);
12005
+ return { width: size, height: size, rgba: new Uint8Array(id.data) };
12006
+ }
12007
+ function emptyMediaSnapshot() {
12008
+ return {
12009
+ hasMedia: false,
12010
+ state: MEDIA_PLAYBACK.STOPPED,
12011
+ title: "",
12012
+ artist: "",
12013
+ album: "",
12014
+ albumArtist: "",
12015
+ position: 0,
12016
+ duration: 0,
12017
+ hasThumbnail: false,
12018
+ primaryColor: media.mediaVec3(0, 0, 0),
12019
+ secondaryColor: media.mediaVec3(0, 0, 0),
12020
+ tertiaryColor: media.mediaVec3(0, 0, 0),
12021
+ textColor: media.mediaVec3(1, 1, 1),
12022
+ highContrastColor: media.mediaVec3(1, 1, 1),
12023
+ trackIndex: -1,
12024
+ lyrics: [],
12025
+ lyricLine: "",
12026
+ lyricIndex: -1
12027
+ };
12028
+ }
12029
+ async function openMicAnalyser() {
12030
+ if (!navigator.mediaDevices?.getUserMedia) return null;
12031
+ try {
12032
+ const stream = await navigator.mediaDevices.getUserMedia({
12033
+ audio: {
12034
+ echoCancellation: false,
12035
+ noiseSuppression: false,
12036
+ autoGainControl: false
12037
+ },
12038
+ video: false
12039
+ });
12040
+ const ctx = new AudioContext();
12041
+ const src = ctx.createMediaStreamSource(stream);
12042
+ const analyser = ctx.createAnalyser();
12043
+ analyser.fftSize = 2048;
12044
+ analyser.smoothingTimeConstant = 0.8;
12045
+ src.connect(analyser);
12046
+ if (ctx.state === "suspended") await ctx.resume().catch(() => {
12047
+ });
12048
+ return { ctx, stream, analyser, buf: new Uint8Array(analyser.frequencyBinCount) };
12049
+ } catch {
12050
+ return null;
12051
+ }
12052
+ }
12053
+ async function startLiveSystem(opts) {
12054
+ const origin = opts?.origin ?? (typeof location !== "undefined" ? location.origin : "");
12055
+ const onArtwork = opts?.onArtwork;
12056
+ const audioSnap = zeroBands();
12057
+ const mediaSnap = emptyMediaSnapshot();
12058
+ const winSnap = { app: "", title: "", url: "", index: 0 };
12059
+ let audioMode = "off";
12060
+ let mediaMode = "offline";
12061
+ let windowMode = "offline";
12062
+ let trackKey = "";
12063
+ let hasArtwork = false;
12064
+ let lastArtworkKey = "";
12065
+ const mic = await openMicAnalyser();
12066
+ if (mic) audioMode = "mic";
12067
+ else if (!navigator.mediaDevices?.getUserMedia) audioMode = "unavailable";
12068
+ else audioMode = "denied";
12069
+ let es = null;
12070
+ let pollTimer = null;
12071
+ let disposed = false;
12072
+ const requestArtwork = (key, title, artist) => {
12073
+ if (!origin || !onArtwork || !hasArtwork) return;
12074
+ if (lastArtworkKey === key) return;
12075
+ lastArtworkKey = key;
12076
+ onArtwork({
12077
+ url: `${origin}/api/system/artwork?k=${encodeURIComponent(key)}&_=${Date.now()}`,
12078
+ trackKey: key,
12079
+ title,
12080
+ artist
12081
+ });
12082
+ };
12083
+ const applyMediaPayload = (m) => {
12084
+ if (!m || !m.hasMedia) {
12085
+ mediaSnap.hasMedia = false;
12086
+ mediaSnap.state = MEDIA_PLAYBACK.STOPPED;
12087
+ mediaSnap.title = "";
12088
+ mediaSnap.artist = "";
12089
+ mediaSnap.album = "";
12090
+ mediaSnap.albumArtist = "";
12091
+ mediaSnap.position = 0;
12092
+ mediaSnap.duration = 0;
12093
+ mediaSnap.hasThumbnail = false;
12094
+ mediaSnap.trackIndex = -1;
12095
+ mediaMode = m ? "empty" : "offline";
12096
+ trackKey = "";
12097
+ hasArtwork = false;
12098
+ lastArtworkKey = "";
12099
+ return;
12100
+ }
12101
+ mediaMode = "live";
12102
+ mediaSnap.hasMedia = true;
12103
+ mediaSnap.state = Number(m.state) === 2 ? 2 : Number(m.state) === 1 ? 1 : 0;
12104
+ mediaSnap.title = String(m.title ?? "");
12105
+ mediaSnap.artist = String(m.artist ?? "");
12106
+ mediaSnap.album = String(m.album ?? "");
12107
+ mediaSnap.albumArtist = String(m.albumArtist ?? m.artist ?? "");
12108
+ mediaSnap.position = Number(m.position) || 0;
12109
+ mediaSnap.duration = Number(m.duration) || 0;
12110
+ hasArtwork = m.hasArtwork === true;
12111
+ const key = `${mediaSnap.title}|${mediaSnap.artist}|${mediaSnap.album}`;
12112
+ if (key !== trackKey) {
12113
+ trackKey = key;
12114
+ lastArtworkKey = "";
12115
+ mediaSnap.trackIndex = mediaSnap.trackIndex + 1 | 0;
12116
+ applyPalette(mediaSnap, key);
12117
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
12118
+ } else {
12119
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
12120
+ }
12121
+ };
12122
+ const applyWindowPayload = (w) => {
12123
+ if (!w) {
12124
+ windowMode = "offline";
12125
+ return;
12126
+ }
12127
+ winSnap.app = String(w.app ?? "");
12128
+ winSnap.title = String(w.title ?? "");
12129
+ winSnap.url = String(w.url ?? "");
12130
+ windowMode = winSnap.app || winSnap.title ? "live" : "empty";
12131
+ };
12132
+ const pollOnce = async () => {
12133
+ if (!origin || disposed) return;
12134
+ try {
12135
+ const [mr, wr] = await Promise.all([
12136
+ fetch(`${origin}/api/system/media`, { cache: "no-store" }),
12137
+ fetch(`${origin}/api/system/window`, { cache: "no-store" })
12138
+ ]);
12139
+ if (mr.ok) {
12140
+ const j = await mr.json();
12141
+ applyMediaPayload(j);
12142
+ } else {
12143
+ mediaMode = "offline";
12144
+ }
12145
+ if (wr.ok) {
12146
+ applyWindowPayload(await wr.json());
12147
+ }
12148
+ } catch {
12149
+ mediaMode = mediaMode === "live" ? "live" : "offline";
12150
+ windowMode = windowMode === "live" ? "live" : "offline";
12151
+ }
12152
+ };
12153
+ if (origin) {
12154
+ await pollOnce();
12155
+ pollTimer = setInterval(() => void pollOnce(), 1e3);
12156
+ try {
12157
+ es = new EventSource(`${origin}/api/system/stream`);
12158
+ es.onmessage = (ev) => {
12159
+ if (disposed) return;
12160
+ try {
12161
+ const data = JSON.parse(ev.data);
12162
+ applyMediaPayload(data.media);
12163
+ applyWindowPayload(data.window);
12164
+ } catch {
12165
+ }
12166
+ };
12167
+ } catch {
12168
+ }
12169
+ }
12170
+ const postControl = (action) => {
12171
+ if (!origin || disposed) return;
12172
+ void fetch(`${origin}/api/system/media-control`, {
12173
+ method: "POST",
12174
+ headers: { "Content-Type": "application/json" },
12175
+ body: JSON.stringify({ action })
12176
+ }).then(async (r) => {
12177
+ if (!r.ok) return null;
12178
+ return r.json();
12179
+ }).then((j) => {
12180
+ if (j && typeof j === "object") applyMediaPayload(j);
12181
+ void pollOnce();
12182
+ }).catch(() => {
12183
+ void pollOnce();
12184
+ });
12185
+ };
12186
+ return {
12187
+ audio: {
12188
+ snapshot: audioSnap,
12189
+ pump: () => {
12190
+ if (!mic || disposed) {
12191
+ audioSnap.level = 0;
12192
+ audioSnap.silent = true;
12193
+ return;
12194
+ }
12195
+ mic.analyser.getByteFrequencyData(mic.buf);
12196
+ fillFromByteFreq(audioSnap, mic.buf, mic.ctx.sampleRate || 48e3);
12197
+ }
12198
+ },
12199
+ media: {
12200
+ snapshot: mediaSnap,
12201
+ pump: () => {
12202
+ },
12203
+ skipNext: () => postControl("skipNext"),
12204
+ skipPrevious: () => postControl("skipPrevious"),
12205
+ play: () => postControl("play"),
12206
+ pause: () => postControl("pause"),
12207
+ playPause: () => postControl("playPause")
12208
+ },
12209
+ windowTitle: {
12210
+ snapshot: winSnap,
12211
+ pump: () => {
12212
+ }
12213
+ },
12214
+ status: () => ({
12215
+ audio: audioMode,
12216
+ media: mediaMode,
12217
+ window: windowMode,
12218
+ title: mediaSnap.title,
12219
+ artist: mediaSnap.artist,
12220
+ app: winSnap.app,
12221
+ windowTitle: winSnap.title,
12222
+ hasArtwork
12223
+ }),
12224
+ dispose: () => {
12225
+ disposed = true;
12226
+ if (pollTimer) {
12227
+ clearInterval(pollTimer);
12228
+ pollTimer = null;
12229
+ }
12230
+ try {
12231
+ es?.close();
12232
+ } catch {
12233
+ }
12234
+ es = null;
12235
+ if (mic) {
12236
+ try {
12237
+ mic.stream.getTracks().forEach((t) => t.stop());
12238
+ void mic.ctx.close();
12239
+ } catch {
12240
+ }
12241
+ }
12242
+ }
12243
+ };
12244
+ }
11152
12245
  const WE_SHADER_HEADERS = {
11153
12246
  "common.h": `// WE common.h(重建子集,供 we-scene 浏览器渲染)
11154
12247
  #define M_PI 3.14159265359
@@ -11421,10 +12514,18 @@ mat3 squareToQuad(vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
11421
12514
  float d = p1.y - p0.y + g * p1.y;
11422
12515
  float e = p3.y - p0.y + h * p3.y;
11423
12516
  float f = p0.y;
11424
- // 行向量约定:[u v 1] * M
11425
- return mat3(a, d, g,
11426
- b, e, h,
11427
- c, f, 1.0);
12517
+ // 调用点一律是 mul(vec3(uv,1), inverse(本函数结果)),hlsl2glsl 把它转写成
12518
+ // transpose(xform) * vec3(uv,1)。要让屏幕点 s 得到 texCoord = S⁻¹·s(S 为
12519
+ // Heckbert 正向矩阵 [[a,b,c],[d,e,f],[g,h,1]],单位方→四边形),必须
12520
+ // xform = inverse(本函数结果) 满足 transpose(xform)·s = S⁻¹·s,
12521
+ // 即本函数返回 S 的**转置**:mat3 列主序构造为 (a,d,g)(b,e,h)(c,f,1) 的转置
12522
+ // = (a,b,c)(d,e,f)(g,h,1)。排布差一个转置,perspective/水波等全部错位——
12523
+ // 3174556087 的音谱柱被贴到窗户侧边竖排(应为贴下窗沿横排)实测确认。
12524
+ // 2026-09-05 数值模拟:旧排布 transpose(S⁻¹)·corner 与正确 S⁻¹·corner 逐项不同,
12525
+ // 可见区塌缩成一条斜带;新排布后中心 (0.5,0.5) → (0.478,0.511) ∈ [0,1]²。
12526
+ return mat3(a, b, c,
12527
+ d, e, f,
12528
+ g, h, 1.0);
11428
12529
  }
11429
12530
  `,
11430
12531
  // WE common_blur.h(重建)。blurNa 的权重不是估算的 —— 壁纸 1444077782 里存着
@@ -11554,6 +12655,66 @@ vec3 DecompressNormal(vec4 tex) {
11554
12655
  "common_vertex.h": `// WE common_vertex.h(重建:空占位,见 headers.ts 注释)
11555
12656
  `
11556
12657
  };
12658
+ function sanitizeFontForBrowser(src) {
12659
+ if (!src || src.length < 12) return src;
12660
+ const b0 = src[0], b1 = src[1], b2 = src[2], b3 = src[3];
12661
+ const isTtf = b0 === 0 && b1 === 1 && b2 === 0 && b3 === 0;
12662
+ const isOtto = b0 === 79 && b1 === 84 && b2 === 84 && b3 === 79;
12663
+ if (!isTtf && !isOtto) return src;
12664
+ const out = Uint8Array.from(src);
12665
+ const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
12666
+ const numTables = dv.getUint16(4);
12667
+ if (numTables <= 0 || 12 + numTables * 16 > out.length) return src;
12668
+ let cmapEntry = -1;
12669
+ let cmapOffset = 0;
12670
+ let cmapLength = 0;
12671
+ for (let i = 0; i < numTables; i++) {
12672
+ const e = 12 + i * 16;
12673
+ const tag = String.fromCharCode(out[e], out[e + 1], out[e + 2], out[e + 3]);
12674
+ if (tag === "cmap") {
12675
+ cmapEntry = e;
12676
+ cmapOffset = dv.getUint32(e + 8);
12677
+ cmapLength = dv.getUint32(e + 12);
12678
+ break;
12679
+ }
12680
+ }
12681
+ if (cmapEntry < 0 || cmapOffset + cmapLength > out.length) return src;
12682
+ let changed = false;
12683
+ const numEnc = dv.getUint16(cmapOffset + 2);
12684
+ for (let i = 0; i < numEnc; i++) {
12685
+ const rec = cmapOffset + 4 + i * 8;
12686
+ const soff = dv.getUint32(rec + 4);
12687
+ const abs = cmapOffset + soff;
12688
+ if (abs + 14 > out.length) continue;
12689
+ if (dv.getUint16(abs) !== 4) continue;
12690
+ const segCountX2 = dv.getUint16(abs + 6);
12691
+ const segCount = segCountX2 >>> 1;
12692
+ if (segCount < 1) continue;
12693
+ const expSearch = 2 * Math.pow(2, Math.floor(Math.log2(segCount)));
12694
+ const expSel = Math.floor(Math.log2(segCount));
12695
+ const expShift = segCountX2 - expSearch;
12696
+ const curSearch = dv.getUint16(abs + 8);
12697
+ const curSel = dv.getUint16(abs + 10);
12698
+ const curShift = dv.getUint16(abs + 12);
12699
+ if (curSearch === expSearch && curSel === expSel && curShift === expShift) continue;
12700
+ dv.setUint16(abs + 8, expSearch);
12701
+ dv.setUint16(abs + 10, expSel);
12702
+ dv.setUint16(abs + 12, expShift);
12703
+ changed = true;
12704
+ }
12705
+ if (!changed) return src;
12706
+ let sum = 0;
12707
+ const end = cmapOffset + cmapLength;
12708
+ for (let p = cmapOffset; p < end; p += 4) {
12709
+ const b02 = out[p] || 0;
12710
+ const b12 = p + 1 < end ? out[p + 1] : 0;
12711
+ const b22 = p + 2 < end ? out[p + 2] : 0;
12712
+ const b32 = p + 3 < end ? out[p + 3] : 0;
12713
+ sum = sum + (b02 << 24 | b12 << 16 | b22 << 8 | b32) >>> 0;
12714
+ }
12715
+ dv.setUint32(cmapEntry + 4, sum);
12716
+ return out;
12717
+ }
11557
12718
  const SYSTEM_FONT_FAMILIES = {
11558
12719
  systemfont_segoe: "'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
11559
12720
  systemfont_arial: "Arial, 'Helvetica Neue', sans-serif",
@@ -11574,7 +12735,48 @@ vec3 DecompressNormal(vec4 tex) {
11574
12735
  systemfont_simhei: "SimHei, 'Heiti SC', sans-serif"
11575
12736
  };
11576
12737
  const fontFaceCache = /* @__PURE__ */ new Map();
12738
+ function fontKeyHash(key) {
12739
+ let h = 5381;
12740
+ for (let i = 0; i < key.length; i++) h = (h << 5) + h + key.charCodeAt(i) | 0;
12741
+ return (h >>> 0).toString(36);
12742
+ }
12743
+ function releaseFontFaces(keys) {
12744
+ for (const key of keys) {
12745
+ const entry = fontFaceCache.get(key);
12746
+ if (!entry) continue;
12747
+ entry.refs--;
12748
+ if (entry.refs > 0) continue;
12749
+ fontFaceCache.delete(key);
12750
+ try {
12751
+ const dead = [];
12752
+ document.fonts.forEach((f) => {
12753
+ if (f.family === entry.family) dead.push(f);
12754
+ });
12755
+ for (const f of dead) document.fonts.delete(f);
12756
+ } catch {
12757
+ }
12758
+ }
12759
+ }
11577
12760
  const pkgCache = /* @__PURE__ */ new Map();
12761
+ const PKG_CACHE_MAX_BYTES = 512 * 1024 * 1024;
12762
+ let pkgCacheBytes = 0;
12763
+ function pkgCacheEvict(currentKey) {
12764
+ while (pkgCache.size > 0 && (pkgCache.size > 2 || pkgCacheBytes > PKG_CACHE_MAX_BYTES)) {
12765
+ let oldestKey = null;
12766
+ let oldestAt = Infinity;
12767
+ for (const [k, v] of pkgCache) {
12768
+ if (k === currentKey) continue;
12769
+ if (v.at < oldestAt) {
12770
+ oldestAt = v.at;
12771
+ oldestKey = k;
12772
+ }
12773
+ }
12774
+ if (!oldestKey) break;
12775
+ const victim = pkgCache.get(oldestKey);
12776
+ pkgCacheBytes -= victim.parsed.fileSize || 0;
12777
+ pkgCache.delete(oldestKey);
12778
+ }
12779
+ }
11578
12780
  async function loadParsedPkg(rt, cfg, source, signal) {
11579
12781
  const cacheKey = source.key;
11580
12782
  if (cacheKey) {
@@ -11597,38 +12799,40 @@ vec3 DecompressNormal(vec4 tex) {
11597
12799
  const parsed = pkg.parsePkg(bytes);
11598
12800
  if (!cacheKey) return parsed;
11599
12801
  pkgCache.set(cacheKey, { parsed, at: Date.now() });
11600
- if (pkgCache.size > 2) {
11601
- let oldestKey = null;
11602
- let oldestAt = Infinity;
11603
- for (const [k, v] of pkgCache) {
11604
- if (k === cacheKey) continue;
11605
- if (v.at < oldestAt) {
11606
- oldestAt = v.at;
11607
- oldestKey = k;
11608
- }
11609
- }
11610
- if (oldestKey) pkgCache.delete(oldestKey);
11611
- }
12802
+ pkgCacheBytes += parsed.fileSize || 0;
12803
+ pkgCacheEvict(cacheKey);
11612
12804
  return parsed;
11613
12805
  }
11614
12806
  function mountScene(rt, cfg) {
11615
12807
  clear(rt);
11616
- const c = cfg.canvas ?? document.createElement("canvas");
12808
+ const c = (cfg.canvas instanceof HTMLCanvasElement ? cfg.canvas : null) ?? document.createElement("canvas");
11617
12809
  const dpr = effectiveDpr(rt, cfg);
11618
12810
  const vw = c.clientWidth || window.innerWidth || 1;
11619
12811
  const vh = c.clientHeight || window.innerHeight || 1;
11620
12812
  c.width = Math.max(1, Math.round(vw * dpr));
11621
12813
  c.height = Math.max(1, Math.round(vh * dpr));
11622
- if (!cfg.canvas) {
12814
+ if (!(cfg.canvas instanceof HTMLCanvasElement)) {
11623
12815
  c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11624
12816
  rt.wrap?.appendChild(c);
11625
12817
  }
11626
12818
  rt.canvas = c;
11627
12819
  let disposed = false;
12820
+ const origWarn = console.warn.bind(console);
12821
+ console.warn = (...args) => {
12822
+ const s = args.map((a) => typeof a === "string" ? a : String(a?.message ?? a)).join(" ");
12823
+ if (s.includes("[we-scene]")) {
12824
+ try {
12825
+ reportDiag(rt, cfg, s.slice(0, 300));
12826
+ } catch {
12827
+ }
12828
+ }
12829
+ origWarn(...args);
12830
+ };
11628
12831
  const pkgAbort = new AbortController();
11629
12832
  let particleCleanup;
11630
12833
  rt.sceneCleanup = () => {
11631
12834
  disposed = true;
12835
+ console.warn = origWarn;
11632
12836
  pkgAbort.abort();
11633
12837
  rt.sceneTextUpdate = void 0;
11634
12838
  if (particleCleanup) {
@@ -11685,6 +12889,11 @@ vec3 DecompressNormal(vec4 tex) {
11685
12889
  const sceneEntry = pkg.getEntry(parsedPkg, "scene.json");
11686
12890
  if (!sceneEntry) throw new Error("pkg 中没有 scene.json(不是场景壁纸?)");
11687
12891
  const scene = scn.parseScene(JSON.parse(readText(sceneEntry)), project);
12892
+ if (cfg.clearColor) {
12893
+ const g = scene.general ??= {};
12894
+ g.clearcolor = cfg.clearColor;
12895
+ g.clearenabled = true;
12896
+ }
11688
12897
  {
11689
12898
  const zRaw = scene.general?.zoom;
11690
12899
  const zVal = zRaw && typeof zRaw === "object" ? Number(zRaw.value) : Number(zRaw);
@@ -11755,6 +12964,22 @@ vec3 DecompressNormal(vec4 tex) {
11755
12964
  if (disposed) return;
11756
12965
  const supportsAudioProcessing = project?.general?.supportsaudioprocessing !== false;
11757
12966
  const simAudio = createSimulatedAudio();
12967
+ const simMedia = media.createSimulatedMedia();
12968
+ const simWindow = system.createSimulatedWindowTitle();
12969
+ let live = null;
12970
+ const liveHold = {
12971
+ mediaDriver: null,
12972
+ lastSnap: {
12973
+ get: () => null,
12974
+ setHasThumbnail: () => {
12975
+ }
12976
+ }
12977
+ };
12978
+ const audioDriverRef = {
12979
+ current: null
12980
+ };
12981
+ let mediaDriver = simMedia;
12982
+ let windowDriver = simWindow;
11758
12983
  const audioSim = { enabled: supportsAudioProcessing };
11759
12984
  const zero = (n) => new Float32Array(n);
11760
12985
  const SILENT_AUDIO = {
@@ -11767,32 +12992,108 @@ vec3 DecompressNormal(vec4 tex) {
11767
12992
  level: 0,
11768
12993
  silent: true
11769
12994
  };
11770
- renderer.setAudioProvider(() => audioSim.enabled ? simAudio.snapshot : SILENT_AUDIO);
12995
+ const hostAudio = (() => {
12996
+ const snapshot = {
12997
+ left64: zero(64),
12998
+ right64: zero(64),
12999
+ left32: zero(32),
13000
+ right32: zero(32),
13001
+ left16: zero(16),
13002
+ right16: zero(16),
13003
+ // 未钳位频谱:网页驱动会对它做 gamma 对比扩展。宿主给的已是 0..1
13004
+ // 归一化值,没有 pre-GAIN 概念,直接与 left64/right64 共用同一份数据
13005
+ preL64: zero(64),
13006
+ preR64: zero(64),
13007
+ level: 0,
13008
+ silent: true
13009
+ };
13010
+ const down = (dst, src) => {
13011
+ const g = src.length / dst.length;
13012
+ for (let i = 0; i < dst.length; i++) {
13013
+ let s = 0;
13014
+ const i0 = Math.floor(i * g);
13015
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
13016
+ for (let j = i0; j < i1; j++) s += src[j];
13017
+ dst[i] = s / (i1 - i0);
13018
+ }
13019
+ };
13020
+ return {
13021
+ active: false,
13022
+ snapshot,
13023
+ /** 每帧从宿主拉一次。宿主返回 null(未采集/无权限)时置 active=false 回落模拟源 */
13024
+ pump() {
13025
+ const src = rt.audioBridge?.();
13026
+ if (!src || !src.left || !src.right) {
13027
+ this.active = false;
13028
+ return;
13029
+ }
13030
+ const n = Math.min(64, src.left.length, src.right.length);
13031
+ let sum = 0;
13032
+ for (let i = 0; i < n; i++) {
13033
+ const l = src.left[i] || 0;
13034
+ const r = src.right[i] || 0;
13035
+ snapshot.left64[i] = l;
13036
+ snapshot.right64[i] = r;
13037
+ snapshot.preL64[i] = l;
13038
+ snapshot.preR64[i] = r;
13039
+ if (i < 48) sum += l;
13040
+ }
13041
+ for (let i = n; i < 64; i++) {
13042
+ snapshot.left64[i] = 0;
13043
+ snapshot.right64[i] = 0;
13044
+ snapshot.preL64[i] = 0;
13045
+ snapshot.preR64[i] = 0;
13046
+ }
13047
+ down(snapshot.left32, snapshot.left64);
13048
+ down(snapshot.right32, snapshot.right64);
13049
+ down(snapshot.left16, snapshot.left64);
13050
+ down(snapshot.right16, snapshot.right64);
13051
+ snapshot.level = Math.min(1, sum / 48);
13052
+ snapshot.silent = snapshot.level < 0.02;
13053
+ this.active = true;
13054
+ }
13055
+ };
13056
+ })();
13057
+ const activeAudioSnapshot = () => hostAudio.active ? hostAudio.snapshot : audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
13058
+ renderer.setAudioProvider(() => {
13059
+ if (!audioSim.enabled) return SILENT_AUDIO;
13060
+ return activeAudioSnapshot();
13061
+ });
11771
13062
  const audioViews = /* @__PURE__ */ new Map();
11772
13063
  window.__audioStats = () => ({
11773
13064
  enabled: audioSim.enabled,
11774
- level: audioSim.enabled ? Math.round(simAudio.snapshot.level * 1e3) / 1e3 : 0,
11775
- silent: audioSim.enabled ? simAudio.snapshot.silent : true,
11776
- bass: audioSim.enabled ? Math.round(simAudio.snapshot.left64[2] * 1e3) / 1e3 : 0
13065
+ live: !!audioDriverRef.current,
13066
+ level: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).level * 1e3) / 1e3 : 0,
13067
+ silent: audioSim.enabled ? (audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).silent : true,
13068
+ bass: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).left64[2] * 1e3) / 1e3 : 0
11777
13069
  });
11778
13070
  window.__audioMute = (on) => {
11779
13071
  audioSim.enabled = !on;
11780
13072
  return audioSim.enabled;
11781
13073
  };
11782
- reportDiag(rt, cfg, `audio: simulated stream, supportsaudioprocessing=${supportsAudioProcessing}`);
11783
- const simMedia = media.createSimulatedMedia();
11784
- const simWindow = system.createSimulatedWindowTitle();
13074
+ reportDiag(
13075
+ rt,
13076
+ cfg,
13077
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13078
+ );
11785
13079
  const shortcuts = system.createShortcutHandler((name) => {
11786
13080
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
11787
13081
  });
11788
13082
  const mediaSim = { enabled: true, override: null };
11789
13083
  const mediaHooks = [];
11790
13084
  let lastMediaSnap = null;
13085
+ liveHold.lastSnap = {
13086
+ get: () => lastMediaSnap,
13087
+ setHasThumbnail: (v) => {
13088
+ if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
13089
+ }
13090
+ };
13091
+ const mediaSnapshot = () => mediaDriver.snapshot;
11791
13092
  const registerMediaHook = (sb) => {
11792
13093
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
11793
13094
  mediaHooks.push(sb);
11794
- if (!simMedia.snapshot.hasMedia) return;
11795
- for (const { name, event } of media.diffMediaEvents(null, simMedia.snapshot)) {
13095
+ if (!mediaSnapshot().hasMedia) return;
13096
+ for (const { name, event } of media.diffMediaEvents(null, mediaSnapshot())) {
11796
13097
  try {
11797
13098
  sb.callMedia(name, event);
11798
13099
  } catch {
@@ -11801,26 +13102,27 @@ vec3 DecompressNormal(vec4 tex) {
11801
13102
  };
11802
13103
  window.__mediaStats = () => ({
11803
13104
  enabled: mediaSim.enabled,
13105
+ live: !!live,
11804
13106
  hooks: mediaHooks.length,
11805
- title: simMedia.snapshot.title,
11806
- artist: simMedia.snapshot.artist,
11807
- album: simMedia.snapshot.album,
11808
- state: simMedia.snapshot.state,
11809
- position: Math.round(simMedia.snapshot.position),
11810
- duration: simMedia.snapshot.duration,
11811
- hasThumbnail: simMedia.snapshot.hasThumbnail,
11812
- lyric: simMedia.snapshot.lyricLine,
11813
- primaryColor: simMedia.snapshot.primaryColor ? [simMedia.snapshot.primaryColor.x, simMedia.snapshot.primaryColor.y, simMedia.snapshot.primaryColor.z] : null
13107
+ title: mediaSnapshot().title,
13108
+ artist: mediaSnapshot().artist,
13109
+ album: mediaSnapshot().album,
13110
+ state: mediaSnapshot().state,
13111
+ position: Math.round(mediaSnapshot().position),
13112
+ duration: mediaSnapshot().duration,
13113
+ hasThumbnail: mediaSnapshot().hasThumbnail,
13114
+ lyric: mediaSnapshot().lyricLine,
13115
+ primaryColor: mediaSnapshot().primaryColor ? [mediaSnapshot().primaryColor.x, mediaSnapshot().primaryColor.y, mediaSnapshot().primaryColor.z] : null
11814
13116
  });
11815
13117
  window.__mediaSet = (patch) => {
11816
- Object.assign(simMedia.snapshot, patch || {});
11817
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
13118
+ Object.assign(mediaSnapshot(), patch || {});
13119
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11818
13120
  for (const { name, event } of evts) for (const sb of mediaHooks) sb.callMedia(name, event);
11819
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
13121
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11820
13122
  return window.__mediaStats();
11821
13123
  };
11822
13124
  const dispatchMediaNow = () => {
11823
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
13125
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11824
13126
  for (const { name, event } of evts) {
11825
13127
  for (const sb of mediaHooks) {
11826
13128
  try {
@@ -11829,42 +13131,50 @@ vec3 DecompressNormal(vec4 tex) {
11829
13131
  }
11830
13132
  }
11831
13133
  }
11832
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
13134
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11833
13135
  };
11834
13136
  const mediaControl = {
11835
- snapshot: simMedia.snapshot,
13137
+ get snapshot() {
13138
+ return mediaSnapshot();
13139
+ },
11836
13140
  skipNext: () => {
11837
- simMedia.skipNext();
13141
+ mediaDriver.skipNext();
11838
13142
  dispatchMediaNow();
11839
- return simMedia.snapshot;
13143
+ return mediaSnapshot();
11840
13144
  },
11841
13145
  skipPrevious: () => {
11842
- simMedia.skipPrevious();
13146
+ mediaDriver.skipPrevious();
11843
13147
  dispatchMediaNow();
11844
- return simMedia.snapshot;
13148
+ return mediaSnapshot();
11845
13149
  },
11846
13150
  play: () => {
11847
- simMedia.play();
13151
+ mediaDriver.play();
11848
13152
  dispatchMediaNow();
11849
- return simMedia.snapshot;
13153
+ return mediaSnapshot();
11850
13154
  },
11851
13155
  pause: () => {
11852
- simMedia.pause();
13156
+ mediaDriver.pause();
11853
13157
  dispatchMediaNow();
11854
- return simMedia.snapshot;
13158
+ return mediaSnapshot();
11855
13159
  },
11856
13160
  playPause: () => {
11857
- simMedia.playPause();
13161
+ mediaDriver.playPause();
11858
13162
  dispatchMediaNow();
11859
- return simMedia.snapshot;
13163
+ return mediaSnapshot();
11860
13164
  }
11861
13165
  };
11862
13166
  window.__mediaControl = mediaControl;
11863
13167
  window.__system = {
11864
13168
  media: mediaControl,
11865
- windowTitle: simWindow.snapshot,
11866
- shortcuts: shortcuts.last
13169
+ windowTitle: windowDriver.snapshot,
13170
+ shortcuts: shortcuts.last,
13171
+ live: null
11867
13172
  };
13173
+ window.__liveSystem = () => ({
13174
+ audio: "off",
13175
+ media: "offline",
13176
+ window: "offline"
13177
+ });
11868
13178
  const pointerSrc = pointerLib.createPointerSource(
11869
13179
  cfg.canvas ? {
11870
13180
  target: cfg.canvas,
@@ -11875,6 +13185,10 @@ vec3 DecompressNormal(vec4 tex) {
11875
13185
  } : {}
11876
13186
  );
11877
13187
  renderer.setPointerProvider(() => pointerSrc);
13188
+ rt.pointerCtl = {
13189
+ push: (p) => pointerSrc.pushExternal(p),
13190
+ leave: () => pointerSrc.pushExternalLeave()
13191
+ };
11878
13192
  {
11879
13193
  const prevCleanup = particleCleanup;
11880
13194
  particleCleanup = () => {
@@ -11968,6 +13282,118 @@ vec3 DecompressNormal(vec4 tex) {
11968
13282
  textures.set("$mediaPreviousThumbnail", mkThumb(tracks[tracks.length - 1]));
11969
13283
  }
11970
13284
  }
13285
+ if (cfg.liveSystem) {
13286
+ try {
13287
+ const uploadLiveArtwork = async (info) => {
13288
+ try {
13289
+ const res = await fetch(info.url, { cache: "no-store" });
13290
+ if (!res.ok) return;
13291
+ const blob = await res.blob();
13292
+ const bmp = await createImageBitmap(blob);
13293
+ const raster = rasterizeArtwork(bmp, bmp.width, bmp.height, 512);
13294
+ const palette = sampleArtworkPalette(bmp, bmp.width, bmp.height);
13295
+ bmp.close?.();
13296
+ const cur = textures.get("$mediaThumbnail");
13297
+ if (cur) textures.set("$mediaPreviousThumbnail", cur);
13298
+ const gl = renderer.gl;
13299
+ const existing = textures.get("$mediaThumbnail");
13300
+ if (existing?.glTex) {
13301
+ gl.bindTexture(gl.TEXTURE_2D, existing.glTex);
13302
+ gl.texImage2D(
13303
+ gl.TEXTURE_2D,
13304
+ 0,
13305
+ gl.RGBA,
13306
+ raster.width,
13307
+ raster.height,
13308
+ 0,
13309
+ gl.RGBA,
13310
+ gl.UNSIGNED_BYTE,
13311
+ raster.rgba
13312
+ );
13313
+ existing.width = raster.width;
13314
+ existing.height = raster.height;
13315
+ existing.mips = [raster];
13316
+ } else {
13317
+ textures.set("$mediaThumbnail", {
13318
+ glTex: rnd.makeTextureMip(gl, [raster], false),
13319
+ width: raster.width,
13320
+ height: raster.height,
13321
+ rg88: false,
13322
+ mips: [raster],
13323
+ generated: true
13324
+ });
13325
+ }
13326
+ const snap = mediaDriver.snapshot;
13327
+ if (palette) {
13328
+ snap.primaryColor = media.mediaVec3(...palette.primary);
13329
+ snap.secondaryColor = media.mediaVec3(...palette.secondary);
13330
+ snap.tertiaryColor = media.mediaVec3(...palette.tertiary);
13331
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
13332
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
13333
+ }
13334
+ snap.hasThumbnail = true;
13335
+ liveHold.lastSnap.setHasThumbnail(false);
13336
+ reportDiag(rt, cfg, `liveSystem: artwork ${info.title || info.trackKey}`);
13337
+ } catch (e) {
13338
+ reportDiag(
13339
+ rt,
13340
+ cfg,
13341
+ `liveSystem: artwork 失败 (${e instanceof Error ? e.message : e})`
13342
+ );
13343
+ }
13344
+ };
13345
+ live = await startLiveSystem({
13346
+ origin: location.origin,
13347
+ onArtwork: (info) => {
13348
+ void uploadLiveArtwork(info);
13349
+ }
13350
+ });
13351
+ mediaDriver = live.media;
13352
+ windowDriver = live.windowTitle;
13353
+ liveHold.mediaDriver = live.media;
13354
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13355
+ if (mediaDriver.snapshot.hasMedia) {
13356
+ for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
13357
+ for (const sb of mediaHooks) {
13358
+ try {
13359
+ sb.callMedia(name, event);
13360
+ } catch {
13361
+ }
13362
+ }
13363
+ }
13364
+ lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
13365
+ }
13366
+ const st = live.status();
13367
+ reportDiag(
13368
+ rt,
13369
+ cfg,
13370
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13371
+ );
13372
+ reportDiag(
13373
+ rt,
13374
+ cfg,
13375
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13376
+ );
13377
+ window.__system = {
13378
+ media: mediaControl,
13379
+ windowTitle: windowDriver.snapshot,
13380
+ shortcuts: shortcuts.last,
13381
+ live: () => live.status()
13382
+ };
13383
+ window.__liveSystem = () => live.status();
13384
+ } catch (e) {
13385
+ reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
13386
+ live = null;
13387
+ }
13388
+ }
13389
+ {
13390
+ const prevCleanup = particleCleanup;
13391
+ particleCleanup = () => {
13392
+ prevCleanup?.();
13393
+ live?.dispose();
13394
+ live = null;
13395
+ };
13396
+ }
11971
13397
  const texInflight = /* @__PURE__ */ new Map();
11972
13398
  const loadTexInner = async (name) => {
11973
13399
  if (textures.has(name)) return textures.get(name);
@@ -12159,6 +13585,13 @@ vec3 DecompressNormal(vec4 tex) {
12159
13585
  model = JSON.parse(readText(modelEntry));
12160
13586
  }
12161
13587
  scn.applySolidFromModel(layer, model);
13588
+ const instUt = layer.srcObject?.instance?.usertextures;
13589
+ const instUtName = instUt?.[0] && typeof instUt[0].name === "string" && instUt[0].name.startsWith("$") ? instUt[0].name : null;
13590
+ const instBoundTex = instUtName && textures.has(instUtName) ? instUtName : null;
13591
+ if (instBoundTex) {
13592
+ layer.textureName = instBoundTex;
13593
+ layer.solid = false;
13594
+ }
12162
13595
  if (model && typeof model === "object" && "width" in model && "height" in model) {
12163
13596
  const m = model;
12164
13597
  if ((layer.size?.[0] || 0) === 0 && (layer.size?.[1] || 0) === 0 && m.width > 0 && m.height > 0) {
@@ -12199,7 +13632,7 @@ vec3 DecompressNormal(vec4 tex) {
12199
13632
  texJobs.push(
12200
13633
  loadTex(tn).then((entry) => {
12201
13634
  if (!entry) return;
12202
- if (si === 0) {
13635
+ if (si === 0 && !instBoundTex) {
12203
13636
  layer.textureName = tn;
12204
13637
  loadedTex++;
12205
13638
  if (entry.videoCtl) layer.videoCtl = entry.videoCtl;
@@ -12257,6 +13690,7 @@ vec3 DecompressNormal(vec4 tex) {
12257
13690
  if (usedVisible && !rt.paused) texEntry.videoCtl.play();
12258
13691
  }
12259
13692
  const particleSystems = [];
13693
+ const particleDirty = [];
12260
13694
  const particleSystemsByLayer = /* @__PURE__ */ new Map();
12261
13695
  let builtinTexCount = 0;
12262
13696
  const loadParticleTex = async (name) => {
@@ -12270,7 +13704,10 @@ vec3 DecompressNormal(vec4 tex) {
12270
13704
  height: gen.height,
12271
13705
  rg88: false,
12272
13706
  mips: [gen],
12273
- generated: true
13707
+ generated: true,
13708
+ // 内置贴图的帧表(rain1/rain2 的 1×4 图集):randomframe 预设依赖它
13709
+ // 随机取帧,缺了就整图采样画出超长丝(1823900922)。
13710
+ frames: ptex.builtinParticleFrames(name) ?? void 0
12274
13711
  };
12275
13712
  textures.set(name, entry);
12276
13713
  builtinTexCount++;
@@ -12472,12 +13909,15 @@ vec3 DecompressNormal(vec4 tex) {
12472
13909
  const py = originY != null ? originY : wy;
12473
13910
  for (const ps of particleSystems) ps.setPointer(wx, py);
12474
13911
  }
12475
- for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? simAudio.snapshot : null);
13912
+ if (particleDirty.length) {
13913
+ for (const ps of particleDirty) ps.syncLayerTransform();
13914
+ }
13915
+ for (const ps of particleSystems) ps.advance(pdt, audioSim.enabled ? activeAudioSnapshot() : null);
12476
13916
  if (particleDiagFrame < 2) {
12477
13917
  particleDiagFrame++;
12478
13918
  if (particleDiagFrame === 2) {
12479
- const live = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
12480
- reportDiag(rt, cfg, `particles live: ${live} across ${particleSystems.length} systems`);
13919
+ const live2 = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
13920
+ reportDiag(rt, cfg, `particles live: ${live2} across ${particleSystems.length} systems`);
12481
13921
  }
12482
13922
  }
12483
13923
  },
@@ -12496,7 +13936,7 @@ vec3 DecompressNormal(vec4 tex) {
12496
13936
  `particles: ${particleSystems.length} systems, ${builtinTexCount} builtin tex generated`
12497
13937
  );
12498
13938
  window.__particleStats = () => particleSystems.map((ps) => {
12499
- let live = 0;
13939
+ let live2 = 0;
12500
13940
  let minX = Infinity;
12501
13941
  let maxX = -Infinity;
12502
13942
  let minY = Infinity;
@@ -12505,7 +13945,7 @@ vec3 DecompressNormal(vec4 tex) {
12505
13945
  let maxS = -Infinity;
12506
13946
  for (const p of ps.pool) {
12507
13947
  if (!p.alive) continue;
12508
- live++;
13948
+ live2++;
12509
13949
  const px = ps.originX + p.x * ps.scaleX;
12510
13950
  const py = ps.originY + p.y * ps.scaleY;
12511
13951
  if (px < minX) minX = px;
@@ -12517,13 +13957,13 @@ vec3 DecompressNormal(vec4 tex) {
12517
13957
  if (s > maxS) maxS = s;
12518
13958
  }
12519
13959
  return {
12520
- live,
13960
+ live: live2,
12521
13961
  max: ps.maxCount,
12522
13962
  blend: ps.blend,
12523
13963
  renderer: ps.renderers.map((r) => r.kind).join("+"),
12524
13964
  origin: [Math.round(ps.originX), Math.round(ps.originY)],
12525
- bbox: live ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
12526
- size: live ? [Math.round(minS), Math.round(maxS)] : null
13965
+ bbox: live2 ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
13966
+ size: live2 ? [Math.round(minS), Math.round(maxS)] : null
12527
13967
  };
12528
13968
  });
12529
13969
  window.__particleToggle = (on, onlyIndex) => {
@@ -12657,6 +14097,17 @@ vec3 DecompressNormal(vec4 tex) {
12657
14097
  if (attachFollows.length) {
12658
14098
  reportDiag(rt, cfg, `attachments: ${attachFollows.length} hanging layers`);
12659
14099
  }
14100
+ const transformDirty = scn.collectTransformDirty(
14101
+ scene.layers,
14102
+ attachFollows.map((f) => f.layer)
14103
+ );
14104
+ if (transformDirty.size) {
14105
+ reportDiag(rt, cfg, `transform graph: ${transformDirty.size} live layers`);
14106
+ for (const [lid, list] of particleSystemsByLayer) {
14107
+ if (!transformDirty.has(lid)) continue;
14108
+ for (const ps of list) particleDirty.push(ps);
14109
+ }
14110
+ }
12660
14111
  const textWidgets = [];
12661
14112
  const textLayerText = /* @__PURE__ */ new Map();
12662
14113
  const textShared = {};
@@ -12670,15 +14121,19 @@ vec3 DecompressNormal(vec4 tex) {
12670
14121
  const win0 = fitWindow(normalizeFit(rt.cfg.fit), projW, projH, c.width, c.height);
12671
14122
  const quality = Math.min(3, Math.max(0.5, c.width / Math.max(1, win0.viewW)));
12672
14123
  const fontFamilies = /* @__PURE__ */ new Map();
14124
+ const usedFontKeys = [];
12673
14125
  const fontPaths = /* @__PURE__ */ new Set();
12674
14126
  for (const l of scene.layers) if (l.isText && l.textFont) fontPaths.add(l.textFont);
12675
14127
  for (const e of parsedPkg.entries || []) {
12676
14128
  if (typeof e.name === "string" && /^fonts\/.+\.(ttf|otf|woff2?)$/i.test(e.name)) fontPaths.add(e.name);
12677
14129
  }
12678
14130
  for (const fp of fontPaths) {
12679
- const cached = fontFaceCache.get(`${cfg.src}|${fp}`);
14131
+ const key = `${cfg.src}|${fp}`;
14132
+ const cached = fontFaceCache.get(key);
12680
14133
  if (cached) {
12681
- fontFamilies.set(fp, cached);
14134
+ cached.refs++;
14135
+ usedFontKeys.push(key);
14136
+ fontFamilies.set(fp, cached.family);
12682
14137
  continue;
12683
14138
  }
12684
14139
  const sys = SYSTEM_FONT_FAMILIES[fp.toLowerCase()];
@@ -12689,18 +14144,33 @@ vec3 DecompressNormal(vec4 tex) {
12689
14144
  try {
12690
14145
  const fe = pkg.getEntry(parsedPkg, fp);
12691
14146
  if (!fe) continue;
12692
- const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
12693
- const url = URL.createObjectURL(new Blob([fe]));
14147
+ const bytes = sanitizeFontForBrowser(
14148
+ fe instanceof Uint8Array ? fe : new Uint8Array(fe)
14149
+ );
14150
+ const fam = "wefont_" + fontKeyHash(key) + "_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
14151
+ const url = URL.createObjectURL(new Blob([bytes]));
12694
14152
  const ff = new FontFace(fam, `url(${url})`);
12695
14153
  await ff.load();
14154
+ if (disposed) {
14155
+ URL.revokeObjectURL(url);
14156
+ break;
14157
+ }
12696
14158
  document.fonts.add(ff);
12697
14159
  (rt.objectUrls ??= []).push(url);
12698
14160
  fontFamilies.set(fp, fam);
12699
- fontFaceCache.set(`${cfg.src}|${fp}`, fam);
14161
+ fontFaceCache.set(key, { family: fam, refs: 1 });
14162
+ usedFontKeys.push(key);
12700
14163
  } catch (e) {
12701
14164
  console.warn(`字体加载失败 ${fp}: ${e.message}`);
12702
14165
  }
12703
14166
  }
14167
+ if (usedFontKeys.length) {
14168
+ const prevCleanup = rt.sceneCleanup;
14169
+ rt.sceneCleanup = () => {
14170
+ releaseFontFaces(usedFontKeys);
14171
+ prevCleanup?.();
14172
+ };
14173
+ }
12704
14174
  textCanvas = document.createElement("canvas");
12705
14175
  textCtx = textCanvas.getContext("2d");
12706
14176
  const MAX_TEX = 2048;
@@ -12734,7 +14204,7 @@ vec3 DecompressNormal(vec4 tex) {
12734
14204
  // 与对象/效果开关/常量/general 统一走 engineTimers(P1-1)。
12735
14205
  ...timerOpts,
12736
14206
  mediaControl,
12737
- windowTitle: simWindow.snapshot,
14207
+ windowTitle: windowDriver.snapshot,
12738
14208
  openUserShortcut: shortcuts.openUserShortcut,
12739
14209
  getLayerText: (name) => textLayerText.get(name),
12740
14210
  onError: (e) => {
@@ -12753,10 +14223,18 @@ vec3 DecompressNormal(vec4 tex) {
12753
14223
  const hw = layer.size[0] * (layer.scale[0] || 1) / 2;
12754
14224
  const hh = layer.size[1] * (layer.scale[1] || 1) / 2;
12755
14225
  const a = layer.textAnchor;
12756
- if (a.includes("left")) layer.origin[0] += hw;
12757
- if (a.includes("right")) layer.origin[0] -= hw;
12758
- if (a.includes("top")) layer.origin[1] -= hh;
12759
- if (a.includes("bottom")) layer.origin[1] += hh;
14226
+ let adx = 0;
14227
+ let ady = 0;
14228
+ if (a.includes("left")) adx += hw;
14229
+ if (a.includes("right")) adx -= hw;
14230
+ if (a.includes("top")) ady -= hh;
14231
+ if (a.includes("bottom")) ady += hh;
14232
+ layer.origin[0] += adx;
14233
+ layer.origin[1] += ady;
14234
+ if (layer.localOrigin) {
14235
+ layer.localOrigin[0] += adx;
14236
+ layer.localOrigin[1] += ady;
14237
+ }
12760
14238
  }
12761
14239
  const em0 = TEXT_EM_SCALE * Math.max(1, layer.textPointsize);
12762
14240
  const marginCap = wtext.textLayerHasTintMask(layer) ? 8 : 256;
@@ -12982,7 +14460,7 @@ vec3 DecompressNormal(vec4 tex) {
12982
14460
  return clone;
12983
14461
  },
12984
14462
  mediaControl,
12985
- windowTitle: simWindow.snapshot,
14463
+ windowTitle: windowDriver.snapshot,
12986
14464
  openUserShortcut: shortcuts.openUserShortcut,
12987
14465
  isScreensaver: false
12988
14466
  };
@@ -12999,6 +14477,15 @@ vec3 DecompressNormal(vec4 tex) {
12999
14477
  if (sb && sb.hasMediaHook) registerMediaHook(sb);
13000
14478
  }
13001
14479
  });
14480
+ const LOCAL_SLOT = {
14481
+ origin: "localOrigin",
14482
+ scale: "localScale",
14483
+ angles: "localAngles"
14484
+ };
14485
+ const fieldSlot = (layer, field) => {
14486
+ const slot = LOCAL_SLOT[field];
14487
+ return slot && layer && Array.isArray(layer[slot]) ? slot : field;
14488
+ };
13002
14489
  for (const layer of scene.layers) {
13003
14490
  const defs = layer.objectAnimations;
13004
14491
  if (!defs) continue;
@@ -13009,11 +14496,13 @@ vec3 DecompressNormal(vec4 tex) {
13009
14496
  const ctrl = anim.createAnimation(def.animation);
13010
14497
  ctrl.field = field;
13011
14498
  ctrl.baseValue = def.value;
13012
- const live = layer[field];
13013
- ctrl.baseNumeric = Array.isArray(live) ? live.slice() : live;
14499
+ const slot = fieldSlot(layer, field);
14500
+ const live2 = layer[slot];
14501
+ ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
14502
+ ctrl.slot = slot;
13014
14503
  layer.animationList.push(ctrl);
13015
14504
  if (ctrl.name) layer.animations[ctrl.name] = ctrl;
13016
- animRuns.push({ layer, field, ctrl });
14505
+ animRuns.push({ layer, field, slot, ctrl });
13017
14506
  } catch (e) {
13018
14507
  reportDiag(rt, cfg, `animation '${layer.name}.${field}' 建控制器失败: ${String(e.message).slice(0, 80)}`);
13019
14508
  }
@@ -13078,7 +14567,8 @@ vec3 DecompressNormal(vec4 tex) {
13078
14567
  });
13079
14568
  if (sandbox) {
13080
14569
  propSandboxes.push(sandbox);
13081
- const fieldVal = layer[field];
14570
+ const initSlot = fieldSlot(layer, field);
14571
+ const fieldVal = layer[initSlot];
13082
14572
  const initArg = field === "angles" && Array.isArray(fieldVal) ? wtext.radToScriptAngles(fieldVal) : Array.isArray(fieldVal) ? { x: fieldVal[0] ?? 0, y: fieldVal[1] ?? 0, z: fieldVal[2] ?? 0 } : fieldVal;
13083
14573
  sandbox.init(initArg);
13084
14574
  sandbox.applyUserProperties(objUserProps);
@@ -13088,6 +14578,8 @@ vec3 DecompressNormal(vec4 tex) {
13088
14578
  objectScriptRuns.push({
13089
14579
  layer,
13090
14580
  field,
14581
+ // 变换字段逐帧也在 local 槽上收发(与 init 同一空间)。
14582
+ slot: initSlot,
13091
14583
  kind: field === "visible" ? "bool" : field === "alpha" || field === "brightness" ? "scalar" : "vec3",
13092
14584
  sandbox
13093
14585
  });
@@ -13151,6 +14643,7 @@ vec3 DecompressNormal(vec4 tex) {
13151
14643
  window.__objScripts = objectScriptRuns;
13152
14644
  }
13153
14645
  window.__mediaHooks = mediaHooks;
14646
+ window.__sceneLayers = scene.layers;
13154
14647
  window.__compositeStats = () => renderer.compositeStats?.() ?? null;
13155
14648
  window.__compositeEnable = (on) => renderer.setCompositeEnabled?.(on);
13156
14649
  window.__scene = scene;
@@ -13211,6 +14704,7 @@ vec3 DecompressNormal(vec4 tex) {
13211
14704
  const playingVideos = [];
13212
14705
  const playingAudios = [];
13213
14706
  let lastRender = -Infinity;
14707
+ let lastAnimT = 0;
13214
14708
  const renderLoop = (now) => {
13215
14709
  if (disposed || rt.paused) return;
13216
14710
  const fps = rt.cfg.sceneFps || 60;
@@ -13227,8 +14721,10 @@ vec3 DecompressNormal(vec4 tex) {
13227
14721
  const t = (now - start - pauseAccum) / 1e3;
13228
14722
  inputView.update(pointerSrc.state);
13229
14723
  if (mediaSim.enabled) {
13230
- simMedia.update(t);
13231
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
14724
+ if (live?.media) live.media.pump();
14725
+ else simMedia.update(t);
14726
+ const snap = mediaSnapshot();
14727
+ const evts = media.diffMediaEvents(lastMediaSnap, snap);
13232
14728
  if (evts.length) {
13233
14729
  for (const { name, event } of evts) {
13234
14730
  for (const sb of mediaHooks) {
@@ -13236,38 +14732,42 @@ vec3 DecompressNormal(vec4 tex) {
13236
14732
  sb.callMedia(name, event);
13237
14733
  }
13238
14734
  }
13239
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
14735
+ lastMediaSnap = media.cloneMediaSnapshot(snap);
13240
14736
  }
13241
14737
  }
13242
- simWindow.update(t);
14738
+ if (live?.windowTitle) live.windowTitle.pump();
14739
+ else simWindow.update(t);
14740
+ const animDt = Math.max(0, t - lastAnimT);
14741
+ lastAnimT = t;
13243
14742
  for (const run of animRuns) {
13244
- run.ctrl.advance(interval / 1e3);
14743
+ run.ctrl.advance(animDt);
13245
14744
  const field = run.field;
14745
+ const slot = run.slot || field;
13246
14746
  const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
13247
14747
  if (Array.isArray(out)) {
13248
- const cur = run.layer[field];
14748
+ const cur = run.layer[slot];
13249
14749
  if (Array.isArray(cur)) for (let i = 0; i < out.length && i < cur.length; i++) cur[i] = out[i];
13250
14750
  } else if (Number.isFinite(out)) {
13251
14751
  if (field === "visible") run.layer[field] = !!out;
13252
- else run.layer[field] = out;
14752
+ else run.layer[slot] = out;
13253
14753
  }
13254
14754
  }
13255
14755
  for (const run of generalAnimRuns) {
13256
- run.ctrl.advance(interval / 1e3);
14756
+ run.ctrl.advance(animDt);
13257
14757
  const out = run.ctrl.applyTo(run.ctrl.baseNumeric);
13258
14758
  if (typeof out === "number" && Number.isFinite(out)) run.write(out);
13259
14759
  else if (Array.isArray(out) && Number.isFinite(out[0])) run.write(out[0]);
13260
14760
  }
13261
14761
  for (const run of effectVisibleRuns) {
13262
14762
  if (run.sandbox.disabled) continue;
13263
- run.sandbox.engine.frametime = interval / 1e3;
14763
+ run.sandbox.engine.frametime = animDt;
13264
14764
  run.sandbox.engine.runtime = t;
13265
14765
  const ret = run.sandbox.callUpdate(!!run.effect.visible);
13266
14766
  if (typeof ret === "boolean") run.effect.visible = ret;
13267
14767
  }
13268
14768
  for (const run of generalScriptRuns) {
13269
14769
  if (run.sandbox.disabled) continue;
13270
- run.sandbox.engine.frametime = interval / 1e3;
14770
+ run.sandbox.engine.frametime = animDt;
13271
14771
  run.sandbox.engine.runtime = t;
13272
14772
  const g = scene.general || {};
13273
14773
  const cur = g[run.field] && typeof g[run.field] === "object" && "value" in g[run.field] ? g[run.field].value : g[run.field];
@@ -13275,10 +14775,16 @@ vec3 DecompressNormal(vec4 tex) {
13275
14775
  if (ret !== void 0) run.write(ret);
13276
14776
  }
13277
14777
  const screenRes = { x: c.clientWidth || window.innerWidth || 1, y: c.clientHeight || window.innerHeight || 1 };
14778
+ for (const sb of propSandboxes) {
14779
+ if (!sb || sb.disabled) continue;
14780
+ sb.engine.frametime = animDt;
14781
+ sb.engine.runtime = t;
14782
+ sb.engine.screenResolution = screenRes;
14783
+ }
13278
14784
  let visibilityDirty = false;
13279
14785
  for (const run of objectScriptRuns) {
13280
14786
  if (run.sandbox.disabled) continue;
13281
- run.sandbox.engine.frametime = interval / 1e3;
14787
+ run.sandbox.engine.frametime = animDt;
13282
14788
  run.sandbox.engine.runtime = t;
13283
14789
  run.sandbox.engine.screenResolution = screenRes;
13284
14790
  const cur = run.layer[run.field];
@@ -13299,16 +14805,23 @@ vec3 DecompressNormal(vec4 tex) {
13299
14805
  const n = Number(ret);
13300
14806
  if (Number.isFinite(n)) run.layer[run.field] = n;
13301
14807
  } else {
13302
- const v = run.field === "angles" ? wtext.radToScriptAngles(cur) : { x: cur[0] || 0, y: cur[1] || 0, z: cur[2] || 0 };
14808
+ const slot = run.slot || run.field;
14809
+ const lcur = run.layer[slot];
14810
+ const v = run.field === "angles" ? wtext.radToScriptAngles(lcur) : { x: lcur[0] || 0, y: lcur[1] || 0, z: lcur[2] || 0 };
13303
14811
  const ret = run.sandbox.callUpdate(v);
13304
14812
  const o = ret && typeof ret === "object" && "x" in ret ? ret : v;
13305
- run.layer[run.field] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
14813
+ run.layer[slot] = run.field === "angles" ? wtext.scriptAnglesToRad(o) : [o.x || 0, o.y || 0, o.z || 0];
13306
14814
  }
13307
14815
  }
13308
14816
  if (visibilityDirty) recomputeVisibility();
14817
+ if (transformDirty.size) scn.recomposeWorld(scene.layers, transformDirty);
13309
14818
  if (audioSim.enabled) {
13310
- simAudio.update(t);
13311
- fillAudioBuffers(audioViews, simAudio.snapshot);
14819
+ hostAudio.pump();
14820
+ if (!hostAudio.active) {
14821
+ if (audioDriverRef.current) audioDriverRef.current.pump();
14822
+ else simAudio.update(t);
14823
+ }
14824
+ fillAudioBuffers(audioViews, activeAudioSnapshot());
13312
14825
  }
13313
14826
  if (attachFollows.length) {
13314
14827
  mdl.followAttachments(attachFollows, t, getBoneOverrides);
@@ -13495,11 +15008,672 @@ vec3 DecompressNormal(vec4 tex) {
13495
15008
  }
13496
15009
  })();
13497
15010
  }
15011
+ const SHIM_MARK = 'data-we-shim="1"';
15012
+ const SHIM_ATTR = "data-we-shim-src";
15013
+ function entryDirUrl(entryUrl) {
15014
+ try {
15015
+ const u = new URL(entryUrl);
15016
+ const path = u.pathname;
15017
+ const i = path.lastIndexOf("/");
15018
+ u.pathname = i < 0 ? "/" : path.slice(0, i + 1);
15019
+ u.hash = "";
15020
+ u.search = "";
15021
+ return u.href;
15022
+ } catch {
15023
+ const s = entryUrl.replace(/[#?].*$/, "");
15024
+ const i = s.lastIndexOf("/");
15025
+ return i < 0 ? s : s.slice(0, i + 1);
15026
+ }
15027
+ }
15028
+ function hasBlockingCsp(html) {
15029
+ const re = /<meta[^>]+http-equiv\s*=\s*["']?Content-Security-Policy["']?[^>]*>/gi;
15030
+ let m;
15031
+ while (m = re.exec(html)) {
15032
+ const tag = m[0];
15033
+ const content = /content\s*=\s*"([^"]*)"/i.exec(tag)?.[1] ?? /content\s*=\s*'([^']*)'/i.exec(tag)?.[1] ?? "";
15034
+ if (!/script-src/i.test(content)) continue;
15035
+ if (/script-src[^;]*'unsafe-inline'/i.test(content)) continue;
15036
+ if (/script-src[^;]*\*/i.test(content)) continue;
15037
+ return true;
15038
+ }
15039
+ return false;
15040
+ }
15041
+ function escapeScriptClose(js) {
15042
+ return js.replace(/<\/script/gi, "<\\/script");
15043
+ }
15044
+ function rewriteHtml(html, shimSource2, opts) {
15045
+ if (!html) html = "";
15046
+ if (html.includes(SHIM_MARK) || html.includes(SHIM_ATTR)) return html;
15047
+ const base = opts.baseHref && !/<base\b/i.test(html) ? `<base href="${opts.baseHref.replace(/"/g, "&quot;")}">` : "";
15048
+ const script = `<script ${SHIM_ATTR}="1">
15049
+ ${escapeScriptClose(shimSource2)}
15050
+ <\/script>`;
15051
+ const seed = opts.seedScript && opts.seedScript.trim() ? `<script>
15052
+ ${escapeScriptClose(opts.seedScript)}
15053
+ <\/script>` : "";
15054
+ const inject = `${base}${script}${seed}`;
15055
+ const headOpen = /<head(\s[^>]*)?>/i.exec(html);
15056
+ if (headOpen) {
15057
+ const at = headOpen.index + headOpen[0].length;
15058
+ return html.slice(0, at) + inject + html.slice(at);
15059
+ }
15060
+ const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
15061
+ if (htmlOpen) {
15062
+ const at = htmlOpen.index + htmlOpen[0].length;
15063
+ return html.slice(0, at) + `<head>${inject}</head>` + html.slice(at);
15064
+ }
15065
+ return `<!DOCTYPE html><html><head>${inject}</head><body>${html}</body></html>`;
15066
+ }
15067
+ const shimSource = '/**\n * WE 网页壁纸兼容 shim(注入到 iframe,必须在作者脚本之前执行)。\n *\n * 语料:本机库 42 张 web 壁纸扫描(2026-09)——\n * wallpaperPropertyListener 29 / RegisterAudioListener 22 /\n * RequestRandomFileForProperty 8 / userDirectoryFiles* 9 /\n * Media*Listener 2 / PluginListener 2\n *\n * 官方 CEF 在任何壁纸脚本前就把这些做成原生函数;工坊顶层直接注册。\n * 本文件作为 <head> 首个 classic script 插入。\n *\n * 父页控制面 __we*(main.ts weShimCall / web.ts 泵):\n * __weSetPaused / __weSetFps / __weSetVolume / __weApplyProps / __weSeedProps\n * __wePushAudio(arr128)\n * __wePushMedia(event) — {op, payload} 见下\n * __wePushDirectoryFiles(prop, files) / __weRemoveDirectoryFiles(prop, files)\n * __weRewriteFileUrl(s) — file:/// → 同源相对(HTTP 页);空 file:/// → ""\n * __wePushPointer(x, y, buttons) / __wePointerLeave() — 外部指针注入(见文末)\n */\n(function (w) {\n "use strict";\n try {\n if (w.document && w.document.documentElement) {\n w.document.documentElement.setAttribute("data-we-shim", "1");\n }\n } catch (_) {\n /* 忽略 */\n }\n\n var audioListener = null;\n var propertyListener = null;\n var paused = false;\n var fps = 60;\n var volume = 0;\n var pendingProps = null;\n var pendingGeneral = null;\n var rafMap = Object.create(null);\n var rafCounter = 0;\n var origRaf = w.requestAnimationFrame.bind(w);\n var origCaf = w.cancelAnimationFrame.bind(w);\n\n // 官方 CEF 以文件系统为源,作者普遍 `\'file:///\' + value`。HTTP 同源页里\n // file:///files/x.webm 加载失败;空 value 变成 file:///(1748506393)。\n // file: 协议页保持原样(真本地嵌入)。\n function rewriteBareFileUrl(url) {\n if (typeof url !== "string") return url;\n var s = url.trim();\n if (!/^file:/i.test(s)) return s;\n try {\n var loc = w.location;\n if (loc && loc.protocol === "file:") return s;\n } catch (_) {\n /* 忽略 */\n }\n var rest = s.replace(/^file:\\/\\//i, "").replace(/^\\/+/, "");\n if (!rest) return "";\n if (/^[a-zA-Z][:|]/.test(rest)) return "";\n if (/^(Users|home|tmp|var|etc|private|Volumes)\\//.test(rest)) return "";\n try {\n var href = w.location && w.location.href;\n if (href) return new URL(rest, href).href;\n } catch (_) {\n /* 忽略 */\n }\n return rest;\n }\n\n function rewriteWeFileUrl(input) {\n if (typeof input !== "string") return input;\n if (/url\\(/i.test(input)) {\n return input.replace(/url\\(\\s*([\'"]?)([^)\'"]*?)\\1\\s*\\)/gi, function (_m, q, inner) {\n var next = rewriteBareFileUrl(inner);\n if (!next) return "none";\n var quote = q || \'"\';\n return "url(" + quote + next + quote + ")";\n });\n }\n return rewriteBareFileUrl(input);\n }\n\n w.__weRewriteFileUrl = rewriteWeFileUrl;\n\n function installFileUrlHooks() {\n try {\n if (w.Element && w.Element.prototype && typeof w.Element.prototype.setAttribute === "function") {\n var origSetAttr = w.Element.prototype.setAttribute;\n w.Element.prototype.setAttribute = function (name, value) {\n var n = String(name || "").toLowerCase();\n if (n === "src" || n === "href" || n === "poster") value = rewriteWeFileUrl(value);\n return origSetAttr.call(this, name, value);\n };\n }\n } catch (_) {\n /* 无 DOM 的 verifier 跳过 */\n }\n var ctorNames = ["HTMLImageElement", "HTMLMediaElement", "HTMLSourceElement", "HTMLScriptElement"];\n for (var i = 0; i < ctorNames.length; i++) {\n try {\n var Ctor = w[ctorNames[i]];\n if (!Ctor || !Ctor.prototype) continue;\n var desc = Object.getOwnPropertyDescriptor(Ctor.prototype, "src");\n if (!desc || typeof desc.set !== "function") continue;\n (function (d) {\n Object.defineProperty(Ctor.prototype, "src", {\n configurable: true,\n enumerable: d.enumerable,\n get: d.get,\n set: function (v) {\n d.set.call(this, rewriteWeFileUrl(v));\n },\n });\n })(desc);\n } catch (_) {\n /* 忽略单个原型 */\n }\n }\n try {\n var styleDesc =\n w.HTMLElement && Object.getOwnPropertyDescriptor(w.HTMLElement.prototype, "style");\n // Chromium 的 backgroundImage 不是原型自有描述符,只能包 HTMLElement.style 的 Proxy。\n if (styleDesc && typeof styleDesc.get === "function" && w.Proxy && w.WeakMap) {\n var styleCache = new w.WeakMap();\n Object.defineProperty(w.HTMLElement.prototype, "style", {\n configurable: true,\n enumerable: styleDesc.enumerable,\n get: function () {\n var raw = styleDesc.get.call(this);\n if (!raw) return raw;\n var cached = styleCache.get(raw);\n if (cached) return cached;\n var proxy = new w.Proxy(raw, {\n set: function (target, prop, value) {\n if (typeof value === "string" && typeof prop === "string" && /background/i.test(prop)) {\n value = rewriteWeFileUrl(value);\n }\n target[prop] = value;\n return true;\n },\n get: function (target, prop) {\n var v = target[prop];\n if (typeof v === "function") return v.bind(target);\n return v;\n },\n });\n styleCache.set(raw, proxy);\n return proxy;\n },\n set: styleDesc.set,\n });\n }\n var styleProto = w.CSSStyleDeclaration && w.CSSStyleDeclaration.prototype;\n if (styleProto && typeof styleProto.setProperty === "function") {\n var origSetProp = styleProto.setProperty;\n styleProto.setProperty = function (name, value, priority) {\n if (typeof value === "string" && /background/i.test(String(name || ""))) {\n value = rewriteWeFileUrl(value);\n }\n return origSetProp.call(this, name, value, priority);\n };\n }\n } catch (_) {\n /* 忽略 */\n }\n }\n installFileUrlHooks();\n\n // propertyName → string[](绝对/相对路径;随机文件从此抽)\n var directoryFiles = Object.create(null);\n\n var mediaListeners = {\n properties: null,\n thumbnail: null,\n playback: null,\n timeline: null,\n status: null,\n };\n // 晚注册时回放最近一帧(作者脚本常在 DOMContentLoaded 后才 Register)\n var lastMedia = {\n properties: null,\n thumbnail: null,\n playback: null,\n timeline: null,\n status: null,\n };\n\n function callApplyUserProperties(props) {\n if (!propertyListener || typeof propertyListener.applyUserProperties !== "function") return;\n try {\n propertyListener.applyUserProperties(props || {});\n } catch (_) {\n /* 壁纸脚本抛错不打断宿主 */\n }\n }\n\n function callApplyGeneralProperties(props) {\n if (!propertyListener || typeof propertyListener.applyGeneralProperties !== "function") return;\n try {\n propertyListener.applyGeneralProperties(props || {});\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function callSetPaused(v) {\n if (!propertyListener || typeof propertyListener.setPaused !== "function") return;\n try {\n propertyListener.setPaused(!!v);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function callDirectoryAdded(prop, files) {\n if (!propertyListener || typeof propertyListener.userDirectoryFilesAddedOrChanged !== "function")\n return;\n try {\n propertyListener.userDirectoryFilesAddedOrChanged(prop, files);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function callDirectoryRemoved(prop, files) {\n if (!propertyListener || typeof propertyListener.userDirectoryFilesRemoved !== "function") return;\n try {\n propertyListener.userDirectoryFilesRemoved(prop, files);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n function flushPending() {\n if (pendingProps) {\n var p = pendingProps;\n pendingProps = null;\n callApplyUserProperties(p);\n }\n if (pendingGeneral) {\n var g = pendingGeneral;\n pendingGeneral = null;\n callApplyGeneralProperties(g);\n }\n }\n\n /**\n * 工坊常在 React render 里写 `window.wallpaperPropertyListener = {…}`(2905017768)。\n * 官方 CEF 不会在赋值当下同步回调 setPaused/apply*;若我们同步 flush,\n * 等于 render 中 setState → React 熔断 → #root 空(一片黑)。\n */\n function afterAssign(fn) {\n try {\n if (typeof w.queueMicrotask === "function") w.queueMicrotask(fn);\n else w.setTimeout(fn, 0);\n } catch (_) {\n try {\n fn();\n } catch (_) {\n /* 忽略 */\n }\n }\n }\n\n function safeCall(fn, arg) {\n if (typeof fn !== "function") return;\n try {\n fn(arg);\n } catch (_) {\n /* 忽略 */\n }\n }\n\n // —— 媒体集成枚举(3747222633:缺省时 PLAYBACK_PLAYING||0 会把「播放」当成 0)——\n w.wallpaperMediaIntegration = {\n PLAYBACK_STOPPED: 0,\n PLAYBACK_PLAYING: 1,\n PLAYBACK_PAUSED: 2,\n };\n\n // —— 官方 API:音频 ——\n w.wallpaperRegisterAudioListener = function (cb) {\n audioListener = typeof cb === "function" ? cb : null;\n };\n\n // —— 官方 API:媒体 ——\n w.wallpaperRegisterMediaPropertiesListener = function (cb) {\n mediaListeners.properties = typeof cb === "function" ? cb : null;\n if (mediaListeners.properties && lastMedia.properties) {\n safeCall(mediaListeners.properties, lastMedia.properties);\n }\n };\n w.wallpaperRegisterMediaThumbnailListener = function (cb) {\n mediaListeners.thumbnail = typeof cb === "function" ? cb : null;\n if (mediaListeners.thumbnail && lastMedia.thumbnail) {\n safeCall(mediaListeners.thumbnail, lastMedia.thumbnail);\n }\n };\n w.wallpaperRegisterMediaPlaybackListener = function (cb) {\n mediaListeners.playback = typeof cb === "function" ? cb : null;\n if (mediaListeners.playback && lastMedia.playback) {\n safeCall(mediaListeners.playback, lastMedia.playback);\n }\n };\n w.wallpaperRegisterMediaTimelineListener = function (cb) {\n mediaListeners.timeline = typeof cb === "function" ? cb : null;\n if (mediaListeners.timeline && lastMedia.timeline) {\n safeCall(mediaListeners.timeline, lastMedia.timeline);\n }\n };\n w.wallpaperRegisterMediaStatusListener = function (cb) {\n mediaListeners.status = typeof cb === "function" ? cb : null;\n if (mediaListeners.status && lastMedia.status) {\n safeCall(mediaListeners.status, lastMedia.status);\n }\n };\n\n // —— 官方 API:随机文件(slideshow)——\n // 回调签名:function(propertyName, filePath)。无库存文件时 filePath 为空串(语料 if(i) 守卫)。\n w.wallpaperRequestRandomFileForProperty = function (propertyName, callback) {\n if (typeof callback !== "function") return;\n var prop = String(propertyName || "");\n var list = directoryFiles[prop];\n var path = "";\n if (list && list.length) {\n path = String(list[(Math.random() * list.length) | 0] || "");\n }\n try {\n callback(prop, path);\n } catch (_) {\n /* 忽略 */\n }\n };\n\n // —— PropertyListener(getter/setter;回调延后到微任务,见 afterAssign)——\n // 官方在页面加载完成后才发全量属性/暂停状态;首屏脚本(body onLoad=init 等)常\n // 假设属性到达时 DOM/场景已初始化(827982449:applyUserProperties→cl() 在 load 前\n // 跑会撞上未创建的 scene/material)。未加载完成时等 window load + 一个宏任务\n // (保证排在 onLoad 属性处理器之后),已加载完成则微任务即发。\n function whenPageReady(fn) {\n var ready = "complete";\n try {\n ready = w.document.readyState;\n } catch (_) {\n /* 忽略 */\n }\n if (ready === "complete") {\n afterAssign(fn);\n return;\n }\n try {\n w.addEventListener("load", function () {\n // setTimeout 保证排在 load 同步链(onLoad 处理器)之后\n w.setTimeout(fn, 0);\n }, { once: true });\n } catch (_) {\n afterAssign(fn);\n }\n }\n Object.defineProperty(w, "wallpaperPropertyListener", {\n configurable: true,\n enumerable: true,\n get: function () {\n return propertyListener;\n },\n set: function (v) {\n var next = v && typeof v === "object" ? v : null;\n var prev = propertyListener;\n propertyListener = next;\n if (!next) return;\n // 仅首次注册补发挂载状态。官方 CEF 从不在赋值当下回调;2905017768 等 React 壁纸在\n // 渲染体里重新赋值(新对象字面量),若每次都补 setPaused 会形成\n // 渲染 → 赋值 → 补发 setState → 再渲染 的微任务死循环(点下一曲整页卡死)。\n if (prev) return;\n whenPageReady(function () {\n flushPending();\n callApplyGeneralProperties({ fps: fps });\n callSetPaused(paused);\n // 已缓存的目录文件补推一次(作者可能后挂 userDirectoryFilesAddedOrChanged)\n for (var prop in directoryFiles) {\n if (Object.prototype.hasOwnProperty.call(directoryFiles, prop) && directoryFiles[prop].length) {\n callDirectoryAdded(prop, directoryFiles[prop].slice());\n }\n }\n });\n },\n });\n\n // —— Plugin(iCUE 等;无硬件时空实现,避免 if 判断失败)——\n if (!w.wallpaperPluginListener) {\n w.wallpaperPluginListener = {\n onPluginLoaded: function () {},\n };\n }\n\n // —— 定时器冻结:官方暂停 = "fully freeze the process that renders the wallpaper",\n // rAF 已在节流层挂起,这里冻结定时器:暂停期间新建的挂起登记、恢复时按原延迟/间隔\n // 重新启动;**已启动**的真定时器到期由包装回调拦下——timeout 转挂起(恢复后立即补跑,\n // 近似官方的剩余等待),interval 直接跳过该周期(恢复后从下个周期继续)。\n var pendTimers = [];\n var tmSeq = 0;\n var TM_BASE = 0x40000000; // 假 id 段,避免与真实 timer id 混淆\n var origST = w.setTimeout;\n var origSI = w.setInterval;\n var origCTO = w.clearTimeout;\n var origCIT = w.clearInterval;\n function guardTimeout(fn) {\n if (typeof fn !== "function") return fn;\n return function () {\n if (paused) {\n pendTimers.push({ id: 0, kind: "t", fn: fn, ms: 1, extra: [] });\n return;\n }\n return fn.apply(this, arguments);\n };\n }\n function guardInterval(fn) {\n if (typeof fn !== "function") return fn;\n return function () {\n if (paused) return;\n return fn.apply(this, arguments);\n };\n }\n function startTimer(kind, fn, ms, extra) {\n if (paused) {\n var id = TM_BASE + ++tmSeq;\n pendTimers.push({ id: id, kind: kind, fn: fn, ms: ms, extra: extra });\n return id;\n }\n var args = [kind === "t" ? guardTimeout(fn) : guardInterval(fn), ms].concat(extra);\n return (kind === "t" ? origST : origSI).apply(w, args);\n }\n w.setTimeout = function (fn, ms) {\n return startTimer("t", fn, ms, Array.prototype.slice.call(arguments, 2));\n };\n w.setInterval = function (fn, ms) {\n return startTimer("i", fn, ms, Array.prototype.slice.call(arguments, 2));\n };\n function unpend(id) {\n for (var i = 0; i < pendTimers.length; i++) {\n if (pendTimers[i].id === id) {\n pendTimers.splice(i, 1);\n return true;\n }\n }\n return false;\n }\n w.clearTimeout = function (id) {\n if (unpend(id)) return;\n origCTO.call(w, id);\n };\n w.clearInterval = function (id) {\n if (unpend(id)) return;\n origCIT.call(w, id);\n };\n function resumeTimers() {\n var list = pendTimers;\n pendTimers = [];\n for (var i = 0; i < list.length; i++) {\n var t = list[i];\n (t.kind === "t" ? origST : origSI).call(w, t.fn, t.ms, t.extra);\n }\n }\n\n /**\n * 恢复暂停期间被挂起的 rAF 请求。\n *\n * **不补跑这些回调,主循环就永久断掉**(1278092907 Monstercat:`draw()` 在函数体\n * 开头就 `requestAnimationFrame(draw)` 再画,暂停期间那次请求被登记成 hold,\n * 恢复后没人跑它 → 整条链没有下一帧,画面永久定格,且没有任何报错)。\n * 这是 rAF 自递归的通用形态,不是这一张的特例。\n *\n * 走 `w.requestAnimationFrame` 而不是 `origRaf`:此时已 unpaused,要让它重新经过\n * 节流层(低 fps 时该走 setTimeout 路径),并照常发 we-frame 打点。\n */\n function resumeRafHolds() {\n var holds = [];\n for (var id in rafMap) {\n if (!Object.prototype.hasOwnProperty.call(rafMap, id)) continue;\n if (rafMap[id] && rafMap[id].kind === "hold") {\n holds.push(rafMap[id].cb);\n delete rafMap[id];\n }\n }\n for (var i = 0; i < holds.length; i++) {\n try {\n w.requestAnimationFrame(holds[i]);\n } catch (_) {\n /* 单个回调重挂失败不影响其它 */\n }\n }\n }\n\n // —— 媒体音量:对齐官方 CEF 语义(浏览器级主音量与作者页面内音量独立相乘)——\n // 作者常在播放前重设 a.volume = uiVolume(Bocchi),且音频多为 `new Audio()` 不进\n // DOM——querySelectorAll 找不到、直接覆盖 volume 又会被作者回写。因此 hook 原型:\n // setter 记作者值,元素实际音量 = 作者值 × 主音量;`__weSetVolume` 改系数并刷新\n // 全部活实例(DOM 内 + Audio 构造器登记的 WeakRef)。\n var hostVolume = 1;\n var liveMedia = []; // WeakRef<HTMLMediaElement>\n function trackMedia(el) {\n if (!w.WeakRef) return;\n liveMedia.push(new w.WeakRef(el));\n }\n function applyMediaVolume(el) {\n if (el.__weBaseVol != null) {\n mediaVolDesc.set.call(el, el.__weBaseVol * hostVolume);\n } else {\n mediaVolDesc.set.call(el, hostVolume);\n }\n var baseMuted = !!el.__weBaseMuted;\n mediaMutedDesc.set.call(el, baseMuted || hostVolume <= 0);\n }\n function refreshAllMediaVolume() {\n try {\n var nodes = w.document.querySelectorAll("audio,video");\n for (var i = 0; i < nodes.length; i++) applyMediaVolume(nodes[i]);\n } catch (_) {\n /* 忽略 */\n }\n for (var j = liveMedia.length - 1; j >= 0; j--) {\n var el = liveMedia[j].deref();\n if (!el) {\n liveMedia.splice(j, 1);\n continue;\n }\n applyMediaVolume(el);\n }\n }\n var mediaVolDesc = null;\n var mediaMutedDesc = null;\n function installMediaVolumeHooks() {\n try {\n if (!w.HTMLMediaElement || !w.HTMLMediaElement.prototype) return;\n var proto = w.HTMLMediaElement.prototype;\n mediaVolDesc = Object.getOwnPropertyDescriptor(proto, "volume");\n mediaMutedDesc = Object.getOwnPropertyDescriptor(proto, "muted");\n if (mediaVolDesc && typeof mediaVolDesc.set === "function") {\n Object.defineProperty(proto, "volume", {\n configurable: true,\n enumerable: mediaVolDesc.enumerable,\n get: function () {\n return this.__weBaseVol != null ? this.__weBaseVol : mediaVolDesc.get.call(this);\n },\n set: function (v) {\n this.__weBaseVol = Math.max(0, Math.min(1, Number(v) || 0));\n mediaVolDesc.set.call(this, this.__weBaseVol * hostVolume);\n },\n });\n }\n if (mediaMutedDesc && typeof mediaMutedDesc.set === "function") {\n Object.defineProperty(proto, "muted", {\n configurable: true,\n enumerable: mediaMutedDesc.enumerable,\n get: function () {\n return this.__weBaseMuted != null\n ? this.__weBaseMuted || hostVolume <= 0\n : mediaMutedDesc.get.call(this);\n },\n set: function (v) {\n this.__weBaseMuted = !!v;\n mediaMutedDesc.set.call(this, !!v || hostVolume <= 0);\n },\n });\n }\n // `new Audio()` 不进 DOM:构造器登记 WeakRef 以便主音量变化时刷新\n if (typeof w.Audio === "function" && w.WeakRef) {\n var OrigAudio = w.Audio;\n function WrappedAudio(src) {\n var a = new OrigAudio(src);\n trackMedia(a);\n applyMediaVolume(a);\n return a;\n }\n WrappedAudio.prototype = OrigAudio.prototype;\n w.Audio = WrappedAudio;\n }\n } catch (_) {\n /* 无媒体环境的 verifier 跳过 */\n }\n }\n installMediaVolumeHooks();\n\n // —— 父页控制面 ——\n // 官方 setPaused 只在暂停状态实际变化时调用一次;重复调用去重。\n // 暂停还要冻结页内媒体:官方是进程级冻结(无声、解码器可回收),作者的\n // setPaused 常只管自己的逻辑。只记录「我们代为暂停」的元素,恢复时仅还原这部分,\n // 不碰作者自己暂停的。\n var weFrozenMedia = [];\n function freezePageMedia() {\n weFrozenMedia.length = 0;\n try {\n var nodes = w.document.querySelectorAll("audio,video");\n for (var i = 0; i < nodes.length; i++) {\n if (!nodes[i].paused) {\n weFrozenMedia.push(nodes[i]);\n try {\n nodes[i].pause();\n } catch (_) {\n /* 忽略 */\n }\n }\n }\n } catch (_) {\n /* 忽略 */\n }\n }\n function thawPageMedia() {\n for (var i = 0; i < weFrozenMedia.length; i++) {\n try {\n var p = weFrozenMedia[i].play();\n if (p && p.catch) p.catch(function () {});\n } catch (_) {\n /* 忽略 */\n }\n }\n weFrozenMedia.length = 0;\n }\n\n /**\n * 暂停还要冻结 **CSS 动画 / 过渡**(Web Animations 时间轴)。\n *\n * rAF 与定时器冻结管不到它们:CSS `animation` 由浏览器**合成器**独立驱动,\n * 与 JS 主线程无关。1444432396 Glitch Clock 的整个视觉(背景移动、抖动、故障\n * 闪烁)是 10 处 `animation: … infinite`,只有时钟文字走 `setInterval` ——\n * 暂停后画面照旧动个不停,用户看到的就是「无法暂停」(实测暂停期间 6 个动画\n * 全为 `playState:"running"`,`currentTime` 700ms 推进整 700ms)。\n *\n * 官方暂停语义是「fully freeze the process that renders the wallpaper」,\n * 合成器动画自然也在冻结范围内。\n *\n * 与媒体冻结同一条纪律:**只记录我们代为暂停的**,恢复时仅还原这部分——\n * 作者自己用 `animation-play-state: paused` 停下的(常见于 hover 才播的装饰)\n * 不能被我们唤醒。`getAnimations()` 拿的是活动动画对象,`pause()`/`play()`\n * 直接作用在时间轴上,比改 `style.animationPlayState` 干净(后者会污染作者的\n * 内联样式,且被作者下一次样式写入覆盖)。\n */\n var weFrozenAnims = [];\n function freezePageAnimations() {\n weFrozenAnims.length = 0;\n try {\n if (typeof w.document.getAnimations !== "function") return;\n var anims = w.document.getAnimations();\n for (var i = 0; i < anims.length; i++) {\n var a = anims[i];\n if (a && a.playState === "running") {\n weFrozenAnims.push(a);\n try {\n a.pause();\n } catch (_) {\n /* 个别动画不可暂停时跳过 */\n }\n }\n }\n } catch (_) {\n /* 旧引擎无 getAnimations:退化为不冻结,不报错 */\n }\n }\n function thawPageAnimations() {\n for (var i = 0; i < weFrozenAnims.length; i++) {\n try {\n weFrozenAnims[i].play();\n } catch (_) {\n /* 已被作者移除的动画忽略 */\n }\n }\n weFrozenAnims.length = 0;\n }\n w.__weSetPaused = function (v) {\n var next = !!v;\n if (next === paused) return;\n paused = next;\n if (paused) {\n callSetPaused(true);\n freezePageMedia();\n freezePageAnimations();\n } else {\n callSetPaused(false);\n thawPageMedia();\n thawPageAnimations();\n resumeTimers();\n // rAF 挂起项必须补跑,否则自递归的主循环永久断链(1278092907)\n resumeRafHolds();\n }\n };\n\n w.__weSetFps = function (n) {\n var next = Number(n);\n if (!Number.isFinite(next) || next <= 0) return;\n fps = next;\n callApplyGeneralProperties({ fps: fps });\n };\n\n w.__weSetVolume = function (v) {\n var next = Math.max(0, Math.min(1, Number(v) || 0));\n volume = next;\n hostVolume = next;\n refreshAllMediaVolume();\n };\n\n w.__weApplyProps = function (props) {\n if (!props || typeof props !== "object") return;\n // file 属性:值是路径时登记进随机池(单文件 slideshow)\n try {\n for (var key in props) {\n if (!Object.prototype.hasOwnProperty.call(props, key)) continue;\n var ent = props[key];\n var val = ent && typeof ent === "object" && "value" in ent ? ent.value : ent;\n if (typeof val === "string" && val !== "" && /\\.(png|jpe?g|gif|webp|webm|mp4|bmp)$/i.test(val)) {\n directoryFiles[key] = [val];\n }\n }\n } catch (_) {\n /* 忽略 */\n }\n if (!propertyListener || typeof propertyListener.applyUserProperties !== "function") {\n pendingProps = props;\n return;\n }\n callApplyUserProperties(props);\n };\n\n w.__weSeedProps = function (props) {\n if (!props || typeof props !== "object") return;\n if (propertyListener && typeof propertyListener.applyUserProperties === "function") {\n w.__weApplyProps(props);\n } else {\n pendingProps = props;\n }\n };\n\n w.__wePushAudio = function (arr) {\n if (paused || !audioListener) return;\n try {\n audioListener(arr);\n } catch (_) {\n /* 忽略 */\n }\n };\n\n /**\n * 媒体事件泵。payload 形态对齐官方:\n * { op:"properties", title, artist, album, albumArtist }\n * { op:"thumbnail", thumbnail, primaryColor, textColor, ... }\n * { op:"playback", state } // 0/1/2\n * { op:"timeline", position, duration }\n * { op:"status", enabled }\n */\n w.__wePushMedia = function (payload) {\n if (!payload || typeof payload !== "object") return;\n var op = payload.op;\n if (op === "properties") {\n lastMedia.properties = payload;\n safeCall(mediaListeners.properties, payload);\n } else if (op === "thumbnail") {\n lastMedia.thumbnail = payload;\n safeCall(mediaListeners.thumbnail, payload);\n } else if (op === "playback") {\n lastMedia.playback = payload;\n safeCall(mediaListeners.playback, payload);\n } else if (op === "timeline") {\n lastMedia.timeline = payload;\n safeCall(mediaListeners.timeline, payload);\n } else if (op === "status") {\n lastMedia.status = payload;\n safeCall(mediaListeners.status, payload);\n }\n };\n\n /** 目录文件列表(首次或追加)。files: string[] */\n w.__wePushDirectoryFiles = function (propertyName, files) {\n var prop = String(propertyName || "");\n if (!prop || !Array.isArray(files)) return;\n var cleaned = [];\n for (var i = 0; i < files.length; i++) {\n if (files[i] != null && String(files[i]) !== "") cleaned.push(String(files[i]));\n }\n if (!directoryFiles[prop]) directoryFiles[prop] = [];\n // 首次全量替换语义由调用方决定;这里 concat 去重\n var seen = Object.create(null);\n for (var j = 0; j < directoryFiles[prop].length; j++) seen[directoryFiles[prop][j]] = 1;\n var added = [];\n for (var k = 0; k < cleaned.length; k++) {\n if (!seen[cleaned[k]]) {\n seen[cleaned[k]] = 1;\n directoryFiles[prop].push(cleaned[k]);\n added.push(cleaned[k]);\n }\n }\n if (added.length) callDirectoryAdded(prop, added);\n };\n\n w.__weRemoveDirectoryFiles = function (propertyName, files) {\n var prop = String(propertyName || "");\n if (!prop || !Array.isArray(files) || !directoryFiles[prop]) return;\n var removeSet = Object.create(null);\n for (var i = 0; i < files.length; i++) removeSet[String(files[i])] = 1;\n var kept = [];\n var removed = [];\n for (var j = 0; j < directoryFiles[prop].length; j++) {\n var f = directoryFiles[prop][j];\n if (removeSet[f]) removed.push(f);\n else kept.push(f);\n }\n directoryFiles[prop] = kept;\n if (removed.length) callDirectoryRemoved(prop, removed);\n };\n\n // —— 外部指针注入(桌面 underlay 层收不到鼠标事件,父页经 __wp.pushPointer 推入)——\n //\n // 场景壁纸那条通道是「写一个状态对象、渲染器每帧读」(render/pointer.js);网页壁纸\n // 没有这样的单一消费点 —— 作者代码就是**监听 DOM 事件**的,所以这里必须把推送\n // 还原成一串合成事件。语料(本机 49 张 web):mousemove 24 张、click 29 张、\n // mouseover/out 17 张、mouseenter/leave 8 张、pointer* 16 张(createjs 系一律走\n // pointerdown/move/up)、.button 18 张、.which 17 张、pointerId/relatedTarget 15 张。\n //\n // 三条要点(都有语料依据,改错了会静默失效):\n //\n // 1. **必须 elementFromPoint 按命中元素派发**,不能一律打 document。作者既有挂\n // document/window 的(15 张,靠冒泡收到),也有挂 canvas 上读 `event.offsetX`\n // 的(1748506393 流体 `pointers[0].dx = (e.offsetX - …)`)。offsetX/offsetY 由\n // 浏览器按 target 的 padding box 现算 —— target 打错就是错的偏移,且无任何报错。\n // pageX/pageY 同理由 clientX + 滚动量现算,不用我们填。\n //\n // 2. **over/out/enter/leave 链要按 W3C 语义补全**。1748506393 靠 canvas 的\n // `mouseenter` 把 `pointers[0].down` 置 true(不进这个分支则鼠标怎么动都不出染料)、\n // 靠 window 的 `mouseleave` 复位;1081733658 animatedGrid 靠 `document.body` 的\n // mouseover/mouseleave 起停整个网格动画。leave/enter 不冒泡,必须自己沿祖先链走到\n // 最近公共祖先,只发生变化的那一段。\n //\n // 3. **click 要靠 down/up 边缘合成**,且 down 与 up 的 target 不同(拖拽)时不发。\n // 29 张听 click 是最大的消费方;轮询推送里没有「点击」这个事件,只有按键掩码的\n // 跳变,边缘丢了就等于整类交互消失。\n //\n // 硬限制(写在这里避免反复试):CSS `:hover` 由浏览器自己的 hit-test 驱动,合成事件\n // 永远点不亮它(18 张含 `:hover`)—— 纯 CSS hover 动画的壁纸无法用注入通道响应,\n // 这不是实现缺陷,是合成事件的固有边界。\n var ptrHas = false; // 是否收到过推送(首帧 movement 归零用)\n var ptrX = 0;\n var ptrY = 0;\n var ptrButtons = 0;\n var ptrTarget = null; // 上次命中元素(over/out 链的旧端)\n var ptrDownTarget = null; // 按下时的命中元素(click 判定)\n var ptrLastClickTime = 0;\n var ptrLastClickTarget = null;\n /** 双击判定窗口(ms)。与主流浏览器一致,语料里 5 张听 dblclick。 */\n var PTR_DBLCLICK_MS = 500;\n\n function ptrRoot() {\n try {\n return w.document.body || w.document.documentElement || null;\n } catch (_) {\n return null;\n }\n }\n\n function ptrHitTest(x, y) {\n try {\n if (typeof w.document.elementFromPoint === "function") {\n var el = w.document.elementFromPoint(x, y);\n if (el) return el;\n }\n } catch (_) {\n /* 忽略 */\n }\n return ptrRoot();\n }\n\n /** node → [node, parent, …, root];用 parentNode 而非 parentElement,\n * 这样 document / documentElement 也在链里(作者挂 document 的 leave 要收到)。 */\n function ptrChain(node) {\n var out = [];\n var n = node;\n while (n) {\n out.push(n);\n try {\n n = n.parentNode || null;\n } catch (_) {\n n = null;\n }\n }\n return out;\n }\n\n function ptrCommonAncestor(a, b) {\n if (!a || !b) return null;\n var ca = ptrChain(a);\n var seen = [];\n for (var i = 0; i < ca.length; i++) seen.push(ca[i]);\n var cb = ptrChain(b);\n for (var j = 0; j < cb.length; j++) {\n for (var k = 0; k < seen.length; k++) {\n if (seen[k] === cb[j]) return cb[j];\n }\n }\n return null;\n }\n\n /**\n * 造一个合成鼠标/指针事件。\n *\n * `PointerEvent` 优先:createjs 一族(语料 7 张)只挂 pointerdown/move/up,\n * 且会读 `pointerId` / `pointerType` / `isPrimary`。环境没有 PointerEvent 时\n * 退回 MouseEvent(事件名照旧,作者的 addEventListener(\'pointermove\') 仍能收到)。\n */\n function ptrMakeEvent(type, x, y, opts) {\n var o = opts || {};\n var isPointer = type.indexOf("pointer") === 0;\n var init = {\n bubbles: o.bubbles !== false,\n cancelable: o.cancelable !== false,\n // composed:作者把 canvas 放进 shadow DOM 时事件要能穿出来\n composed: true,\n view: w,\n detail: o.detail || 0,\n clientX: x,\n clientY: y,\n // screenX/screenY 是 init 字段(不像 pageX/offsetX 那样现算)。iframe 里\n // 只能按外层窗口原点近似;16 张读 screenX,多用于算相对位移而非绝对定位。\n screenX: x + (Number(w.screenX) || 0),\n screenY: y + (Number(w.screenY) || 0),\n // button:**移动/悬停类事件必须是 -1**,只有 down/up/click 才是 0(左)/1(中)/2(右)。\n // 这条是 W3C 规定的「没有按键状态变化」哨兵值,不是可省的细节:GameMaker HTML5\n // 导出的运行时(2517518192 FNAF)在 pointermove 分支里照抄 `_tq = e.button` 再\n // `_mq |= (1 << _tq)`,而 _mq 只在 pointerup/out 才清零 —— 填 0 等于告诉游戏\n // 「左键一直按着」,鼠标只是移过去就永久卡在按下态(且没有任何报错)。\n button: o.button != null ? o.button : -1,\n buttons: o.buttons != null ? o.buttons : ptrButtons,\n movementX: o.movementX || 0,\n movementY: o.movementY || 0,\n ctrlKey: false,\n shiftKey: false,\n altKey: false,\n metaKey: false,\n };\n if ("relatedTarget" in o) init.relatedTarget = o.relatedTarget || null;\n // `button: -1` 无法经 MouseEvent 构造器表达:Chromium 把 -1 规范化成 0\n // (实测 `new MouseEvent("x", {button:-1}).button === 0`,而 -2 能原样通过 ——\n // 不是钳位,是对 -1 的特殊处理)。PointerEvent 构造器则保留 -1。\n // 所以 mouse 类事件必须在构造后把 -1 盖回去,否则「移动=左键按下」的坑\n // 只在 pointer 路径修好、mouse 路径依旧(2517518192 恰好走 pointer,\n // 光看它会误以为已经修完)。\n var needsButtonPatch = init.button < 0;\n var ev = null;\n if (isPointer) {\n init.pointerId = 1;\n init.pointerType = "mouse";\n init.isPrimary = true;\n init.width = 1;\n init.height = 1;\n init.pressure = init.buttons ? 0.5 : 0;\n try {\n if (typeof w.PointerEvent === "function") ev = new w.PointerEvent(type, init);\n } catch (_) {\n /* 退回 MouseEvent */\n }\n }\n if (!ev) {\n try {\n if (typeof w.MouseEvent === "function") ev = new w.MouseEvent(type, init);\n } catch (_) {\n /* 忽略 */\n }\n }\n if (ev && needsButtonPatch && ev.button !== init.button) {\n try {\n Object.defineProperty(ev, "button", { configurable: true, get: function () {\n return init.button;\n } });\n } catch (_) {\n /* 只读且不可重定义时保持构造值 */\n }\n }\n return ev;\n }\n\n function ptrDispatch(node, type, x, y, opts) {\n if (!node || typeof node.dispatchEvent !== "function") return;\n var ev = ptrMakeEvent(type, x, y, opts);\n if (!ev) return;\n try {\n node.dispatchEvent(ev);\n } catch (_) {\n /* 作者处理器抛错不打断后续事件(与官方 CEF 一致:一个坏 listener 不该\n 让整条链断掉,否则 leave 发不出去会留下永久 hover/按下态) */\n }\n }\n\n /** 命中元素变化时补 out/leave + over/enter 四段,顺序与浏览器一致。 */\n function ptrCrossBoundary(prev, next, x, y) {\n if (prev === next) return;\n var ancestor = ptrCommonAncestor(prev, next);\n if (prev) {\n ptrDispatch(prev, "pointerout", x, y, { relatedTarget: next });\n ptrDispatch(prev, "mouseout", x, y, { relatedTarget: next });\n var leaving = ptrChain(prev);\n for (var i = 0; i < leaving.length; i++) {\n if (leaving[i] === ancestor) break;\n // leave 不冒泡:必须逐个发,且 target 就是它自己\n ptrDispatch(leaving[i], "pointerleave", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: next,\n });\n ptrDispatch(leaving[i], "mouseleave", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: next,\n });\n }\n }\n if (next) {\n ptrDispatch(next, "pointerover", x, y, { relatedTarget: prev });\n ptrDispatch(next, "mouseover", x, y, { relatedTarget: prev });\n var entering = [];\n var chain = ptrChain(next);\n for (var j = 0; j < chain.length; j++) {\n if (chain[j] === ancestor) break;\n entering.push(chain[j]);\n }\n // enter 由外向内(祖先先收到),与浏览器一致\n for (var k = entering.length - 1; k >= 0; k--) {\n ptrDispatch(entering[k], "pointerenter", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: prev,\n });\n ptrDispatch(entering[k], "mouseenter", x, y, {\n bubbles: false,\n cancelable: false,\n relatedTarget: prev,\n });\n }\n }\n }\n\n /**\n * 外部指针注入入口。\n *\n * @param {number} x 相对 iframe 视口左边的 **CSS 像素**(= clientX 空间)\n * @param {number} y 同上,相对上边,Y 朝下\n * @param {number} [buttons] 按键位掩码,bit0 左键。与场景通道同一约定,\n * 当前只消费 bit0(右/中键位保留;桌面右键属于 Finder,不该被壁纸劫持)\n *\n * 接**像素**而不是归一化坐标:网页壁纸的 iframe 在 cover 露底自适配下可能比舞台大\n * 并带居中偏移(见 web.ts installLetterboxFix),换算需要 iframe 的几何 —— 那是父页\n * 才知道的信息,父页换算完再推进来,shim 不做二次除法。\n *\n * 暂停期间丢弃:官方暂停语义是「冻结渲染进程」,此时派发事件会让作者的动画状态\n * 在冻结中继续推进,恢复时画面跳一下。\n */\n w.__wePushPointer = function (x, y, buttons) {\n if (paused) return;\n var nx = Number(x);\n var ny = Number(y);\n // 非有限值直接丢弃(与场景通道同一约定):NaN 传进 clientX 会让 elementFromPoint\n // 返回 null、后续 offsetX 全成 NaN,作者的位移积分会一次性污染成 NaN 且不报错。\n if (!isFinite(nx) || !isFinite(ny)) return;\n var mask = Number(buttons) || 0;\n var moved = !ptrHas || nx !== ptrX || ny !== ptrY;\n var maskChanged = mask !== ptrButtons;\n // 位置与按键都没变就什么都不发:宿主按 ~90Hz 推送,静止时重复派发\n // mousemove 会让作者的「有没有在动」判定(1081733658 网格)永远认为在动。\n if (!moved && !maskChanged) return;\n\n var dx = ptrHas ? nx - ptrX : 0;\n var dy = ptrHas ? ny - ptrY : 0;\n ptrX = nx;\n ptrY = ny;\n ptrHas = true;\n\n var target = ptrHitTest(nx, ny);\n if (moved) {\n ptrCrossBoundary(ptrTarget, target, nx, ny);\n ptrTarget = target;\n ptrDispatch(target, "pointermove", nx, ny, { movementX: dx, movementY: dy });\n ptrDispatch(target, "mousemove", nx, ny, { movementX: dx, movementY: dy });\n } else {\n ptrTarget = target;\n }\n\n if (!maskChanged) return;\n var wasDown = (ptrButtons & 1) !== 0;\n var isDown = (mask & 1) !== 0;\n ptrButtons = mask;\n if (isDown === wasDown) return; // 只有高位变化:当前不消费\n if (isDown) {\n ptrDownTarget = target;\n ptrDispatch(target, "pointerdown", nx, ny, { button: 0, detail: 1 });\n ptrDispatch(target, "mousedown", nx, ny, { button: 0, detail: 1 });\n return;\n }\n ptrDispatch(target, "pointerup", nx, ny, { button: 0, detail: 1 });\n ptrDispatch(target, "mouseup", nx, ny, { button: 0, detail: 1 });\n // click 只在 down/up 落在同一元素上时发(否则是拖拽,浏览器也不发)\n if (ptrDownTarget && ptrDownTarget === target) {\n var now = Date.now();\n var isDouble =\n ptrLastClickTarget === target && now - ptrLastClickTime <= PTR_DBLCLICK_MS;\n ptrDispatch(target, "click", nx, ny, { button: 0, detail: isDouble ? 2 : 1 });\n if (isDouble) {\n ptrDispatch(target, "dblclick", nx, ny, { button: 0, detail: 2 });\n ptrLastClickTarget = null;\n ptrLastClickTime = 0;\n } else {\n ptrLastClickTarget = target;\n ptrLastClickTime = now;\n }\n }\n ptrDownTarget = null;\n };\n\n /**\n * 指针离开本窗口(鼠标去了别的显示器)。\n *\n * 与场景通道不同,这里**必须把 out/leave 链发出去**:场景侧只是清一个状态位,\n * 而网页作者的 hover 态是自己记的,不发 leave 就永久卡在「鼠标还在上面」\n * (1081733658 网格会一直跑、1748506393 的 `pointers[0].down` 一直为 true)。\n * 按下态也要补一次 up,否则拖拽逻辑永远不结束。\n */\n w.__wePointerLeave = function () {\n if ((ptrButtons & 1) !== 0 && ptrTarget) {\n ptrDispatch(ptrTarget, "pointerup", ptrX, ptrY, { button: 0, buttons: 0, detail: 1 });\n ptrDispatch(ptrTarget, "mouseup", ptrX, ptrY, { button: 0, buttons: 0, detail: 1 });\n }\n ptrButtons = 0;\n ptrDownTarget = null;\n if (ptrTarget) {\n ptrCrossBoundary(ptrTarget, null, ptrX, ptrY);\n ptrTarget = null;\n }\n // 位置(ptrX/ptrY)与 ptrHas 保留:下次进来时 movement 才是真实位移,\n // 而不是从 (0,0) 跳过来的一个巨大假 delta。\n };\n\n // —— rAF 节流(带 __weThrottled,避免父页 injectGpuThrottle 双层减半)——\n\n function installRafThrottle() {\n var throttled = function (cb) {\n if (typeof cb !== "function") return 0;\n if (paused) {\n var idHold = ++rafCounter;\n rafMap[idHold] = { kind: "hold", cb: cb };\n return idHold;\n }\n var limit = fps >= 60 ? 0 : 1000 / fps;\n if (limit <= 0) {\n var idNative = origRaf(function (now) {\n delete rafMap[idNative];\n try {\n cb(now);\n } catch (_) {\n /* 忽略 */\n }\n try {\n w.parent.postMessage({ op: "we-frame", t: now }, "*");\n } catch (_) {\n /* 忽略 */\n }\n });\n rafMap[idNative] = { kind: "native", id: idNative };\n return idNative;\n }\n var id = ++rafCounter;\n var to = w.setTimeout(function () {\n delete rafMap[id];\n origRaf(function (now) {\n try {\n cb(now);\n } catch (_) {\n /* 忽略 */\n }\n try {\n w.parent.postMessage({ op: "we-frame", t: now }, "*");\n } catch (_) {\n /* 忽略 */\n }\n });\n }, limit);\n rafMap[id] = { kind: "timeout", to: to };\n return id;\n };\n throttled.__weThrottled = true;\n w.requestAnimationFrame = throttled;\n w.cancelAnimationFrame = function (id) {\n var ent = rafMap[id];\n if (!ent) {\n try {\n origCaf(id);\n } catch (_) {\n /* 忽略 */\n }\n return;\n }\n delete rafMap[id];\n if (ent.kind === "timeout") w.clearTimeout(ent.to);\n else if (ent.kind === "native") origCaf(ent.id);\n };\n }\n\n installRafThrottle();\n})(window);\n\n';
15068
+ function weShimCall(rt, call) {
15069
+ try {
15070
+ const win = rt.iframe?.contentWindow;
15071
+ if (win) call(win);
15072
+ } catch {
15073
+ }
15074
+ }
15075
+ function injectGpuThrottle(rt, f, _doc) {
15076
+ const win = f.contentWindow;
15077
+ if (!win) return;
15078
+ if (win.requestAnimationFrame?.__weThrottled) return;
15079
+ const fps = rt.cfg.sceneFps || 30;
15080
+ if (fps >= 60) return;
15081
+ const interval = 1e3 / fps;
15082
+ try {
15083
+ const origRaf = win.requestAnimationFrame.bind(win);
15084
+ const rafMap = /* @__PURE__ */ new Map();
15085
+ let counter = 0;
15086
+ win.requestAnimationFrame = (cb) => {
15087
+ const id = ++counter;
15088
+ const to = win.setTimeout(() => {
15089
+ rafMap.delete(id);
15090
+ origRaf((now) => {
15091
+ try {
15092
+ cb(now);
15093
+ } catch {
15094
+ }
15095
+ });
15096
+ }, interval);
15097
+ rafMap.set(id, to);
15098
+ return id;
15099
+ };
15100
+ win.cancelAnimationFrame = (id) => {
15101
+ const to = rafMap.get(id);
15102
+ if (to !== void 0) {
15103
+ win.clearTimeout(to);
15104
+ rafMap.delete(id);
15105
+ }
15106
+ };
15107
+ } catch {
15108
+ }
15109
+ }
15110
+ const pumpBuffer = new Float32Array(128);
15111
+ function packWebAudioArrayInto(out, left, right) {
15112
+ const nL = Math.min(64, left.length);
15113
+ const nR = Math.min(64, right.length);
15114
+ for (let i = 0; i < nL; i++) out[i] = Number(left[i]) || 0;
15115
+ for (let i = 0; i < nR; i++) out[64 + i] = Number(right[i]) || 0;
15116
+ return out;
15117
+ }
15118
+ const WEB_SIM_AUDIO_GAIN = 1.8;
15119
+ const WEB_SIM_AUDIO_GAMMA = 1.8;
15120
+ const WEB_AUDIO_PUMP_HZ = 30;
15121
+ function shapeWebAudioBand(pre) {
15122
+ const v = Number(pre) || 0;
15123
+ if (v <= 0) return 0;
15124
+ return Math.min(1, Math.pow(v, WEB_SIM_AUDIO_GAMMA) * WEB_SIM_AUDIO_GAIN);
15125
+ }
15126
+ function defaultAudioDriver() {
15127
+ const sim = createSimulatedAudio();
15128
+ const left = new Float32Array(64);
15129
+ const right = new Float32Array(64);
15130
+ return {
15131
+ tick(nowMs) {
15132
+ sim.update(nowMs / 1e3);
15133
+ },
15134
+ snapshot() {
15135
+ const s = sim.snapshot;
15136
+ const preL = s.preL64;
15137
+ const preR = s.preR64;
15138
+ for (let i = 0; i < 64; i++) {
15139
+ if (preL && preR) {
15140
+ left[i] = shapeWebAudioBand(preL[i]);
15141
+ right[i] = shapeWebAudioBand(preR[i]);
15142
+ } else {
15143
+ left[i] = (Number(s.left64[i]) || 0) * 0.2;
15144
+ right[i] = (Number(s.right64[i]) || 0) * 0.2;
15145
+ }
15146
+ }
15147
+ return { left, right };
15148
+ }
15149
+ };
15150
+ }
15151
+ function resolveContainer(rt, cfg) {
15152
+ if (rt.wrap) return rt.wrap;
15153
+ const el = cfg.canvas;
15154
+ if (!el) return null;
15155
+ if (el instanceof HTMLCanvasElement) {
15156
+ const parent = el.parentElement;
15157
+ if (parent) {
15158
+ reportDiag(rt, cfg, "网页壁纸挂在 canvas 父容器上(canvas 不能有子节点;更适合空 div)");
15159
+ return parent;
15160
+ }
15161
+ return null;
15162
+ }
15163
+ return el;
15164
+ }
15165
+ function buildSeedScript(props, fps, volume) {
15166
+ const parts = [];
15167
+ if (fps != null && Number.isFinite(fps)) parts.push(`window.__weSetFps(${Number(fps)});`);
15168
+ if (volume != null && Number.isFinite(volume)) {
15169
+ parts.push(`window.__weSetVolume(${Math.max(0, Math.min(1, Number(volume)))});`);
15170
+ }
15171
+ if (props && Object.keys(props).length) {
15172
+ parts.push(`window.__weSeedProps(${JSON.stringify(props)});`);
15173
+ }
15174
+ return parts.join("\n");
15175
+ }
15176
+ function installLetterboxFix(rt, f, container) {
15177
+ const BASE = "position:absolute;border:none;background:transparent;";
15178
+ const applyFull = () => {
15179
+ f.style.cssText = BASE + "inset:0;width:100%;height:100%;";
15180
+ };
15181
+ applyFull();
15182
+ let lastKey = "";
15183
+ const relayout = () => {
15184
+ if (!f.isConnected) return;
15185
+ let doc = null;
15186
+ try {
15187
+ doc = f.contentDocument;
15188
+ } catch {
15189
+ return;
15190
+ }
15191
+ if (!doc) return;
15192
+ const stageW = container.clientWidth || window.innerWidth || 0;
15193
+ const stageH = container.clientHeight || window.innerHeight || 0;
15194
+ const cover = normalizeFit(rt.cfg.fit) === "cover";
15195
+ applyFull();
15196
+ const box = cover && stageW > 0 && stageH > 0 ? measureWebLetterbox(doc) : null;
15197
+ const vp = box ? webCoverViewport(stageW, stageH, box.contentAspect) : null;
15198
+ const key = vp ? `${Math.round(vp.width)}x${Math.round(vp.height)}` : "full";
15199
+ if (!vp) {
15200
+ lastKey = "full";
15201
+ return;
15202
+ }
15203
+ f.style.cssText = BASE + `left:${vp.left}px;top:${vp.top}px;width:${vp.width}px;height:${vp.height}px;`;
15204
+ if (key !== lastKey) {
15205
+ lastKey = key;
15206
+ reportDiag(
15207
+ rt,
15208
+ rt.cfg,
15209
+ `网页壁纸露底自适配:视口按内容比例改为 ${Math.round(vp.width)}×${Math.round(vp.height)}(cover 居中裁切)`
15210
+ );
15211
+ }
15212
+ };
15213
+ const onResize = () => relayout();
15214
+ window.addEventListener("resize", onResize);
15215
+ let ro;
15216
+ if (typeof ResizeObserver !== "undefined") {
15217
+ ro = new ResizeObserver(() => relayout());
15218
+ ro.observe(container);
15219
+ }
15220
+ const timers = [];
15221
+ const onLoad = () => {
15222
+ relayout();
15223
+ for (const d of [120, 400, 1200]) timers.push(window.setTimeout(relayout, d));
15224
+ try {
15225
+ const doc = f.contentDocument;
15226
+ if (doc) {
15227
+ for (const el of doc.querySelectorAll("video,img")) {
15228
+ el.addEventListener("loadedmetadata", relayout, { once: true });
15229
+ el.addEventListener("load", relayout, { once: true });
15230
+ }
15231
+ }
15232
+ } catch {
15233
+ }
15234
+ };
15235
+ f.addEventListener("load", onLoad);
15236
+ rt.webRelayout = relayout;
15237
+ const prev = rt.sceneCleanup;
15238
+ rt.sceneCleanup = () => {
15239
+ window.removeEventListener("resize", onResize);
15240
+ ro?.disconnect();
15241
+ for (const t of timers) clearTimeout(t);
15242
+ f.removeEventListener("load", onLoad);
15243
+ if (rt.webRelayout === relayout) rt.webRelayout = void 0;
15244
+ try {
15245
+ prev?.();
15246
+ } catch {
15247
+ }
15248
+ };
15249
+ }
15250
+ function webPointerToClient(u, v, stage, frame, client) {
15251
+ if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
15252
+ if (!(stage.width > 0) || !(stage.height > 0)) return null;
15253
+ const sx = frame.width > 0 && client.width > 0 ? frame.width / client.width : 1;
15254
+ const sy = frame.height > 0 && client.height > 0 ? frame.height / client.height : 1;
15255
+ return {
15256
+ x: (u * stage.width - (frame.left - stage.left)) / (sx || 1),
15257
+ y: (v * stage.height - (frame.top - stage.top)) / (sy || 1)
15258
+ };
15259
+ }
15260
+ function installWebPointerBridge(rt, f, container) {
15261
+ rt.pointerCtl = {
15262
+ push(p) {
15263
+ if (!f.isConnected) return;
15264
+ const cRect = container.getBoundingClientRect();
15265
+ const fRect = f.getBoundingClientRect();
15266
+ const pt = webPointerToClient(
15267
+ Number(p?.u),
15268
+ Number(p?.v),
15269
+ {
15270
+ left: cRect.left,
15271
+ top: cRect.top,
15272
+ width: cRect.width || container.clientWidth || window.innerWidth || 0,
15273
+ height: cRect.height || container.clientHeight || window.innerHeight || 0
15274
+ },
15275
+ { left: fRect.left, top: fRect.top, width: fRect.width, height: fRect.height },
15276
+ { width: f.clientWidth, height: f.clientHeight }
15277
+ );
15278
+ if (!pt) return;
15279
+ weShimCall(rt, (w) => w.__wePushPointer?.(pt.x, pt.y, Number(p.buttons) || 0));
15280
+ },
15281
+ leave() {
15282
+ weShimCall(rt, (w) => w.__wePointerLeave?.());
15283
+ }
15284
+ };
15285
+ }
15286
+ function attachIframe(rt, cfg, container, src, opts) {
15287
+ const f = document.createElement("iframe");
15288
+ f.setAttribute("sandbox", "allow-scripts allow-same-origin");
15289
+ f.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:none;background:transparent;";
15290
+ if (!rt.wrap && getComputedStyle(container).position === "static") {
15291
+ container.style.position = "relative";
15292
+ }
15293
+ f.src = src;
15294
+ container.appendChild(f);
15295
+ rt.iframe = f;
15296
+ if (opts.blobUrl) {
15297
+ (rt.objectUrls ??= []).push(opts.blobUrl);
15298
+ }
15299
+ installLetterboxFix(rt, f, container);
15300
+ if (opts.injected) installWebPointerBridge(rt, f, container);
15301
+ const onFrameMsg = (ev) => {
15302
+ if (ev.source !== f.contentWindow) return;
15303
+ const data = ev.data;
15304
+ if (!data || data.op !== "we-frame") return;
15305
+ if (rt.paused) return;
15306
+ const t = typeof data.t === "number" ? data.t : performance.now();
15307
+ if (opts.frameClock) opts.frameClock.last = t;
15308
+ markFrame(rt, t);
15309
+ };
15310
+ window.addEventListener("message", onFrameMsg);
15311
+ const prevCleanup = rt.sceneCleanup;
15312
+ rt.sceneCleanup = () => {
15313
+ window.removeEventListener("message", onFrameMsg);
15314
+ try {
15315
+ prevCleanup?.();
15316
+ } catch {
15317
+ }
15318
+ };
15319
+ f.addEventListener("load", () => {
15320
+ try {
15321
+ const doc = f.contentDocument;
15322
+ if (doc) window.__blockContextMenu?.(doc);
15323
+ if (!opts.injected) injectGpuThrottle(rt, f, doc);
15324
+ } catch {
15325
+ }
15326
+ weShimCall(rt, (w2) => {
15327
+ const wire = {};
15328
+ for (const [k, v] of Object.entries(rt.liveUserProps ?? {})) wire[k] = { value: v };
15329
+ w2.__weApplyProps?.(wire);
15330
+ w2.__weSetFps?.(rt.cfg.sceneFps ?? 60);
15331
+ w2.__weSetVolume?.(rt.cfg.muted === false ? 1 : 0);
15332
+ if (rt.paused) w2.__weSetPaused?.(true);
15333
+ });
15334
+ try {
15335
+ rt.onFirstFrame?.();
15336
+ rt.onFirstFrame = void 0;
15337
+ } catch {
15338
+ }
15339
+ const w = container.clientWidth || window.innerWidth || 1;
15340
+ const h = container.clientHeight || window.innerHeight || 1;
15341
+ try {
15342
+ rt.onSceneInfo?.({
15343
+ width: w,
15344
+ height: h,
15345
+ layerCount: 0,
15346
+ hasModels: false,
15347
+ hasParticles: false,
15348
+ hasText: false
15349
+ });
15350
+ } catch {
15351
+ }
15352
+ });
15353
+ }
15354
+ const WEB_ASPECT_EPS = 5e-3;
15355
+ const WEB_LETTERBOX_MIN_RATIO = 0.01;
15356
+ const WEB_ASPECT_MIN = 0.2;
15357
+ const WEB_ASPECT_MAX = 6;
15358
+ function webCoverViewport(stageW, stageH, contentAspect) {
15359
+ if (!(stageW > 0) || !(stageH > 0) || !(contentAspect > 0)) return null;
15360
+ const stageAspect = stageW / stageH;
15361
+ if (Math.abs(stageAspect - contentAspect) <= WEB_ASPECT_EPS) return null;
15362
+ if (stageAspect < contentAspect) {
15363
+ const width = stageH * contentAspect;
15364
+ return { width, height: stageH, left: (stageW - width) / 2, top: 0 };
15365
+ }
15366
+ const height = stageW / contentAspect;
15367
+ return { width: stageW, height, left: 0, top: (stageH - height) / 2 };
15368
+ }
15369
+ function measureWebLetterbox(doc) {
15370
+ const win = doc.defaultView;
15371
+ if (!win) return null;
15372
+ const vw = win.innerWidth;
15373
+ const vh = win.innerHeight;
15374
+ if (!(vw > 0) || !(vh > 0)) return null;
15375
+ const cands = [...doc.querySelectorAll("video,img")];
15376
+ for (const el of cands) {
15377
+ const r = el.getBoundingClientRect();
15378
+ if (r.width <= 0 || r.height <= 0) continue;
15379
+ if (r.width < vw * 0.98) continue;
15380
+ if (Math.abs(r.left) > vw * 0.02 || r.top > vh * 0.02) continue;
15381
+ if (vh - r.height < vh * WEB_LETTERBOX_MIN_RATIO) continue;
15382
+ const natW = el.videoWidth || el.naturalWidth || 0;
15383
+ const natH = el.videoHeight || el.naturalHeight || 0;
15384
+ if (!(natW > 0) || !(natH > 0)) continue;
15385
+ const aspect = natW / natH;
15386
+ if (!Number.isFinite(aspect) || aspect < WEB_ASPECT_MIN || aspect > WEB_ASPECT_MAX) continue;
15387
+ return { contentAspect: aspect };
15388
+ }
15389
+ return null;
15390
+ }
15391
+ function vecToCss(v) {
15392
+ if (!v) return "rgb(128,128,128)";
15393
+ const r = Math.round(Math.max(0, Math.min(1, Number(v.x) || 0)) * 255);
15394
+ const g = Math.round(Math.max(0, Math.min(1, Number(v.y) || 0)) * 255);
15395
+ const b = Math.round(Math.max(0, Math.min(1, Number(v.z) || 0)) * 255);
15396
+ return `rgb(${r},${g},${b})`;
15397
+ }
15398
+ function thumbDataUrlFromSnap(snap) {
15399
+ try {
15400
+ const c = document.createElement("canvas");
15401
+ c.width = c.height = 64;
15402
+ const ctx = c.getContext("2d");
15403
+ if (!ctx) return "";
15404
+ const p = snap.primaryColor;
15405
+ const s = snap.secondaryColor;
15406
+ const grd = ctx.createLinearGradient(0, 0, 64, 64);
15407
+ grd.addColorStop(0, vecToCss(p));
15408
+ grd.addColorStop(1, vecToCss(s));
15409
+ ctx.fillStyle = grd;
15410
+ ctx.fillRect(0, 0, 64, 64);
15411
+ return c.toDataURL("image/jpeg", 0.85);
15412
+ } catch {
15413
+ return "";
15414
+ }
15415
+ }
15416
+ function defaultMediaDriver() {
15417
+ return media.createSimulatedMedia();
15418
+ }
15419
+ function pushMediaDiff(rt, prev, snap) {
15420
+ const events = media.diffMediaEvents(prev, snap);
15421
+ for (const { name, event } of events) {
15422
+ if (name === "mediaStatusChanged") {
15423
+ weShimCall(rt, (w) => w.__wePushMedia?.({ op: "status", enabled: !!event.enabled }));
15424
+ } else if (name === "mediaPropertiesChanged") {
15425
+ weShimCall(
15426
+ rt,
15427
+ (w) => w.__wePushMedia?.({
15428
+ op: "properties",
15429
+ title: event.title ?? "",
15430
+ artist: event.artist ?? "",
15431
+ album: event.album ?? "",
15432
+ albumArtist: event.albumArtist ?? ""
15433
+ })
15434
+ );
15435
+ } else if (name === "mediaThumbnailChanged") {
15436
+ const thumb = thumbDataUrlFromSnap(snap);
15437
+ weShimCall(
15438
+ rt,
15439
+ (w) => w.__wePushMedia?.({
15440
+ op: "thumbnail",
15441
+ thumbnail: thumb,
15442
+ hasThumbnail: !!event.hasThumbnail || !!thumb,
15443
+ primaryColor: vecToCss(event.primaryColor),
15444
+ secondaryColor: vecToCss(event.secondaryColor),
15445
+ tertiaryColor: vecToCss(event.tertiaryColor),
15446
+ textColor: vecToCss(event.textColor),
15447
+ highContrastColor: vecToCss(event.highContrastColor)
15448
+ })
15449
+ );
15450
+ } else if (name === "mediaPlaybackChanged") {
15451
+ weShimCall(rt, (w) => w.__wePushMedia?.({ op: "playback", state: Number(event.state) || 0 }));
15452
+ } else if (name === "mediaTimelineChanged") {
15453
+ weShimCall(
15454
+ rt,
15455
+ (w) => w.__wePushMedia?.({
15456
+ op: "timeline",
15457
+ position: Number(event.position) || 0,
15458
+ duration: Number(event.duration) || 0
15459
+ })
15460
+ );
15461
+ }
15462
+ }
15463
+ return media.cloneMediaSnapshot(snap);
15464
+ }
15465
+ function startAudioPump(rt, driver, frameClock) {
15466
+ if (!driver) return;
15467
+ let raf = 0;
15468
+ let lastPush = 0;
15469
+ const tick = (now) => {
15470
+ raf = requestAnimationFrame(tick);
15471
+ if (rt.paused || !rt.iframe) return;
15472
+ const fps = rt.cfg.sceneFps || 60;
15473
+ const pumpFps = Math.min(Math.max(1, fps), WEB_AUDIO_PUMP_HZ);
15474
+ const interval = 1e3 / pumpFps;
15475
+ if (now - lastPush < interval * 0.85) return;
15476
+ lastPush = now;
15477
+ try {
15478
+ driver.tick?.(now);
15479
+ const snap = driver.snapshot();
15480
+ const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
15481
+ weShimCall(rt, (w) => w.__wePushAudio?.(arr));
15482
+ if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
15483
+ } catch {
15484
+ }
15485
+ };
15486
+ raf = requestAnimationFrame(tick);
15487
+ const prev = rt.sceneCleanup;
15488
+ rt.sceneCleanup = () => {
15489
+ cancelAnimationFrame(raf);
15490
+ try {
15491
+ prev?.();
15492
+ } catch {
15493
+ }
15494
+ };
15495
+ }
15496
+ function startMediaPump(rt, driver) {
15497
+ if (!driver) return;
15498
+ let raf = 0;
15499
+ let lastMedia = null;
15500
+ let lastTick = 0;
15501
+ const tick = (now) => {
15502
+ raf = requestAnimationFrame(tick);
15503
+ if (rt.paused || !rt.iframe) return;
15504
+ if (now - lastTick < 200) return;
15505
+ lastTick = now;
15506
+ try {
15507
+ driver.update(now / 1e3);
15508
+ lastMedia = pushMediaDiff(rt, lastMedia, driver.snapshot);
15509
+ } catch {
15510
+ }
15511
+ };
15512
+ raf = requestAnimationFrame(tick);
15513
+ const prev = rt.sceneCleanup;
15514
+ rt.sceneCleanup = () => {
15515
+ cancelAnimationFrame(raf);
15516
+ try {
15517
+ prev?.();
15518
+ } catch {
15519
+ }
15520
+ };
15521
+ }
15522
+ function installWebCtl(rt) {
15523
+ rt.sceneCtl = {
15524
+ pause() {
15525
+ rt.paused = true;
15526
+ weShimCall(rt, (w) => w.__weSetPaused?.(true));
15527
+ },
15528
+ resume() {
15529
+ rt.paused = false;
15530
+ weShimCall(rt, (w) => w.__weSetPaused?.(false));
15531
+ },
15532
+ applyUserProperties(props) {
15533
+ const flat = { ...rt.liveUserProps ?? {} };
15534
+ for (const [k, v] of Object.entries(props ?? {})) {
15535
+ const val = v && typeof v === "object" && "value" in v ? v.value : v;
15536
+ flat[k] = val;
15537
+ }
15538
+ rt.liveUserProps = flat;
15539
+ weShimCall(rt, (w) => w.__weApplyProps?.(props));
15540
+ }
15541
+ };
15542
+ }
15543
+ function isSameOriginUrl(url) {
15544
+ try {
15545
+ return new URL(url, location.href).origin === location.origin;
15546
+ } catch {
15547
+ return false;
15548
+ }
15549
+ }
15550
+ function projectPropertiesToWire(project) {
15551
+ const props = project?.general?.properties;
15552
+ if (!props || typeof props !== "object") return {};
15553
+ const out = {};
15554
+ for (const [name, def] of Object.entries(props)) {
15555
+ if (!def || typeof def !== "object" || typeof def.type !== "string") continue;
15556
+ const raw = def.value;
15557
+ const type = def.type.toLowerCase();
15558
+ if (raw === null || raw === void 0) {
15559
+ if (type === "file" || type === "directory") {
15560
+ out[name] = { value: "" };
15561
+ continue;
15562
+ }
15563
+ if (!("value" in def)) continue;
15564
+ }
15565
+ out[name] = { value: raw };
15566
+ }
15567
+ return out;
15568
+ }
15569
+ async function fetchProjectWire(entryUrl) {
15570
+ try {
15571
+ const projUrl = new URL("project.json", new URL(entryUrl, location.href));
15572
+ const r = await fetch(projUrl.href, { credentials: "same-origin" });
15573
+ if (!r.ok) return {};
15574
+ return projectPropertiesToWire(await r.json());
15575
+ } catch {
15576
+ return {};
15577
+ }
15578
+ }
15579
+ function mergeLiveIntoWire(defaults, live) {
15580
+ const out = { ...defaults };
15581
+ if (live) {
15582
+ for (const [k, v] of Object.entries(live)) out[k] = { value: v };
15583
+ }
15584
+ return out;
15585
+ }
15586
+ function mountWeb(rt, cfg) {
15587
+ clear(rt);
15588
+ rt.cfg = cfg;
15589
+ const container = resolveContainer(rt, cfg);
15590
+ if (!container) {
15591
+ reportDiag(rt, cfg, "网页壁纸:无可用容器");
15592
+ rt.onError?.(new Error("网页壁纸:无可用容器"));
15593
+ return;
15594
+ }
15595
+ const entry = cfg.src ?? "";
15596
+ if (!entry) {
15597
+ reportDiag(rt, cfg, "网页壁纸:缺少 src");
15598
+ rt.onError?.(new Error("网页壁纸:缺少 src"));
15599
+ return;
15600
+ }
15601
+ installWebCtl(rt);
15602
+ const cfgExt = cfg;
15603
+ const audioDriver = cfgExt._webAudio === null ? null : cfgExt._webAudio ?? defaultAudioDriver();
15604
+ const mediaDriver = cfgExt._webMedia === null ? null : cfgExt._webMedia ?? defaultMediaDriver();
15605
+ const finishBare = (why) => {
15606
+ reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
15607
+ attachIframe(rt, cfg, container, entry, { injected: false });
15608
+ startAudioPump(rt, null);
15609
+ startMediaPump(rt, null);
15610
+ };
15611
+ const frameClock = { last: 0 };
15612
+ const startPumps = () => {
15613
+ startAudioPump(rt, audioDriver, frameClock);
15614
+ startMediaPump(rt, mediaDriver);
15615
+ };
15616
+ void (async () => {
15617
+ const defaults = await fetchProjectWire(entry);
15618
+ const wire = mergeLiveIntoWire(defaults, rt.liveUserProps);
15619
+ rt.liveUserProps = Object.fromEntries(Object.entries(wire).map(([k, w]) => [k, w.value]));
15620
+ if (isSameOriginUrl(entry)) {
15621
+ attachIframe(rt, cfg, container, entry, { injected: true, frameClock });
15622
+ startPumps();
15623
+ const f = rt.iframe;
15624
+ f?.addEventListener(
15625
+ "load",
15626
+ () => {
15627
+ let hasShim = false;
15628
+ weShimCall(rt, (w) => {
15629
+ hasShim = typeof w.__weSetPaused === "function";
15630
+ });
15631
+ if (!hasShim) {
15632
+ reportDiag(
15633
+ rt,
15634
+ cfg,
15635
+ "网页壁纸:同源入口未检测到 WE shim(host 未注入?);Spine 类壁纸请确认 /web/ HTML 改写"
15636
+ );
15637
+ }
15638
+ },
15639
+ { once: true }
15640
+ );
15641
+ return;
15642
+ }
15643
+ try {
15644
+ const res = await fetch(entry, { credentials: "same-origin" });
15645
+ if (!res.ok) {
15646
+ finishBare(`HTTP ${res.status}`);
15647
+ return;
15648
+ }
15649
+ const html = await res.text();
15650
+ if (hasBlockingCsp(html)) {
15651
+ finishBare("CSP 阻止 inline script");
15652
+ return;
15653
+ }
15654
+ const rewritten = rewriteHtml(html, shimSource, {
15655
+ baseHref: entryDirUrl(entry),
15656
+ seedScript: buildSeedScript(wire, cfg.sceneFps, cfg.muted === false ? 1 : 0)
15657
+ });
15658
+ const blob = new Blob([rewritten], { type: "text/html;charset=utf-8" });
15659
+ const blobUrl = URL.createObjectURL(blob);
15660
+ attachIframe(rt, cfg, container, blobUrl, { blobUrl, injected: true, frameClock });
15661
+ startPumps();
15662
+ } catch (e) {
15663
+ finishBare(e instanceof Error ? e.message : String(e));
15664
+ }
15665
+ })();
15666
+ }
13498
15667
  function mountWallpaper(rt, cfg) {
13499
- if ((cfg.type === "video" || cfg.type === "gif" || cfg.type === "image") && cfg.src) {
15668
+ const type = String(cfg.type ?? "").toLowerCase();
15669
+ cfg = { ...cfg, type };
15670
+ rt.cfg = cfg;
15671
+ if ((type === "video" || type === "gif" || type === "image") && cfg.src) {
13500
15672
  mountMedia(rt, cfg);
13501
- } else if (cfg.type === "scene" && (cfg.source || cfg.src)) {
15673
+ } else if (type === "scene" && (cfg.source || cfg.src)) {
13502
15674
  mountScene(rt, cfg);
15675
+ } else if (type === "web" && cfg.src) {
15676
+ mountWeb(rt, cfg);
13503
15677
  } else {
13504
15678
  rt.onUnhandledType?.(cfg);
13505
15679
  }
@@ -13509,22 +15683,60 @@ vec3 DecompressNormal(vec4 tex) {
13509
15683
  if (fit === "fill") return "cover";
13510
15684
  return fit === "contain" || fit === "stretch" ? fit : "cover";
13511
15685
  }
13512
- function toConfig(canvas, o) {
13513
- return {
13514
- type: "scene",
13515
- canvas,
13516
- source: o.source,
15686
+ function isWebProject(project) {
15687
+ const t = project?.type;
15688
+ return typeof t === "string" && t.toLowerCase() === "web";
15689
+ }
15690
+ function ensureSceneCanvas(el) {
15691
+ if (el instanceof HTMLCanvasElement) return el;
15692
+ const existing = el.querySelector(":scope > canvas[data-webwallgl]");
15693
+ if (existing instanceof HTMLCanvasElement) return existing;
15694
+ const c = document.createElement("canvas");
15695
+ c.setAttribute("data-webwallgl", "1");
15696
+ c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
15697
+ if (getComputedStyle(el).position === "static") el.style.position = "relative";
15698
+ el.appendChild(c);
15699
+ return c;
15700
+ }
15701
+ async function resolveMountConfig(el, o) {
15702
+ const base = {
13517
15703
  fit: normalizeFitOption(o.fit),
13518
15704
  renderDpr: o.renderDpr ?? 1,
13519
15705
  sceneFps: o.fps ?? 60,
13520
15706
  muted: (o.volume ?? 0) <= 0,
13521
- loop: true
15707
+ loop: true,
15708
+ canvas: el,
15709
+ source: o.source
13522
15710
  };
15711
+ let project = null;
15712
+ try {
15713
+ project = await o.source.project?.() ?? null;
15714
+ } catch {
15715
+ project = null;
15716
+ }
15717
+ if (isWebProject(project)) {
15718
+ let url;
15719
+ try {
15720
+ const entry = await o.source.webEntry?.();
15721
+ url = entry?.url;
15722
+ } catch {
15723
+ url = void 0;
15724
+ }
15725
+ if (!url && o.source.key) {
15726
+ const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") || "index.html" : "index.html";
15727
+ url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
15728
+ }
15729
+ if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
15730
+ return { ...base, type: "web", src: url, source: o.source };
15731
+ }
15732
+ const canvas = ensureSceneCanvas(el);
15733
+ return { ...base, type: "scene", canvas, source: o.source };
13523
15734
  }
13524
- function createScene(canvas, options) {
15735
+ function createScene(el, options) {
13525
15736
  const rt = createRuntime();
13526
15737
  const events = { ready: [], error: [], diagnostic: [] };
13527
15738
  let currentOptions = { ...options ?? {}, source: null };
15739
+ let boundEl = el;
13528
15740
  const emitError = (err) => {
13529
15741
  for (const fn of events.error) {
13530
15742
  try {
@@ -13563,6 +15775,24 @@ vec3 DecompressNormal(vec4 tex) {
13563
15775
  rt.onFirstFrame = () => {
13564
15776
  rt.onFirstFrame = void 0;
13565
15777
  prev?.();
15778
+ const info = rt.info ?? {
15779
+ width: 0,
15780
+ height: 0,
15781
+ layerCount: 0,
15782
+ hasModels: false,
15783
+ hasParticles: false,
15784
+ hasText: false
15785
+ };
15786
+ try {
15787
+ currentOptions.onReady?.(info);
15788
+ } catch {
15789
+ }
15790
+ for (const fn of events.ready) {
15791
+ try {
15792
+ fn(info);
15793
+ } catch {
15794
+ }
15795
+ }
13566
15796
  resolve();
13567
15797
  };
13568
15798
  });
@@ -13576,7 +15806,9 @@ vec3 DecompressNormal(vec4 tex) {
13576
15806
  return { promise, off };
13577
15807
  };
13578
15808
  const instance = {
13579
- canvas,
15809
+ get canvas() {
15810
+ return boundEl;
15811
+ },
13580
15812
  pause() {
13581
15813
  rt.paused = true;
13582
15814
  rt.sceneCtl?.pause();
@@ -13595,11 +15827,13 @@ vec3 DecompressNormal(vec4 tex) {
13595
15827
  },
13596
15828
  setFps(fps) {
13597
15829
  rt.cfg.sceneFps = fps;
15830
+ weShimCall(rt, (w) => w.__weSetFps?.(fps));
13598
15831
  },
13599
15832
  setVolume(volume) {
13600
15833
  const v = Math.max(0, Math.min(1, volume));
13601
15834
  rt.cfg.muted = v <= 0;
13602
15835
  rt.sceneAudio?.setVolume(v);
15836
+ weShimCall(rt, (w) => w.__weSetVolume?.(v));
13603
15837
  },
13604
15838
  setRenderDpr(dpr) {
13605
15839
  rt.cfg.renderDpr = dpr;
@@ -13616,17 +15850,17 @@ vec3 DecompressNormal(vec4 tex) {
13616
15850
  async load(source) {
13617
15851
  currentOptions = { ...currentOptions, source };
13618
15852
  wireOptions(currentOptions);
13619
- const cfg = {
13620
- ...rt.cfg,
13621
- type: "scene",
13622
- source,
13623
- src: void 0,
13624
- mediaBase: void 0
13625
- };
15853
+ const cfg = await resolveMountConfig(boundEl, currentOptions);
15854
+ if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
15855
+ boundEl = cfg.canvas;
15856
+ }
13626
15857
  rt.cfg = cfg;
13627
15858
  rt.paused = false;
13628
15859
  rt.info = void 0;
13629
15860
  resetCoverAlign(rt);
15861
+ if (currentOptions.properties) {
15862
+ rt.liveUserProps = { ...currentOptions.properties };
15863
+ }
13630
15864
  const firstFrame = armFirstFrame();
13631
15865
  const failure = armFailure();
13632
15866
  mountWallpaper(rt, cfg);
@@ -13670,10 +15904,17 @@ vec3 DecompressNormal(vec4 tex) {
13670
15904
  const applyOptions = async (o) => {
13671
15905
  currentOptions = o;
13672
15906
  wireOptions(o);
13673
- rt.cfg = toConfig(canvas, o);
15907
+ const cfg = await resolveMountConfig(el, o);
15908
+ if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
15909
+ boundEl = cfg.canvas;
15910
+ }
15911
+ rt.cfg = cfg;
13674
15912
  rt.paused = o.autoplay === false;
13675
15913
  rt.info = void 0;
13676
15914
  resetCoverAlign(rt);
15915
+ if (o.properties && Object.keys(o.properties).length) {
15916
+ rt.liveUserProps = { ...o.properties };
15917
+ }
13677
15918
  const firstFrame = armFirstFrame();
13678
15919
  const failure = armFailure();
13679
15920
  mountWallpaper(rt, rt.cfg);
@@ -13688,8 +15929,8 @@ vec3 DecompressNormal(vec4 tex) {
13688
15929
  instance.__applyOptions = applyOptions;
13689
15930
  return instance;
13690
15931
  }
13691
- async function mount(canvas, options) {
13692
- const instance = createScene(canvas, options);
15932
+ async function mount(el, options) {
15933
+ const instance = createScene(el, options);
13693
15934
  const withApply = instance;
13694
15935
  await withApply.__applyOptions(options);
13695
15936
  return instance;