wallpaper-engine-dsh 1.0.4 → 1.0.6
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 +186 -46
- 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, 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.
|
|
16
16
|
|
package/README.zh.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩。下拉菜单、弹窗等浮层显示在 DSH 原生的不透明表面上,不受影响。
|
|
12
12
|
|
|
13
|
-
**手动取景。** 先选基础排布(
|
|
13
|
+
**手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并可旋转任意角度(±90° 按钮或 Alt+拖动),同时始终保留一小块在屏幕内,不会拖丢。「镜像」水平翻转壁纸,所有排布下都可用——翻转不会破坏铺满。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移/旋转取景(变换直接作用于 iframe)。
|
|
14
14
|
|
|
15
15
|
**四个实时滑杆。** 壁纸模糊、暗化、边框、玻璃,每一步调节立即生效并保存。模糊在原位渲染:任意模糊度下,画面的比例、排布与取景完全一致;屏幕边缘一个模糊半径内的画面向遮罩过渡,被界面边饰覆盖,通常不可见。模糊时叠加轻微噪声,消除渐变色带;模糊为零时媒体完全绕过滤镜管线,像素保持不变。
|
|
16
16
|
|
package/lib/client.js
CHANGED
|
@@ -91,9 +91,16 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
fitContain: "适应",
|
|
92
92
|
fitFill: "拉伸",
|
|
93
93
|
fitNone: "原始",
|
|
94
|
+
fitFree: "自由",
|
|
94
95
|
adjust: "调整画面",
|
|
95
96
|
adjustDone: "完成",
|
|
96
97
|
adjustHint: "拖动平移 · 滚轮缩放 · Esc 或点「完成」退出",
|
|
98
|
+
adjustHintFree: "拖动平移 · 滚轮缩放 · Alt+拖动旋转 · Esc 或点「完成」退出",
|
|
99
|
+
mirror: "镜像",
|
|
100
|
+
rotateCcw: "⟲",
|
|
101
|
+
rotateCw: "⟳",
|
|
102
|
+
rotateCcwTitle: "逆时针旋转 90°",
|
|
103
|
+
rotateCwTitle: "顺时针旋转 90°",
|
|
97
104
|
resetCrop: "重置裁剪",
|
|
98
105
|
statusGroup: (name, total, usable, interval, order) =>
|
|
99
106
|
"列表「" + name + "」:" + total + " 项 · " + usable + " 可用 · 每 " + interval + " 分钟 · " + order,
|
|
@@ -168,9 +175,16 @@ window.__ModuleLoader__.load({
|
|
|
168
175
|
fitContain: "Contain",
|
|
169
176
|
fitFill: "Stretch",
|
|
170
177
|
fitNone: "Original",
|
|
178
|
+
fitFree: "Free",
|
|
171
179
|
adjust: "Adjust",
|
|
172
180
|
adjustDone: "Done",
|
|
173
181
|
adjustHint: "Drag to pan · wheel to zoom · Esc or Done to exit",
|
|
182
|
+
adjustHintFree: "Drag to pan · wheel to zoom · Alt+drag to rotate · Esc or Done to exit",
|
|
183
|
+
mirror: "Mirror",
|
|
184
|
+
rotateCcw: "⟲",
|
|
185
|
+
rotateCw: "⟳",
|
|
186
|
+
rotateCcwTitle: "Rotate 90° counterclockwise",
|
|
187
|
+
rotateCwTitle: "Rotate 90° clockwise",
|
|
174
188
|
resetCrop: "Reset crop",
|
|
175
189
|
statusGroup: (name, total, usable, interval, order) =>
|
|
176
190
|
"List \"" + name + "\": " + total + " items · " + usable + " usable · every " + interval + " min · " + order,
|
|
@@ -224,9 +238,11 @@ window.__ModuleLoader__.load({
|
|
|
224
238
|
zoom: 1,
|
|
225
239
|
offsetX: 0,
|
|
226
240
|
offsetY: 0,
|
|
241
|
+
rotation: 0, // free-fit only; degrees, positive = clockwise
|
|
242
|
+
mirrored: false, // horizontal flip, available in every fit mode
|
|
227
243
|
};
|
|
228
244
|
|
|
229
|
-
const FIT_MODES = ["cover", "contain", "fill", "none"];
|
|
245
|
+
const FIT_MODES = ["cover", "contain", "fill", "none", "free"];
|
|
230
246
|
|
|
231
247
|
// ── Persisted selection ─────────────────────────────────────────────────────
|
|
232
248
|
function clampNum(v, lo, hi, fallback) {
|
|
@@ -273,11 +289,14 @@ window.__ModuleLoader__.load({
|
|
|
273
289
|
autoScrim: o.autoScrim !== false,
|
|
274
290
|
lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
|
|
275
291
|
fit: FIT_MODES.indexOf(o.fit) >= 0 ? o.fit : DEFAULTS.fit,
|
|
276
|
-
// Older builds could persist
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
292
|
+
// Older builds could persist unclamped crops — clampCrop() at store
|
|
293
|
+
// creation re-seats both into the envelope the CURRENT fit mode
|
|
294
|
+
// allows (free: zoom ≥ 0.1 with keep-visible pans; others: gap-free).
|
|
295
|
+
zoom: clampNum(o.zoom, 0.1, 4, DEFAULTS.zoom),
|
|
296
|
+
offsetX: clampNum(o.offsetX, -300, 300, DEFAULTS.offsetX),
|
|
297
|
+
offsetY: clampNum(o.offsetY, -300, 300, DEFAULTS.offsetY),
|
|
298
|
+
rotation: clampNum(o.rotation, -1080, 1080, DEFAULTS.rotation),
|
|
299
|
+
mirrored: o.mirrored === true,
|
|
281
300
|
};
|
|
282
301
|
} catch {
|
|
283
302
|
return { id: "", ...DEFAULTS };
|
|
@@ -302,8 +321,8 @@ window.__ModuleLoader__.load({
|
|
|
302
321
|
inventory: { installDir: null, wallpapers: [], total: 0, portableCount: 0, playlists: [], error: null },
|
|
303
322
|
loaded: false,
|
|
304
323
|
};
|
|
305
|
-
//
|
|
306
|
-
//
|
|
324
|
+
// Re-seat any persisted crop into the envelope the persisted fit mode
|
|
325
|
+
// allows before the first render.
|
|
307
326
|
clampCrop();
|
|
308
327
|
|
|
309
328
|
const listeners = new Set();
|
|
@@ -336,6 +355,8 @@ window.__ModuleLoader__.load({
|
|
|
336
355
|
zoom: selection.zoom,
|
|
337
356
|
offsetX: selection.offsetX,
|
|
338
357
|
offsetY: selection.offsetY,
|
|
358
|
+
rotation: selection.rotation,
|
|
359
|
+
mirrored: selection.mirrored,
|
|
339
360
|
}));
|
|
340
361
|
} catch { /* storage full / blocked → session-only state, not fatal */ }
|
|
341
362
|
}
|
|
@@ -729,17 +750,47 @@ window.__ModuleLoader__.load({
|
|
|
729
750
|
// never receive pointer events. Adjust mode moves the LIVE layer node into a
|
|
730
751
|
// topmost transparent overlay — the user edits the real thing, WYSIWYG.
|
|
731
752
|
//
|
|
732
|
-
// Crop geometry
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
753
|
+
// Crop geometry is chosen by the fit mode. The four object-fit modes keep a
|
|
754
|
+
// GAP-FREE invariant: the media box covers the viewport exactly at zoom 1,
|
|
755
|
+
// zoom floors at 1, pan is clamped to ±(zoom−1)/2·100 %, and rotation is
|
|
756
|
+
// forced to 0 — a rotated rectangle cannot cover the screen, so turning
|
|
757
|
+
// belongs to free mode only. Mirroring is allowed everywhere: a flip is
|
|
758
|
+
// symmetric and never breaks coverage. The "free" mode drops the gap-free
|
|
759
|
+
// invariant on purpose: the media sits at natural size on a black canvas,
|
|
760
|
+
// zoom runs 0.1–4×, rotation is arbitrary, and pan is only clamped to keep
|
|
761
|
+
// a sliver (≥10 % of the box) on screen so the wallpaper can never be
|
|
762
|
+
// dragged entirely out of sight.
|
|
736
763
|
function clampCrop() {
|
|
737
|
-
|
|
764
|
+
if (!isFreeFit()) selection.rotation = 0;
|
|
765
|
+
else {
|
|
766
|
+
// Keep the stored angle canonical (−180, 180]: Alt+drag can accumulate
|
|
767
|
+
// several full turns, and persisting raw degrees would come back from
|
|
768
|
+
// storage truncated at the ±1080 read clamp — a DIFFERENT visual angle.
|
|
769
|
+
// Modulo keeps every equivalent representation identical on disk.
|
|
770
|
+
selection.rotation = ((selection.rotation + 180) % 360 + 360) % 360 - 180;
|
|
771
|
+
}
|
|
772
|
+
selection.zoom = Math.round(Math.min(4, Math.max(isFreeFit() ? 0.1 : 1, selection.zoom)) * 100) / 100;
|
|
773
|
+
if (isFreeFit()) {
|
|
774
|
+
// Content spans ±zoom·50 % of the box around its centre; the pan offset
|
|
775
|
+
// shifts it by its own % of the box. Rotation expands the projected
|
|
776
|
+
// extent by k = |cos θ| + |sin θ| (up to √2 at 45°). |offset| ≤
|
|
777
|
+
// (zoom·k+1)·50 − 10 keeps ≥10 % of the media visible in both axes.
|
|
778
|
+
const rad = selection.rotation * Math.PI / 180;
|
|
779
|
+
const k = Math.abs(Math.cos(rad)) + Math.abs(Math.sin(rad));
|
|
780
|
+
const reach = (selection.zoom * k + 1) * 50 - 10;
|
|
781
|
+
selection.offsetX = Math.min(reach, Math.max(-reach, selection.offsetX));
|
|
782
|
+
selection.offsetY = Math.min(reach, Math.max(-reach, selection.offsetY));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
738
785
|
const pan = (selection.zoom - 1) * 50;
|
|
739
786
|
selection.offsetX = Math.min(pan, Math.max(-pan, selection.offsetX));
|
|
740
787
|
selection.offsetY = Math.min(pan, Math.max(-pan, selection.offsetY));
|
|
741
788
|
}
|
|
742
789
|
|
|
790
|
+
function isFreeFit() {
|
|
791
|
+
return selection.fit === "free";
|
|
792
|
+
}
|
|
793
|
+
|
|
743
794
|
let adjustOverlay = null;
|
|
744
795
|
let adjustCleanup = null;
|
|
745
796
|
|
|
@@ -767,12 +818,31 @@ window.__ModuleLoader__.load({
|
|
|
767
818
|
bar.className = "webg-adjust-bar";
|
|
768
819
|
const hint = document.createElement("span");
|
|
769
820
|
hint.className = "webg-adjust-hint";
|
|
770
|
-
hint.textContent = S().adjustHint;
|
|
821
|
+
hint.textContent = isFreeFit() ? S().adjustHintFree : S().adjustHint;
|
|
771
822
|
const done = document.createElement("button");
|
|
772
823
|
done.type = "button";
|
|
773
824
|
done.className = "webg-btn webg-btn--primary";
|
|
774
825
|
done.textContent = S().adjustDone;
|
|
775
826
|
bar.appendChild(hint);
|
|
827
|
+
// Free fit only: a rotated rectangle cannot cover the screen, so turning
|
|
828
|
+
// exists solely where exposed edges are allowed. ±90° buttons cover the
|
|
829
|
+
// common cases; arbitrary angles come from Alt+drag below.
|
|
830
|
+
if (isFreeFit()) {
|
|
831
|
+
for (const dir of [-90, 90]) {
|
|
832
|
+
const rot = document.createElement("button");
|
|
833
|
+
rot.type = "button";
|
|
834
|
+
rot.className = "webg-btn";
|
|
835
|
+
rot.textContent = dir < 0 ? S().rotateCcw : S().rotateCw;
|
|
836
|
+
rot.title = dir < 0 ? S().rotateCcwTitle : S().rotateCwTitle;
|
|
837
|
+
rot.addEventListener("click", () => {
|
|
838
|
+
selection.rotation += dir;
|
|
839
|
+
clampCrop();
|
|
840
|
+
applyEffects();
|
|
841
|
+
persistSelection();
|
|
842
|
+
});
|
|
843
|
+
bar.appendChild(rot);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
776
846
|
bar.appendChild(done);
|
|
777
847
|
overlay.appendChild(bar);
|
|
778
848
|
document.body.appendChild(overlay);
|
|
@@ -782,14 +852,34 @@ window.__ModuleLoader__.load({
|
|
|
782
852
|
// translate() in the transform list is applied AFTER scale() (right-to-
|
|
783
853
|
// left), so pan deltas are independent of zoom: dx px = dx/width %.
|
|
784
854
|
let drag = null;
|
|
855
|
+
// Alt+drag (free fit): swing the media around the overlay's centre —
|
|
856
|
+
// the angle delta between pointer positions becomes the rotation delta.
|
|
857
|
+
let rot = null; // { a0: starting pointer angle, base: starting rotation }
|
|
858
|
+
const centerAngle = (e) => {
|
|
859
|
+
const cx = (overlay.clientWidth || 1) / 2;
|
|
860
|
+
const cy = (overlay.clientHeight || 1) / 2;
|
|
861
|
+
return Math.atan2(e.clientY - cy, e.clientX - cx);
|
|
862
|
+
};
|
|
785
863
|
const onPointerDown = (e) => {
|
|
786
864
|
if (e.target === done || bar.contains(e.target)) return;
|
|
787
|
-
|
|
865
|
+
if (e.altKey && isFreeFit()) {
|
|
866
|
+
rot = { a0: centerAngle(e), base: selection.rotation };
|
|
867
|
+
} else {
|
|
868
|
+
drag = { x: e.clientX, y: e.clientY };
|
|
869
|
+
}
|
|
870
|
+
// Capture for EITHER gesture: pointer moves must keep arriving even
|
|
871
|
+
// when the cursor leaves the window mid-swing.
|
|
788
872
|
if (overlay.setPointerCapture && e.pointerId !== undefined) {
|
|
789
873
|
try { overlay.setPointerCapture(e.pointerId); } catch {}
|
|
790
874
|
}
|
|
791
875
|
};
|
|
792
876
|
const onPointerMove = (e) => {
|
|
877
|
+
if (rot) {
|
|
878
|
+
selection.rotation = rot.base + (centerAngle(e) - rot.a0) * 180 / Math.PI;
|
|
879
|
+
clampCrop();
|
|
880
|
+
applyEffects();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
793
883
|
if (!drag) return;
|
|
794
884
|
// translate % refers to the media box, which is the viewport exactly —
|
|
795
885
|
// divide the pixel delta by it so the content tracks the cursor 1:1.
|
|
@@ -797,28 +887,26 @@ window.__ModuleLoader__.load({
|
|
|
797
887
|
const h = overlay.clientHeight || 1;
|
|
798
888
|
selection.offsetX += ((e.clientX - drag.x) / w) * 100;
|
|
799
889
|
selection.offsetY += ((e.clientY - drag.y) / h) * 100;
|
|
800
|
-
clampCrop(); // gap-free
|
|
890
|
+
clampCrop(); // mode envelope: gap-free pan headroom, or free keep-visible
|
|
801
891
|
drag = { x: e.clientX, y: e.clientY };
|
|
802
892
|
applyEffects();
|
|
803
893
|
};
|
|
804
|
-
const
|
|
894
|
+
const endPointer = () => {
|
|
895
|
+
if (rot) { rot = null; persistSelection(); return; }
|
|
805
896
|
if (!drag) return;
|
|
806
897
|
drag = null;
|
|
807
898
|
persistSelection();
|
|
808
899
|
};
|
|
900
|
+
const onPointerUp = endPointer;
|
|
809
901
|
// pointercancel (touch gesture takeover): end the drag without losing the
|
|
810
902
|
// current offsets — otherwise drag stays "stuck" until the next pointerup.
|
|
811
|
-
const onPointerCancel =
|
|
812
|
-
if (!drag) return;
|
|
813
|
-
drag = null;
|
|
814
|
-
persistSelection();
|
|
815
|
-
};
|
|
903
|
+
const onPointerCancel = endPointer;
|
|
816
904
|
const onWheel = (e) => {
|
|
817
905
|
if (typeof e.preventDefault === "function") e.preventDefault();
|
|
818
|
-
//
|
|
819
|
-
// shrunken box in empty space
|
|
820
|
-
// re-clamps the pan
|
|
821
|
-
//
|
|
906
|
+
// clampCrop enforces the mode's zoom floor: the gap-free modes never
|
|
907
|
+
// zoom below 1 (a shrunken box floating in empty space is not a crop)
|
|
908
|
+
// and tightening zoom re-clamps the pan so zooming back to 1 pulls the
|
|
909
|
+
// framing home; free mode shrinks down to 0.1× on its black canvas.
|
|
822
910
|
selection.zoom *= Math.exp(-(e.deltaY || 0) * 0.0012);
|
|
823
911
|
clampCrop();
|
|
824
912
|
applyEffects();
|
|
@@ -851,6 +939,7 @@ window.__ModuleLoader__.load({
|
|
|
851
939
|
selection.zoom = 1;
|
|
852
940
|
selection.offsetX = 0;
|
|
853
941
|
selection.offsetY = 0;
|
|
942
|
+
selection.rotation = 0; // framing geometry home; mirroring keeps its own toggle
|
|
854
943
|
persistSelection();
|
|
855
944
|
applyEffects();
|
|
856
945
|
emit();
|
|
@@ -1009,7 +1098,7 @@ window.__ModuleLoader__.load({
|
|
|
1009
1098
|
const key = [
|
|
1010
1099
|
dark, selection.scrim, selection.border, selection.blur, selection.wallpaperBlur,
|
|
1011
1100
|
selection.autoScrim, sampledLuminance, selection.fit, selection.zoom,
|
|
1012
|
-
selection.offsetX, selection.offsetY,
|
|
1101
|
+
selection.offsetX, selection.offsetY, selection.rotation, selection.mirrored,
|
|
1013
1102
|
].join("\u0000");
|
|
1014
1103
|
if (key === lastEffectKey) return; // nothing it reads changed → no writes, no reflow
|
|
1015
1104
|
lastEffectKey = key;
|
|
@@ -1044,11 +1133,16 @@ window.__ModuleLoader__.load({
|
|
|
1044
1133
|
selection.wallpaperBlur > 0 ? (0.02 + selection.wallpaperBlur * 0.0004).toFixed(4) : "0");
|
|
1045
1134
|
// Canvas fit: object-fit base mode plus the user's manual crop — pan
|
|
1046
1135
|
// offsets (percent of the media box) and zoom, applied as a transform so
|
|
1047
|
-
// no remount is ever needed while dragging.
|
|
1048
|
-
|
|
1136
|
+
// no remount is ever needed while dragging. "free" draws at natural size
|
|
1137
|
+
// (object-fit: none) and paints the layer black, so exposed edges read as
|
|
1138
|
+
// a canvas instead of whatever sits behind the app frame.
|
|
1139
|
+
s.setProperty("--webg-fit", selection.fit === "free" ? "none" : selection.fit);
|
|
1140
|
+
s.setProperty("--webg-layer-bg", selection.fit === "free" ? "#000" : "transparent");
|
|
1049
1141
|
s.setProperty("--webg-zoom", String(selection.zoom));
|
|
1050
1142
|
s.setProperty("--webg-offset-x", selection.offsetX + "%");
|
|
1051
1143
|
s.setProperty("--webg-offset-y", selection.offsetY + "%");
|
|
1144
|
+
s.setProperty("--webg-rotate", selection.rotation + "deg");
|
|
1145
|
+
s.setProperty("--webg-mirror", selection.mirrored ? "-1" : "1");
|
|
1052
1146
|
|
|
1053
1147
|
// Write the scrim colour directly too, then force a synchronous layout, so
|
|
1054
1148
|
// slider feedback lands on this frame even on stalled compositors.
|
|
@@ -1071,9 +1165,12 @@ window.__ModuleLoader__.load({
|
|
|
1071
1165
|
s.removeProperty("--webg-noise-display");
|
|
1072
1166
|
s.removeProperty("--webg-noise-opacity");
|
|
1073
1167
|
s.removeProperty("--webg-fit");
|
|
1168
|
+
s.removeProperty("--webg-layer-bg");
|
|
1074
1169
|
s.removeProperty("--webg-zoom");
|
|
1075
1170
|
s.removeProperty("--webg-offset-x");
|
|
1076
1171
|
s.removeProperty("--webg-offset-y");
|
|
1172
|
+
s.removeProperty("--webg-rotate");
|
|
1173
|
+
s.removeProperty("--webg-mirror");
|
|
1077
1174
|
const scrim = document.getElementById(SCRIM_ID);
|
|
1078
1175
|
if (scrim) scrim.style.background = "";
|
|
1079
1176
|
}
|
|
@@ -1268,6 +1365,19 @@ window.__ModuleLoader__.load({
|
|
|
1268
1365
|
|
|
1269
1366
|
function onFit(e) {
|
|
1270
1367
|
selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
|
|
1368
|
+
// Re-seat the crop into the new mode's envelope: leaving free for a
|
|
1369
|
+
// gap-free mode pulls zoom back up to 1, reins the pan in and zeroes the
|
|
1370
|
+
// rotation; the other direction only ever relaxes bounds, so the framing
|
|
1371
|
+
// carries over. Mirroring survives every switch — a flip never breaks
|
|
1372
|
+
// coverage.
|
|
1373
|
+
clampCrop();
|
|
1374
|
+
persistSelection();
|
|
1375
|
+
applyEffects();
|
|
1376
|
+
emit();
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function onToggleMirror() {
|
|
1380
|
+
selection.mirrored = !selection.mirrored;
|
|
1271
1381
|
persistSelection();
|
|
1272
1382
|
applyEffects();
|
|
1273
1383
|
emit();
|
|
@@ -1496,21 +1606,29 @@ window.__ModuleLoader__.load({
|
|
|
1496
1606
|
|
|
1497
1607
|
function EffectsPanel(t, sel) {
|
|
1498
1608
|
return React.createElement(React.Fragment, null,
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1609
|
+
// The fit select covers all wallpaper types: for video/stills it picks
|
|
1610
|
+
// the object-fit base mode, and "free" additionally unlocks shrink +
|
|
1611
|
+
// off-edge dragging for every kind — an iframe ignores object-fit, but
|
|
1612
|
+
// the zoom/pan transform applies to it the same way. The manual crop
|
|
1613
|
+
// (drag / wheel in the adjust overlay) is a plain CSS transform and
|
|
1614
|
+
// applies to EVERY kind, so 调整画面 / 重置裁剪 are always available.
|
|
1504
1615
|
React.createElement("div", { className: "webg-row webg-fit-row" },
|
|
1505
|
-
|
|
1506
|
-
|
|
1616
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
|
|
1617
|
+
React.createElement("select", {
|
|
1507
1618
|
className: "webg-select webg-fit-select",
|
|
1508
1619
|
value: sel.fit,
|
|
1509
1620
|
onChange: onFit,
|
|
1510
1621
|
},
|
|
1511
|
-
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone].map((label, i) =>
|
|
1622
|
+
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone, t.fitFree].map((label, i) =>
|
|
1512
1623
|
React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
|
|
1513
1624
|
)),
|
|
1625
|
+
// Mirror is a coverage-preserving flip: valid in EVERY fit mode and
|
|
1626
|
+
// for every wallpaper kind (the transform applies to iframes too).
|
|
1627
|
+
React.createElement("button", {
|
|
1628
|
+
className: "webg-btn" + (sel.mirrored ? " webg-btn--active" : ""),
|
|
1629
|
+
type: "button",
|
|
1630
|
+
onClick: onToggleMirror,
|
|
1631
|
+
}, t.mirror),
|
|
1514
1632
|
React.createElement("button", {
|
|
1515
1633
|
className: "webg-btn", type: "button", onClick: enterAdjust,
|
|
1516
1634
|
disabled: sel.adjusting,
|
|
@@ -1623,10 +1741,13 @@ window.__ModuleLoader__.load({
|
|
|
1623
1741
|
// Everything reads DSH design tokens (--dsw-*) so the UI blends into the
|
|
1624
1742
|
// shell and follows light/dark theme switches automatically.
|
|
1625
1743
|
const CSS = `
|
|
1626
|
-
/* Wallpaper layers: fixed, sunk below the app frame, crossfading.
|
|
1744
|
+
/* Wallpaper layers: fixed, sunk below the app frame, crossfading. The
|
|
1745
|
+
background paints black only in the "free" fit mode, where the media
|
|
1746
|
+
can be dragged off the edges and the exposed area reads as a canvas. */
|
|
1627
1747
|
.webg-layer {
|
|
1628
1748
|
position: fixed; inset: 0; z-index: -2; overflow: hidden;
|
|
1629
1749
|
pointer-events: none; opacity: 1;
|
|
1750
|
+
background: var(--webg-layer-bg, transparent);
|
|
1630
1751
|
transition: opacity ${CROSSFADE_MS}ms ease;
|
|
1631
1752
|
}
|
|
1632
1753
|
.webg-layer--enter { opacity: 0; }
|
|
@@ -1649,9 +1770,13 @@ window.__ModuleLoader__.load({
|
|
|
1649
1770
|
display: block;
|
|
1650
1771
|
background: transparent; border: 0;
|
|
1651
1772
|
filter: var(--webg-media-filter, none);
|
|
1652
|
-
/* Manual crop:
|
|
1653
|
-
|
|
1654
|
-
|
|
1773
|
+
/* Manual crop: the transform list is ordered pan → rotate → zoom →
|
|
1774
|
+
mirror. translate comes first so pan offsets stay screen-aligned no
|
|
1775
|
+
matter the rotation; mirror is innermost so the flip happens in the
|
|
1776
|
+
media's own axes. */
|
|
1777
|
+
transform: translate(var(--webg-offset-x, 0%), var(--webg-offset-y, 0%))
|
|
1778
|
+
rotate(var(--webg-rotate, 0deg)) scale(var(--webg-zoom, 1))
|
|
1779
|
+
scaleX(var(--webg-mirror, 1));
|
|
1655
1780
|
transform-origin: center center;
|
|
1656
1781
|
}
|
|
1657
1782
|
|
|
@@ -1772,17 +1897,32 @@ window.__ModuleLoader__.load({
|
|
|
1772
1897
|
border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.5));
|
|
1773
1898
|
}
|
|
1774
1899
|
.webg-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1775
|
-
/* brand-primary is monochrome (near-black light / near-white dark)
|
|
1776
|
-
|
|
1900
|
+
/* brand-primary is monochrome (near-black light / near-white dark). The
|
|
1901
|
+
shell's brand-primary-INVERT token aliases the same shade as brand-
|
|
1902
|
+
primary itself in both themes (no contrast), so anything sitting ON a
|
|
1903
|
+
brand-primary fill takes its contrast colour from the inverted label
|
|
1904
|
+
ramp instead — near-white on the dark light-theme fill, dark bluish-800
|
|
1905
|
+
on the near-white dark-theme fill. */
|
|
1906
|
+
body {
|
|
1907
|
+
--webg-on-brand: var(--dsw-alias-label-primary-inverted,
|
|
1908
|
+
var(--dsw-alias-label-primary-foreground, rgb(249, 250, 251)));
|
|
1909
|
+
}
|
|
1777
1910
|
.webg-btn--primary {
|
|
1778
1911
|
background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1779
1912
|
border-color: transparent;
|
|
1780
|
-
color: var(--
|
|
1913
|
+
color: var(--webg-on-brand);
|
|
1781
1914
|
}
|
|
1782
1915
|
.webg-btn--primary:hover:not(:disabled) {
|
|
1783
1916
|
background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1784
1917
|
opacity: 0.85;
|
|
1785
1918
|
}
|
|
1919
|
+
/* Toggle-style button showing an engaged state (e.g. 镜像): brand fill
|
|
1920
|
+
with the contrasting on-brand label. */
|
|
1921
|
+
.webg-btn--active {
|
|
1922
|
+
background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1923
|
+
border-color: transparent;
|
|
1924
|
+
color: var(--webg-on-brand);
|
|
1925
|
+
}
|
|
1786
1926
|
|
|
1787
1927
|
.webg-select, .webg-text {
|
|
1788
1928
|
padding: 5px 10px;
|
|
@@ -1842,7 +1982,7 @@ window.__ModuleLoader__.load({
|
|
|
1842
1982
|
-webkit-appearance: none;
|
|
1843
1983
|
width: 14px; height: 14px; border-radius: 50%;
|
|
1844
1984
|
margin-top: -5px; /* centers the 14px thumb on the 4px track */
|
|
1845
|
-
background: var(--
|
|
1985
|
+
background: var(--webg-on-brand);
|
|
1846
1986
|
border: 2.5px solid var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1847
1987
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
|
|
1848
1988
|
transition: transform 120ms ease;
|
|
@@ -1940,7 +2080,7 @@ window.__ModuleLoader__.load({
|
|
|
1940
2080
|
position: absolute; top: 4px; left: 4px; width: 18px; height: 18px;
|
|
1941
2081
|
border-radius: 4px;
|
|
1942
2082
|
background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1943
|
-
color: var(--
|
|
2083
|
+
color: var(--webg-on-brand);
|
|
1944
2084
|
font-size: 12px; line-height: 18px; text-align: center;
|
|
1945
2085
|
}
|
|
1946
2086
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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",
|