HEXO 开发笔记(5)自建主题:视觉系统
创建于 2026-06-19
更新于 2026-06-19
科技
hexo
主题开发
CSS
Canvas
5360 字 · 约 18 分钟

前言

上一篇介绍了 DoraTiger 主题的架构设计,本篇聚焦视觉系统:暗色主题的颜色体系、Canvas 动画的实现原理、JS 驱动的响应式布局,以及可复用的 Stylus 动画 Mixin。这些是主题"看起来怎么样"的核心。

一、暗色主题设计语言

1.1 为什么是暗色主题

DoraTiger 的暗色主题继承自 Fan 主题 — Fan 本身就采用了星空背景,整体偏暗色风格。从 Fan 重构时延续了这个基调,一方面视觉上已经习惯了,另一方面星空背景确实很适合 Canvas 动画的发挥。

中间考虑过加一个亮色主题,但一直没想好亮色对应的背景该用什么。纯白太单调,渐变又容易和内容区域冲突,就搁置了。所以目前只有暗色方案。

1.2 颜色体系

所有颜色通过 theme-config() 从配置文件读取,支持运行时覆盖:

stylus
1
2
3
4
5
6
// 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 毛玻璃效果

内容区域使用半透明白色背景,配合边框和阴影实现毛玻璃感:

stylus
1
2
$color-content-background = theme-config('style.color.content_background', 'rgba(255, 255, 255, 0.1)');

1.4 随机色板

自动生成 11 个颜色 class,用于社交链接圆点等动态着色场景:

stylus
1
2
3
4
5
$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 驱动三种粒子效果:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 排版引擎做精确文字布局:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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” 文字 + 浮动粒子,倒计时后自动跳转首页:

javascript
1
2
3
4
5
6
7
8
9
10
// 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 动态计算,原因有两个:

媒体查询的局限:断点是固定的(比如 768px1024px),但实际布局中每个区域的可用宽度不同。header 的宽度不等于 sidebar 的宽度,用统一的断点无法精确控制每个区域的行为。

JS 的灵活性:通过 getBoundingClientRect() 获取实际可用宽度,可以精确到像素级别控制显示/隐藏。而且 JS 可以通过 hook 机制按需嵌入,媒体查询则需要在 CSS 中写死所有断点规则,维护成本更高。

javascript
1
2
3
4
5
6
7
// 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 实现平滑动画:

stylus
1
2
3
4
5
6
7
8
9
// CSS #sidebar-container width $sidebar-width transition $transition-delay &.closed width 0 min-width 0 visibility hidden
javascript
1
2
3
4
5
// JS — 切换时通知其他组件 function toggleSidebar() { sidebarContainer.classList.toggle("closed"); window.dispatchEvent(new Event('layoutchange')); }

layoutchange 事件让 Hero 等组件感知布局变化,重新计算尺寸。

3.3 Meta 项自动换行

文章列表中,日期、分类、标签等 meta 项在内容过多时自动换行:

stylus
1
2
3
4
5
.post-item-header-meta display flex flex-direction row flex-wrap wrap // 关键:内容超宽时换行 gap 0.25rem 0 // 行间垂直间距

四、可复用动画 Mixin

三个核心 Mixin 覆盖了大部分交互动画场景:

4.1 下划线展开

stylus
1
2
3
4
5
6
7
hover-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/lefthover 时从外部展开到完整边框。用于统计数字、友情链接等卡片。

4.3 光扫按钮

::before 伪元素实现光扫效果:

stylus
1
2
3
4
5
6
button-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 待修复。整体来说,视觉系统达到了"够用且可定制"的目标,后续会继续迭代优化。下一篇将深入核心功能的实现细节。

参考

本文作者: 有次元袋的 tiger
本文链接: https://www.superheaoz.top/2026/06/50649/
版权声明: 本站点所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 我的个人天地
手机扫码阅读