粒子效果源码心得

作者 Haojen Ma 日期 2016-07-18
粒子效果源码心得

好奇萌小助官网的粒子效果,所以在网上找到了一个类似的例子,尝试理解其实现原理

源码实现的大致流程

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 的运算的依赖提升性能。 (唐阳)

模块化函数

在该源码中,每一个函数都是一个简单明了的功能,和有一个语意化的函数名。 这样做的好处,我意识到有以下几点:

  1. 降低了代码之间的耦合,有助于二次开发和查错。
  2. 每个函数应该只专注一件事;
  3. 语意化的名称有助于理解函数或对象的作用,无形的注释。

总结

代码是一种工具,亦是思想的体现。越是在写前对代码的 整体结构需求 越有清晰的认识,写出的代码越就是 可维护性高、更健壮。

算是第一次写阅读源码心得,与其说在源码中学到了什么,不如说学习如何阅读源代码。