Files
tamabox/dev/theme-builder/README.md
T
tamakyi c3d34b53d2 feat(theme-builder): 图片处理——透明度 / 压白 / 去色 / 裁边 / 去白底
素材卡片左上角 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= 把项目里的图直接当素材载入。
2026-10-20 14:52:36 +08:00

171 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 主题生成器
一个可视化编辑主题的小工具:**以项目真实页面为画布**,按页面分别摆装饰,导出 zip 解压即用。
它替换了早先那版「单文件、仿制预览」的生成器 —— 那一版的问题很直接:预览是我自己画的
示意图,跟真实页面长得不一样;而且只能配「背景 + 人物 + 道具」三件套。
## 为什么需要起一个本地服务
生成器要把 `outputs/preview-*.html`(由 `go test` 渲染出来的**真实页面**)放进 iframe 当画板,
再往里注入正在编辑的主题 CSS。`file://` 下浏览器不允许父页面访问 iframe 的 DOM(即使同目录),
预览就只能退回示意图。起本地服务后父子同源,拖拽、注入、实时改令牌都能工作。
```bash
# 1) 先渲染真实页面快照(改了模板/数据后要重跑)
TEST_PREVIEW=1 go test ./templates -run TestGenPreview
# 2) 起服务(只读,不改项目任何文件)
python dev/theme-builder/serve.py # 默认 8777
# 3) 打开
http://127.0.0.1:8777/dev/theme-builder/
```
不想用脚本也行:在项目根跑 `python -m http.server 8777`,效果一样。
## 它和线上是同一条链路
| 环节 | 生成器 | 线上 |
| --- | --- | --- |
| 画板 | `outputs/preview-*.html`(真模板渲染) | 同一套模板 |
| 作用域 | iframe 里设 `data-page-scope` | `internal/context.pageScope()` 输出到 `<html>` |
| 装饰层 | **固定 10 槽** | `base.css` 的 12 层背景(pattern 1 + deco 10 + image 1) |
| 位置 | `calc(0%|50%|100% ± Npx)` | 同一套 `background-position` |
因为是同一套机制,工具里拖出来的位置换视口不会跑位,也不会出现「预览好看、实机错位」。
## 三块面板
**页面** —— 每个页面作用域可以单独配,各有一份背景与装饰:
| 作用域 | 对应路径 |
| --- | --- |
| 全局 | 所有页面默认 |
| `box` | `/_/{domain}` 箱主页 |
| `question` | `/_/{domain}/{id}` 单题页 |
| `admin` | `/user/questions` 题目列表 |
| `profile` | `/user/*` 设置页 |
| `vote` | `/vote/{token}` 投票页 |
默认全部「跟随全局」;勾掉就变成独立配置,导出时才会产出覆盖块。
没配的页面自动沿用全局,不会出现「某页装饰消失」。
(首页 `/` 与 `/explore` 不列出来 —— 全站公开页不注入 `ThemeSlug`,不加载主题 css,配了也没效果。)
**装饰图层** —— 每个作用域最多 10 层,三种类型:
- **图片**:从图片池选,按高度定尺寸(宽度自动等比),选中后可直接在画板上拖动;
- **光斑**:`radial-gradient`,给空白一点体积,不占位、不会变成贴纸;
- **线性光**:`linear-gradient`,做窗光/压深。
锚点用 9 宫格 + 像素偏移,生成 `calc(0% + 40px) calc(100% - 36px)` 这种两值形式。
**↑ / ↓ 调层级**。顺序就是层级:`background-image` 的第一层画在最上面,
所以「谁挡住谁」是靠顺序定的(shiroko 里自行车被人挡住就是这么来的)。
**窄屏** —— 手机上卡片几乎占满屏幕,桌面那套 16:9 场景会被 `cover` 裁成一条无意义纹理,
固定 px 的人物会被卡片盖住。这一栏可以:指定一张竖版背景图、决定装饰是否退场、
改断点(默认 900px)。导出时生成对应的 `@media` 块,只写浅色规则
(深色块特异性更高,本来就压住它)。
**图片池** —— 上传任意张,浏览器本地转 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%。
## 工程文件与草稿
调主题是连续几十次的活,所以配置必须留得住:
- **自动草稿**:每次改动后 700ms 存进 `localStorage`,下次打开自动恢复。
图片多到顶破 5MB 配额时会提示改用 json(只提示一次)。
- **保存工程 / 打开工程**:`theme-<slug>.json`,图片以 base64 内嵌,一个文件就能带走;
可以进版本库,也可以发给别人接着改。
- 不想恢复草稿就加 `?fresh=1`。
## 导出
点「导出 zip」得到:
```
static/css/themes/<slug>.css
static/images/themes/<slug>/*.webp
INSTALL-<slug>.txt
```
点导出时会先跑一遍校验(slug 合法、图片层是否选了图、有没有引用已删除的素材、
装饰是否超过 10 层、生成出来的 CSS 每处装饰是不是真的 10 个值),有问题直接列出来,
不会让你导出后再去等 `go test`。
解压到项目根,然后按 `INSTALL-<slug>.txt` 做三步:
1. `conf/app.ini` 的 `[theme] available` 加上 slug;
2. `internal/conf/static.go` 的 `StaticVersion` +1;
3. `go build`(模板与静态资源都是编译期 embed,只重启不重编不会变)。
## 它不做什么
- **不做复杂修图**:没有笔刷、蒙版、自由裁切、调色曲线 —— 这些该在专门的工具里做完再进来。
- **不做调色**:想统一「雾度」(人物压白 16%、场景 30~38%)得在生成端做,
见 `docs/theme-shiroko-assets-spec.md`。
- **不写配置**:`[theme] available` 只给要粘贴的那一行。
- **不碰深色的图片**:深色块一律 `--tb-page-image: none` + 装饰全关(白天场景压在深底上会脏)。
想在深色下也放图,需要手工加深色块。
## 自检
没有 UI 也能回归,两个钩子:
```
?demo=1 载入示例素材(canvas 画的背景 + 剪影人物)
?scope=admin 直接落在某个作用域
?fresh=1 忽略 localStorage 草稿,从空白开始
?dump=css 把最终 CSS 写进 <pre id="dump">
?dump=zip 把 zip 的 base64 写进 <pre id="dump">
?dump=validate 把 validate() 的结果写进 <pre id="dump">
?dump=install 把安装说明写进 <pre id="dump">
?dump=fx 跑处理算法自检(13 个断言,输出 JSON)
?dump=fxpipe 跑弹窗链路冒烟(打开→改参→预览→落盘→无改动不重编码)
?dump=fxkey 端到端去白底:载入白底图,统计透明像素比例
?loadurl=/static/images/x.webp&fx=x
直接把项目里的图当素材载入(不必先下载再上传),fx= 打开处理面板
?fxjson={"key":{"on":true}}
配合 loadurl 给初始处理参数
```
```bash
chrome --headless=old --virtual-time-budget=6000 \
--dump-dom "http://127.0.0.1:8777/dev/theme-builder/?demo=1&dump=css"
```
把抓到的 CSS 存成 `static/css/themes/<slug>.css`,可跑:
```bash
go test ./internal/theme -run "TestThemeCSSComplete|TestDecoSlotCount"
```
`TestDecoSlotCount` 是这次新增的:它会逐个检查 `base.css` 与所有主题 css 里每一处
`--tb-deco*` 声明是否都有 10 个值。少写一个不会报错,只会让图层与 size/position 错位
(表现为整页重复同一张插画,或人物跑到左上角),靠注释守不住。