webwallgl 1.0.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 +238 -3
- package/README.md +237 -3
- package/package.json +1 -1
- package/types.d.ts +175 -18
- package/webwallgl.d.ts +37 -17
- package/webwallgl.global.js +2244 -217
- package/webwallgl.global.js.map +1 -1
- package/webwallgl.global.min.js +1241 -33
- package/webwallgl.global.min.js.map +1 -1
- package/webwallgl.min.mjs +1241 -33
- package/webwallgl.min.mjs.map +1 -1
- package/webwallgl.mjs +2245 -218
- package/webwallgl.mjs.map +1 -1
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Introduction
|
|
6
6
|
|
|
7
|
-
WebWallGL is a browser-side renderer for Wallpaper Engine wallpapers —
|
|
7
|
+
WebWallGL is a browser-side renderer for Wallpaper Engine wallpapers — scene, video, and web: its main job is replaying workshop scene packages (scene.pkg) in WebGL in real time, with layer effect chains, particles, 3D puppet bones, text widgets, script sandboxes, audio response and live user-property updates; web wallpapers run in a sandboxed iframe with a WE API shim injected before author scripts. Upcoming versions will add effects exclusive to this library — stay tuned.
|
|
8
8
|
|
|
9
9
|
- [GitHub repository](https://github.com/oneincase/webwallgl)
|
|
10
10
|
- [Live demo (GitHub Pages)](https://oneincase.github.io/webwallgl/)
|
|
@@ -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.
|
|
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.
|
|
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>
|
|
@@ -70,6 +70,31 @@ console.log("live fps:", wp.stats.fps);
|
|
|
70
70
|
|
|
71
71
|
The canvas CSS size is the render size: the library aligns the backing store to clientWidth/clientHeight, and the aspect follows container resizes automatically — no manual resize handling.
|
|
72
72
|
|
|
73
|
+
## Mount target: canvas or container div
|
|
74
|
+
|
|
75
|
+
mount() takes any HTMLElement as its first argument, not just a canvas. Pass a canvas and it is used directly; pass a plain container (a div, say) and the library creates a full-bleed canvas inside it (tagged data-webwallgl, reused on remount; a position:static container is switched to relative).
|
|
76
|
+
|
|
77
|
+
This is not a style choice — **web wallpapers require a container**. A web wallpaper does not use WebGL; the library appendChild()s a sandboxed iframe into the element you pass, and a canvas cannot have children, so passing one fails. If the same code path must handle both scene and web wallpapers (a general wallpaper player, say), always pass a div:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
<!-- Works for both wallpaper types -->
|
|
81
|
+
<div id="wp" style="position:relative;width:100%;height:400px"></div>
|
|
82
|
+
|
|
83
|
+
// Scene wallpaper: the library builds a canvas inside the div
|
|
84
|
+
// Web wallpaper: the library mounts a sandboxed iframe inside the div
|
|
85
|
+
const wp = await mount(document.querySelector("#wp"), {
|
|
86
|
+
source: httpSource("https://cdn.example.com/wallpapers/2517518192"),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// wp.canvas is always readable: the real canvas on the scene path,
|
|
90
|
+
// the container you passed on the web path
|
|
91
|
+
console.log(wp.canvas);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- You never branch on type yourself: mount() reads project.json first — type "web" takes the web path, everything else goes through scene assembly
|
|
95
|
+
- The web entry URL resolves as Source.webEntry() → {httpSource base}/{project.file or index.html}; if neither yields a URL, mount throws
|
|
96
|
+
- On the web path the WebGL-side options (fit / renderDpr / features) do not apply; pause/resume, setVolume and setProperties still work, relayed to author code through the shim
|
|
97
|
+
|
|
73
98
|
## Loading scenes: Source
|
|
74
99
|
|
|
75
100
|
The library makes only two network requests (scene.pkg and optional project.json), so the resource abstraction is one interface with three built-in implementations:
|
|
@@ -116,12 +141,139 @@ input.addEventListener("change", () => {
|
|
|
116
141
|
| `setRenderDpr(dpr)` | Changing DPR rebuilds the canvas; remounts internally (pkg cache hit, no re-download) |
|
|
117
142
|
| `setProperties(props)` | Live property updates: patches the property table / effect constants / script sandboxes in place, no re-fetch |
|
|
118
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) |
|
|
119
149
|
| `load(source)` | Switch scenes reusing the same canvas and WebGL context; resolves after the first frame |
|
|
120
150
|
| `release() / restore()` | Free GL resources keeping the config (display sleep) / rebuild from the kept config |
|
|
121
151
|
| `destroy()` | Terminal: frees resources, unbinds listeners; the instance is dead afterwards |
|
|
122
152
|
| `stats / info` | Measured FPS ({fps, running}, zeroes out instead of freezing) / scene info (logical size, layer count, has models/particles/text) |
|
|
123
153
|
| `on(ev, fn)` | Subscribe to ready / error / diagnostic; returns an unsubscribe function |
|
|
124
154
|
|
|
155
|
+
## User properties
|
|
156
|
+
|
|
157
|
+
User properties are the settings a wallpaper author exposes in WE (colors, toggles, sliders, dropdowns), declared under general.properties in project.json. Keys are the author's property names (typically things like schemecolor or newproperty12); values must be scalars matching the property type:
|
|
158
|
+
|
|
159
|
+
| Property type | What to pass | Example |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `color` | A "r g b" string: three 0..1 floats separated by spaces (not #RRGGBB, not 0..255) | `"0.5 0.2 0.8"` |
|
|
162
|
+
| `bool` | A boolean | `true` |
|
|
163
|
+
| `slider` | A number within the author's min/max | `100` |
|
|
164
|
+
| `combo` | The option value; pass a number when options are integers (integer strings also work) | `1` |
|
|
165
|
+
| `textinput / file / directory` | A string | `"https://…/clock.png"` |
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
// First see which properties this wallpaper has and their current values
|
|
169
|
+
console.log(wp.getProperties());
|
|
170
|
+
// → { schemecolor: "0 0 0", newproperty12: true, … }
|
|
171
|
+
|
|
172
|
+
// Then set them by name (pass only what you change; the rest stays put)
|
|
173
|
+
wp.setProperties({ schemecolor: "0.5 0.2 0.8", newproperty12: false });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- Property names differ per wallpaper; there is no cross-wallpaper naming convention — read getProperties() first instead of hardcoding guesses
|
|
177
|
+
- Writing a name the current scene doesn't declare raises no error: the value still lands in the table (a later scene may use it) but changes nothing on screen — a typo shows up as "nothing happened", not as an exception
|
|
178
|
+
- setProperties() patches in place — property table, effect constants and script sandboxes — with no re-fetch and no re-parse
|
|
179
|
+
- Wallpapers without a project.json still run: the property table is empty and fields fall back to the scene.json snapshot values
|
|
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 <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 <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
|
+
|
|
125
277
|
## Events & diagnostics
|
|
126
278
|
|
|
127
279
|
```
|
|
@@ -154,6 +306,8 @@ b.pause(); // does not affect a
|
|
|
154
306
|
- Parsed scene.pkg entries are cached by source.key (up to 2): pause/resume, property changes and setRenderDpr remounts never re-download
|
|
155
307
|
- After release(), stats.running goes false and the FPS reading zeroes out — a stopped meter must not freeze on its last value
|
|
156
308
|
- After destroy() the canvas is yours again; mount() a fresh instance any time
|
|
309
|
+
- load() puts the instance back into the playing state (even if it was paused before); call pause() again after load() to stay paused
|
|
310
|
+
- load() re-applies the properties given at mount time; values set later via setProperties() do not carry over — property names are per-scene anyway, so re-apply them after load() if you need them
|
|
157
311
|
|
|
158
312
|
## Troubleshooting
|
|
159
313
|
|
|
@@ -162,6 +316,87 @@ b.pause(); // does not affect a
|
|
|
162
316
|
- Failed to fetch with no status: custom-protocol/WKWebView behavior for missing paths — by design; just read the final error
|
|
163
317
|
- stats.fps is 0 while the picture moves: the meter counts committed frames only; browsers suspend rAF for occluded tabs — expected
|
|
164
318
|
- Audio starts late: autoplay policy requires user interaction before sound; that's why volume defaults to 0
|
|
319
|
+
- Web wallpaper has no audio/properties: the entry HTML must be same-origin or CORS-readable so the library can inject the WE shim; unreadable cross-origin falls back to a bare iframe (no official APIs)
|
|
320
|
+
- Web wallpaper relative assets 404: resources rely on <base href> pointing at the original directory; wallpapers that build URLs from location.href may break under blob loading
|
|
321
|
+
|
|
322
|
+
## Changelog
|
|
323
|
+
|
|
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.
|
|
325
|
+
|
|
326
|
+
| Version | Date | Notes |
|
|
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 |
|
|
336
|
+
| `1.0.0` | 2026-09-06 | First stable release: the public API is settled (mount / SceneInstance / Source) |
|
|
337
|
+
| `1.0.0-beta1` | 2026-09-04 | First public preview |
|
|
338
|
+
|
|
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 <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 <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):
|
|
391
|
+
|
|
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
|
|
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
|
|
394
|
+
- Effect-pass compile failures driven to near zero (seven rounds): the transpiler now handles int/float mixing, macro scoping, vector narrowing, scientific notation and more. Library-wide effect-pass compilation went from 1653/1873 (88.3%) to 1823/1873 (97.3%), +170 in total. The symptom was an effect silently missing — a failed compile only warns, so god rays / visualizers / glows simply vanished
|
|
395
|
+
- Pause semantics completed: pausing must freeze rAF/timers and CSS animations together (compositor-driven CSS animations ignore JS freezing — 1444432396 kept animating after pause); resuming must re-arm held rAF callbacks (self-recursive rAF wallpapers break their chain permanently — 1278092907 froze forever after resume), restoring only what we paused
|
|
396
|
+
- Fidelity fixes: keyframe animations now run on the real clock (previously they accumulated the target frame interval, diverging from the bone clock by 5.5s over 30s — hair desynced from the head and the scalp showed through); object scripts and keyframe animations now work in local space with per-frame parent/child recomposition (previously local return values were written straight into world slots, so elements drifted away untouched and got clipped); hidden mask layers referenced by clipping_mask now correctly read back what is behind them (previously they fell back to the referencing layer itself, painting a solid white block)
|
|
397
|
+
- Two new documentation sections, "Mount target" and "User properties": web wallpapers require a container div rather than a canvas, and what value shape each property type expects
|
|
398
|
+
|
|
399
|
+
The full commit history lives in the GitHub repository; every fix records its symptom, root cause, measured scope and verification method in the commit message.
|
|
165
400
|
|
|
166
401
|
## Copyright & compliance
|
|
167
402
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 简介
|
|
6
6
|
|
|
7
|
-
WebWallGL 是一个浏览器端的 Wallpaper Engine「scene」场景、视频、web
|
|
7
|
+
WebWallGL 是一个浏览器端的 Wallpaper Engine「scene」场景、视频、web 网页壁纸渲染库:主要功能是把创意工坊场景包(scene.pkg)在 WebGL 里实时还原,支持图层效果链、粒子、3D 木偶骨骼、文字挂件、脚本沙箱、音频响应与用户自定义属性热更新;网页类型壁纸经 sandbox iframe + 加载前 WE shim 注入运行。后续版本将加入本库独有效果支持,请敬请期待。
|
|
8
8
|
|
|
9
9
|
- [GitHub 开源仓库](https://github.com/oneincase/webwallgl)
|
|
10
10
|
- [在线版(GitHub Pages)](https://oneincase.github.io/webwallgl/)
|
|
@@ -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.
|
|
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.
|
|
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>
|
|
@@ -70,6 +70,30 @@ console.log("实测帧率", wp.stats.fps);
|
|
|
70
70
|
|
|
71
71
|
画布的 CSS 尺寸就是渲染尺寸:库把 backing store 对齐 clientWidth/clientHeight,容器改大小后画面宽高比自动跟随,不需要手动 resize。
|
|
72
72
|
|
|
73
|
+
## 挂载目标:canvas 还是容器 div
|
|
74
|
+
|
|
75
|
+
mount() 的第一个参数收任意 HTMLElement,不限于 canvas。传 canvas 就直接用它;传普通容器(div 等)则库在其内部自建一块铺满的 canvas(带 data-webwallgl 标记,重复挂载会复用同一块,容器若是 position:static 会被改成 relative)。
|
|
76
|
+
|
|
77
|
+
选哪个不是风格问题——**网页类型壁纸必须传容器**。网页壁纸不走 WebGL,库会把 sandbox iframe 直接 appendChild 进你传的元素;canvas 不能有子元素,传 canvas 会挂不上。如果同一段代码要同时应付场景壁纸和网页壁纸(例如一个通用壁纸播放器),一律传 div 最稳妥:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
<!-- 通用写法:两类壁纸都能挂 -->
|
|
81
|
+
<div id="wp" style="position:relative;width:100%;height:400px"></div>
|
|
82
|
+
|
|
83
|
+
// 场景壁纸:库在 div 内自建 canvas
|
|
84
|
+
// 网页壁纸:库在 div 内挂 sandbox iframe
|
|
85
|
+
const wp = await mount(document.querySelector("#wp"), {
|
|
86
|
+
source: httpSource("https://cdn.example.com/wallpapers/2517518192"),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// wp.canvas 始终可读:场景路径是那块真 canvas,网页路径是你传入的容器
|
|
90
|
+
console.log(wp.canvas);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- 类型不用你判断:mount() 先取 project.json,type 为 "web" 走网页路径,其余一律走场景装配
|
|
94
|
+
- 网页入口 URL 的解析顺序是 Source.webEntry() → {httpSource 基址}/{project.file 或 index.html};两者都给不出就抛错
|
|
95
|
+
- 网页路径下 fit / renderDpr / features 这些 WebGL 侧选项自然不适用;pause/resume、setVolume、setProperties 仍然有效(经 shim 转达给作者代码)
|
|
96
|
+
|
|
73
97
|
## 资源来源 Source
|
|
74
98
|
|
|
75
99
|
库对网络只发两个请求(scene.pkg 与可选的 project.json),所以资源抽象只有一个接口、三个内置实现:
|
|
@@ -116,12 +140,139 @@ input.addEventListener("change", () => {
|
|
|
116
140
|
| `setRenderDpr(dpr)` | 改 DPR 需重建画布,内部自动重挂(pkg 缓存命中,不重新下载) |
|
|
117
141
|
| `setProperties(props)` | 属性热更新:就地改属性表/效果常量/脚本沙箱,不重新拉包 |
|
|
118
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 明显抽一下) |
|
|
119
148
|
| `load(source)` | 换场景,复用同一 canvas 与 WebGL 上下文;首帧后 resolve |
|
|
120
149
|
| `release() / restore()` | 释放显存但保留配置(显示器睡眠)/ 用保留的配置重建 |
|
|
121
150
|
| `destroy()` | 终态:释放资源、解绑监听,之后实例不可再用 |
|
|
122
151
|
| `stats / info` | 实测帧率({fps, running},停了会归零而不是冻住)/ 场景基本信息(逻辑分辨率、图层数、是否含模型/粒子/文字) |
|
|
123
152
|
| `on(ev, fn)` | 订阅 ready / error / diagnostic,返回取消函数 |
|
|
124
153
|
|
|
154
|
+
## 用户属性 properties
|
|
155
|
+
|
|
156
|
+
用户属性就是 WE 里作者暴露给观众的那些设置项(颜色、开关、滑条、下拉),定义在 project.json 的 general.properties。键是属性名(作者自定的,常见形态是 schemecolor、newproperty12 这类),值必须按属性类型给对应的标量:
|
|
157
|
+
|
|
158
|
+
| 属性类型 | 传什么 | 示例 |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `color` | 字符串 "r g b",三个 0..1 浮点用空格分隔(不是 #RRGGBB,也不是 0..255) | `"0.5 0.2 0.8"` |
|
|
161
|
+
| `bool` | 布尔 | `true` |
|
|
162
|
+
| `slider` | 数字,落在作者定义的 min/max 内 | `100` |
|
|
163
|
+
| `combo` | 选项值;选项为整数时给 number(整数字符串也认) | `1` |
|
|
164
|
+
| `textinput / file / directory` | 字符串 | `"https://…/clock.png"` |
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
// 先看这张壁纸有哪些属性、当前值是什么
|
|
168
|
+
console.log(wp.getProperties());
|
|
169
|
+
// → { schemecolor: "0 0 0", newproperty12: true, … }
|
|
170
|
+
|
|
171
|
+
// 再按名字改(只传要改的,其余保持不动)
|
|
172
|
+
wp.setProperties({ schemecolor: "0.5 0.2 0.8", newproperty12: false });
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- 属性名逐壁纸不同,没有跨壁纸通用的名字——先 getProperties() 读一遍再改,不要硬编码猜名字
|
|
176
|
+
- 写一个当前场景没有的名字不会报错:值会照样进属性表(换场景后可能被用上),但对当前画面无任何影响——拼错名字的症状是「调了没反应」而不是异常
|
|
177
|
+
- setProperties() 是就地热更新:改属性表、效果常量与脚本沙箱,不重新拉包也不重新解析
|
|
178
|
+
- 没有 project.json 的壁纸也能跑:此时属性表为空,场景字段一律用 scene.json 里的快照值
|
|
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 直接控制 <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
|
+
- 视频壁纸的音频频谱会自动从 <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
|
+
|
|
125
276
|
## 事件与诊断
|
|
126
277
|
|
|
127
278
|
```
|
|
@@ -154,6 +305,8 @@ b.pause(); // 不影响 a
|
|
|
154
305
|
- 解析后的 scene.pkg 按 source.key 缓存(最多 2 份):暂停恢复、改属性、setRenderDpr 重挂都不重新下载
|
|
155
306
|
- release() 后 stats.running 变 false、读数归零 —— 停住的读数不该冻在最后一个值上
|
|
156
307
|
- destroy() 之后 canvas 归还给你,库不再碰它;可以再 mount() 一个新实例
|
|
308
|
+
- load() 换场景会把实例恢复成播放态(即使换之前是暂停的),要保持暂停就在 load() 之后再 pause() 一次
|
|
309
|
+
- load() 会重新套用挂载时传入的 properties,此前用 setProperties() 改的值不会延续到新场景——属性名本就是逐场景定义的,要沿用得自己在 load() 之后再设一次
|
|
157
310
|
|
|
158
311
|
## 故障排查
|
|
159
312
|
|
|
@@ -162,6 +315,87 @@ b.pause(); // 不影响 a
|
|
|
162
315
|
- Failed to fetch 且无状态码:自定义协议/WKWebView 对缺失路径的行为,属正常容错路径,看最后一条错误即可
|
|
163
316
|
- stats.fps 为 0 但画面在动:读数是「真正提交渲染」的帧,标签页被遮挡时浏览器会暂停 rAF,属预期
|
|
164
317
|
- 有声音但延迟起播:自动播放策略要求用户交互后才允许出声,volume 默认 0 正是为此
|
|
318
|
+
- 网页壁纸无音频/属性:入口 HTML 必须同源或 CORS 可读,库才能改写注入 WE shim;跨域不可读时会退回裸 iframe(无官方 API)
|
|
319
|
+
- 网页壁纸相对资源 404:依赖 <base href> 指回原站点目录;依赖 location.href 拼路径的壁纸在 blob 加载下可能异常
|
|
320
|
+
|
|
321
|
+
## 版本更新说明
|
|
322
|
+
|
|
323
|
+
当前版本 1.3.5。本节只记对使用者可见的变化(API、行为、兼容性、还原度),逐条对应仓库里的提交;纯内部重构与判据脚本不列。
|
|
324
|
+
|
|
325
|
+
| 版本 | 日期 | 说明 |
|
|
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 编译清零、暂停语义补全 |
|
|
335
|
+
| `1.0.0` | 2026-09-06 | 首个正式版:公共 API 定稿(mount / SceneInstance / Source 三件套) |
|
|
336
|
+
| `1.0.0-beta1` | 2026-09-04 | 首个公开测试版 |
|
|
337
|
+
|
|
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——<video muted> 下只改 volume 一点用都没有;取消静音被自动播放策略拒绝时经 onDiagnostic 如实报出,不静默吞掉
|
|
377
|
+
- MediaSource 扩成 driver 的真实契约(18 字段快照 + 5 个可选控制方法),并由 scene 与 web **共用同一个实例**——此前两侧各自 new 一份模拟源,宿主无从注入。新增 createMediaSource() 只需给已知字段,配色/歌词行/trackIndex 由库补齐并保证颜色是可链式调用的实例
|
|
378
|
+
- 新增 SceneInstance.setMedia() 与 media 控制面(读快照 + 上一曲/下一曲/播放暂停),反向控制直接转发给宿主 driver;宿主更新数据后引擎同帧可见
|
|
379
|
+
- 视频壁纸的频谱自动从 <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 之后合入):
|
|
390
|
+
|
|
391
|
+
- 新增外部指针注入通道 __wp.pushPointer / pointerLeave:桌面壁纸窗口收不到鼠标时(如 macOS 下 Finder 桌面窗口吃掉事件),由宿主轮询系统鼠标后推进来。场景与网页两类壁纸共用同一套协议,调用方不必判断类型
|
|
392
|
+
- 网页壁纸接入同一条注入通道:shim 按命中元素合成 DOM 事件(over/out/enter/leave 链完整、click 靠按键边缘合成)。本机库 49 张网页壁纸里 mousemove 24 / click 29 / pointer* 16 张的交互从「完全无反应」变为可用。硬限制:CSS :hover 由浏览器 hit-test 驱动,合成事件点不亮
|
|
393
|
+
- 效果 pass 编译失败清零(七批):转译器修掉整浮混用、宏作用域、向量收窄、科学计数法等形态。全库效果 pass 编译通过率 1653/1873 (88.3%) → 1823/1873 (97.3%),累计 +170。症状是「某个效果静默不出现」——编译失败只 warn 不报错,画面上表现为体积光/音谱/光晕整个缺失
|
|
394
|
+
- 暂停语义补全:暂停必须同时冻结 rAF/定时器与 CSS 动画(合成器驱动的 CSS 动画不受 JS 冻结影响,1444432396 表现为「点了暂停画面照旧」);恢复必须重挂 rAF 挂起项(rAF 自递归的壁纸暂停一次就永久断链,1278092907 表现为「恢复后永久定格」),且只还原我们代为暂停的部分
|
|
395
|
+
- 还原度修复若干:关键帧动画改用真实时钟(此前按目标帧间隔累加,与骨骼两套时基必然发散,30 秒漂 5.5 秒,表现为头发与头不同步、头顶漏模);对象脚本与关键帧动画的坐标空间改为 local 并每帧重算父子变换(此前把脚本返回的 local 值直接写进 world 槽,表现为元素无人操作就自行滑走、被边缘裁切);clipping_mask 引用的隐藏遮罩层现在能正确回读身后画面(此前回退成引用方自身,表现为一块纯白板)
|
|
396
|
+
- 使用说明新增「挂载目标」与「用户属性」两节:网页壁纸必须传容器 div 而非 canvas,以及各类属性该传什么形态的值
|
|
397
|
+
|
|
398
|
+
完整提交历史见 GitHub 仓库;每条修复在提交信息里都写明了症状、根因、影响面数字与验证方式。
|
|
165
399
|
|
|
166
400
|
## 版权与合规
|
|
167
401
|
|