最近一直忙于CO,深感疲惫,于是打算对博客进行美化。既是为了放松一下心情,也是创造一个更愉悦的写作环境,让自己多写一些东西。
美化清单
- 全屏背景
- 落雪动画
- 毛玻璃主面板
- 签名动画
- 标签变化
- 文章滑入动画
- 运行时间
- 调整局部文字颜色
自定义美化原理和一般流程
原理
<!-- 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代码:
这里我们可以看到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() {
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);
this.y = Math.floor(Math.random() * canvasHeight);
this.size = Math.random() * flakeSize + 2;
this.maxSize = flakeSize;
this.speed = Math.random() * 0.2 + fallSpeed;
this.fallSpeed = fallSpeed;
this.velY = this.speed;
this.velX = 0;
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)");
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,随缘吧。