【Hexo】Fluid主题美化

最近一直忙于CO,深感疲惫,于是打算对博客进行美化。既是为了放松一下心情,也是创造一个更愉悦的写作环境,让自己多写一些东西。

美化清单

  1. 全屏背景
  2. 落雪动画
  3. 毛玻璃主面板
  4. 签名动画
  5. 标签变化
  6. 文章滑入动画
  7. 运行时间
  8. 调整局部文字颜色

自定义美化原理和一般流程

原理

<!-- hexo injector body_begin start -->实现以上的自定义美化借助的是Hexo的注入器功能。通过注入器功能,我们可以在指定位置注入HTML代码,实现元素的增加和JavaScript脚本的引入和执行。同时Fluid支持我们引入自定义JS/CSS文件。这样我们就可以把自定义的JS和CSS文件引入到最终渲染的HTML页面中,实现我们想要的自定义美化效果。
首先来看注入器:
我们需要在博客根目录下的script文件夹新建一个JavaScript文件,例如injector.js(名字随意,只要是js后缀即可)。以我的Js文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { root: siteRoot  =  "/" } =  hexo.config;

hexo.extend.injector.register("body_begin", `<div id="web_bg"></div>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/backgroundize.js"></script>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/snow.js"></script>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/title.js"></script>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/sign.js"></script>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/scrollAnimation.js"></script>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/time-insert.js"></script>`);

hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/time.js"></script>`);

我们再来观察HTML代码:

HTML

这里我们可以看到body_begin的两条注释之间有我们在injector中注入的诸多div块,同时两条注释之间有我们注入的Js脚本块和canvas画布。相信到这里已经可以明白注入器的工作效果。

在写这里时,我发现了一个Hexo的bug,有时间会在另一篇文章中详细写出。我对injector的源码进行了更改,现在我的Pull Request仍在审核,如果通过那么我将很高兴!

一般流程

想要自定义美化效果,我们只需要引入Js文件和CSS文件。
首先在注入器中加入我们希望注入的代码块,建议将Js脚本注入body_end部分,同时对照开发者工具中的HTML代码选择适当的注入位置。注入器中的Js文件都会被自动加入到网页资源中,因此不需要在_config.fluid.yml中的custom_js中重复引入。

注意这里的Js文件存放的位置应该在博客根目录下的source文件夹新建一个js文件夹目录,存放我们所有的Javascript文件。

然后我们引入CSS文件。我们需要在_config.fluid.yml中的custom_css选项中引入我们的自定义css文件,注意这里的根目录都是source文件夹。

1
2
3
4
5
6
7
8
9
custom_css:

- /css/glassBg.css

- /css/sign.css

- /css/part-text.css

- /css/scrollAnimation.css

注意这里的css文件存放的位置应该在博客根目录下的source文件夹新建一个css文件夹目录,存放我们所有的css文件。

这样我们就可以方便地添加自定义的美化效果。

具体实现

全屏背景

思路是在body部分添加一个div块使背景图片填满整个屏幕并且固定其位置,呈现出的效果即实现了背景全屏。同时删除原只在banner部分存在的背景图,并将banner部分的蒙版改为透明,使其不影响全屏效果的呈现。

backgroundize.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document

.querySelector('#web_bg')

.setAttribute('style', `background-image: ${document.querySelector('.banner').style.background.split(' ')[0]};position: fixed;width: 100%;height: 100%;z-index: -1;background-size: cover;`);

document

.querySelector("#banner")

.setAttribute('style', 'background-image: url()')

document

.querySelector("#banner .mask")

.setAttribute('style', 'background-color:rgba(0,0,0,0)')

落雪动画

在整个body上创建画布并创建雪花运动对象,模拟其运动即可。原文链接:分享两种圣诞节雪花特效JS代码(网站下雪效果)

snow.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/* 控制下雪 */

function snowFall(snow) {

/* 可配置属性 */

snow = snow || {};

this.maxFlake = snow.maxFlake || 200; /* 最多片数 */

this.flakeSize = snow.flakeSize || 10; /* 雪花形状 */

this.fallSpeed = snow.fallSpeed || 1; /* 坠落速度 */

}

/* 兼容写法 */

requestAnimationFrame = window.requestAnimationFrame ||

window.mozRequestAnimationFrame ||

window.webkitRequestAnimationFrame ||

window.msRequestAnimationFrame ||

window.oRequestAnimationFrame ||

function(callback) { setTimeout(callback, 1000 / 60); };

cancelAnimationFrame = window.cancelAnimationFrame ||

window.mozCancelAnimationFrame ||

window.webkitCancelAnimationFrame ||

window.msCancelAnimationFrame ||

window.oCancelAnimationFrame;

/* 开始下雪 */

snowFall.prototype.start = function(){

/* 创建画布 */

snowCanvas.apply(this);

/* 创建雪花形状 */

createFlakes.apply(this);

/* 画雪 */

drawSnow.apply(this)

}

/* 创建画布 */

function snowCanvas() {

/* 添加Dom结点 */

var snowcanvas = document.createElement("canvas");

snowcanvas.id = "snowfall";

snowcanvas.width = window.innerWidth;

snowcanvas.height = window.innerHeight;

snowcanvas.setAttribute("style", "position: fixed; top: 0; left: 0; z-index: 1; pointer-events: none;");

document.getElementsByTagName("body")[0].appendChild(snowcanvas);

this.canvas = snowcanvas;

this.ctx = snowcanvas.getContext("2d");

/* 窗口大小改变的处理 */

window.onresize = function() {

snowcanvas.width = window.innerWidth;

snowcanvas.height = window.innerHeight;

}

}

/* 雪运动对象 */

function flakeMove(canvasWidth, canvasHeight, flakeSize, fallSpeed) {

this.x = Math.floor(Math.random() * canvasWidth); /* x坐标 */

this.y = Math.floor(Math.random() * canvasHeight); /* y坐标 */

this.size = Math.random() * flakeSize + 2; /* 形状 */

this.maxSize = flakeSize; /* 最大形状 */

this.speed = Math.random() * 0.2 + fallSpeed; /* 坠落速度 */

this.fallSpeed = fallSpeed; /* 坠落速度 */

this.velY = this.speed; /* Y方向速度 */

this.velX = 0; /* X方向速度 */

this.stepSize = Math.random() / 30; /* 步长 */

this.step = Math.random()*Math.PI*2; /* 步数 */

}

flakeMove.prototype.update = function() {

var x = this.x,

y = this.y;

/* 左右摆动(余弦) */

this.velX *= 0.98;

if (this.velY <= this.speed) {

this.velY = this.speed

}

this.velX += Math.cos(this.step += .05) * this.stepSize;

this.y += this.velY;

this.x += this.velX;

/* 飞出边界的处理 */

if (this.x >= canvas.width || this.x <= 0 || this.y >= canvas.height || this.y <= 0) {

this.reset(canvas.width, canvas.height)

}

};

/* 飞出边界-放置最顶端继续坠落 */

flakeMove.prototype.reset = function(width, height) {

this.x = Math.floor(Math.random() * width);

this.y = 0;

this.size = Math.random() * this.maxSize + 2;

this.speed = Math.random() * 1 + this.fallSpeed;

this.velY = this.speed;

this.velX = 0;

};

// 渲染雪花-随机形状(此处可修改雪花颜色!!!)

flakeMove.prototype.render = function(ctx) {

var snowFlake = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);

snowFlake.addColorStop(0, "rgba(255, 255, 255, 0.9)"); /* 此处是雪花颜色,默认是白色 */

snowFlake.addColorStop(.5, "rgba(255, 255, 255, 0.5)"); /* 若要改为其他颜色,请自行查 */

snowFlake.addColorStop(1, "rgba(255, 255, 255, 0)"); /* 找16进制的RGB 颜色代码。 */

ctx.save();

ctx.fillStyle = snowFlake;

ctx.beginPath();

ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);

ctx.fill();

ctx.restore();

};

/* 创建雪花-定义形状 */

function createFlakes() {

var maxFlake = this.maxFlake,

flakes = this.flakes = [],

canvas = this.canvas;

for (var i = 0; i < maxFlake; i++) {

flakes.push(new flakeMove(canvas.width, canvas.height, this.flakeSize, this.fallSpeed))

}

}

/* 画雪 */

function drawSnow() {

var maxFlake = this.maxFlake,

flakes = this.flakes;

ctx = this.ctx, canvas = this.canvas, that = this;

/* 清空雪花 */

ctx.clearRect(0, 0, canvas.width, canvas.height);

for (var e = 0; e < maxFlake; e++) {

flakes[e].update();

flakes[e].render(ctx);

}

/* 一帧一帧的画 */

this.loop = requestAnimationFrame(function() {

drawSnow.apply(that);

});

}

/* 调用及控制方法 */

var snow = new snowFall({maxFlake:60});

snow.start();

毛玻璃主面板

首先在_config.fluid.yml中调整board_color:

1
2
3
board_color: "#ffffffad"

board_color_dark: "#000000ad"

然后添加css文件:

glassBg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#board {
-webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}

#toc {
padding: 10px;
top: 4rem;
background-color: var(--board-bg-color);
border-radius: 10px;
-webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}

这样便可以为主面板board部分和侧边栏toc部分都添加了模糊样式,实现了毛玻璃效果。

注意:修改某些yml、css、js文件后更新部署页面内容后在浏览器上查看可能会没有变化,这是因为浏览器会将这些静态资源缓存,以提高访问效率。为了查看变化,可以在开发者工具中的网络菜单下选择“禁用缓存”然后刷新,也可以手动清除浏览器缓存。

签名动画

原理可以在这里学习:SVG的描边动画。SVG可以在Google Font获取。

对应的sign.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const  navbarBrand  =  document.querySelector('.container a');

navbarBrand.innerHTML = `

<svg class="svg" width="516.211" height="104.591" viewBox="0 0 516.211 104.591" xmlns="http://www.w3.org/2000/svg">

<g class="g" id="svgGroup" stroke-linecap="round"
fill-rule="evenodd" font-size="9pt" stroke="#000"
stroke-width="0.25mm" fill="none" style="stroke:#000;stroke-width:0.25mm;fill:none">

<path d="省略的内容" vector-effect="non-scaling-stroke"/>

</g>

</svg>

`;

const paths = document.querySelector('.container .navbar-brand .svg .g path')

const len = paths.getTotalLength()

paths.style.setProperty('--l', len)

对应的sign.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.svg {
width: 250pt;
height: 50pt;
}

.svg path {
stroke: white;
stroke-width: 1pt;
stroke-linecap: round;
stroke-dasharray: var(--l);
stroke-dashoffset: var(--l);
fill: none;
fill-rule: nonzero;
animation: stroke 25s forwards;
-webkit-animation: stroke 25s forwards;
}

@keyframes stroke {
to {
stroke-dashoffset: 0;
}
}

实现过程是让一条虚线的长度等于path的长度,起始偏移量也等于path的长度,这样起始状态是看不到任何线条的,然后通过css绘制关键帧,结束时偏移量为0,这样实现了结束时线条完全显现的效果。

标签变化

思路很简单,利用JavaScript监视页面状态是否变化即可。
title.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var  OriginTitle  =  document.title;

var titleTime;

document.addEventListener('visibilitychange', function () {

if (document.hidden) {

document.title = '(ง •̀_•́)ง‼这里是mRNA的Blog~';

clearTimeout(titleTime);

}

else {

document.title = '( つ•̀ω•́)つ欢迎回来!';

titleTime = setTimeout(function () {

document.title = OriginTitle;

}, 2000);

}

});

文章滑入动画

借鉴了大佬的文章和代码,感谢大佬!原文链接
scrollAnimation.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const  cards  =  document.querySelectorAll('.index-card')

if (cards.length) {

document.querySelector('.row').setAttribute('style', 'overflow: hidden;')

const coefficient = document.documentElement.clientWidth > 768 ? .5 : .3

const origin = document.documentElement.clientHeight - cards[0].getBoundingClientRect().height * coefficient

function throttle(fn, wait) {

let timer = null;

return function () {

const context = this;

const args = arguments;

if (!timer) {

timer = setTimeout(function () {

fn.apply(context, args);

timer = null;

}, wait)

}

}

}

function handle() {

cards.forEach(card => {

card.setAttribute('style', `--state: ${(card.getBoundingClientRect().top - origin) < 0 ? 1 : 0};`)

})

console.log(1)

}

document.addEventListener("scroll", throttle(handle, 100));

}

scrollAnimation.css:

1
2
3
4
5
6
7
8
9
10
.index-card {
transition: all 0.5s;
transform: scale(calc(1.5 - 0.5 * var(--state)));
opacity: var(--state);
margin-bottom: 2rem;
}

.index-img img {
margin: 20px 0;
}

运行时间

首先在适当的位置插入div块和脚本,我选择在底部的统计信息和备案信息间插入。
运行时间
time-insert.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

var newDiv = document.createElement("div");

newDiv.innerHTML = `

<span id="time"></span>

<script src="/js/time.js"></script>

`;

document.addEventListener("DOMContentLoaded", function() {

var div1 = document.getElementsByClassName("statistics")[0];

var div2 = document.getElementsByClassName("beian")[0];

/*调试检查*/
console.log(div1);
console.log(div2);

if (div1 && div2) {

div1.parentNode.insertBefore(newDiv, div2);

}

});

这里为了保证能正确找到div1和div2,选择在DOM内容加载完成后进行插入操作。

计算运行时间的脚本time.js内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var  now  =  new  Date();

function createtime() {

var grt= new Date("07/23/2024 16:37:00");

now.setTime(now.getTime()+250);

days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days);

hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours);

if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);

mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;}

seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);

snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;}

document.getElementById("time").innerHTML = "本站已安全运行 "+ dnum+" 天 "+ hnum + " 小时 " + mnum + " 分 " + snum + " 秒";

}

setInterval("createtime()",250);

调整局部文字颜色

如果在_config.fluid.yml中直接调整,那么改变的是所有文字的颜色,为了仅调整页面底部footer区域内文字的颜色,可以利用css中的!important
part-text.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
footer {
color: #c58989!important;
}

footer span {
color: #c58989!important;
}

.beian {
color: #c58989!important;
}

.beian span {
color: #c58989!important;
}

.beian a {
color: #c58989!important;
}

这样就完成了局部文字颜色的调整,如果希望调整其他区域,可以利用开发者工具找到对应位置的代码块,在css里进行修改即可。

小结

根据我的个人喜好完成了以上美化,看着装修好后的博客,写作的想法强烈了许多。小提一句,通过我的域名mrna16.top来访问是通过http访问的,这样会导致浏览器警告不安全,本想趁这个机会配置一下,但是好像需要对服务器nginx很麻烦地配置一通,于是作罢,毕竟还有mrna16.github.io可以通过https访问,至于何时将top网址的http访问转接到https,随缘吧。


【Hexo】Fluid主题美化
http://example.com/2024/11/14/【Hexo】Fluid主题美化/
作者
mRNA
发布于
2024年11月14日
许可协议