theme(shiroko): 按二次元官网构图重做——整页场景 + 悬浮磨砂 UI

参考蔚蓝档案 JP 首页(bluearchive.jp)的做法:整个视口是一张场景插画,UI 浮在场景之上,
而不是"白底 + 留白里贴几个人物"。之前几版反复调摆位都显得怪,根源就是缺了这层场景——
人物没有环境,怎么摆都是贴纸。

- 新增教室场景两张(1920×1080 / 720×1003,共 28KB):生成后裁掉右下角水印、
  混白 38%、模糊 1.4px,保证白卡片仍有对比度;
- 卡片与顶栏改半透明白 + backdrop-filter 磨砂(深色关闭,背后没场景时半透明只会变浑);
- 人物站在场景左侧地面(340px,离底边 36px)+ 椭圆接触阴影;
- 浅色不再叠平铺纹样(压在实景上会变噪点),pattern.svg 删除,深色保留 pattern-dark;
- 顺带修 ruleBody 不剥 CSS 注释的缺陷:令牌前写注释会让 tokenValue 取到粘连键,
  色块预览静默失效(新增 TestRuleBodyIgnoresComments 守住);
- StaticVersion → 20261019q
This commit is contained in:
tamakyi
2026-10-19 23:01:55 +08:00
parent f876aa6f4f
commit 33e31cf4dc
9 changed files with 110 additions and 86 deletions
+41 -26
View File
@@ -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。
+1 -1
View File
@@ -10,7 +10,7 @@ var (
BuildCommit = "dev"
// StaticVersion 静态资源缓存版本号:base.css 与主题 css 共用,改样式只改这一处。
StaticVersion = "20261019p"
StaticVersion = "20261019q"
)
// 文本审核通道取值([security] text_censor_source)。定义在 conf 侧,
+5 -1
View File
@@ -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.<slug> 提供,
@@ -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 {
+13
View File
@@ -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"))
}
+49 -41
View File
@@ -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);
}
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
<g fill="none" stroke="#8FB4DE" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.055">
<g fill="none" stroke="#8FB4DE" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.045">
<g transform="translate(48 52)">
<path d="M0 -18V18M-15.6 -9L15.6 9M-15.6 9L15.6 -9"/>
<path d="M0 -18l-4 4M0 -18l4 4M0 18l-4-4M0 18l4-4"/>

Before

Width:  |  Height:  |  Size: 671 B

After

Width:  |  Height:  |  Size: 671 B

-16
View File
@@ -1,16 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
<!-- 元素按 3 行均布:集中在某一侧时,铺开后会出现横向条纹带 -->
<g fill="none" stroke="#6F90BC" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.085">
<g transform="translate(48 52)">
<path d="M0 -18V18M-15.6 -9L15.6 9M-15.6 9L15.6 -9"/>
<path d="M0 -18l-4 4M0 -18l4 4M0 18l-4-4M0 18l4-4"/>
</g>
<circle cx="152" cy="64" r="1.6"/>
<ellipse cx="198" cy="132" rx="18" ry="6.4"/>
<g transform="translate(88 152) scale(0.62)">
<path d="M0 -18V18M-15.6 -9L15.6 9M-15.6 9L15.6 -9"/>
</g>
<circle cx="42" cy="212" r="1.4"/>
<circle cx="164" cy="204" r="1.7"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB