Files
tamabox/.workbuddy/memory/2026-10-20.md
T

23 KiB
Raw Blame History

2026-10-20

01:10 shiroko 主题补「空气层」

  • 用户问「还有没有其他元素,贴合主题又不突兀」。结论:场景压白 38% 后整页亮度只有 235~246, 再摆物件(书包/粉笔/纸飞机)一定会像贴纸,所以走纯渐变造光路线。
  • 做了 4 个候选截图(A 纯窗光 / B 白散景 / C 光环符号 / D 组合),用户选 D。 ⚠️ B 是反面教材:白色散景叠在已经压白的场景上,实测差异 0.14 级,肉眼完全不可见。 要"提亮"必须先有暗部,真正起作用的是「底部压深 + 暖色光斑」的组合。
  • 最终 --tb-deco 后 4 槽:左窗光柱(112deg)、右侧 120px 暖散景(88% 30%)、 脚下 560×170 地面光斑(28% 94%)、底部 24% 压深。亮度差 3~9 级。
  • 验证手段(本轮模型读不了图,全靠数值):留白区差分(与候选 D 差 0.0)、 分区亮度表、横向扫描相邻像素差 max 4(判色阶断层)、PNG 体积比对。
  • ⚠️ 造预览页两个坑:① 覆盖块必须包在 <style> 里,裸写会被当正文渲染,整页布局炸掉; ② --tb-deco 各层之间漏逗号会让整条声明失效 → 人物和所有背景层全没,且各变体看起来"完全一样"。 以后先 diff 两个变体的 CSS 文本再截图。
  • StaticVersion → 20261020a;提交 fb94b8c 已推送。文档 docs/theme-shiroko.md 加「空气层」一节。

08:30 重画光环

  • 用户反馈「光环变形太奇怪,而且中间没有扣干净」。根因两条: ① AI 画的环本身歪(bbox 291,93–584,232,比头还宽、偏移到左边); ② 环内是封闭白孔(面积 5190px 的包围区域),四角 floodfill 进不去 → 抠完环里糊着一团白。
  • 做法:非白像素从环上一点漫水拿到独立连通域(与头发不连通,头部顶端在 y≈265,环只到 232), 整块抹白;先把 alpha 建好,再在 RGBA 上画新环——环心天然就是透明的,不用给封闭孔补洞。
  • 新环参数:中心 (515,160)、100×34、线宽 10、-7° 倾斜、4 倍超采样后 LANCZOS 缩小、 外加 45% 模糊柔光;颜色 (127,211,220),压白 16% 后实测环心 (164,220,224)。
  • 坑:擦除矩形右边界要绕开灰色狼耳(x>630,y 190-260,颜色 (139,140,146) 不是青色), 差点一起抹掉。判据是连通域 bbox 而不是"上方全是空白"。
  • 验证:alpha 字符图确认环闭合、环心 alpha=0;渲染后页面里青色像素 bbox (201,467)-(271,649), 环在 y 468-486,头顶约 505,间距正常。
  • StaticVersion → 20261020b,图片 ?v=20261020b;提交 2e9bd6b 已推送。

09:40 部署反馈三修 + 自行车/步枪

  • 狼耳顶端被削平:上一版用矩形 (281,83)-(594,242) 抹旧光环,右下角正好切到左耳尖 (耳尖在 y≈185、x 420-480)。改成只抹光环的非白连通域(bbox 291,93-584,232,与头发不连通)。
  • 手内侧 / 大腿内侧白没抠干净:那是闭合的白色区域,四角 floodfill 进不去。 加封闭白孔检测(>=246 且不与边缘连通、面积>=40)整块掏成透明。 ⚠️ 阈值必须 >=251 才能当"背景":背景恒为 (253,253,253),而白袜子是 237, 用 246 当背景阈值会把袜子一起吃掉(第一版就吃穿了一只袜子)。
  • 人物像站在课桌上:原场景前景全是课桌,根本没有地面。重出场景: 前景留空地、课桌退到右后方,人物脚下才是地板。
  • 自行车 + 步枪:独立素材 props/bike.webp(镜像成车头朝右、与人同向、压白 26%、 自带接触阴影),放在人物身后偏左。deco 层顺序 = 画在上面的在前,所以第一层是人、 第三层是车,车天然被她挡住。
  • ⚠️ background-position 的百分比是相对 (定位区宽 - 图片宽),不是相对视口宽。 人(117px)和车(397px)用同一个 calc(50% - 901px) 会差出 280px,车直接跑出屏幕只剩一片轮圈。 按各自宽度反解后:人 calc(50% - 524px)、车 calc(50% - 612px)。
  • ⚠️ 道具不能烘焙进背景:背景走 cover,视口一变裁切量就变,和固定 px 的人物必然分家。
  • 场景重出后色调用「逐通道均值/方差对齐旧场景」保持同一雾度;移动端从同图竖裁,避免两个房间。
  • StaticVersion → 20261020d;提交 15aebd6 已推送。

09:55 素材规格书

  • 用户打算换工具自己生成素材 → 写了 docs/theme-shiroko-assets-spec.md: 画布/导出/显示尺寸、比例(人 1:3.41、道具 1.53:1、背景 16:9)、雾度(人 16%、车 26%、场景 3038%+模糊 1.21.4px)、 构图硬约束(左侧 22% 前景必须是空地,否则又回到"站在课桌上")、换图清单与定位公式。
  • 配色给的是压白后的显示值 + 反算公式 原色=(显示值-40.8)/0.84,并特别标注袜子要用 #ededed 而不是纯白(纯白会和背景粘连,抠图必然吃穿)。

10:40 主题生成器(dev/theme-builder)

  • 单文件 dev/theme-builder/index.html:上传背景/人物/道具 → 拖拽调位置 → 导出 zip, 解压到项目根即可新增主题。不联网、不起服务、图片全在浏览器本地转 WebP。
  • 预览用和线上同一套机制渲染(pattern + deco 6 层 + page-image,size/position 对齐), 不是另画示意图;拖出来的「左沿像素」反解成 calc(50% - Npx),换视口不跑位。
  • 导出包:static/css/themes/<slug>.css + static/images/themes/<slug>/*.webp + INSTALL-<slug>.txt。 CSS 同时写浅色/深色两块,10 个必需令牌齐全——实测通过 TestThemeCSSComplete。
  • zip 自己实现(STORE + CRC32,约 40 行),不引 CDN;unzip -t 无错。
  • 踩坑:① 面板里 c.text2 写成 undefined(light 对象里才算出来的派生色,要用 light.text2); ② contain 模式如果不按内容裁画布,会留大片透明边 → 导出宽高比全错; ③ 预览容器 grid-template-columns:340px 1fr 会被 1512px 的 frame 撑破 → minmax(0,1fr); ④ 无头 Chrome 下 canvas.toBlob 的回调在 --dump-dom 前不触发 → 全部改 toDataURL 同步化, 自检才能跑(?selftest=1 输出 CSS/zip 字节,?demo=1 载入示例素材)。
  • ⚠️ 预览里深色必须走「暗色渐变 + deco none」,否则会拿白天场景图压深底(和导出结果不一致)。

11:05 主题生成器重做:真实页面画布 + 按页面作用域

用户原话:旧生成器「只针对当前遇到的问题,需要更通用」「示例页面和我的项目完全不一样」 「想针对箱主页面、问题页面、问题列表、设置页面分别配置」。

机制层(新)

  • internal/context.pageScope() 按路径归类,<html data-page-scope>: box(//{domain}) / question(//{domain}/{id}) / admin(/user/questions,/user/votes*) / profile(/user/*) / vote(/vote/{token})。首页与 /explore 返回了值但不加载主题 css, 所以生成器里不列它们(列了就是骗人)。
  • ⚠️ 深色覆盖必须是三重选择器 html[data-theme="dark"][data-theme-style][data-page-scope]: 浅色作用域块特异性 0,2,1 与深色块相同、且写在后面,二重选择器会被它盖掉。
  • 装饰槽位 6 → 10(base.css 背景 8 → 12 层 = pattern1 + deco10 + image1)。 TestDecoSlotCount 逐文件逐声明检查,变异验证有效(写 6 个会 FAIL)。

生成器(重写)

  • 画板 = iframe 加载 outputs/preview-*.html(真模板渲染)。必须起本地服务: file:// 下父页面读不到 iframe 的 DOM(即使同目录),预览只能退回示意图。 dev/theme-builder/serve.py 把项目根当静态目录(只读)。
  • 每个作用域独立「整页背景 + 最多 10 层装饰」,默认跟随全局;不产覆盖块就自动回退。
  • 图层 = 图片 / 径向光斑 / 线性光;9 宫格锚点 + 像素偏移;图片层可在画板上直接拖。
  • ⚠️ 位置统一 calc(0%|50%|100% ± Npx) 两值:百分比是相对 (容器 − 图片)。 不要 left/bottom 关键字混搭 —— 「水平居中 + 垂直贴底」是三值语法,非法时整条声明作废 → 图层静默掉到左上角(这是之前踩过的坑,这次直接规避)。
  • ⚠️ 配色只让填 6 个基础色,另 4 个必需令牌派生:surface-2 往 text 混(浅色下变深、 深色下变亮,方向自动对),text-secondary/muted 往 bg 混,primary-weak 加 alpha。 漏了 --tb-primary-weak 时 TestThemeCSSComplete 只报「缺少」,页面看着没问题,很容易混过去。
  • 自检钩子:?dump=css / ?dump=zip(写进 <pre id="dump">),配合 --dump-dom 回归。

验证(都是实测,不是推断)

  • 生成器导出的 CSS 放进 static/css/themes/ → TestThemeCSSComplete + TestDecoSlotCount 双绿; 删掉深色块做变异 → FAIL,说明测试真的在查。
  • 真实箱主页计算样式:image/size/position/repeat/attachment 各 12 层,前几层尺寸与 shiroko 一致 (auto 400 / 96x13 / auto 259 / 100%×3),确认扩槽没打乱顺序。
  • 临时主题 zztest 验证:box 页有人物、question 页 --tb-deco: none×10 后人物消失; 不写覆盖块时回退全局(人物仍在)。

顺手发现:.gitignore 写的是 ./outputs(无效写法),导致 247 个 outputs 文件被跟踪, 每次跑 TestGenPreview 都会产生一堆 M。没擅自改(会让人以为要清理历史产物),留待用户决定。

12:20 outputs 不再纳入版本控制

  • 根因:.gitignore 里写的是 ./outputs,gitignore 不认 ./ 前缀,整条规则等于失效, 247 个产物(预览快照、截图、历史 review 报告)一直被跟踪,每次跑 TestGenPreview 都产生一堆 M。
  • 改成 /outputs/ + git rm -r --cached outputs:索引删除 247 个,磁盘文件一个没动。 git check-ignore -v outputs/x 确认命中。
  • 另两条同样无效的规则没动:./workbuddy(目录根本不存在,笔误)、./scripts(有 1 个文件被跟踪)。 用户只要求 outputs;要清理得单独确认。

13:10 生成器能力盘点(含实测)

用户问「有什么改进建议」,顺手实测了一个之前没验证过的点:窄屏(390 宽)。

  • 没有 @media 规则的主题,在 390×844 下:16:9 桌面背景被 cover 裁成一条无意义纹理, 人物被占满屏的卡片完全盖住 —— 等于白配。
  • shiroko 因为手写了 @media (max-width:900px)(换 scene-mobile.webp + deco 全关), 同一视口下背景是协调的竖版、装饰退场。
  • 结论:生成器导出的主题没有窄屏段,这是真实缺口,不是理论问题。

其它已确认的缺口(grep 过,不是猜):无 localStorage / 无 json 导入导出(关掉全丢)、 无图层上移下移(遮挡关系由顺序决定,只能删了重加)、无导出前校验、无既有主题导入。

⚠️ 测窄屏时必须让 iframe 宽度一起变(verify.html?w=390)。第一次只改外层 window, iframe 内部仍是 1512,媒体查询按 1512 判定 → 结论是错的。

14:20 生成器补 P0 四项

用户问建议 → 我给了 P0/P1 清单 → 他选「P0 四条一起做」。已实现并推送。

  1. 配置留得住:localStorage 自动草稿(renderAll 后 700ms debounce)+ theme-<slug>.json 保存/打开(图片 base64 内嵌,单文件带走);zip 里也塞一份 json;?fresh=1 跳过恢复。
  2. 图层 ↑↓:顺序即层级(background-image 第一层画在最上)。
  3. 窄屏段:竖版背景图 + 隐藏装饰 + 断点(默认 900),导出 @media 块。 ⚠️ 只写浅色规则:深色块特异性 0,2,1 > 浅色 0,1,1,本来就压住它;多写一份反而容易出错。
  4. 导出前校验:slug / 图片层缺图 / 引用已删素材 / 超 10 层 / 生成 CSS 每处装饰是否真 10 个值(与 Go TestDecoSlotCount 同规则,给工具自身兜底)。

实测

  • 390 视口首层装饰 url(char.webp) → none,1512 下不变;两种视口都 12 层对齐。
  • 草稿往返:带 ?demo=1 保存 → 不带 demo 打开,validate 仍为空(说明恢复成功)。
  • 示例 CSS 过 TestThemeCSSComplete + TestDecoSlotCount(27 处声明)。

踩到的小坑

  • makeDemo() 里 S.assets.push 不检查重复 → 重复点「载入示例」会撞 id(assetById 取第一个), 导出时静默用错图。改成开头 S.assets = []。
  • 拖拽标签写 top:-17px 会贴到画板边缘被压线/裁切,改成放在框内左上角; 整块半透明填充会把人物罩成一层米色(看着像"框住空气"),改为纯虚线 + 角标签。
  • 面板一多左栏就滚不到底:按使用频率重排(图层 → 背景 → 窄屏 → 图片池 → 配色)。

15:20 生成器:图片处理(透明度 / 压白 / 去色 / 裁边 / 去白底)

用户问「图片的透明度或者一些简单的处理能否安排上」。已实现并推送 c3d34b5。

非破坏模型(关键设计)

  • orig 存上传时的原始字节(FileReader.readAsDataURL,不重编码),fx 只存参数, 每次处理都从 orig 重算 → 「压白 16% → 20%」不会叠加两次 webp 压缩。
  • 打开面板看一眼就关:参数没变就跳过重编码,一个像素都不动(fxSnapshot 比对)。 这条不加,用户点开看看就会把图越磨越糊。
  • 预览按 420px 宽算(滑杆跟手),完成时全分辨率重算;用序号 fxSeq 丢弃过期结果, 否则快速拖动时旧渲染覆盖新的。

去白底两个坑(都实测过)

  • 手内侧 / 两腿之间是闭合白区,四边漫水进不去 → 按连通域单独掏,面积 ≥ 24 才掏 (更小的多是眼睛高光,掏了变透明洞)。
  • 容差默认 16,不是 22:背景 253、白袜子 237 只差 16。容差 30 实测把袜子吃穿(alpha 0)。

验证(跑出来的)

  • ?dump=fx 13 项断言全过:封闭孔开/关、容差 16 保袜子 / 30 吃穿、收边后轮廓 1px、 alpha 50%→128、压白 20% 时 237→241、去色后 R=G=B、裁边 200×160→80×40。
  • ?dump=fxpipe 链路:弹窗开关、预览 300×960、参数重开后仍在、改参后 dataURL 变、 无改动时 dataURL 一字节不变(这条是防回归的关键)。
  • ?dump=fxkey 端到端:把 shiroko 立绘合成到 253 白底再去白底,透明占比 46.3%, 与手写 floodfill 抠出来的 46.6% 差 0.3%。
  • 生成的 CSS 仍过 TestThemeCSSComplete + TestDecoSlotCount。

顺带

  • 工程文件升 v3:存 orig + fx,未处理时不重复存 dataURL(否则 json 平白翻倍); 读 v2 旧工程把 dataURL 当 orig 用。
  • 新增 ?loadurl= 把项目里的图直接当素材载入(不必先下载再上传),?fxjson= 给初始参数。
  • 截图看出两个 UI 问题:拖拽标签 top:-17px 贴到画板边缘被压线、半透明填充把人物罩成米色 → 改框内左上角标签 + 纯虚线(上一轮已修,本轮沿用)。

16:10 生成器:图层换不了图 / 背景关不掉 / 草稿清不掉

用户反馈三条,都是交互设计问题,核心逻辑其实没坏(实测换图是生效的)。已推送 937a890。

  • 「每层都只有这张图、换不了」:add-layer 里 l.asset = S.assets[0].id,连加三层全是同一张; 而且换图的下拉只在「选中那一层的编辑器」里,用户看不出每层能各自换。 → 新层不再自动绑图(素材池只剩一张时才绑);每行带缩略图 + 「图片」下拉直接换。
  • 「背景关不掉」:inherit 时背景下拉 disabled —— 用户在箱主页看到背景还在又点不动。 → 一律可点,一改自动 ensureOwn(true):切成独立配置 + 把全局当前值复制过来当起点 (否则改背景会顺带把图案弄丢);复制图层要重发 id,不然两个作用域共用一批 id,拖拽/高亮会串。
  • 「上次的数据一直在」:localStorage 草稿只有隐藏参数 ?fresh=1 能绕过,UI 无出口。 → 顶栏加「新建」(清空 + 删草稿 + 二次确认)。

⚠️ 跟随全局时不能给拖拽框:那时 cfgOf(S.scope) 返回的是 global 对象,一拖就改到全局去了。 syncOverlay 里已加提前 return。同理 ensureOwn() 是「任何写操作前」的必调项。

回归(?dump=layers):加层后 [null,null]、行内下拉 2 个、换第 2 层后 [null,blue] 且 CSS 只出现 blue; box 页 inherit 时 addDisabled/bgDisabled 都 false;在 box 加层后本页 3 层而全局仍 2 层; 新建后 assets 0、层 0、草稿已删、scope 回 global。

17:5x 生成器:视口自适应 + 导出画质

用户两个问题:插入素材后不同屏幕被裁、导出的 webp 太大。都在 dev/theme-builder/index.html 里解决。

尺寸单位 4 档(l.unit):fit(按视口高,人物)/ fitw(按视口宽,横幅)/ px / vh。 输出 auto clamp(min(calc(100vh - 24px), 231px), 46.67vh, 420px) 这种。

  • 关键点 1:内层 min(视口 - 24px, 基准×55%) —— 下限让位给视口,自适应单位因此永不溢出。 写死 55% 下限的话,390 竖屏放不下 1067 宽的图,照样裁。
  • 关键点 2:必须有 fitw。fit 只按高度缩,宽幅素材宽度按比例涨:1920×1080 素材 h=600 在 390 视口下 fit 出 1001px 宽(裁),fitw 出 366px(完整)。实测见 ?dump=fit 的 fitw 数组。
  • 关键点 3:layerSize()(出 CSS)与 unitGeom()(出预览框/溢出检测)必须共用一套换算, 两套各算迟早漂成「框没超、图已裁」。
  • ⚠️ 下拉 value 要用 l.unit,不能用 unitOf()(把 fit 归一成 vh → 选了 fit 却显示「按视口高比例」)。

视口预设带高度(VIEWPORTS 改成 {w,h,label},加了 390×844)。原来只有宽度、iframe 恒 900 高, 768 的旧笔电和 844 手机根本模拟不出来 —— 而裁切几乎全由高度决定。

溢出检测 + 一键修好:layerMetrics(l,a,VW,VH) 给 overL/R/T/B,图层编辑区显示 「1512×900 下显示 131×420,完整可见」或橙色「⚠ 左右各 88px 越界」+「按当前视口修好」按钮 (切 px 并算刚好放下的尺寸)。导出前 overflowReport() 再扫一遍,只提醒不拦。

窄屏每层策略 l.mob(inherit/scale/hide)+ 全局 S.narrow.scale(默认 0.62)。 ⚠️ 顺手修了一个既有 bug:@media 块原来只写 html[data-theme-style](0,1,1), 被页面覆盖块 (0,2,1) 压住 → 独立配置的页面在手机上从来没生效过。现在每个独立 scope 各写一条。 ⚠️ 竖版图只替换 cfg.bg 非空的页面(否则给纯色页塞图 = 改了他的设计)。

背景焦点 cfg.bgPos → --tb-page-position;base.css 里 var(--tb-page-position, center center) 兜底 (旧主题不写也不坏),StaticVersion → 20261020f。

导出画质:S.export = {quality: 0.88, maxSide: 1600},改完全量 rebakeAll()(从 orig 重算,不叠加压缩)。

  • ⚠️ toDataURL('image/webp', 1) 是坑:Skia 在 q=1 切无损,输出反常 —— 实测同一张噪声图 q=1 只有 52KB、q=0.95 是 440KB。canvasAsset 与滑块都夹到 0.95。
  • ⚠️ 平涂/线稿降 q 可能变大(有损引入噪点;实测平涂图各档都是 1KB,噪声图 0.6→0.95 涨 75%)。 所以文案写「先砍长边再降质量」,不承诺「降 q 一定更小」。
  • 缩小加 imageSmoothingQuality='high'。

自检:新增 ?dump=fit(视口占用/溢出/fitw/旧工程兼容/@media 覆盖各 scope)与 ?dump=quality(q 单调性、q=1 被夹、长边真压像素),两项 ok:true;?dump=validate 空、layers/fx/fxpipe 全绿。 生成 CSS 临时存成 static/css/themes/gentest.css 跑 go test ./internal/theme → 30 处声明全过(clamp 里的逗号没被当层分隔符)。 ⚠️ 顺带发现 //go:embed 的 * 不匹配 _ 开头文件 —— _scaffold.css 与 _gentest.css 都不进 embed, 所以那个校验一开始"跑了但数字没变",改名后才真正生效。

UI 踩坑:面板仅 ~240px,input[type=range] 有固有宽度 → 必须 flex:1 1 0;width:0;min-width:0, 且质量数值要放进 label 里(作第三个 flex 项会被顶出可视区,表现为"滑块没有数字")。

18:2x 排查用户部署的 test01:窄屏素材消失 + 主题名显示成 key

用户自己导出 test01 部署到 box-dev-api(admin6),报两个问题。两个都复现了,都不是"环境坏了"。

测试环境体检(都正常):/_/admin6 200、test01.css 200 且与本地 md5 一致、 base.css?v=20261020f(新版,含 --tb-page-position)、<html data-theme-style="test01" data-page-scope="box">。 部署本身是干净的。

问题 1:窄屏素材消失 = 工程里「窄屏隐藏全部装饰层」勾着 → 导出 CSS 的 @media 把 10 槽全写 none。 桌面完全不受影响,所以只看桌面发现不了。定位过程绕了两步弯路,记下来:

  • 先怀疑 CSS 表达式非法(clamp(min(calc(...)),…))→ 最小复现页证明表达式没问题,图能显示。
  • 再怀疑层级:其实素材位置 calc(50% - 0.67vh) 正好在卡片正下方,卡片 rgba(255,255,255,0.86) 只剩 14% 可见,本来就非常淡。
  • ⚠️ 本地 preview 快照没有 data-page-scope(TestGenPreview 不走 Contexter),所以 [data-page-scope="question"] 覆盖块在快照里根本不生效,读到的是 global 的 --tb-deco: none。 本地验证主题必须手动给 <html> 补 data-page-scope,否则会误判成"主题没生效"。
  • 修:删掉 test01.css 的 @media 段(装饰是 fit 单位,浏览器自己会缩)。390 截图确认素材回来了。
  • 生成器侧:勾选时橙色警告(几层会消失)+ 图层行标「窄屏隐藏」+ 加层/导出提醒 + 「窄屏」下拉被覆盖时标注;新增 ?narrow=hide 复现。

问题 2:设置页显示 theme.name.test01 = 模板直接 {{t .LangCode (printf "theme.name.%s" .Slug)}}, i18n.T 缺词条返回 key。修:theme.DisplayName(lang, slug) 缺词条回退 slug,模板函数 themeName。 加了 TestDisplayName(自建主题必须回退 slug,不能返回 key)。这样自建主题依旧零改代码。

StaticVersion → 20261020g(主题 css 内容变了,不换缓存键浏览器 1 天内还是旧文件)。