知乎粒子束的實(shí)現(xiàn)

最近上手了canvas,正好看見一個(gè)知乎粒子束的實(shí)現(xiàn),覺得蠻有意思的,自己就照著做了一遍。原效果是用es6實(shí)現(xiàn)的,我這篇文章也就用es6的語法講了,但是可能有些人對es6的語法不熟悉,我又用es5的語法寫了一遍,一方面加深理解,一方面也可以練習(xí)一下es5繼承的實(shí)現(xiàn),這些都放在倉庫里了,可以根據(jù)需要自己查看。

倉庫地址
效果地址

整體框架

這個(gè)效果大體可以分為兩個(gè)部分:

  1. 進(jìn)入頁面初始化粒子束
  2. 當(dāng)鼠標(biāo)進(jìn)入頁面,在當(dāng)前坐標(biāo)畫一個(gè)圓,并和初始化的效果進(jìn)行交互。

具體效果是:

  1. 在頁面隨機(jī)位置畫圓
  2. 圓以一定的速度在頁面移動(dòng)
  3. 當(dāng)兩個(gè)圓靠近時(shí),鏈接一條線

分析完需求之后,無論是初始化還是鼠標(biāo)的交互,都離不開下面那三種具體的效果。唯一不同的地方在于,當(dāng)鼠標(biāo)進(jìn)入頁面的時(shí)候,圓圈產(chǎn)生的位置不是固定的,而是以鼠標(biāo)的坐標(biāo)為準(zhǔn),因此這個(gè)方法對于鼠標(biāo)的行為來說是獨(dú)立的。因此,最開始的結(jié)構(gòu)就可以這樣寫:

class Circle{
// 父類

    // Circle的構(gòu)造函數(shù)
    constructor() {}
    
    //以下是circle原型上的方法
    //方法1 畫圓
    drawCircle(){}
    
    //方法2 移動(dòng)
    move(){}
    
    //方法3 連線
    drawLine(){}

}

class currentCircle extends Circle{
// 鼠標(biāo)的對象,也就是子類

    // 繼承父類的構(gòu)造函數(shù)的屬性
    constructor(x, y) {}
    
    // 新增一個(gè)自己的方法
    // 當(dāng)鼠標(biāo)進(jìn)入頁面,在鼠標(biāo)坐標(biāo)畫圓
    drawCircle(){}
}

具體實(shí)現(xiàn)

就這樣,基本的結(jié)構(gòu)就完成了,我們來具體看一下這個(gè)結(jié)構(gòu),在Circle(之后統(tǒng)稱為父類),定義了一個(gè)構(gòu)造函數(shù),這里面都是canvas畫圖用到的相關(guān)屬性,按照我們的需求,這里面需要有圓的x坐標(biāo),y坐標(biāo),圓的半徑,圓每次移動(dòng)的距離,那就可以這樣寫:

// 父類
constructor(x, y) {
    this.x = x;
    this.y = y;
    this.r = Math.random() * 10; //圓的半徑
    this._mx = Math.random(); //圓在x軸上移動(dòng)的距離
    this._my = Math.random(); //圓在y軸上移動(dòng)的距離
}

這里面,之所以只有x,y需要以參數(shù)的形式定義,先猜猜為什么?

前面提到過,無論是初始化效果還是鼠標(biāo)的交互,只有一個(gè)地方不一樣,就是后者的鼠標(biāo)坐標(biāo)就是新產(chǎn)生的圓的坐標(biāo),而非隨機(jī)的。currentCircle(之后統(tǒng)稱為子類)繼承了父類構(gòu)造函數(shù)中的屬性,所以只有以參數(shù)的形式傳入才能靈活的選擇是隨機(jī)還是鼠標(biāo)坐標(biāo)定義圓的位置。如果現(xiàn)在不好理解的話,等文章結(jié)束,就會明白了。

完成屬性之后,我們就來完善父類的方法。

無論是畫圓還是說連線,都需要用到canvas,因此方法內(nèi)部都要用到canvas的2D上下文對象,這個(gè)既可以用參數(shù)傳入。

連線的方法,不僅要知道線的起始點(diǎn)在哪,還需要知道重點(diǎn)在哪,起始點(diǎn)很好確定,當(dāng)前圓的中心點(diǎn)的坐標(biāo)即可,終點(diǎn)則不好確定,因此我們可以把另一個(gè)圓作為參數(shù)傳入,讀取它的坐標(biāo),因此就是這樣:

//父類
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就是需要產(chǎn)生連線的另一個(gè)圓
    let dx = this.x - _circle.x; // 兩個(gè)圓心在x軸上的距離
    let dy = this.y - _circle.y; // 兩個(gè)圓心在y軸上的距離
    let d = Math.sqrt(dx * dx + dy * dy) // 利用三角函數(shù)計(jì)算出兩個(gè)圓心之間的距離
    if (d < 150) {
        ctx.beginPath();
        ctx.moveTo(this.x, this.y); // 線的起點(diǎn)
        ctx.lineTo(_circle.x, _circle.y); // 線的終點(diǎn)
        ctx.closePath();
        ctx.strokeStyle = 'rgba(204, 204, 204, 0.3)';
        ctx.stroke();
    }
}

之前我也說過,線的產(chǎn)生是在兩個(gè)圓接近的地方產(chǎn)生,否則就不畫線,因此需要判斷距離,代碼中的距離是150像素,這個(gè)根據(jù)需求可以隨意改。

最后就是移動(dòng)啦:-D

那首先,我們是不是得保證所有效果的實(shí)現(xiàn)都是在canvas里面,不允許有超出的現(xiàn)象發(fā)生,如果碰到邊界了,應(yīng)該返回去。氮素每個(gè)人的電腦屏幕又不一樣大,因此這個(gè)大小就不能是固定的,因此就只能寫成參數(shù)的形式了。

//父類
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)越大,移動(dòng)越快,下同
    this.y += this._my / 2;       
}

這里面,w和h分別代表畫布的寬和高,我具體想說一下里面對距離的判斷。

根據(jù)寫法可以看出來,會先判斷這個(gè)圓的x坐標(biāo)和y坐標(biāo)是不是在畫布內(nèi)。
如果是,就給一個(gè)正值。
如果不是,就給一個(gè)負(fù)值。

但我也在擔(dān)心,如果圓一開始就向左邊或者上面移動(dòng),那不就移動(dòng)的距離變負(fù)值,飄出頁面了么?不知道有沒有人看出來我這個(gè)想法有多蠢。

首先,無論是初始化的效果,亦或是鼠標(biāo)交互產(chǎn)生的圓,能確定的是他們一定在畫布的范圍內(nèi)。所以一開始對于移動(dòng)距離的判斷就肯定是正值,這樣的話,圓的移動(dòng)方向就是向右或者向下這個(gè)范圍里的一個(gè)方向所以他們的結(jié)果就是一定會先碰到右邊和下邊的邊界,此時(shí),距離為負(fù)值,向相反的方向移動(dòng),下次再碰到左邊和上邊的邊界時(shí),距離為正值,在向相反的方向運(yùn)動(dòng),不斷循環(huán)。因此效果根本不會跑出圈外。

至此,父類的內(nèi)容就寫完了,相比,子類其實(shí)就很簡單了,一個(gè)是繼承屬性,一個(gè)是修改方法。

// 子類

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方法不同的地方在于,前者的圓半徑是固定的,如果說你希望半徑隨機(jī),這個(gè)方法就不必改寫,直接繼承父類的就可以。

父類和子類的問題解決之后,我們來看一些公共的屬性和方法。

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從定義看就是一個(gè)空數(shù)組,那么它的意義是什么呢?

我們最初的目的就是在畫布中畫一個(gè)個(gè)的圓,并且這些圓都按照自己的方向移動(dòng),靠近還會連線,那這每一個(gè)圓就可以看做是一個(gè)對象,每一個(gè)對象都包含這個(gè)圓的x坐標(biāo),y左邊,半徑,移動(dòng)的距離這些基本信息,然后基于這些信息畫圓,移動(dòng),再和另一個(gè)圓交互劃線。

因此這個(gè)circles就是儲存了頁面中所有圓圈對象的一個(gè)集合。那肯定我們得先創(chuàng)建這么一個(gè)集合:

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

num就是頁面中圓的個(gè)數(shù),也是circle的length。至于循環(huán),就是按照你需要的個(gè)數(shù)創(chuàng)建父類的實(shí)例,每一個(gè)實(shí)例都有自己的各種屬性,然后將他們添加到集合中。這樣就完成了對數(shù)組的初始化。

再看后面那句。

這里創(chuàng)建了一個(gè)子類的實(shí)例,這個(gè)實(shí)例是用來進(jìn)行鼠標(biāo)交互的,這里創(chuàng)建實(shí)例的時(shí)候,傳入的x和y都是0,這個(gè)很重要,后面再說為什么。

現(xiàn)在,我們初始化了所有的圓,實(shí)例化了鼠標(biāo)的行為,創(chuàng)建好了畫布,但只是這樣,瀏覽器是不知道我們要干什么的,我們現(xiàn)在還需要一個(gè)方法告訴瀏覽器我們要做什么。

關(guān)于這個(gè)方法,我們得告訴瀏覽器,你需要按照我給定的數(shù)目畫圓,每個(gè)圓按照一定的頻率和距離移動(dòng),然后兩個(gè)圓還得連線?,F(xiàn)在數(shù)組已經(jīng)有了,就這樣寫:

let draw = ()=>{
    for(let i=0;i<circle.length;i++){
        // 這里遍歷了數(shù)組的每一個(gè)對象
        // 那這個(gè)對象先要用方法把自己按照自己的屬性畫出來
        // 再按照屬性規(guī)定的方式移動(dòng)
        circle[i].drwaCircle(ctx)
        circle[i].move(w,h)
        for(let j =i+1;j<circle.length;j++){
            // 之前說過,劃線需要有一個(gè)起始點(diǎn)和一個(gè)終止點(diǎn)
            // 起始點(diǎn)很好解決,就是調(diào)用該方法的圓的坐標(biāo)
            // 終止點(diǎn)就可以遍歷數(shù)組中的其他對象,如果這個(gè)對象的距離小于我們規(guī)定的距離,劃線成功,反之就不畫線
            circle[i].drawLine(circle[j])
        }
    }
}

但是這樣夠么?我們這里只是告訴了瀏覽器一開始怎么做,但是沒有告訴瀏覽器鼠標(biāo)進(jìn)入該怎么辦。但是我們得先判斷鼠標(biāo)有沒有進(jìn)入頁面,也就是有沒有x值和y值產(chǎn)生。

記得之前在初始化鼠標(biāo)實(shí)例的時(shí)候傳入了兩個(gè)0么,正好就可以借助這個(gè)判斷一下:

let draw = ()=>{
    for(let i=0;i<circle.length;i++){
        // 這里遍歷了數(shù)組的每一個(gè)對象
        // 那這個(gè)對象先要用方法把自己按照自己的屬性畫出來
        // 再按照屬性規(guī)定的方式移動(dòng)
        circle[i].drwaCircle(ctx)
        circle[i].move(w,h)
        for(let j =i+1;j<circle.length;j++){
            // 之前說過,劃線需要有一個(gè)起始點(diǎn)和一個(gè)終止點(diǎn)
            // 起始點(diǎn)很好解決,就是調(diào)用該方法的圓的坐標(biāo)
            // 終止點(diǎn)就可以遍歷數(shù)組中的其他對象,如果這個(gè)對象的距離小于我們規(guī)定的距離,劃線成功,反之就不畫線
            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]) 
       }
    }
}

這樣告訴瀏覽器該干什么就完成了,但是這個(gè)方法只會執(zhí)行一遍,而我們需要的是動(dòng)畫效果,所以還需要一個(gè)計(jì)時(shí)器,這里推薦使用新的API:requestAnimationFrame

這個(gè)方法非常適用于動(dòng)畫效果,我們知道,計(jì)時(shí)器并不是那么完美,至少,他不一定會按照你給的時(shí)間間隔運(yùn)行,而這個(gè)方法是按照屏幕的刷新頻率運(yùn)行的,因此動(dòng)畫效果更流暢。

醬紫,這個(gè)方法就寫完了:

let draw = ()=>{
    for(let i=0;i<circle.length;i++){
        // 這里遍歷了數(shù)組的每一個(gè)對象
        // 那這個(gè)對象先要用方法把自己按照自己的屬性畫出來
        // 再按照屬性規(guī)定的方式移動(dòng)
        circle[i].drwaCircle(ctx)
        circle[i].move(w,h)
        for(let j =i+1;j<circle.length;j++){
            // 之前說過,劃線需要有一個(gè)起始點(diǎn)和一個(gè)終止點(diǎn)
            // 起始點(diǎn)很好解決,就是調(diào)用該方法的圓的坐標(biāo)
            // 終止點(diǎn)就可以遍歷數(shù)組中的其他對象,如果這個(gè)對象的距離小于我們規(guī)定的距離,劃線成功,反之就不畫線
            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)
}

然后把這個(gè)方法寫進(jìn)初始化的方法里:

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

之后再告訴瀏覽器什么時(shí)候進(jìn)行初始化:

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;

};

然后監(jiān)控鼠標(biāo)何時(shí)進(jìn)入頁面,監(jiān)測其坐標(biāo)并把值附給鼠標(biāo)實(shí)例。

醬紫,整個(gè)效果就完成了,因?yàn)榇a是用es6語法寫的,因此需要了解一些該語法的特性,如果實(shí)在看不明白,可以對照著es5版本的語法一起看。

謝謝大家。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,881評論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評論 19 139
  • 《校園這個(gè)江湖》 深沉的不一定被珍惜如萬古 輕快的不一定如陽光耀及暮 如花的不一定讓容顏逆年駐 心機(jī)者不一定總暗無...
    天鏡泊興閱讀 227評論 2 5
  • 大家好,我是剛從醫(yī)院回來的蛋蛋~ 就在一個(gè)小時(shí)之前,我經(jīng)歷了人生中的第一次磁共振檢查。這對想象力豐富的我來說是一次...
    Anciens安森閱讀 51,797評論 3 4
  • 《讀了多少本書,我推薦多少本》,聽說這是一個(gè)套路。我以前之所以沒有用,那是因?yàn)榍叭齻€(gè)月的每一個(gè)月,我還讀不上10本...
    安之騰閱讀 3,485評論 40 64

友情鏈接更多精彩內(nèi)容