From c3d34b53d26d429552b8e124cb353dd7e04b88b3 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Tue, 20 Oct 2026 14:52:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(theme-builder):=20=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=A4=84=E7=90=86=E2=80=94=E2=80=94=E9=80=8F=E6=98=8E=E5=BA=A6?= =?UTF-8?q?=20/=20=E5=8E=8B=E7=99=BD=20/=20=E5=8E=BB=E8=89=B2=20/=20?= =?UTF-8?q?=E8=A3=81=E8=BE=B9=20/=20=E5=8E=BB=E7=99=BD=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 素材卡片左上角 fx 按钮进处理面板,非破坏: - orig 存上传时的原始字节(不重编码),fx 只存参数,每次从 orig 重算。 「压白 16% → 20%」不会叠加两次压缩;打开看一眼就关一个像素都不动 (参数没变直接跳过,有损编码反复走会累积模糊)。 - 预览按 420px 宽算(滑杆要跟手),完成时用全分辨率重算;加了序号丢弃过期结果, 否则快速拖动时旧渲染会覆盖新的。 功能: - 不透明度 0~100%(CSS 背景图没法单独设 opacity,只能烘焙) - 压白 v×(1-k)+255×k,与主题里场景的雾度同一公式 - 去色、裁掉透明边 - 去白底:四边漫水 + 封闭白孔检测 + 可选 1px 收边 去白底两个坑(都写进注释了): - 手内侧 / 两腿之间是闭合白区,四边漫水进不去,得按连通域单独掏(面积≥24) - 容差默认 16 而不是 22:背景 253、白袜子 237 只差 16, 容差 30 会把袜子吃穿成一个透明洞 验证(都是跑出来的,不是看缩略图): - ?dump=fx 13 项算法断言全过:封闭孔开/关、容差 16 保袜子/30 吃穿、 收边后轮廓 1px、alpha 50%→128、压白 20% 时 237→241、去色后 R=G=B、裁边 80×40 - ?dump=fxpipe 链路:弹窗开关、预览 300×960、参数重开后仍在、 改参后 dataURL 变、无改动时 dataURL 一个字节不变 - ?dump=fxkey 端到端:白底立绘去白底后透明占比 46.3%, 与手写 floodfill 抠的 46.6% 差 0.3% - 生成 CSS 仍通过 TestThemeCSSComplete + TestDecoSlotCount 顺带:工程文件升到 v3(存 orig + fx,未处理时不重复存 dataURL), 读 v2 旧工程时把 dataURL 当 orig 用。新增 ?loadurl= 把项目里的图直接当素材载入。 --- dev/theme-builder/README.md | 33 +- dev/theme-builder/index.html | 668 ++++++++++++++++++++++++++++++++++- 2 files changed, 680 insertions(+), 21 deletions(-) diff --git a/dev/theme-builder/README.md b/dev/theme-builder/README.md index 0f2f156..bb455b8 100644 --- a/dev/theme-builder/README.md +++ b/dev/theme-builder/README.md @@ -70,7 +70,29 @@ http://127.0.0.1:8777/dev/theme-builder/ (深色块特异性更高,本来就压住它)。 **图片池** —— 上传任意张,浏览器本地转 WebP,画布尺寸 = 原图尺寸(不缩放、不留透明边)。 -导出时只打包实际用到的图。 +导出时只打包实际用到的图。每张图左上角有 `fx` 按钮,进处理面板(处理过的会高亮)。 + +### 图片处理 + +| 功能 | 说明 | +| --- | --- | +| 不透明度 | 0~100%。CSS 背景图没法单独设 `opacity`,只能烘焙进像素 | +| 压白 / 雾度 | `v×(1-k)+255×k`。场景压白 38% 时人物常用 12~26%,否则是全页对比度最高的一块 | +| 去色 | 往灰度混。彩色素材压不住素场景时用 | +| 裁掉透明边 | 生成图四周留白多时裁掉,否则按高度定尺寸会让主体看着比实际小 | +| 去白底 | 四边漫水 + 封闭白孔检测,默认容差 16 | +| 边缘收 1px | 去白底后把残留的半透明白边收掉,代价是轮廓缩一圈 | + +**非破坏**:原文件字节存在 `orig` 里,`fx` 只存参数,每次处理都从 `orig` 重算。 +所以「压白 16% → 改成 20%」不会叠加两次压缩,反复调参也不会把边缘越磨越糊。 +打开面板看一眼就关,一个像素都不会动(参数没变就跳过重编码)。 + +**去白底的两个关键点**(都是踩过的): + +- **封闭白孔**:手内侧、两腿之间那种被轮廓包住的白色,四边漫水根本进不去, + 必须单独按连通域掏(面积 ≥ 24 才掏,更小的多半是眼睛高光)。可以关掉。 +- **容差别贪**:背景常见 253,白袜子 237,只差 16。容差 16 保得住袜子,30 就吃穿了。 + 实测白底立绘去白底后透明占比 46.3%,与手写 floodfill 抠出来的 46.6% 差 0.3%。 ## 工程文件与草稿 @@ -104,7 +126,7 @@ INSTALL-.txt ## 它不做什么 -- **不抠图**:图片必须本身是透明底,白底图导出的主题会带一块白。 +- **不做复杂修图**:没有笔刷、蒙版、自由裁切、调色曲线 —— 这些该在专门的工具里做完再进来。 - **不做调色**:想统一「雾度」(人物压白 16%、场景 30~38%)得在生成端做, 见 `docs/theme-shiroko-assets-spec.md`。 - **不写配置**:`[theme] available` 只给要粘贴的那一行。 @@ -123,6 +145,13 @@ INSTALL-.txt ?dump=zip 把 zip 的 base64 写进
 ?dump=validate     把 validate() 的结果写进 
 ?dump=install      把安装说明写进 
+?dump=fx           跑处理算法自检(13 个断言,输出 JSON)
+?dump=fxpipe       跑弹窗链路冒烟(打开→改参→预览→落盘→无改动不重编码)
+?dump=fxkey        端到端去白底:载入白底图,统计透明像素比例
+?loadurl=/static/images/x.webp&fx=x
+                   直接把项目里的图当素材载入(不必先下载再上传),fx= 打开处理面板
+?fxjson={"key":{"on":true}}
+                   配合 loadurl 给初始处理参数
 ```
 
 ```bash
diff --git a/dev/theme-builder/index.html b/dev/theme-builder/index.html
index cea4aeb..9413ea9 100644
--- a/dev/theme-builder/index.html
+++ b/dev/theme-builder/index.html
@@ -108,6 +108,43 @@
     position:absolute;inset:auto 0 0 0;font-size:9px;text-align:center;background:#000a;color:#cbd6e4;
     padding:1px 0;
   }
+  /* 处理入口常驻(不靠 hover):处理过的素材要能一眼看出来,否则
+     同一张图有「原图」和「压白 16% 版」两副样子,改完就分不清哪张是现在的 */
+  .as .fx{
+    position:absolute;top:2px;left:1px;font-size:10px;line-height:1;font-weight:600;
+    color:#cbd6e4;background:#0009;border-radius:4px;padding:2px 3px;cursor:pointer;
+  }
+  .as .fx:hover{background:var(--ui-accent);color:#08121c}
+  .as .fx.on{background:var(--ui-accent);color:#08121c}
+
+  /* ---------- 图片处理弹窗 ---------- */
+  .modal{position:fixed;inset:0;background:#000b;z-index:50;display:none;align-items:center;justify-content:center}
+  .modal.on{display:flex}
+  .mbox{
+    width:min(900px,94vw);max-height:90vh;display:flex;flex-direction:column;
+    background:var(--ui-panel);border:1px solid var(--ui-line);border-radius:10px;
+    overflow:hidden;box-shadow:0 24px 64px #000a;
+  }
+  .mbox header{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--ui-line)}
+  .mbox header .t{font-weight:600}
+  .mbox header .sub{font-size:11px;color:var(--ui-text2)}
+  .mbox header .spacer{flex:1}
+  .mbody{display:grid;grid-template-columns:minmax(0,1fr) 252px;flex:1;min-height:0}
+  .mprev{
+    display:flex;align-items:center;justify-content:center;padding:18px;min-height:0;min-width:0;
+    /* 棋盘格:不透明度是这一屏唯一看不出来的东西,必须有背景参照 */
+    background:conic-gradient(#1a2029 25%, #0d1117 0 50%, #1a2029 0 75%, #0d1117 0) 0 0/16px 16px;
+  }
+  .mprev canvas{max-width:100%;max-height:58vh;display:block;box-shadow:0 6px 20px #0006}
+  .mparams{padding:14px;border-left:1px solid var(--ui-line);overflow:auto}
+  .fxrow{margin-bottom:13px}
+  .fxrow .lab{display:flex;justify-content:space-between;align-items:baseline;font-size:11px;color:var(--ui-text2);margin-bottom:2px}
+  .fxrow .lab b{color:var(--ui-text);font-weight:600;font-variant-numeric:tabular-nums}
+  .fxrow input[type=range]{width:100%;accent-color:var(--ui-accent);margin:0}
+  .fxrow .sub{font-size:10px;color:#6f7c8d;margin-top:2px}
+  .fxrow.chkrow{margin-bottom:8px}
+  .mbox footer{display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid var(--ui-line)}
+  .mbox footer .info{flex:1;font-size:11px;color:var(--ui-text2)}
 
   .pal{display:flex;flex-direction:column;gap:5px}
   .pal .p{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--ui-text2)}
@@ -208,6 +245,28 @@
   
 
 
+
+
+