webwallgl 1.1.0 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -36,12 +36,12 @@ import { mount, httpSource } from "webwallgl";
36
36
 
37
37
  ```
38
38
  // 2) ESM CDN via jsDelivr (without a bundler)
39
- import { mount, httpSource } from "https://cdn.jsdelivr.net/npm/webwallgl@1.1.0/webwallgl.min.mjs";
39
+ import { mount, httpSource } from "https://cdn.jsdelivr.net/npm/webwallgl@1.3.5/webwallgl.min.mjs";
40
40
  ```
41
41
 
42
42
  ```
43
43
  <!-- 3) UMD <script>: exposes the global WebWallGL -->
44
- <script src="https://cdn.jsdelivr.net/npm/webwallgl@1.1.0/webwallgl.global.min.js"></script>
44
+ <script src="https://cdn.jsdelivr.net/npm/webwallgl@1.3.5/webwallgl.global.min.js"></script>
45
45
  <script>
46
46
  const { mount, httpSource } = WebWallGL;
47
47
  </script>
@@ -141,6 +141,11 @@ input.addEventListener("change", () => {
141
141
  | `setRenderDpr(dpr)` | Changing DPR rebuilds the canvas; remounts internally (pkg cache hit, no re-download) |
142
142
  | `setProperties(props)` | Live property updates: patches the property table / effect constants / script sandboxes in place, no re-fetch |
143
143
  | `getProperties()` | The current flattened property value map |
144
+ | `setAudio(src)` | Swap the audio spectrum source (pull model, once per frame); null falls back to the built-in sim. Survives scene changes. Works for scene and web |
145
+ | `setMedia(src)` | Swap the system media source (Now Playing); shared by scene and web, survives scene changes |
146
+ | `media` | Media control surface: read snapshot, plus skipNext / skipPrevious / play / pause / playPause transport control |
147
+ | `pushPointer(u, v, buttons?)` | Inject pointer state (u/v normalized 0..1). For hosts whose window cannot receive the mouse; works for scene and web |
148
+ | `pointerLeave()` | Pointer left: clears buttons but keeps the last position (dropping it makes parallax and xray visibly jump) |
144
149
  | `load(source)` | Switch scenes reusing the same canvas and WebGL context; resolves after the first frame |
145
150
  | `release() / restore()` | Free GL resources keeping the config (display sleep) / rebuild from the kept config |
146
151
  | `destroy()` | Terminal: frees resources, unbinds listeners; the instance is dead afterwards |
@@ -173,6 +178,102 @@ wp.setProperties({ schemecolor: "0.5 0.2 0.8", newproperty12: false });
173
178
  - setProperties() patches in place — property table, effect constants and script sandboxes — with no re-fetch and no re-parse
174
179
  - Wallpapers without a project.json still run: the property table is empty and fields fall back to the scene.json snapshot values
175
180
 
181
+ ## Three wallpaper types: scene / video / web
182
+
183
+ You never branch on type yourself: mount() reads project.json first and routes on its type — web goes to a sandboxed iframe, video / gif / image take the media path, everything else goes through scene assembly. All three share one mount() and one SceneInstance; pause/resume, setVolume, stats and friends work the same way.
184
+
185
+ ```
186
+ // One code path for any type (pass a container div — see the previous section)
187
+ const wp = await mount(document.querySelector("#wp"), {
188
+ source: httpSource("https://cdn.example.com/wallpapers/3789109327"),
189
+ });
190
+ console.log(wp.info); // { width, height, layerCount, ... }
191
+ ```
192
+
193
+ - A video wallpaper's asset URL comes from Source.mediaEntry(); httpSource implements it (reads project.json's file field and joins {base}/{file})
194
+ - For arbitrary video/images use mediaSource(): mediaSource(url) or mediaSource(file) — the type is inferred from the extension, no project.json needed
195
+ - An explicit project.type always wins; sniffing only fills in when it is absent — some scene wallpapers point project.file at an .mp4 (a video texture inside the scene, not "this wallpaper is a video")
196
+ - Media wallpapers need WebGL2; when it is unavailable the failure arrives via onError for you to handle — the library does not silently switch to DOM rendering
197
+ - pause/resume, setVolume, setFps and setFit all work on media wallpapers too (setVolume drives the &lt;video> element's volume and muted directly)
198
+
199
+ ```
200
+ import { mount, mediaSource } from "webwallgl";
201
+
202
+ // Remote video: type inferred from the extension; a signed URL's ?query is stripped correctly
203
+ await mount(box, { source: mediaSource("https://cdn/clip.mp4?token=…") });
204
+
205
+ // Local import: a video/image from drag & drop or <input type=file>
206
+ input.addEventListener("change", async () => {
207
+ const wp = await mount(box, { source: mediaSource(input.files[0]) });
208
+ // destroy() revokes the objectURL the library created internally
209
+ });
210
+
211
+ // Specify explicitly when the extension is unreliable
212
+ mediaSource(streamUrl, { type: "video" });
213
+ ```
214
+
215
+ ## System media (Now Playing) & transport control
216
+
217
+ "Now Playing" wallpapers read the title, artist, progress, cover palette and lyrics; some also have previous/next/play-pause buttons. All of it comes from a single MediaSource — **scene and web wallpapers share one instance**, so the host maintains a single driver and both wallpaper types see the same data.
218
+
219
+ ```
220
+ import { mount, createMediaSource } from "webwallgl";
221
+
222
+ // Supply only what you have; the library fills in palette, lyric line and trackIndex
223
+ const media = createMediaSource(
224
+ { title: "Night Star", artist: "Phase Shift", playing: true,
225
+ position: 30, duration: 212,
226
+ lyrics: [[0, "first line"], [20, "second line"]] },
227
+ // Transport control: wallpaper buttons call these; forward them to the real player
228
+ { skipNext: () => player.next(),
229
+ playPause: () => player.toggle() },
230
+ );
231
+
232
+ const wp = await mount(box, { source, media });
233
+
234
+ // Update when the system's Now Playing changes (the lyric line re-resolves from position)
235
+ media.set({ title: "Next Track", position: 0 });
236
+
237
+ // You can also install / swap / remove it after mounting
238
+ wp.setMedia(media);
239
+ wp.setMedia(null); // fall back to the built-in simulation
240
+
241
+ // The host can read the snapshot and issue transport commands too
242
+ console.log(wp.media.snapshot.title);
243
+ wp.media.playPause();
244
+ ```
245
+
246
+ - The five palette fields must be chainable color objects (scripts write c.subtract(o).multiply(t).add(o); a plain array throws a TypeError that kills the whole script) — createMediaSource guarantees this for you
247
+ - All transport methods are optional: if you only provide metadata, wallpaper buttons are silently inert rather than throwing
248
+ - setMedia survives scene changes: install once and it applies to every scene loaded afterwards
249
+ - For video wallpapers the audio spectrum is captured from the &lt;video> automatically (visualizers react to the video's own audio); an explicit setAudio() injection takes precedence
250
+
251
+ ## Injecting pointer & audio
252
+
253
+ A wallpaper host often cannot rely on the browser's native input: desktop wallpapers sit in the desktop underlay layer where the system's desktop window swallows mouse events, and the audio spectrum has to be captured by the host itself. Both channels are fed through instance methods.
254
+
255
+ ```
256
+ // Pointer: u/v are normalized 0..1; buttons matches MouseEvent.buttons
257
+ wp.pushPointer(0.5, 0.5, 0); // hover at the center
258
+ wp.pushPointer(0.5, 0.5, 1); // press the left button
259
+ wp.pointerLeave(); // pointer left (clears buttons, keeps last position)
260
+
261
+ // Audio: pull model — the render loop calls snapshot() once per frame
262
+ let latest = { left: new Float32Array(64), right: new Float32Array(64) };
263
+ wp.setAudio({ snapshot: () => latest });
264
+
265
+ // e.g. update `latest` from the host's spectrum stream
266
+ evtSource.onmessage = (e) => { latest = JSON.parse(e.data); };
267
+
268
+ wp.setAudio(null); // remove the source, fall back to the built-in sim
269
+ ```
270
+
271
+ - Injected pointer state coexists with the canvas's own DOM listeners — last writer wins. It works for scene and web wallpapers; media wallpapers have no pointer concept, so the call is silently inert
272
+ - Audio contract: 64 bands per channel, values 0..1. Short arrays are zero-padded and long ones truncated; the 32/16-band downsamples plus level and silence detection are derived by the library
273
+ - Returning null (or throwing) from snapshot() means "no data this frame" and the engine falls back to the built-in simulation — no special handling needed while host capture is still warming up
274
+ - setAudio survives scene changes: install it once and it applies to every scene loaded afterwards
275
+ - Audio injection works for both scene and web wallpapers: the web side receives the same data through the iframe shim audio pump, and both pumps pick their source per frame so calling setAudio after mount() works too
276
+
176
277
  ## Events & diagnostics
177
278
 
178
279
  ```
@@ -220,14 +321,73 @@ b.pause(); // does not affect a
220
321
 
221
322
  ## Changelog
222
323
 
223
- Current version: 1.1.0. This section records only user-visible changes (API, behavior, compatibility, fidelity), each backed by a commit in the repository; pure internal refactors and verifier scripts are omitted.
324
+ Current version: 1.3.5. This section records only user-visible changes (API, behavior, compatibility, fidelity), each backed by a commit in the repository; pure internal refactors and verifier scripts are omitted.
224
325
 
225
326
  | Version | Date | Notes |
226
327
  | --- | --- | --- |
328
+ | `1.3.5` | 2026-09-08 | xray effect: when the author doesn't configure size in the scene, the fallback is now 1 (identity) instead of the shader comment's 0.2 |
329
+ | `1.3.4` | 2026-09-08 | Closes the four items deferred from 1.3.3: setFit now affects web wallpapers, audio:null truly mutes, the bare-iframe fallback no longer reports 0 fps, and debug globals are cleared on unmount |
330
+ | `1.3.3` | 2026-09-08 | Full audit: fixed autoplay:false hanging mount(), setMedia being inert on the scene path, AudioContext leaking on wallpaper swap, and more |
331
+ | `1.3.2` | 2026-09-08 | Fix: the "live system" microphone only fed scene wallpapers; web wallpaper visualizers still showed the synthetic stream |
332
+ | `1.3.1` | 2026-09-08 | Fix: injected audio/media sources never reached web wallpapers (visualizers kept playing the default stream) |
333
+ | `1.3.0` | 2026-09-08 | mediaSource() for arbitrary video/images; type sniffing; volume for media wallpapers; one Now Playing driver shared by scene and web |
334
+ | `1.2.0` | 2026-09-08 | Video wallpapers work through the library entry; audio and pointer injection wired into the public API (three downstream reports) |
335
+ | `1.1.0` | 2026-09-07 | External pointer injection channel, web wallpaper interaction, effect-pass compile fixes, complete pause semantics |
227
336
  | `1.0.0` | 2026-09-06 | First stable release: the public API is settled (mount / SceneInstance / Source) |
228
337
  | `1.0.0-beta1` | 2026-09-04 | First public preview |
229
338
 
230
- After 1.0.0 (unreleased, already on the repository's main branch)these will ship in the next version:
339
+ 1.3.5 contains a single change: the xray effect's fallback value. xray's size drives the effect radius (it is inverted internally, so size=1 is identity). When the author doesn't write size into the scene's constantshadervalues, the shader declaration comment's "default":0.2 used to be applied but that is the slider's initial position when the WE editor creates the effect, not a runtime fallback: as soon as the editor attaches the effect to a layer it writes the current slider value into the scene file, so the official runtime always reads an explicit value. Applying 0.2 shrank the effect radius to a fifth, leaving only a small patch around the cursor. The fallback is now 1. The change is scoped to this one parameter; the comment defaults for multiply and the texture slots are unchanged.
340
+
341
+ 1.3.4 closes the four items 1.3.3 listed as deferred. All four are observable behaviour bugs, not cleanup refactors:
342
+
343
+ - **setFit() did nothing on web wallpapers**: it only updated the config and the cover alignment, while a web wallpaper's scaling lives in an iframe transform written by a layout pass. Without a re-layout the old ratio stayed. setFit now triggers one (scene and media wallpapers read the config every frame and were never affected)
344
+ - **The bare-iframe fallback reported 0 fps forever**: when shim injection fails that path starts no rAF at all, so nothing advanced the frame counter and hosts saw what looked like a dead wallpaper. The fallback now runs a heartbeat rAF and reports the real refresh rate
345
+ - **audio:null fell back to the synthetic stream on web wallpapers instead of muting**: the check only asked whether an injected source existed, conflating "explicitly disabled" with "never set". The two are now distinct, and audio:null / media:null genuinely disable on both scene and web (setAudio(src) re-enables)
346
+ - **Debug globals outlived unmount**: 19 diagnostic hooks such as __scene and __textures stayed on window after clear(), pointing at the destroyed scene's object graph. That both pinned the previous wallpaper's textures and layer tree in memory and let hosts read stale state from the console. clear() now deletes each of them
347
+
348
+ 1.3.3 is a full audit covering unwired API, defects and memory leaks. Everything fixed here was confirmed by measurement:
349
+
350
+ - **autoplay:false hung mount() forever** (scene and media wallpapers): pausing before assembly meant the render loop never ran a single frame, so the only first-frame trigger was unreachable and the promise neither resolved nor rejected. Measured: the same wallpaper resolved by default but was still pending after 500 live rAF frames with autoplay:false. It now assembles normally and pauses after the first frame is drawn
351
+ - The first-frame callback now fires after render() completes (previously it ran before the render call, landing one frame early, so autoplay:false handed back a blank canvas)
352
+ - **setMedia() was inert on scene wallpapers**: the scene captured its media driver once at assembly, while setMedia is typically called after mount(). It now re-picks on every read (the web path already did)
353
+ - **Swapping wallpapers leaked an AudioContext**: the video spectrum takeover registered its release on the instance-level list, but swapping goes through clear() while only destroy() drained it. Browsers cap out at roughly 6 AudioContexts, after which audio reactivity silently dies. A per-wallpaper release list is now drained by clear()
354
+ - **One throwing cleanup dropped the entire teardown**: clear() called the assembly-layer cleanup without try/catch, so a single exception skipped the WebGL context release, video element recycling and blob revocation that followed
355
+ - **Swapping wallpapers during the mic permission prompt orphaned the stream**: getUserMedia blocks on the system dialog, and the release was registered after the await, so nothing on that path ever called it and the browser's recording indicator stayed lit. Both assembly paths now register the release slot synchronously
356
+ - The instance.media control surface is reset by clear() (previously it still pointed at the destroyed scene's sandbox closures after release()/destroy())
357
+
358
+ Still unwired by design (marked in the type comments): MountOptions' pointer and features. Use pushPointer() to feed pointer state from outside.
359
+
360
+ 1.3.2 closes the **second channel** of the same symptom: 1.3.1 fixed host injection (setAudio), while the bench's "live system" checkbox takes a different path (cfg.liveSystem, where the library captures the microphone itself). That path was only consumed by the scene assembly — web.ts referenced liveSystem zero times — so ticking the box made scene visualizers follow the mic while web wallpapers stayed on the synthetic stream.
361
+
362
+ - Web wallpapers now receive the live-system microphone through the same capture path as scene (startLiveSystem)
363
+ - Microphone capture is asynchronous (getUserMedia needs user consent) and does not block pump startup: the default source runs first, then the mic is filled in and the per-frame source pick switches over
364
+ - Audio source priority: host injection (setAudio) > live-system microphone > built-in simulation
365
+ - The microphone stream is released on teardown (otherwise the browser's recording indicator stays lit)
366
+
367
+ 1.3.1 fixes a defect found downstream in real use: **the microphone was connected, yet web wallpapers kept showing the default synthetic stream**.
368
+
369
+ - Root cause 1: the web assembly path never read rt.audioBridge (1.3.0 wired this up for the media source but missed the audio line), so injected spectra never reached the iframe
370
+ - Root cause 2: both the audio and media pumps captured their driver at assembly time, while setAudio()/setMedia() are typically called after mount() (that is when the host's microphone or SSE channel becomes ready) — a fixed driver means a later source never takes effect. Both pumps now pick their source per frame and fall back cleanly when the source is removed
371
+ - Injected spectra are no longer put through the gamma contrast expansion: that step exists for the built-in simulation's unclamped bands, and applying it to a host's already-normalized 0..1 spectrum would peg every bar at full scale
372
+
373
+ 1.3.0 continues directly from 1.2.0, closing four more items reported by the downstream host:
374
+
375
+ - New mediaSource(urlOrFile): any video or image can be a wallpaper, from a remote URL or a local File (drag & drop). The library revokes the objectURL on destroy()/source swap — without that, every wallpaper change would leak a multi-megabyte blob
376
+ - Type sniffing: with no project.type the URL extension decides (correctly stripping a signed URL's ?query and #hash), falling back to scene only when unrecognized. An explicit project.type always wins and is never overridden
377
+ - setVolume now works on media wallpapers (previously a complete no-op — volume only reached the scene audio graph). It writes both volume and muted, since changing volume on a muted &lt;video> does nothing; if unmuting is blocked by the autoplay policy that is reported via onDiagnostic rather than silently swallowed
378
+ - MediaSource was widened to the driver's real contract (an 18-field snapshot plus five optional transport methods) and is now **one shared instance across scene and web** — previously each side created its own simulation with no way for the host to inject. The new createMediaSource() takes only the fields you have and fills in palette, lyric line and trackIndex, guaranteeing chainable color instances
379
+ - New SceneInstance.setMedia() and a media control surface (snapshot plus previous/next/play-pause); transport commands forward straight to the host driver, and host-side data updates are visible to the engine in the same frame
380
+ - Video wallpapers capture their spectrum from the &lt;video> itself so visualizers react to the video's own audio; an explicit setAudio() injection takes precedence
381
+
382
+ 1.2.0 closes three gaps where the runtime capability already worked but was never exposed through the library entry (reported by the downstream host wallpaperEM):
383
+
384
+ - video / gif / image wallpapers can now be mounted via mount(): a new Source.mediaEntry() supplies the URL and the media path gained the library contract it lacked. Previously it fired neither onFirstFrame nor onError, so even with routing in place the mount() promise would hang forever — never resolving, never rejecting
385
+ - The media path now honors a caller-supplied canvas and sizes its backing store from CSS dimensions rather than the window (previously an embedded canvas got a full-window buffer and was never inserted into the DOM at all)
386
+ - MountOptions.audio is actually wired now (it was a declared-but-unreferenced dead field), plus a new SceneInstance.setAudio() for swapping after mount — host spectrum channels usually become ready only after mount()
387
+ - New SceneInstance.pushPointer() / pointerLeave(), matching the full-page renderer's __wp in both name and signature so downstream code needs no changes when migrating to the library
388
+ - Known boundaries at the time: audio injection was scene-only — web wallpapers use a separate iframe-shim channel, wired up in 1.3.1; media wallpapers have no pointer concept; MountOptions' pointer / media / features were unwired (media landed in 1.3.0)
389
+
390
+ What went into 1.1.0 (merged after 1.0.0):
231
391
 
232
392
  - New external pointer injection channel (__wp.pushPointer / pointerLeave): when the wallpaper window cannot receive the mouse (e.g. the Finder desktop window swallows events on macOS), the host polls the system cursor and pushes it in. Scene and web wallpapers share one protocol — callers need not branch on type
233
393
  - Web wallpapers joined the same channel: the shim synthesizes DOM events against the hit element (full over/out/enter/leave chains, click derived from button edges). Of 49 local web wallpapers, interaction went from dead to working on 24 with mousemove, 29 with click and 16 with pointer events. Hard limit: CSS :hover is driven by browser hit-testing and cannot be lit by synthetic events
package/README.md CHANGED
@@ -36,12 +36,12 @@ import { mount, httpSource } from "webwallgl";
36
36
 
37
37
  ```
38
38
  // 2) ESM CDN(jsDelivr,vite/webpack 之外的直引方式)
39
- import { mount, httpSource } from "https://cdn.jsdelivr.net/npm/webwallgl@1.1.0/webwallgl.min.mjs";
39
+ import { mount, httpSource } from "https://cdn.jsdelivr.net/npm/webwallgl@1.3.5/webwallgl.min.mjs";
40
40
  ```
41
41
 
42
42
  ```
43
43
  <!-- 3) UMD <script>:暴露全局 WebWallGL -->
44
- <script src="https://cdn.jsdelivr.net/npm/webwallgl@1.1.0/webwallgl.global.min.js"></script>
44
+ <script src="https://cdn.jsdelivr.net/npm/webwallgl@1.3.5/webwallgl.global.min.js"></script>
45
45
  <script>
46
46
  const { mount, httpSource } = WebWallGL;
47
47
  </script>
@@ -140,6 +140,11 @@ input.addEventListener("change", () => {
140
140
  | `setRenderDpr(dpr)` | 改 DPR 需重建画布,内部自动重挂(pkg 缓存命中,不重新下载) |
141
141
  | `setProperties(props)` | 属性热更新:就地改属性表/效果常量/脚本沙箱,不重新拉包 |
142
142
  | `getProperties()` | 当前生效的扁平化属性值表 |
143
+ | `setAudio(src)` | 换音频频谱源(拉模式,每帧一次);null 回落内置模拟。换场景不清空。scene 与 web 都生效 |
144
+ | `setMedia(src)` | 换系统媒体源(Now Playing);scene 与 web 共用同一实例,换场景不清空 |
145
+ | `media` | 媒体控制面:读 snapshot,以及 skipNext / skipPrevious / play / pause / playPause 反向控制 |
146
+ | `pushPointer(u, v, buttons?)` | 外部指针注入(u/v 为 0..1 归一化)。用于窗口收不到鼠标的宿主;scene 与 web 均生效 |
147
+ | `pointerLeave()` | 指针离开:只清按键、保留最后位置(清位置会让视差与 xray 明显抽一下) |
143
148
  | `load(source)` | 换场景,复用同一 canvas 与 WebGL 上下文;首帧后 resolve |
144
149
  | `release() / restore()` | 释放显存但保留配置(显示器睡眠)/ 用保留的配置重建 |
145
150
  | `destroy()` | 终态:释放资源、解绑监听,之后实例不可再用 |
@@ -172,6 +177,102 @@ wp.setProperties({ schemecolor: "0.5 0.2 0.8", newproperty12: false });
172
177
  - setProperties() 是就地热更新:改属性表、效果常量与脚本沙箱,不重新拉包也不重新解析
173
178
  - 没有 project.json 的壁纸也能跑:此时属性表为空,场景字段一律用 scene.json 里的快照值
174
179
 
180
+ ## 三类壁纸:scene / video / web
181
+
182
+ 类型不用你判断:mount() 先取 project.json,按其中的 type 分流 —— web 走 sandbox iframe,video / gif / image 走媒体路径,其余一律走场景装配。三类都用同一个 mount()、同一个 SceneInstance,pause/resume、setVolume、stats 等成员通用。
183
+
184
+ ```
185
+ // 同一段代码挂任意类型(记得传容器 div,见上一节)
186
+ const wp = await mount(document.querySelector("#wp"), {
187
+ source: httpSource("https://cdn.example.com/wallpapers/3789109327"),
188
+ });
189
+ console.log(wp.info); // { width, height, layerCount, ... }
190
+ ```
191
+
192
+ - 视频壁纸的资源地址由 Source.mediaEntry() 给出;httpSource 已实现(读 project.json 的 file 字段拼 {基址}/{file})
193
+ - 放任意视频/图片用 mediaSource():mediaSource(url) 或 mediaSource(file),按扩展名自动判类型,无需 project.json
194
+ - project.type 若已显式声明则永远优先,嗅探只在它缺失时兜底——有些场景壁纸的 project.file 指向 .mp4(那是场景内的视频纹理素材,不是「这张壁纸是个视频」)
195
+ - 媒体壁纸需要 WebGL2;不可用时走 onError 交给调用方决定,库不自作主张换 DOM 渲染
196
+ - pause/resume、setVolume、setFps、setFit 对媒体壁纸同样生效(setVolume 直接控制 &lt;video> 的 volume 与 muted)
197
+
198
+ ```
199
+ import { mount, mediaSource } from "webwallgl";
200
+
201
+ // 远程视频:按扩展名判类型,签名 URL 的 ?query 会被正确剥掉
202
+ await mount(box, { source: mediaSource("https://cdn/clip.mp4?token=…") });
203
+
204
+ // 本地导入:拖拽或 <input type=file> 进来的视频/图片
205
+ input.addEventListener("change", async () => {
206
+ const wp = await mount(box, { source: mediaSource(input.files[0]) });
207
+ // destroy() 时库会自动 revoke 内部创建的 objectURL
208
+ });
209
+
210
+ // 扩展名不可靠时显式指定
211
+ mediaSource(streamUrl, { type: "video" });
212
+ ```
213
+
214
+ ## 系统媒体(Now Playing)与反向控制
215
+
216
+ 「正在播放」类壁纸要读歌名、歌手、进度、封面配色与歌词,部分还带上一曲/下一曲/播放暂停按钮。这些统一由一个 MediaSource 提供 —— **scene 与 web 壁纸共用同一个实例**,宿主只需维护一套 driver,两类壁纸看到同一份数据。
217
+
218
+ ```
219
+ import { mount, createMediaSource } from "webwallgl";
220
+
221
+ // 只给你拿得到的字段,其余(配色、歌词行、trackIndex)由库补齐
222
+ const media = createMediaSource(
223
+ { title: "夜航星", artist: "相位迁移", playing: true,
224
+ position: 30, duration: 212,
225
+ lyrics: [[0, "第一句"], [20, "第二句"]] },
226
+ // 反向控制:壁纸里的按钮会调到这里,你转发给真实播放器
227
+ { skipNext: () => player.next(),
228
+ playPause: () => player.toggle() },
229
+ );
230
+
231
+ const wp = await mount(box, { source, media });
232
+
233
+ // 系统 Now Playing 变化时更新(歌词行会按 position 自动重算)
234
+ media.set({ title: "下一首", position: 0 });
235
+
236
+ // 也可以挂载后再装/换/撤
237
+ wp.setMedia(media);
238
+ wp.setMedia(null); // 回落内置模拟源
239
+
240
+ // 宿主侧也能读快照与发控制指令
241
+ console.log(wp.media.snapshot.title);
242
+ wp.media.playPause();
243
+ ```
244
+
245
+ - 五个配色字段必须是可链式调用的颜色对象(脚本会写 c.subtract(o).multiply(t).add(o),给普通数组会 TypeError 熔断整个脚本)——用 createMediaSource 构造即自动满足
246
+ - 控制方法全是可选的:只提供元数据、不支持控制时,壁纸里的按钮点了静默无效,不会报错
247
+ - setMedia 换场景不清空,装一次对之后所有场景生效
248
+ - 视频壁纸的音频频谱会自动从 &lt;video> 取(音条能跟着视频里的音乐动),宿主已用 setAudio 显式注入时则不接管
249
+
250
+ ## 注入指针与音频
251
+
252
+ 壁纸宿主常常拿不到浏览器天然的输入:桌面壁纸叠在桌面 underlay 层,鼠标事件被系统的桌面窗口吃掉;音频频谱也得由宿主自己采集。两条通道都由实例方法喂进来。
253
+
254
+ ```
255
+ // 指针:u/v 是 0..1 归一化坐标,buttons 同 MouseEvent.buttons
256
+ wp.pushPointer(0.5, 0.5, 0); // 悬停在正中
257
+ wp.pushPointer(0.5, 0.5, 1); // 按下左键
258
+ wp.pointerLeave(); // 鼠标移出(只清按键,保留最后位置)
259
+
260
+ // 音频:拉模式,渲染循环每帧调一次 snapshot()
261
+ let latest = { left: new Float32Array(64), right: new Float32Array(64) };
262
+ wp.setAudio({ snapshot: () => latest });
263
+
264
+ // 例:订阅宿主的频谱推送后更新 latest
265
+ evtSource.onmessage = (e) => { latest = JSON.parse(e.data); };
266
+
267
+ wp.setAudio(null); // 撤源,回落内置模拟
268
+ ```
269
+
270
+ - 指针注入与 canvas 自身的 DOM 监听并存,谁后写谁赢;scene 与 web 壁纸都生效,媒体壁纸没有指针概念,调用静默无效
271
+ - 音频契约:left/right 各 64 段、值域 0..1。段数不足补零、超出截断;32/16 段降采样与响度、静音判定由库派生
272
+ - snapshot() 返回 null(或抛错)表示本帧无数据,引擎自动回落内置模拟源——宿主采集还没就绪时不必特殊处理
273
+ - setAudio 换场景不清空:装一次对之后 load() 的所有场景都生效
274
+ - 音频注入对 scene 与 web 壁纸都生效:网页侧经 iframe shim 的音频泵收到同一份数据;两个泵都逐帧选源,所以 mount() 之后再 setAudio 同样有效
275
+
175
276
  ## 事件与诊断
176
277
 
177
278
  ```
@@ -219,14 +320,73 @@ b.pause(); // 不影响 a
219
320
 
220
321
  ## 版本更新说明
221
322
 
222
- 当前版本 1.1.0。本节只记对使用者可见的变化(API、行为、兼容性、还原度),逐条对应仓库里的提交;纯内部重构与判据脚本不列。
323
+ 当前版本 1.3.5。本节只记对使用者可见的变化(API、行为、兼容性、还原度),逐条对应仓库里的提交;纯内部重构与判据脚本不列。
223
324
 
224
325
  | 版本 | 日期 | 说明 |
225
326
  | --- | --- | --- |
327
+ | `1.3.5` | 2026-09-08 | xray 效果:作者未在场景里配置 size 时,缺省从 shader 注释的 0.2 改为 1(恒等) |
328
+ | `1.3.4` | 2026-09-08 | 补齐 1.3.3 遗留四项:setFit 对网页壁纸生效、audio:null 真静音、裸 iframe 回退不再帧数恒 0、调试全局随卸载清理 |
329
+ | `1.3.3` | 2026-09-08 | 全量审计:修 autoplay:false 挂死 mount()、scene 侧 setMedia 无效、换壁纸泄漏 AudioContext 等 |
330
+ | `1.3.2` | 2026-09-08 | 修复:「系统实况」麦克风此前只喂 scene,网页壁纸音谱仍是合成流 |
331
+ | `1.3.1` | 2026-09-08 | 修复:注入的频谱/媒体源到不了网页壁纸(音谱仍放默认流) |
332
+ | `1.3.0` | 2026-09-08 | mediaSource() 任意视频/图片;类型嗅探;媒体壁纸支持音量;scene 与 web 共用一套 Now Playing driver |
333
+ | `1.2.0` | 2026-09-08 | video 壁纸可走库入口;音频与指针注入接到公共 API(下游三项反馈) |
334
+ | `1.1.0` | 2026-09-07 | 外部指针注入通道、网页壁纸交互、效果 pass 编译清零、暂停语义补全 |
226
335
  | `1.0.0` | 2026-09-06 | 首个正式版:公共 API 定稿(mount / SceneInstance / Source 三件套) |
227
336
  | `1.0.0-beta1` | 2026-09-04 | 首个公开测试版 |
228
337
 
229
- 1.0.0 之后(未发版,已在仓库主线)—— 下个版本会包含这些:
338
+ 1.3.5 只有一项,改的是 xray 效果的缺省值。xray 的 size 决定效果范围(内部取倒数,size=1 是恒等)。作者若没在场景的 constantshadervalues 里写 size,此前会套用 shader 声明注释里的 "default":0.2 —— 但那是 WE 编辑器新建效果时滑条的初始位置,不是运行时缺省:编辑器一旦把效果加到层上就会把当时的滑条值写进场景文件,所以官方运行时永远读得到显式值。套 0.2 会让效果范围缩成五分之一,只剩光标旁一小块。现在缺省是 1。作用面收窄在这一个参数上,multiply 与贴图槽的注释缺省不变。
339
+
340
+ 1.3.4 收掉 1.3.3 结尾列为「留待后续」的四项,都是能观测到的行为偏差,不是清理式重构:
341
+
342
+ - **setFit() 对网页壁纸不生效**:它只改了配置和 cover 对齐量,而网页壁纸的缩放是靠一次布局计算写进 iframe 的 transform 的,不重排就还是旧比例。现在 setFit 会触发重排(场景与媒体壁纸本来就每帧读配置,不受影响)
343
+ - **注入 shim 失败退回裸 iframe 后帧率恒 0**:这条路径不启动任何 rAF,统计里没人推进帧计数,宿主看到的是「壁纸挂了」。回退分支补上心跳 rAF,帧率与实际刷新一致
344
+ - **audio:null 在网页壁纸上不是静音,而是退回合成流**:判定只看「有没有设过注入源」,分不清「显式禁用」与「没设置」。现在两者分开,audio:null / media:null 在 scene 与 web 上都真的关掉(setAudio(src) 会重新打开)
345
+ - **调试全局不随卸载清理**:__scene / __textures 等 19 个诊断入口在 clear() 后仍挂在 window 上,指着已销毁场景的对象图,既让上一张壁纸的纹理和层树无法回收,也会让宿主在控制台里读到过期状态。改为 clear() 时逐个删除
346
+
347
+ 1.3.3 是一次覆盖「漏接 / 缺陷 / 内存泄漏」三类的全量审计,修掉的都是实测确认的问题:
348
+
349
+ - **autoplay:false 会让 mount() 永久挂起**(scene 与媒体壁纸):装配前就置 paused,渲染循环一帧都不跑,唯一触发首帧回调的地方永远到不了,Promise 既不 resolve 也不 reject。实测同一张壁纸默认能 resolve、autoplay:false 在 rAF 活跃 500 帧后仍 pending。改为照常装配、首帧画完再暂停
350
+ - 首帧回调改到 render() **完成之后**触发(此前排在 render 调用之前,早一帧落地,autoplay:false 会拿到一张空画布)
351
+ - **setMedia() 在场景壁纸上无效**:scene 把 media driver 在装配时一次性捕获,而 setMedia 通常在 mount() 之后才调用。改为每次读取重新选(web 侧本来就是这样)
352
+ - **换壁纸会泄漏 AudioContext**:视频壁纸的频谱接管把释放登记在实例级列表里,而换壁纸走的是 clear(),只有 destroy() 才排空。浏览器约 6 个 AudioContext 就到顶,之后音频响应静默失效。新增壁纸级释放列表,clear() 逐张排空
353
+ - **清理链一处抛异常会丢掉整个 teardown**:clear() 里调用装配层清理没有 try/catch,一旦抛出,后面的 WebGL 上下文释放、视频元素回收、blob revoke 全部跳过
354
+ - **麦克风授权期间换壁纸会漏掉麦克风流**:getUserMedia 阻塞在系统弹窗上,此前的释放登记写在 await 之后,这条路径上没人会调它,浏览器录音指示一直亮着。两条装配路径都改为同步登记释放槽
355
+ - instance.media 控制面在 clear() 时重置(此前 release()/destroy() 之后它仍指向已销毁场景的沙箱闭包)
356
+
357
+ 已知仍未接线(有意为之,类型注释已标注):MountOptions 的 pointer 与 features。外部喂指针请用 pushPointer()。
358
+
359
+ 1.3.2 补上同一症状的**第二条通道**:1.3.1 修的是宿主注入(setAudio),而测试台「系统实况」勾选框走的是另一条路(cfg.liveSystem,库自己采麦克风),它此前只被 scene 装配路径消费——web.ts 里 liveSystem 零引用,所以勾上之后场景壁纸的音条跟着麦克风动、网页壁纸却始终是合成流。
360
+
361
+ - 网页壁纸现在也接系统实况麦克风,与 scene 用同一份采集(startLiveSystem)
362
+ - 麦克风采集是异步的(getUserMedia 要用户授权),不阻塞泵启动:先按默认源跑,授权通过后回填、由逐帧选源切过去
363
+ - 音频源优先级:宿主注入(setAudio)> 系统实况麦克风 > 内置模拟
364
+ - 卸载时释放麦克风流(否则浏览器地址栏的录音指示会一直亮着)
365
+
366
+ 1.3.1 修一个下游实测发现的缺陷:**麦克风都接上了,网页壁纸的音谱还在放默认合成流**。
367
+
368
+ - 根因一:web 装配路径压根不读 rt.audioBridge(1.3.0 给媒体源接了这一环,音频这行漏了),注入的频谱到不了 iframe
369
+ - 根因二:音频与媒体两个泵都在**装配时**捕获 driver,而 setAudio()/setMedia() 通常在 mount() 之后才调用(宿主的麦克风 / SSE 通道那时才就绪)——定死 driver 等于后装的源永远不生效。两个泵均改为逐帧选源,撤源后也能落回默认
370
+ - 注入的频谱**不再套 gamma 对比扩展**:那道处理是给内置模拟源的未钳位频段用的,对宿主给的 0..1 真实频谱再乘一遍会把音条整体顶到满格
371
+
372
+ 1.3.0 是 1.2.0 的直接延续,继续按下游宿主的反馈补齐(四项):
373
+
374
+ - 新增 mediaSource(urlOrFile):任意视频/图片可直接当壁纸,支持远程 URL 与本地 File(拖拽导入)。本地文件的 objectURL 由库在 destroy()/换源时自动 revoke——不 revoke 就是每换一次壁纸泄漏一个几十 MB 的 blob
375
+ - 类型嗅探:project.type 缺失时按 URL 扩展名判定(正确剥掉签名 URL 的 ?query 与 #hash),认不出才落回 scene。显式声明的 project.type 永远优先,不会被嗅探覆盖
376
+ - 媒体壁纸现在支持 setVolume(此前完全无效:音量只打到 scene 的音频节点)。同时写 volume 与 muted——&lt;video muted> 下只改 volume 一点用都没有;取消静音被自动播放策略拒绝时经 onDiagnostic 如实报出,不静默吞掉
377
+ - MediaSource 扩成 driver 的真实契约(18 字段快照 + 5 个可选控制方法),并由 scene 与 web **共用同一个实例**——此前两侧各自 new 一份模拟源,宿主无从注入。新增 createMediaSource() 只需给已知字段,配色/歌词行/trackIndex 由库补齐并保证颜色是可链式调用的实例
378
+ - 新增 SceneInstance.setMedia() 与 media 控制面(读快照 + 上一曲/下一曲/播放暂停),反向控制直接转发给宿主 driver;宿主更新数据后引擎同帧可见
379
+ - 视频壁纸的频谱自动从 &lt;video> 取,音条能跟着视频里的音乐动;宿主已用 setAudio 显式注入时不接管
380
+
381
+ 1.2.0 补的是三个「运行时早就能跑、只是公共库入口没接出来」的缺口(由下游宿主 wallpaperEM 反馈):
382
+
383
+ - video / gif / image 壁纸现在能经 mount() 挂载:新增 Source.mediaEntry() 取址,并补齐媒体路径的库化契约。此前媒体路径不触发 onFirstFrame / onError,即便接上分流,mount() 的 Promise 也会永久挂起——成功不 resolve、失败不 reject
384
+ - 媒体路径改为支持调用方传入的 canvas,并按 CSS 尺寸而非窗口尺寸分配缓冲区(此前嵌入式画布会拿到整窗口大小的 backing store,且画布根本不会被插入 DOM)
385
+ - MountOptions.audio 真正接线(此前是声明了却零引用的死字段),并新增 SceneInstance.setAudio() 供挂载后切换——宿主的频谱通道常在 mount() 之后才就绪
386
+ - 新增 SceneInstance.pushPointer() / pointerLeave(),与整页渲染器的 __wp 同名同签名,下游从整页迁到库时代码不用改
387
+ - 已知边界(当时状态):音频注入只对 scene 生效——网页壁纸走 iframe shim 的另一条通道,1.3.1 已补上;媒体壁纸没有指针概念;MountOptions 的 pointer / media / features 当时未接线(media 已在 1.3.0 接线)
388
+
389
+ 1.1.0 的内容(在 1.0.0 之后合入):
230
390
 
231
391
  - 新增外部指针注入通道 __wp.pushPointer / pointerLeave:桌面壁纸窗口收不到鼠标时(如 macOS 下 Finder 桌面窗口吃掉事件),由宿主轮询系统鼠标后推进来。场景与网页两类壁纸共用同一套协议,调用方不必判断类型
232
392
  - 网页壁纸接入同一条注入通道:shim 按命中元素合成 DOM 事件(over/out/enter/leave 链完整、click 靠按键边缘合成)。本机库 49 张网页壁纸里 mousemove 24 / click 29 / pointer* 16 张的交互从「完全无反应」变为可用。硬限制:CSS :hover 由浏览器 hit-test 驱动,合成事件点不亮
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webwallgl",
3
- "version": "1.1.0",
3
+ "version": "1.3.5",
4
4
  "description": "Wallpaper Engine scene wallpaper renderer for the browser (npm / CDN)",
5
5
  "license": "MIT",
6
6
  "author": "oneincase <462534624@qq.com>",