前言
上一篇介绍了 DoraTiger 主题的架构设计,本篇聚焦视觉系统:暗色主题的颜色体系、Canvas 动画的实现原理、JS 驱动的响应式布局,以及可复用的 Stylus 动画 Mixin。这些是主题"看起来怎么样"的核心。
一、暗色主题设计语言
1.1 为什么是暗色主题
DoraTiger 的暗色主题继承自 Fan 主题 — Fan 本身就采用了星空背景,整体偏暗色风格。从 Fan 重构时延续了这个基调,一方面视觉上已经习惯了,另一方面星空背景确实很适合 Canvas 动画的发挥。
中间考虑过加一个亮色主题,但一直没想好亮色对应的背景该用什么。纯白太单调,渐变又容易和内容区域冲突,就搁置了。所以目前只有暗色方案。
1.2 颜色体系
所有颜色通过 theme-config() 从配置文件读取,支持运行时覆盖:
stylus123456// source/css/_variable/variable.styl $color-theme = theme-config('style.color.theme', 'rgba(230, 119, 0, 1)'); // 强调色(橙) $color-sub-theme = theme-config('style.color.sub_theme', 'rgba(7, 144, 232, 1)'); // 辅助色(蓝) $color-text = theme-config('style.color.text', 'rgba(255, 255, 255, 1)'); // 主文字(白) $color-background = theme-config('style.color.background', 'radial-gradient(100% 100% at 70% 120%, rgba(33, 39, 80, 1) 10%, #020409 100%)');
配色方案同样继承自 Fan 主题 — 橙色强调 + 蓝色辅助的组合在暗色背景下辨识度很高,一直沿用至今。亮色方案因为没找到合适的背景搭配,所以配色也没有重新调整。
设计原则:
- 深蓝黑径向渐变背景,不是纯黑 — 径向渐变让视觉有纵深感
- 白色文字 +
rgba透明度控制层级 — 主文字1.0,次要文字0.6 - 橙色强调用于交互反馈(
hover、激活态),蓝色辅助用于链接
1.3 毛玻璃效果
内容区域使用半透明白色背景,配合边框和阴影实现毛玻璃感:
stylus12$color-content-background = theme-config('style.color.content_background', 'rgba(255, 255, 255, 0.1)');
1.4 随机色板
自动生成 11 个颜色 class,用于社交链接圆点等动态着色场景:
stylus12345$colors = #495057 #f03e3e #ae3ec9 #7048e8 #4263eb #1098ad #0ca678 #37b24d #f59f00 #f76707 #6f42c1; for color, i in $colors { .bg-color{i} { background: color; } }
二、Canvas 动画系统
2.1 星空背景
全屏 <canvas> 元素 #universe,通过 requestAnimationFrame 驱动三种粒子效果:
javascript123456789101112131415// source/js/layout/background.js class Background { constructor() { this.canva = document.getElementById("universe"); this.starDensity = 0.001; // 每像素星星密度 this.createUniverse(); } createUniverse() { // 三种星星: // 1. 巨星:大尺寸,缓慢水平漂移 // 2. 彗星:快速对角线划过,带拖尾 // 3. 普通星:随机闪烁(opacity 变化) } }
画布覆盖在所有内容之下(z-index: -1),不影响交互。星星数量根据屏幕尺寸动态计算(starDensity * width),保证不同设备上都有合适的粒子密度。
移动端的 Canvas 性能表现一般 — 粒子数量虽然会根据屏幕缩小,但在低端设备上仍然有掉帧。目前没有做更激进的降级处理(比如关闭动画),算是一个已知的体验短板。
2.2 Hero 文字渲染
首页顶部的标题和副标题使用 Canvas 渲染,配合 pretext 排版引擎做精确文字布局:
javascript12345678910111213141516171819// source/js/layout/hero.js class Hero { resize() { const rect = this.canvas.parentElement.getBoundingClientRect(); this.width = rect.width; // 根据容器宽度动态计算字体大小 const titleFontSize = Math.min(this.width / 10, 64); // 使用 pretext 做精确排版(支持自动换行) const prepared = prepareWithSegments(this.title, font); const { lines } = layoutWithLines(prepared, maxWidth * dpr, lineHeight * dpr); } draw(progress) { // 每行文字渐入动画(stagger + easeOutExpo) const lineDelay = line.type === 'title' ? i * 0.08 : 0.4 + i * 0.06; ctx.globalAlpha = eased; ctx.translate(0, (1 - eased) * 20); // 滑入效果 } }
pretext 库是前一阵刷技术博客时偶然发现的,看着能做 Canvas 文字排版就随手加进来了,没有做太多调研。实际用下来基本能用,但在响应式布局上还有一些小 bug(比如侧边栏切换时文字居中计算不准确),目前还没完全修好。
2.3 404 页面
Canvas 绘制发光的 “404” 文字 + 浮动粒子,倒计时后自动跳转首页:
javascript12345678910// source/js/layout/page404.js class Page404 { draw() { // 文字描边 + 发光效果 ctx.shadowColor = 'rgba(255, 255, 255, 0.5)'; ctx.shadowBlur = 20; ctx.strokeText('404', ...); // 浮动粒子 } }
三、响应式布局
3.1 为什么用 JS 而非媒体查询
最初考虑过媒体查询方案,但实际对比后选择了 JS 动态计算,原因有两个:
媒体查询的局限:断点是固定的(比如 768px、1024px),但实际布局中每个区域的可用宽度不同。header 的宽度不等于 sidebar 的宽度,用统一的断点无法精确控制每个区域的行为。
JS 的灵活性:通过 getBoundingClientRect() 获取实际可用宽度,可以精确到像素级别控制显示/隐藏。而且 JS 可以通过 hook 机制按需嵌入,媒体查询则需要在 CSS 中写死所有断点规则,维护成本更高。
javascript1234567// source/js/layout/header.js function autoResizeHeaderRight() { const containerWidth = headerWrapper.getBoundingClientRect().width; // 空间不足时依次隐藏:搜索按钮 → 站点标题 → 时钟 if (containerWidth < 600) searchButton.style.display = 'none'; if (containerWidth < 400) titleElement.style.display = 'none'; }
不过移动端的自适应效果一般 — 主要是因为之前开发 Spring Boot + Bootstrap 项目时习惯了容器化排版,对 flex 布局有一种天然的偏好,所以做了自适应。但移动端屏幕尺寸有限,很多在桌面端好看的效果在手机上效果平平,算是一个妥协。
3.2 侧边栏折叠
侧边栏通过 CSS class 切换控制显隐,配合 transition 实现平滑动画:
stylus123456789// CSS #sidebar-container width $sidebar-width transition $transition-delay &.closed width 0 min-width 0 visibility hidden
javascript12345// JS — 切换时通知其他组件 function toggleSidebar() { sidebarContainer.classList.toggle("closed"); window.dispatchEvent(new Event('layoutchange')); }
layoutchange 事件让 Hero 等组件感知布局变化,重新计算尺寸。
3.3 Meta 项自动换行
文章列表中,日期、分类、标签等 meta 项在内容过多时自动换行:
stylus12345.post-item-header-meta display flex flex-direction row flex-wrap wrap // 关键:内容超宽时换行 gap 0.25rem 0 // 行间垂直间距
四、可复用动画 Mixin
三个核心 Mixin 覆盖了大部分交互动画场景:
4.1 下划线展开
stylus1234567hover-underline($left = 0) &:after left $left; width 0; height 0.125rem background $color-theme &:hover &:after left 0; width 100%
下划线从指定位置展开到全宽,用于菜单项、链接等。
4.2 四边框动画
四个子元素 .line-top/right/bottom/left,hover 时从外部展开到完整边框。用于统计数字、友情链接等卡片。
4.3 光扫按钮
::before 伪元素实现光扫效果:
stylus123456button-hover-effect() &::before left -100% background linear-gradient(to left, rgba(255,255,255,0), rgba(255,255,255,0.5), rgba(255,255,255,0)) &:hover::before left 100%; transition left 0.5s ease
CSS 的动画语法确实绕,尤其是 ::before/::after 伪元素配合 transition 的各种组合,踩了不少坑。最终真正熟练掌握的主要是 Stylus 的变量、Mixin 和函数体系,底层 CSS 动画还是在实践中慢慢摸索出来的。
五、设计令牌系统
所有视觉参数通过 theme-config() 从配置文件读取,用户可在 _config.hexo-theme-doratiger.yml 中覆盖:
| 令牌 | 默认值 | 可配置 |
|---|---|---|
$color-theme |
橙色 | ✅ |
$color-background |
深蓝黑径向渐变 | ✅ |
$sidebar-width |
300px | ✅ |
$main-content-max-width |
800px | ✅ |
$transition-delay |
all 0.5s | ✅ |
这个过程并不是一开始就设计好的。最初写样式的时候,颜色值都是直接硬编码在各个 .styl 文件里的 — post.styl 里写一个 rgba(230, 119, 0, 1),sidebar.styl 里又写一个,header.styl 里再来一个。后来想做主题色可配置,才发现要改的地方遍地都是,费了不少功夫把散落在十几个文件中的硬编码颜色逐一替换为 theme-config() 引用。这也是为什么现在所有颜色都集中在 _variable/variable.styl 中统一管理 — 代价是前期的重构成本,收益是后续配置的便利性。
这意味着用户无需修改任何 Stylus 代码,只改配置文件就能调整整个主题的视觉风格。
六、总结
DoraTiger 的视觉系统围绕三个核心:设计令牌驱动(配置即样式)、Canvas 动画(星空 + Hero + 404)、JS 响应式(动态计算而非媒体查询)。配色继承自 Fan 主题,暗色方案稳定可用,亮色方案仍在探索中。移动端适配做了基础自适应但效果有限,pretext 排版引擎的响应式也还有小 bug 待修复。整体来说,视觉系统达到了"够用且可定制"的目标,后续会继续迭代优化。下一篇将深入核心功能的实现细节。

