wallpaper-engine-dsh 2.1.0 → 2.1.2
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.md +2 -1
- package/README.zh.md +2 -1
- package/lib/client.js +80 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A DSH bundle that turns your local **Wallpaper Engine** library into the live ba
|
|
|
10
10
|
|
|
11
11
|
**Legibility on any wallpaper.** Text stays readable by construction, not by luck. The veil always opposes the text colour — black in the dark theme, white in the light theme — so neither a pitch-black nor a pure-white wallpaper can swallow the UI. On top of that, a Smart veil samples the wallpaper's brightness every few seconds and automatically strengthens the veil when a scene drifts too bright (or too dark) for the current theme (web wallpapers' pixels are unreadable inside the sandbox, so their preview still stands in as the sample). While a wallpaper is active, dropdown menus, the slash-command popup, the queue-dock panel, the settings dialog, and the image lightbox all render as frosted glass: the shell's own menu primitive carries an isolation property that silently kills its internal blur layer, so the plugin applies the blur on the surface element itself and deepens the menu fill, keeping overlay text readable over any wallpaper. Everything reverts to the host look when the wallpaper is off.
|
|
12
12
|
|
|
13
|
-
**Manual framing.** Pick a base layout — Cover, Contain, Stretch, Original, or Free — then frame the shot yourself. **Adjust** opens the live wallpaper fullscreen: scroll to zoom, drag to pan. In the first four layouts the crop is clamped so the image always fills the screen with no exposed edges; **Free** places the wallpaper at natural size on a black canvas where it can be dragged anywhere, zoomed from 0.1× to 4×, and rotated to any angle (±90° buttons or Alt+drag), with just enough kept on screen that it can never be dragged out of sight. **Mirror** flips the wallpaper horizontally and works in every layout — a flip never exposes an edge. Esc or Done finishes, the framing persists, and **Reset crop** restores the default in one click. Web wallpapers take the same select and the same zoom/pan/rotate framing — the transform applies to the iframe directly.
|
|
13
|
+
**Manual framing.** Pick a base layout — Cover, Contain, Stretch, Original, or Free — then frame the shot yourself. **Adjust** opens the live wallpaper fullscreen: scroll to zoom, drag to pan. In the seamless layouts the picture already fills the screen at 100% — zoom in with the wheel first and panning unlocks (the hint bar says so). In the first four layouts the crop is clamped so the image always fills the screen with no exposed edges; **Free** places the wallpaper at natural size on a black canvas where it can be dragged anywhere, zoomed from 0.1× to 4×, and rotated to any angle (±90° buttons or Alt+drag), with just enough kept on screen that it can never be dragged out of sight. **Mirror** flips the wallpaper horizontally and works in every layout — a flip never exposes an edge. Esc or Done finishes, the framing persists, and **Reset crop** restores the default in one click. Web wallpapers take the same select and the same zoom/pan/rotate framing — the transform applies to the iframe directly.
|
|
14
14
|
|
|
15
15
|
**Four live sliders.** Wallpaper blur, Scrim, Borders, and Glass — every change lands instantly and persists. Blur renders in place: composition, layout, and crop stay identical at every blur level, and within one blur radius of the screen edge the picture fades softly into the veil beneath the UI chrome. A whisper of dither noise keeps smooth gradients band-free; at zero blur the media bypasses the filter pipeline entirely for bit-exact pixels. The Glass slider's colour melt is anchored to the shell's own menu material: at the 40px ceiling it reproduces the host recipe blur(40px) saturate(150%) exactly, so full glass reads as one system with the shell's native frost.
|
|
16
16
|
|
|
@@ -73,6 +73,7 @@ Media serving is defensive by design: tokens are unguessable 72-bit values, stab
|
|
|
73
73
|
- Scene and application wallpapers are not listed: they are Wallpaper Engine's private `.pkg` scene packages, and their live animation can only be rendered by Wallpaper Engine itself on the desktop.
|
|
74
74
|
- Web wallpapers run isolated from the page's storage, so ones that persist state across loads won't keep it.
|
|
75
75
|
- Web wallpapers with sound may stay silent until you interact with the page — browsers gate audio in sandboxed documents on a user gesture, and wallpapers that rely on Wallpaper Engine's system-audio feed (visualizers) have no signal to draw from.
|
|
76
|
+
- Video codecs are bounded by what the browser can decode. H.264/AVC, VP8/VP9, and AV1 play natively; workshop videos compressed with H.265/HEVC cannot be software-decoded on most machines — previously a solid black screen, now detected as "playing but zero decoded frames" and automatically demoted to the preview still with a status note. Transcoding to H.264 mp4 or VP9 webm restores full playback.
|
|
76
77
|
- The glass effect reads DSH design tokens; if a future shell redesign renames them, frosting degrades to plain transparency.
|
|
77
78
|
|
|
78
79
|
## Development
|
package/README.zh.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩(网页壁纸的像素在沙箱中读不到,改用其预览图作为采样依据)。壁纸激活时,下拉菜单、斜杠命令提示框、队列面板、设置弹窗与图片灯箱统一为磨砂玻璃:外壳的菜单原语自身带隔离属性,其内部模糊层在宿主原样下并不生效,插件改为在 surface 元素上施加模糊,并把菜单底色加深,保证浮层文字在任意壁纸上可读;关闭壁纸后恢复宿主原样。
|
|
12
12
|
|
|
13
|
-
**手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由)
|
|
13
|
+
**手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。填充等无缝排布下,画面在 100% 倍率时已铺满屏幕,先滚轮放大后拖动平移才会生效(提示条会同步说明)。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并可旋转任意角度(±90° 按钮或 Alt+拖动),同时始终保留一小块在屏幕内,不会拖丢。「镜像」水平翻转壁纸,所有排布下都可用——翻转不会破坏铺满。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移/旋转取景(变换直接作用于 iframe)。
|
|
14
14
|
|
|
15
15
|
**四个实时滑杆。** 壁纸模糊、暗化、边框、玻璃,每一步调节立即生效并保存。模糊在原位渲染:任意模糊度下,画面的比例、排布与取景完全一致;屏幕边缘一个模糊半径内的画面向遮罩过渡,被界面边饰覆盖,通常不可见。模糊时叠加轻微噪声,消除渐变色带;模糊为零时媒体完全绕过滤镜管线,像素保持不变。玻璃滑杆的「色彩熔融」在 40px 满档时正好等于外壳官方菜单材质的配方(blur(40px) saturate(150%)),满档玻璃与外壳原生磨砂观感一致。
|
|
16
16
|
|
|
@@ -73,6 +73,7 @@ bundle 分为两半:
|
|
|
73
73
|
- 场景与应用壁纸不出现在列表中:它们是 Wallpaper Engine 私有的 `.pkg` 场景包,动态渲染只能由桌面端的 Wallpaper Engine 完成。
|
|
74
74
|
- 网页壁纸与页面存储隔离,需要跨加载保存状态的壁纸无法保留状态。
|
|
75
75
|
- 带声音的网页壁纸可能在你与页面交互前保持静默——浏览器对沙箱文档的音频有自动播放限制;依赖 Wallpaper Engine 系统音频输入的壁纸(如音频可视化)在浏览器中没有信号可画。
|
|
76
|
+
- 视频编码受浏览器解码能力约束。浏览器可以直接播放 H.264/AVC、VP8/VP9 与 AV1;创意工坊中用 H.265/HEVC 压缩的视频在多数机器上无法软解——之前表现为只有一片黑色,现在检测到「声称在播放但解不出任何帧」时会自动降级为预览图并在状态行说明。手工转码为 H.264 的 mp4 或 VP9 的 webm 即可完整播放。
|
|
76
77
|
- 玻璃效果读 DSH 设计令牌;若未来外壳重构重命名令牌,磨砂会退化为普通半透明。
|
|
77
78
|
|
|
78
79
|
## 开发
|
package/lib/client.js
CHANGED
|
@@ -102,6 +102,7 @@ window.__ModuleLoader__.load({
|
|
|
102
102
|
adjustDone: "完成",
|
|
103
103
|
adjustHint: "拖动平移 · 滚轮缩放 · Esc 或点「完成」退出",
|
|
104
104
|
adjustHintFree: "拖动平移 · 滚轮缩放 · Alt+拖动旋转 · Esc 或点「完成」退出",
|
|
105
|
+
adjustHintZoom: "当前倍数下画面已铺满,先滚轮放大,即可拖动平移",
|
|
105
106
|
mirror: "镜像",
|
|
106
107
|
rotateCcw: "⟲",
|
|
107
108
|
rotateCw: "⟳",
|
|
@@ -186,6 +187,7 @@ window.__ModuleLoader__.load({
|
|
|
186
187
|
adjustDone: "Done",
|
|
187
188
|
adjustHint: "Drag to pan · wheel to zoom · Esc or Done to exit",
|
|
188
189
|
adjustHintFree: "Drag to pan · wheel to zoom · Alt+drag to rotate · Esc or Done to exit",
|
|
190
|
+
adjustHintZoom: "The picture already fills the screen — zoom in with the wheel to unlock panning",
|
|
189
191
|
mirror: "Mirror",
|
|
190
192
|
rotateCcw: "⟲",
|
|
191
193
|
rotateCw: "⟳",
|
|
@@ -727,32 +729,64 @@ window.__ModuleLoader__.load({
|
|
|
727
729
|
});
|
|
728
730
|
}
|
|
729
731
|
|
|
732
|
+
function handleMediaFailure(sel) {
|
|
733
|
+
const id = sel.id;
|
|
734
|
+
const count = (mediaFailures.get(id) || 0) + 1;
|
|
735
|
+
mediaFailures.set(id, count);
|
|
736
|
+
const w = selection.inventory.wallpapers.find((x) => x.id === id);
|
|
737
|
+
if (count === 1) {
|
|
738
|
+
// Maybe just a dead token: refetch fresh URLs and remount. Throttled
|
|
739
|
+
// PER WALLPAPER - a global window would strand a video whose first
|
|
740
|
+
// error lands soon after an unrelated recovery (no remount, no second
|
|
741
|
+
// error, blank layer until a manual refresh).
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
if (now - (lastRecoveryAt.get(id) || 0) < 10000) return;
|
|
744
|
+
lastRecoveryAt.set(id, now);
|
|
745
|
+
forceRemount = true;
|
|
746
|
+
loadInventory(true);
|
|
747
|
+
} else if (w && w.preview && !demotedToPreview.has(id)) {
|
|
748
|
+
// The media itself is unusable here - fall back to the still preview.
|
|
749
|
+
demotedToPreview.add(id);
|
|
750
|
+
applySelection(id);
|
|
751
|
+
} else {
|
|
752
|
+
// Unrenderable here with no preview to fall back on (or the entry has
|
|
753
|
+
// vanished from the inventory entirely): clear the selection rather
|
|
754
|
+
// than leave a dead element on the layer.
|
|
755
|
+
applySelection("");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
730
759
|
function attachMediaError(media, sel) {
|
|
731
|
-
media.addEventListener("error", () =>
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
760
|
+
media.addEventListener("error", () => handleMediaFailure(sel));
|
|
761
|
+
if (sel.type === "video") attachZeroFrameDetector(media, sel);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Zero-decoded-frames detector: Chromium can report a video as PLAYING
|
|
765
|
+
// without ever decoding a frame - no error event, currentTime advancing,
|
|
766
|
+
// picture permanently black. Typical with HEVC/H.265 content when the
|
|
767
|
+
// machine has no usable hardware decode. getVideoPlaybackQuality exposes
|
|
768
|
+
// the truth (totalVideoFrames stays 0); a genuinely dark wallpaper is
|
|
769
|
+
// unaffected because its frames DO decode. Wired into the same recovery
|
|
770
|
+
// ladder as a media error: first hit retries with fresh tokens, second
|
|
771
|
+
// hit demotes to the preview still (or clears without one).
|
|
772
|
+
function attachZeroFrameDetector(media, sel) {
|
|
773
|
+
media.addEventListener("playing", () => {
|
|
774
|
+
// One probe ~2s into claimed playback: enough for even a slow
|
|
775
|
+
// hardware pipeline to produce its first frames.
|
|
776
|
+
schedule(() => {
|
|
777
|
+
// Stale guard: crossfade may have swapped the element out already,
|
|
778
|
+
// and the probe must never run while the user paused playback.
|
|
779
|
+
const layer = document.getElementById(LAYER_ID);
|
|
780
|
+
if (!layer || layer.querySelector("video") !== media) return;
|
|
781
|
+
if (media.paused || !(media.currentTime > 0)) return;
|
|
782
|
+
const q = typeof media.getVideoPlaybackQuality === "function"
|
|
783
|
+
? media.getVideoPlaybackQuality() : null;
|
|
784
|
+
const frames = q ? q.totalVideoFrames
|
|
785
|
+
: (typeof media.webkitDecodedFrameCount === "number"
|
|
786
|
+
? media.webkitDecodedFrameCount : null);
|
|
787
|
+
if (frames === null) return; // no probe available -> cannot detect
|
|
788
|
+
if (frames === 0) handleMediaFailure(sel);
|
|
789
|
+
}, 2000);
|
|
756
790
|
});
|
|
757
791
|
}
|
|
758
792
|
|
|
@@ -829,7 +863,15 @@ window.__ModuleLoader__.load({
|
|
|
829
863
|
bar.className = "webg-adjust-bar";
|
|
830
864
|
const hint = document.createElement("span");
|
|
831
865
|
hint.className = "webg-adjust-hint";
|
|
832
|
-
|
|
866
|
+
const updateHint = () => {
|
|
867
|
+
// Pan feedback: in the gap-free fits the pan headroom is (zoom-1)*50,
|
|
868
|
+
// so at zoom 1 dragging is pinned BY DESIGN - say so instead of
|
|
869
|
+
// leaving a silent no-op that reads as "pan is broken".
|
|
870
|
+
hint.textContent = isFreeFit() ? S().adjustHintFree
|
|
871
|
+
: selection.zoom > 1 ? S().adjustHint
|
|
872
|
+
: S().adjustHintZoom;
|
|
873
|
+
};
|
|
874
|
+
updateHint();
|
|
833
875
|
const done = document.createElement("button");
|
|
834
876
|
done.type = "button";
|
|
835
877
|
done.className = "webg-btn webg-btn--primary";
|
|
@@ -901,6 +943,7 @@ window.__ModuleLoader__.load({
|
|
|
901
943
|
clampCrop(); // mode envelope: gap-free pan headroom, or free keep-visible
|
|
902
944
|
drag = { x: e.clientX, y: e.clientY };
|
|
903
945
|
applyEffects();
|
|
946
|
+
updateHint();
|
|
904
947
|
};
|
|
905
948
|
const endPointer = () => {
|
|
906
949
|
if (rot) { rot = null; persistSelection(); return; }
|
|
@@ -921,6 +964,7 @@ window.__ModuleLoader__.load({
|
|
|
921
964
|
selection.zoom *= Math.exp(-(e.deltaY || 0) * 0.0012);
|
|
922
965
|
clampCrop();
|
|
923
966
|
applyEffects();
|
|
967
|
+
updateHint();
|
|
924
968
|
persistSelection();
|
|
925
969
|
};
|
|
926
970
|
const onKey = (e) => { if (e.key === "Escape") exitAdjust(); };
|
|
@@ -1896,12 +1940,18 @@ window.__ModuleLoader__.load({
|
|
|
1896
1940
|
.webg-adjust {
|
|
1897
1941
|
position: fixed; inset: 0; z-index: 2147483000;
|
|
1898
1942
|
cursor: grab; touch-action: none;
|
|
1943
|
+
/* Desktop: the shell titlebar strip is a window-drag region and the
|
|
1944
|
+
union drag region ignores z-order - without no-drag, drags starting
|
|
1945
|
+
near the top of the adjust overlay move the WINDOW instead of
|
|
1946
|
+
panning, and the bar's Done button stops receiving clicks. */
|
|
1947
|
+
-webkit-app-region: no-drag;
|
|
1948
|
+
app-region: no-drag;
|
|
1899
1949
|
}
|
|
1900
1950
|
.webg-adjust:active { cursor: grabbing; }
|
|
1901
1951
|
.webg-adjust .webg-layer { position: absolute; z-index: 0; }
|
|
1902
1952
|
.webg-adjust-bar {
|
|
1903
1953
|
position: absolute; top: 18px; left: 50%; transform: translateX(-50%);
|
|
1904
|
-
z-index: 1; cursor: default;
|
|
1954
|
+
z-index: 1; cursor: default; -webkit-app-region: no-drag; app-region: no-drag;
|
|
1905
1955
|
display: flex; align-items: center; gap: 12px;
|
|
1906
1956
|
padding: 8px 8px 8px 14px; border-radius: 12px;
|
|
1907
1957
|
background: var(--dsw-alias-bg-overlay, rgba(30, 32, 38, 0.85));
|
|
@@ -1984,9 +2034,9 @@ window.__ModuleLoader__.load({
|
|
|
1984
2034
|
Same on the queue dock panel (:before carries the blur). With a
|
|
1985
2035
|
wallpaper active, menus showed sharp content through a 45% fill.
|
|
1986
2036
|
An element's OWN backdrop-filter ignores its own isolation, so
|
|
1987
|
-
the filter moves onto the surface element; fill deepened
|
|
1988
|
-
body[${ACTIVE_ATTR}] { --dsw-menu-surface-fill: rgba(67, 69, 74, 0.
|
|
1989
|
-
body[${ACTIVE_ATTR}]:not([${DARK_ATTR}]) { --dsw-menu-surface-fill: rgba(248, 249, 250, 0.
|
|
2037
|
+
the filter moves onto the surface element; fill deepened 85%. */
|
|
2038
|
+
body[${ACTIVE_ATTR}] { --dsw-menu-surface-fill: rgba(67, 69, 74, 0.85); }
|
|
2039
|
+
body[${ACTIVE_ATTR}]:not([${DARK_ATTR}]) { --dsw-menu-surface-fill: rgba(248, 249, 250, 0.85); }
|
|
1990
2040
|
body[${ACTIVE_ATTR}] [data-menu-material],
|
|
1991
2041
|
body[${ACTIVE_ATTR}] [data-composer-dock] [class*="_panel"] {
|
|
1992
2042
|
-webkit-backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.2)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Wallpaper Engine library as the DSH web GUI background — video/web/still wallpapers, rotation lists, search, resource monitor, bilingual UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|