@taosee258/dsh-client-ui-skin-qingxiao 0.2.3 → 0.3.0

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
@@ -14,6 +14,7 @@
14
14
 
15
15
  - **背景画卷**:默认铺一张清宵插画当背景,亮色、暗色模式都能透出它。可以在设置面板里分别为两种模式换成你自己的图(PNG / JPG / WebP / GIF,7MB 以内),也能一键换回默认。
16
16
  - **全套配色重制**:侧边栏、按钮、输入框、对话区、代码块、滚动条……界面每一处都按清宵的配色重新调过;暗色模式是专门的「玄夜剑鸣」氛围。
17
+ - **开屏动画**:每次打开 DSH,AI 白描线稿会在夜幕上沿噪声场平滑显影,完整线稿定格片刻让你看清细节,一道剑光闪过翻成素描纸底,彩色画卷再带饱和度爬升浮现,最后淡出交给正常界面。按 Esc 或点击可跳过;系统开了「减少动态效果」自动不播;不想要可以在调色盘里关掉。
17
18
  - **毛玻璃效果**:打开插件的小窗口(比如用量统计)、左右两侧面板、插件页/自动化任务页等整页视图、设置弹窗乃至窗口顶栏,背后的画面都会被柔和地虚化掉——壁纸的轮廓透得出来,但绝不跟文字打架。想多糊可以多糊(0–30 档随意调),不想要可以整体关掉,也可以只关插件窗或只关侧栏。
18
19
  - **字永远看得清**:文字颜色专门为「压在插画背景上」调校过——正文浓黑/亮白,次要文字也比 DSH 原生的灰色深两档;背景还有可调浓度的柔光遮罩兜底。
19
20
  - **剑气流光**:可选的粒子装饰,青碧或鎏金两种色调;系统开了「减少动态效果」会自动静止。
@@ -31,7 +32,7 @@
31
32
  | 阅读清晰度 | 文字柔光开关 · 背景遮罩浓淡 · 面板实透 · 正文字号大小 |
32
33
  | 界面琉璃 | 对话区宽度 · 面板磨砂底 |
33
34
  | 毛玻璃虚化 | 总开关 · 插件窗/侧栏面板分开控制 · 虚化强度 0–30(0=关) |
34
- | 剑气流光 | 开关 · 粒子数量 · 流动速度 |
35
+ | 剑气流光 | 开屏动画(线稿上色)· 开关 · 粒子数量 · 流动速度 |
35
36
  | 复原 | 全部恢复默认 |
36
37
 
37
38
  ## 版本
@@ -82,7 +83,7 @@ dsh plugin --profile desktop add /path/to/dsh-client-ui-skin-qingxiao
82
83
  - DSH Web / 桌面端:0.1.0-rc.6 至 0.2.0-rc.2
83
84
  - 自 0.2.0 起针对官方桌面端 0.2.0-rc.2 深度适配:字号令牌新基准、顶栏毛玻璃横带与原生窗口按钮融合、插件/任务等整页视图毛玻璃背板、开关与徽章配色对齐
84
85
  - 平台:Web / Windows 桌面端(设置保存在各自档案的 `data/` 目录下)
85
- - 最近验证:2026-10-01(0.2.0-rc.2 桌面端亮/暗两态真机实测)
86
+ - 最近验证:2026-10-02(0.2.0-rc.2 桌面端亮/暗两态真机实测)
86
87
 
87
88
  ## 版权所有人
88
89
 
Binary file
package/lib/client.js CHANGED
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
13
13
 
14
14
  var SKIN_OWNER = "qingxiao";
15
15
  // 与 package.json 的 version 保持一致(面板页脚会展示)。
16
- var SKIN_VERSION = "0.2.3";
16
+ var SKIN_VERSION = "0.3.0";
17
17
  var SKIN_TITLE = "清宵 · DeepSeek Harness";
18
18
  var SKIN_SYSTEM_CHROME_COLOR_LIGHT = "#EEF7FB";
19
19
  var SKIN_SYSTEM_CHROME_COLOR_DARK = "#0B1E2E";
@@ -111,6 +111,178 @@ window.__ModuleLoader__.load({
111
111
  return isFinite(size) ? Math.round(size * 100) / 100 : null;
112
112
  }
113
113
 
114
+ // ═══════════════════════════════════════════════════════════════
115
+ // 开屏动画:线稿显影 → 定格 → 闪白翻底 → 上色淡入 → 淡出归位
116
+ // 手法学习自 panding999/codex-startup-animation(macOS 版)。
117
+ // 线稿用 AI 生成的清宵白描稿原像(assets/qx-lineart.png,与原画
118
+ // 同构图 3:2,经 /api/dsh-qingxiao/boot-lineart 下发),显影用
119
+ // 「平滑噪声场 + 对角扫掠」的混合阈值场逐帧揭开,低分辨率噪声
120
+ // 放大绘制自带羽化边缘,无锯齿且丝滑;翻底直接 CSS invert
121
+ // (白线黑底反相即墨线纸底),不再描线。
122
+ // ═══════════════════════════════════════════════════════════════
123
+
124
+ var bootAnimCancel = null;
125
+
126
+ function playBootAnimation(body, settings, isDisposed) {
127
+ if (window.__QX_BOOT_PLAYED__) return; // 每次页面装载只播一次(HMR 重挂不重播)
128
+ window.__QX_BOOT_PLAYED__ = true;
129
+ if (!settings.bootAnimation) return;
130
+ if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
131
+ if (isDisposed()) return;
132
+
133
+ // 时间轴(ms):显影 → 定格(看清完整线稿)→ 闪白翻底 → 上色 → 淡出
134
+ var T_GROW = 2600, T_HOLD = 1150, T_FLASH = 320, T_COLOR = 850, T_FADE = 750;
135
+
136
+ var overlay = document.createElement("div");
137
+ overlay.dataset.skinChrome = "boot-veil";
138
+ overlay.dataset.skinOwner = SKIN_OWNER;
139
+ overlay.setAttribute("aria-hidden", "true");
140
+ overlay.style.cssText = "position:fixed;inset:0;z-index:2147483300;overflow:hidden;pointer-events:none;background:#000;";
141
+ var canvas = document.createElement("canvas");
142
+ canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
143
+ var dpr = Math.min(window.devicePixelRatio || 1, 2);
144
+ canvas.width = Math.round(window.innerWidth * dpr);
145
+ canvas.height = Math.round(window.innerHeight * dpr);
146
+ var paper = document.createElement("div");
147
+ paper.style.cssText = "position:absolute;inset:0;background-image:url('/api/dsh-qingxiao/boot-lineart');background-position:center;background-size:cover;filter:invert(1);opacity:0;";
148
+ var artLayer = document.createElement("div");
149
+ artLayer.style.cssText = "position:absolute;inset:0;background-position:center;background-size:cover;background-repeat:no-repeat;opacity:0;will-change:opacity,filter,transform;";
150
+ var flash = document.createElement("div");
151
+ flash.style.cssText = "position:absolute;inset:0;background:#F4F9FC;opacity:0;";
152
+ overlay.append(canvas, paper, artLayer, flash);
153
+ body.append(overlay);
154
+
155
+ // 彩色画卷:与 body 背景同源(当前模式的自定义背景或默认画卷)
156
+ var dark = isDarkMode(body);
157
+ var custom = dark ? settings.darkBackground : settings.lightBackground;
158
+ artLayer.style.backgroundImage = 'url("' + (custom || DEFAULT_BG_URL) + '")';
159
+
160
+ // 噪声场(预生成一次):平滑随机 + 对角扫掠混合,显影有运笔方向
161
+ var NW = 132, NH = 88;
162
+ var field = new Float32Array(NW * NH);
163
+ var seed = 20260401;
164
+ function rand() { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; }
165
+ var i, x, y;
166
+ for (i = 0; i < field.length; i++) field[i] = rand();
167
+ for (var pass = 0; pass < 2; pass++) {
168
+ const src = Float32Array.from(field);
169
+ for (y = 0; y < NH; y++) for (x = 0; x < NW; x++) {
170
+ let s = 0;
171
+ for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) {
172
+ const yy = Math.min(NH - 1, Math.max(0, y + dy)), xx = Math.min(NW - 1, Math.max(0, x + dx));
173
+ s += src[yy * NW + xx];
174
+ }
175
+ field[y * NW + x] = s / 25;
176
+ }
177
+ }
178
+ var fmin = Infinity, fmax = -Infinity;
179
+ for (i = 0; i < field.length; i++) {
180
+ field[i] = field[i] * 0.55 + ((i % NW) / (NW - 1)) * 0.31 + (((i / NW) | 0) / (NH - 1)) * 0.14;
181
+ if (field[i] < fmin) fmin = field[i];
182
+ if (field[i] > fmax) fmax = field[i];
183
+ }
184
+ for (i = 0; i < field.length; i++) field[i] = (field[i] - fmin) / (fmax - fmin);
185
+
186
+ // 低分辨率遮罩画布:逐帧阈值化后放大绘制,边缘自然羽化
187
+ var mcv = document.createElement("canvas"); mcv.width = NW; mcv.height = NH;
188
+ var mctx = mcv.getContext("2d");
189
+ var mimg = mctx.createImageData(NW, NH);
190
+ for (i = 0; i < field.length; i++) { mimg.data[i * 4] = 255; mimg.data[i * 4 + 1] = 255; mimg.data[i * 4 + 2] = 255; }
191
+ function drawMask(p) {
192
+ var d = mimg.data;
193
+ for (i = 0; i < field.length; i++) {
194
+ var a = (p - field[i]) / 0.055;
195
+ d[i * 4 + 3] = a <= 0 ? 0 : a >= 1 ? 255 : a * 255;
196
+ }
197
+ mctx.putImageData(mimg, 0, 0);
198
+ }
199
+
200
+ var lineart = new Image();
201
+ var raf = 0, start = 0, paperShown = false, ticking = false;
202
+ function cleanup() {
203
+ if (raf) { clearTimeout(raf); cancelAnimationFrame(raf); raf = 0; }
204
+ document.removeEventListener("keydown", onKey, true);
205
+ document.removeEventListener("click", onSkip, true);
206
+ document.removeEventListener("visibilitychange", onVis, false);
207
+ bootAnimCancel = null;
208
+ }
209
+ function removeOverlay() { if (overlay.isConnected) overlay.remove(); }
210
+ function finishNow() {
211
+ cleanup();
212
+ overlay.style.transition = "opacity .3s ease";
213
+ overlay.style.opacity = "0";
214
+ setTimeout(removeOverlay, 320);
215
+ }
216
+ function onKey(e) { if (e.key === "Escape") { e.preventDefault(); finishNow(); } }
217
+ function onSkip() { finishNow(); }
218
+ function onVis() { if (!document.hidden) tick(); }
219
+ document.addEventListener("keydown", onKey, true);
220
+ document.addEventListener("click", onSkip, true);
221
+ document.addEventListener("visibilitychange", onVis, false);
222
+ bootAnimCancel = function() { cleanup(); removeOverlay(); };
223
+
224
+ function renderGrow(p) {
225
+ var ctx = canvas.getContext("2d");
226
+ ctx.globalCompositeOperation = "copy";
227
+ ctx.imageSmoothingEnabled = true;
228
+ ctx.drawImage(lineart, 0, 0, canvas.width, canvas.height);
229
+ ctx.globalCompositeOperation = "destination-in";
230
+ ctx.drawImage(mcv, 0, 0, canvas.width, canvas.height);
231
+ ctx.globalCompositeOperation = "source-over";
232
+ }
233
+ function frame(now) {
234
+ if (isDisposed() || !overlay.isConnected) return;
235
+ if (!start) start = now;
236
+ var t = now - start;
237
+ if (t < T_GROW) {
238
+ var p = t / T_GROW;
239
+ var e = 0.5 - Math.cos(Math.PI * p) / 2; // easeInOutSine,首尾柔、中段匀
240
+ drawMask(e);
241
+ renderGrow(e);
242
+ } else if (t < T_GROW + T_HOLD) {
243
+ drawMask(1);
244
+ renderGrow(1);
245
+ } else if (t < T_GROW + T_HOLD + T_FLASH) {
246
+ if (!paperShown) { paperShown = true; drawMask(1); renderGrow(1); }
247
+ var ft = (t - T_GROW - T_HOLD) / T_FLASH;
248
+ paper.style.opacity = String(Math.min(1, ft / 0.55));
249
+ flash.style.opacity = String(ft < 0.3 ? ft / 0.3 * 0.95 : 0.95 * (1 - (ft - 0.3) / 0.7));
250
+ } else if (t < T_GROW + T_HOLD + T_FLASH + T_COLOR) {
251
+ var ct = (t - T_GROW - T_HOLD - T_FLASH) / T_COLOR;
252
+ var e2 = 1 - Math.pow(1 - ct, 3);
253
+ artLayer.style.opacity = String(e2);
254
+ artLayer.style.filter = "saturate(" + (0.25 + 0.8 * e2).toFixed(3) + ") brightness(" + (1 + 0.08 * (1 - e2)).toFixed(3) + ")";
255
+ artLayer.style.transform = "scale(" + (1.05 - 0.05 * e2).toFixed(4) + ")";
256
+ flash.style.opacity = "0";
257
+ } else {
258
+ var ft2 = (t - T_GROW - T_HOLD - T_FLASH - T_COLOR) / T_FADE;
259
+ overlay.style.opacity = String(Math.max(0, 1 - ft2).toFixed(3));
260
+ if (ft2 >= 1) { cleanup(); setTimeout(removeOverlay, 60); return; }
261
+ }
262
+ schedule();
263
+ }
264
+ function schedule() {
265
+ if (ticking || isDisposed() || !overlay.isConnected) return;
266
+ ticking = true;
267
+ var go = function() { ticking = false; frame(performance.now()); };
268
+ // 前台用 rAF(丝滑);后台 rAF 冻结,改 setTimeout 兜底让墙钟
269
+ // 推进、动画粗粒度走完,遮罩不会捂死界面
270
+ if (document.visibilityState === "visible") raf = requestAnimationFrame(go);
271
+ else raf = setTimeout(go, 16);
272
+ }
273
+
274
+ lineart.onload = function() {
275
+ if (isDisposed() || !overlay.isConnected) { removeOverlay(); return; }
276
+ schedule();
277
+ };
278
+ lineart.onerror = function() { finishNow(); }; // 线稿取不到就整段跳过
279
+ lineart.src = "/api/dsh-qingxiao/boot-lineart";
280
+ }
281
+
282
+ // ═══════════════════════════════════════════════════════════════
283
+ // PALETTE STATE
284
+ // ═══════════════════════════════════════════════════════════════
285
+
114
286
  // ═══════════════════════════════════════════════════════════════
115
287
  // PALETTE STATE
116
288
  // ═══════════════════════════════════════════════════════════════
@@ -130,6 +302,7 @@ window.__ModuleLoader__.load({
130
302
  particleCount: 25,
131
303
  particleSpeed: 100,
132
304
  fontScale: 100,
305
+ bootAnimation: true,
133
306
  frostMaster: true,
134
307
  frostPlugin: true,
135
308
  frostNative: true,
@@ -333,29 +506,25 @@ body[data-dsh-qingxiao] [class*='detailsCol'],
333
506
  body[data-dsh-qingxiao] [class*='rightbarCol'] {
334
507
  background: rgba(var(--qx-panel-rgb), calc(var(--qx-panel-alpha) * 0.72));
335
508
  }
336
- body[data-dsh-qingxiao] [class*='_centerCol'] > * {
337
- max-width: var(--qx-content-width);
338
- /* 说明:设置等弹窗“逐字竖排/挤在左边”的真正元凶是上方容器的
339
- backdrop-filter(它把弹窗 portal 困在侧边栏里),而不是本条的
340
- margin:auto。保留 max-width 限宽即可。 */
341
- }
342
509
  /* ── 对话区宽度 + 正文字号缩放:接到真正的对话内容上 ──
343
- 旧选择器只作用于 centerCol 的直接子元素,没命中更深的对话内容,所以
344
- 拖这两个滑块没反应。对话内容在 [data-slot='main.conversation'] > *
345
- (0.1.x 为 [data-slot='conversation'],滚动容器 [data-conversation-scroll]
346
- 两代同名),正文用 [data-conversation-scroll] 定位。 */
510
+ 0.2.0 起会话根(Dc7zOa_root,头部行/角部按钮/迎宾页/输入卡都在其中)
511
+ 是 [data-slot='main.conversation'] 的直接子元素。容器绝不能限宽:
512
+ 旧版规则把根压到内容宽度后,头部行右上的「打开右侧边栏」角部按钮
513
+ 会跟着内收(离右缘 182px 而非 ~16px),[data-slot='main'] 上更宽的
514
+ 旧限宽规则还会挤压插件页/任务页。宽度控制只走令牌:
515
+ DSH 自带居中机制(50% ± 宽/2 的宽度手柄布局),会按令牌居中内容;
516
+ 但 0.2.0 的对话体在 _body 上重新定义了 --dsh-chat-content-width
517
+ (var(--dsh-chat-user-width, clamp(...)),DSH 自己的拖宽手柄),
518
+ 所以令牌覆盖必须比它更深一层(_body),滑块才能继续生效。 */
347
519
  body[data-dsh-qingxiao] :is([data-slot='main.conversation'], [data-slot='conversation']) > * {
348
- /* DSH 的对话列宽走 --dsh-chat-content-width(正文列、输入卡、表格溢出都读它),
349
- 只在容器上写 max-width 到 748px 就被 DSH 自己的令牌卡住,所以这里一并覆盖。 */
350
520
  --dsh-chat-content-width: var(--qx-content-width);
351
- max-width: var(--qx-content-width);
352
- margin-left: auto;
353
- margin-right: auto;
354
- width: 100%;
355
521
  /* 居中显示。之前浮层跑到中间是 zoom 改了 position:fixed 坐标所致,
356
522
  此处已去掉 zoom,浮层会正确锚定在标注旁。 */
357
523
  background: transparent !important;
358
524
  }
525
+ body[data-dsh-qingxiao] :is([data-slot='main.conversation'], [data-slot='conversation']) [class*='_body'] {
526
+ --dsh-chat-content-width: var(--qx-content-width);
527
+ }
359
528
  /* 正文字号缩放:不能对对话正文用 zoom(会错位 position:fixed 浮层),
360
529
  思路仍是覆盖 DSH 的 markdown 字号令牌,只作用于对话区。
361
530
  但绝不能在 font 简写的字号/行高位置上写 calc()——DSH 消费这些令牌的方式是
@@ -1550,6 +1719,7 @@ body[data-dsh-qingxiao] [id=root] [class*='_markdown'] input[type='checkbox'] {
1550
1719
  secFx.className = "pp-section";
1551
1720
  secFx.textContent = "剑 气 流 光";
1552
1721
  bodyEl.append(secFx);
1722
+ bodyEl.append(makeToggle("开屏动画(线稿上色)", "bootAnimation"));
1553
1723
  bodyEl.append(makeToggle("启用流光", "particles"));
1554
1724
  bodyEl.append(makeSlider("流光数量", "particleCount", 0, 60, 2, function(v) { return String(v); }));
1555
1725
  bodyEl.append(makeSlider("流光速度", "particleSpeed", 20, 200, 10, function(v) { return v + "%"; }));
@@ -1873,6 +2043,7 @@ body[data-dsh-qingxiao] [id=root] [class*='_markdown'] input[type='checkbox'] {
1873
2043
  applyPaletteSettings();
1874
2044
  rebuildPanel();
1875
2045
  loadPaletteSettings();
2046
+ playBootAnimation(body, paletteSettings, function() { return disposed; });
1876
2047
 
1877
2048
  // --- Sidebar width → css variable (decor follows rail/narrow/wide) ---
1878
2049
  var widthSheet = document.createElement("style");
@@ -1931,6 +2102,7 @@ body[data-dsh-qingxiao] [id=root] [class*='_markdown'] input[type='checkbox'] {
1931
2102
  document.removeEventListener("click", frostSchedule, true);
1932
2103
  frostObserver.disconnect();
1933
2104
  frostHostsSeen.length = 0;
2105
+ if (typeof bootAnimCancel === "function") { try { bootAnimCancel(); } catch (_) {} }
1934
2106
  // 打标(.qx-frost / .qx-frost-solid)全部摘除,不给下一个实例/裸 DSH 留痕迹。
1935
2107
  document.querySelectorAll("." + QX_FROST_CLASS).forEach(function(el) {
1936
2108
  el.classList.remove(QX_FROST_CLASS, QX_FROST_SOLID);
package/lib/index.js CHANGED
@@ -7,9 +7,11 @@ const name = "@taosee258/dsh-client-ui-skin-qingxiao";
7
7
  const inject = ["webServer"];
8
8
  const SETTINGS_ROUTE = "/api/dsh-qingxiao/settings";
9
9
  const DEFAULT_BG_ROUTE = "/api/dsh-qingxiao/default-background";
10
+ const LINEART_ROUTE = "/api/dsh-qingxiao/boot-lineart";
10
11
  const SETTINGS_DIR = "dsh-client-ui-skin-qingxiao";
11
12
  const SETTINGS_FILE = "settings.json";
12
13
  const DEFAULT_BG_FILE = fileURLToPath(new URL("../assets/qx.jpg", import.meta.url));
14
+ const LINEART_FILE = fileURLToPath(new URL("../assets/qx-lineart.png", import.meta.url));
13
15
  const MAX_BODY_BYTES = 15 * 1024 * 1024;
14
16
  const MAX_BACKGROUND_BYTES = 7 * 1024 * 1024;
15
17
 
@@ -17,7 +19,9 @@ const BOOLEAN_KEYS = [
17
19
  "textContrastBoost", "panelBlur", "goldTrim",
18
20
  "particles", "lightCharms", "darkCharms",
19
21
  // v0.1.4 毛玻璃虚化:总开关 + 插件窗/原生面板两个分项
20
- "frostMaster", "frostPlugin", "frostNative"
22
+ "frostMaster", "frostPlugin", "frostNative",
23
+ // v0.3.0 开屏动画(线稿上色)
24
+ "bootAnimation"
21
25
  ];
22
26
  const NUMBER_RANGES = {
23
27
  scrimOpacity: [20, 95],
@@ -182,9 +186,34 @@ function handleDefaultBackgroundRoute(req, res) {
182
186
  res.end(req.method === "HEAD" ? undefined : defaultBgCache);
183
187
  }
184
188
 
189
+ let lineartCache = null;
190
+ function handleLineartRoute(req, res) {
191
+ if (req.method !== "GET" && req.method !== "HEAD") {
192
+ res.writeHead(405, { allow: "GET, HEAD" });
193
+ res.end();
194
+ return;
195
+ }
196
+ try {
197
+ if (lineartCache === null || !lineartCache.length) {
198
+ lineartCache = readFileSync(LINEART_FILE);
199
+ }
200
+ } catch {
201
+ res.writeHead(404);
202
+ res.end("lineart missing");
203
+ return;
204
+ }
205
+ res.writeHead(200, {
206
+ "content-type": "image/png",
207
+ "cache-control": "no-store",
208
+ "content-length": String(lineartCache.length)
209
+ });
210
+ res.end(req.method === "HEAD" ? undefined : lineartCache);
211
+ }
212
+
185
213
  function apply(ctx) {
186
214
  ctx.webServer.register({ kind: "exact", path: SETTINGS_ROUTE, handler: handleSettingsRoute });
187
- return ctx.webServer.register({ kind: "exact", path: DEFAULT_BG_ROUTE, handler: handleDefaultBackgroundRoute });
215
+ ctx.webServer.register({ kind: "exact", path: DEFAULT_BG_ROUTE, handler: handleDefaultBackgroundRoute });
216
+ return ctx.webServer.register({ kind: "exact", path: LINEART_ROUTE, handler: handleLineartRoute });
188
217
  }
189
218
 
190
219
  export { apply, inject, name };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@taosee258/dsh-client-ui-skin-qingxiao",
3
3
  "description": "Qingxiao (Wuthering Waves) themed skin with ice-blue/jade/moon-white palette, sword-light motes and user-swappable background for the dsh web GUI",
4
- "version": "0.2.3",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {