diff --git a/docs/theme-shiroko.md b/docs/theme-shiroko.md index 6b32803..4e6ec28 100644 --- a/docs/theme-shiroko.md +++ b/docs/theme-shiroko.md @@ -20,33 +20,27 @@ available = default,sakura,shiroko | `--tb-text` | `#222d3b` | `#e3eaf4` | | `--tb-primary` | `#4a7fc1` | `#6fa3e0` | -## 插画布置 +## 视觉构图 -| 位置 | 内容 | 尺寸 | +参考对象:**蔚蓝档案 JP 官网首页**(`https://bluearchive.jp/`)。它的做法不是"白底 + 留白里贴几个人物", +而是 **整个视口是一张场景插画,UI 浮在场景之上**。本主题照这个结构重排: + +| 层 | 内容 | 实现 | | --- | --- | --- | -| 卡片左下外侧 | `sit`(盘腿坐),右侧约 40px 钻进卡片底下 | `auto 340px`,≤1179px 时 `280px` | -| 人物脚下 | 椭圆接触阴影(CSS `radial-gradient`,不是图片) | `128px 15px` | -| 整页平铺 | 雪花 ×2 + 光环 + 两点(340px 一铺,opacity 0.085) | `pattern.svg` | -| ≤900px | 不出人物 —— 卡片占满宽度,两侧没有视觉空间 | `@media` | +| 底层 | 教室场景插画(压白 38%、轻度虚化) | `--tb-page-image`,16:9 桌面 / 9:16 移动两张 | +| 中层 | 卡片与顶栏:半透明白 + `backdrop-filter` 磨砂 | `--tb-surface: rgba(255,255,255,.86)` / `--tb-navbar` | +| 上层 | 人物站在场景左侧地面(337px 高)+ 椭圆接触阴影 | `--tb-deco` 前两个槽位 | +| 深色 | 不用场景(白天教室压在深底上会脏),回到深色渐变 + 极淡纹样 | 深色块单独覆盖 | -摆位为什么是这个人样: +摆位经验(都是踩过的): -- **不要四角各摆一个**。人物之间、人物与内容之间都没有关系,读起来就是贴纸墙纸; -- **右下角不能放人**。页脚文案右对齐(社区规则 / 备案号 / 存活时长),人物会和它挤在一起; -- **要半隐在卡片后面**。被卡片挡掉一部分才有空间关系,否则就是浮在留白中间; - 卡片定宽 750px 居中,左沿 = `50% - 375px`,人物宽约 177px,左移 515px 即遮住 40px; -- **离底边留 26px**。脚踩在窗口最后一行像素上会被读成"被裁掉",抬高之后必须配接触阴影, - 否则变成悬空。 -- **纹样元素要均布**。全挤在 tile 上半部时,铺开后在整页形成明显的横向条纹带。 - -插画走 **装饰层**(`--tb-deco` / `--tb-deco-size` / `--tb-deco-position`),不是整页背景图: - -- 固定像素尺寸 + 四角定位,**任何视口比例都不会被裁切**(`cover` 做不到,竖屏桌面会把贴边的插画切掉); -- 固定 6 个槽位,必须写满 6 个值,缺的填 `none` —— 多背景各列表的层数必须恒定 - (1 图案 + 6 装饰 + 1 背景 = 8),值一多一少,分层就整体错位(会出现整页重复小人); -- 深色模式关掉装饰层,只留底色与浅色图案。人物是银白/浅灰为主,压到深色底上会发灰发脏。 -- 图案是图片,不走 `StaticVersion` 缓存键(`Cache-Control: max-age=86400`): - 换 `pattern*.svg` 时要自己给 css 里的 url 补一个 `?v=`,否则老访客最多 24 小时看到旧纹样。 +- **不要四角各摆一个人**。人物之间、人物与内容之间都没有关系,读起来就是贴纸墙纸; + 人物必须"站在场景里"——有地面、有阴影、有环境,才不像贴纸。 +- **右下角不能放人**。页脚文案右对齐(社区规则 / 备案号 / 存活时长),会和人物挤在一起。 +- **离底边留 36px**。脚踩在窗口最后一行像素上会被读成"被裁切",抬高之后必须配接触阴影,否则变成悬空。 +- **纹样不要压在实景上**。平铺纹样叠在教室插画上会变成噪点,只在深色(无场景)时使用。 +- **背景图会被 `cover` 裁切**,所以场景里不能有必须完整露出的元素:教室的黑板和窗户都在中部,裁掉两头不影响。 +- **人物比例要跟场景对得上**。340px 是相对"站在前景"的尺度;缩到 240px 以下会显得像贴纸浮在半空。 ## 素材清单 @@ -59,8 +53,9 @@ available = default,sakura,shiroko | `chibi/think.webp` | 托腮 | 283×620 | 26KB | | `chibi/doze.webp` | 打瞌睡 | 274×620 | 26KB | | `chibi/sit.webp` | 盘腿坐 | 321×620 | 29KB | -| `pattern.svg` | 浅色平铺(几何纹样) | 240×240 | 0.9KB | -| `pattern-dark.svg` | 深色平铺 | 240×240 | 0.8KB | +| `scene-desktop.webp` | 教室场景(16:9,压白 38% + 虚化 1.4px) | 1920×1080 | 22KB | +| `scene-mobile.webp` | 教室场景(9:16 中裁) | 720×1003 | 6KB | +| `pattern-dark.svg` | 深色模式平铺纹样(雪花 + 光环) | 240×240 | 0.7KB | ## 来源与授权 @@ -68,7 +63,8 @@ available = default,sakura,shiroko | --- | --- | | `static/images/shiroko-cutout.png` | **用户自行用 AI 生成**的立绘,仅作生成参考图,不再上页面 | | `chibi/*.webp` | WorkBuddy ImageGen **以用户立绘为参考图**(image-to-image)生成,再抠白底 | -| `pattern.svg` / `pattern-dark.svg` | 手写 SVG,纯几何,无外部素材 | +| `scene-desktop.webp` / `scene-mobile.webp` | WorkBuddy ImageGen 生成(提示词见下),未使用任何外部素材 | +| `pattern-dark.svg` | 手写 SVG,纯几何,无外部素材 | ### 生成与抠图参数 @@ -134,3 +130,22 @@ level=info msg="主题注册表已加载" enabled="default,sakura" level=warning msg="[theme] available 未登记以下主题,它们不会出现在设置页" available="default,sakura,shiroko" themes=shiroko ``` + +## 场景图的生成 + +整页背景是一张生成的教室插画,提示词: + +``` +Empty Japanese high school classroom in the afternoon, flat pastel anime background art, +very light and airy, pale blue-grey and warm cream palette, rows of desks softly out of focus, +big windows with soft daylight, thin clean shapes, low contrast, minimal detail, +large calm empty area in the center, no characters, no text, no watermark, +wide scene suitable as a website background +``` + +生成后处理(PIL,脚本未入库,参数记在这里): + +1. 裁掉图片底部 160px —— 生成器会在右下角打 `AI生成 WORKBUDDY` 水印,不裁会出现在页面上; +2. 按 16:9 取顶部 864px,整体混白 38%(`v*0.62 + 255*0.38`)—— 保证白卡片放上去仍有足够对比度; +3. 高斯模糊 1.4px,让桌椅不抢正文; +4. 导出 WebP q80(1920×1080,22KB);移动端从同一张按 9:16 中裁 620px 宽,导出 720×1003。 diff --git a/internal/conf/static.go b/internal/conf/static.go index 4327ad6..b4fe946 100644 --- a/internal/conf/static.go +++ b/internal/conf/static.go @@ -10,7 +10,7 @@ var ( BuildCommit = "dev" // StaticVersion 静态资源缓存版本号:base.css 与主题 css 共用,改样式只改这一处。 - StaticVersion = "20261019p" + StaticVersion = "20261019q" ) // 文本审核通道取值([security] text_censor_source)。定义在 conf 侧, diff --git a/internal/theme/theme.go b/internal/theme/theme.go index c77b855..aacda6e 100644 --- a/internal/theme/theme.go +++ b/internal/theme/theme.go @@ -28,6 +28,8 @@ var ( slugRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,31}$`) // 仅接受纯色值,避免把主题文件里的任意内容拼进设置页的 style 属性。 colorRe = regexp.MustCompile(`^(#[0-9a-fA-F]{3,8}|rgba?\([0-9.,\s%/]+\))$`) + // 主题 css 里的块注释;取令牌前先剥掉,避免注释与令牌粘连 + commentRe = regexp.MustCompile(`(?s)/\*.*?\*/`) ) // Theme 是设置页需要的一项。名称由 i18n 键 theme.name. 提供, @@ -174,7 +176,9 @@ func ruleBody(css, selector string) string { if end < 0 { return "" } - return css[start : start+end] + // 注释必须剥掉:tokenValue 按 ";" 切分再按 ":" 取值,行内注释会和后面的令牌粘成一段, + // 取到的键变成 "/* ... */ --tb-surface",色块预览就静默失效。 + return commentRe.ReplaceAllString(css[start:start+end], "") } func tokenValue(block, name string) string { diff --git a/internal/theme/theme_test.go b/internal/theme/theme_test.go index e81aa9e..2a5e315 100644 --- a/internal/theme/theme_test.go +++ b/internal/theme/theme_test.go @@ -102,3 +102,16 @@ func TestSwatch(t *testing.T) { assert.Equal(t, "", swatchOf(`html[data-theme-style="x"] { --tb-bg: url(evil); --tb-primary: #fff; }`, "x"), "非色值内容不得进入 swatch") } + +func TestRuleBodyIgnoresComments(t *testing.T) { + // 令牌前面写一行注释很常见;不剥注释的话 tokenValue 会取到 + // "/* ... */ --tb-bg" 这种粘连键,色块预览静默失效。 + css := `html[data-theme-style="x"] { + /* 磨砂卡片:半透明白 */ + --tb-bg: #ffffff; + --tb-primary: rgba(74, 127, 193, 0.11); +}` + body := ruleBody(css, `html[data-theme-style="x"]`) + assert.Equal(t, "#ffffff", tokenValue(body, "--tb-bg")) + assert.Equal(t, "rgba(74, 127, 193, 0.11)", tokenValue(body, "--tb-primary")) +} diff --git a/static/css/themes/shiroko.css b/static/css/themes/shiroko.css index 25153fb..a0517d1 100644 --- a/static/css/themes/shiroko.css +++ b/static/css/themes/shiroko.css @@ -1,16 +1,22 @@ /* 砂狼白子(shiroko):阿拜多斯学园的灰蓝/银白配色。 只覆盖令牌与外观层面的组件微调,不改任何布局。 + 做法参考二次元官网(蔚蓝档案 JP 首页)的构图:**整个视口是一张场景插画,UI 浮在场景之上**, + 而不是"白底 + 留白里贴几个人"。这里用一张教室场景(压白 38%)当整页背景, + 卡片与顶栏改成磨砂半透明,人物站在场景左侧地面 —— 人物有环境,才不像贴纸。 + 素材(static/images/themes/shiroko/): + - scene-desktop.webp / scene-mobile.webp 教室场景(16:9 / 9:16,已压白与轻度虚化) - chibi/*.webp Q 版人物(wave/cheer/think/doze/sit),以用户立绘为参考图生成 + 抠白底 - - pattern.svg / pattern-dark.svg 雪花 + 光环的极淡平铺纹样 - 来源、提示词与抠图参数见 docs/theme-shiroko.md。 */ + - pattern.svg / pattern-dark.svg 雪花 + 光环的极淡平铺纹样(深色模式用) + 来源、提示词与处理参数见 docs/theme-shiroko.md。 */ html[data-theme-style="shiroko"] { - --tb-bg: #edf2f8; - --tb-surface: #ffffff; - --tb-surface-2: #e5ecf5; - --tb-surface-hover: #eaf1f9; - --tb-navbar: #edf2f8; + --tb-bg: #eef3f9; + /* 磨砂卡片:半透明白 + backdrop-filter,让场景的颜色透上来一点点 */ + --tb-surface: rgba(255, 255, 255, 0.86); + --tb-surface-2: rgba(229, 236, 245, 0.92); + --tb-surface-hover: rgba(234, 241, 249, 0.92); + --tb-navbar: rgba(255, 255, 255, 0.52); --tb-border: #d6e0ec; --tb-text: #222d3b; @@ -20,28 +26,23 @@ html[data-theme-style="shiroko"] { --tb-primary: #4a7fc1; --tb-primary-weak: rgba(74, 127, 193, 0.11); - --tb-shadow: 0 1px 2px rgba(38, 56, 82, 0.05), 0 12px 28px -18px rgba(38, 56, 82, 0.24); - --tb-shadow-hover: 0 1px 2px rgba(38, 56, 82, 0.07), 0 18px 40px -20px rgba(38, 56, 82, 0.32); + --tb-shadow: 0 1px 2px rgba(38, 56, 82, 0.05), 0 18px 44px -22px rgba(38, 56, 82, 0.30); + --tb-shadow-hover: 0 1px 2px rgba(38, 56, 82, 0.07), 0 24px 56px -24px rgba(38, 56, 82, 0.38); - /* 整页底色用极浅的竖向渐变(纯 CSS):比图片稳,不会有 cover 缩放差异 */ - --tb-page-image: linear-gradient(180deg, #f8fbfe 0%, #edf3f9 58%, #e2ecf6 100%); - --tb-page-pattern: url("/static/images/themes/shiroko/pattern.svg?v=20261019p"); - --tb-page-pattern-size: 340px 340px; + /* 整页场景:一张教室插画,cover 铺满;不再叠纹样(纹样压在实景上会变成噪点) */ + --tb-page-image: url("/static/images/themes/shiroko/scene-desktop.webp?v=20261019q"); + --tb-page-pattern: none; - /* 装饰层:一个人半隐在卡片左侧后面。 - 卡片是 750px 定宽居中,左沿 = 50% - 375px;人物宽约 177px,左移 515px 就有 40px - 钻进卡片底下 —— 被卡片挡住一部分,才有「站在内容后面」的空间关系,而不是浮在留白里。 - 离底边 26px:踩在浏览器的最后一行像素上会像被裁掉。 - 右下角不能放人 —— 页脚文案右对齐(社区规则 / 备案号),会和人物挤在一起。 */ + /* 人物站在场景左侧地面:离底边 36px,配一层椭圆接触阴影,避免悬空 */ --tb-deco: url("/static/images/themes/shiroko/chibi/sit.webp"), - radial-gradient(ellipse at center, rgba(63, 96, 138, 0.22) 0%, rgba(63, 96, 138, 0) 72%), + radial-gradient(ellipse at center, rgba(63, 96, 138, 0.20) 0%, rgba(63, 96, 138, 0) 72%), none, none, none, none; - --tb-deco-size: auto 340px, 128px 15px, auto, auto, auto, auto; - --tb-deco-position: left max(6px, calc(50% - 515px)) bottom 26px, - left max(6px, calc(50% - 487px)) bottom 19px, left top, left top, left top, left top; + --tb-deco-size: auto 340px, 120px 14px, auto, auto, auto, auto; + --tb-deco-position: left max(6px, calc(50% - 520px)) bottom 36px, + left max(6px, calc(50% - 492px)) bottom 29px, left top, left top, left top, left top; } html[data-theme="dark"][data-theme-style="shiroko"] { @@ -62,48 +63,55 @@ html[data-theme="dark"][data-theme-style="shiroko"] { --tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.34), 0 12px 28px -18px rgba(0, 0, 0, 0.58); --tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.40), 0 18px 40px -20px rgba(0, 0, 0, 0.68); + /* 深色不用浅色场景(白天教室压在深底上会很脏),回到纯色渐变 + 极淡纹样 */ --tb-page-image: linear-gradient(180deg, #161d26 0%, #141a22 60%, #11161d 100%); - --tb-page-pattern: url("/static/images/themes/shiroko/pattern-dark.svg?v=20261019p"); + --tb-page-pattern: url("/static/images/themes/shiroko/pattern-dark.svg?v=20261019q"); --tb-page-pattern-size: 340px 340px; - - /* 深色下人物整体关掉:浅色立绘在深底上会变成一块块亮斑,抢正文 */ --tb-deco: none, none, none, none, none, none; --tb-deco-size: auto, auto, auto, auto, auto, auto; --tb-deco-position: left top, left top, left top, left top, left top, left top; } -/* 卡片最宽 750px,两侧留白在 1000px 以下已经放不下整个人物, - 这时缩小并靠近边缘,让人物被卡片挡住一部分(读作「站在卡片后面」,比硬挤在留白里自然) */ -@media (max-width: 1179px) { - html[data-theme-style="shiroko"] { - --tb-deco-size: auto 280px, 104px 12px, auto, auto, auto, auto; - --tb-deco-position: left max(6px, calc(50% - 481px)) bottom 22px, - left max(6px, calc(50% - 457px)) bottom 16px, left top, left top, left top, left top; - } -} - -/* 窄屏卡片占满宽度,两侧没有视觉空间:人物退场,只留底色与纹样,顺带省掉 60KB 图片请求 */ +/* 窄屏:卡片占满宽度,两侧没有视觉空间 —— 换成竖版场景,人物退场 */ @media (max-width: 900px) { html[data-theme-style="shiroko"] { + --tb-page-image: url("/static/images/themes/shiroko/scene-mobile.webp?v=20261019q"); + --tb-surface: rgba(255, 255, 255, 0.92); --tb-deco: none, none, none, none, none, none; - --tb-page-pattern-size: 260px 260px; + --tb-deco-size: auto, auto, auto, auto, auto, auto; + --tb-deco-position: left top, left top, left top, left top, left top, left top; } } /* 组件微调(仅外观) */ -/* 白卡片压在近白底色上几乎看不出边界,补一圈 1px 冷调描边把它托起来(box-shadow 不占布局) */ +/* 磨砂:卡片与顶栏让场景透上来,才像"UI 浮在画面上"而不是压在照片上 */ html[data-theme-style="shiroko"] .uk-card-default { + backdrop-filter: blur(16px) saturate(1.05); + -webkit-backdrop-filter: blur(16px) saturate(1.05); box-shadow: 0 0 0 1px rgba(74, 127, 193, 0.12), var(--tb-shadow); } html[data-theme-style="shiroko"] .uk-card-default.uk-card-hover:hover { box-shadow: 0 0 0 1px rgba(74, 127, 193, 0.18), var(--tb-shadow-hover); } -html[data-theme="dark"][data-theme-style="shiroko"] .uk-card-default { - box-shadow: 0 0 0 1px rgba(111, 163, 224, 0.14), var(--tb-shadow); +html[data-theme-style="shiroko"] .uk-navbar-container:not(.uk-navbar-transparent) { + background-color: var(--tb-navbar); + backdrop-filter: blur(14px) saturate(1.1); + -webkit-backdrop-filter: blur(14px) saturate(1.1); } -/* 顶栏融进页面底色后,UIkit 默认的 #999 在灰蓝底上偏淡,提到次要文字色 */ +/* 深色不用半透明(背后没有场景,半透明只会让层次变浑),也不加模糊 */ +html[data-theme="dark"][data-theme-style="shiroko"] .uk-card-default { + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: 0 0 0 1px rgba(111, 163, 224, 0.14), var(--tb-shadow); +} +html[data-theme="dark"][data-theme-style="shiroko"] .uk-navbar-container:not(.uk-navbar-transparent) { + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +/* 顶栏融进场景后,UIkit 默认的 #999 偏淡,提到次要文字色 */ html[data-theme-style="shiroko"] .uk-navbar-nav > li > a { color: var(--tb-text-secondary); } diff --git a/static/images/themes/shiroko/pattern-dark.svg b/static/images/themes/shiroko/pattern-dark.svg index 240b924..931d3d1 100644 --- a/static/images/themes/shiroko/pattern-dark.svg +++ b/static/images/themes/shiroko/pattern-dark.svg @@ -1,5 +1,5 @@ - + diff --git a/static/images/themes/shiroko/pattern.svg b/static/images/themes/shiroko/pattern.svg deleted file mode 100644 index 8c51904..0000000 --- a/static/images/themes/shiroko/pattern.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/static/images/themes/shiroko/scene-desktop.webp b/static/images/themes/shiroko/scene-desktop.webp new file mode 100644 index 0000000..0540486 Binary files /dev/null and b/static/images/themes/shiroko/scene-desktop.webp differ diff --git a/static/images/themes/shiroko/scene-mobile.webp b/static/images/themes/shiroko/scene-mobile.webp new file mode 100644 index 0000000..42fe18b Binary files /dev/null and b/static/images/themes/shiroko/scene-mobile.webp differ