最近上手了canvas,正好看見一個知乎粒子束的實現,以爲蠻有意思的,本身就照着作了一遍。原效果是用es6實現的,我這篇文章也就用es6的語法講了,可是可能有些人對es6的語法不熟悉,我又用es5的語法寫了一遍,一方面加深理解,一方面也能夠練習一下es5繼承的實現,這些都放在倉庫裏了,能夠根據須要本身查看。html
這個效果大致能夠分爲兩個部分:es6
具體效果是:github
分析完需求以後,不管是初始化仍是鼠標的交互,都離不開下面那三種具體的效果。惟一不一樣的地方在於,當鼠標進入頁面的時候,圓圈產生的位置不是固定的,而是以鼠標的座標爲準,所以這個方法對於鼠標的行爲來講是獨立的。所以,最開始的結構就能夠這樣寫: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版本的語法一塊兒看。
謝謝你們。