原生Javascript实现漂亮的粒子效果

首先,效果如上所示 ↑ 经测试在Chrome52上能一切正常,全屏显示粒子效果的代码见 Github

动机

现在Javascript简直有一统天下的感觉,除了像C、C++、Go等纯服务器端的语言外,似乎都感受到了Javascript的压力了,不精通Javascript都快不好意思出门打招呼了。十一假期正好有空,学点Javascript防老吧,正好看见这个漂亮的粒子效果,就决定造个轮子。

目标分解

造轮子不是件多么高大上的事,但也得一步一步来。先看看要实现这个粒子效果的目标都有哪些子任务要实现,简单的进行任务分解如下:

  • 粒子能正确显示
  • 粒子能正确移动
  • 粒子之间的连接线能正确显示
  • 鼠标悬停点与其它粒子之间的连接线能正确显示

手把手实现

首先,我们得有一个画布,HTML5的Canvas就是最佳的画布。

1
<canvas id="myCanvas" style="background-color:#00BFFF;border: 0px;"></canvas>

显示粒子

要正确的显示粒子,需要知道每一个粒子的具体坐标,在开始的时候可以随机生成所有粒子的坐标。

1
2
3
4
5
6
7
8
9
var points = new Array(args.point_num);

// 随机生成粒子的坐标
for (let i = 0; i < points.length; i++) {
points[i] = {
x: Math.floor(Math.random()*c.width),
y: Math.floor(Math.random()*c.height),
};
}

不过在窗口resize时,需要正确处理粒子坐标按比例重新定位,否则当窗口大小变化时不能正确显示。

1
2
3
4
5
6
7
8
9
10
11
12
// 处理窗口缩放
function resizeCanvas() {
for (let i = 0; i < points.length; i++) {
var p = points[i];
p.x = (p.x / c.width) * document.body.clientWidth;
p.y = (p.y / c.height) * document.body.clientHeight;
}
c.width = document.body.clientWidth;
c.height = document.body.clientHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas, false);

粒子的移动

粒子的移动方向用point.move的属性控制,这个move的范围从0-2ℼ,表示当前运动方向的角度所对应的弧度。

1
2
3
4
5
6
7
8
// 随机生成粒子的坐标
for (let i = 0; i < points.length; i++) {
points[i] = {
x: Math.floor(Math.random()*c.width),
y: Math.floor(Math.random()*c.height),
move: Math.random()*2*Math.PI // 添加了 move 属性来控制粒子的运动方向
};
}

粒子在每次刷新时,根据运动的方向角进行坐标的重新计算。并在触碰到边界时,重新计算它的方向角。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 粒子移动
function pointsMove(ps) {
for (let i = 0; i < ps.length; i++){
var p = ps[i];

p.x += args.speed * Math.sin(p.move);
p.y += args.speed * Math.cos(p.move);

if (p.x < 0 || p.x > c.width) {
p.move = 2*Math.PI - p.move;
} else if (p.y > c.height || p.y < 0) {
p.move = Math.PI - p.move;
}
}
}

粒子之间的连接

粒子之间的连接线,当它们之间的距离小于一定值时才画线,并根据距离的大小决定线条的透明度。

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
// 粒子间连接线
function lineOfTwoPoints(p1, p2, cxt, d_connect) {
var d = Math.sqrt(Math.pow(p1.x-p2.x, 2) + Math.pow(p1.y-p2.y, 2));
cxt.globalAlpha = (d_connect - d) / d_connect;

if (d < d_connect) {
cxt.moveTo(p1.x, p1.y);
cxt.lineTo(p2.x, p2.y);
cxt.stroke();
}
}

// 先排序,减少计算量
points.sort(function(a, b) {return a.x - b.x;});

// 画出粒子之间的连接线
for (let i = 0; i < points.length; i++){
var p1 = points[i];

for (let j = i + 1; j < points.length; j++){
var p2 = points[j];
lineOfTwoPoints(p1, p2, cxt, d_connect);

if (p1.x + d_connect < p2.x) {
break; // 减少计算量
}
}
}

鼠标悬停点与粒子的连接

首先要正确捕获鼠标事件,获取鼠标相对于Canvas的相对坐标和是否在Canvas的上方。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 鼠标事件
function canvasMouseMove(e) {
var bbox =c.getBoundingClientRect();
var pos = { x: e.clientX - bbox.left *(c.width / bbox.width),
y: e.clientY - bbox.top * (c.height / bbox.height)
};
mouse_point = {x:pos.x, y:pos.y};
}
function canvasMouseOut() {
mouse_point = {x:-1, y:-1};
}
c.addEventListener('mousemove', canvasMouseMove,false);
c.addEventListener('mouseout', canvasMouseOut, false);

因为鼠标点只有一个,直接与其它所有的粒子进行连接线计算即可。

1
2
3
4
5
6
// 画出粒子与鼠标当前位置的连接线
if (mouse_point.x >= 0) {
for (let i = 0; i < points.length; i++){
lineOfTwoPoints(mouse_point, points[i], cxt, d_connect);
}
}

总结

最终完整的代码如下

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
<canvas id="myCanvas" style="background-color:#00BFFF;border: 0px;"></canvas>

<script type="text/javascript">
// Author: chuanqi.tan # gmail.com
// http://tanchuanqi.com

// 初始化
var args = {
point_num: 66,
speed: 0.5,
connect_dist: 0.1,
color: "#FFFFF0"
};
var c = document.getElementById("myCanvas");
var points = new Array(args.point_num);
var mouse_point = {x:-1, y:-1};

// 随机生成粒子的坐标
for (let i = 0; i < points.length; i++) {
points[i] = {
x: Math.floor(Math.random()*c.width),
y: Math.floor(Math.random()*c.height),
move: Math.random()*2*Math.PI
};
}

// 处理窗口缩放
function resizeCanvas() {
for (let i = 0; i < points.length; i++) {
var p = points[i];
p.x = (p.x / c.width) * document.body.clientWidth;
p.y = (p.y / c.height) * document.body.clientHeight;
}
c.width = document.body.clientWidth;
c.height = document.body.clientHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas, false);

// 粒子移动
function pointsMove(ps) {
for (let i = 0; i < ps.length; i++){
var p = ps[i];

p.x += args.speed * Math.sin(p.move);
p.y += args.speed * Math.cos(p.move);

if (p.x < 0 || p.x > c.width) {
p.move = 2*Math.PI - p.move;
} else if (p.y > c.height || p.y < 0) {
p.move = Math.PI - p.move;
}
}
}

// 粒子间连接线
function lineOfTwoPoints(p1, p2, cxt, d_connect) {
var d = Math.sqrt(Math.pow(p1.x-p2.x, 2) + Math.pow(p1.y-p2.y, 2));
cxt.globalAlpha = (d_connect - d) / d_connect;

if (d < d_connect) {
cxt.moveTo(p1.x, p1.y);
cxt.lineTo(p2.x, p2.y);
cxt.stroke();
}
}

// 鼠标事件
function canvasMouseMove(e) {
var bbox =c.getBoundingClientRect();
var pos = { x: e.clientX - bbox.left *(c.width / bbox.width),
y: e.clientY - bbox.top * (c.height / bbox.height)
};
mouse_point = {x:pos.x, y:pos.y};
}
function canvasMouseOut() {
mouse_point = {x:-1, y:-1};
}
c.addEventListener('mousemove', canvasMouseMove,false);
c.addEventListener('mouseout', canvasMouseOut, false);

// 显示逻辑
function show() {
// 先排序,减少计算量
points.sort(function(a, b) {return a.x - b.x;});

// 画出每一个粒子点
var cxt = c.getContext("2d");
cxt.clearRect(0, 0, c.width, c.height);
cxt.fillStyle=args.color;
cxt.strokeStyle=args.color;
var d_connect = Math.max(c.width, c.height) * args.connect_dist;

// 画出粒子之间的连接线
for (let i = 0; i < points.length; i++){
var p1 = points[i];
cxt.globalAlpha = 1;
cxt.beginPath();
cxt.arc(p1.x, p1.y, 3, 0, Math.PI*2, true);
cxt.closePath();
cxt.fill();

for (let j = i + 1; j < points.length; j++){
var p2 = points[j];
lineOfTwoPoints(p1, p2, cxt, d_connect);

if (p1.x + d_connect < p2.x) {
break;
}
}
}

// 画出粒子与鼠标当前位置的连接线
if (mouse_point.x >= 0) {
for (let i = 0; i < points.length; i++){
lineOfTwoPoints(mouse_point, points[i], cxt, d_connect);
}
}

pointsMove(points);
}
show();
setInterval(show,1000/30); // 以30帧的频率进行刷新
</script>

Javascript固然不是一门设计的多么精妙的语言,连它的作者都承认这点,但这一点也不妨碍它一统天下。从Javascript的大红大紫中,也可以看出一门语言真是没什么优劣之分,大家也根本不关心,关键是看它能应用到什么场景。可以想象,如果现在所有的现代浏览器都支持一门XXOOScript的语言,这门语言也一样大红大紫,根本无关语言的设计。
程序员门更关心的是什么?大家都希望少用几门语言就能解决大部分问题,大家都厌倦了学A语言、B语言、C语言,而这些语言实质上又没有本质区别,浪费了时间在语法细节和类库的学习上。所以,Javascript在Web开发中的全栈优势迅速得到了大家的认可。至于Javascript这门语言设计中的固有缺陷,根本没关系,只要有需求程序员们很容易就能想办法绕过去或解决掉。

0%