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.
package/webwallgl.mjs CHANGED
@@ -995,6 +995,7 @@ function parseScene(sceneJson, project) {
995
995
  // anchor 是盒子相对 origin 的锚点(同 image alignment 枚举,外加 "none")。
996
996
  // 本机 563 个文字层:none 237 / 缺省 301 / center 20 —— none 与缺省都按 center 处理
997
997
  // (WE 对象缺省对齐就是 center;显式 center 的挂件与时钟层行为一致)。
998
+ // 2780710296 实验过默认改 top:竖直阶梯对了,但会平移其它壁纸文字相对图元的位置,已回滚。
998
999
  textAnchor: typeof o.anchor === "string" && o.anchor !== "none" ? o.anchor : "center",
999
1000
  textMaxwidth: parseNum(o.maxwidth, 0),
1000
1001
  textMaxrows: parseNum(o.maxrows, 0),
@@ -2425,6 +2426,45 @@ function hlsl2glsl(src, stage, combos, includeResolver, siblingSrc) {
2425
2426
  }
2426
2427
  }
2427
2428
  }
2429
+ {
2430
+ const inVecN = /* @__PURE__ */ new Map();
2431
+ const declRe = /^\s*in\s+(?:highp|mediump|lowp\s+)?(vec[34])\s+([A-Za-z_]\w*)\s*;/gm;
2432
+ let dm;
2433
+ while ((dm = declRe.exec(code)) !== null) inVecN.set(dm[2], Number(dm[1].slice(3)));
2434
+ const localVecN = new Map(inVecN);
2435
+ const locRe = /\b(vec[34])\s+([A-Za-z_]\w*)\s*[=;]/g;
2436
+ while ((dm = locRe.exec(code)) !== null) {
2437
+ if (!localVecN.has(dm[2])) localVecN.set(dm[2], Number(dm[1].slice(3)));
2438
+ }
2439
+ if (localVecN.size > 0) {
2440
+ const swizzleUvArg = (arg) => {
2441
+ const t = arg.trim();
2442
+ if (!t) return arg;
2443
+ const bare = /^([A-Za-z_]\w*)$/.exec(t);
2444
+ if (bare && localVecN.has(bare[1])) return bare[1] + ".xy";
2445
+ const bin = /^([A-Za-z_]\w*)(\s*[+\-].+)$/.exec(t);
2446
+ if (bin && localVecN.has(bin[1])) return "(" + bin[1] + ".xy" + bin[2] + ")";
2447
+ return arg;
2448
+ };
2449
+ for (const fn of ["textureLod", "texture"]) {
2450
+ code = rewriteCall(code, fn, (inner) => {
2451
+ const args = splitArgs(inner);
2452
+ if (args.length >= 2) args[1] = swizzleUvArg(args[1]);
2453
+ return fn + "(" + args.join(", ") + ")";
2454
+ });
2455
+ }
2456
+ }
2457
+ if (inVecN.size > 0) {
2458
+ code = code.split("\n").map((line) => {
2459
+ if (!/\bvec2\s+[A-Za-z_]\w*\s*=/.test(line)) return line;
2460
+ let out = line;
2461
+ for (const name of inVecN.keys()) {
2462
+ out = out.replace(new RegExp("\\b" + name + "\\b(?!\\s*[.\\w])", "g"), name + ".xy");
2463
+ }
2464
+ return out;
2465
+ }).join("\n");
2466
+ }
2467
+ }
2428
2468
  const written = /* @__PURE__ */ new Set();
2429
2469
  const inNames = /* @__PURE__ */ new Set();
2430
2470
  {
@@ -2720,7 +2760,8 @@ function compile(gl, type, src) {
2720
2760
  return s;
2721
2761
  }
2722
2762
  function parseVec3Local(s) {
2723
- const p = String(s).trim().split(/\s+/).map(Number);
2763
+ if (s !== null && typeof s === "object" && "value" in s) s = s.value;
2764
+ const p = String(s ?? "").trim().split(/\s+/).map(Number);
2724
2765
  return [p[0] || 0, p[1] || 0, p[2] || 0];
2725
2766
  }
2726
2767
  function makeTexture(gl, rgba, width, height, bitmap = null) {
@@ -3269,7 +3310,11 @@ function createRenderer(canvas, opts = {}) {
3269
3310
  }
3270
3311
  }
3271
3312
  const key = shaderName + "|" + JSON.stringify(effectiveCombos);
3272
- if (progCache.has(key)) return progCache.get(key);
3313
+ if (progCache.has(key)) {
3314
+ const hit = progCache.get(key);
3315
+ if (hit === null) throw new Error("shader=" + shaderName + " 编译失败(已缓存)");
3316
+ return hit;
3317
+ }
3273
3318
  for (let attempt = 0; attempt < 4; attempt++) {
3274
3319
  const missing = /* @__PURE__ */ new Set();
3275
3320
  const resolver = (file) => {
@@ -3284,6 +3329,7 @@ function createRenderer(canvas, opts = {}) {
3284
3329
  try {
3285
3330
  prog = linkProgram(gl, vertGlsl, fragGlsl);
3286
3331
  } catch (e) {
3332
+ progCache.set(key, null);
3287
3333
  throw new Error("shader=" + shaderName + " " + (e && e.message));
3288
3334
  }
3289
3335
  const uni = /* @__PURE__ */ new Map();
@@ -4477,7 +4523,10 @@ function createRenderer(canvas, opts = {}) {
4477
4523
  try {
4478
4524
  progEntry = await getEffectProgram(mp.shader, combos, mergedTex);
4479
4525
  } catch (e) {
4480
- console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, e && e.message || e);
4526
+ const msg = e && e.message || String(e);
4527
+ if (!/已缓存/.test(msg)) {
4528
+ console.warn("[we-scene] 跳过效果(pass 编译失败):", mp.shader, msg);
4529
+ }
4481
4530
  failedEffects.add(eff2);
4482
4531
  continue;
4483
4532
  }
@@ -4827,6 +4876,7 @@ layout(location=1) in vec3 a_pos; // 实例中心(投影空间世界
4827
4876
  layout(location=2) in vec2 a_sizeRot; // x=size(像素) y=rot(弧度)
4828
4877
  layout(location=3) in vec4 a_color; // rgb + alpha
4829
4878
  layout(location=4) in vec3 a_stretchFrame; // xy=非等比拉伸 z=帧序号
4879
+ layout(location=5) in vec2 a_vrange; // 段两端沿贴图 v 的取值(rope 连线用;普通精灵 0..1)
4830
4880
  uniform mat4 u_mvp;
4831
4881
  // 序列帧 uv 变换表(TEXS 帧矩形归一化后的 offset/scale),最多 128 帧
4832
4882
  // (matrix spritesheet 72 有 71 帧,旧上限 64 会丢末尾字符)
@@ -4844,7 +4894,8 @@ void main(){
4844
4894
  gl_Position = u_mvp * vec4(a_pos.xy + rotated, a_pos.z, 1.0);
4845
4895
  // quad 角 → 贴图 uv。世界 y 已翻转到投影空间(y 向下),故 quad 的 +y 角
4846
4896
  // 对应屏幕上方,应采样纹理顶行 v=1(与 renderer.js 的 layerQuadVerts 同约定)。
4847
- vec2 uv = a_corner + 0.5;
4897
+ // a_vrange rope 段两端各取自己的 v(沿绳连续渐变);普通精灵是 (0,1) 恒等。
4898
+ vec2 uv = vec2(a_corner.x + 0.5, mix(a_vrange.x, a_vrange.y, a_corner.y + 0.5));
4848
4899
  if (u_frameCount > 0) {
4849
4900
  // 帧矩形以左上为原点(TEXS 是 top-down 像素坐标),故先把 v 翻成 top-down
4850
4901
  int fi = int(a_stretchFrame.z);
@@ -4918,7 +4969,7 @@ void main(){
4918
4969
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
4919
4970
  gl.vertexAttribDivisor(0, 0);
4920
4971
  gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
4921
- const S = 48;
4972
+ const S = 56;
4922
4973
  gl.enableVertexAttribArray(1);
4923
4974
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, S, 0);
4924
4975
  gl.vertexAttribDivisor(1, 1);
@@ -4931,6 +4982,9 @@ void main(){
4931
4982
  gl.enableVertexAttribArray(4);
4932
4983
  gl.vertexAttribPointer(4, 3, gl.FLOAT, false, S, 36);
4933
4984
  gl.vertexAttribDivisor(4, 1);
4985
+ gl.enableVertexAttribArray(5);
4986
+ gl.vertexAttribPointer(5, 2, gl.FLOAT, false, S, 48);
4987
+ gl.vertexAttribDivisor(5, 1);
4934
4988
  gl.bindVertexArray(null);
4935
4989
  return {
4936
4990
  prog: {
@@ -4982,6 +5036,20 @@ function spriteTrailRotation(dx, dy) {
4982
5036
  function particleInstanceSegs(trailCfg, trailSegments) {
4983
5037
  return trailCfg && trailCfg.kind === "ropetrail" ? Math.max(1, trailSegments || 1) : 1;
4984
5038
  }
5039
+ function ropeTrailHistoryCount(cfg) {
5040
+ if (!cfg || cfg.kind !== "ropetrail") return 1;
5041
+ const segs = Math.round(Number(cfg.segments) || 0);
5042
+ if (segs >= 2) return Math.min(32, segs);
5043
+ return 8;
5044
+ }
5045
+ function ropeTrailDuration(cfg) {
5046
+ if (!cfg || cfg.kind !== "ropetrail") return 0;
5047
+ const L = Number(cfg.length);
5048
+ return Number.isFinite(L) && L > 0 ? L : 0.2;
5049
+ }
5050
+ function ropeParticleV(p) {
5051
+ return p.life > 0 ? p.age / p.life : 0;
5052
+ }
4985
5053
  class Particle {
4986
5054
  constructor() {
4987
5055
  this.alive = false;
@@ -5010,6 +5078,8 @@ class Particle {
5010
5078
  this.turbSpeed = 0;
5011
5079
  this.turbPhase = 0;
5012
5080
  this.trail = null;
5081
+ this.trailClock = 0;
5082
+ this.seq = 0;
5013
5083
  }
5014
5084
  }
5015
5085
  class ParticleSystem {
@@ -5070,6 +5140,9 @@ class ParticleSystem {
5070
5140
  this._followParent = null;
5071
5141
  this._followMode = null;
5072
5142
  this._followOffset = [0, 0, 0];
5143
+ this.ropeRenderer = null;
5144
+ this._seq = 0;
5145
+ this._ropeOrder = [];
5073
5146
  this._ov = {};
5074
5147
  this._applyOverride();
5075
5148
  this.startTime = Math.max(0, Math.min(30, num(this.model.starttime, 0)));
@@ -5346,24 +5419,34 @@ class ParticleSystem {
5346
5419
  const kind = r && r.name || "sprite";
5347
5420
  return {
5348
5421
  kind,
5349
- length: num(r && r.length, kind === "spritetrail" ? 0.1 : 0),
5422
+ length: num(r && r.length, kind === "spritetrail" ? 0.1 : kind === "ropetrail" ? 0.2 : 0),
5350
5423
  maxLength: num(r && r.maxlength, 0),
5351
5424
  minLength: num(r && r.minlength, 0),
5352
5425
  subdivision: num(r && r.subdivision, 1),
5426
+ // Rope Trail 段数(官方 `segments`);与 spritetrail 的 maxlength 无关
5427
+ segments: num(r && r.segments, 0),
5353
5428
  orientation: r && r.orientation || null
5354
5429
  };
5355
5430
  });
5356
5431
  if (omitted && !this.renderers.length) {
5357
- this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, orientation: null }];
5432
+ this.renderers = [{ kind: "sprite", length: 0, maxLength: 0, minLength: 0, subdivision: 1, segments: 0, orientation: null }];
5358
5433
  }
5359
5434
  const tr = this.renderers.find((r) => r.kind === "spritetrail" || r.kind === "ropetrail");
5360
5435
  this.trailCfg = tr || null;
5361
5436
  this.trailSegments = 1;
5437
+ this.trailDuration = 0;
5438
+ this.trailSampleDt = 0;
5362
5439
  if (tr && tr.kind === "ropetrail") {
5363
- const segs = Math.max(2, Math.min(16, Math.round(tr.maxLength || tr.subdivision || 6)));
5440
+ const segs = ropeTrailHistoryCount(tr);
5364
5441
  this.trailSegments = segs;
5365
- for (const p of this.pool) p.trail = new Float32Array(segs * 3);
5442
+ this.trailDuration = ropeTrailDuration(tr);
5443
+ this.trailSampleDt = this.trailDuration / Math.max(1, segs - 1);
5444
+ for (const p of this.pool) {
5445
+ p.trail = new Float32Array(segs * 3);
5446
+ p.trailClock = 0;
5447
+ }
5366
5448
  }
5449
+ this.ropeRenderer = this.renderers.find((r) => r.kind === "rope") || null;
5367
5450
  }
5368
5451
  setModel(model) {
5369
5452
  this.model = model;
@@ -5494,6 +5577,7 @@ class ParticleSystem {
5494
5577
  p.rotVel = 0;
5495
5578
  p.vx = p.vy = p.vz = 0;
5496
5579
  p.frame = 0;
5580
+ p.seq = this._seq++;
5497
5581
  const o = em.origin;
5498
5582
  if (em.kind === "box") {
5499
5583
  const d = em.distanceMax || [0, 0, 0];
@@ -5640,6 +5724,7 @@ class ParticleSystem {
5640
5724
  p.trail[i + 1] = p.y;
5641
5725
  p.trail[i + 2] = p.z;
5642
5726
  }
5727
+ p.trailClock = 0;
5643
5728
  }
5644
5729
  }
5645
5730
  // ---------- 每粒子更新 ----------
@@ -5790,10 +5875,20 @@ class ParticleSystem {
5790
5875
  }
5791
5876
  if (p.trail) {
5792
5877
  const tr = p.trail;
5793
- for (let i = tr.length - 3; i >= 3; i -= 3) {
5794
- tr[i] = tr[i - 3];
5795
- tr[i + 1] = tr[i - 2];
5796
- tr[i + 2] = tr[i - 1];
5878
+ const step = this.trailSampleDt;
5879
+ if (step > 0) {
5880
+ p.trailClock = (p.trailClock || 0) + dt;
5881
+ let shifts = 0;
5882
+ const cap = this.trailSegments || 8;
5883
+ while (p.trailClock >= step && shifts < cap) {
5884
+ p.trailClock -= step;
5885
+ shifts++;
5886
+ for (let i = tr.length - 3; i >= 3; i -= 3) {
5887
+ tr[i] = tr[i - 3];
5888
+ tr[i + 1] = tr[i - 2];
5889
+ tr[i + 2] = tr[i - 1];
5890
+ }
5891
+ }
5797
5892
  }
5798
5893
  tr[0] = p.x;
5799
5894
  tr[1] = p.y;
@@ -5888,13 +5983,22 @@ class ParticleSystem {
5888
5983
  if (!this._prog) this._buildProgram(gl);
5889
5984
  const trail = this.trailCfg && this.trailCfg.kind === "ropetrail" ? this.trailCfg : null;
5890
5985
  const spriteTrail = this.trailCfg && this.trailCfg.kind === "spritetrail" ? this.trailCfg : null;
5986
+ const rope = this.ropeRenderer;
5891
5987
  const segs = particleInstanceSegs(this.trailCfg, this.trailSegments);
5892
- const STRIDE = 12;
5988
+ const STRIDE = 14;
5893
5989
  const pool = this.pool;
5990
+ let order = null;
5991
+ if (rope) {
5992
+ order = this._ropeOrder;
5993
+ order.length = 0;
5994
+ for (let i = 0; i < pool.length; i++) if (pool[i].alive) order.push(pool[i]);
5995
+ order.sort((a, b) => a.seq - b.seq);
5996
+ }
5894
5997
  let live = 0;
5895
- for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
5896
- if (live === 0) return;
5897
- const instCount = live * segs;
5998
+ if (order) live = order.length;
5999
+ else for (let i = 0; i < pool.length; i++) if (pool[i].alive) live++;
6000
+ if (live === 0 || rope && live < 2) return;
6001
+ const instCount = rope ? live - 1 : live * segs;
5898
6002
  const need = instCount * STRIDE;
5899
6003
  if (!this._data || this._data.length < need) this._data = new Float32Array(Math.max(need, 1024));
5900
6004
  const data = this._data;
@@ -5914,7 +6018,34 @@ class ParticleSystem {
5914
6018
  const py = ly * sy;
5915
6019
  return [ox + px * cos - py * sin, projH - (oy + px * sin + py * cos)];
5916
6020
  };
5917
- for (let i = 0; i < pool.length; i++) {
6021
+ if (rope) {
6022
+ for (let i = 0; i + 1 < live; i++) {
6023
+ const a = order[i];
6024
+ const b = order[i + 1];
6025
+ const wa = toWorld(a.x, a.y);
6026
+ const wb = toWorld(b.x, b.y);
6027
+ const dx = wb[0] - wa[0];
6028
+ const dy = wb[1] - wa[1];
6029
+ const dist = Math.hypot(dx, dy);
6030
+ const width2 = (Math.abs(a.size) + Math.abs(b.size)) * 0.5 * sysScale;
6031
+ if (!(width2 > 0)) continue;
6032
+ data[k++] = (wa[0] + wb[0]) * 0.5;
6033
+ data[k++] = (wa[1] + wb[1]) * 0.5;
6034
+ data[k++] = 0;
6035
+ data[k++] = width2;
6036
+ data[k++] = Math.atan2(-dx, dy);
6037
+ data[k++] = (a.r + b.r) * 0.5 * bright;
6038
+ data[k++] = (a.g + b.g) * 0.5 * bright;
6039
+ data[k++] = (a.b + b.b) * 0.5 * bright;
6040
+ data[k++] = (a.alpha + b.alpha) * 0.5;
6041
+ data[k++] = 1;
6042
+ data[k++] = dist / width2;
6043
+ data[k++] = 0;
6044
+ data[k++] = ropeParticleV(a);
6045
+ data[k++] = ropeParticleV(b);
6046
+ }
6047
+ }
6048
+ for (let i = 0; i < pool.length && !rope; i++) {
5918
6049
  const p = pool[i];
5919
6050
  if (!p.alive) continue;
5920
6051
  for (let s = 0; s < segs; s++) {
@@ -5922,18 +6053,53 @@ class ParticleSystem {
5922
6053
  let ly = p.y;
5923
6054
  let segAlpha = 1;
5924
6055
  let segSize = 1;
6056
+ let rot = p.rot;
6057
+ let instStretchX = stretchX;
6058
+ let instStretchY = stretchY;
6059
+ let wx;
6060
+ let wy;
5925
6061
  if (trail && p.trail) {
5926
6062
  lx = p.trail[s * 3];
5927
6063
  ly = p.trail[s * 3 + 1];
5928
6064
  const t = segs > 1 ? s / (segs - 1) : 0;
5929
6065
  segAlpha = 1 - t;
5930
6066
  segSize = 1 - t * 0.55;
6067
+ let tdx = 0;
6068
+ let tdy = 0;
6069
+ if (s + 1 < segs) {
6070
+ tdx = p.trail[s * 3] - p.trail[(s + 1) * 3];
6071
+ tdy = p.trail[s * 3 + 1] - p.trail[(s + 1) * 3 + 1];
6072
+ } else if (s > 0) {
6073
+ tdx = p.trail[(s - 1) * 3] - p.trail[s * 3];
6074
+ tdy = p.trail[(s - 1) * 3 + 1] - p.trail[s * 3 + 1];
6075
+ } else {
6076
+ tdx = p.vx;
6077
+ tdy = p.vy;
6078
+ }
6079
+ const w0 = toWorld(lx, ly);
6080
+ const w1 = toWorld(lx - tdx, ly - tdy);
6081
+ const dx = w0[0] - w1[0];
6082
+ const dy = w0[1] - w1[1];
6083
+ const dist = Math.hypot(dx, dy);
6084
+ const base = Math.max(1e-3, Math.abs(p.size) * sysScale * segSize);
6085
+ if (dist > 1e-3) {
6086
+ rot = spriteTrailRotation(dx, dy);
6087
+ wx = (w0[0] + w1[0]) * 0.5;
6088
+ wy = (w0[1] + w1[1]) * 0.5;
6089
+ instStretchY = Math.max(stretchY, dist / base);
6090
+ } else {
6091
+ wx = w0[0];
6092
+ wy = w0[1];
6093
+ }
6094
+ } else {
6095
+ const w = toWorld(lx, ly);
6096
+ wx = w[0];
6097
+ wy = w[1];
5931
6098
  }
5932
- const w = toWorld(lx, ly);
5933
- let rot = p.rot;
5934
- let instStretchX = stretchX;
5935
- let instStretchY = stretchY;
5936
6099
  if (spriteTrail) {
6100
+ const w = toWorld(p.x, p.y);
6101
+ wx = w[0];
6102
+ wy = w[1];
5937
6103
  const w1 = toWorld(p.x + p.vx, p.y + p.vy);
5938
6104
  rot = spriteTrailRotation(w1[0] - w[0], w1[1] - w[1]);
5939
6105
  const factor = spriteTrailLengthFactor(
@@ -5944,8 +6110,8 @@ class ParticleSystem {
5944
6110
  );
5945
6111
  instStretchY = stretchY * factor;
5946
6112
  }
5947
- data[k++] = w[0];
5948
- data[k++] = w[1];
6113
+ data[k++] = wx;
6114
+ data[k++] = wy;
5949
6115
  data[k++] = 0;
5950
6116
  data[k++] = Math.abs(p.size) * sysScale * segSize;
5951
6117
  data[k++] = rot;
@@ -5956,6 +6122,8 @@ class ParticleSystem {
5956
6122
  data[k++] = instStretchX;
5957
6123
  data[k++] = instStretchY;
5958
6124
  data[k++] = p.frame;
6125
+ data[k++] = 0;
6126
+ data[k++] = 1;
5959
6127
  }
5960
6128
  }
5961
6129
  const prog = this._prog;
@@ -6089,6 +6257,9 @@ const particlesMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.define
6089
6257
  particleInstanceSegs,
6090
6258
  particlePassRefract,
6091
6259
  rgbaIsBlankWhite,
6260
+ ropeParticleV,
6261
+ ropeTrailDuration,
6262
+ ropeTrailHistoryCount,
6092
6263
  spriteTrailLengthFactor,
6093
6264
  spriteTrailRotation
6094
6265
  }, Symbol.toStringTag, { value: "Module" }));
@@ -6194,7 +6365,7 @@ function spikyStar(size, points, len, sharp, coreR) {
6194
6365
  return { width: size, height: size, rgba };
6195
6366
  }
6196
6367
  function beam(w, h, coreWidth, fadeBoth, peak) {
6197
- const pk = peak === void 0 ? 0.55 : peak;
6368
+ const pk = 0.55;
6198
6369
  const rgba = new Uint8Array(w * h * 4);
6199
6370
  for (let y = 0; y < h; y++) {
6200
6371
  const ty = y / (h - 1);
@@ -6207,6 +6378,41 @@ function beam(w, h, coreWidth, fadeBoth, peak) {
6207
6378
  }
6208
6379
  return { width: w, height: h, rgba };
6209
6380
  }
6381
+ const RAIN_FRAMES = 4;
6382
+ function rainStreak(w, h, tiltDeg, corePx, peak) {
6383
+ const pk = peak === void 0 ? 0.85 : peak;
6384
+ const rgba = new Uint8Array(w * h * 4);
6385
+ const fh = h / RAIN_FRAMES;
6386
+ const rng = mulberry32$1(335009);
6387
+ const tilt = Math.tan(tiltDeg * Math.PI / 180);
6388
+ const cx = w / 2;
6389
+ for (let f = 0; f < RAIN_FRAMES; f++) {
6390
+ const peakF = pk * (0.8 + rng() * 0.35);
6391
+ const coreF = corePx * (0.85 + rng() * 0.5);
6392
+ const ph = (rng() - 0.5) * w * 0.12;
6393
+ for (let y = 0; y < fh; y++) {
6394
+ const ty = y / (fh - 1);
6395
+ const lineX = cx + ph + (fh - 1) * tilt / 2 - (fh - 1) * tilt * ty;
6396
+ const vy = gauss(ty - 0.5, 0.27);
6397
+ for (let x = 0; x < w; x++) {
6398
+ const d = Math.abs(x + 0.5 - lineX);
6399
+ const g = Math.exp(-(d * d) / (coreF * coreF));
6400
+ const a = g * vy * peakF;
6401
+ if (a < 4e-3) continue;
6402
+ writeWhite(rgba, ((f * fh + y) * w + x) * 4, a);
6403
+ }
6404
+ }
6405
+ }
6406
+ return { width: w, height: h, rgba };
6407
+ }
6408
+ function builtinParticleFrames(name) {
6409
+ if (name === "particle/nature/rain1" || name === "particle/nature/rain2") {
6410
+ const list = [];
6411
+ for (let i = 0; i < RAIN_FRAMES; i++) list.push({ ou: 0, ov: i / RAIN_FRAMES, su: 1, sv: 1 / RAIN_FRAMES });
6412
+ return list;
6413
+ }
6414
+ return null;
6415
+ }
6210
6416
  function ring(size, radius, thickness) {
6211
6417
  const rgba = new Uint8Array(size * size * 4);
6212
6418
  const half = size / 2;
@@ -6887,8 +7093,8 @@ const BUILDERS = {
6887
7093
  // 水滴(原生 64×256 → 128×512):竖长泪滴
6888
7094
  "particle/drop": () => teardrop(128, 512),
6889
7095
  // 雨丝(原生 64×256 → 128×512):细长条,两端渐隐
6890
- "particle/nature/rain1": () => beam(128, 512, 0.16, true, 0.6),
6891
- "particle/nature/rain2": () => beam(128, 512, 0.24, true, 0.55),
7096
+ "particle/nature/rain1": () => rainStreak(128, 512, 10, 1, 0.78),
7097
+ "particle/nature/rain2": () => rainStreak(128, 512, 10, 1.6, 0.66),
6892
7098
  // 雨滴 sheet(原生 128×256 → 256×512,2×4 格):每格一颗上圆下尖小水滴
6893
7099
  "particle/water/rain_drops_sheet": () => dropSheet(256, 512, 2, 4),
6894
7100
  // 雾(原生 256 → 512):絮状 fBm,弱遮罩铺满;三张不同尺度/种子
@@ -7006,6 +7212,7 @@ function isBuiltinParticleTextureName(name) {
7006
7212
  const particleTexMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
7007
7213
  __proto__: null,
7008
7214
  buildBuiltinParticleTexture,
7215
+ builtinParticleFrames,
7009
7216
  isBuiltinParticleTextureName,
7010
7217
  listBuiltinParticleTextureNames,
7011
7218
  particleNormalNameForAlbedo,
@@ -7623,6 +7830,10 @@ function applyAttachmentBindOrigins(layers) {
7623
7830
  c.origin[0] += d[0];
7624
7831
  c.origin[1] += d[1];
7625
7832
  }
7833
+ layer.parallaxDepth = parent.parallaxDepth ? parent.parallaxDepth.slice() : null;
7834
+ for (const c of desc) {
7835
+ c.parallaxDepth = layer.parallaxDepth ? layer.parallaxDepth.slice() : null;
7836
+ }
7626
7837
  follows.push({
7627
7838
  layer,
7628
7839
  parent,
@@ -7930,10 +8141,12 @@ function layoutText(content, opts, measure) {
7930
8141
  const totalH = lines.length * lineHeight;
7931
8142
  const halign = opts.halign || "center";
7932
8143
  const valign = opts.valign || "center";
7933
- const y0 = valign === "top" ? pad : valign === "bottom" ? Math.max(pad, boxH - pad - totalH) : (boxH - totalH) / 2;
8144
+ const midX = boxW / 2;
8145
+ const midY = boxH / 2;
8146
+ const y0 = valign === "top" ? midY : valign === "bottom" ? midY - totalH : (boxH - totalH) / 2;
7934
8147
  const out = lines.map((text, i) => {
7935
8148
  const w = widths[i];
7936
- const x = halign === "left" ? pad : halign === "right" ? Math.max(pad, boxW - pad - w) : (boxW - w) / 2;
8149
+ const x = halign === "left" ? midX : halign === "right" ? midX - w : (boxW - w) / 2;
7937
8150
  return { text, width: w, x, y: y0 + i * lineHeight };
7938
8151
  });
7939
8152
  return { lines: out, lineHeight, totalH, truncated, boxW, boxH };
@@ -8308,6 +8521,11 @@ function propValue(v) {
8308
8521
  if (v !== null && typeof v === "object" && "value" in v) return v.value;
8309
8522
  return v;
8310
8523
  }
8524
+ function engineCanvasSize(cs) {
8525
+ const src = cs || { width: 1920, height: 1080 };
8526
+ if (src.x !== void 0 && src.y !== void 0) return src;
8527
+ return { x: src.width, y: src.height, width: src.width, height: src.height };
8528
+ }
8311
8529
  function evalTextScript(script, scriptprops, opts = {}) {
8312
8530
  if (typeof script !== "string" || script.length === 0) return null;
8313
8531
  const body = scriptToFunctionBody(script);
@@ -8349,7 +8567,7 @@ function evalTextScript(script, scriptprops, opts = {}) {
8349
8567
  },
8350
8568
  frametime: 1 / 60,
8351
8569
  runtime: 0,
8352
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
8570
+ canvasSize: engineCanvasSize(opts.canvasSize),
8353
8571
  // [we-scene patch] engine.screenResolution(全库 10 处 / 2 壁纸):
8354
8572
  // 屏幕**像素**尺寸,脚本用它把 input.cursorScreenPosition 归一化
8355
8573
  // (3791967416 除它得 [0,1];3509243656 减半屏得 [-1,1])。
@@ -9299,7 +9517,7 @@ function evalObjectScript(script, scriptprops, opts = {}) {
9299
9517
  },
9300
9518
  frametime: 1 / 60,
9301
9519
  runtime: 0,
9302
- canvasSize: opts.canvasSize || { width: 1920, height: 1080 },
9520
+ canvasSize: engineCanvasSize(opts.canvasSize),
9303
9521
  screenResolution: opts.screenResolution || { x: 1920, y: 1080 },
9304
9522
  timeOfDay: typeof opts.timeOfDay === "number" ? opts.timeOfDay : 0,
9305
9523
  userProperties: opts.userProperties || {},
@@ -9734,7 +9952,7 @@ function createEngineTimers(host = {}, opts = {}) {
9734
9952
  cancel.handle = state.handle;
9735
9953
  return cancel;
9736
9954
  }
9737
- function setInterval(fn, ms) {
9955
+ function setInterval2(fn, ms) {
9738
9956
  if (typeof fn !== "function") return makeCancel({ fired: true, handle: null }, null);
9739
9957
  const state = { fired: false, handle: null };
9740
9958
  const cancel = makeCancel(state, clearI);
@@ -9750,7 +9968,7 @@ function createEngineTimers(host = {}, opts = {}) {
9750
9968
  }
9751
9969
  if (clearT && h != null) clearT(h);
9752
9970
  }
9753
- function clearInterval(h) {
9971
+ function clearInterval2(h) {
9754
9972
  if (typeof h === "function") {
9755
9973
  h();
9756
9974
  return;
@@ -9763,9 +9981,9 @@ function createEngineTimers(host = {}, opts = {}) {
9763
9981
  }
9764
9982
  return {
9765
9983
  setTimeout,
9766
- setInterval,
9984
+ setInterval: setInterval2,
9767
9985
  clearTimeout,
9768
- clearInterval,
9986
+ clearInterval: clearInterval2,
9769
9987
  dispose,
9770
9988
  /** 测试与诊断用:尚未触发且未取消的定时器数 */
9771
9989
  pendingCount: () => pending.size
@@ -9815,11 +10033,11 @@ function createSimulatedAudio(seed = 20260830) {
9815
10033
  const patterns = makePatterns(rand2);
9816
10034
  const phases = new Float32Array(64);
9817
10035
  for (let i = 0; i < 64; i++) phases[i] = rand2() * 64;
9818
- const BANDS = 64;
9819
- const rawL = new Float32Array(BANDS);
9820
- const rawR = new Float32Array(BANDS);
9821
- const left64 = new Float32Array(BANDS);
9822
- const right64 = new Float32Array(BANDS);
10036
+ const BANDS2 = 64;
10037
+ const rawL = new Float32Array(BANDS2);
10038
+ const rawR = new Float32Array(BANDS2);
10039
+ const left64 = new Float32Array(BANDS2);
10040
+ const right64 = new Float32Array(BANDS2);
9823
10041
  const left32 = new Float32Array(32);
9824
10042
  const right32 = new Float32Array(32);
9825
10043
  const left16 = new Float32Array(16);
@@ -9836,7 +10054,7 @@ function createSimulatedAudio(seed = 20260830) {
9836
10054
  /** 渲染器诊断:当前是否处于「静音段」 */
9837
10055
  silent: false
9838
10056
  };
9839
- function downsample(dst, src) {
10057
+ function downsample2(dst, src) {
9840
10058
  const g = src.length / dst.length;
9841
10059
  for (let i = 0; i < dst.length; i++) {
9842
10060
  let s = 0;
@@ -9863,8 +10081,8 @@ function createSimulatedAudio(seed = 20260830) {
9863
10081
  const hatV = patterns.hat[i16] * hitEnv * drumGate;
9864
10082
  const riser = buildup > 0 ? Math.pow(buildup, 3) * (0.4 + 0.6 * Math.abs(vnoise(step * 2, 7))) : 0;
9865
10083
  let levelSum = 0;
9866
- for (let i = 0; i < BANDS; i++) {
9867
- const fq = i / BANDS;
10084
+ for (let i = 0; i < BANDS2; i++) {
10085
+ const fq = i / BANDS2;
9868
10086
  const tilt = Math.pow(1 - fq * 0.85, 1.6);
9869
10087
  let v = midGate * (0.5 + 0.3 * vnoise(beat * 0.5 + phases[i] * 0.05, i % 8));
9870
10088
  v *= 0.35 + 0.65 * fq;
@@ -9888,10 +10106,10 @@ function createSimulatedAudio(seed = 20260830) {
9888
10106
  }
9889
10107
  left64.set(rawL);
9890
10108
  right64.set(rawR);
9891
- downsample(left32, rawL);
9892
- downsample(right32, rawR);
9893
- downsample(left16, rawL);
9894
- downsample(right16, rawR);
10109
+ downsample2(left32, rawL);
10110
+ downsample2(right32, rawR);
10111
+ downsample2(left16, rawL);
10112
+ downsample2(right16, rawR);
9895
10113
  snapshot.level = Math.min(1, levelSum / (48 * 1.2));
9896
10114
  snapshot.silent = silent;
9897
10115
  return snapshot;
@@ -9900,7 +10118,7 @@ function createSimulatedAudio(seed = 20260830) {
9900
10118
  update,
9901
10119
  snapshot,
9902
10120
  /** 频段基数 */
9903
- bands: BANDS
10121
+ bands: BANDS2
9904
10122
  };
9905
10123
  }
9906
10124
  function fillAudioBuffers(views, snapshot) {
@@ -9922,7 +10140,7 @@ function fillOne(dst, src64) {
9922
10140
  dst[i] = s / (i1 - i0);
9923
10141
  }
9924
10142
  }
9925
- const MEDIA_PLAYBACK = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
10143
+ const MEDIA_PLAYBACK$1 = { STOPPED: 0, PLAYING: 1, PAUSED: 2 };
9926
10144
  class MediaVec3 {
9927
10145
  constructor(x, y, z) {
9928
10146
  this.x = Number(x) || 0;
@@ -10047,7 +10265,7 @@ function createSimulatedMedia(seed = 20260901) {
10047
10265
  const cycle = tracks.reduce((s, t) => s + t.duration + GAP, 0);
10048
10266
  const snapshot = {
10049
10267
  hasMedia: false,
10050
- state: MEDIA_PLAYBACK.STOPPED,
10268
+ state: MEDIA_PLAYBACK$1.STOPPED,
10051
10269
  title: "",
10052
10270
  artist: "",
10053
10271
  album: "",
@@ -10114,10 +10332,10 @@ function createSimulatedMedia(seed = 20260901) {
10114
10332
  snapshot.duration = tr.duration;
10115
10333
  snapshot.position = pos;
10116
10334
  const frac = tr.duration > 0 ? pos / tr.duration : 0;
10117
- if (held) snapshot.state = MEDIA_PLAYBACK.PAUSED;
10118
- else if (inGap) snapshot.state = MEDIA_PLAYBACK.STOPPED;
10119
- else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK.PAUSED;
10120
- else snapshot.state = MEDIA_PLAYBACK.PLAYING;
10335
+ if (held) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10336
+ else if (inGap) snapshot.state = MEDIA_PLAYBACK$1.STOPPED;
10337
+ else if (frac > 0.7 && frac < 0.76) snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10338
+ else snapshot.state = MEDIA_PLAYBACK$1.PLAYING;
10121
10339
  snapshot.hasThumbnail = !inGap;
10122
10340
  const c = tr.colors;
10123
10341
  snapshot.primaryColor = new MediaVec3(c.primary[0], c.primary[1], c.primary[2]);
@@ -10156,7 +10374,7 @@ function createSimulatedMedia(seed = 20260901) {
10156
10374
  if (held) return snapshot;
10157
10375
  holdT = lastWall + seekOffset;
10158
10376
  held = true;
10159
- snapshot.state = MEDIA_PLAYBACK.PAUSED;
10377
+ snapshot.state = MEDIA_PLAYBACK$1.PAUSED;
10160
10378
  return snapshot;
10161
10379
  }
10162
10380
  function play() {
@@ -10173,7 +10391,7 @@ function createSimulatedMedia(seed = 20260901) {
10173
10391
  snapshot,
10174
10392
  tracks,
10175
10393
  cycle,
10176
- MEDIA_PLAYBACK,
10394
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10177
10395
  skipNext,
10178
10396
  skipPrevious,
10179
10397
  play,
@@ -10281,7 +10499,7 @@ function cloneMediaSnapshot(s) {
10281
10499
  }
10282
10500
  const mediaMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10283
10501
  __proto__: null,
10284
- MEDIA_PLAYBACK,
10502
+ MEDIA_PLAYBACK: MEDIA_PLAYBACK$1,
10285
10503
  cloneMediaSnapshot,
10286
10504
  createSimulatedMedia,
10287
10505
  diffMediaEvents,
@@ -11145,6 +11363,382 @@ const SKIP_TEXT = false;
11145
11363
  const SKIP_PARTICLES = false;
11146
11364
  const SKIP_SCENE_EFFECTS = false;
11147
11365
  const TEXT_EM_SCALE = 4;
11366
+ const BANDS = 64;
11367
+ const MEDIA_PLAYBACK = { STOPPED: 0 };
11368
+ function zeroBands() {
11369
+ return {
11370
+ left64: new Float32Array(BANDS),
11371
+ right64: new Float32Array(BANDS),
11372
+ left32: new Float32Array(32),
11373
+ right32: new Float32Array(32),
11374
+ left16: new Float32Array(16),
11375
+ right16: new Float32Array(16),
11376
+ level: 0,
11377
+ silent: true
11378
+ };
11379
+ }
11380
+ function downsample(dst, src64) {
11381
+ const g = src64.length / dst.length;
11382
+ for (let i = 0; i < dst.length; i++) {
11383
+ let s = 0;
11384
+ const i0 = Math.floor(i * g);
11385
+ const i1 = Math.max(i0 + 1, Math.floor((i + 1) * g));
11386
+ for (let j = i0; j < i1; j++) s += src64[j];
11387
+ dst[i] = s / (i1 - i0);
11388
+ }
11389
+ }
11390
+ function fillFromByteFreq(out, bytes, sampleRate) {
11391
+ const n = bytes.length;
11392
+ const nyquist = sampleRate * 0.5;
11393
+ const fMin = 20;
11394
+ const fMax = Math.min(2e4, nyquist);
11395
+ let levelSum = 0;
11396
+ for (let b = 0; b < BANDS; b++) {
11397
+ const t0 = b / BANDS;
11398
+ const t1 = (b + 1) / BANDS;
11399
+ const loHz = fMin * Math.pow(fMax / fMin, t0);
11400
+ const hiHz = fMin * Math.pow(fMax / fMin, t1);
11401
+ const i0 = Math.max(0, Math.floor(loHz / nyquist * n));
11402
+ const i1 = Math.min(n, Math.max(i0 + 1, Math.ceil(hiHz / nyquist * n)));
11403
+ let s = 0;
11404
+ for (let i = i0; i < i1; i++) s += bytes[i] / 255;
11405
+ const v = Math.min(1, s / (i1 - i0) * 1.35);
11406
+ out.left64[b] = v;
11407
+ out.right64[b] = v;
11408
+ if (b < 48) levelSum += v;
11409
+ }
11410
+ downsample(out.left32, out.left64);
11411
+ downsample(out.right32, out.right64);
11412
+ downsample(out.left16, out.left64);
11413
+ downsample(out.right16, out.right64);
11414
+ out.level = Math.min(1, levelSum / (48 * 1.2));
11415
+ out.silent = out.level < 0.02;
11416
+ }
11417
+ function hashHue(s) {
11418
+ let h = 2166136261;
11419
+ for (let i = 0; i < s.length; i++) {
11420
+ h ^= s.charCodeAt(i);
11421
+ h = Math.imul(h, 16777619);
11422
+ }
11423
+ return (h >>> 0) % 360;
11424
+ }
11425
+ function hslToRgb(h, sat, light) {
11426
+ const s = sat / 100;
11427
+ const l = light / 100;
11428
+ const c = (1 - Math.abs(2 * l - 1)) * s;
11429
+ const hp = h / 60;
11430
+ const x = c * (1 - Math.abs(hp % 2 - 1));
11431
+ let r = 0, g = 0, b = 0;
11432
+ if (hp < 1) [r, g, b] = [c, x, 0];
11433
+ else if (hp < 2) [r, g, b] = [x, c, 0];
11434
+ else if (hp < 3) [r, g, b] = [0, c, x];
11435
+ else if (hp < 4) [r, g, b] = [0, x, c];
11436
+ else if (hp < 5) [r, g, b] = [x, 0, c];
11437
+ else [r, g, b] = [c, 0, x];
11438
+ const m = l - c / 2;
11439
+ return [r + m, g + m, b + m];
11440
+ }
11441
+ function applyPalette(snap, seed) {
11442
+ const hue = hashHue(seed || "empty");
11443
+ const [pr, pg, pb] = hslToRgb(hue, 72, 48);
11444
+ const [sr, sg, sb] = hslToRgb((hue + 40) % 360, 55, 28);
11445
+ const [tr, tg, tb] = hslToRgb((hue + 20) % 360, 70, 72);
11446
+ snap.primaryColor = media.mediaVec3(pr, pg, pb);
11447
+ snap.secondaryColor = media.mediaVec3(sr, sg, sb);
11448
+ snap.tertiaryColor = media.mediaVec3(tr, tg, tb);
11449
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
11450
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
11451
+ snap.hasThumbnail = !!seed;
11452
+ }
11453
+ function sampleArtworkPalette(img, w, h) {
11454
+ const c = document.createElement("canvas");
11455
+ c.width = 32;
11456
+ c.height = 32;
11457
+ const ctx = c.getContext("2d", { willReadFrequently: true });
11458
+ if (!ctx) return null;
11459
+ ctx.drawImage(img, 0, 0, w, h, 0, 0, 32, 32);
11460
+ const data = ctx.getImageData(0, 0, 32, 32).data;
11461
+ let r = 0, g = 0, b = 0, n = 0;
11462
+ let br = 0, bg = 0, bb = 0, best = -1;
11463
+ for (let i = 0; i < data.length; i += 4) {
11464
+ const pr = data[i] / 255, pg = data[i + 1] / 255, pb = data[i + 2] / 255;
11465
+ r += pr;
11466
+ g += pg;
11467
+ b += pb;
11468
+ n++;
11469
+ const mx = Math.max(pr, pg, pb), mn = Math.min(pr, pg, pb);
11470
+ const sat = mx - mn;
11471
+ const lum = 0.2126 * pr + 0.7152 * pg + 0.0722 * pb;
11472
+ const score = sat * 1.4 + (lum > 0.15 && lum < 0.85 ? 0.3 : 0);
11473
+ if (score > best) {
11474
+ best = score;
11475
+ br = pr;
11476
+ bg = pg;
11477
+ bb = pb;
11478
+ }
11479
+ }
11480
+ if (!n) return null;
11481
+ const primary = [br, bg, bb];
11482
+ const secondary = [r / n * 0.55, g / n * 0.55, b / n * 0.55];
11483
+ const tertiary = [
11484
+ Math.min(1, primary[0] * 0.45 + 0.55),
11485
+ Math.min(1, primary[1] * 0.45 + 0.55),
11486
+ Math.min(1, primary[2] * 0.45 + 0.55)
11487
+ ];
11488
+ return { primary, secondary, tertiary };
11489
+ }
11490
+ function rasterizeArtwork(img, srcW, srcH, size = 512) {
11491
+ const c = document.createElement("canvas");
11492
+ c.width = size;
11493
+ c.height = size;
11494
+ const ctx = c.getContext("2d");
11495
+ const scale = Math.max(size / Math.max(1, srcW), size / Math.max(1, srcH));
11496
+ const dw = srcW * scale;
11497
+ const dh = srcH * scale;
11498
+ ctx.fillStyle = "#000";
11499
+ ctx.fillRect(0, 0, size, size);
11500
+ ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
11501
+ const id = ctx.getImageData(0, 0, size, size);
11502
+ return { width: size, height: size, rgba: new Uint8Array(id.data) };
11503
+ }
11504
+ function emptyMediaSnapshot() {
11505
+ return {
11506
+ hasMedia: false,
11507
+ state: MEDIA_PLAYBACK.STOPPED,
11508
+ title: "",
11509
+ artist: "",
11510
+ album: "",
11511
+ albumArtist: "",
11512
+ position: 0,
11513
+ duration: 0,
11514
+ hasThumbnail: false,
11515
+ primaryColor: media.mediaVec3(0, 0, 0),
11516
+ secondaryColor: media.mediaVec3(0, 0, 0),
11517
+ tertiaryColor: media.mediaVec3(0, 0, 0),
11518
+ textColor: media.mediaVec3(1, 1, 1),
11519
+ highContrastColor: media.mediaVec3(1, 1, 1),
11520
+ trackIndex: -1,
11521
+ lyrics: [],
11522
+ lyricLine: "",
11523
+ lyricIndex: -1
11524
+ };
11525
+ }
11526
+ async function openMicAnalyser() {
11527
+ if (!navigator.mediaDevices?.getUserMedia) return null;
11528
+ try {
11529
+ const stream = await navigator.mediaDevices.getUserMedia({
11530
+ audio: {
11531
+ echoCancellation: false,
11532
+ noiseSuppression: false,
11533
+ autoGainControl: false
11534
+ },
11535
+ video: false
11536
+ });
11537
+ const ctx = new AudioContext();
11538
+ const src = ctx.createMediaStreamSource(stream);
11539
+ const analyser = ctx.createAnalyser();
11540
+ analyser.fftSize = 2048;
11541
+ analyser.smoothingTimeConstant = 0.8;
11542
+ src.connect(analyser);
11543
+ if (ctx.state === "suspended") await ctx.resume().catch(() => {
11544
+ });
11545
+ return { ctx, stream, analyser, buf: new Uint8Array(analyser.frequencyBinCount) };
11546
+ } catch {
11547
+ return null;
11548
+ }
11549
+ }
11550
+ async function startLiveSystem(opts) {
11551
+ const origin = opts?.origin ?? (typeof location !== "undefined" ? location.origin : "");
11552
+ const onArtwork = opts?.onArtwork;
11553
+ const audioSnap = zeroBands();
11554
+ const mediaSnap = emptyMediaSnapshot();
11555
+ const winSnap = { app: "", title: "", url: "", index: 0 };
11556
+ let audioMode = "off";
11557
+ let mediaMode = "offline";
11558
+ let windowMode = "offline";
11559
+ let trackKey = "";
11560
+ let hasArtwork = false;
11561
+ let lastArtworkKey = "";
11562
+ const mic = await openMicAnalyser();
11563
+ if (mic) audioMode = "mic";
11564
+ else if (!navigator.mediaDevices?.getUserMedia) audioMode = "unavailable";
11565
+ else audioMode = "denied";
11566
+ let es = null;
11567
+ let pollTimer = null;
11568
+ let disposed = false;
11569
+ const requestArtwork = (key, title, artist) => {
11570
+ if (!origin || !onArtwork || !hasArtwork) return;
11571
+ if (lastArtworkKey === key) return;
11572
+ lastArtworkKey = key;
11573
+ onArtwork({
11574
+ url: `${origin}/api/system/artwork?k=${encodeURIComponent(key)}&_=${Date.now()}`,
11575
+ trackKey: key,
11576
+ title,
11577
+ artist
11578
+ });
11579
+ };
11580
+ const applyMediaPayload = (m) => {
11581
+ if (!m || !m.hasMedia) {
11582
+ mediaSnap.hasMedia = false;
11583
+ mediaSnap.state = MEDIA_PLAYBACK.STOPPED;
11584
+ mediaSnap.title = "";
11585
+ mediaSnap.artist = "";
11586
+ mediaSnap.album = "";
11587
+ mediaSnap.albumArtist = "";
11588
+ mediaSnap.position = 0;
11589
+ mediaSnap.duration = 0;
11590
+ mediaSnap.hasThumbnail = false;
11591
+ mediaSnap.trackIndex = -1;
11592
+ mediaMode = m ? "empty" : "offline";
11593
+ trackKey = "";
11594
+ hasArtwork = false;
11595
+ lastArtworkKey = "";
11596
+ return;
11597
+ }
11598
+ mediaMode = "live";
11599
+ mediaSnap.hasMedia = true;
11600
+ mediaSnap.state = Number(m.state) === 2 ? 2 : Number(m.state) === 1 ? 1 : 0;
11601
+ mediaSnap.title = String(m.title ?? "");
11602
+ mediaSnap.artist = String(m.artist ?? "");
11603
+ mediaSnap.album = String(m.album ?? "");
11604
+ mediaSnap.albumArtist = String(m.albumArtist ?? m.artist ?? "");
11605
+ mediaSnap.position = Number(m.position) || 0;
11606
+ mediaSnap.duration = Number(m.duration) || 0;
11607
+ hasArtwork = m.hasArtwork === true;
11608
+ const key = `${mediaSnap.title}|${mediaSnap.artist}|${mediaSnap.album}`;
11609
+ if (key !== trackKey) {
11610
+ trackKey = key;
11611
+ lastArtworkKey = "";
11612
+ mediaSnap.trackIndex = mediaSnap.trackIndex + 1 | 0;
11613
+ applyPalette(mediaSnap, key);
11614
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
11615
+ } else {
11616
+ requestArtwork(key, mediaSnap.title, mediaSnap.artist);
11617
+ }
11618
+ };
11619
+ const applyWindowPayload = (w) => {
11620
+ if (!w) {
11621
+ windowMode = "offline";
11622
+ return;
11623
+ }
11624
+ winSnap.app = String(w.app ?? "");
11625
+ winSnap.title = String(w.title ?? "");
11626
+ winSnap.url = String(w.url ?? "");
11627
+ windowMode = winSnap.app || winSnap.title ? "live" : "empty";
11628
+ };
11629
+ const pollOnce = async () => {
11630
+ if (!origin || disposed) return;
11631
+ try {
11632
+ const [mr, wr] = await Promise.all([
11633
+ fetch(`${origin}/api/system/media`, { cache: "no-store" }),
11634
+ fetch(`${origin}/api/system/window`, { cache: "no-store" })
11635
+ ]);
11636
+ if (mr.ok) {
11637
+ const j = await mr.json();
11638
+ applyMediaPayload(j);
11639
+ } else {
11640
+ mediaMode = "offline";
11641
+ }
11642
+ if (wr.ok) {
11643
+ applyWindowPayload(await wr.json());
11644
+ }
11645
+ } catch {
11646
+ mediaMode = mediaMode === "live" ? "live" : "offline";
11647
+ windowMode = windowMode === "live" ? "live" : "offline";
11648
+ }
11649
+ };
11650
+ if (origin) {
11651
+ await pollOnce();
11652
+ pollTimer = setInterval(() => void pollOnce(), 1e3);
11653
+ try {
11654
+ es = new EventSource(`${origin}/api/system/stream`);
11655
+ es.onmessage = (ev) => {
11656
+ if (disposed) return;
11657
+ try {
11658
+ const data = JSON.parse(ev.data);
11659
+ applyMediaPayload(data.media);
11660
+ applyWindowPayload(data.window);
11661
+ } catch {
11662
+ }
11663
+ };
11664
+ } catch {
11665
+ }
11666
+ }
11667
+ const postControl = (action) => {
11668
+ if (!origin || disposed) return;
11669
+ void fetch(`${origin}/api/system/media-control`, {
11670
+ method: "POST",
11671
+ headers: { "Content-Type": "application/json" },
11672
+ body: JSON.stringify({ action })
11673
+ }).then(async (r) => {
11674
+ if (!r.ok) return null;
11675
+ return r.json();
11676
+ }).then((j) => {
11677
+ if (j && typeof j === "object") applyMediaPayload(j);
11678
+ void pollOnce();
11679
+ }).catch(() => {
11680
+ void pollOnce();
11681
+ });
11682
+ };
11683
+ return {
11684
+ audio: {
11685
+ snapshot: audioSnap,
11686
+ pump: () => {
11687
+ if (!mic || disposed) {
11688
+ audioSnap.level = 0;
11689
+ audioSnap.silent = true;
11690
+ return;
11691
+ }
11692
+ mic.analyser.getByteFrequencyData(mic.buf);
11693
+ fillFromByteFreq(audioSnap, mic.buf, mic.ctx.sampleRate || 48e3);
11694
+ }
11695
+ },
11696
+ media: {
11697
+ snapshot: mediaSnap,
11698
+ pump: () => {
11699
+ },
11700
+ skipNext: () => postControl("skipNext"),
11701
+ skipPrevious: () => postControl("skipPrevious"),
11702
+ play: () => postControl("play"),
11703
+ pause: () => postControl("pause"),
11704
+ playPause: () => postControl("playPause")
11705
+ },
11706
+ windowTitle: {
11707
+ snapshot: winSnap,
11708
+ pump: () => {
11709
+ }
11710
+ },
11711
+ status: () => ({
11712
+ audio: audioMode,
11713
+ media: mediaMode,
11714
+ window: windowMode,
11715
+ title: mediaSnap.title,
11716
+ artist: mediaSnap.artist,
11717
+ app: winSnap.app,
11718
+ windowTitle: winSnap.title,
11719
+ hasArtwork
11720
+ }),
11721
+ dispose: () => {
11722
+ disposed = true;
11723
+ if (pollTimer) {
11724
+ clearInterval(pollTimer);
11725
+ pollTimer = null;
11726
+ }
11727
+ try {
11728
+ es?.close();
11729
+ } catch {
11730
+ }
11731
+ es = null;
11732
+ if (mic) {
11733
+ try {
11734
+ mic.stream.getTracks().forEach((t) => t.stop());
11735
+ void mic.ctx.close();
11736
+ } catch {
11737
+ }
11738
+ }
11739
+ }
11740
+ };
11741
+ }
11148
11742
  const WE_SHADER_HEADERS = {
11149
11743
  "common.h": `// WE common.h(重建子集,供 we-scene 浏览器渲染)
11150
11744
  #define M_PI 3.14159265359
@@ -11417,10 +12011,18 @@ mat3 squareToQuad(vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
11417
12011
  float d = p1.y - p0.y + g * p1.y;
11418
12012
  float e = p3.y - p0.y + h * p3.y;
11419
12013
  float f = p0.y;
11420
- // 行向量约定:[u v 1] * M
11421
- return mat3(a, d, g,
11422
- b, e, h,
11423
- c, f, 1.0);
12014
+ // 调用点一律是 mul(vec3(uv,1), inverse(本函数结果)),hlsl2glsl 把它转写成
12015
+ // transpose(xform) * vec3(uv,1)。要让屏幕点 s 得到 texCoord = S⁻¹·s(S 为
12016
+ // Heckbert 正向矩阵 [[a,b,c],[d,e,f],[g,h,1]],单位方→四边形),必须
12017
+ // xform = inverse(本函数结果) 满足 transpose(xform)·s = S⁻¹·s,
12018
+ // 即本函数返回 S 的**转置**:mat3 列主序构造为 (a,d,g)(b,e,h)(c,f,1) 的转置
12019
+ // = (a,b,c)(d,e,f)(g,h,1)。排布差一个转置,perspective/水波等全部错位——
12020
+ // 3174556087 的音谱柱被贴到窗户侧边竖排(应为贴下窗沿横排)实测确认。
12021
+ // 2026-09-05 数值模拟:旧排布 transpose(S⁻¹)·corner 与正确 S⁻¹·corner 逐项不同,
12022
+ // 可见区塌缩成一条斜带;新排布后中心 (0.5,0.5) → (0.478,0.511) ∈ [0,1]²。
12023
+ return mat3(a, b, c,
12024
+ d, e, f,
12025
+ g, h, 1.0);
11424
12026
  }
11425
12027
  `,
11426
12028
  // WE common_blur.h(重建)。blurNa 的权重不是估算的 —— 壁纸 1444077782 里存着
@@ -11550,6 +12152,66 @@ vec3 DecompressNormal(vec4 tex) {
11550
12152
  "common_vertex.h": `// WE common_vertex.h(重建:空占位,见 headers.ts 注释)
11551
12153
  `
11552
12154
  };
12155
+ function sanitizeFontForBrowser(src) {
12156
+ if (!src || src.length < 12) return src;
12157
+ const b0 = src[0], b1 = src[1], b2 = src[2], b3 = src[3];
12158
+ const isTtf = b0 === 0 && b1 === 1 && b2 === 0 && b3 === 0;
12159
+ const isOtto = b0 === 79 && b1 === 84 && b2 === 84 && b3 === 79;
12160
+ if (!isTtf && !isOtto) return src;
12161
+ const out = Uint8Array.from(src);
12162
+ const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
12163
+ const numTables = dv.getUint16(4);
12164
+ if (numTables <= 0 || 12 + numTables * 16 > out.length) return src;
12165
+ let cmapEntry = -1;
12166
+ let cmapOffset = 0;
12167
+ let cmapLength = 0;
12168
+ for (let i = 0; i < numTables; i++) {
12169
+ const e = 12 + i * 16;
12170
+ const tag = String.fromCharCode(out[e], out[e + 1], out[e + 2], out[e + 3]);
12171
+ if (tag === "cmap") {
12172
+ cmapEntry = e;
12173
+ cmapOffset = dv.getUint32(e + 8);
12174
+ cmapLength = dv.getUint32(e + 12);
12175
+ break;
12176
+ }
12177
+ }
12178
+ if (cmapEntry < 0 || cmapOffset + cmapLength > out.length) return src;
12179
+ let changed = false;
12180
+ const numEnc = dv.getUint16(cmapOffset + 2);
12181
+ for (let i = 0; i < numEnc; i++) {
12182
+ const rec = cmapOffset + 4 + i * 8;
12183
+ const soff = dv.getUint32(rec + 4);
12184
+ const abs = cmapOffset + soff;
12185
+ if (abs + 14 > out.length) continue;
12186
+ if (dv.getUint16(abs) !== 4) continue;
12187
+ const segCountX2 = dv.getUint16(abs + 6);
12188
+ const segCount = segCountX2 >>> 1;
12189
+ if (segCount < 1) continue;
12190
+ const expSearch = 2 * Math.pow(2, Math.floor(Math.log2(segCount)));
12191
+ const expSel = Math.floor(Math.log2(segCount));
12192
+ const expShift = segCountX2 - expSearch;
12193
+ const curSearch = dv.getUint16(abs + 8);
12194
+ const curSel = dv.getUint16(abs + 10);
12195
+ const curShift = dv.getUint16(abs + 12);
12196
+ if (curSearch === expSearch && curSel === expSel && curShift === expShift) continue;
12197
+ dv.setUint16(abs + 8, expSearch);
12198
+ dv.setUint16(abs + 10, expSel);
12199
+ dv.setUint16(abs + 12, expShift);
12200
+ changed = true;
12201
+ }
12202
+ if (!changed) return src;
12203
+ let sum = 0;
12204
+ const end = cmapOffset + cmapLength;
12205
+ for (let p = cmapOffset; p < end; p += 4) {
12206
+ const b02 = out[p] || 0;
12207
+ const b12 = p + 1 < end ? out[p + 1] : 0;
12208
+ const b22 = p + 2 < end ? out[p + 2] : 0;
12209
+ const b32 = p + 3 < end ? out[p + 3] : 0;
12210
+ sum = sum + (b02 << 24 | b12 << 16 | b22 << 8 | b32) >>> 0;
12211
+ }
12212
+ dv.setUint32(cmapEntry + 4, sum);
12213
+ return out;
12214
+ }
11553
12215
  const SYSTEM_FONT_FAMILIES = {
11554
12216
  systemfont_segoe: "'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
11555
12217
  systemfont_arial: "Arial, 'Helvetica Neue', sans-serif",
@@ -11751,6 +12413,22 @@ function mountScene(rt, cfg) {
11751
12413
  if (disposed) return;
11752
12414
  const supportsAudioProcessing = project?.general?.supportsaudioprocessing !== false;
11753
12415
  const simAudio = createSimulatedAudio();
12416
+ const simMedia = media.createSimulatedMedia();
12417
+ const simWindow = system.createSimulatedWindowTitle();
12418
+ let live = null;
12419
+ const liveHold = {
12420
+ mediaDriver: null,
12421
+ lastSnap: {
12422
+ get: () => null,
12423
+ setHasThumbnail: () => {
12424
+ }
12425
+ }
12426
+ };
12427
+ const audioDriverRef = {
12428
+ current: null
12429
+ };
12430
+ let mediaDriver = simMedia;
12431
+ let windowDriver = simWindow;
11754
12432
  const audioSim = { enabled: supportsAudioProcessing };
11755
12433
  const zero = (n) => new Float32Array(n);
11756
12434
  const SILENT_AUDIO = {
@@ -11763,32 +12441,45 @@ function mountScene(rt, cfg) {
11763
12441
  level: 0,
11764
12442
  silent: true
11765
12443
  };
11766
- renderer.setAudioProvider(() => audioSim.enabled ? simAudio.snapshot : SILENT_AUDIO);
12444
+ renderer.setAudioProvider(() => {
12445
+ if (!audioSim.enabled) return SILENT_AUDIO;
12446
+ return audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot;
12447
+ });
11767
12448
  const audioViews = /* @__PURE__ */ new Map();
11768
12449
  window.__audioStats = () => ({
11769
12450
  enabled: audioSim.enabled,
11770
- level: audioSim.enabled ? Math.round(simAudio.snapshot.level * 1e3) / 1e3 : 0,
11771
- silent: audioSim.enabled ? simAudio.snapshot.silent : true,
11772
- bass: audioSim.enabled ? Math.round(simAudio.snapshot.left64[2] * 1e3) / 1e3 : 0
12451
+ live: !!audioDriverRef.current,
12452
+ level: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).level * 1e3) / 1e3 : 0,
12453
+ silent: audioSim.enabled ? (audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).silent : true,
12454
+ bass: audioSim.enabled ? Math.round((audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot).left64[2] * 1e3) / 1e3 : 0
11773
12455
  });
11774
12456
  window.__audioMute = (on) => {
11775
12457
  audioSim.enabled = !on;
11776
12458
  return audioSim.enabled;
11777
12459
  };
11778
- reportDiag(rt, cfg, `audio: simulated stream, supportsaudioprocessing=${supportsAudioProcessing}`);
11779
- const simMedia = media.createSimulatedMedia();
11780
- const simWindow = system.createSimulatedWindowTitle();
12460
+ reportDiag(
12461
+ rt,
12462
+ cfg,
12463
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
12464
+ );
11781
12465
  const shortcuts = system.createShortcutHandler((name) => {
11782
12466
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
11783
12467
  });
11784
12468
  const mediaSim = { enabled: true, override: null };
11785
12469
  const mediaHooks = [];
11786
12470
  let lastMediaSnap = null;
12471
+ liveHold.lastSnap = {
12472
+ get: () => lastMediaSnap,
12473
+ setHasThumbnail: (v) => {
12474
+ if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
12475
+ }
12476
+ };
12477
+ const mediaSnapshot = () => mediaDriver.snapshot;
11787
12478
  const registerMediaHook = (sb) => {
11788
12479
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
11789
12480
  mediaHooks.push(sb);
11790
- if (!simMedia.snapshot.hasMedia) return;
11791
- for (const { name, event } of media.diffMediaEvents(null, simMedia.snapshot)) {
12481
+ if (!mediaSnapshot().hasMedia) return;
12482
+ for (const { name, event } of media.diffMediaEvents(null, mediaSnapshot())) {
11792
12483
  try {
11793
12484
  sb.callMedia(name, event);
11794
12485
  } catch {
@@ -11797,26 +12488,27 @@ function mountScene(rt, cfg) {
11797
12488
  };
11798
12489
  window.__mediaStats = () => ({
11799
12490
  enabled: mediaSim.enabled,
12491
+ live: !!live,
11800
12492
  hooks: mediaHooks.length,
11801
- title: simMedia.snapshot.title,
11802
- artist: simMedia.snapshot.artist,
11803
- album: simMedia.snapshot.album,
11804
- state: simMedia.snapshot.state,
11805
- position: Math.round(simMedia.snapshot.position),
11806
- duration: simMedia.snapshot.duration,
11807
- hasThumbnail: simMedia.snapshot.hasThumbnail,
11808
- lyric: simMedia.snapshot.lyricLine,
11809
- primaryColor: simMedia.snapshot.primaryColor ? [simMedia.snapshot.primaryColor.x, simMedia.snapshot.primaryColor.y, simMedia.snapshot.primaryColor.z] : null
12493
+ title: mediaSnapshot().title,
12494
+ artist: mediaSnapshot().artist,
12495
+ album: mediaSnapshot().album,
12496
+ state: mediaSnapshot().state,
12497
+ position: Math.round(mediaSnapshot().position),
12498
+ duration: mediaSnapshot().duration,
12499
+ hasThumbnail: mediaSnapshot().hasThumbnail,
12500
+ lyric: mediaSnapshot().lyricLine,
12501
+ primaryColor: mediaSnapshot().primaryColor ? [mediaSnapshot().primaryColor.x, mediaSnapshot().primaryColor.y, mediaSnapshot().primaryColor.z] : null
11810
12502
  });
11811
12503
  window.__mediaSet = (patch) => {
11812
- Object.assign(simMedia.snapshot, patch || {});
11813
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
12504
+ Object.assign(mediaSnapshot(), patch || {});
12505
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11814
12506
  for (const { name, event } of evts) for (const sb of mediaHooks) sb.callMedia(name, event);
11815
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
12507
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11816
12508
  return window.__mediaStats();
11817
12509
  };
11818
12510
  const dispatchMediaNow = () => {
11819
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
12511
+ const evts = media.diffMediaEvents(lastMediaSnap, mediaSnapshot());
11820
12512
  for (const { name, event } of evts) {
11821
12513
  for (const sb of mediaHooks) {
11822
12514
  try {
@@ -11825,42 +12517,50 @@ function mountScene(rt, cfg) {
11825
12517
  }
11826
12518
  }
11827
12519
  }
11828
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
12520
+ lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
11829
12521
  };
11830
12522
  const mediaControl = {
11831
- snapshot: simMedia.snapshot,
12523
+ get snapshot() {
12524
+ return mediaSnapshot();
12525
+ },
11832
12526
  skipNext: () => {
11833
- simMedia.skipNext();
12527
+ mediaDriver.skipNext();
11834
12528
  dispatchMediaNow();
11835
- return simMedia.snapshot;
12529
+ return mediaSnapshot();
11836
12530
  },
11837
12531
  skipPrevious: () => {
11838
- simMedia.skipPrevious();
12532
+ mediaDriver.skipPrevious();
11839
12533
  dispatchMediaNow();
11840
- return simMedia.snapshot;
12534
+ return mediaSnapshot();
11841
12535
  },
11842
12536
  play: () => {
11843
- simMedia.play();
12537
+ mediaDriver.play();
11844
12538
  dispatchMediaNow();
11845
- return simMedia.snapshot;
12539
+ return mediaSnapshot();
11846
12540
  },
11847
12541
  pause: () => {
11848
- simMedia.pause();
12542
+ mediaDriver.pause();
11849
12543
  dispatchMediaNow();
11850
- return simMedia.snapshot;
12544
+ return mediaSnapshot();
11851
12545
  },
11852
12546
  playPause: () => {
11853
- simMedia.playPause();
12547
+ mediaDriver.playPause();
11854
12548
  dispatchMediaNow();
11855
- return simMedia.snapshot;
12549
+ return mediaSnapshot();
11856
12550
  }
11857
12551
  };
11858
12552
  window.__mediaControl = mediaControl;
11859
12553
  window.__system = {
11860
12554
  media: mediaControl,
11861
- windowTitle: simWindow.snapshot,
11862
- shortcuts: shortcuts.last
12555
+ windowTitle: windowDriver.snapshot,
12556
+ shortcuts: shortcuts.last,
12557
+ live: null
11863
12558
  };
12559
+ window.__liveSystem = () => ({
12560
+ audio: "off",
12561
+ media: "offline",
12562
+ window: "offline"
12563
+ });
11864
12564
  const pointerSrc = pointerLib.createPointerSource(
11865
12565
  cfg.canvas ? {
11866
12566
  target: cfg.canvas,
@@ -11964,6 +12664,118 @@ function mountScene(rt, cfg) {
11964
12664
  textures.set("$mediaPreviousThumbnail", mkThumb(tracks[tracks.length - 1]));
11965
12665
  }
11966
12666
  }
12667
+ if (cfg.liveSystem) {
12668
+ try {
12669
+ const uploadLiveArtwork = async (info) => {
12670
+ try {
12671
+ const res = await fetch(info.url, { cache: "no-store" });
12672
+ if (!res.ok) return;
12673
+ const blob = await res.blob();
12674
+ const bmp = await createImageBitmap(blob);
12675
+ const raster = rasterizeArtwork(bmp, bmp.width, bmp.height, 512);
12676
+ const palette = sampleArtworkPalette(bmp, bmp.width, bmp.height);
12677
+ bmp.close?.();
12678
+ const cur = textures.get("$mediaThumbnail");
12679
+ if (cur) textures.set("$mediaPreviousThumbnail", cur);
12680
+ const gl = renderer.gl;
12681
+ const existing = textures.get("$mediaThumbnail");
12682
+ if (existing?.glTex) {
12683
+ gl.bindTexture(gl.TEXTURE_2D, existing.glTex);
12684
+ gl.texImage2D(
12685
+ gl.TEXTURE_2D,
12686
+ 0,
12687
+ gl.RGBA,
12688
+ raster.width,
12689
+ raster.height,
12690
+ 0,
12691
+ gl.RGBA,
12692
+ gl.UNSIGNED_BYTE,
12693
+ raster.rgba
12694
+ );
12695
+ existing.width = raster.width;
12696
+ existing.height = raster.height;
12697
+ existing.mips = [raster];
12698
+ } else {
12699
+ textures.set("$mediaThumbnail", {
12700
+ glTex: rnd.makeTextureMip(gl, [raster], false),
12701
+ width: raster.width,
12702
+ height: raster.height,
12703
+ rg88: false,
12704
+ mips: [raster],
12705
+ generated: true
12706
+ });
12707
+ }
12708
+ const snap = mediaDriver.snapshot;
12709
+ if (palette) {
12710
+ snap.primaryColor = media.mediaVec3(...palette.primary);
12711
+ snap.secondaryColor = media.mediaVec3(...palette.secondary);
12712
+ snap.tertiaryColor = media.mediaVec3(...palette.tertiary);
12713
+ snap.textColor = media.mediaVec3(0.98, 0.98, 1);
12714
+ snap.highContrastColor = media.mediaVec3(1, 1, 1);
12715
+ }
12716
+ snap.hasThumbnail = true;
12717
+ liveHold.lastSnap.setHasThumbnail(false);
12718
+ reportDiag(rt, cfg, `liveSystem: artwork ${info.title || info.trackKey}`);
12719
+ } catch (e) {
12720
+ reportDiag(
12721
+ rt,
12722
+ cfg,
12723
+ `liveSystem: artwork 失败 (${e instanceof Error ? e.message : e})`
12724
+ );
12725
+ }
12726
+ };
12727
+ live = await startLiveSystem({
12728
+ origin: location.origin,
12729
+ onArtwork: (info) => {
12730
+ void uploadLiveArtwork(info);
12731
+ }
12732
+ });
12733
+ mediaDriver = live.media;
12734
+ windowDriver = live.windowTitle;
12735
+ liveHold.mediaDriver = live.media;
12736
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
12737
+ if (mediaDriver.snapshot.hasMedia) {
12738
+ for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
12739
+ for (const sb of mediaHooks) {
12740
+ try {
12741
+ sb.callMedia(name, event);
12742
+ } catch {
12743
+ }
12744
+ }
12745
+ }
12746
+ lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
12747
+ }
12748
+ const st = live.status();
12749
+ reportDiag(
12750
+ rt,
12751
+ cfg,
12752
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
12753
+ );
12754
+ reportDiag(
12755
+ rt,
12756
+ cfg,
12757
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
12758
+ );
12759
+ window.__system = {
12760
+ media: mediaControl,
12761
+ windowTitle: windowDriver.snapshot,
12762
+ shortcuts: shortcuts.last,
12763
+ live: () => live.status()
12764
+ };
12765
+ window.__liveSystem = () => live.status();
12766
+ } catch (e) {
12767
+ reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
12768
+ live = null;
12769
+ }
12770
+ }
12771
+ {
12772
+ const prevCleanup = particleCleanup;
12773
+ particleCleanup = () => {
12774
+ prevCleanup?.();
12775
+ live?.dispose();
12776
+ live = null;
12777
+ };
12778
+ }
11967
12779
  const texInflight = /* @__PURE__ */ new Map();
11968
12780
  const loadTexInner = async (name) => {
11969
12781
  if (textures.has(name)) return textures.get(name);
@@ -12155,6 +12967,13 @@ function mountScene(rt, cfg) {
12155
12967
  model = JSON.parse(readText(modelEntry));
12156
12968
  }
12157
12969
  scn.applySolidFromModel(layer, model);
12970
+ const instUt = layer.srcObject?.instance?.usertextures;
12971
+ const instUtName = instUt?.[0] && typeof instUt[0].name === "string" && instUt[0].name.startsWith("$") ? instUt[0].name : null;
12972
+ const instBoundTex = instUtName && textures.has(instUtName) ? instUtName : null;
12973
+ if (instBoundTex) {
12974
+ layer.textureName = instBoundTex;
12975
+ layer.solid = false;
12976
+ }
12158
12977
  if (model && typeof model === "object" && "width" in model && "height" in model) {
12159
12978
  const m = model;
12160
12979
  if ((layer.size?.[0] || 0) === 0 && (layer.size?.[1] || 0) === 0 && m.width > 0 && m.height > 0) {
@@ -12195,7 +13014,7 @@ function mountScene(rt, cfg) {
12195
13014
  texJobs.push(
12196
13015
  loadTex(tn).then((entry) => {
12197
13016
  if (!entry) return;
12198
- if (si === 0) {
13017
+ if (si === 0 && !instBoundTex) {
12199
13018
  layer.textureName = tn;
12200
13019
  loadedTex++;
12201
13020
  if (entry.videoCtl) layer.videoCtl = entry.videoCtl;
@@ -12266,7 +13085,10 @@ function mountScene(rt, cfg) {
12266
13085
  height: gen.height,
12267
13086
  rg88: false,
12268
13087
  mips: [gen],
12269
- generated: true
13088
+ generated: true,
13089
+ // 内置贴图的帧表(rain1/rain2 的 1×4 图集):randomframe 预设依赖它
13090
+ // 随机取帧,缺了就整图采样画出超长丝(1823900922)。
13091
+ frames: ptex.builtinParticleFrames(name) ?? void 0
12270
13092
  };
12271
13093
  textures.set(name, entry);
12272
13094
  builtinTexCount++;
@@ -12472,8 +13294,8 @@ function mountScene(rt, cfg) {
12472
13294
  if (particleDiagFrame < 2) {
12473
13295
  particleDiagFrame++;
12474
13296
  if (particleDiagFrame === 2) {
12475
- const live = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
12476
- reportDiag(rt, cfg, `particles live: ${live} across ${particleSystems.length} systems`);
13297
+ const live2 = particleSystems.reduce((s, ps) => s + ps.liveCount(), 0);
13298
+ reportDiag(rt, cfg, `particles live: ${live2} across ${particleSystems.length} systems`);
12477
13299
  }
12478
13300
  }
12479
13301
  },
@@ -12492,7 +13314,7 @@ function mountScene(rt, cfg) {
12492
13314
  `particles: ${particleSystems.length} systems, ${builtinTexCount} builtin tex generated`
12493
13315
  );
12494
13316
  window.__particleStats = () => particleSystems.map((ps) => {
12495
- let live = 0;
13317
+ let live2 = 0;
12496
13318
  let minX = Infinity;
12497
13319
  let maxX = -Infinity;
12498
13320
  let minY = Infinity;
@@ -12501,7 +13323,7 @@ function mountScene(rt, cfg) {
12501
13323
  let maxS = -Infinity;
12502
13324
  for (const p of ps.pool) {
12503
13325
  if (!p.alive) continue;
12504
- live++;
13326
+ live2++;
12505
13327
  const px = ps.originX + p.x * ps.scaleX;
12506
13328
  const py = ps.originY + p.y * ps.scaleY;
12507
13329
  if (px < minX) minX = px;
@@ -12513,13 +13335,13 @@ function mountScene(rt, cfg) {
12513
13335
  if (s > maxS) maxS = s;
12514
13336
  }
12515
13337
  return {
12516
- live,
13338
+ live: live2,
12517
13339
  max: ps.maxCount,
12518
13340
  blend: ps.blend,
12519
13341
  renderer: ps.renderers.map((r) => r.kind).join("+"),
12520
13342
  origin: [Math.round(ps.originX), Math.round(ps.originY)],
12521
- bbox: live ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
12522
- size: live ? [Math.round(minS), Math.round(maxS)] : null
13343
+ bbox: live2 ? [Math.round(minX), Math.round(minY), Math.round(maxX), Math.round(maxY)] : null,
13344
+ size: live2 ? [Math.round(minS), Math.round(maxS)] : null
12523
13345
  };
12524
13346
  });
12525
13347
  window.__particleToggle = (on, onlyIndex) => {
@@ -12685,8 +13507,11 @@ function mountScene(rt, cfg) {
12685
13507
  try {
12686
13508
  const fe = pkg.getEntry(parsedPkg, fp);
12687
13509
  if (!fe) continue;
13510
+ const bytes = sanitizeFontForBrowser(
13511
+ fe instanceof Uint8Array ? fe : new Uint8Array(fe)
13512
+ );
12688
13513
  const fam = "wefont_" + fp.split("/").pop().replace(/[^a-zA-Z0-9]/g, "_");
12689
- const url = URL.createObjectURL(new Blob([fe]));
13514
+ const url = URL.createObjectURL(new Blob([bytes]));
12690
13515
  const ff = new FontFace(fam, `url(${url})`);
12691
13516
  await ff.load();
12692
13517
  document.fonts.add(ff);
@@ -12730,7 +13555,7 @@ function mountScene(rt, cfg) {
12730
13555
  // 与对象/效果开关/常量/general 统一走 engineTimers(P1-1)。
12731
13556
  ...timerOpts,
12732
13557
  mediaControl,
12733
- windowTitle: simWindow.snapshot,
13558
+ windowTitle: windowDriver.snapshot,
12734
13559
  openUserShortcut: shortcuts.openUserShortcut,
12735
13560
  getLayerText: (name) => textLayerText.get(name),
12736
13561
  onError: (e) => {
@@ -12978,7 +13803,7 @@ function mountScene(rt, cfg) {
12978
13803
  return clone;
12979
13804
  },
12980
13805
  mediaControl,
12981
- windowTitle: simWindow.snapshot,
13806
+ windowTitle: windowDriver.snapshot,
12982
13807
  openUserShortcut: shortcuts.openUserShortcut,
12983
13808
  isScreensaver: false
12984
13809
  };
@@ -13005,8 +13830,8 @@ function mountScene(rt, cfg) {
13005
13830
  const ctrl = anim.createAnimation(def.animation);
13006
13831
  ctrl.field = field;
13007
13832
  ctrl.baseValue = def.value;
13008
- const live = layer[field];
13009
- ctrl.baseNumeric = Array.isArray(live) ? live.slice() : live;
13833
+ const live2 = layer[field];
13834
+ ctrl.baseNumeric = Array.isArray(live2) ? live2.slice() : live2;
13010
13835
  layer.animationList.push(ctrl);
13011
13836
  if (ctrl.name) layer.animations[ctrl.name] = ctrl;
13012
13837
  animRuns.push({ layer, field, ctrl });
@@ -13223,8 +14048,10 @@ function mountScene(rt, cfg) {
13223
14048
  const t = (now - start - pauseAccum) / 1e3;
13224
14049
  inputView.update(pointerSrc.state);
13225
14050
  if (mediaSim.enabled) {
13226
- simMedia.update(t);
13227
- const evts = media.diffMediaEvents(lastMediaSnap, simMedia.snapshot);
14051
+ if (live?.media) live.media.pump();
14052
+ else simMedia.update(t);
14053
+ const snap = mediaSnapshot();
14054
+ const evts = media.diffMediaEvents(lastMediaSnap, snap);
13228
14055
  if (evts.length) {
13229
14056
  for (const { name, event } of evts) {
13230
14057
  for (const sb of mediaHooks) {
@@ -13232,10 +14059,11 @@ function mountScene(rt, cfg) {
13232
14059
  sb.callMedia(name, event);
13233
14060
  }
13234
14061
  }
13235
- lastMediaSnap = media.cloneMediaSnapshot(simMedia.snapshot);
14062
+ lastMediaSnap = media.cloneMediaSnapshot(snap);
13236
14063
  }
13237
14064
  }
13238
- simWindow.update(t);
14065
+ if (live?.windowTitle) live.windowTitle.pump();
14066
+ else simWindow.update(t);
13239
14067
  for (const run of animRuns) {
13240
14068
  run.ctrl.advance(interval / 1e3);
13241
14069
  const field = run.field;
@@ -13303,8 +14131,12 @@ function mountScene(rt, cfg) {
13303
14131
  }
13304
14132
  if (visibilityDirty) recomputeVisibility();
13305
14133
  if (audioSim.enabled) {
13306
- simAudio.update(t);
13307
- fillAudioBuffers(audioViews, simAudio.snapshot);
14134
+ if (audioDriverRef.current) audioDriverRef.current.pump();
14135
+ else simAudio.update(t);
14136
+ fillAudioBuffers(
14137
+ audioViews,
14138
+ audioDriverRef.current ? audioDriverRef.current.snapshot : simAudio.snapshot
14139
+ );
13308
14140
  }
13309
14141
  if (attachFollows.length) {
13310
14142
  mdl.followAttachments(attachFollows, t, getBoneOverrides);