webwallgl 1.0.0-beta1 → 1.0.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.
@@ -999,6 +999,7 @@
999
999
  // anchor 是盒子相对 origin 的锚点(同 image alignment 枚举,外加 "none")。
1000
1000
  // 本机 563 个文字层:none 237 / 缺省 301 / center 20 —— none 与缺省都按 center 处理
1001
1001
  // (WE 对象缺省对齐就是 center;显式 center 的挂件与时钟层行为一致)。
1002
+ // 2780710296 实验过默认改 top:竖直阶梯对了,但会平移其它壁纸文字相对图元的位置,已回滚。
1002
1003
  textAnchor: typeof o.anchor === "string" && o.anchor !== "none" ? o.anchor : "center",
1003
1004
  textMaxwidth: parseNum(o.maxwidth, 0),
1004
1005
  textMaxrows: parseNum(o.maxrows, 0),
@@ -2429,6 +2430,45 @@
2429
2430
  }
2430
2431
  }
2431
2432
  }
2433
+ {
2434
+ const inVecN = /* @__PURE__ */ new Map();
2435
+ const declRe = /^\s*in\s+(?:highp|mediump|lowp\s+)?(vec[34])\s+([A-Za-z_]\w*)\s*;/gm;
2436
+ let dm;
2437
+ while ((dm = declRe.exec(code)) !== null) inVecN.set(dm[2], Number(dm[1].slice(3)));
2438
+ const localVecN = new Map(inVecN);
2439
+ const locRe = /\b(vec[34])\s+([A-Za-z_]\w*)\s*[=;]/g;
2440
+ while ((dm = locRe.exec(code)) !== null) {
2441
+ if (!localVecN.has(dm[2])) localVecN.set(dm[2], Number(dm[1].slice(3)));
2442
+ }
2443
+ if (localVecN.size > 0) {
2444
+ const swizzleUvArg = (arg) => {
2445
+ const t = arg.trim();
2446
+ if (!t) return arg;
2447
+ const bare = /^([A-Za-z_]\w*)$/.exec(t);
2448
+ if (bare && localVecN.has(bare[1])) return bare[1] + ".xy";
2449
+ const bin = /^([A-Za-z_]\w*)(\s*[+\-].+)$/.exec(t);
2450
+ if (bin && localVecN.has(bin[1])) return "(" + bin[1] + ".xy" + bin[2] + ")";
2451
+ return arg;
2452
+ };
2453
+ for (const fn of ["textureLod", "texture"]) {
2454
+ code = rewriteCall(code, fn, (inner) => {
2455
+ const args = splitArgs(inner);
2456
+ if (args.length >= 2) args[1] = swizzleUvArg(args[1]);
2457
+ return fn + "(" + args.join(", ") + ")";
2458
+ });
2459
+ }
2460
+ }
2461
+ if (inVecN.size > 0) {
2462
+ code = code.split("\n").map((line) => {
2463
+ if (!/\bvec2\s+[A-Za-z_]\w*\s*=/.test(line)) return line;
2464
+ let out = line;
2465
+ for (const name of inVecN.keys()) {
2466
+ out = out.replace(new RegExp("\\b" + name + "\\b(?!\\s*[.\\w])", "g"), name + ".xy");
2467
+ }
2468
+ return out;
2469
+ }).join("\n");
2470
+ }
2471
+ }
2432
2472
  const written = /* @__PURE__ */ new Set();
2433
2473
  const inNames = /* @__PURE__ */ new Set();
2434
2474
  {
@@ -2724,7 +2764,8 @@ void main() {
2724
2764
  return s;
2725
2765
  }
2726
2766
  function parseVec3Local(s) {
2727
- const p = String(s).trim().split(/\s+/).map(Number);
2767
+ if (s !== null && typeof s === "object" && "value" in s) s = s.value;
2768
+ const p = String(s ?? "").trim().split(/\s+/).map(Number);
2728
2769
  return [p[0] || 0, p[1] || 0, p[2] || 0];
2729
2770
  }
2730
2771
  function makeTexture(gl, rgba, width, height, bitmap = null) {
@@ -3273,7 +3314,11 @@ void main() {
3273
3314
  }
3274
3315
  }
3275
3316
  const key = shaderName + "|" + JSON.stringify(effectiveCombos);
3276
- if (progCache.has(key)) return progCache.get(key);
3317
+ if (progCache.has(key)) {
3318
+ const hit = progCache.get(key);
3319
+ if (hit === null) throw new Error("shader=" + shaderName + " 编译失败(已缓存)");
3320
+ return hit;
3321
+ }
3277
3322
  for (let attempt = 0; attempt < 4; attempt++) {
3278
3323
  const missing = /* @__PURE__ */ new Set();
3279
3324
  const resolver = (file) => {
@@ -3288,6 +3333,7 @@ void main() {
3288
3333
  try {
3289
3334
  prog = linkProgram(gl, vertGlsl, fragGlsl);
3290
3335
  } catch (e) {
3336
+ progCache.set(key, null);
3291
3337
  throw new Error("shader=" + shaderName + " " + (e && e.message));
3292
3338
  }
3293
3339
  const uni = /* @__PURE__ */ new Map();
@@ -4481,7 +4527,10 @@ void main() {
4481
4527
  try {
4482
4528
  progEntry = await getEffectProgram(mp.shader, combos, mergedTex);
4483
4529
  } catch (e) {
4484
- console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, e && e.message || e);
4530
+ const msg = e && e.message || String(e);
4531
+ if (!/已缓存/.test(msg)) {
4532
+ console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, msg);
4533
+ }
4485
4534
  failedEffects.add(eff2);
4486
4535
  continue;
4487
4536
  }
@@ -4831,6 +4880,7 @@ layout(location=1) in vec3 a_pos; // 实例中心(投影空间世界
4831
4880
  layout(location=2) in vec2 a_sizeRot; // x=size(像素) y=rot(弧度)
4832
4881
  layout(location=3) in vec4 a_color; // rgb + alpha
4833
4882
  layout(location=4) in vec3 a_stretchFrame; // xy=非等比拉伸 z=帧序号
4883
+ layout(location=5) in vec2 a_vrange; // 段两端沿贴图 v 的取值(rope 连线用;普通精灵 0..1)
4834
4884
  uniform mat4 u_mvp;
4835
4885
  // 序列帧 uv 变换表(TEXS 帧矩形归一化后的 offset/scale),最多 128 帧
4836
4886
  // (matrix spritesheet 72 有 71 帧,旧上限 64 会丢末尾字符)
@@ -4848,7 +4898,8 @@ void main(){
4848
4898
  gl_Position = u_mvp * vec4(a_pos.xy + rotated, a_pos.z, 1.0);
4849
4899
  // quad 角 → 贴图 uv。世界 y 已翻转到投影空间(y 向下),故 quad 的 +y 角
4850
4900
  // 对应屏幕上方,应采样纹理顶行 v=1(与 renderer.js 的 layerQuadVerts 同约定)。
4851
- vec2 uv = a_corner + 0.5;
4901
+ // a_vrange rope 段两端各取自己的 v(沿绳连续渐变);普通精灵是 (0,1) 恒等。
4902
+ vec2 uv = vec2(a_corner.x + 0.5, mix(a_vrange.x, a_vrange.y, a_corner.y + 0.5));
4852
4903
  if (u_frameCount > 0) {
4853
4904
  // 帧矩形以左上为原点(TEXS 是 top-down 像素坐标),故先把 v 翻成 top-down
4854
4905
  int fi = int(a_stretchFrame.z);
@@ -4922,7 +4973,7 @@ void main(){
4922
4973
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
4923
4974
  gl.vertexAttribDivisor(0, 0);
4924
4975
  gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
4925
- const S = 48;
4976
+ const S = 56;
4926
4977
  gl.enableVertexAttribArray(1);
4927
4978
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, S, 0);
4928
4979
  gl.vertexAttribDivisor(1, 1);
@@ -4935,6 +4986,9 @@ void main(){
4935
4986
  gl.enableVertexAttribArray(4);
4936
4987
  gl.vertexAttribPointer(4, 3, gl.FLOAT, false, S, 36);
4937
4988
  gl.vertexAttribDivisor(4, 1);
4989
+ gl.enableVertexAttribArray(5);
4990
+ gl.vertexAttribPointer(5, 2, gl.FLOAT, false, S, 48);
4991
+ gl.vertexAttribDivisor(5, 1);
4938
4992
  gl.bindVertexArray(null);
4939
4993
  return {
4940
4994
  prog: {
@@ -4986,6 +5040,20 @@ void main(){
4986
5040
  function particleInstanceSegs(trailCfg, trailSegments) {
4987
5041
  return trailCfg && trailCfg.kind === "ropetrail" ? Math.max(1, trailSegments || 1) : 1;
4988
5042
  }
5043
+ function ropeTrailHistoryCount(cfg) {
5044
+ if (!cfg || cfg.kind !== "ropetrail") return 1;
5045
+ const segs = Math.round(Number(cfg.segments) || 0);
5046
+ if (segs >= 2) return Math.min(32, segs);
5047
+ return 8;
5048
+ }
5049
+ function ropeTrailDuration(cfg) {
5050
+ if (!cfg || cfg.kind !== "ropetrail") return 0;
5051
+ const L = Number(cfg.length);
5052
+ return Number.isFinite(L) && L > 0 ? L : 0.2;
5053
+ }
5054
+ function ropeParticleV(p) {
5055
+ return p.life > 0 ? p.age / p.life : 0;
5056
+ }
4989
5057
  class Particle {
4990
5058
  constructor() {
4991
5059
  this.alive = false;
@@ -5014,6 +5082,8 @@ void main(){
5014
5082
  this.turbSpeed = 0;
5015
5083
  this.turbPhase = 0;
5016
5084
  this.trail = null;
5085
+ this.trailClock = 0;
5086
+ this.seq = 0;
5017
5087
  }
5018
5088
  }
5019
5089
  class ParticleSystem {
@@ -5074,6 +5144,9 @@ void main(){
5074
5144
  this._followParent = null;
5075
5145
  this._followMode = null;
5076
5146
  this._followOffset = [0, 0, 0];
5147
+ this.ropeRenderer = null;
5148
+ this._seq = 0;
5149
+ this._ropeOrder = [];
5077
5150
  this._ov = {};
5078
5151
  this._applyOverride();
5079
5152
  this.startTime = Math.max(0, Math.min(30, num(this.model.starttime, 0)));
@@ -5350,24 +5423,34 @@ void main(){
5350
5423
  const kind = r && r.name || "sprite";
5351
5424
  return {
5352
5425
  kind,
5353
- length: num(r && r.length, kind === "spritetrail" ? 0.1 : 0),
5426
+ length: num(r && r.length, kind === "spritetrail" ? 0.1 : kind === "ropetrail" ? 0.2 : 0),
5354
5427
  maxLength: num(r && r.maxlength, 0),
5355
5428
  minLength: num(r && r.minlength, 0),
5356
5429
  subdivision: num(r && r.subdivision, 1),
5430
+ // Rope Trail 段数(官方 `segments`);与 spritetrail 的 maxlength 无关
5431
+ segments: num(r && r.segments, 0),
5357
5432
  orientation: r && r.orientation || null
5358
5433
  };
5359
5434
  });
5360
5435
  if (omitted && !this.renderers.length) {
5361
- this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, orientation: null }];
5436
+ this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, segments: 0, orientation: null }];
5362
5437
  }
5363
5438
  const tr = this.renderers.find((r) => r.kind === "spritetrail" || r.kind === "ropetrail");
5364
5439
  this.trailCfg = tr || null;
5365
5440
  this.trailSegments = 1;
5441
+ this.trailDuration = 0;
5442
+ this.trailSampleDt = 0;
5366
5443
  if (tr && tr.kind === "ropetrail") {
5367
- const segs = Math.max(2, Math.min(16, Math.round(tr.maxLength || tr.subdivision || 6)));
5444
+ const segs = ropeTrailHistoryCount(tr);
5368
5445
  this.trailSegments = segs;
5369
- for (const p of this.pool) p.trail = new Float32Array(segs * 3);
5446
+ this.trailDuration = ropeTrailDuration(tr);
5447
+ this.trailSampleDt = this.trailDuration / Math.max(1, segs - 1);
5448
+ for (const p of this.pool) {
5449
+ p.trail = new Float32Array(segs * 3);
5450
+ p.trailClock = 0;
5451
+ }
5370
5452
  }
5453
+ this.ropeRenderer = this.renderers.find((r) => r.kind === "rope") || null;
5371
5454
  }
5372
5455
  setModel(model) {
5373
5456
  this.model = model;
@@ -5498,6 +5581,7 @@ void main(){
5498
5581
  p.rotVel = 0;
5499
5582
  p.vx = p.vy = p.vz = 0;
5500
5583
  p.frame = 0;
5584
+ p.seq = this._seq++;
5501
5585
  const o = em.origin;
5502
5586
  if (em.kind === "box") {
5503
5587
  const d = em.distanceMax || [0, 0, 0];
@@ -5644,6 +5728,7 @@ void main(){
5644
5728
  p.trail[i + 1] = p.y;
5645
5729
  p.trail[i + 2] = p.z;
5646
5730
  }
5731
+ p.trailClock = 0;
5647
5732
  }
5648
5733
  }
5649
5734
  // ---------- 每粒子更新 ----------
@@ -5794,10 +5879,20 @@ void main(){
5794
5879
  }
5795
5880
  if (p.trail) {
5796
5881
  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];
5882
+ const step = this.trailSampleDt;
5883
+ if (step > 0) {
5884
+ p.trailClock = (p.trailClock || 0) + dt;
5885
+ let shifts = 0;
5886
+ const cap = this.trailSegments || 8;
5887
+ while (p.trailClock >= step && shifts < cap) {
5888
+ p.trailClock -= step;
5889
+ shifts++;
5890
+ for (let i = tr.length - 3; i >= 3; i -= 3) {
5891
+ tr[i] = tr[i - 3];
5892
+ tr[i + 1] = tr[i - 2];
5893
+ tr[i + 2] = tr[i - 1];
5894
+ }
5895
+ }
5801
5896
  }
5802
5897
  tr[0] = p.x;
5803
5898
  tr[1] = p.y;
@@ -5892,13 +5987,22 @@ void main(){
5892
5987
  if (!this._prog) this._buildProgram(gl);
5893
5988
  const trail = this.trailCfg && this.trailCfg.kind === "ropetrail" ? this.trailCfg : null;
5894
5989
  const spriteTrail = this.trailCfg && this.trailCfg.kind === "spritetrail" ? this.trailCfg : null;
5990
+ const rope = this.ropeRenderer;
5895
5991
  const segs = particleInstanceSegs(this.trailCfg, this.trailSegments);
5896
- const STRIDE = 12;
5992
+ const STRIDE = 14;
5897
5993
  const pool = this.pool;
5994
+ let order = null;
5995
+ if (rope) {
5996
+ order = this._ropeOrder;
5997
+ order.length = 0;
5998
+ for (let i = 0; i < pool.length; i++) if (pool[i].alive) order.push(pool[i]);
5999
+ order.sort((a, b) => a.seq - b.seq);
6000
+ }
5898
6001
  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;
6002
+ if (order) live = order.length;
6003
+ else for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
6004
+ if (live === 0 || rope && live < 2) return;
6005
+ const instCount = rope ? live - 1 : live * segs;
5902
6006
  const need = instCount * STRIDE;
5903
6007
  if (!this._data || this._data.length < need) this._data = new Float32Array(Math.max(need, 1024));
5904
6008
  const data = this._data;
@@ -5918,7 +6022,34 @@ void main(){
5918
6022
  const py = ly * sy;
5919
6023
  return [ox + px * cos - py * sin, projH - (oy + px * sin + py * cos)];
5920
6024
  };
5921
- for (let i = 0; i < pool.length; i++) {
6025
+ if (rope) {
6026
+ for (let i = 0; i + 1 < live; i++) {
6027
+ const a = order[i];
6028
+ const b = order[i + 1];
6029
+ const wa = toWorld(a.x, a.y);
6030
+ const wb = toWorld(b.x, b.y);
6031
+ const dx = wb[0] - wa[0];
6032
+ const dy = wb[1] - wa[1];
6033
+ const dist = Math.hypot(dx, dy);
6034
+ const width2 = (Math.abs(a.size) + Math.abs(b.size)) * 0.5 * sysScale;
6035
+ if (!(width2 > 0)) continue;
6036
+ data[k++] = (wa[0] + wb[0]) * 0.5;
6037
+ data[k++] = (wa[1] + wb[1]) * 0.5;
6038
+ data[k++] = 0;
6039
+ data[k++] = width2;
6040
+ data[k++] = Math.atan2(-dx, dy);
6041
+ data[k++] = (a.r + b.r) * 0.5 * bright;
6042
+ data[k++] = (a.g + b.g) * 0.5 * bright;
6043
+ data[k++] = (a.b + b.b) * 0.5 * bright;
6044
+ data[k++] = (a.alpha + b.alpha) * 0.5;
6045
+ data[k++] = 1;
6046
+ data[k++] = dist / width2;
6047
+ data[k++] = 0;
6048
+ data[k++] = ropeParticleV(a);
6049
+ data[k++] = ropeParticleV(b);
6050
+ }
6051
+ }
6052
+ for (let i = 0; i < pool.length && !rope; i++) {
5922
6053
  const p = pool[i];
5923
6054
  if (!p.alive) continue;
5924
6055
  for (let s = 0; s < segs; s++) {
@@ -5926,18 +6057,53 @@ void main(){
5926
6057
  let ly = p.y;
5927
6058
  let segAlpha = 1;
5928
6059
  let segSize = 1;
6060
+ let rot = p.rot;
6061
+ let instStretchX = stretchX;
6062
+ let instStretchY = stretchY;
6063
+ let wx;
6064
+ let wy;
5929
6065
  if (trail && p.trail) {
5930
6066
  lx = p.trail[s * 3];
5931
6067
  ly = p.trail[s * 3 + 1];
5932
6068
  const t = segs > 1 ? s / (segs - 1) : 0;
5933
6069
  segAlpha = 1 - t;
5934
6070
  segSize = 1 - t * 0.55;
6071
+ let tdx = 0;
6072
+ let tdy = 0;
6073
+ if (s + 1 < segs) {
6074
+ tdx = p.trail[s * 3] - p.trail[(s + 1) * 3];
6075
+ tdy = p.trail[s * 3 + 1] - p.trail[(s + 1) * 3 + 1];
6076
+ } else if (s > 0) {
6077
+ tdx = p.trail[(s - 1) * 3] - p.trail[s * 3];
6078
+ tdy = p.trail[(s - 1) * 3 + 1] - p.trail[s * 3 + 1];
6079
+ } else {
6080
+ tdx = p.vx;
6081
+ tdy = p.vy;
6082
+ }
6083
+ const w0 = toWorld(lx, ly);
6084
+ const w1 = toWorld(lx - tdx, ly - tdy);
6085
+ const dx = w0[0] - w1[0];
6086
+ const dy = w0[1] - w1[1];
6087
+ const dist = Math.hypot(dx, dy);
6088
+ const base = Math.max(1e-3, Math.abs(p.size) * sysScale * segSize);
6089
+ if (dist > 1e-3) {
6090
+ rot = spriteTrailRotation(dx, dy);
6091
+ wx = (w0[0] + w1[0]) * 0.5;
6092
+ wy = (w0[1] + w1[1]) * 0.5;
6093
+ instStretchY = Math.max(stretchY, dist / base);
6094
+ } else {
6095
+ wx = w0[0];
6096
+ wy = w0[1];
6097
+ }
6098
+ } else {
6099
+ const w = toWorld(lx, ly);
6100
+ wx = w[0];
6101
+ wy = w[1];
5935
6102
  }
5936
- const w = toWorld(lx, ly);
5937
- let rot = p.rot;
5938
- let instStretchX = stretchX;
5939
- let instStretchY = stretchY;
5940
6103
  if (spriteTrail) {
6104
+ const w = toWorld(p.x, p.y);
6105
+ wx = w[0];
6106
+ wy = w[1];
5941
6107
  const w1 = toWorld(p.x + p.vx, p.y + p.vy);
5942
6108
  rot = spriteTrailRotation(w1[0] - w[0], w1[1] - w[1]);
5943
6109
  const factor = spriteTrailLengthFactor(
@@ -5948,8 +6114,8 @@ void main(){
5948
6114
  );
5949
6115
  instStretchY = stretchY * factor;
5950
6116
  }
5951
- data[k++] = w[0];
5952
- data[k++] = w[1];
6117
+ data[k++] = wx;
6118
+ data[k++] = wy;
5953
6119
  data[k++] = 0;
5954
6120
  data[k++] = Math.abs(p.size) * sysScale * segSize;
5955
6121
  data[k++] = rot;
@@ -5960,6 +6126,8 @@ void main(){
5960
6126
  data[k++] = instStretchX;
5961
6127
  data[k++] = instStretchY;
5962
6128
  data[k++] = p.frame;
6129
+ data[k++] = 0;
6130
+ data[k++] = 1;
5963
6131
  }
5964
6132
  }
5965
6133
  const prog = this._prog;
@@ -6093,6 +6261,9 @@ void main(){
6093
6261
  particleInstanceSegs,
6094
6262
  particlePassRefract,
6095
6263
  rgbaIsBlankWhite,
6264
+ ropeParticleV,
6265
+ ropeTrailDuration,
6266
+ ropeTrailHistoryCount,
6096
6267
  spriteTrailLengthFactor,
6097
6268
  spriteTrailRotation
6098
6269
  }, Symbol.toStringTag, { value: "Module" }));
@@ -6198,7 +6369,7 @@ void main(){
6198
6369
  return { width: size, height: size, rgba };
6199
6370
  }
6200
6371
  function beam(w, h, coreWidth, fadeBoth, peak) {
6201
- const pk = peak === void 0 ? 0.55 : peak;
6372
+ const pk = 0.55;
6202
6373
  const rgba = new Uint8Array(w * h * 4);
6203
6374
  for (let y = 0; y < h; y++) {
6204
6375
  const ty = y / (h - 1);
@@ -6211,6 +6382,41 @@ void main(){
6211
6382
  }
6212
6383
  return { width: w, height: h, rgba };
6213
6384
  }
6385
+ const RAIN_FRAMES = 4;
6386
+ function rainStreak(w, h, tiltDeg, corePx, peak) {
6387
+ const pk = peak === void 0 ? 0.85 : peak;
6388
+ const rgba = new Uint8Array(w * h * 4);
6389
+ const fh = h / RAIN_FRAMES;
6390
+ const rng = mulberry32$1(335009);
6391
+ const tilt = Math.tan(tiltDeg * Math.PI / 180);
6392
+ const cx = w / 2;
6393
+ for (let f = 0; f < RAIN_FRAMES; f++) {
6394
+ const peakF = pk * (0.8 + rng() * 0.35);
6395
+ const coreF = corePx * (0.85 + rng() * 0.5);
6396
+ const ph = (rng() - 0.5) * w * 0.12;
6397
+ for (let y = 0; y < fh; y++) {
6398
+ const ty = y / (fh - 1);
6399
+ const lineX = cx + ph + (fh - 1) * tilt / 2 - (fh - 1) * tilt * ty;
6400
+ const vy = gauss(ty - 0.5, 0.27);
6401
+ for (let x = 0; x < w; x++) {
6402
+ const d = Math.abs(x + 0.5 - lineX);
6403
+ const g = Math.exp(-(d * d) / (coreF * coreF));
6404
+ const a = g * vy * peakF;
6405
+ if (a < 4e-3) continue;
6406
+ writeWhite(rgba, ((f * fh + y) * w + x) * 4, a);
6407
+ }
6408
+ }
6409
+ }
6410
+ return { width: w, height: h, rgba };
6411
+ }
6412
+ function builtinParticleFrames(name) {
6413
+ if (name === "particle/nature/rain1" || name === "particle/nature/rain2") {
6414
+ const list = [];
6415
+ for (let i = 0; i < RAIN_FRAMES; i++) list.push({ ou: 0, ov: i / RAIN_FRAMES, su: 1, sv: 1 / RAIN_FRAMES });
6416
+ return list;
6417
+ }
6418
+ return null;
6419
+ }
6214
6420
  function ring(size, radius, thickness) {
6215
6421
  const rgba = new Uint8Array(size * size * 4);
6216
6422
  const half = size / 2;
@@ -6891,8 +7097,8 @@ void main(){
6891
7097
  // 水滴(原生 64×256 → 128×512):竖长泪滴
6892
7098
  "particle/drop": () => teardrop(128, 512),
6893
7099
  // 雨丝(原生 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),
7100
+ "particle/nature/rain1": () => rainStreak(128, 512, 10, 1, 0.78),
7101
+ "particle/nature/rain2": () => rainStreak(128, 512, 10, 1.6, 0.66),
6896
7102
  // 雨滴 sheet(原生 128×256 → 256×512,2×4 格):每格一颗上圆下尖小水滴
6897
7103
  "particle/water/rain_drops_sheet": () => dropSheet(256, 512, 2, 4),
6898
7104
  // 雾(原生 256 → 512):絮状 fBm,弱遮罩铺满;三张不同尺度/种子
@@ -7010,6 +7216,7 @@ void main(){
7010
7216
  const particleTexMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
7011
7217
  __proto__: null,
7012
7218
  buildBuiltinParticleTexture,
7219
+ builtinParticleFrames,
7013
7220
  isBuiltinParticleTextureName,
7014
7221
  listBuiltinParticleTextureNames,
7015
7222
  particleNormalNameForAlbedo,
@@ -7627,6 +7834,10 @@ void main(){
7627
7834
  c.origin[0] += d[0];
7628
7835
  c.origin[1] += d[1];
7629
7836
  }
7837
+ layer.parallaxDepth = parent.parallaxDepth ? parent.parallaxDepth.slice() : null;
7838
+ for (const c of desc) {
7839
+ c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
7840
+ }
7630
7841
  follows.push({
7631
7842
  layer,
7632
7843
  parent,
@@ -7934,10 +8145,12 @@ void main() {
7934
8145
  const totalH = lines.length * lineHeight;
7935
8146
  const halign = opts.halign || "center";
7936
8147
  const valign = opts.valign || "center";
7937
- const y0 = valign === "top" ? pad : valign === "bottom" ? Math.max(pad, boxH - pad - totalH) : (boxH - totalH) / 2;
8148
+ const midX = boxW / 2;
8149
+ const midY = boxH / 2;
8150
+ const y0 = valign === "top" ? midY : valign === "bottom" ? midY - totalH : (boxH - totalH) / 2;
7938
8151
  const out = lines.map((text, i) => {
7939
8152
  const w = widths[i];
7940
- const x = halign === "left" ? pad : halign === "right" ? Math.max(pad, boxW - pad - w) : (boxW - w) / 2;
8153
+ const x = halign === "left" ? midX : halign === "right" ? midX - w : (boxW - w) / 2;
7941
8154
  return { text, width: w, x, y: y0 + i * lineHeight };
7942
8155
  });
7943
8156
  return { lines: out, lineHeight, totalH, truncated, boxW, boxH };
@@ -8312,6 +8525,11 @@ void main() {
8312
8525
  if (v !== null && typeof v === "object" && "value" in v) return v.value;
8313
8526
  return v;
8314
8527
  }
8528
+ function engineCanvasSize(cs) {
8529
+ const src = cs || { width: 1920, height: 1080 };
8530
+ if (src.x !== void 0 && src.y !== void 0) return src;
8531
+ return { x: src.width, y: src.height, width: src.width, height: src.height };
8532
+ }
8315
8533
  function evalTextScript(script, scriptprops, opts = {}) {
8316
8534
  if (typeof script !== "string" || script.length === 0) return null;
8317
8535
  const body = scriptToFunctionBody(script);
@@ -8353,7 +8571,7 @@ void main() {
8353
8571
  },
8354
8572
  frametime: 1 / 60,
8355
8573
  runtime: 0,
8356
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
8574
+ canvasSize: engineCanvasSize(opts.canvasSize),
8357
8575
  // [we-scene patch] engine.screenResolution(全库 10 处 / 2 壁纸):
8358
8576
  // 屏幕**像素**尺寸,脚本用它把 input.cursorScreenPosition 归一化
8359
8577
  // (3791967416 除它得 [0,1];3509243656 减半屏得 [-1,1])。
@@ -9303,7 +9521,7 @@ void main() {
9303
9521
  },
9304
9522
  frametime: 1 / 60,
9305
9523
  runtime: 0,
9306
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
9524
+ canvasSize: engineCanvasSize(opts.canvasSize),
9307
9525
  screenResolution: opts.screenResolution || { x: 1920, y: 1080 },
9308
9526
  timeOfDay: typeof opts.timeOfDay === "number" ? opts.timeOfDay : 0,
9309
9527
  userProperties: opts.userProperties || {},
@@ -9738,7 +9956,7 @@ void main() {
9738
9956
  cancel.handle = state.handle;
9739
9957
  return cancel;
9740
9958
  }
9741
- function setInterval(fn, ms) {
9959
+ function setInterval2(fn, ms) {
9742
9960
  if (typeof fn !== "function") return makeCancel({ fired: true, handle: null }, null);
9743
9961
  const state = { fired: false, handle: null };
9744
9962
  const cancel = makeCancel(state, clearI);
@@ -9754,7 +9972,7 @@ void main() {
9754
9972
  }
9755
9973
  if (clearT && h != null) clearT(h);
9756
9974
  }
9757
- function clearInterval(h) {
9975
+ function clearInterval2(h) {
9758
9976
  if (typeof h === "function") {
9759
9977
  h();
9760
9978
  return;
@@ -9767,9 +9985,9 @@ void main() {
9767
9985
  }
9768
9986
  return {
9769
9987
  setTimeout,
9770
- setInterval,
9988
+ setInterval: setInterval2,
9771
9989
  clearTimeout,
9772
- clearInterval,
9990
+ clearInterval: clearInterval2,
9773
9991
  dispose,
9774
9992
  /** 测试与诊断用:尚未触发且未取消的定时器数 */
9775
9993
  pendingCount: () => pending.size
@@ -9819,11 +10037,11 @@ void main() {
9819
10037
  const patterns = makePatterns(rand2);
9820
10038
  const phases = new Float32Array(64);
9821
10039
  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);
10040
+ const BANDS2 = 64;
10041
+ const rawL = new Float32Array(BANDS2);
10042
+ const rawR = new Float32Array(BANDS2);
10043
+ const left64 = new Float32Array(BANDS2);
10044
+ const right64 = new Float32Array(BANDS2);
9827
10045
  const left32 = new Float32Array(32);
9828
10046
  const right32 = new Float32Array(32);
9829
10047
  const left16 = new Float32Array(16);
@@ -9840,7 +10058,7 @@ void main() {
9840
10058
  /** 渲染器诊断:当前是否处于「静音段」 */
9841
10059
  silent: false
9842
10060
  };
9843
- function downsample(dst, src) {
10061
+ function downsample2(dst, src) {
9844
10062
  const g = src.length / dst.length;
9845
10063
  for (let i = 0; i < dst.length; i++) {
9846
10064
  let s = 0;
@@ -9867,8 +10085,8 @@ void main() {
9867
10085
  const hatV = patterns.hat[i16] * hitEnv * drumGate;
9868
10086
  const riser = buildup > 0 ? Math.pow(buildup, 3) * (0.4 + 0.6 * Math.abs(vnoise(step * 2, 7))) : 0;
9869
10087
  let levelSum = 0;
9870
- for (let i = 0; i < BANDS; i++) {
9871
- const fq = i / BANDS;
10088
+ for (let i = 0; i < BANDS2; i++) {
10089
+ const fq = i / BANDS2;
9872
10090
  const tilt = Math.pow(1 - fq * 0.85, 1.6);
9873
10091
  let v = midGate * (0.5 + 0.3 * vnoise(beat * 0.5 + phases[i] * 0.05, i % 8));
9874
10092
  v *= 0.35 + 0.65 * fq;
@@ -9892,10 +10110,10 @@ void main() {
9892
10110
  }
9893
10111
  left64.set(rawL);
9894
10112
  right64.set(rawR);
9895
- downsample(left32, rawL);
9896
- downsample(right32, rawR);
9897
- downsample(left16, rawL);
9898
- downsample(right16, rawR);
10113
+ downsample2(left32, rawL);
10114
+ downsample2(right32, rawR);
10115
+ downsample2(left16, rawL);
10116
+ downsample2(right16, rawR);
9899
10117
  snapshot.level = Math.min(1, levelSum / (48 * 1.2));
9900
10118
  snapshot.silent = silent;
9901
10119
  return snapshot;
@@ -9904,7 +10122,7 @@ void main() {
9904
10122
  update,
9905
10123
  snapshot,
9906
10124
  /** 频段基数 */
9907
- bands: BANDS
10125
+ bands: BANDS2
9908
10126
  };
9909
10127
  }
9910
10128
  function fillAudioBuffers(views, snapshot) {
@@ -9926,7 +10144,7 @@ void main() {
9926
10144
  dst[i] = s / (i1 - i0);
9927
10145
  }
9928
10146
  }
9929
- const MEDIA_PLAYBACK = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
10147
+ const MEDIA_PLAYBACK$1 = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
9930
10148
  class MediaVec3 {
9931
10149
  constructor(x, y, z) {
9932
10150
  this.x = Number(x) || 0;
@@ -10051,7 +10269,7 @@ void main() {
10051
10269
  const cycle = tracks.reduce((s, t) => s + t.duration + GAP, 0);
10052
10270
  const snapshot = {
10053
10271
  hasMedia: false,
10054
- state: MEDIA_PLAYBACK.STOPPED,
10272
+ state: MEDIA_PLAYBACK$1.STOPPED,
10055
10273
  title: "",
10056
10274
  artist: "",
10057
10275
  album: "",
@@ -10118,10 +10336,10 @@ void main() {
10118
10336
  snapshot.duration = tr.duration;
10119
10337
  snapshot.position = pos;
10120
10338
  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;
10339
+ if (held) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10340
+ else if (inGap) snapshot.state = MEDIA_PLAYBACK$1.STOPPED;
10341
+ else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10342
+ else snapshot.state = MEDIA_PLAYBACK$1.PLAYING;
10125
10343
  snapshot.hasThumbnail = !inGap;
10126
10344
  const c = tr.colors;
10127
10345
  snapshot.primaryColor = new MediaVec3(c.primary[0], c.primary[1], c.primary[2]);
@@ -10160,7 +10378,7 @@ void main() {
10160
10378
  if (held) return snapshot;
10161
10379
  holdT = lastWall + seekOffset;
10162
10380
  held = true;
10163
- snapshot.state = MEDIA_PLAYBACK.PAUSED;
10381
+ snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10164
10382
  return snapshot;
10165
10383
  }
10166
10384
  function play() {
@@ -10177,7 +10395,7 @@ void main() {
10177
10395
  snapshot,
10178
10396
  tracks,
10179
10397
  cycle,
10180
- MEDIA_PLAYBACK,
10398
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10181
10399
  skipNext,
10182
10400
  skipPrevious,
10183
10401
  play,
@@ -10285,7 +10503,7 @@ void main() {
10285
10503
  }
10286
10504
  const mediaMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10287
10505
  __proto__: null,
10288
- MEDIA_PLAYBACK,
10506
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10289
10507
  cloneMediaSnapshot,
10290
10508
  createSimulatedMedia,
10291
10509
  diffMediaEvents,
@@ -11149,6 +11367,382 @@ void main() {
11149
11367
  const SKIP_PARTICLES = false;
11150
11368
  const SKIP_SCENE_EFFECTS = false;
11151
11369
  const TEXT_EM_SCALE = 4;
11370
+ const BANDS = 64;
11371
+ const MEDIA_PLAYBACK = { STOPPED: 0 };
11372
+ function zeroBands() {
11373
+ return {
11374
+ left64: new Float32Array(BANDS),
11375
+ right64: new Float32Array(BANDS),
11376
+ left32: new Float32Array(32),
11377
+ right32: new Float32Array(32),
11378
+ left16: new Float32Array(16),
11379
+ right16: new Float32Array(16),
11380
+ level: 0,
11381
+ silent: true
11382
+ };
11383
+ }
11384
+ function downsample(dst, src64) {
11385
+ const g = src64.length / dst.length;
11386
+ for (let i = 0; i < dst.length; i++) {
11387
+ let s = 0;
11388
+ const i0 = Math.floor(i * g);
11389
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
11390
+ for (let j = i0; j < i1; j++) s += src64[j];
11391
+ dst[i] = s / (i1 - i0);
11392
+ }
11393
+ }
11394
+ function fillFromByteFreq(out, bytes, sampleRate) {
11395
+ const n = bytes.length;
11396
+ const nyquist = sampleRate * 0.5;
11397
+ const fMin = 20;
11398
+ const fMax = Math.min(2e4, nyquist);
11399
+ let levelSum = 0;
11400
+ for (let b = 0; b < BANDS; b++) {
11401
+ const t0 = b / BANDS;
11402
+ const t1 = (b + 1) / BANDS;
11403
+ const loHz = fMin * Math.pow(fMax / fMin, t0);
11404
+ const hiHz = fMin * Math.pow(fMax / fMin, t1);
11405
+ const i0 = Math.max(0, Math.floor(loHz / nyquist * n));
11406
+ const i1 = Math.min(n, Math.max(i0 + 1, Math.ceil(hiHz / nyquist * n)));
11407
+ let s = 0;
11408
+ for (let i = i0; i < i1; i++) s += bytes[i] / 255;
11409
+ const v = Math.min(1, s / (i1 - i0) * 1.35);
11410
+ out.left64[b] = v;
11411
+ out.right64[b] = v;
11412
+ if (b < 48) levelSum += v;
11413
+ }
11414
+ downsample(out.left32, out.left64);
11415
+ downsample(out.right32, out.right64);
11416
+ downsample(out.left16, out.left64);
11417
+ downsample(out.right16, out.right64);
11418
+ out.level = Math.min(1, levelSum / (48 * 1.2));
11419
+ out.silent = out.level < 0.02;
11420
+ }
11421
+ function hashHue(s) {
11422
+ let h = 2166136261;
11423
+ for (let i = 0; i < s.length; i++) {
11424
+ h ^= s.charCodeAt(i);
11425
+ h = Math.imul(h, 16777619);
11426
+ }
11427
+ return (h >>> 0) % 360;
11428
+ }
11429
+ function hslToRgb(h, sat, light) {
11430
+ const s = sat / 100;
11431
+ const l = light / 100;
11432
+ const c = (1 - Math.abs(2 * l - 1)) * s;
11433
+ const hp = h / 60;
11434
+ const x = c * (1 - Math.abs(hp % 2 - 1));
11435
+ let r = 0, g = 0, b = 0;
11436
+ if (hp < 1) [r, g, b] = [c, x, 0];
11437
+ else if (hp < 2) [r, g, b] = [x, c, 0];
11438
+ else if (hp < 3) [r, g, b] = [0, c, x];
11439
+ else if (hp < 4) [r, g, b] = [0, x, c];
11440
+ else if (hp < 5) [r, g, b] = [x, 0, c];
11441
+ else [r, g, b] = [c, 0, x];
11442
+ const m = l - c / 2;
11443
+ return [r + m, g + m, b + m];
11444
+ }
11445
+ function applyPalette(snap, seed) {
11446
+ const hue = hashHue(seed || "empty");
11447
+ const [pr, pg, pb] = hslToRgb(hue, 72, 48);
11448
+ const [sr, sg, sb] = hslToRgb((hue + 40) % 360, 55, 28);
11449
+ const [tr, tg, tb] = hslToRgb((hue + 20) % 360, 70, 72);
11450
+ snap.primaryColor = media.mediaVec3(pr, pg, pb);
11451
+ snap.secondaryColor = media.mediaVec3(sr, sg, sb);
11452
+ snap.tertiaryColor = media.mediaVec3(tr, tg, tb);
11453
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
11454
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
11455
+ snap.hasThumbnail = !!seed;
11456
+ }
11457
+ function sampleArtworkPalette(img, w, h) {
11458
+ const c = document.createElement("canvas");
11459
+ c.width = 32;
11460
+ c.height = 32;
11461
+ const ctx = c.getContext("2d", { willReadFrequently: true });
11462
+ if (!ctx) return null;
11463
+ ctx.drawImage(img, 0, 0, w, h, 0, 0, 32, 32);
11464
+ const data = ctx.getImageData(0, 0, 32, 32).data;
11465
+ let r = 0, g = 0, b = 0, n = 0;
11466
+ let br = 0, bg = 0, bb = 0, best = -1;
11467
+ for (let i = 0; i < data.length; i += 4) {
11468
+ const pr = data[i] / 255, pg = data[i + 1] / 255, pb = data[i + 2] / 255;
11469
+ r += pr;
11470
+ g += pg;
11471
+ b += pb;
11472
+ n++;
11473
+ const mx = Math.max(pr, pg, pb), mn = Math.min(pr, pg, pb);
11474
+ const sat = mx - mn;
11475
+ const lum = 0.2126 * pr + 0.7152 * pg + 0.0722 * pb;
11476
+ const score = sat * 1.4 + (lum > 0.15 && lum < 0.85 ? 0.3 : 0);
11477
+ if (score > best) {
11478
+ best = score;
11479
+ br = pr;
11480
+ bg = pg;
11481
+ bb = pb;
11482
+ }
11483
+ }
11484
+ if (!n) return null;
11485
+ const primary = [br, bg, bb];
11486
+ const secondary = [r / n * 0.55, g / n * 0.55, b / n * 0.55];
11487
+ const tertiary = [
11488
+ Math.min(1, primary[0] * 0.45 + 0.55),
11489
+ Math.min(1, primary[1] * 0.45 + 0.55),
11490
+ Math.min(1, primary[2] * 0.45 + 0.55)
11491
+ ];
11492
+ return { primary, secondary, tertiary };
11493
+ }
11494
+ function rasterizeArtwork(img, srcW, srcH, size = 512) {
11495
+ const c = document.createElement("canvas");
11496
+ c.width = size;
11497
+ c.height = size;
11498
+ const ctx = c.getContext("2d");
11499
+ const scale = Math.max(size / Math.max(1, srcW), size / Math.max(1, srcH));
11500
+ const dw = srcW * scale;
11501
+ const dh = srcH * scale;
11502
+ ctx.fillStyle = "#000";
11503
+ ctx.fillRect(0, 0, size, size);
11504
+ ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
11505
+ const id = ctx.getImageData(0, 0, size, size);
11506
+ return { width: size, height: size, rgba: new Uint8Array(id.data) };
11507
+ }
11508
+ function emptyMediaSnapshot() {
11509
+ return {
11510
+ hasMedia: false,
11511
+ state: MEDIA_PLAYBACK.STOPPED,
11512
+ title: "",
11513
+ artist: "",
11514
+ album: "",
11515
+ albumArtist: "",
11516
+ position: 0,
11517
+ duration: 0,
11518
+ hasThumbnail: false,
11519
+ primaryColor: media.mediaVec3(0, 0, 0),
11520
+ secondaryColor: media.mediaVec3(0, 0, 0),
11521
+ tertiaryColor: media.mediaVec3(0, 0, 0),
11522
+ textColor: media.mediaVec3(1, 1, 1),
11523
+ highContrastColor: media.mediaVec3(1, 1, 1),
11524
+ trackIndex: -1,
11525
+ lyrics: [],
11526
+ lyricLine: "",
11527
+ lyricIndex: -1
11528
+ };
11529
+ }
11530
+ async function openMicAnalyser() {
11531
+ if (!navigator.mediaDevices?.getUserMedia) return null;
11532
+ try {
11533
+ const stream = await navigator.mediaDevices.getUserMedia({
11534
+ audio: {
11535
+ echoCancellation: false,
11536
+ noiseSuppression: false,
11537
+ autoGainControl: false
11538
+ },
11539
+ video: false
11540
+ });
11541
+ const ctx = new AudioContext();
11542
+ const src = ctx.createMediaStreamSource(stream);
11543
+ const analyser = ctx.createAnalyser();
11544
+ analyser.fftSize = 2048;
11545
+ analyser.smoothingTimeConstant = 0.8;
11546
+ src.connect(analyser);
11547
+ if (ctx.state === "suspended") await ctx.resume().catch(() => {
11548
+ });
11549
+ return { ctx, stream, analyser, buf: new Uint8Array(analyser.frequencyBinCount) };
11550
+ } catch {
11551
+ return null;
11552
+ }
11553
+ }
11554
+ async function startLiveSystem(opts) {
11555
+ const origin = opts?.origin ?? (typeof location !== "undefined" ? location.origin : "");
11556
+ const onArtwork = opts?.onArtwork;
11557
+ const audioSnap = zeroBands();
11558
+ const mediaSnap = emptyMediaSnapshot();
11559
+ const winSnap = { app: "", title: "", url: "", index: 0 };
11560
+ let audioMode = "off";
11561
+ let mediaMode = "offline";
11562
+ let windowMode = "offline";
11563
+ let trackKey = "";
11564
+ let hasArtwork = false;
11565
+ let lastArtworkKey = "";
11566
+ const mic = await openMicAnalyser();
11567
+ if (mic) audioMode = "mic";
11568
+ else if (!navigator.mediaDevices?.getUserMedia) audioMode = "unavailable";
11569
+ else audioMode = "denied";
11570
+ let es = null;
11571
+ let pollTimer = null;
11572
+ let disposed = false;
11573
+ const requestArtwork = (key, title, artist) => {
11574
+ if (!origin || !onArtwork || !hasArtwork) return;
11575
+ if (lastArtworkKey === key) return;
11576
+ lastArtworkKey = key;
11577
+ onArtwork({
11578
+ url: `${origin}/api/system/artwork?k=${encodeURIComponent(key)}&_=${Date.now()}`,
11579
+ trackKey: key,
11580
+ title,
11581
+ artist
11582
+ });
11583
+ };
11584
+ const applyMediaPayload = (m) => {
11585
+ if (!m || !m.hasMedia) {
11586
+ mediaSnap.hasMedia = false;
11587
+ mediaSnap.state = MEDIA_PLAYBACK.STOPPED;
11588
+ mediaSnap.title = "";
11589
+ mediaSnap.artist = "";
11590
+ mediaSnap.album = "";
11591
+ mediaSnap.albumArtist = "";
11592
+ mediaSnap.position = 0;
11593
+ mediaSnap.duration = 0;
11594
+ mediaSnap.hasThumbnail = false;
11595
+ mediaSnap.trackIndex = -1;
11596
+ mediaMode = m ? "empty" : "offline";
11597
+ trackKey = "";
11598
+ hasArtwork = false;
11599
+ lastArtworkKey = "";
11600
+ return;
11601
+ }
11602
+ mediaMode = "live";
11603
+ mediaSnap.hasMedia = true;
11604
+ mediaSnap.state = Number(m.state) === 2 ? 2 : Number(m.state) === 1 ? 1 : 0;
11605
+ mediaSnap.title = String(m.title ?? "");
11606
+ mediaSnap.artist = String(m.artist ?? "");
11607
+ mediaSnap.album = String(m.album ?? "");
11608
+ mediaSnap.albumArtist = String(m.albumArtist ?? m.artist ?? "");
11609
+ mediaSnap.position = Number(m.position) || 0;
11610
+ mediaSnap.duration = Number(m.duration) || 0;
11611
+ hasArtwork = m.hasArtwork === true;
11612
+ const key = `${mediaSnap.title}|${mediaSnap.artist}|${mediaSnap.album}`;
11613
+ if (key !== trackKey) {
11614
+ trackKey = key;
11615
+ lastArtworkKey = "";
11616
+ mediaSnap.trackIndex = mediaSnap.trackIndex + 1 | 0;
11617
+ applyPalette(mediaSnap, key);
11618
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
11619
+ } else {
11620
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
11621
+ }
11622
+ };
11623
+ const applyWindowPayload = (w) => {
11624
+ if (!w) {
11625
+ windowMode = "offline";
11626
+ return;
11627
+ }
11628
+ winSnap.app = String(w.app ?? "");
11629
+ winSnap.title = String(w.title ?? "");
11630
+ winSnap.url = String(w.url ?? "");
11631
+ windowMode = winSnap.app || winSnap.title ? "live" : "empty";
11632
+ };
11633
+ const pollOnce = async () => {
11634
+ if (!origin || disposed) return;
11635
+ try {
11636
+ const [mr, wr] = await Promise.all([
11637
+ fetch(`${origin}/api/system/media`, { cache: "no-store" }),
11638
+ fetch(`${origin}/api/system/window`, { cache: "no-store" })
11639
+ ]);
11640
+ if (mr.ok) {
11641
+ const j = await mr.json();
11642
+ applyMediaPayload(j);
11643
+ } else {
11644
+ mediaMode = "offline";
11645
+ }
11646
+ if (wr.ok) {
11647
+ applyWindowPayload(await wr.json());
11648
+ }
11649
+ } catch {
11650
+ mediaMode = mediaMode === "live" ? "live" : "offline";
11651
+ windowMode = windowMode === "live" ? "live" : "offline";
11652
+ }
11653
+ };
11654
+ if (origin) {
11655
+ await pollOnce();
11656
+ pollTimer = setInterval(() => void pollOnce(), 1e3);
11657
+ try {
11658
+ es = new EventSource(`${origin}/api/system/stream`);
11659
+ es.onmessage = (ev) => {
11660
+ if (disposed) return;
11661
+ try {
11662
+ const data = JSON.parse(ev.data);
11663
+ applyMediaPayload(data.media);
11664
+ applyWindowPayload(data.window);
11665
+ } catch {
11666
+ }
11667
+ };
11668
+ } catch {
11669
+ }
11670
+ }
11671
+ const postControl = (action) => {
11672
+ if (!origin || disposed) return;
11673
+ void fetch(`${origin}/api/system/media-control`, {
11674
+ method: "POST",
11675
+ headers: { "Content-Type": "application/json" },
11676
+ body: JSON.stringify({ action })
11677
+ }).then(async (r) => {
11678
+ if (!r.ok) return null;
11679
+ return r.json();
11680
+ }).then((j) => {
11681
+ if (j && typeof j === "object") applyMediaPayload(j);
11682
+ void pollOnce();
11683
+ }).catch(() => {
11684
+ void pollOnce();
11685
+ });
11686
+ };
11687
+ return {
11688
+ audio: {
11689
+ snapshot: audioSnap,
11690
+ pump: () => {
11691
+ if (!mic || disposed) {
11692
+ audioSnap.level = 0;
11693
+ audioSnap.silent = true;
11694
+ return;
11695
+ }
11696
+ mic.analyser.getByteFrequencyData(mic.buf);
11697
+ fillFromByteFreq(audioSnap, mic.buf, mic.ctx.sampleRate || 48e3);
11698
+ }
11699
+ },
11700
+ media: {
11701
+ snapshot: mediaSnap,
11702
+ pump: () => {
11703
+ },
11704
+ skipNext: () => postControl("skipNext"),
11705
+ skipPrevious: () => postControl("skipPrevious"),
11706
+ play: () => postControl("play"),
11707
+ pause: () => postControl("pause"),
11708
+ playPause: () => postControl("playPause")
11709
+ },
11710
+ windowTitle: {
11711
+ snapshot: winSnap,
11712
+ pump: () => {
11713
+ }
11714
+ },
11715
+ status: () => ({
11716
+ audio: audioMode,
11717
+ media: mediaMode,
11718
+ window: windowMode,
11719
+ title: mediaSnap.title,
11720
+ artist: mediaSnap.artist,
11721
+ app: winSnap.app,
11722
+ windowTitle: winSnap.title,
11723
+ hasArtwork
11724
+ }),
11725
+ dispose: () => {
11726
+ disposed = true;
11727
+ if (pollTimer) {
11728
+ clearInterval(pollTimer);
11729
+ pollTimer = null;
11730
+ }
11731
+ try {
11732
+ es?.close();
11733
+ } catch {
11734
+ }
11735
+ es = null;
11736
+ if (mic) {
11737
+ try {
11738
+ mic.stream.getTracks().forEach((t) => t.stop());
11739
+ void mic.ctx.close();
11740
+ } catch {
11741
+ }
11742
+ }
11743
+ }
11744
+ };
11745
+ }
11152
11746
  const WE_SHADER_HEADERS = {
11153
11747
  "common.h": `// WE common.h(重建子集,供 we-scene 浏览器渲染)
11154
11748
  #define M_PI 3.14159265359
@@ -11421,10 +12015,18 @@ mat3 squareToQuad(vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
11421
12015
  float d = p1.y - p0.y + g * p1.y;
11422
12016
  float e = p3.y - p0.y + h * p3.y;
11423
12017
  float f = p0.y;
11424
- // 行向量约定:[u v 1] * M
11425
- return mat3(a, d, g,
11426
- b, e, h,
11427
- c, f, 1.0);
12018
+ // 调用点一律是 mul(vec3(uv,1), inverse(本函数结果)),hlsl2glsl 把它转写成
12019
+ // transpose(xform) * vec3(uv,1)。要让屏幕点 s 得到 texCoord = S⁻¹·s(S 为
12020
+ // Heckbert 正向矩阵 [[a,b,c],[d,e,f],[g,h,1]],单位方→四边形),必须
12021
+ // xform = inverse(本函数结果) 满足 transpose(xform)·s = S⁻¹·s,
12022
+ // 即本函数返回 S 的**转置**:mat3 列主序构造为 (a,d,g)(b,e,h)(c,f,1) 的转置
12023
+ // = (a,b,c)(d,e,f)(g,h,1)。排布差一个转置,perspective/水波等全部错位——
12024
+ // 3174556087 的音谱柱被贴到窗户侧边竖排(应为贴下窗沿横排)实测确认。
12025
+ // 2026-09-05 数值模拟:旧排布 transpose(S⁻¹)·corner 与正确 S⁻¹·corner 逐项不同,
12026
+ // 可见区塌缩成一条斜带;新排布后中心 (0.5,0.5) → (0.478,0.511) ∈ [0,1]²。
12027
+ return mat3(a, b, c,
12028
+ d, e, f,
12029
+ g, h, 1.0);
11428
12030
  }
11429
12031
  `,
11430
12032
  // WE common_blur.h(重建)。blurNa 的权重不是估算的 —— 壁纸 1444077782 里存着
@@ -11554,6 +12156,66 @@ vec3 DecompressNormal(vec4 tex) {
11554
12156
  "common_vertex.h": `// WE common_vertex.h(重建:空占位,见 headers.ts 注释)
11555
12157
  `
11556
12158
  };
12159
+ function sanitizeFontForBrowser(src) {
12160
+ if (!src || src.length < 12) return src;
12161
+ const b0 = src[0], b1 = src[1], b2 = src[2], b3 = src[3];
12162
+ const isTtf = b0 === 0 && b1 === 1 && b2 === 0 && b3 === 0;
12163
+ const isOtto = b0 === 79 && b1 === 84 && b2 === 84 && b3 === 79;
12164
+ if (!isTtf && !isOtto) return src;
12165
+ const out = Uint8Array.from(src);
12166
+ const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
12167
+ const numTables = dv.getUint16(4);
12168
+ if (numTables <= 0 || 12 + numTables * 16 > out.length) return src;
12169
+ let cmapEntry = -1;
12170
+ let cmapOffset = 0;
12171
+ let cmapLength = 0;
12172
+ for (let i = 0; i < numTables; i++) {
12173
+ const e = 12 + i * 16;
12174
+ const tag = String.fromCharCode(out[e], out[e + 1], out[e + 2], out[e + 3]);
12175
+ if (tag === "cmap") {
12176
+ cmapEntry = e;
12177
+ cmapOffset = dv.getUint32(e + 8);
12178
+ cmapLength = dv.getUint32(e + 12);
12179
+ break;
12180
+ }
12181
+ }
12182
+ if (cmapEntry < 0 || cmapOffset + cmapLength > out.length) return src;
12183
+ let changed = false;
12184
+ const numEnc = dv.getUint16(cmapOffset + 2);
12185
+ for (let i = 0; i < numEnc; i++) {
12186
+ const rec = cmapOffset + 4 + i * 8;
12187
+ const soff = dv.getUint32(rec + 4);
12188
+ const abs = cmapOffset + soff;
12189
+ if (abs + 14 > out.length) continue;
12190
+ if (dv.getUint16(abs) !== 4) continue;
12191
+ const segCountX2 = dv.getUint16(abs + 6);
12192
+ const segCount = segCountX2 >>> 1;
12193
+ if (segCount < 1) continue;
12194
+ const expSearch = 2 * Math.pow(2, Math.floor(Math.log2(segCount)));
12195
+ const expSel = Math.floor(Math.log2(segCount));
12196
+ const expShift = segCountX2 - expSearch;
12197
+ const curSearch = dv.getUint16(abs + 8);
12198
+ const curSel = dv.getUint16(abs + 10);
12199
+ const curShift = dv.getUint16(abs + 12);
12200
+ if (curSearch === expSearch && curSel === expSel && curShift === expShift) continue;
12201
+ dv.setUint16(abs + 8, expSearch);
12202
+ dv.setUint16(abs + 10, expSel);
12203
+ dv.setUint16(abs + 12, expShift);
12204
+ changed = true;
12205
+ }
12206
+ if (!changed) return src;
12207
+ let sum = 0;
12208
+ const end = cmapOffset + cmapLength;
12209
+ for (let p = cmapOffset; p < end; p += 4) {
12210
+ const b02 = out[p] || 0;
12211
+ const b12 = p + 1 < end ? out[p + 1] : 0;
12212
+ const b22 = p + 2 < end ? out[p + 2] : 0;
12213
+ const b32 = p + 3 < end ? out[p + 3] : 0;
12214
+ sum = sum + (b02 << 24 | b12 << 16 | b22 << 8 | b32) >>> 0;
12215
+ }
12216
+ dv.setUint32(cmapEntry + 4, sum);
12217
+ return out;
12218
+ }
11557
12219
  const SYSTEM_FONT_FAMILIES = {
11558
12220
  systemfont_segoe: "'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
11559
12221
  systemfont_arial: "Arial, 'Helvetica Neue', sans-serif",
@@ -11755,6 +12417,22 @@ vec3 DecompressNormal(vec4 tex) {
11755
12417
  if (disposed) return;
11756
12418
  const supportsAudioProcessing = project?.general?.supportsaudioprocessing !== false;
11757
12419
  const simAudio = createSimulatedAudio();
12420
+ const simMedia = media.createSimulatedMedia();
12421
+ const simWindow = system.createSimulatedWindowTitle();
12422
+ let live = null;
12423
+ const liveHold = {
12424
+ mediaDriver: null,
12425
+ lastSnap: {
12426
+ get: () => null,
12427
+ setHasThumbnail: () => {
12428
+ }
12429
+ }
12430
+ };
12431
+ const audioDriverRef = {
12432
+ current: null
12433
+ };
12434
+ let mediaDriver = simMedia;
12435
+ let windowDriver = simWindow;
11758
12436
  const audioSim = { enabled: supportsAudioProcessing };
11759
12437
  const zero = (n) => new Float32Array(n);
11760
12438
  const SILENT_AUDIO = {
@@ -11767,32 +12445,45 @@ vec3 DecompressNormal(vec4 tex) {
11767
12445
  level: 0,
11768
12446
  silent: true
11769
12447
  };
11770
- renderer.setAudioProvider(() => audioSim.enabled ? simAudio.snapshot : SILENT_AUDIO);
12448
+ renderer.setAudioProvider(() => {
12449
+ if (!audioSim.enabled) return SILENT_AUDIO;
12450
+ return audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
12451
+ });
11771
12452
  const audioViews = /* @__PURE__ */ new Map();
11772
12453
  window.__audioStats = () => ({
11773
12454
  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
12455
+ live: !!audioDriverRef.current,
12456
+ level: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).level * 1e3) / 1e3 : 0,
12457
+ silent: audioSim.enabled ? (audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).silent : true,
12458
+ bass: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).left64[2] * 1e3) / 1e3 : 0
11777
12459
  });
11778
12460
  window.__audioMute = (on) => {
11779
12461
  audioSim.enabled = !on;
11780
12462
  return audioSim.enabled;
11781
12463
  };
11782
- reportDiag(rt, cfg, `audio: simulated stream, supportsaudioprocessing=${supportsAudioProcessing}`);
11783
- const simMedia = media.createSimulatedMedia();
11784
- const simWindow = system.createSimulatedWindowTitle();
12464
+ reportDiag(
12465
+ rt,
12466
+ cfg,
12467
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
12468
+ );
11785
12469
  const shortcuts = system.createShortcutHandler((name) => {
11786
12470
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
11787
12471
  });
11788
12472
  const mediaSim = { enabled: true, override: null };
11789
12473
  const mediaHooks = [];
11790
12474
  let lastMediaSnap = null;
12475
+ liveHold.lastSnap = {
12476
+ get: () => lastMediaSnap,
12477
+ setHasThumbnail: (v) => {
12478
+ if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
12479
+ }
12480
+ };
12481
+ const mediaSnapshot = () => mediaDriver.snapshot;
11791
12482
  const registerMediaHook = (sb) => {
11792
12483
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
11793
12484
  mediaHooks.push(sb);
11794
- if (!simMedia.snapshot.hasMedia) return;
11795
- for (const { name, event } of media.diffMediaEvents(null, simMedia.snapshot)) {
12485
+ if (!mediaSnapshot().hasMedia) return;
12486
+ for (const { name, event } of media.diffMediaEvents(null, mediaSnapshot())) {
11796
12487
  try {
11797
12488
  sb.callMedia(name, event);
11798
12489
  } catch {
@@ -11801,26 +12492,27 @@ vec3 DecompressNormal(vec4 tex) {
11801
12492
  };
11802
12493
  window.__mediaStats = () => ({
11803
12494
  enabled: mediaSim.enabled,
12495
+ live: !!live,
11804
12496
  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
12497
+ title: mediaSnapshot().title,
12498
+ artist: mediaSnapshot().artist,
12499
+ album: mediaSnapshot().album,
12500
+ state: mediaSnapshot().state,
12501
+ position: Math.round(mediaSnapshot().position),
12502
+ duration: mediaSnapshot().duration,
12503
+ hasThumbnail: mediaSnapshot().hasThumbnail,
12504
+ lyric: mediaSnapshot().lyricLine,
12505
+ primaryColor: mediaSnapshot().primaryColor ? [mediaSnapshot().primaryColor.x, mediaSnapshot().primaryColor.y, mediaSnapshot().primaryColor.z] : null
11814
12506
  });
11815
12507
  window.__mediaSet = (patch) => {
11816
- Object.assign(simMedia.snapshot, patch || {});
11817
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
12508
+ Object.assign(mediaSnapshot(), patch || {});
12509
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11818
12510
  for (const { name, event } of evts) for (const sb of mediaHooks) sb.callMedia(name, event);
11819
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
12511
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11820
12512
  return window.__mediaStats();
11821
12513
  };
11822
12514
  const dispatchMediaNow = () => {
11823
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
12515
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11824
12516
  for (const { name, event } of evts) {
11825
12517
  for (const sb of mediaHooks) {
11826
12518
  try {
@@ -11829,42 +12521,50 @@ vec3 DecompressNormal(vec4 tex) {
11829
12521
  }
11830
12522
  }
11831
12523
  }
11832
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
12524
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11833
12525
  };
11834
12526
  const mediaControl = {
11835
- snapshot: simMedia.snapshot,
12527
+ get snapshot() {
12528
+ return mediaSnapshot();
12529
+ },
11836
12530
  skipNext: () => {
11837
- simMedia.skipNext();
12531
+ mediaDriver.skipNext();
11838
12532
  dispatchMediaNow();
11839
- return simMedia.snapshot;
12533
+ return mediaSnapshot();
11840
12534
  },
11841
12535
  skipPrevious: () => {
11842
- simMedia.skipPrevious();
12536
+ mediaDriver.skipPrevious();
11843
12537
  dispatchMediaNow();
11844
- return simMedia.snapshot;
12538
+ return mediaSnapshot();
11845
12539
  },
11846
12540
  play: () => {
11847
- simMedia.play();
12541
+ mediaDriver.play();
11848
12542
  dispatchMediaNow();
11849
- return simMedia.snapshot;
12543
+ return mediaSnapshot();
11850
12544
  },
11851
12545
  pause: () => {
11852
- simMedia.pause();
12546
+ mediaDriver.pause();
11853
12547
  dispatchMediaNow();
11854
- return simMedia.snapshot;
12548
+ return mediaSnapshot();
11855
12549
  },
11856
12550
  playPause: () => {
11857
- simMedia.playPause();
12551
+ mediaDriver.playPause();
11858
12552
  dispatchMediaNow();
11859
- return simMedia.snapshot;
12553
+ return mediaSnapshot();
11860
12554
  }
11861
12555
  };
11862
12556
  window.__mediaControl = mediaControl;
11863
12557
  window.__system = {
11864
12558
  media: mediaControl,
11865
- windowTitle: simWindow.snapshot,
11866
- shortcuts: shortcuts.last
12559
+ windowTitle: windowDriver.snapshot,
12560
+ shortcuts: shortcuts.last,
12561
+ live: null
11867
12562
  };
12563
+ window.__liveSystem = () => ({
12564
+ audio: "off",
12565
+ media: "offline",
12566
+ window: "offline"
12567
+ });
11868
12568
  const pointerSrc = pointerLib.createPointerSource(
11869
12569
  cfg.canvas ? {
11870
12570
  target: cfg.canvas,
@@ -11968,6 +12668,118 @@ vec3 DecompressNormal(vec4 tex) {
11968
12668
  textures.set("$mediaPreviousThumbnail", mkThumb(tracks[tracks.length - 1]));
11969
12669
  }
11970
12670
  }
12671
+ if (cfg.liveSystem) {
12672
+ try {
12673
+ const uploadLiveArtwork = async (info) => {
12674
+ try {
12675
+ const res = await fetch(info.url, { cache: "no-store" });
12676
+ if (!res.ok) return;
12677
+ const blob = await res.blob();
12678
+ const bmp = await createImageBitmap(blob);
12679
+ const raster = rasterizeArtwork(bmp, bmp.width, bmp.height, 512);
12680
+ const palette = sampleArtworkPalette(bmp, bmp.width, bmp.height);
12681
+ bmp.close?.();
12682
+ const cur = textures.get("$mediaThumbnail");
12683
+ if (cur) textures.set("$mediaPreviousThumbnail", cur);
12684
+ const gl = renderer.gl;
12685
+ const existing = textures.get("$mediaThumbnail");
12686
+ if (existing?.glTex) {
12687
+ gl.bindTexture(gl.TEXTURE_2D, existing.glTex);
12688
+ gl.texImage2D(
12689
+ gl.TEXTURE_2D,
12690
+ 0,
12691
+ gl.RGBA,
12692
+ raster.width,
12693
+ raster.height,
12694
+ 0,
12695
+ gl.RGBA,
12696
+ gl.UNSIGNED_BYTE,
12697
+ raster.rgba
12698
+ );
12699
+ existing.width = raster.width;
12700
+ existing.height = raster.height;
12701
+ existing.mips = [raster];
12702
+ } else {
12703
+ textures.set("$mediaThumbnail", {
12704
+ glTex: rnd.makeTextureMip(gl, [raster], false),
12705
+ width: raster.width,
12706
+ height: raster.height,
12707
+ rg88: false,
12708
+ mips: [raster],
12709
+ generated: true
12710
+ });
12711
+ }
12712
+ const snap = mediaDriver.snapshot;
12713
+ if (palette) {
12714
+ snap.primaryColor = media.mediaVec3(...palette.primary);
12715
+ snap.secondaryColor = media.mediaVec3(...palette.secondary);
12716
+ snap.tertiaryColor = media.mediaVec3(...palette.tertiary);
12717
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
12718
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
12719
+ }
12720
+ snap.hasThumbnail = true;
12721
+ liveHold.lastSnap.setHasThumbnail(false);
12722
+ reportDiag(rt, cfg, `liveSystem: artwork ${info.title || info.trackKey}`);
12723
+ } catch (e) {
12724
+ reportDiag(
12725
+ rt,
12726
+ cfg,
12727
+ `liveSystem: artwork 失败 (${e instanceof Error ? e.message : e})`
12728
+ );
12729
+ }
12730
+ };
12731
+ live = await startLiveSystem({
12732
+ origin: location.origin,
12733
+ onArtwork: (info) => {
12734
+ void uploadLiveArtwork(info);
12735
+ }
12736
+ });
12737
+ mediaDriver = live.media;
12738
+ windowDriver = live.windowTitle;
12739
+ liveHold.mediaDriver = live.media;
12740
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
12741
+ if (mediaDriver.snapshot.hasMedia) {
12742
+ for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
12743
+ for (const sb of mediaHooks) {
12744
+ try {
12745
+ sb.callMedia(name, event);
12746
+ } catch {
12747
+ }
12748
+ }
12749
+ }
12750
+ lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
12751
+ }
12752
+ const st = live.status();
12753
+ reportDiag(
12754
+ rt,
12755
+ cfg,
12756
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
12757
+ );
12758
+ reportDiag(
12759
+ rt,
12760
+ cfg,
12761
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
12762
+ );
12763
+ window.__system = {
12764
+ media: mediaControl,
12765
+ windowTitle: windowDriver.snapshot,
12766
+ shortcuts: shortcuts.last,
12767
+ live: () => live.status()
12768
+ };
12769
+ window.__liveSystem = () => live.status();
12770
+ } catch (e) {
12771
+ reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
12772
+ live = null;
12773
+ }
12774
+ }
12775
+ {
12776
+ const prevCleanup = particleCleanup;
12777
+ particleCleanup = () => {
12778
+ prevCleanup?.();
12779
+ live?.dispose();
12780
+ live = null;
12781
+ };
12782
+ }
11971
12783
  const texInflight = /* @__PURE__ */ new Map();
11972
12784
  const loadTexInner = async (name) => {
11973
12785
  if (textures.has(name)) return textures.get(name);
@@ -12159,6 +12971,13 @@ vec3 DecompressNormal(vec4 tex) {
12159
12971
  model = JSON.parse(readText(modelEntry));
12160
12972
  }
12161
12973
  scn.applySolidFromModel(layer, model);
12974
+ const instUt = layer.srcObject?.instance?.usertextures;
12975
+ const instUtName = instUt?.[0] && typeof instUt[0].name === "string" && instUt[0].name.startsWith("$") ? instUt[0].name : null;
12976
+ const instBoundTex = instUtName && textures.has(instUtName) ? instUtName : null;
12977
+ if (instBoundTex) {
12978
+ layer.textureName = instBoundTex;
12979
+ layer.solid = false;
12980
+ }
12162
12981
  if (model && typeof model === "object" && "width" in model && "height" in model) {
12163
12982
  const m = model;
12164
12983
  if ((layer.size?.[0] || 0) === 0 && (layer.size?.[1] || 0) === 0 && m.width > 0 && m.height > 0) {
@@ -12199,7 +13018,7 @@ vec3 DecompressNormal(vec4 tex) {
12199
13018
  texJobs.push(
12200
13019
  loadTex(tn).then((entry) => {
12201
13020
  if (!entry) return;
12202
- if (si === 0) {
13021
+ if (si === 0 && !instBoundTex) {
12203
13022
  layer.textureName = tn;
12204
13023
  loadedTex++;
12205
13024
  if (entry.videoCtl) layer.videoCtl = entry.videoCtl;
@@ -12270,7 +13089,10 @@ vec3 DecompressNormal(vec4 tex) {
12270
13089
  height: gen.height,
12271
13090
  rg88: false,
12272
13091
  mips: [gen],
12273
- generated: true
13092
+ generated: true,
13093
+ // 内置贴图的帧表(rain1/rain2 的 1×4 图集):randomframe 预设依赖它
13094
+ // 随机取帧,缺了就整图采样画出超长丝(1823900922)。
13095
+ frames: ptex.builtinParticleFrames(name) ?? void 0
12274
13096
  };
12275
13097
  textures.set(name, entry);
12276
13098
  builtinTexCount++;
@@ -12476,8 +13298,8 @@ vec3 DecompressNormal(vec4 tex) {
12476
13298
  if (particleDiagFrame < 2) {
12477
13299
  particleDiagFrame++;
12478
13300
  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`);
13301
+ const live2 = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
13302
+ reportDiag(rt, cfg, `particles live: ${live2} across ${particleSystems.length} systems`);
12481
13303
  }
12482
13304
  }
12483
13305
  },
@@ -12496,7 +13318,7 @@ vec3 DecompressNormal(vec4 tex) {
12496
13318
  `particles: ${particleSystems.length} systems, ${builtinTexCount} builtin tex generated`
12497
13319
  );
12498
13320
  window.__particleStats = () => particleSystems.map((ps) => {
12499
- let live = 0;
13321
+ let live2 = 0;
12500
13322
  let minX = Infinity;
12501
13323
  let maxX = -Infinity;
12502
13324
  let minY = Infinity;
@@ -12505,7 +13327,7 @@ vec3 DecompressNormal(vec4 tex) {
12505
13327
  let maxS = -Infinity;
12506
13328
  for (const p of ps.pool) {
12507
13329
  if (!p.alive) continue;
12508
- live++;
13330
+ live2++;
12509
13331
  const px = ps.originX + p.x * ps.scaleX;
12510
13332
  const py = ps.originY + p.y * ps.scaleY;
12511
13333
  if (px < minX) minX = px;
@@ -12517,13 +13339,13 @@ vec3 DecompressNormal(vec4 tex) {
12517
13339
  if (s > maxS) maxS = s;
12518
13340
  }
12519
13341
  return {
12520
- live,
13342
+ live: live2,
12521
13343
  max: ps.maxCount,
12522
13344
  blend: ps.blend,
12523
13345
  renderer: ps.renderers.map((r) => r.kind).join("+"),
12524
13346
  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
13347
+ bbox: live2 ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
13348
+ size: live2 ? [Math.round(minS), Math.round(maxS)] : null
12527
13349
  };
12528
13350
  });
12529
13351
  window.__particleToggle = (on, onlyIndex) => {
@@ -12689,8 +13511,11 @@ vec3 DecompressNormal(vec4 tex) {
12689
13511
  try {
12690
13512
  const fe = pkg.getEntry(parsedPkg, fp);
12691
13513
  if (!fe) continue;
13514
+ const bytes = sanitizeFontForBrowser(
13515
+ fe instanceof Uint8Array ? fe : new Uint8Array(fe)
13516
+ );
12692
13517
  const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
12693
- const url = URL.createObjectURL(new Blob([fe]));
13518
+ const url = URL.createObjectURL(new Blob([bytes]));
12694
13519
  const ff = new FontFace(fam, `url(${url})`);
12695
13520
  await ff.load();
12696
13521
  document.fonts.add(ff);
@@ -12734,7 +13559,7 @@ vec3 DecompressNormal(vec4 tex) {
12734
13559
  // 与对象/效果开关/常量/general 统一走 engineTimers(P1-1)。
12735
13560
  ...timerOpts,
12736
13561
  mediaControl,
12737
- windowTitle: simWindow.snapshot,
13562
+ windowTitle: windowDriver.snapshot,
12738
13563
  openUserShortcut: shortcuts.openUserShortcut,
12739
13564
  getLayerText: (name) => textLayerText.get(name),
12740
13565
  onError: (e) => {
@@ -12982,7 +13807,7 @@ vec3 DecompressNormal(vec4 tex) {
12982
13807
  return clone;
12983
13808
  },
12984
13809
  mediaControl,
12985
- windowTitle: simWindow.snapshot,
13810
+ windowTitle: windowDriver.snapshot,
12986
13811
  openUserShortcut: shortcuts.openUserShortcut,
12987
13812
  isScreensaver: false
12988
13813
  };
@@ -13009,8 +13834,8 @@ vec3 DecompressNormal(vec4 tex) {
13009
13834
  const ctrl = anim.createAnimation(def.animation);
13010
13835
  ctrl.field = field;
13011
13836
  ctrl.baseValue = def.value;
13012
- const live = layer[field];
13013
- ctrl.baseNumeric = Array.isArray(live) ? live.slice() : live;
13837
+ const live2 = layer[field];
13838
+ ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
13014
13839
  layer.animationList.push(ctrl);
13015
13840
  if (ctrl.name) layer.animations[ctrl.name] = ctrl;
13016
13841
  animRuns.push({ layer, field, ctrl });
@@ -13227,8 +14052,10 @@ vec3 DecompressNormal(vec4 tex) {
13227
14052
  const t = (now - start - pauseAccum) / 1e3;
13228
14053
  inputView.update(pointerSrc.state);
13229
14054
  if (mediaSim.enabled) {
13230
- simMedia.update(t);
13231
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
14055
+ if (live?.media) live.media.pump();
14056
+ else simMedia.update(t);
14057
+ const snap = mediaSnapshot();
14058
+ const evts = media.diffMediaEvents(lastMediaSnap, snap);
13232
14059
  if (evts.length) {
13233
14060
  for (const { name, event } of evts) {
13234
14061
  for (const sb of mediaHooks) {
@@ -13236,10 +14063,11 @@ vec3 DecompressNormal(vec4 tex) {
13236
14063
  sb.callMedia(name, event);
13237
14064
  }
13238
14065
  }
13239
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
14066
+ lastMediaSnap = media.cloneMediaSnapshot(snap);
13240
14067
  }
13241
14068
  }
13242
- simWindow.update(t);
14069
+ if (live?.windowTitle) live.windowTitle.pump();
14070
+ else simWindow.update(t);
13243
14071
  for (const run of animRuns) {
13244
14072
  run.ctrl.advance(interval / 1e3);
13245
14073
  const field = run.field;
@@ -13307,8 +14135,12 @@ vec3 DecompressNormal(vec4 tex) {
13307
14135
  }
13308
14136
  if (visibilityDirty) recomputeVisibility();
13309
14137
  if (audioSim.enabled) {
13310
- simAudio.update(t);
13311
- fillAudioBuffers(audioViews, simAudio.snapshot);
14138
+ if (audioDriverRef.current) audioDriverRef.current.pump();
14139
+ else simAudio.update(t);
14140
+ fillAudioBuffers(
14141
+ audioViews,
14142
+ audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot
14143
+ );
13312
14144
  }
13313
14145
  if (attachFollows.length) {
13314
14146
  mdl.followAttachments(attachFollows, t, getBoneOverrides);