微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

用 Canvas 编织璀璨星空图

2016-05-20 20:23 前端大全

(点击上方公众号,可快速关注)


作者:Cyandev

链接:http://www.jianshu.com/p/f5c0f9c4bc39


先来看看最终的效果:



GitHub项目: CyandevToys / ParticleWeb(https://github.com/unixzii/CyandevToys/tree/master/ParticleWeb)

是不是还蛮酷的呢?本文我们就来一点一点分析怎么实现它!


分析


首先我们看看这个效果具体有那些要点。首先,这么炫酷的效果肯定是要用到 Canvas 了,每个星星可以看作为一个粒子,因此,整个效果实际上就是粒子系统了。此外,我们可以发现每个粒子之间是相互连接的,只不过离的近的粒子之间的连线较粗且透明度较低,而离的远的则相反。


开始 Coding


HTML 部分


这部分我就简单放了一个 标签,设置样式使其填充全屏。


<canvas height="620" width="1360" id="canvas" style="position: absolute; height: 100%;"/>


然后为了让所有元素没有间距和内补,我还加了一条全局样式:


 * {

      margin: 0;

      padding: 0;

}


JavaScript 部分


下面我们来写核心的代码。首先我们要得到那个 canvas 并得到绘制上下文:


var canvasEl = document.getElementById('canvas');

var ctx = canvasEl.getContext('2d');

var mousePos = [0, 0];


紧接着我们声明两个变量,分别用于存储“星星”和边:


var nodes = [];

var edges = [];


下一步,我们做些准备工作,就是让画布在窗口大小发生变化时重新绘制,并且调整自身分辨率:


window.onresize = function () {

    canvasEl.width = document.body.clientWidth;

    canvasEl.height = canvasEl.clientHeight;

 

    if (nodes.length == 0) {

      constructNodes();

    }

 

    render();

};

 

window.onresize(); // trigger the event manually.


我们在第一次修改大小后构建了所有节点,这里就要用到下一个函数(constructNodes)了


这个函数中我们随机创建几个点,我们用字典对象的方式存储这些点的各个信息:


function constructNodes() {

    for (var i = 0; i < 100; i++) {

      var node = {

        drivenByMouse: i == 0,

        x: Math.random() * canvasEl.width,

        y: Math.random() * canvasEl.height,

        vx: Math.random() * 1 - 0.5,

        vy: Math.random() * 1 - 0.5,

        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);

      });

    });

  }


为了实现后面一个更炫酷的效果,我给第一个点加了一个 drivenByMouse 属性,这个点的位置不会被粒子系统管理,也不会绘制出来,但是它会与其他点连线,这样就实现了鼠标跟随的效果了。


这里稍微解释一下 radius 属性的取值,我希望让绝大部分点都是小半径的,而极少数的点半径比较大,所以我这里用了一点小 tricky,就是用概率控制点的半径取值,不断调整这个概率阈值就能获取期待的半径随机分布。


点都构建完毕了,就要构建点与点之间的连线了,我们用到双重遍历,把两个点捆绑成一组,放到 edges 数组中。注意这里我用了另外一个函数来完成这件事,而没有直接用 edges.push() ,为什么?


假设我们之前连接了 A、B两点,也就是外侧循环是A,内侧循环是B,那么在下一次循环中,外侧为B,内侧为A,是不是也会创建一条边呢?而实际上,这两个边除了方向不一样以外是完全一样的,这完全没有必要而且占用资源。因此我们在 addEdge 函数中进行一个判断:


function addEdge(edge) {

    var ignore = false;

 

    edges.forEach(function (e) {

      if (e.from == edge.from & e.to == edge.to) {

        ignore = true;

      }

 

      if (e.to == edge.from & e.from == edge.to) {

        ignore = true;

      }

    });

 

    if (!ignore) {

      edges.push(edge);

    }

  }


至此,我们的准备工作就完毕了,下面我们要让点动起来:


function step() {

    nodes.forEach(function (e) {

      if (e.drivenByMouse) {

        return;

      }

 

      e.x += e.vx;

      e.y += e.vy;

 

      function clamp(min, max, value) {

        if (value > max) {

          return max;

        } else if (value < min) {

          return min;

        } else {

          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);

  }

 

  function adjustNodeDrivenByMouse() {

    nodes[0].x += (mousePos[0] - nodes[0].x) / easingFactor;

    nodes[0].y += (mousePos[1] - nodes[0].y) / easingFactor;

  }


看到这么一大段代码不要害怕,其实做的事情很简单。这是粒子系统的核心,就是遍历粒子,并且更新其状态。更新的公式就是


v = v + a

s = s + v


a是加速度,v是速度,s是位移。由于我们这里不涉及加速度,所以就不写了。然后我们需要作一个边缘的碰撞检测,不然我们的“星星”都无拘无束地一点点飞~走~了~。边缘碰撞后的处理方式就是让速度矢量反转,这样粒子就会“掉头”回来。


还记得我们需要做的鼠标跟随吗?也在这处理,我们让第一个点的位置一点一点移动到鼠标的位置,下面这个公式很有意思,可以轻松实现缓动:


x = x + (t - x) / factor


其中 factor 是缓动因子,t 是最终位置,x 是当前位置。至于这个公式的解释还有个交互大神 Bret Victor 在他的演讲中提到过,视频做的非常好,有条(ti)件(zi)大家一定要看看: Bret Victor – Stop Drawing Dead Fish


好了,回到主题。我们在上面的函数中处理完了一帧中的数据,我们要让整个粒子系统连续地运转起来就需要一个timer了,但是十分不提倡大家使用 setInterval,而是尽可能使用 requestAnimationFrame,它能保证你的帧率锁定在


剩下的就是绘制啦:


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();

    });

  }


常规的 Canvas 绘图操作,注意 beginPath 一定要调用,不然你的线就全部穿在一起了… 需要说明的是,在绘制边的时候,我们先要计算两点距离,然后根据一个阈值来判断是否要绘制这条边,这样我们才能实现距离远的点之间连线不可见的效果。


到这里,我们的整个效果就完成了。如果不明白大家也可以去我文章开头放的 repo 离去看完整的源码。Have fun!!


【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!


 
前端大全 更多文章 详解Javascript中的Object对象 结合个人经历总结的前端入门方法 前端不为人知的一面–前端冷知识集锦 一份优秀的前端开发工程师简历是怎么样的? 浅谈Web缓存
猜您喜欢 R语言入门第十讲:使用函数sample进行抽样 从支付宝隐私门想到的—安卓的公地悲剧 2016年去哪就业?选择达内的八大理由 Android App换肤 全新的方式 最火爆的沙龙,运维帮技术沙龙「运维与架构」 (深圳站·6月25号·免费)