本文已收录至 Butterfly 主题美化教程

侧边栏是 Butterfly 主题的一大功能亮点,在 Butterfly 3.8.0 版本前是不支持自定义侧边栏的,只能通过魔改实现。这也因此诞生了许多优质的魔改教程,效果也很惊艳,比如 Heo 收录的:侧边栏 | Butterfly 主题美化教程 (zhheo.com)

Butterfly 3.8.0 以后,主题支持自定义侧边栏,这也使我们可以通过不修改主题源码的方式自定义想要的侧边栏。(主题官方文档参考:自定義側邊欄 | Butterfly

授人以鱼不如授人以渔。本篇文章将以这个特性为基础,结合 CodePen 中的项目,以三个例子展示定制侧边栏过程:

  • 小风车:这是对主题官方文档示例的举一反三,提供与 CodePen 项目结合的思路。
    image-20230530224311630
  • Crowd Simulator 人潮涌动模拟器:引入 CSS 文件去掉侧边栏容器的边框,嵌入 iframe 页面
    crowdsimudemo.gif
  • Car 小汽车:上例的举一反三
    cardemo-min.gif

本人前端水平有限,欢迎各位博主互相交流!

小风车

首先,在 Trending - CodePen 找到自己想要的小组件,以小风车为例 Windmill (codepen.io),进入代码编辑界面,里面有 HTML 代码和 CSS 代码。

引入 CSS

点击代码框右上小箭头,编译 css 文件:

image-20230530222130344

复制里面的的代码到新文件 windmill.css。并将该 CSS 文件引入主题中。Butterfly 主题的方法为在主题配置文件的 inject 部分引入 CSS,如:

1
2
3
4
5
inject:
head:
- <link rel="stylesheet" href="your css">
bottom:
- <script src="your js"></script>

删除不必要代码(否则导致博客异常)并稍微修改一下类选择器(以防万一):

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
- *,
- *:before,
- *:after {
- ...
- }
- body {
- ...
- }
- .inspiration {
- ...
- }
- .center {
- ...
- }
- @media screen and (max-height: 500px) {
- ...
- }
+ /* 这是为了让svg居中 */
+ .windmill {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }

+ /* 以下关于svg类选择器加上.windmill */
- svg {
+ svg.windmill {
overflow: visible;
}
- svg .blades {
+ svg .blades.windmill {
...
}
- svg .clouds path:nth-child(1) {
+ svg .clouds.windmill path:nth-child(1) {
...
}
- svg .clouds path:nth-child(2) {
+ svg .clouds.windmill path:nth-child(2) {
...
}
...

widget.yml 放入关键 HTML 代码

只提取代码中第一个 div 元素,并对相关元素增加 windmill 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-<div class="center">
+<div class="windmill">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="615.66 311.71 271.03 207.66" width="271.03" height="207.66">
- <g class="clouds" fill="none" stroke-width="3" stroke="#BED3CF" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="3">
+ <svg class="windmill" xmlns="http://www.w3.org/2000/svg" viewBox="615.66 311.71 271.03 207.66" width="271.03" height="207.66">
+ <g class="clouds windmill" fill="none" stroke-width="3" stroke="#BED3CF" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="3">
...
</g>
...
- <g class="blades" fill="#FFF" stroke-width="3" stroke="#4A5370" stroke-linejoin="round" stroke-miterlimit="3">
+ <g class="blades windmill" fill="#FFF" stroke-width="3" stroke="#4A5370" stroke-linejoin="round" stroke-miterlimit="3">
...
</g>
</svg>
</div>

在 widget.yml 中是这样的:

image-20230530223905933

注意 YAML 文件中的缩进!否则会报这样的错误:

1
can not read a block mapping entry; a multiline key may not be an implicit key

Crowd Simulator 人潮涌动模拟器

逛了许多博主的网站,发现了这个卡片:Crowd Simulator (codepen.io)。想尝试一下是不是可以放到侧边栏,试了一晚上成功了。现分享步骤。

新建 HTML 文件

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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>人潮汹涌 Crowd Simulator</title>
<link rel="stylesheet" href="/css/people.css">
</head>

<body><canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
</body>

<style>
body,
html {
height: 100%;
background-color: #fff
}

body {
margin: 0
}

#canvas {
width: 100%;
height: 100%
}

body::-webkit-scrollbar {
display: none
}

</style>

<script>

const config = {
src:
"https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/open-peeps-sheet.png",
rows: 15,
cols: 7
};

// UTILS

const randomRange = (min, max) => min + Math.random() * (max - min);

const randomIndex = (array) => randomRange(0, array.length) | 0;

const removeFromArray = (array, i) => array.splice(i, 1)[0];

const removeItemFromArray = (array, item) =>
removeFromArray(array, array.indexOf(item));

const removeRandomFromArray = (array) =>
removeFromArray(array, randomIndex(array));

const getRandomFromArray = (array) => array[randomIndex(array) | 0];

// TWEEN FACTORIES

const resetPeep = ({ stage, peep }) => {
const direction = Math.random() > 0.5 ? 1 : -1;
// using an ease function to skew random to lower values to help hide that peeps have no legs
const offsetY = 100 - 250 * gsap.parseEase("power2.in")(Math.random());
const startY = stage.height - peep.height + offsetY;
let startX;
let endX;

if (direction === 1) {
startX = -peep.width;
endX = stage.width;
peep.scaleX = 1;
} else {
startX = stage.width + peep.width;
endX = 0;
peep.scaleX = -1;
}

peep.x = startX;
peep.y = startY;
peep.anchorY = startY;

return {
startX,
startY,
endX
};
};

const normalWalk = ({ peep, props }) => {
const { startX, startY, endX } = props;

const xDuration = 10;
const yDuration = 0.25;

const tl = gsap.timeline();
tl.timeScale(randomRange(0.5, 1.5));
tl.to(
peep,
{
duration: xDuration,
x: endX,
ease: "none"
},
0
);
tl.to(
peep,
{
duration: yDuration,
repeat: xDuration / yDuration,
yoyo: true,
y: startY - 10
},
0
);

return tl;
};

const walks = [normalWalk];

// CLASSES

class Peep {
constructor({ image, rect }) {
this.image = image;
this.setRect(rect);

this.x = 0;
this.y = 0;
this.anchorY = 0;
this.scaleX = 1;
this.walk = null;
}

setRect(rect) {
this.rect = rect;
this.width = rect[2];
this.height = rect[3];

this.drawArgs = [this.image, ...rect, 0, 0, this.width, this.height];
}

render(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.scale(this.scaleX, 1);
ctx.drawImage(...this.drawArgs);
ctx.restore();
}
}

// MAIN

const img_crowd = document.createElement("img");
img_crowd.onload = init;
img_crowd.src = config.src;

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");

const stage = {
width: 0,
height: 0
};

var allPeeps = [];
const availablePeeps = [];
const crowd = [];

function init() {
createPeeps();

// resize also (re)populates the stage
resize();

gsap.ticker.add(render);
window.addEventListener("resize", resize);
}

function createPeeps() {
const { rows, cols } = config;
const { naturalWidth: width, naturalHeight: height } = img_crowd;
const total = rows * cols;
const rectWidth = width / rows;
const rectHeight = height / cols;

for (let i = 0; i < total; i++) {
allPeeps.push(
new Peep({
image: img_crowd,
rect: [
(i % rows) * rectWidth,
((i / rows) | 0) * rectHeight,
rectWidth,
rectHeight
]
})
);
}
}

function resize() {
stage.width = canvas.clientWidth;
stage.height = canvas.clientHeight;
canvas.width = stage.width * devicePixelRatio;
canvas.height = stage.height * devicePixelRatio;

crowd.forEach((peep) => {
peep.walk.kill();
});

crowd.length = 0;
availablePeeps.length = 0;
availablePeeps.push(...allPeeps);

initCrowd();
}

function initCrowd() {
while (availablePeeps.length) {
// setting random tween progress spreads the peeps out
addPeepToCrowd().walk.progress(Math.random());
}
}

function addPeepToCrowd() {
const peep = removeRandomFromArray(availablePeeps);
const walk = getRandomFromArray(walks)({
peep,
props: resetPeep({
peep,
stage
})
}).eventCallback("onComplete", () => {
removePeepFromCrowd(peep);
addPeepToCrowd();
});

peep.walk = walk;

crowd.push(peep);
crowd.sort((a, b) => a.anchorY - b.anchorY);

return peep;
}

function removePeepFromCrowd(peep) {
removeItemFromArray(crowd, peep);
availablePeeps.push(peep);
}

function render() {
canvas.width = canvas.width;
ctx.save();
ctx.scale(devicePixelRatio, devicePixelRatio);

crowd.forEach((peep) => {
peep.render(ctx);
});

ctx.restore();
}

</script>

</html>

这个 HTML 模板很简单:

1
2
3
body::-webkit-scrollbar {
display: none
}
  • js 做了写优化(我调错调试出来的)
    • 引入其他 js:gsap.min.js
    • 改了一个变量名:img->img_crowd 防冲突

将新建的 HTML 放到你博客目录 Sourse 文件夹下的合适位置。这里我单独放到 DO_NOT_render 文件夹下,以便后续设置 Hexo 忽略对该文件夹的渲染。注意 DO_NOT_render 文件夹是 sourse 的子文件夹。

_config.yaml 中设置禁止渲染:

1
2
skip_render:
- DO_NOT_render/people.html

引入 CSS

Butterfly 主题配置文件 inject 以下 CSS:

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
#effect_shade{
/* width:40%!important; */
height: 200px;
background-color: var(--lyx-white-acrylic1)!important;
border-radius: 8px;
/* margin-right: 10px !important; */
font-family: PingFang,微软雅黑,HYTMR,SimSun;
background-size:cover;
color: #00000000;
font-size:40px; /*觉得小可以开大一点*/
position: relative;
}
#effect_shade-banner{
width:100%!important;
height:100%!important;
border-radius:8px;
}
#effect_shade-hover{
position:absolute;
right:0%;
height:100%!important;
color:var(--lyx-0);
border-radius: 8px;
padding-left: 5%;
padding-top: 5%;
}
#effect_shade-hover:hover{
color:#ffffff;
background-color: #669900dd;
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%);
}

/* 这是为了无缝侧边栏 */
.seamless-card{
padding: 0px!important;
}
.seamless-card .item-headline{
padding-bottom: 0px!important;
height: 0px!important;
}

从上面的 CSS 中注意到:

1
2
3
4
5
6
7
8
/* 这是为了无缝侧边栏 */
.seamless-card{
padding: 0px!important;
}
.seamless-card .item-headline{
padding-bottom: 0px!important;
height: 0px!important;
}

这是为了覆盖 Butterfly 主题侧边栏样式,实现对目标侧边栏卡片进行调整,达到无缝的效果。这部分也可以独立出一个 css 样式文件出来。

自定义侧边栏 widget.yml

再次提醒

关于自定义侧边栏(widget.yml)信息详看 Butterfly 主题官方文档 自定義側邊欄 | Butterfly

1
2
3
4
5
6
7
8
9
10
11
12
13
top:
- class_name: seamless-card
id_name:
name:
icon:
order:
html: |
<div id="effect_shade">
<iframe id="effect_shade-banner" src="/DO_NOT_render/people.html" frameborder="no"></iframe>
<a id="effect_shade-hover" style="width:100%;height:auto;" href="javascript:randomPost()">
<span>&nbsp</span><i class="fa fa-paper-plane"></i>
<div>&nbsp随便逛逛→</div></a>
</div>

seamless-card 呼应上文的无缝效果。无边框的关键就在于这

注意到,

1
<a id="random-hover" style="width:100%;height:auto;" href="javascript:randomPost()">

中的 js 函数为之前写的随机跳转函数 [1],你可以执行你想要的函数。

从这个例子中,我们学到,HTML 是实现画面内容,CSS 是实现鼠标悬停动效,widget.yaml 可以配置侧边栏效果。以这种方式我们很快就能做出第二个例子。

Car 小汽车

项目:car (codepen.io)

这个项目里只有 css 代码,那这个页面就简单了。新建 HTML 放入 DO_NOT_render 文件夹。

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>

<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>小车</title>
</head>
<script>
// 这里放项目的CSS
</script>
</html>

widget.yml 几乎不用变:

1
2
3
4
5
6
7
8
9
10
11
12
13
 - class_name: seamless-card
id_name:
name:
icon:
order:
html: |
<div id="effect_shade">
- <iframe id="effect_shade-banner" src="/DO_NOT_render/people.html" frameborder="no"></iframe>
+ <iframe id="effect_shade-banner" src="/DO_NOT_render/car/car.html" frameborder="no"></iframe>
<a id="effect_shade-hover" style="width:100%;height:auto;" href="javascript:randomPost()">
<span>&nbsp</span><i class="fa fa-paper-plane"></i>
<div>&nbsp随便逛逛→</div></a>
</div>

就 OK 了。

其他侧边栏定制文章

Butterfly 侧边栏实现 Obsidian 关系图谱

本文参考


  1. 随机访问一篇文章:随机访问一篇文章的实现 | Leonus ↩︎