最终效果

效果是页面顶部的效果。Matrix Digital Rain,也叫做Matrix雨。

效果在普通模式和黑夜模式下分别呈现出不同的效果。

说明

首先说明,哥们是个后端,所以基本上全靠chatgpt的指导,才能做出这种功能。

效果实现的整体思路是先把原本的图片元素隐藏起来,然后在相同的位置上,也就是在id为page-headerheader元素下面,挂一个canvas元素。然后通过js代码来渲染出效果。

前端元素

首先是页面元素的定义。这里设定id为matrix。同时加入指向js文件的代码。

修改index.pug文件定义页面元素的部分,修改后的修改部分如下。

最后一行是定义功能的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
header#page-header(class=`${headerClassName+isFixedClass}`)

  canvas#matrix

  !=partial('includes/header/nav', {}, {cache: true})

  if is_post()

  include ./post-info.pug

  else if is_home()

    #site-info

      h1#site-title=config.title

      if theme.subtitle.enable

        - var loadSubJs = true

        #site-subtitle

          span#subtitle

      if(theme.social)

        #site_social_icons

          !=partial('includes/header/social', {}, {cache: true})

    #scroll-down

      i.fas.fa-angle-down.scroll-down-effects

  else

    #page-site-info

      h1#site-title=page.title || page.tag || page.category



  script(src='/js/matrix_rain.js')

CSS设定

这里遇到一个不好解决的问题,就是butterfly的默认主题颜色是蓝色。

对于我们正在做的这件事情来说,就意味着我们的Matrix雨在开始渲染之前,整个页面会呈现出主题的蓝色,很难看。

暂时没找到原因,所以也没找到解决办法。

作为替代方案,我把主题色调成了黑色,这样显得相对自然一些。

调整主题颜色,在butterfly的配置文件里就能做,有一个叫做theme_color的配置项,这一项和下面的项都被注释了。

影响主题颜色的配置项是theme_color下的main配置,放开注释,把这项调成黑色。

1
2
theme_color:
main: "#000000"

在butterfly的配置文件中,已经有canvas这个元素的配置文件了,这个配置文件是function.styl。我们在index.pug这个文件里,给canvas元素定义的id是matrix,所以加上以下的配置。

1
2
3
4
5
6
7
#matrix
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: 0

z-index设置成0,高于0的话会覆盖表面的标题,低于0会被覆盖掉。

JS功能

我不希望把功能代码混用,所以新建一个matrix_rain.js文件,存放功能逻辑,文件存放在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
document.addEventListener('DOMContentLoaded', () => {

    const canvas = document.getElementById("matrix");

    const context = canvas.getContext("2d");



    let intervalId;  // 保存 setInterval 的 ID



    // 检查初始模式

    let theme = document.documentElement.getAttribute("data-theme") || "light";

    createRainEffect(theme);



    // 监听模式切换

    const observer = new MutationObserver(() => {

        const newTheme = document.documentElement.getAttribute("data-theme");

        if (newTheme !== theme) {

            theme = newTheme;

            clearInterval(intervalId);  // 清除旧的字符雨动画

            createRainEffect(theme);  // 切换字符雨效果

        }

    });

    observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });



    // 根据模式生成不同的字符雨

    function createRainEffect(theme) {

        canvas.width = window.innerWidth;

        canvas.height = document.querySelector('#page-header').offsetHeight;



        // 清空之前的字符雨效果

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

        let fontSize, drawFunction, frequency;



        if (theme === "light") {

            // 常规模式:中国风字符雨

            fontSize = 32;

            context.font = `${fontSize}px 'KaiTi', 'SimSun', 'serif'`;

            context.fillStyle = "rgba(0, 0, 0, 0.7)";  // 黑色字符

            frequency = 120;



            // 列的数量和每列的初始 y 位置(随机化避免所有字符同时出现)

            const columns = canvas.width / (fontSize * 2);

            const drops = Array(Math.floor(columns)).fill(0).map(() => Math.floor(Math.random() * canvas.height / fontSize));



            const sentences = [

                " 君子疾夫舍曰“欲之”而必为之辞。吾恐季孙之忧,不在颛臾,而在萧墙之内也。",

                " 吾生也有涯,而知也无涯。以有涯随无涯,殆已!",

                " 对酒当歌,人生几何?譬如朝露,去日苦多。慨当以慷,忧思难忘。",

                " 今汉室倾颓,宜奉天子以令不臣,则可以无敌于天下,进而兴复汉室,还于旧都。",

                " 胜兵先胜而后求战,败兵先战而后求胜。",

                " 不晓事,则挟私固谬,秉公亦谬,小人固谬,君子亦谬,乡愿固谬,狂狷亦谬。",

                " 一派青山景色幽,前人田地后人收。后人收得休欢喜,还有收人在后头。"

            ];

            const sentenceIndex = Array(Math.floor(columns)).fill(0).map(() => Math.floor(Math.random() * sentences.length));  // 每列对应的句子索引



            drawFunction = function () {

                context.fillStyle = "rgba(245, 245, 245, 0.1)";

                context.fillRect(0, 0, canvas.width, canvas.height);



                context.fillStyle = "rgba(0, 0, 0, 0.7)";

                context.font = `${fontSize}px 'KaiTi', 'SimSun', 'serif'`;



                drops.forEach((y, index) => {

                    const currentSentence = sentences[sentenceIndex[index]];  // 每列选择不同的句子

                    const letters = currentSentence.split("");  // 将句子分割成字符数组

                    const charIndex = drops[index] % letters.length;  // 确保字符索引按顺序显示



                    const text = letters[charIndex];  // 按顺序展示句子中的字符

                    context.globalAlpha = Math.random() * 0.5 + 0.5;  // 随机透明度

                    context.fillText(text, index * fontSize * 2, y * fontSize);



                    if (y * fontSize > canvas.height && Math.random() > 0.98) {

                        drops[index] = 0;  // 重置当前列

                        sentenceIndex[index] = Math.floor(Math.random() * sentences.length);  // 重新为该列选择一个句子

                    }



                    drops[index]++;

                });

            };



        } else if (theme === "dark") {

            // 夜间模式:黑绿风格字符雨

            const letters = "01".split("");

            fontSize = 16;

            context.font = `${fontSize}px monospace`;

            context.fillStyle = "#0F0";  // 绿色字符

            frequency = 80;



            // 列的数量和每列的初始 y 位置(随机化避免所有字符同时出现)

            const columns = canvas.width / (fontSize * 2);

            const drops = Array(Math.floor(columns)).fill(0).map(() => Math.floor(Math.random() * canvas.height / fontSize));



            drawFunction = function () {

                context.fillStyle = "rgba(0, 0, 0, 0.05)";

                context.fillRect(0, 0, canvas.width, canvas.height);



                context.fillStyle = "#0F0";

                context.font = `${fontSize}px monospace`;



                drops.forEach((y, index) => {

                    // 从字符列表中随机选择一个字符

                    const text = letters[Math.floor(Math.random() * letters.length)];

                    context.globalAlpha = Math.random() * 0.5 + 0.5;

                    context.fillText(text, index * fontSize * 2, y * fontSize);



                    if (y * fontSize > canvas.height && Math.random() > 0.98) {

                        drops[index] = 0;

                    }

                    drops[index]++;

                });

            };

        }



        // 开始字符雨动画,保存 intervalId 以便之后清除

        intervalId = setInterval(drawFunction, frequency);

    }

});

说实话,很感谢AI技术的进步,不然我根本就写不出这样的东西来。

关键部分都有注释说明。

另外,在script/events目录下的cdn.js文件中登记这个js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const internalSrc = {

    // ...上面是butterfly原有的cdn配置

    matrix_rain: {

      name: 'hexo-theme-butterfly',

      file: 'js/search/matrix_rain.js',

      version

    }

  }

这样,重新生成文件,功能完成!