以前顶栏只能在配色里填一个颜色字符串:写 rgba 拿不到模糊,档位和颜色又分在两处,
改了一处就忘了另一处。现在三档一个面板说话。
- S.navbar = {mode, alpha, blur},基色仍取「配色 → 顶栏」,navbarValue() 统一计算:
solid 先 flatten 压平(旧工程里可能自带 alpha)、glass 加 alpha、clear 出 transparent。
半透明档额外写 .uk-navbar-container{backdrop-filter:blur(N)} —— 令牌只能给颜色,
模糊必须落成规则;选择器不带 data-theme,深浅两套共用一条。
- 全透明**只给预览**:validate() 拦导出,与 internal/theme 的 TestNavbarNotFullyTransparent
同一条规矩,面板上直接写明原因。
- 面板带「导航文字 x.x:1」读数,按压平后的顶栏算(半透明时是顶栏压在底色上的结果),
低于 4.5 变橙。
- 旧工程迁移:配色里塞过 rgba(...,0.52) 的,按 alpha 反推档位(1→实色 / 0→全透明 / 其余→半透明),
打开旧工程不会突然变实色。
- 自检 ?dump=navbar(10 项:三档产出、blur 有无、全透明被拦、alpha 0.2/0.95 边界)。
- 滑杆步长 0.02 → 0.01:0.55 会被吸附成 0.54/0.56,手感像坏了。
404 lines
25 KiB
Markdown
404 lines
25 KiB
Markdown
# 主题生成器
|
||
|
||
一个可视化编辑主题的小工具:**以项目真实页面为画布**,按页面分别摆装饰,导出 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 # 默认监听 0.0.0.0:8777,同局域网可访问
|
||
python dev/theme-builder/serve.py --host 127.0.0.1 # 只给本机用(公共网络请加这个)
|
||
|
||
# 3) 打开终端里打印的地址
|
||
# 本机 http://127.0.0.1:8777/dev/theme-builder/
|
||
# 局域网 http://192.168.x.x:8777/dev/theme-builder/
|
||
```
|
||
|
||
默认绑 `0.0.0.0`,是为了能躺在床上用手机 / 平板调主题:启动时会把本机各网卡的地址都列出来,
|
||
照着点就行(手机与电脑要在同一网段)。第一次启动 Windows 会弹防火墙询问,要允许私有网络。
|
||
|
||
⚠️ 两个必须知道的点:
|
||
|
||
- **上传接口没有账号体系**:能打开这个端口的人,都能用你填的密钥往对象存储写 / 删文件。
|
||
公共网络(咖啡馆、宿舍、公司网)一律 `--host 127.0.0.1`。写接口还会校验 `Origin` 与
|
||
`Host` 同源,挡掉「别的网页借你浏览器发请求」,但挡不住同网段主动连过来的人。
|
||
- **换地址 = 换 origin**:`127.0.0.1` 与 `192.168.x.x` 在浏览器眼里是两个站点,
|
||
草稿(localStorage)与 S3 密钥(sessionStorage)不共享 —— 第一次用新地址要重填一次密钥。
|
||
|
||
不想用脚本也行:在项目根跑 `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}` 投票页 |
|
||
|
||
默认全部「跟随全局」;勾掉就变成独立配置,导出时才会产出覆盖块。
|
||
没配的页面自动沿用全局,不会出现「某页装饰消失」。
|
||
|
||
以前「跟随全局」时背景下拉和「加一层」都是 disabled —— 用户在箱主页看到背景还在、
|
||
又点不动,这就是「背景图关不掉」的来源。现在:
|
||
|
||
- 「加一层」「改背景」永远可点,**一改就自动把这一页切成独立配置**,并把全局当前值复制过来当起点
|
||
(改背景不会顺带把图案和图层弄丢;图层重新发 id,避免两个作用域共用一批 id,拖拽/高亮会串);
|
||
- 这种状态下图层列表不展示全局的层、也不给拖拽框 —— 否则一拖就改到全局去了;
|
||
- 想主动切换用「跟随全局」复选框,或用列表里的「改为独立配置」按钮。
|
||
|
||
(首页 `/` 与 `/explore` 不列出来 —— 全站公开页不注入 `ThemeSlug`,不加载主题 css,配了也没效果。)
|
||
|
||
**装饰图层** —— 每个作用域最多 10 层,三种类型:
|
||
|
||
- **图片**:从图片池选,选中后可直接在画板上拖动;
|
||
- **光斑**:`radial-gradient`,给空白一点体积,不占位、不会变成贴纸;
|
||
- **线性光**:`linear-gradient`,做窗光/压深。
|
||
|
||
锚点用 9 宫格 + 像素偏移,生成 `calc(0% + 40px) calc(100% - 36px)` 这种两值形式。
|
||
|
||
### 尺寸单位 × 视口(「换个屏幕就被裁」的根因)
|
||
|
||
装饰层是**视口固定层**(`background-attachment: fixed`),所以同一个 `400px` 在 900 高的屏幕上占
|
||
44%、在 768 的旧笔电上占 52%,窄屏上直接被切掉。尺寸单位就是回答「屏幕变小的时候这张图怎么办」:
|
||
|
||
| 单位 | 生成 | 适合 | 低于 100% 视口时的兜底 |
|
||
| --- | --- | --- | --- |
|
||
| `fit` 自适应·按高度 | `auto clamp(min(calc(100vh - 24px), 231px), 46.67vh, 420px)` | 人物立绘、竖幅道具 | 下限让位给视口,永远不纵向溢出 |
|
||
| `fitw` 自适应·按宽度 | `clamp(min(calc(100vw - 24px), 587px), 70.57vw, 1067px) auto` | 横幅、整屏插画 | 下限让位给视口,永远不横向溢出 |
|
||
| `px` 固定 | `auto 420px` | 需要绝对尺寸(如对齐某个 UI 元素) | 不缩,会裁 |
|
||
| `vh` 视口比例 | `auto 46.67vh` | 一定要占同样屏高的元素 | 无下限 |
|
||
|
||
**为什么必须有两种自适应**:`fit` 只按高度缩,宽度跟着比例涨 —— 一张 16:9 场景 `h=600` 在 1024 宽下
|
||
是 910px 刚好,在 390 的竖屏上按高度缩完还有 1001px,照样裁两侧。宽幅素材只能按宽度缩。
|
||
`fitw` 就是干这个的。实测同一张 1920×1080 素材:
|
||
|
||
| 视口 | `px 600` | `fit`(按高) | `fitw`(按宽) |
|
||
| --- | --- | --- | --- |
|
||
| 1512×900 | 1067 宽 | 1067 宽 | 1067 宽 |
|
||
| 1024×768 | 1067 宽 | 910 宽 | 723 宽 |
|
||
| 390×844 | 1067 宽(裁) | 1001 宽(裁) | 366 宽(完整) |
|
||
|
||
`min(calc(100vh - 24px), 231px)` 这一段是关键:`clamp()` 的下限如果固定成「基准的 55%」,
|
||
屏幕小到连 55% 都放不下时还是会溢出。让下限与视口取小值,自适应单位就不会把图撑出屏幕。
|
||
|
||
**每个图层底部有一行实测**:`1512×900 下显示 131×420,完整可见(源 300×960)`,
|
||
溢出时会变成橙色 `⚠ 1024×768:左 88px / 右 88px 越界`,并给一个「按 1024×768 修好」按钮
|
||
(改成固定 px 并算出刚好放下的尺寸)。视口栏可以切 1512/1280/1024/820/390 五档 —— 都是真视口高,
|
||
不是只改宽度,因为裁切几乎全由高度决定。
|
||
|
||
导出前还会再扫一遍,把「哪些层在哪些视口下越界」附在结果里(只提醒,不阻止 ——
|
||
故意让插画出血也是合法设计)。
|
||
|
||
**每一行都能直接换图**:行里带缩略图 + 「图片」下拉,一眼看到这层用的是哪张。
|
||
之前换图的下拉只出现在「选中那一层」的编辑器里,加上新层又默认绑第一张素材,
|
||
连加三层就全是同一张图,看起来像「每层只能显示这张、换不了」。
|
||
现在新加的层**不会**自动绑第一张(只有素材池只剩一张时才绑),行里显示「(选图片)」等你选。
|
||
|
||
**↑ / ↓ 调层级**。顺序就是层级:`background-image` 的第一层画在最上面,
|
||
所以「谁挡住谁」是靠顺序定的(shiroko 里自行车被人挡住就是这么来的)。
|
||
|
||
**窄屏** —— 手机上卡片几乎占满屏幕,桌面那套 16:9 场景会被 `cover` 裁成一条无意义纹理。
|
||
三段优先级从高到低:
|
||
|
||
1. **隐藏全部装饰层(急停)** —— 一键全关,最省事;
|
||
2. **每层的窄屏策略**:原样 / 缩小到 N%(缩放系数默认 62%)/ 隐藏。缩小是把高度与偏移
|
||
一起乘系数,锚点不变,所以「贴底靠左」的构图缩完还在原位;
|
||
3. **尺寸单位**:`fit` / `fitw` 的层浏览器会自己按视口缩,**不依赖这段规则**。
|
||
|
||
这一栏还能指定竖版背景图、改断点(默认 900px)。导出时生成 `@media` 块,只写浅色规则
|
||
(深色块特异性更高,本来就压住它)。
|
||
|
||
⚠️ `@media` 里**每个独立配置的页面都要各写一条** `html[data-theme-style][data-page-scope="x"]`:
|
||
页面覆盖块特异性是 (0,2,1),只写 `html[data-theme-style]`(0,1,1)的话,
|
||
独立配置的页面在手机上根本不生效(旧版本就是这个 bug)。
|
||
|
||
⚠️ 竖版图只替换**本来就有背景图**的页面。某页故意配成纯色(`--tb-page-image: none`),
|
||
手机上新塞一张竖图等于改了他的设计。
|
||
|
||
**整页背景 · 焦点** —— 背景图是 `cover`,宽高比不匹配就必裁。九宫格焦点决定裁哪一边:
|
||
16:9 场景在竖屏上只剩中间一条,人物偏一侧时把焦点挪过去才不会裁到脸。
|
||
生成 `--tb-page-position`(`base.css` 里以 `var(--tb-page-position, center center)` 兜底,
|
||
旧主题不写这个令牌也不会坏)。
|
||
|
||
**配色** —— 只填 6 个基础色(底色 / 卡片 / 顶栏 / 正文 / 主色 / 边框),
|
||
其余 4 个派生:`surface-2` 往正文靠、`surface-hover` 往主色靠、`primary-weak` 加 12% 透明、
|
||
`text-secondary` / `text-muted` 往底色淡出。
|
||
|
||
⚠️ **派生文字色有对比度下限,不是固定比例**。正文往背景淡出时,二分找到「最淡但仍 ≥ 目标」的位置:
|
||
次要文字 5.5:1、弱化文字 4.5:1(WCAG AA 正文)。旧版按「混 55%」硬算,
|
||
在浅底上直接掉到 2.9:1 —— `base.css` 特意把 `--tb-text-muted` 从 `#8a919c` 提到 `#6b7280`(4.83:1),
|
||
生成器按固定比例算等于每导出一次就把那个修复覆盖回去。
|
||
|
||
- 卡片是半透明(`rgba(255,255,255,0.86)`)时先**压平到页面底色**再算,否则白卡会被当成纯白、对比度偏低;
|
||
- 同时检查页面底色与卡片两个背景,取更不利的那个;
|
||
- 手填的正文本身就达不到目标时**原样返回**(不再往淡里调)——宁可不够淡,不能产出看不见的字;
|
||
- 面板底部有读数 `正文 12.5:1 · 次要 5.5:1 · 淡文字 4.6:1 · 主色 4.6:1`,低于 4.5 变橙(手填色只提醒不拦)。
|
||
|
||
主色除了当链接色,还当按钮/标签的**背景**,所以还要交代「压在上面的字」:
|
||
导出时写 `--tb-on-primary`,在白字和深字(`#0b1016`)里挑对比度更高的那个。
|
||
浅色主题主色深 → 白字;深色主题主色普遍偏亮(`#5c8dff` 之流)→ 白字只有 2.2~3.1:1,必须换深字。
|
||
主色卡在**中间调**时两种字都不够 —— 面板会给橙色提示「压深一点配白字,或提亮一点配深字」,
|
||
`?dump=palette` 里也会列进 `warnings`(不判失败:那是主色要调,不是工具挑错)。
|
||
|
||
顺带:这套字号阶梯(`--tb-fs-xs…hero`)来自 `base.css`,生成器**不碰字号** ——
|
||
主题的职责是配色与背景,改字号会连带挤坏所有页面排版。
|
||
|
||
**顶栏** —— 三档,只管「配色 → 顶栏」那个基色怎么用:
|
||
|
||
| 档位 | 输出 | 适用 |
|
||
| --- | --- | --- |
|
||
| 实色 | `--tb-navbar: rgb(...)` | 页面没有装饰层穿过顶栏;最简单 |
|
||
| 半透明 | `--tb-navbar: rgba(...,0.72)` + `backdrop-filter: blur(12px)` | 装饰层从角落长出来、会经过顶栏时(推荐) |
|
||
| 全透明 | `--tb-navbar: transparent` | **只能预览**,导出会被拦 |
|
||
|
||
- 半透明档的两个滑杆:不透明度 0.2~0.95、背景模糊 0~24px。令牌只能是颜色值,
|
||
模糊另外写成 `.uk-navbar-container:not(.uk-navbar-transparent)` 规则(深浅两套共用一条)。
|
||
- 面板底部有 `导航文字 6.0:1` 的读数,按**压平后的顶栏**算(半透明时是「顶栏压在底色上」的结果,
|
||
不是顶栏色本身)。低于 4.5 变橙 —— 装饰层从顶栏后面穿过时还会更低,取最不利情况定透明度。
|
||
- ⚠️ **全透明不给导出**:顶栏一透明导航就失去依托、文字直接压在花纹上,比"被裁一段"更糟。
|
||
这一档留着的唯一用途是**预览对比**;`validate()` 会拦,仓库的 `TestNavbarNotFullyTransparent`
|
||
也会拦(两处同一条规矩,生成器里绕过也没用)。
|
||
- 旧工程(配色里直接塞 `rgba(...,0.52)`)打开时按 alpha 反推档位:1 → 实色、0 → 全透明、其余 → 半透明。
|
||
|
||
**图片池** —— 上传任意张,浏览器本地转 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%。
|
||
|
||
## 导出画质
|
||
|
||
WebP 体积 ≈ 像素数 × 质量。**真正的大头是像素**:CSS 里只显示 400px 高的立绘,
|
||
2 倍屏也只要 800px,上传 3000px 的图纯属白扛。所以顺序是:先砍长边,再降质量。
|
||
|
||
| 设置 | 默认 | 说明 |
|
||
| --- | --- | --- |
|
||
| 质量 | 0.88 | 0.85~0.9 肉眼几乎看不出差别,低于 0.8 才明显糊 |
|
||
| 长边上限 | 1600 | 只缩不放。整页背景建议 1920/不限制;装饰人物 1200 够用 |
|
||
| 素材前缀 | 空 | 空 = 图片放 `static/images/themes/<slug>/` 随二进制发布;填 `https://cdn.example.com/themes` = CSS 写绝对地址,图片放自己的对象存储(R2 / MinIO / B2 / OSS / COS,任何 S3 兼容都行)。**前缀里不要带 slug** |
|
||
|
||
导出的 CSS 只有 `urlOf()` 一处决定图片地址,图层、背景、窄屏块全走它,不会出现
|
||
「背景走 CDN、装饰还指本地」的半吊子状态。素材前缀只影响导出,预览永远是内联 dataURL。
|
||
|
||
详细取舍(生成器前缀 / 应用内 301 / 边缘改写)见 `docs/theme-assets-cdn.md`。
|
||
|
||
## 素材托管(本地 / 任意 S3 兼容)
|
||
|
||
「图片放哪」由你自己定,面板里二选一:
|
||
|
||
- **本地**(默认):打进 `static/images/themes/<slug>/`,随二进制发布。部署即生效、不依赖外部服务,
|
||
代价是换一张图要重新编译。
|
||
- **S3 兼容存储**:传到你自己的对象存储,导出的 CSS 写绝对地址。这里刻意不锁厂商 ——
|
||
R2 / MinIO / Backblaze B2 / 阿里 OSS(S3) / 腾讯 COS(S3) 用的是同一套 SigV4,
|
||
差别只在 `Endpoint`、`Region`、是否路径风格,全都做成了字段。
|
||
|
||
填好 Endpoint / Region / 桶名 / 路径前缀 / 密钥后:
|
||
|
||
1. **测试连接** —— 依次验四件事:`HEAD` 桶(连通与凭据)、`PUT` 一个探针对象(写权限)、
|
||
`GET` 读回比对(内容一致)、匿名 `GET`(传上去的图能不能被浏览器直接读到),最后删掉探针。
|
||
失败时会把 S3 的 `<Code>` 翻成人话(`SignatureDoesNotMatch` → 密钥/Region/时钟三选一)。
|
||
2. **上传素材** —— 把本次主题真正引用到的图(与导出同一个集合,`usedAssetIds()`)逐张 PUT,
|
||
键为 `<路径前缀>/<slug>/<id>.webp`,带 `Cache-Control: public, max-age=31536000, immutable`。
|
||
成功后自动把上面的「素材前缀」填成推导出的公开地址。
|
||
|
||
⚠️ 上传走的是本机 `serve.py` 的 `/api/s3/test`、`/api/s3/upload`,浏览器不直连对象存储 ——
|
||
所以**不用给桶配 CORS**,密钥也不会出现在页面发往第三方的请求里。它只存在当前标签页的
|
||
`sessionStorage` 与内存:不进 `localStorage` 草稿、不进工程 json、不进导出 zip。
|
||
用 `python -m http.server` 起的只有静态服务,探测不到接口时按钮会禁用并说明原因。
|
||
|
||
公开地址留空时按 endpoint 推导(path 风格 = `<endpoint>/<桶>/<前缀>`)。R2 与私有桶
|
||
需要自定义域或 CDN 域名,那种情况自己填「公开前缀」。测试里的「匿名可读」会给一条提醒,
|
||
但不会拦上传 —— 很多人就是靠自定义域分发的。
|
||
|
||
### 检测与清理
|
||
|
||
传上去只是第一步,「传上去之后对不对」是另一回事:同一个 slug 传过三次、换过 slug
|
||
旧图还留在桶里、重新导出后远端还是旧版 —— 这些在页面上都看不出来。所以还有一块
|
||
「远端素材」,点「**检测**」列出 `<路径前缀>/<slug>/` 下所有对象,按对象键名跟本地比:
|
||
|
||
| 状态 | 判断依据 | 能做什么 |
|
||
| --- | --- | --- |
|
||
| 一致 | 远端大小 == 本地编码后大小 | — |
|
||
| 旧图 | 同名但大小不同(多半重新导出过) | 行内 ↻ 用本地覆盖 |
|
||
| 多余 | 远端有、当前主题已不再引用(换过 slug / 删过素材) | 行内 ✕ 或「删除多余」批量删 |
|
||
| 缺失 | 本地有、远端没有 | 「补齐差异」一次传完 |
|
||
|
||
- 「**补齐差异**」只传缺失 + 旧图,一致的不重传(省流量也省时间);
|
||
- 「**删除多余**」列清单后二次确认,走 `DeleteObject`(幂等,不存在的键也算成功);
|
||
- 改过 slug 会提示「重新检测」—— 拿旧主题的清单去比新主题,会满屏「多余」;
|
||
- 只动远端,本地素材与工程文件不受影响。
|
||
|
||
厂商怎么填(2026 年的常见值,各家控制台都有「S3 API」页面):
|
||
|
||
| 厂商 | Endpoint | Region | 路径风格 |
|
||
| --- | --- | --- | --- |
|
||
| Cloudflare R2 | `https://<账号ID>.r2.cloudflarestorage.com` | `auto` | 勾选 |
|
||
| MinIO(自建) | `http://<host>:9000` | `us-east-1`(看服务端配置) | 勾选 |
|
||
| Backblaze B2 | `https://s3.<区域>.backblazeb2.com` | 区域名,如 `us-west-004` | 勾选 |
|
||
| 阿里云 OSS | `https://s3.oss-<区域>.aliyuncs.com` | 区域,如 `cn-hangzhou` | 看控制台 |
|
||
| 腾讯云 COS | `https://cos.<区域>.myqcloud.com` | 区域,如 `ap-hangzhou` | 看控制台 |
|
||
|
||
AWS S3 或已绑定的桶级自定义域可以取消「路径风格」(走 `<桶>.<endpoint>`)。
|
||
|
||
两条自检(都在本目录,不依赖 UI):
|
||
|
||
```bash
|
||
python dev/theme-builder/check_sigv4.py # 与 aws-sdk-go-v2 的 signer 逐字节对拍
|
||
python dev/theme-builder/selftest_upload.py # 起假 S3,跑「测试连接 → 上传 → 读回」
|
||
python dev/theme-builder/mock_s3.py --port 9900 # 想手动点面板时,先起这个假 S3
|
||
# 生成器在别的设备上打开时,假 S3 也要 --host 0.0.0.0,面板里的 Endpoint 填本机 IP
|
||
```
|
||
|
||
面板底部实时显示 `3 张,共 19KB;最大一张 bg.webp 7KB(1920×1080)`。
|
||
改完立刻按「原图 + fx 参数」重编码全部素材,并报出省了多少 —— 与图片处理同一条非破坏原则,
|
||
来回试不会叠加压缩。
|
||
|
||
⚠️ 两个反直觉的实测结论(都在 `?dump=quality` 里做了断言):
|
||
|
||
- **`quality = 1` 不能用**。Skia 在这个值会切到 WebP 无损模式,输出反常:同一张噪声图
|
||
q=1 只有 52KB,q=0.95 是 440KB。滑块上限卡 0.95,`canvasAsset` 里也夹一道。
|
||
- **平涂/线稿降质量可能变大**。有损编码对纯色会引入噪点,噪声图 q 从 0.6 到 0.95 体积涨 75%,
|
||
而平涂图怎么调都是 1KB —— 不能承诺「降 q 一定更小」。这类素材优先调长边。
|
||
|
||
缩小走 `imageSmoothingQuality = 'high'`,一次缩太多也不会糊成一团。
|
||
|
||
## 工程文件与草稿
|
||
|
||
调主题是连续几十次的活,所以配置必须留得住:
|
||
|
||
- **新建**:顶栏按钮,清空当前工程并删掉浏览器草稿(有二次确认)。
|
||
之前只有 `?fresh=1` 这个隐藏参数 —— 用户看到「上次的数据」一直在,就会以为关不掉。
|
||
- **自动草稿**:每次改动后 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`。
|
||
|
||
导出结果会报「几个文件 / 总共多大 / 其中几张图共多大」,并附上「哪些层在哪些视口下越界」——
|
||
看到 `其中 4 张图共 3.2MB` 就该回去调长边上限了。
|
||
|
||
解压到项目根,然后按 `INSTALL-<slug>.txt` 做三步:
|
||
|
||
1. `conf/app.ini` 的 `[theme] available` 加上 slug;
|
||
2. `internal/conf/static.go` 的 `StaticVersion` +1;
|
||
3. `go build`(模板与静态资源都是编译期 embed,只重启不重编不会变)。
|
||
|
||
设置页下拉里显示的**就是 slug**。想要中文/日文名,到 `internal/i18n/dict_*.go` 加
|
||
`theme.name.<slug>` 词条(四份词典都要加)。不加也不会露 key —— 模板走 `themeName`
|
||
函数,缺词条时回退成 slug(见 `internal/theme.DisplayName`)。
|
||
|
||
⚠️ **窄屏段导出前先看一眼「窄屏隐藏全部装饰层」有没有勾着**:勾着的话 `@media` 会把
|
||
10 个槽位全写成 `none`,桌面完全正常、手机上一个素材都没有 —— 这是「素材在窄屏下消失」
|
||
最常见的来源。现在勾选时面板会给橙色警告、图层行标「窄屏隐藏」、加层和导出时也会再提醒一次。
|
||
|
||
## 它不做什么
|
||
|
||
- **不做复杂修图**:没有笔刷、蒙版、自由裁切、调色曲线 —— 这些该在专门的工具里做完再进来。
|
||
- **不做调色**:想统一「雾度」(人物压白 16%、场景 30~38%)得在生成端做,
|
||
见 `docs/theme-shiroko-assets-spec.md`。
|
||
- **不写配置**:`[theme] available` 只给要粘贴的那一行。
|
||
- **不碰深色的图片**:深色块一律 `--tb-page-image: none` + 装饰全关(白天场景压在深底上会脏)。
|
||
想在深色下也放图,需要手工加深色块。
|
||
|
||
## 自检
|
||
|
||
没有 UI 也能回归,两个钩子:
|
||
|
||
```
|
||
?demo=1 载入示例素材(canvas 画的背景 + 剪影人物)
|
||
?narrow=hide 直接落在「窄屏隐藏全部装饰层」状态(复现「素材在手机上不见了」)
|
||
?rows=2 加 2 层并依次绑到不同素材(截图多图层效果用)
|
||
?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=layers 图层/作用域回归(加层默认不绑图、行内换图、自动切独立、新建清空)
|
||
?dump=fit 尺寸自适应回归(五档视口占用、溢出检测、fitw、旧工程兼容、@media 覆盖各作用域)
|
||
?dump=quality 画质回归(q 单调性、q=1 被夹到 0.95、长边上限真的压像素)
|
||
?dump=palette 派生文字色的对比度回归(四种配色下 ≥AA、层级不倒挂、手填低对比不继续变淡)
|
||
?dump=navbar 顶栏三档回归(实色无 blur、半透明带 alpha+blur、全透明被 validate 拦、alpha 边界)
|
||
?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 错位
|
||
(表现为整页重复同一张插画,或人物跑到左上角),靠注释守不住。
|