素材卡片左上角 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= 把项目里的图直接当素材载入。
主题生成器
一个可视化编辑主题的小工具:以项目真实页面为画布,按页面分别摆装饰,导出 zip 解压即用。
它替换了早先那版「单文件、仿制预览」的生成器 —— 那一版的问题很直接:预览是我自己画的 示意图,跟真实页面长得不一样;而且只能配「背景 + 人物 + 道具」三件套。
为什么需要起一个本地服务
生成器要把 outputs/preview-*.html(由 go test 渲染出来的真实页面)放进 iframe 当画板,
再往里注入正在编辑的主题 CSS。file:// 下浏览器不允许父页面访问 iframe 的 DOM(即使同目录),
预览就只能退回示意图。起本地服务后父子同源,拖拽、注入、实时改令牌都能工作。
# 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% |
因为是同一套机制,工具里拖出来的位置换视口不会跑位,也不会出现「预览好看、实机错位」。
三块面板
页面 —— 每个页面作用域可以单独配,各有一份背景与装饰:
| 作用域 | 对应路径 |
|---|---|
| 全局 | 所有页面默认 |
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 做三步:
conf/app.ini的[theme] available加上 slug;internal/conf/static.go的StaticVersion+1;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 给初始处理参数
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,可跑:
go test ./internal/theme -run "TestThemeCSSComplete|TestDecoSlotCount"
TestDecoSlotCount 是这次新增的:它会逐个检查 base.css 与所有主题 css 里每一处
--tb-deco* 声明是否都有 10 个值。少写一个不会报错,只会让图层与 size/position 错位
(表现为整页重复同一张插画,或人物跑到左上角),靠注释守不住。