canvas 實(shí)現(xiàn)動(dòng)態(tài)粒子連線

粒子連線.gif

如果經(jīng)常上網(wǎng)就一定見過一些博客的背景有一些粒子跑來跑去,還可以連線。看著是挺炫酷的?;旧暇皖愃粕厦娴男Ч=裉旖K于用代碼敲出來了,雖然過程不是太順利,就是粒子跟隨鼠標(biāo)那個(gè)效果,我怎么都想不到實(shí)現(xiàn)思路。還好最后死想出來了,用的方法雖然挺笨的。

這個(gè)效果一直是我想做的,在我玩 HEXO 搭建個(gè)人博客的時(shí)候,參考了很多人的網(wǎng)頁其中見到效果最多的就是這個(gè),當(dāng)時(shí)就暗暗下決心我的把它給寫出來,但那時(shí)候我覺得老難了,啥思路沒有,畢竟那時(shí)候的我知識(shí)儲(chǔ)備不夠,而且當(dāng)時(shí)主要學(xué)習(xí)理論為主要,實(shí)踐的東西很少。

現(xiàn)在的我雖然不是很大神,但是可是能用canvas寫出來游戲的學(xué)生,絕大多數(shù)canvas的效果我都是能做出來的,還是很厲害滴。字?jǐn)?shù)夠了,開始正文,再講下去怕被打。。。

使用技術(shù):

  • 面向?qū)ο?/li>
  • canvas技術(shù)

真的寫完感覺也沒啥難得。還是講講思路吧!

實(shí)現(xiàn)步驟

  1. 首先的會(huì)制作粒子,粒子的制作和使用 canvas 寫炫彩小球的例子差不多。不會(huì)的可以參考上篇文章 Canvas 使用指南和幾個(gè)套路 里面就有這個(gè)粒子,有代碼注釋的很清楚。

  2. 粒子連線的原理。求直線之間的距離,使用的是數(shù)學(xué)中求直角三角形斜邊的方法。x2 + y2 = z2,看數(shù)學(xué)還是很重要的。接下來只需要給定一個(gè)特定的值,來比較求得值與特定值的大小,然后根據(jù)判斷結(jié)果,進(jìn)行連線操作。

  3. 鼠標(biāo)連線和跟隨。這點(diǎn)沒人教還是挺難的,想了一個(gè)上午,下午才想出來。

  • 鼠標(biāo)和粒子之間的連線和粒子之間的連線原理一樣的公式。不難。難得是鼠標(biāo)的跟隨。結(jié)合圖講。
  • 假如下圖中心點(diǎn)是粒子的話,所有在一軌道之內(nèi)的粒子均可以和中心點(diǎn)的粒子連線。
  • 假如下圖中心點(diǎn)是鼠標(biāo)的話所有三軌道之內(nèi)的粒子可以和鼠標(biāo)點(diǎn)連線。粒子之間的連線仍然以粒子為中心方圓 6000 的平方根之內(nèi)。但是粒子的會(huì)在二軌道和三軌道之間向中心靠攏。假如粒子經(jīng)過三軌道受向心力,向中心靠攏,到達(dá)二軌道,此時(shí)粒子運(yùn)動(dòng)的初始方向,與現(xiàn)在外加運(yùn)動(dòng)趨勢(shì)相同,那么粒子在圖中將會(huì)保持運(yùn)動(dòng)方向不變,穿過垂直中心線到達(dá)另一邊,在另一邊二軌道之內(nèi)不會(huì)受到向心力的作用(物理大神現(xiàn)場教學(xué))只保留自己最初的運(yùn)動(dòng)方向,當(dāng)?shù)竭_(dá)二軌道會(huì)有個(gè)向心運(yùn)動(dòng)趨勢(shì)。粒子就會(huì)被一直固定在另一側(cè)的二軌道附近。相反如果粒子一開始就在二軌道受向心運(yùn)動(dòng)且與初始位置相反,因?yàn)橄蛐倪\(yùn)動(dòng)趨勢(shì)大,粒子逃離不了,會(huì)被固定在二軌道附近。

(以上廢話看不懂沒關(guān)系,請(qǐng)看源代碼注釋!自己結(jié)合著理解。)


示意圖

源代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas實(shí)現(xiàn)背景動(dòng)態(tài)粒子連線</title>
    <style>
        html,body{
            margin:0;
            padding:0;
            width: 100%;
            height:100%;
            /*去掉瀏覽器默認(rèn)垂直和水平滑動(dòng)條*/
            overflow:hidden;
        }
    </style>
</head>
<body>
    <canvas></canvas>
</body>
</html>
<script>
    //得到標(biāo)簽
    var myCanvas = document.querySelector("canvas");
    //設(shè)置畫布的寬高為瀏覽器的寬高
    myCanvas.width = document.documentElement.clientWidth;
    myCanvas.height = document.documentElement.clientHeight;
    //得到canvas的上下文
    var ctx = myCanvas.getContext("2d");
    //自適應(yīng)瀏覽器的寬高
    window.onresize = function(){
        //設(shè)置畫布的寬高為瀏覽器的寬高
        myCanvas.width = document.documentElement.clientWidth;
        myCanvas.height = document.documentElement.clientHeight;
    }
    // particle粒子構(gòu)造函數(shù)
    function Particle(){
        // 隨機(jī)生成粒子距離原點(diǎn)的x,y距離用dotx和doty來表示
        this.dotx = Math.random() *  myCanvas.width;
        this.doty = Math.random() *  myCanvas.height;
        // 隨機(jī)生成x,和y方向的運(yùn)動(dòng)信號(hào)量
        do{
            this.idx = (Math.random() * 2) - 1;
            this.idy = (Math.random() * 2) - 1;
            // 當(dāng)生成的信號(hào)量為零重新生成
        }while(this.idx == 0 && this.idy == 0);
        // 兩點(diǎn)之間最大距離的平方
        this.max = 6000;
        // 渲染粒子
        this.render();
        // 粒子放進(jìn)數(shù)組
        dotsArr.push(this);
    }
    // 初始化粒子
    Particle.prototype.render = function(){
        ctx.beginPath();
        ctx.fillRect(this.dotx,this.doty,1,1);
    }
    // 定時(shí)器更新粒子的狀態(tài)
    Particle.prototype.update = function(){
        // 改變每個(gè)粒子的x,y值,讓粒子運(yùn)動(dòng)起來
        this.dotx += this.idx;
        this.doty += this.idy;
        // 碰撞檢測(cè),防止粒子運(yùn)動(dòng)出瀏覽器
        if(this.dotx > myCanvas.width || this.dotx < 0){
            this.idx = -this.idx;
        }
        if(this.doty > myCanvas.height || this.doty < 0){
            this.idy = - this.idy;
        }
        // 渲染粒子
        this.render();
        // 鼠標(biāo)的中心值,當(dāng)沒在窗口移動(dòng)鼠標(biāo),或鼠標(biāo)移出窗口,鼠標(biāo)的mouse.mouse_x
        // 為null不進(jìn)行渲染
        if(mouse.mouse_x != null){
            // 鼠標(biāo)與各個(gè)粒子之間的x和y的距離
            var mousex = this.dotx - mouse.mouse_x;
            var mousey = this.doty - mouse.mouse_y;
            //運(yùn)用直角三角形公式 x2 + y2 = z2求出兩點(diǎn)距離的平方
            var mousez = mousex * mousex + mousey * mousey;
            // 當(dāng)粒子距離鼠標(biāo)在10000~20000之間,向鼠標(biāo)靠攏
            if(mousez > mouse.max / 2 && mousez < mouse.max){

                this.dotx -= mousex*0.03;
                this.doty -= mousey*0.03;
            }
            // 距離鼠標(biāo)平方兩萬以內(nèi)的全部和鼠標(biāo)連線
            if(mousez < mouse.max){
                // 調(diào)用連線功能
                this.toline(mouse.mouse_x,mouse.mouse_y,mousez);
            }
        }
        // 某一粒子實(shí)例與所有粒子距離的判斷
        for(let j = 0;j < dotsArr.length;j++){
            // 不等于本身
            if(dotsArr[j] != this){
                // 實(shí)例與各個(gè)粒子之間的x和y的距離
               var dx = this.dotx - dotsArr[j].dotx;
               var dy = this.doty - dotsArr[j].doty;
               //運(yùn)用直角三角形公式 x2 + y2 = z2求出兩點(diǎn)距離的平方
               var dz = dx * dx + dy * dy;
                // 距離鼠標(biāo)平方六千以內(nèi)的全部和鼠標(biāo)連線
               if(this.max > dz){
                // 調(diào)用連線函數(shù)
                    this.toline(dotsArr[j].dotx,dotsArr[j].doty,dz);
               }
            }
        }
    }
    // 連線函數(shù)
    Particle.prototype.toline = function(x,y,z){
        // 傳來的鼠標(biāo)與粒子距離平方大于粒子之間的距離的平方
        // 確保能夠得到各自的rate
        if(z > this.max){
            var rate = (mouse.max - z) / mouse.max;
        }else{
            var rate = (this.max - z) / this.max;
        }
        // 開始劃線
        ctx.beginPath();
        // 線寬
        ctx.lineWidth = rate / 2;
        // 線的顏色
        ctx.strokeStyle = "rgba(0,0,0," + (rate + 0.2) + ")";
        // 線的起點(diǎn)
        ctx.moveTo(this.dotx,this.doty);
        // 線的終點(diǎn)
        ctx.lineTo(x,y);
        // 劃線
        ctx.stroke();
    }
    // 定義一個(gè)數(shù)組存放所有的粒子
    var dotsArr = [];
    // 向?yàn)g覽器里面放入兩百五十個(gè)粒子
    for(let i = 0;i < 250;i++){
        // 調(diào)用實(shí)例
        new Particle();
    }
    // 定時(shí)器讓粒子動(dòng)起來
    setInterval(function(){
        // 畫布清屏
        ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
        // 更新每個(gè)粒子
        for(let i = 0;i < dotsArr.length;i++){
            dotsArr[i].update();
        }  
    },10);
    // 鼠標(biāo)的位置參數(shù)
    var mouse = {"mouse_x":null,"mouse_y":null,"max":20000}
    // 監(jiān)測(cè)鼠標(biāo)的移動(dòng)
    document.onmousemove = function(event){
        mouse.mouse_x = event.clientX;
        mouse.mouse_y = event.clientY;
    }
    document.onmouseout = function(event){
        mouse.mouse_x = null;
        mouse.mouse_y = null;
    }
</script>

上面是使用定時(shí)器做的,在沒了解 requestAnimationFrame 之前看著感覺挺不錯(cuò)的。很完美,但是如果使用 requestAnimationFrame 來替換定時(shí)器,動(dòng)畫會(huì)變得更加流暢,尤其是兩者對(duì)比。下圖右邊使用的是定時(shí)器做的。


GIF 動(dòng)圖效果的影響看著不是太明顯

window.requestAnimationFrame - Web API 接口參考 | MDN

window.requestAnimationFrame() 是告訴瀏覽器——你希望執(zhí)行一個(gè)動(dòng)畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動(dòng)畫。該方法需要傳入一個(gè)回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會(huì)在瀏覽器下一次重繪之前執(zhí)行

注意:若你想在瀏覽器下次重繪之前繼續(xù)更新下一幀動(dòng)畫,那么回調(diào)函數(shù)自身必須再次調(diào)用 window.requestAnimationFrame()

看例子:

 var timer = requestAnimationFrame(function(){
        console.log("我被打印了!");
    });
    console.log(timer);
打印結(jié)果

首先看出函數(shù)是異步的,其次函數(shù)默認(rèn)返回一個(gè)值1,且函數(shù)只執(zhí)行了一次。所以如果想達(dá)到和定時(shí)器一樣的功能必須在改進(jìn)一下。改進(jìn)如下:

    callback();
    function callback(){
        var timer = requestAnimationFrame(callback);
        console.log("我被打印了!",timer);
    }
改進(jìn)結(jié)果

返回值是一個(gè)整數(shù),請(qǐng)求 ID ,是回調(diào)列表中唯一的標(biāo)識(shí)。是個(gè)非零值,沒別的意義。你可以傳這個(gè)值給 window.cancelAnimationFrame(timer) 取消一個(gè)先前通過調(diào)用window.requestAnimationFrame()方法添加到計(jì)劃中的動(dòng)畫幀請(qǐng)求,以取消回調(diào)函數(shù)。

那么這個(gè) API 的優(yōu)點(diǎn)是什么那?window.requestAnimationFrame() 的優(yōu)點(diǎn)就是使用系統(tǒng)時(shí)間來統(tǒng)一操作回調(diào)函數(shù)里面的數(shù)據(jù)。系統(tǒng)時(shí)間的頻率為 60 赫茲,此API默認(rèn)調(diào)用時(shí)間為 1000ms/60hz ,正是因?yàn)樗y(tǒng)一操作這個(gè)優(yōu)點(diǎn),才會(huì)使動(dòng)畫看起來更加流暢。

改進(jìn)的動(dòng)態(tài)粒子連線源代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas實(shí)現(xiàn)背景動(dòng)態(tài)粒子連線</title>
    <style>
        html,body{
            margin:0;
            padding:0;
            width: 100%;
            height:100%;
            /*去掉瀏覽器默認(rèn)垂直和水平滑動(dòng)條*/
            overflow:hidden;
        }
    </style>
</head>
<body>
    <canvas></canvas>
</body>
</html>
<script>
    //得到標(biāo)簽
    var myCanvas = document.querySelector("canvas");
    //設(shè)置畫布的寬高為瀏覽器的寬高
    myCanvas.width = document.documentElement.clientWidth;
    myCanvas.height = document.documentElement.clientHeight;
    //得到canvas的上下文
    var ctx = myCanvas.getContext("2d");
    //自適應(yīng)瀏覽器的寬高
    window.onresize = function(){
        //設(shè)置畫布的寬高為瀏覽器的寬高
        myCanvas.width = document.documentElement.clientWidth;
        myCanvas.height = document.documentElement.clientHeight;
    }
    // particle粒子構(gòu)造函數(shù)
    function Particle(){
        // 隨機(jī)生成粒子距離原點(diǎn)的x,y距離用dotx和doty來表示
        this.dotx = Math.random() *  myCanvas.width;
        this.doty = Math.random() *  myCanvas.height;
        // 隨機(jī)生成x,和y方向的運(yùn)動(dòng)信號(hào)量
        do{
            this.idx = (Math.random() * 2) - 1;
            this.idy = (Math.random() * 2) - 1;
            // 當(dāng)生成的信號(hào)量為零重新生成
        }while(this.idx == 0 && this.idy == 0);
        // 兩點(diǎn)之間最大距離的平方
        this.max = 6000;
        // 渲染粒子
        this.render();
        // 粒子放進(jìn)數(shù)組
        dotsArr.push(this);
    }
    // 初始化粒子
    Particle.prototype.render = function(){
        ctx.beginPath();
        ctx.fillRect(this.dotx,this.doty,1,1);
    }
    // 定時(shí)器更新粒子的狀態(tài)
    Particle.prototype.update = function(){
        // 改變每個(gè)粒子的x,y值,讓粒子運(yùn)動(dòng)起來
        this.dotx += this.idx;
        this.doty += this.idy;
        // 碰撞檢測(cè),防止粒子運(yùn)動(dòng)出瀏覽器
        if(this.dotx > myCanvas.width || this.dotx < 0){
            this.idx = -this.idx;
        }
        if(this.doty > myCanvas.height || this.doty < 0){
            this.idy = - this.idy;
        }
        // 渲染粒子
        this.render();
        // 鼠標(biāo)的中心值,當(dāng)沒在窗口移動(dòng)鼠標(biāo),或鼠標(biāo)移出窗口,鼠標(biāo)的mouse.mouse_x
        // 為null不進(jìn)行渲染
        if(mouse.mouse_x != null){
            // 鼠標(biāo)與各個(gè)粒子之間的x和y的距離
            var mousex = this.dotx - mouse.mouse_x;
            var mousey = this.doty - mouse.mouse_y;
            //運(yùn)用直角三角形公式 x2 + y2 = z2求出兩點(diǎn)距離的平方
            var mousez = mousex * mousex + mousey * mousey;
            // 當(dāng)粒子距離鼠標(biāo)在10000~20000之間,向鼠標(biāo)靠攏
            if(mousez > mouse.max / 2 && mousez < mouse.max){

                this.dotx -= mousex*0.03;
                this.doty -= mousey*0.03;
            }
            // 距離鼠標(biāo)平方兩萬以內(nèi)的全部和鼠標(biāo)連線
            if(mousez < mouse.max){
                // 調(diào)用連線功能
                this.toline(mouse.mouse_x,mouse.mouse_y,mousez);
            }
        }
        // 某一粒子實(shí)例與所有粒子距離的判斷
        for(let j = 0;j < dotsArr.length;j++){
            // 不等于本身
            if(dotsArr[j] != this){
                // 實(shí)例與各個(gè)粒子之間的x和y的距離
               var dx = this.dotx - dotsArr[j].dotx;
               var dy = this.doty - dotsArr[j].doty;
               //運(yùn)用直角三角形公式 x2 + y2 = z2求出兩點(diǎn)距離的平方
               var dz = dx * dx + dy * dy;
                // 距離鼠標(biāo)平方六千以內(nèi)的全部和鼠標(biāo)連線
               if(this.max > dz){
                // 調(diào)用連線函數(shù)
                    this.toline(dotsArr[j].dotx,dotsArr[j].doty,dz);
               }
            }
        }
    }
    // 連線函數(shù)
    Particle.prototype.toline = function(x,y,z){
        // 傳來的鼠標(biāo)與粒子距離平方大于粒子之間的距離的平方
        // 確保能夠得到各自的rate
        if(z > this.max){
            var rate = (mouse.max - z) / mouse.max;
        }else{
            var rate = (this.max - z) / this.max;
        }
        // 開始劃線
        ctx.beginPath();
        // 線寬
        ctx.lineWidth = rate / 2;
        // 線的顏色
        ctx.strokeStyle = "rgba(0,0,0," + (rate + 0.2) + ")";
        // 線的起點(diǎn)
        ctx.moveTo(this.dotx,this.doty);
        // 線的終點(diǎn)
        ctx.lineTo(x,y);
        // 劃線
        ctx.stroke();
    }
    // 定義一個(gè)數(shù)組存放所有的粒子
    var dotsArr = [];
    // 向?yàn)g覽器里面放入兩百五十個(gè)粒子
    for(let i = 0;i < 250;i++){
        // 調(diào)用實(shí)例
        new Particle();
    }
    var mouse = {"mouse_x":null,"mouse_y":null,"max":20000}
    // 監(jiān)測(cè)鼠標(biāo)的移動(dòng)
    document.onmousemove = function(event){
        mouse.mouse_x = event.clientX;
        mouse.mouse_y = event.clientY;
    }
    document.onmouseout = function(event){
        mouse.mouse_x = null;
        mouse.mouse_y = null;
    }
    // 定時(shí)器讓粒子動(dòng)起來
    callback();
    function callback(){
        // 畫布清屏
        ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
        // 更新每個(gè)粒子
        for(let i = 0;i < dotsArr.length;i++){
            dotsArr[i].update();
        }
        requestAnimationFrame(callback);
    }
    // setInterval(function(){
    //     // 畫布清屏
    //     ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
    //     // 更新每個(gè)粒子
    //     for(let i = 0;i < dotsArr.length;i++){
    //         dotsArr[i].update();
    //     }  
    // },10);
    // 鼠標(biāo)的位置參數(shù)
</script>

好了,接下來出大招,看看 canvas-nest.js 的使用。CDN 鏈接,最好收藏下這個(gè)網(wǎng)址,里面挺多庫的。

<script src="https://cdn.bootcss.com/canvas-nest.js/2.0.4/canvas-nest.js"></script>

最后放下代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas-nest.js</title>
</head>
<body>

</body>
</html>
<script color="99,205,205" opacity='0.9' zIndex="-2" count="199">
    /**
 * Copyright (c) 2016 hustcc
 * License: MIT
 * Version: v1.0.1
 * GitHub: https://github.com/hustcc/canvas-nest.js
**/
!function(){function n(n,e,t){return n.getAttribute(e)||t}function e(n){return document.getElementsByTagName(n)}function t(){var t=e("script"),o=t.length,i=t[o-1];return{l:o,z:n(i,"zIndex",-1),o:n(i,"opacity",.5),c:n(i,"color","0,0,0"),n:n(i,"count",99)}}function o(){a=m.width=window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,c=m.height=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight}function i(){r.clearRect(0,0,a,c);var n,e,t,o,m,l;s.forEach(function(i,x){for(i.x+=i.xa,i.y+=i.ya,i.xa*=i.x>a||i.x<0?-1:1,i.ya*=i.y>c||i.y<0?-1:1,r.fillRect(i.x-.5,i.y-.5,1,1),e=x+1;e<u.length;e++)n=u[e],null!==n.x&&null!==n.y&&(o=i.x-n.x,m=i.y-n.y,l=o*o+m*m,l<n.max&&(n===y&&l>=n.max/2&&(i.x-=.03*o,i.y-=.03*m),t=(n.max-l)/n.max,r.beginPath(),r.lineWidth=t/2,r.strokeStyle="rgba("+d.c+","+(t+.2)+")",r.moveTo(i.x,i.y),r.lineTo(n.x,n.y),r.stroke()))}),x(i)}var a,c,u,m=document.createElement("canvas"),d=t(),l="c_n"+d.l,r=m.getContext("2d"),x=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(n){window.setTimeout(n,1e3/45)},w=Math.random,y={x:null,y:null,max:2e4};m.id=l,m.style.cssText="position:fixed;top:0;left:0;z-index:"+d.z+";opacity:"+d.o,e("body")[0].appendChild(m),o(),window.onresize=o,window.onmousemove=function(n){n=n||window.event,y.x=n.clientX,y.y=n.clientY},window.onmouseout=function(){y.x=null,y.y=null};for(var s=[],f=0;d.n>f;f++){var h=w()*a,g=w()*c,v=2*w()-1,p=2*w()-1;s.push({x:h,y:g,xa:v,ya:p,max:6e3})}u=s.concat([y]),setTimeout(function(){i()},100)}();
</script>

插件的作用和我們寫的功能一模一樣。

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

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

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