源码解读——Matrix-Code-Rain

最近自己的博客刚刚建起来,想好好经营一下。内容比较少,另外希望能产出一些高质量的文章,所以不想将CSDN博客上的文章迁移过来。那么就得自己发点干货了。废话不多说,转入正题。

数个月前在github上阅读过一个小项目的源码——Matrix-code-rain。其效果是黑客帝国中代码在屏幕上从上至下滑落。DEMO见此链接。如下截图所示即代码雨效果图: 代码雨

首先观察效果图,大致分为三个部分: 左上角的帧频监测模块、左下角的快照工具栏代码雨主体。

项目的index.html文件中的body部分包含了两个主要的元素:

  • id="info"的div元素
  • id="canvas"的画布

前者提供了快照功能,以旋转方式显示/隐藏的特效(该效果是以CSS3实现的);后者实现了帧频检测功能以及主效果代码雨

下面先把JS主干代码贴出,并做简要解读。

主干代码

这里仅标出代码主干,具体细节请查看源码。代码的主干很简单,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.body.appendChild( stats.domElement );
var M = {
...
};
function eventListenerz(){
...
}
window.onload = function(){
M.init();
eventListenerz();
};

先简单分析一下主干代码,然后再分析具体细节:
开头五行代码,引用了一个外部JS文件(stats.min.js)定义的一个构造函数Stats(),然后初始化一个对象,这个对象的功能就是帧频检测,最后把它放到左上角。
接着初始化一个对象M,这个对象内部定义了很多属性和方法:

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
var M = {
// 属性
// 关于每一栏canvas的属性设置
setting: {
COL_WIDTH: 15, // 每一栏的宽度
COL_HEIGHT: 25, // 每一栏的高度
VELOCITY_PARAMS: {
min: 4, // 代码雨的最小速度
max: 8 // 代码雨的最大速度
},
CODE_LENGTH_PARAMS: {
min: 20,// 代码雨的最小长度
max: 40 // 代码雨的最大长度
}
},

animation: null,

c: null,
ctx: null,

lineC: null,
ctx2: null,

video: null,

WIDTH: window.innerWidth,
HEIGHT: window.innerHeight,

COLUMNS: null, // canvas列数
canvii: [],
font: '30px matrix-code',
letters: ['a', 'b', 'c', ...],

codes: [],

createCodeLoop: null,
codesCounter: 0,

// 方法
init(),
loop(),
draw(),
createCode(),
createCanvii(),
createLines(),
assignColumn(),
randomFromInterval(),
snapshot()
};

然后定义一个添加事件监听的函数eventListenerz

当页面加载后执行如下代码:

1
2
3
4
window.onload = {
M.init(); // 初始化M
eventListenerz(); //添加事件监听
};
M.init()做了以下这些事:

  1. 将canvas元素赋值给M.c;
  2. 获取画布上的绘图环境,并赋值给M.ctx(后面称之为画布);
  3. 获取页面的高度、宽度并设置画布的高度和宽度,让画布充满整个页面;
  4. 设置画布背景色为黑色;
  5. 设置画布的字体为30px matrix-code
  6. 创造屏幕质感,画一条条的横线。这里动态创建了一个canvas元素,并设置画布的宽高与页面一致,通过调用M.createLines()方法来绘制满屏的横线,其实为了效果更好每条横线下面紧挨一条颜色更淡的横线,达到色差缓冲的效果;
  7. 根据网页的宽度、预设的canvas宽度,计算网页横向能放多少个canvas;
  8. 针对每一栏canvas初始化一个codes数组,数组的0索引的值是一个对象:
1
2
3
4
5
{
'open': true,
'position': {'x', 0, 'y': 0},
'strength': 0
}
  1. 调用M.loop()方法,该方法内部调用了requestAnimationFrame,并把其任务ID赋值给M.animation。然后是loop方法的主体,调用M.draw()——想必就是绘制代码雨的效果。此外,同时更新帧频检测器的数据状态,达到循环动画的效果。
  2. 调用M.createCode()方法,该方法什么作用?请继续往下阅读。

这时候,代码的大体流程已经知道了,我们只要了解M.draw()是如何绘制代码雨,以及M.createCode()是如何初始化的即可。

代码雨

先来看我绘制的一张图,改图简要的介绍了代码雨的组成,在看具体分析之前大家可以先自己想想其实现方式。

代码雨原理图

要了解代码雨原理,首先了解M.draw()是如何工作的。

M.draw()

M.draw()做了以下工作:

  1. 清理画布,避免之前绘制的图像遗留在画布上产生重影;
  2. 设置如何将新图像绘制到已有图像之上,默认为source-over
  3. 对每一canvas进行处理。
    1). 当其codes[0]包含canvas属性时,获取其速度值、canvas的高度,x、y坐标,canvas元素,canvas画布。然后根据其位置将这个canvas添加到主canvas上。
    a. 当y坐标小于网页高度时(即canvas的y坐标还在网页范围内),更新y坐标(y减去速度值); b. 否则,将y坐标设为0,这就达到了同一列不停的循环的效果。

看到第3步,对比代码主干一节中第8步,我们发现:M.draw()阶段时每一栏的canvas的codes数组根本没有canvas属性。这种情况下,第3步中的处理条件根本无法达到。所以肯定缺少初始化的一步,这肯定包含在M.createCode()中,其实看方法名也能看出来。

M.createCode()

M.createCode()做了以下工作:

  1. 判断M.codesCounter是否大于canvas列数。如果是,清除M.createCodeLoop的定时任务,并返回。否则,继续往下执行。推断一下:毕竟网页横向被分解成了很多个canvas,有多少个canvas,M.createCode()就会被调用多少次吧。
  2. 给局部变量randomInterval赋值为M.randomFromInterval(0,100)的执行结果。直接跳到这段代码看看,哦,生成一个0到100之间的随机数。
  3. 给局部变量colum赋值为M.assignColumn()的执行结果。直接跳到这段代码看看:随机获取一个canvas的索引,如果该canvas的codes[0]的open属性为true,则置为false,并返回canvas的索引;否则直接返回false。仔细想想,这段代码把open置为false后,并没有还原成true。
  4. 根据column索引值,对对应一列的canvas进行处理。
    1. 随机获取一个代码雨长度值,并赋值给codeLength;
    2. 随机获取一个代码雨速度值,并赋值给codeVelocity;
    3. 获取代码雨字符表的长度,并赋值给lettersLength;
    4. 设置该canvas的codes[0].position属性,起始的x坐标与列索引有关,y坐标都为0;
    5. 设置该canvas的codes[0].velocity属性,为随机获取的速度值;
    6. 设置该canvas的codes[0].strength属性,为其速度/速度上限值,这个到底什么作用呢?先放着继续往下看;
    7. 根据代码雨长度值,获取相应数量的字符,这是通过在字符表中随机获取的,并将字符依次赋值给该canvas的codes[1], codes[2]...
    8. 调用M.createCanvii(column)。这里把canvas的列索引值传递进去了,想必就是进行绘制操作了。
    9. M.codesCount++,这一步验证了第1步的猜想。
  5. 根据局部变量randomInterval来设置另一列canvas的初始化。

M.createCanvii()

上面一节第8步对M.createCanvii(col)的作用进行了猜想,下面我们来看看是不是符合我们的猜想。 M.createCanvii(col)做了以下工作:

  1. 获取该列canvas要显示的字符数,赋值给codeLen;
  2. 获取该列canvas的高度,通过字符数*每个字符的高度即可得到;
  3. 获取该列canvas的速度,通过codes[0].velocity即可得到;
  4. 获取该列canvas的strength,此时我们还是不知道这是个什么参数;
  5. 创建一个canvas元素,并获取其画布环境,并设置其宽度和高度;
  6. 根据codeLen,绘制所有字符,这里并不是单纯的绘制。前5个和最后4个不太一样,哪里不一样呢?之前不明白其含义的strength出现了,原来是为了让两端的颜色变淡一些。
  7. 绘制完毕后,将该列canvas的codes[0].canvas的值赋为这里绘创建的canvas元素。哦,这时候,M.draw()一节的第3步的条件就成立了,就可以把这个创建的canvas添加到网页的主canvas了。

现在一切就明了了,接下来有兴趣的话可以看看快照工具栏的效果实现。

快照工具栏

先来看看CSS3提供的几个动画方法和特性:

transform: none | transform-functions

1
2
3
4
5
6
matrix():		/*定义转换*/  
translate(): /*原点坐标偏移*/
scale(): /*缩放*/
rotate(): /*沿轴旋转*/
skew(): /*沿轴倾斜*/
perspective(): /*定义透视*/

transition: property duration timing-function delay

1
2
3
transition-property: none|all|property;  
transition-duration: time(s/ms);
transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n)
transform属性的是transform-function,上述只列出了6类方法,每一类方法对应还有针对2D, 3D的方法。

很明显,快照工具栏的旋转显示/隐藏方式是以tramsform分别对显示时定义一个状态隐藏时定义一个状态,然后通过CSS3的transition属性来进行状态切换设置。

果然代码中也是以这种方式实现的:

显示状态

1
2
3
transform-origin: bottom center;
transform: rotate(0deg);
transition: transform 1s ease-in-out;

隐藏状态

1
transform: rotate(180deg);

状态转移方式

1
transition: transform 1s ease-in-out;  

两个状态、转移方式都定义好了,那么就可以通过事件了切换这两者的状态了。用toggle的方法来添加/删除类来达到状态切换的效果。果然,JS代码中有一个函数eventListenerz()就包含了状态切换的处理。
此外,快照工具栏还有一个主要功能: 快照。还是在上面那个函数里面,包含了这两行代码:

1
2
var snapshotBtn = document.getElementById('snapshot');
snapshotBtn.addEventListener('click', M.snapshot, false);
第一行代码获取了快照按钮的buttn元素;第二行代码则是对该按钮添加了一个click事件监听以及相应的回调函数。
M.snapshot是其内部定义的一个方法。其源码如下:
1
2
3
4
snapshot: function(){
M.createLines(M.ctx);
window.open(M.c.toDataURL());
}
M.c为页面的主canvas,对其调用toDataURL()方法,该方法将canvas进行转化成一个特定格式的图片(默认PNG),并返回一个data URI。最后新开一个窗口显示该图片。

(完)