wallpaper-engine-dsh 0.1.6 → 0.1.7

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
@@ -9,8 +9,9 @@ A DSH bundle that turns your local **Wallpaper Engine** library into the live ba
9
9
  - **Whole-library browsing** — video and web wallpapers render live; scene and application wallpapers show their preview as a static background. Everything with a preview or playable file is selectable, badged by type (Video / Web / Still).
10
10
  - **Search** — filter the grid instantly by title or workshop id.
11
11
  - **Rotation lists** — any number of carousel lists, each with its own wallpapers, interval (1–120 min) and order (sequence/random). Your first playable Wallpaper Engine playlist is imported automatically; others can be imported into any list.
12
- - **Four live sliders** — wallpaper blur, scrim, border contrast, and glass frosting, all instant and persisted.
13
- - **Canvas fit** — cover/contain/stretch/original sizing plus five-way alignment for video and still wallpapers.
12
+ - **Four live sliders** — Wallpaper blur, Scrim, Borders, Glass — all instant and persisted.
13
+ - **Legibility on any wallpaper** — the veil opposes the text colour (white veil in light theme, black in dark) and a Smart veil toggle samples the wallpaper's brightness every few seconds, automatically strengthening the veil when a scene gets too bright or too dark for the current theme. Dropdowns, menus, and dialogs keep DSH's opaque surfaces untouched.
14
+ - **Canvas fit** — Cover/Contain/Stretch/Original sizing plus five-way alignment for video and still wallpapers.
14
15
  - **Resource monitor** — FPS readout with a low-framerate hint, plus auto-pause when the tab is hidden or the battery runs low (both optional).
15
16
  - **Bilingual** — the settings UI follows the shell's Language preference (Chinese/English), with a manual override in the picker.
16
17
  - **Crossfade** — wallpaper switches and clearing fade smoothly instead of snapping.
package/README.zh.md CHANGED
@@ -9,7 +9,8 @@
9
9
  - **全库浏览** —— 视频与网页壁纸动态渲染;场景与应用壁纸显示预览图作为静态背景。凡是有预览图或可播放文件的壁纸都可选用,按类型标注角标(视频 / 网页 / 静态)。
10
10
  - **搜索** —— 按标题或创意工坊 ID 即时筛选网格。
11
11
  - **轮播列表** —— 可建任意多个列表,各自拥有壁纸集合、切换间隔(1–120 分钟)和顺序(顺序/随机)。首次运行自动导入第一个可播放的 Wallpaper Engine 播放列表,其余列表可随时导入。
12
- - **四个实时滑杆** —— 壁纸模糊、暗化、边框对比、玻璃磨砂,即时生效并持久保存。
12
+ - **四个实时滑杆** —— 壁纸模糊、暗化、边框、玻璃,即时生效并持久保存。
13
+ - **任意壁纸都可读** —— 遮罩颜色与文字颜色相对(浅色主题提亮、深色主题压暗);"智能可视"开关每隔几秒采样一次壁纸亮度,画面过亮或过暗时自动加强遮罩。下拉菜单、弹窗等浮层保持 DSH 原生不透明表面,不受影响。
13
14
  - **画布排布** —— 视频与静态壁纸支持填充/适应/拉伸/原始四种排布和五向对齐。
14
15
  - **资源监控** —— 帧率读数与低帧率提示,页面隐藏或电量不足时自动暂停(均可开关)。
15
16
  - **中英双语** —— 设置界面跟随 DSH 的"语言"偏好自动切换,也可在面板内手动指定。
package/lib/client.js CHANGED
@@ -79,6 +79,8 @@ window.__ModuleLoader__.load({
79
79
  glass: "玻璃",
80
80
  pauseOnHidden: "页面隐藏时暂停",
81
81
  pauseOnBattery: "低电量时暂停",
82
+ autoScrim: "智能可视",
83
+ autoScrimTitle: "采样壁纸亮度,过亮或过暗时自动加强遮罩,保证文字可读",
82
84
  fpsTitle: "页面当前渲染帧率",
83
85
  lowFpsHint: "帧率偏低,可尝试暂停视频、增大壁纸模糊或换用静态壁纸",
84
86
  language: "语言",
@@ -157,6 +159,8 @@ window.__ModuleLoader__.load({
157
159
  glass: "Glass",
158
160
  pauseOnHidden: "Pause when tab hidden",
159
161
  pauseOnBattery: "Pause on low battery",
162
+ autoScrim: "Smart veil",
163
+ autoScrimTitle: "Samples wallpaper brightness and strengthens the veil when needed so text stays readable",
160
164
  fpsTitle: "Current page frame rate",
161
165
  lowFpsHint: "Low frame rate — try pausing the video, raising wallpaper blur, or using a still wallpaper",
162
166
  language: "Language",
@@ -218,6 +222,7 @@ window.__ModuleLoader__.load({
218
222
  rotationSeeded: false,
219
223
  pauseOnHidden: true,
220
224
  pauseOnBattery: true,
225
+ autoScrim: true,
221
226
  lang: "auto",
222
227
  fit: "cover",
223
228
  position: "center",
@@ -268,6 +273,7 @@ window.__ModuleLoader__.load({
268
273
  rotationSeeded: o.rotationSeeded === true,
269
274
  pauseOnHidden: o.pauseOnHidden !== false,
270
275
  pauseOnBattery: o.pauseOnBattery !== false,
276
+ autoScrim: o.autoScrim !== false,
271
277
  lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
272
278
  fit: FIT_MODES.indexOf(o.fit) >= 0 ? o.fit : DEFAULTS.fit,
273
279
  position: POSITIONS.indexOf(o.position) >= 0 ? o.position : DEFAULTS.position,
@@ -319,6 +325,7 @@ window.__ModuleLoader__.load({
319
325
  rotationSeeded: selection.rotationSeeded,
320
326
  pauseOnHidden: selection.pauseOnHidden,
321
327
  pauseOnBattery: selection.pauseOnBattery,
328
+ autoScrim: selection.autoScrim,
322
329
  lang: selection.lang,
323
330
  fit: selection.fit,
324
331
  position: selection.position,
@@ -660,6 +667,14 @@ window.__ModuleLoader__.load({
660
667
  media.setAttribute("decoding", "async");
661
668
  media.className = "webg-media";
662
669
  }
670
+ // First luminance sample as soon as the frame is decodable (video/still
671
+ // only — iframe contents are opaque to us). Later samples ride the timer.
672
+ if (sel.type === "video" || sel.type === "image") {
673
+ media.addEventListener(sel.type === "video" ? "loadeddata" : "load", () => {
674
+ sampleWallpaperLuminance();
675
+ applyEffects();
676
+ });
677
+ }
663
678
  media.addEventListener("error", () => {
664
679
  const id = sel.id;
665
680
  const count = (mediaFailures.get(id) || 0) + 1;
@@ -765,9 +780,52 @@ window.__ModuleLoader__.load({
765
780
  }
766
781
 
767
782
  // ── Effect application: push the knobs into CSS variables ───────────────────
783
+ // Wallpaper luminance sampled from the live media (null until the first
784
+ // sample; iframes of web wallpapers can never be sampled — opaque origin).
785
+ let sampledLuminance = null;
786
+ let sampleCanvas = null;
787
+
788
+ /** Average Rec.601 luminance (0..1) of the current video/img, or null. */
789
+ function sampleWallpaperLuminance() {
790
+ if (typeof document === "undefined") return;
791
+ const layer = document.getElementById(LAYER_ID);
792
+ const media = layer && (layer.querySelector("video") || layer.querySelector("img"));
793
+ if (!media) { sampledLuminance = null; return; }
794
+ try {
795
+ if (!sampleCanvas) {
796
+ const c = document.createElement("canvas");
797
+ if (typeof c.getContext !== "function") return; // no canvas → no sampling
798
+ c.width = 16; c.height = 16;
799
+ sampleCanvas = c;
800
+ }
801
+ const g = sampleCanvas.getContext("2d", { willReadFrequently: true });
802
+ g.drawImage(media, 0, 0, 16, 16);
803
+ const d = g.getImageData(0, 0, 16, 16).data;
804
+ let sum = 0;
805
+ for (let i = 0; i < d.length; i += 4) {
806
+ sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
807
+ }
808
+ sampledLuminance = sum / (d.length / 4) / 255;
809
+ } catch { /* frame not ready / unreadable → keep the previous sample */ }
810
+ }
811
+
768
812
  function applyEffects() {
769
813
  const s = document.body.style;
770
- s.setProperty("--webg-scrim-color", "rgba(0,0,0," + selection.scrim + ")");
814
+ // The veil OPPOSES the text colour: dark theme (light text) darkens bright
815
+ // wallpapers, light theme (dark text) brightens dark ones — either way the
816
+ // text side wins. This is what makes ANY wallpaper survivable.
817
+ const dark = Boolean(document.body.hasAttribute && document.body.hasAttribute("data-ds-dark-theme"));
818
+ let alpha = selection.scrim;
819
+ if (selection.autoScrim && sampledLuminance !== null) {
820
+ // Distance of the wallpaper's brightness from the "comfortable middle"
821
+ // towards the text colour's weakness, mapped to a veil-alpha floor.
822
+ const need = dark
823
+ ? Math.min(0.55, Math.max(0, sampledLuminance - 0.5))
824
+ : Math.min(0.55, Math.max(0, 0.5 - sampledLuminance));
825
+ alpha = Math.max(alpha, Math.round(need * 1000) / 1000);
826
+ }
827
+ const veilColor = "rgba(" + (dark ? "0,0,0" : "255,255,255") + "," + alpha + ")";
828
+ s.setProperty("--webg-scrim-color", veilColor);
771
829
  s.setProperty("--webg-border-alpha", String(selection.border));
772
830
  s.setProperty("--webg-blur", selection.blur + "px");
773
831
  // The glass "colour melt" scales with blur radius: 0 blur → no melt.
@@ -785,7 +843,7 @@ window.__ModuleLoader__.load({
785
843
  // Write the scrim colour directly too, then force a synchronous layout, so
786
844
  // slider feedback lands on this frame even on stalled compositors.
787
845
  const scrim = document.getElementById(SCRIM_ID);
788
- if (scrim) scrim.style.background = "rgba(0,0,0," + selection.scrim + ")";
846
+ if (scrim) scrim.style.background = veilColor;
789
847
  if (document.body && document.body.offsetHeight !== undefined) {
790
848
  void document.body.offsetHeight;
791
849
  }
@@ -947,6 +1005,12 @@ window.__ModuleLoader__.load({
947
1005
  persistSelection();
948
1006
  emit();
949
1007
  };
1008
+ const onToggleAutoScrim = () => {
1009
+ selection.autoScrim = !selection.autoScrim;
1010
+ persistSelection();
1011
+ applyEffects();
1012
+ emit();
1013
+ };
950
1014
 
951
1015
  const onScrim = (pct) => { selection.scrim = pct / 100; persistSelection(); applyEffects(); emit(); };
952
1016
  const onBorder = (pct) => { selection.border = pct / 100; persistSelection(); applyEffects(); emit(); };
@@ -1242,6 +1306,12 @@ window.__ModuleLoader__.load({
1242
1306
  }),
1243
1307
  t.pauseOnBattery,
1244
1308
  ),
1309
+ React.createElement("label", { className: "webg-toggle", title: t.autoScrimTitle },
1310
+ React.createElement("input", {
1311
+ type: "checkbox", checked: sel.autoScrim, onChange: onToggleAutoScrim,
1312
+ }),
1313
+ t.autoScrim,
1314
+ ),
1245
1315
  sel.fps !== null && React.createElement("span", {
1246
1316
  className: "webg-hint webg-fps" + (sel.fps < LOW_FPS ? " webg-fps--low" : ""),
1247
1317
  title: t.fpsTitle,
@@ -1612,9 +1682,28 @@ window.__ModuleLoader__.load({
1612
1682
  const unsubEffects = subscribe(applyEffects);
1613
1683
  syncLayers();
1614
1684
  applyEffects();
1685
+ // Luminance sampler: re-reads the wallpaper periodically so a bright
1686
+ // scene arriving mid-video re-raises the veil. (rAF-free: 2.5 s is
1687
+ // plenty for legibility, and an idle timer costs ~nothing.)
1688
+ let samplerId = null;
1689
+ if (typeof window !== "undefined" && typeof window.setInterval === "function") {
1690
+ samplerId = window.setInterval(() => {
1691
+ sampleWallpaperLuminance();
1692
+ applyEffects();
1693
+ }, 2500);
1694
+ }
1695
+ // A theme switch flips the veil colour immediately (black ↔ white).
1696
+ let observer = null;
1697
+ if (typeof MutationObserver === "function") {
1698
+ observer = new MutationObserver(() => applyEffects());
1699
+ observer.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
1700
+ }
1615
1701
  return () => {
1616
1702
  unsubLayers();
1617
1703
  unsubEffects();
1704
+ if (samplerId !== null) window.clearInterval(samplerId);
1705
+ if (observer) observer.disconnect();
1706
+ sampledLuminance = null;
1618
1707
  clearRotationTimer();
1619
1708
  // Cancel pending crossfade/clear timers so they never fire on
1620
1709
  // torn-down nodes after dispose.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallpaper-engine-dsh",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",