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 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 Original — then frame the shot yourself. **Adjust** opens the live wallpaper fullscreen: scroll to zoom in (up to 4×), drag to pan within the zoomed framing — the crop is clamped so the image always fills the screen with no exposed edges. Esc or Done finishes, the framing persists, and **Reset crop** restores the default in one click. Web wallpapers take the zoom/pan framing too; the base-layout select applies to video and stills and hides itself for web.
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
- **手动取景。** 先选基础排布(填充、适应、拉伸、原始),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮放大(最高 4×),拖动在放大后的画面内平移。裁剪范围经过限制,画面始终铺满屏幕,不露出边缘。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸同样支持缩放与平移;排布下拉只对视频与静态图生效,网页壁纸时自动隐藏。
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 zoom < 1 / unclamped pans — clampCrop()
277
- // at store creation re-seats both into the gap-free envelope.
278
- zoom: clampNum(o.zoom, 0.25, 4, DEFAULTS.zoom),
279
- offsetX: clampNum(o.offsetX, -200, 200, DEFAULTS.offsetX),
280
- offsetY: clampNum(o.offsetY, -200, 200, DEFAULTS.offsetY),
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
- // Older builds allowed zoom < 1 and unclamped pans — re-seat any persisted
306
- // crop into the gap-free envelope before the first render.
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 invariants: the media box covers the viewport exactly at
733
- // zoom 1; zooming creates headroom, panning spends it. Zoom floors at 1
734
- // and pan is clamped to ±(zoom-1)/2·100 %, so a gap-free fullscreen image
735
- // is guaranteed at every setting.
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 guarantee: pan never outruns the zoom headroom
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
- // Crop semantics: zoom IN only (1–4×). Zooming below cover would float a
819
- // shrunken box in empty space — that is not a crop. Tightening zoom also
820
- // re-clamps the pan into the new headroom, so zooming back to 1 pulls
821
- // the framing home instead of leaving edge gaps.
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
- s.setProperty("--webg-fit", selection.fit);
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
- // object-fit only means something for video/still wallpapers — an
1500
- // iframe ignores it and always fills its layer — so the base-mode
1501
- // select is hidden for web. The manual crop (drag / wheel in the
1502
- // adjust overlay) is a plain CSS transform and applies to EVERY kind,
1503
- // so 调整画面 / 重置裁剪 stay available even for iframes.
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
- sel.type !== "web" && React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
1506
- sel.type !== "web" && React.createElement("select", {
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.4",
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",