wallpaper-engine-dsh 1.0.4 → 1.0.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.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +60 -29
- 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; it also strengthens the border contrast and the glass frosting on the same evidence. Dropdowns, menus, and dialogs sit on DSH's opaque surfaces and are never affected.
|
|
12
12
|
|
|
13
|
-
**Manual framing.** Pick a base layout — Cover, Contain, Stretch, or
|
|
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 and zoomed from 0.1× to 4×, with just enough kept on screen that it can never be dragged out of sight. 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 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.
|
|
16
16
|
|
package/README.zh.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩。下拉菜单、弹窗等浮层显示在 DSH 原生的不透明表面上,不受影响。
|
|
12
12
|
|
|
13
|
-
**手动取景。** 先选基础排布(
|
|
13
|
+
**手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并始终保留一小块在屏幕内,不会拖丢。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移取景(变换直接作用于 iframe)。
|
|
14
14
|
|
|
15
15
|
**四个实时滑杆。** 壁纸模糊、暗化、边框、玻璃,每一步调节立即生效并保存。模糊在原位渲染:任意模糊度下,画面的比例、排布与取景完全一致;屏幕边缘一个模糊半径内的画面向遮罩过渡,被界面边饰覆盖,通常不可见。模糊时叠加轻微噪声,消除渐变色带;模糊为零时媒体完全绕过滤镜管线,像素保持不变。
|
|
16
16
|
|
package/lib/client.js
CHANGED
|
@@ -91,6 +91,7 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
fitContain: "适应",
|
|
92
92
|
fitFill: "拉伸",
|
|
93
93
|
fitNone: "原始",
|
|
94
|
+
fitFree: "自由",
|
|
94
95
|
adjust: "调整画面",
|
|
95
96
|
adjustDone: "完成",
|
|
96
97
|
adjustHint: "拖动平移 · 滚轮缩放 · Esc 或点「完成」退出",
|
|
@@ -168,6 +169,7 @@ window.__ModuleLoader__.load({
|
|
|
168
169
|
fitContain: "Contain",
|
|
169
170
|
fitFill: "Stretch",
|
|
170
171
|
fitNone: "Original",
|
|
172
|
+
fitFree: "Free",
|
|
171
173
|
adjust: "Adjust",
|
|
172
174
|
adjustDone: "Done",
|
|
173
175
|
adjustHint: "Drag to pan · wheel to zoom · Esc or Done to exit",
|
|
@@ -226,7 +228,7 @@ window.__ModuleLoader__.load({
|
|
|
226
228
|
offsetY: 0,
|
|
227
229
|
};
|
|
228
230
|
|
|
229
|
-
const FIT_MODES = ["cover", "contain", "fill", "none"];
|
|
231
|
+
const FIT_MODES = ["cover", "contain", "fill", "none", "free"];
|
|
230
232
|
|
|
231
233
|
// ── Persisted selection ─────────────────────────────────────────────────────
|
|
232
234
|
function clampNum(v, lo, hi, fallback) {
|
|
@@ -273,11 +275,12 @@ window.__ModuleLoader__.load({
|
|
|
273
275
|
autoScrim: o.autoScrim !== false,
|
|
274
276
|
lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
|
|
275
277
|
fit: FIT_MODES.indexOf(o.fit) >= 0 ? o.fit : DEFAULTS.fit,
|
|
276
|
-
// Older builds could persist
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
// Older builds could persist unclamped crops — clampCrop() at store
|
|
279
|
+
// creation re-seats both into the envelope the CURRENT fit mode
|
|
280
|
+
// allows (free: zoom ≥ 0.1 with keep-visible pans; others: gap-free).
|
|
281
|
+
zoom: clampNum(o.zoom, 0.1, 4, DEFAULTS.zoom),
|
|
282
|
+
offsetX: clampNum(o.offsetX, -300, 300, DEFAULTS.offsetX),
|
|
283
|
+
offsetY: clampNum(o.offsetY, -300, 300, DEFAULTS.offsetY),
|
|
281
284
|
};
|
|
282
285
|
} catch {
|
|
283
286
|
return { id: "", ...DEFAULTS };
|
|
@@ -302,8 +305,8 @@ window.__ModuleLoader__.load({
|
|
|
302
305
|
inventory: { installDir: null, wallpapers: [], total: 0, portableCount: 0, playlists: [], error: null },
|
|
303
306
|
loaded: false,
|
|
304
307
|
};
|
|
305
|
-
//
|
|
306
|
-
//
|
|
308
|
+
// Re-seat any persisted crop into the envelope the persisted fit mode
|
|
309
|
+
// allows before the first render.
|
|
307
310
|
clampCrop();
|
|
308
311
|
|
|
309
312
|
const listeners = new Set();
|
|
@@ -729,17 +732,33 @@ window.__ModuleLoader__.load({
|
|
|
729
732
|
// never receive pointer events. Adjust mode moves the LIVE layer node into a
|
|
730
733
|
// topmost transparent overlay — the user edits the real thing, WYSIWYG.
|
|
731
734
|
//
|
|
732
|
-
// Crop geometry
|
|
733
|
-
//
|
|
734
|
-
// and pan is clamped to ±(zoom
|
|
735
|
-
//
|
|
735
|
+
// Crop geometry is chosen by the fit mode. The four object-fit modes keep a
|
|
736
|
+
// GAP-FREE invariant: the media box covers the viewport exactly at zoom 1,
|
|
737
|
+
// zoom floors at 1, and pan is clamped to ±(zoom−1)/2·100 % — the image
|
|
738
|
+
// always fills the screen. The "free" mode drops that invariant on purpose:
|
|
739
|
+
// the media sits at natural size on a black canvas, zoom runs 0.1–4×, and
|
|
740
|
+
// pan is only clamped to keep a sliver (≥10 % of the box) on screen so the
|
|
741
|
+
// wallpaper can never be dragged entirely out of sight.
|
|
736
742
|
function clampCrop() {
|
|
737
|
-
selection.zoom = Math.round(Math.min(4, Math.max(1, selection.zoom)) * 100) / 100;
|
|
743
|
+
selection.zoom = Math.round(Math.min(4, Math.max(isFreeFit() ? 0.1 : 1, selection.zoom)) * 100) / 100;
|
|
744
|
+
if (isFreeFit()) {
|
|
745
|
+
// Content spans ±zoom·50 % of the box around its centre; the pan offset
|
|
746
|
+
// shifts it by its own % of the box. |offset| ≤ (zoom+1)·50 − 10 keeps
|
|
747
|
+
// at least 10 % of the media edge-to-edge visible in both axes.
|
|
748
|
+
const reach = (selection.zoom + 1) * 50 - 10;
|
|
749
|
+
selection.offsetX = Math.min(reach, Math.max(-reach, selection.offsetX));
|
|
750
|
+
selection.offsetY = Math.min(reach, Math.max(-reach, selection.offsetY));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
738
753
|
const pan = (selection.zoom - 1) * 50;
|
|
739
754
|
selection.offsetX = Math.min(pan, Math.max(-pan, selection.offsetX));
|
|
740
755
|
selection.offsetY = Math.min(pan, Math.max(-pan, selection.offsetY));
|
|
741
756
|
}
|
|
742
757
|
|
|
758
|
+
function isFreeFit() {
|
|
759
|
+
return selection.fit === "free";
|
|
760
|
+
}
|
|
761
|
+
|
|
743
762
|
let adjustOverlay = null;
|
|
744
763
|
let adjustCleanup = null;
|
|
745
764
|
|
|
@@ -797,7 +816,7 @@ window.__ModuleLoader__.load({
|
|
|
797
816
|
const h = overlay.clientHeight || 1;
|
|
798
817
|
selection.offsetX += ((e.clientX - drag.x) / w) * 100;
|
|
799
818
|
selection.offsetY += ((e.clientY - drag.y) / h) * 100;
|
|
800
|
-
clampCrop(); // gap-free
|
|
819
|
+
clampCrop(); // mode envelope: gap-free pan headroom, or free keep-visible
|
|
801
820
|
drag = { x: e.clientX, y: e.clientY };
|
|
802
821
|
applyEffects();
|
|
803
822
|
};
|
|
@@ -815,10 +834,10 @@ window.__ModuleLoader__.load({
|
|
|
815
834
|
};
|
|
816
835
|
const onWheel = (e) => {
|
|
817
836
|
if (typeof e.preventDefault === "function") e.preventDefault();
|
|
818
|
-
//
|
|
819
|
-
// shrunken box in empty space
|
|
820
|
-
// re-clamps the pan
|
|
821
|
-
//
|
|
837
|
+
// clampCrop enforces the mode's zoom floor: the gap-free modes never
|
|
838
|
+
// zoom below 1 (a shrunken box floating in empty space is not a crop)
|
|
839
|
+
// and tightening zoom re-clamps the pan so zooming back to 1 pulls the
|
|
840
|
+
// framing home; free mode shrinks down to 0.1× on its black canvas.
|
|
822
841
|
selection.zoom *= Math.exp(-(e.deltaY || 0) * 0.0012);
|
|
823
842
|
clampCrop();
|
|
824
843
|
applyEffects();
|
|
@@ -1044,8 +1063,11 @@ window.__ModuleLoader__.load({
|
|
|
1044
1063
|
selection.wallpaperBlur > 0 ? (0.02 + selection.wallpaperBlur * 0.0004).toFixed(4) : "0");
|
|
1045
1064
|
// Canvas fit: object-fit base mode plus the user's manual crop — pan
|
|
1046
1065
|
// offsets (percent of the media box) and zoom, applied as a transform so
|
|
1047
|
-
// no remount is ever needed while dragging.
|
|
1048
|
-
|
|
1066
|
+
// no remount is ever needed while dragging. "free" draws at natural size
|
|
1067
|
+
// (object-fit: none) and paints the layer black, so exposed edges read as
|
|
1068
|
+
// a canvas instead of whatever sits behind the app frame.
|
|
1069
|
+
s.setProperty("--webg-fit", selection.fit === "free" ? "none" : selection.fit);
|
|
1070
|
+
s.setProperty("--webg-layer-bg", selection.fit === "free" ? "#000" : "transparent");
|
|
1049
1071
|
s.setProperty("--webg-zoom", String(selection.zoom));
|
|
1050
1072
|
s.setProperty("--webg-offset-x", selection.offsetX + "%");
|
|
1051
1073
|
s.setProperty("--webg-offset-y", selection.offsetY + "%");
|
|
@@ -1071,6 +1093,7 @@ window.__ModuleLoader__.load({
|
|
|
1071
1093
|
s.removeProperty("--webg-noise-display");
|
|
1072
1094
|
s.removeProperty("--webg-noise-opacity");
|
|
1073
1095
|
s.removeProperty("--webg-fit");
|
|
1096
|
+
s.removeProperty("--webg-layer-bg");
|
|
1074
1097
|
s.removeProperty("--webg-zoom");
|
|
1075
1098
|
s.removeProperty("--webg-offset-x");
|
|
1076
1099
|
s.removeProperty("--webg-offset-y");
|
|
@@ -1268,6 +1291,10 @@ window.__ModuleLoader__.load({
|
|
|
1268
1291
|
|
|
1269
1292
|
function onFit(e) {
|
|
1270
1293
|
selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
|
|
1294
|
+
// Re-seat the crop into the new mode's envelope: leaving free for a
|
|
1295
|
+
// gap-free mode pulls zoom back up to 1 and reins the pan in; the other
|
|
1296
|
+
// direction only ever relaxes bounds, so the framing carries over.
|
|
1297
|
+
clampCrop();
|
|
1271
1298
|
persistSelection();
|
|
1272
1299
|
applyEffects();
|
|
1273
1300
|
emit();
|
|
@@ -1496,19 +1523,20 @@ window.__ModuleLoader__.load({
|
|
|
1496
1523
|
|
|
1497
1524
|
function EffectsPanel(t, sel) {
|
|
1498
1525
|
return React.createElement(React.Fragment, null,
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1526
|
+
// The fit select covers all wallpaper types: for video/stills it picks
|
|
1527
|
+
// the object-fit base mode, and "free" additionally unlocks shrink +
|
|
1528
|
+
// off-edge dragging for every kind — an iframe ignores object-fit, but
|
|
1529
|
+
// the zoom/pan transform applies to it the same way. The manual crop
|
|
1530
|
+
// (drag / wheel in the adjust overlay) is a plain CSS transform and
|
|
1531
|
+
// applies to EVERY kind, so 调整画面 / 重置裁剪 are always available.
|
|
1504
1532
|
React.createElement("div", { className: "webg-row webg-fit-row" },
|
|
1505
|
-
|
|
1506
|
-
|
|
1533
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
|
|
1534
|
+
React.createElement("select", {
|
|
1507
1535
|
className: "webg-select webg-fit-select",
|
|
1508
1536
|
value: sel.fit,
|
|
1509
1537
|
onChange: onFit,
|
|
1510
1538
|
},
|
|
1511
|
-
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone].map((label, i) =>
|
|
1539
|
+
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone, t.fitFree].map((label, i) =>
|
|
1512
1540
|
React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
|
|
1513
1541
|
)),
|
|
1514
1542
|
React.createElement("button", {
|
|
@@ -1623,10 +1651,13 @@ window.__ModuleLoader__.load({
|
|
|
1623
1651
|
// Everything reads DSH design tokens (--dsw-*) so the UI blends into the
|
|
1624
1652
|
// shell and follows light/dark theme switches automatically.
|
|
1625
1653
|
const CSS = `
|
|
1626
|
-
/* Wallpaper layers: fixed, sunk below the app frame, crossfading.
|
|
1654
|
+
/* Wallpaper layers: fixed, sunk below the app frame, crossfading. The
|
|
1655
|
+
background paints black only in the "free" fit mode, where the media
|
|
1656
|
+
can be dragged off the edges and the exposed area reads as a canvas. */
|
|
1627
1657
|
.webg-layer {
|
|
1628
1658
|
position: fixed; inset: 0; z-index: -2; overflow: hidden;
|
|
1629
1659
|
pointer-events: none; opacity: 1;
|
|
1660
|
+
background: var(--webg-layer-bg, transparent);
|
|
1630
1661
|
transition: opacity ${CROSSFADE_MS}ms ease;
|
|
1631
1662
|
}
|
|
1632
1663
|
.webg-layer--enter { opacity: 0; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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",
|