知乎粒子束的实现

最近上手了canvas,正好看见一个知乎粒子束的实现,以为蛮有意思的,本身就照着作了一遍。原效果是用es6实现的,我这篇文章也就用es6的语法讲了,可是可能有些人对es6的语法不熟悉,我又用es5的语法写了一遍,一方面加深理解,一方面也能够练习一下es5继承的实现,这些都放在仓库里了,能够根据须要本身查看。html

仓库地址
效果地址git

总体框架

这个效果大致能够分为两个部分:es6

  1. 进入页面初始化粒子束
  2. 当鼠标进入页面,在当前坐标画一个圆,并和初始化的效果进行交互。

具体效果是:github

  1. 在页面随机位置画圆
  2. 圆以必定的速度在页面移动
  3. 当两个圆靠近时,连接一条线

分析完需求以后,不管是初始化仍是鼠标的交互,都离不开下面那三种具体的效果。惟一不一样的地方在于,当鼠标进入页面的时候,圆圈产生的位置不是固定的,而是以鼠标的坐标为准,所以这个方法对于鼠标的行为来讲是独立的。所以,最开始的结构就能够这样写:canvas

class Circle{
// 父类

    // Circle的构造函数
    constructor() {}
    
    //如下是circle原型上的方法
    //方法1 画圆
    drawCircle(){}
    
    //方法2 移动
    move(){}
    
    //方法3 连线
    drawLine(){}

}

class currentCircle extends Circle{
// 鼠标的对象,也就是子类

    // 继承父类的构造函数的属性
    constructor(x, y) {}
    
    // 新增一个本身的方法
    // 当鼠标进入页面,在鼠标坐标画圆
    drawCircle(){}
}

具体实现

就这样,基本的结构就完成了,咱们来具体看一下这个结构,在Circle(以后统称为父类),定义了一个构造函数,这里面都是canvas画图用到的相关属性,按照咱们的需求,这里面须要有圆的x坐标,y坐标,圆的半径,圆每次移动的距离,那就能够这样写:数组

// 父类
constructor(x, y) {
    this.x = x;
    this.y = y;
    this.r = Math.random() * 10; //圆的半径
    this._mx = Math.random(); //圆在x轴上移动的距离
    this._my = Math.random(); //圆在y轴上移动的距离
}

这里面,之因此只有x,y须要以参数的形式定义,先猜猜为何?浏览器

前面提到过,不管是初始化效果仍是鼠标的交互,只有一个地方不同,就是后者的鼠标坐标就是新产生的圆的坐标,而非随机的。currentCircle(以后统称为子类)继承了父类构造函数中的属性,因此只有以参数的形式传入才能灵活的选择是随机仍是鼠标坐标定义圆的位置。若是如今很差理解的话,等文章结束,就会明白了。app

完成属性以后,咱们就来完善父类的方法。框架

不管是画圆仍是说连线,都须要用到canvas,所以方法内部都要用到canvas的2D上下文对象,这个既能够用参数传入。dom

连线的方法,不只要知道线的起始点在哪,还须要知道重点在哪,起始点很好肯定,当前圆的中心点的坐标便可,终点则很差肯定,所以咱们能够把另外一个圆做为参数传入,读取它的坐标,所以就是这样:

//父类
drawCircle(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
    ctx.closePath();
    ctx.fillStyle = 'rgba(204, 204, 204, 0.3)';
    ctx.fill();
}

drawLine(ctx, _circle) {
    // _circle就是须要产生连线的另外一个圆
    let dx = this.x - _circle.x; // 两个圆心在x轴上的距离
    let dy = this.y - _circle.y; // 两个圆心在y轴上的距离
    let d = Math.sqrt(dx * dx + dy * dy) // 利用三角函数计算出两个圆心之间的距离
    if (d < 150) {
        ctx.beginPath();
        ctx.moveTo(this.x, this.y); // 线的起点
        ctx.lineTo(_circle.x, _circle.y); // 线的终点
        ctx.closePath();
        ctx.strokeStyle = 'rgba(204, 204, 204, 0.3)';
        ctx.stroke();
    }
}

以前我也说过,线的产生是在两个圆接近的地方产生,不然就不画线,所以须要判断距离,代码中的距离是150像素,这个根据需求能够随意改。

最后就是移动啦:-D

那首先,咱们是否是得保证全部效果的实现都是在canvas里面,不容许有超出的现象发生,若是碰到边界了,应该返回去。氮素每一个人的电脑屏幕又不同大,所以这个大小就不能是固定的,所以就只能写成参数的形式了。

//父类
move(w, h) {
    this._mx = (this.x < w && this.x > 0) ? this._mx : (-this._mx);
    this._my = (this.y < h && this.y > 0) ? this._my : (-this._my);
    this.x += this._mx / 2; // (this._mx / 2)越大,移动越快,下同
    this.y += this._my / 2;       
}

这里面,w和h分别表明画布的宽和高,我具体想说一下里面对距离的判断。

根据写法能够看出来,会先判断这个圆的x坐标和y坐标是否是在画布内。
若是是,就给一个正值。
若是不是,就给一个负值。

但我也在担忧,若是圆一开始就向左边或者上面移动,那不就移动的距离变负值,飘出页面了么?不知道有没有人看出来我这个想法有多蠢。

首先,不管是初始化的效果,亦或是鼠标交互产生的圆,能肯定的是他们必定在画布的范围内。因此一开始对于移动距离的判断就确定是正值,这样的话,圆的移动方向就是向右或者向下这个范围里的一个方向因此他们的结果就是必定会先碰到右边和下边的边界,此时,距离为负值,向相反的方向移动,下次再碰到左边和上边的边界时,距离为正值,在向相反的方向运动,不断循环。所以效果根本不会跑出圈外。

至此,父类的内容就写完了,相比,子类其实就很简单了,一个是继承属性,一个是修改方法。

// 子类

constructor(x, y) {
    super(x, y)
}

drwaCircle(ctx) {
    ctx.beginPath();
    this.r = 8
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false)
    ctx.fillStyle = 'rgba(255, 77, 54, 0.6)'
    ctx.fill();
}

子类的drwaCircle方法和父类的drwaCircle方法不一样的地方在于,前者的圆半径是固定的,若是说你但愿半径随机,这个方法就没必要改写,直接继承父类的就能够。

父类和子类的问题解决以后,咱们来看一些公共的属性和方法。

let canvas = document.createElement('canvas')
document.body.appendChild(canvas)
let ctx = canvas.getContext('2d');
let w = canvas.width = canvas.offsetWidth;
let h = canvas.height = canvas.offsetHeight;
let circles = [];
let current_circle = new currentCircle(0, 0)

这里面我主要说一下这两句

let circles = [];
let current_circle = new currentCircle(0, 0)

circles从定义看就是一个空数组,那么它的意义是什么呢?

咱们最初的目的就是在画布中画一个个的圆,而且这些圆都按照本身的方向移动,靠近还会连线,那这每个圆就能够看作是一个对象,每个对象都包含这个圆的x坐标,y左边,半径,移动的距离这些基本信息,而后基于这些信息画圆,移动,再和另外一个圆交互划线。

所以这个circles就是储存了页面中全部圆圈对象的一个集合。那确定咱们得先建立这么一个集合:

let init = (num)=>{
    for(let i =0;i<num;i++){
        circles.push(new Circle(Math.random()*w,Math.random()*h))
    }
}

num就是页面中圆的个数,也是circle的length。至于循环,就是按照你须要的个数建立父类的实例,每个实例都有本身的各类属性,而后将他们添加到集合中。这样就完成了对数组的初始化。

再看后面那句。

这里建立了一个子类的实例,这个实例是用来进行鼠标交互的,这里建立实例的时候,传入的x和y都是0,这个很重要,后面再说为何。

如今,咱们初始化了全部的圆,实例化了鼠标的行为,建立好了画布,但只是这样,浏览器是不知道咱们要干什么的,咱们如今还须要一个方法告诉浏览器咱们要作什么。

关于这个方法,咱们得告诉浏览器,你须要按照我给定的数目画圆,每一个圆按照必定的频率和距离移动,而后两个圆还得连线。如今数组已经有了,就这样写:

let draw = ()=>{
    for(let i=0;i<circle.length;i++){
        // 这里遍历了数组的每个对象
        // 那这个对象先要用方法把本身按照本身的属性画出来
        // 再按照属性规定的方式移动
        circle[i].drwaCircle(ctx)
        circle[i].move(w,h)
        for(let j =i+1;j<circle.length;j++){
            // 以前说过,划线须要有一个起始点和一个终止点
            // 起始点很好解决,就是调用该方法的圆的坐标
            // 终止点就能够遍历数组中的其余对象,若是这个对象的距离小于咱们规定的距离,划线成功,反之就不画线
            circle[i].drawLine(circle[j])
        }
    }
}

可是这样够么?咱们这里只是告诉了浏览器一开始怎么作,可是没有告诉浏览器鼠标进入该怎么办。可是咱们得先判断鼠标有没有进入页面,也就是有没有x值和y值产生。

记得以前在初始化鼠标实例的时候传入了两个0么,正好就能够借助这个判断一下:

let draw = ()=>{
    for(let i=0;i<circle.length;i++){
        // 这里遍历了数组的每个对象
        // 那这个对象先要用方法把本身按照本身的属性画出来
        // 再按照属性规定的方式移动
        circle[i].drwaCircle(ctx)
        circle[i].move(w,h)
        for(let j =i+1;j<circle.length;j++){
            // 以前说过,划线须要有一个起始点和一个终止点
            // 起始点很好解决,就是调用该方法的圆的坐标
            // 终止点就能够遍历数组中的其余对象,若是这个对象的距离小于咱们规定的距离,划线成功,反之就不画线
            circle[i].drawLine(ctx,circle[j])
        }
    }
    if(current_circle.x){
       current_circle.drawCircle(ctx) 
       for(let i=0;i<circle.length;i++){
            current_circle.drawLine(ctx,circle[i]) 
       }
    }
}

这样告诉浏览器该干什么就完成了,可是这个方法只会执行一遍,而咱们须要的是动画效果,因此还须要一个计时器,这里推荐使用新的API:requestAnimationFrame

这个方法很是适用于动画效果,咱们知道,计时器并非那么完美,至少,他不必定会按照你给的时间间隔运行,而这个方法是按照屏幕的刷新频率运行的,所以动画效果更流畅。

酱紫,这个方法就写完了:

let draw = ()=>{
    for(let i=0;i<circle.length;i++){
        // 这里遍历了数组的每个对象
        // 那这个对象先要用方法把本身按照本身的属性画出来
        // 再按照属性规定的方式移动
        circle[i].drwaCircle(ctx)
        circle[i].move(w,h)
        for(let j =i+1;j<circle.length;j++){
            // 以前说过,划线须要有一个起始点和一个终止点
            // 起始点很好解决,就是调用该方法的圆的坐标
            // 终止点就能够遍历数组中的其余对象,若是这个对象的距离小于咱们规定的距离,划线成功,反之就不画线
            circle[i].drawLine(ctx,circle[j])
        }
    }
    if(current_circle.x){
       current_circle.drawCircle(ctx) 
       for(let i=0;i<circle.length;i++){
            current_circle.drawLine(ctx,circle[i]) 
       }
    }
    requestAnimationFrame(draw)
}

而后把这个方法写进初始化的方法里:

let init = (num)=>{
    for(let i =0;i<num;i++){
        circles.push(new Circle(Math.random()*w,Math.random()*h))
    }
}
draw();

以后再告诉浏览器何时进行初始化:

window.addEventListener('load', init(200));

window.onmousemove = function (e) {
    e = e || window.event;
    current_circle.x = e.clientX;
    current_circle.y = e.clientY;
}
window.onmouseout = function () {
    current_circle.x = null;
    current_circle.y = null;

};

而后监控鼠标什么时候进入页面,监测其坐标并把值附给鼠标实例。

酱紫,整个效果就完成了,由于代码是用es6语法写的,所以须要了解一些该语法的特性,若是实在看不明白,能够对照着es5版本的语法一块儿看。

谢谢你们。

相关文章
相关标签/搜索