好奇萌小助官网的粒子效果,所以在网上找到了一个类似的例子,尝试理解其实现原理
源码实现的大致流程
1 初始化粒子容器、设置粒子的样式、步进速度
var canvasEl = document.getElementById('canvas');
var ctx = canvasEl.getContext('2d');
var mousePos = [0, 0];
var easingFactor = 5.0; // 鼠标跟随移动响应速度
var backgroundColor = 'black';
var nodeColor = '#fff';
var edgeColor = '#fff';
var nodes = [];
var edges = [];
2 创建粒子,然后push到数组里
function constructNodes() {
var nodeNumber = 60; // 生成指定数量的 node
for (var i = 0; i <= nodeNumber; i++) {
var node = {
drivenByMouse: i == 0, // 返回一个 boolean
x: Math.random() * canvasEl.width, // 随机生成 canvas 的位置, 限定在 canvas 的宽度内
y: Math.random() * canvasEl.height,// 随机生成 canvas 的位置, 限定在 canvas 的长度内
vx: Math.random() - .1, // 步进
vy: Math.random() - .1, // 步进
radius: Math.random() > 0.9 ? 3 + Math.random() * 3 : 1 + Math.random() * 3 // 节点的尺寸
};
nodes.push(node); // 将生成的每一个结果都添加到节点数组里
}
nodes.forEach(function (e) {
nodes.forEach(function (e2) {
if (e == e2) {
return;
}
// 如果这两个元素不是同一个, 那就链接他们,在一定的距离内
var edge = {
from: e,
to: e2
};
addEdge(edge);
edges.push(edge); //将他们的节点关系保存到边缘数组里
});
});
}
3 捆绑粒子,将两个不同的粒子相捆绑
function addEdge(edge) {
var ignore = false; // 开关
edges.forEach(function (e) {
if (e.form == edge.form && e.to == edge.to ) {
ignore = true;
}
if (e.to == edge.from && e.form == edge.to) {
ignore = true;
}
});
if (!ignore) {
edges.push(edge);
}
}
4 生成粒子和将胡同的粒子捆绑之后,开始移动他们的位置
function step() {
nodes.forEach(function (e) {
// 不操作第一个节点
if (e.drivenByMouse) {
return;
}
e.x += e.vx; // 改变位置 x
e.y += e.vy; // 改变位置 y
function clamp(min, max, value) {
if (value > max) {
return max;
}
if (value < min) {
return min;
}
return value;
}
if (e.x <= 0 || e.x >= canvasEl.width) {
e.vx *= -1;
e.x = clamp(0, canvasEl.width, e.x)
}
if (e.y <= 0 || e.y >= canvasEl.height) {
e.vy *= -1;
e.y = clamp(0, canvasEl.height, e.y)
}
});
adjustNodeDrivenByMouse();
render();
window.requestAnimationFrame(step);
}
5 鼠标移到粒子上的效果
function adjustNodeDrivenByMouse() {
nodes[0].x += (mousePos[0] - nodes[0].x) / easingFactor;
nodes[0].y += (mousePos[1] - nodes[0].y) / easingFactor;
}
function lengthOfEdge(edge) {
return Math.sqrt(Math.pow((edge.from.x - edge.to.x), 2) + Math.pow((edge.from.y - edge.to.y), 2));
}
6 渲染
function render() {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
edges.forEach(function (e) {
var l = lengthOfEdge(e);
var threshold = canvasEl.width / 8; // 连线距离
if (l > threshold) {
return;
}
ctx.strokeStyle = edgeColor;
ctx.lineWidth = (1.0 - l / threshold) * 2.5;
ctx.globalAlpha = 1.0 - l / threshold; // 根据距离渐变线段的清晰度
ctx.beginPath();
ctx.moveTo(e.from.x, e.from.y);
ctx.lineTo(e.to.x, e.to.y);
ctx.stroke();
});
ctx.globalAlpha = 1.0; // 删掉后有出奇不意的好效果
nodes.forEach(function (e) {
if (e.drivenByMouse) {
// 过滤掉第一个粒子
return;
}
ctx.fillStyle = nodeColor;
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, 2 * Math.PI);
ctx.fill();
});
}
7 窗口改变的时候调整 canvas 的大小,因为canvas是位图,而非SVG的矢量
window.onresize = function () {
//画布的分辨率随着窗口的调整而改变
canvasEl.width = canvasEl.clientWidth;
canvasEl.height = canvasEl.clientHeight;
if (nodes.length == 0) {
// 如果第一次执行的话, 就创建node
constructNodes();
}
render();
};
8 鼠标的位置偏移
window.onmousemove = function (e) {
mousePos[0] = e.clientX;
mousePos[1] = e.clientY;
};
10 第一次打开时手动触发一次页面
window.onresize();
11 动画定时器 ES5 window.requestAnimationFrame(step);
心得
Canvas 性能问题
使用canvas的时候,发现粒子较多的时候CPU占用很高,但可以通过在 css 样式里添加一个 trick,强制启用 GPU 硬件加速,从而降低对 CPU 的运算的依赖提升性能。 (唐阳)
模块化函数
在该源码中,每一个函数都是一个简单明了的功能,和有一个语意化的函数名。 这样做的好处,我意识到有以下几点:
- 降低了代码之间的耦合,有助于二次开发和查错。
- 每个函数应该只专注一件事;
- 语意化的名称有助于理解函数或对象的作用,无形的注释。
总结
代码是一种工具,亦是思想的体现。越是在写前对代码的 整体结构 和 需求 越有清晰的认识,写出的代码越就是 可维护性高、更健壮。
算是第一次写阅读源码心得,与其说在源码中学到了什么,不如说学习如何阅读源代码。
梦想基金