手把手教你系列 - Particle粒子特效(上)

本著瞎折騰的學(xué)習(xí)態(tài)度,在閑暇之余就是要搞點(diǎn)奇奇怪怪的事情粟按。文中如有哪不對(duì)的地方,還請(qǐng)大家指出。本文項(xiàng)目github地址:https://github.com/SmallStoneSK/particle-effect


前言

今天要跟大家一起分享的是最近用canvas寫的一個(gè)Particle粒子特效弦牡,這效果是很早就出現(xiàn)過了友驮,所以想必大家應(yīng)該都見到過(沒見過的看下面的gif圖,或者戳演示地址驾锰,或者直接看知乎首頁卸留。

以前第一次見到的時(shí)候,心想:哇靠椭豫,那么炫酷耻瑟,這是怎么實(shí)現(xiàn)的?于是開始想各種可能的實(shí)現(xiàn)方案赏酥,但是卻從來沒有真正地去實(shí)現(xiàn)一次喳整。最近閑下來之后又想到了這個(gè)特效,手癢就折騰了下裸扶,在這兒跟大家分享一下算柳。


動(dòng)手前的思考

在動(dòng)手前,不妨先思考一下這效果是怎么實(shí)現(xiàn)的呢姓言?有兩種方式:dom 或 canvas瞬项。

第一種dom方法,我們可以把頁面上的粒子看做是一個(gè)個(gè)通過絕對(duì)定位布局的div何荚,然后用定時(shí)器不斷改變它們?cè)陧撁嬷械膖op和left值囱淋,這樣就可以模擬出粒子運(yùn)動(dòng)的效果。但是我們都知道對(duì)dom的操作是很耗性能的餐塘,如果粒子的數(shù)量少的話可能還好妥衣,但要是把粒子的數(shù)量設(shè)的超級(jí)大,那對(duì)瀏覽器來說戒傻,性能方面肯定是個(gè)顧慮税手。所以出于性能考慮,這種方法果斷棄之需纳。

第二種方法是用canvas來實(shí)現(xiàn)芦倒。想必大家都知道canvas是干嘛用的,其實(shí)說白了就是給你一塊畫布不翩,然后你想怎么畫就怎么畫兵扬。那么對(duì)應(yīng)到本文中要實(shí)現(xiàn)的這個(gè)Particle粒子特效,是不是只要把這個(gè)動(dòng)畫的每一幀都畫出來就可以了口蝠?而要畫出每一幀器钟,是不是只要把這個(gè)場(chǎng)景中的所有元素對(duì)應(yīng)到畫布中的具體位置畫出來就可以了?為了便于大家的理解妙蔗,不妨瞅一眼下面的偽代碼:

while(true) {

    // 清除上一幀
    clearLastFrame();
    
    // 繪制(Particle粒子特效的繪制包括drawParticle和drawLine)
    draw();

    // 延遲16.7s(對(duì)于人的肉眼而言傲霸,1秒60幀的動(dòng)畫就可以)
    sleep(1000/60);
}

實(shí)現(xiàn)設(shè)計(jì)

既然已經(jīng)明確使用canvas來實(shí)現(xiàn)我們的Particle粒子特效,那在具體實(shí)現(xiàn)之前,我們不妨先來捋一捋整個(gè)過程昙啄。


1. init初始化

第一步:在html的body中先插入一個(gè)帶id的canvas標(biāo)簽穆役,以便在js中找到它。

<canvas id="canvas">
    <p>your browser doesn't support canvas.</p>
</canvas>
* {
    margin: 0;
    padding: 0;
}
html, body {
    width: 100%;
    height: 100%;
    background-color: #292d35;
}

第二步:新建一個(gè)ParticleEffect.js文件跟衅,添加一個(gè)init方法孵睬,需要設(shè)置canvas的寬高,獲取canvas的上下文伶跷。

// 粒子特效
var ParticleEffect = {
    ctx: null,
    canvas: null,
    init: function() {
        
        var windowSize = Utils.getWindowSize();  // 獲取窗口大小
        this.canvas = document.getElementById('canvas');
        this.ctx = this.canvas.getContext('2d');
        
        if(this.ctx) {
        
            // 設(shè)置canvas的寬高
            this.canvas.width = windowSize.width;
            this.canvas.height = windowSize.height;
            
            // 在這里掰读,你就可以開始用ctx隨心所欲的畫畫了
        }
    }
};

// 工具
var Utils = {
    getWindowSize: function() {
        return {
            width: this.getWindowWidth(),
            height: this.getWindowHeight()
        };
    },
    getWindowWidth: function() {
        return window.innerWidth || document.documentElement.clientWidth;
    },
    getWindowHeight: function() {
        return window.innerHeight || document.documentElement.clientHeight;
    }
};

2.構(gòu)造Particle類

我們會(huì)利用工廠模式創(chuàng)造一個(gè)Particle類,給每個(gè)粒子都添加以下屬性:

// 粒子類
function Particle(info) {

    // 粒子屬性
    this.x = info.x;            // 粒子在畫布中的橫坐標(biāo)
    this.y = info.y;            // 粒子在畫布中的縱坐標(biāo)
    this.vx = info.vx;          // 粒子的橫向運(yùn)動(dòng)速度
    this.vy = info.vy;          // 粒子的縱向運(yùn)動(dòng)速度
    this.color = info.color;    // 粒子的顏色
    this.scale = info.scale;    // 粒子的縮放比例
    this.radius = info.radius;  // 粒子的半徑大小

    // 繪制方法
    if(typeof Particle.prototype.draw === 'undefined') {
        Particle.prototype.draw = function(ctx) {
            // canvas畫圓方法
            ctx.beginPath();
            ctx.fillStyle = this.color;
            ctx.strokeStyle = this.color;
            ctx.arc(this.x, this.y, this.radius * this.scale, 0, 2 * Math.PI, false);
            ctx.closePath();
            ctx.fill();
        }
    }
}

3.生成粒子

現(xiàn)在我們就可以在init方法中用剛創(chuàng)建好的Particle類來生成我們的粒子了叭莫。這里還有一點(diǎn)需要注意的就是蹈集,為了模擬粒子運(yùn)動(dòng)的真實(shí)性,我們就得隨機(jī)的賦予粒子的各個(gè)屬性雇初,包括橫坐標(biāo)拢肆、縱坐標(biāo)、橫向速度靖诗、縱向速度郭怪、半徑、縮放比例刊橘。具體的就看下面代碼吧:

var ParticleEffect = {
     // ... 省去其他代碼
    init: function() {

        // ... 省去其他代碼 
        
        if(this.ctx) {
        
            // ... 省去其他代碼

            // 生成100個(gè)粒子(這里需要注意的是鄙才,由于粒子是有半徑的,所以初始的x, y值范圍需要相應(yīng)的調(diào)整)
            var times = 100;
            this.particles = [];
            while(times--) {
                this.particles.push(new Particle({
                    x: Utils.rangeRandom(10, windowSize.width - 10),
                    y: Utils.rangeRandom(10, windowSize.height - 10),
                    vx: Utils.rangeRandom(-1.2, 1.2),
                    vy: Utils.rangeRandom(-1.2, 1.2),
                    color: 'rgba(255,255,255,.2)',
                    scale: Utils.rangeRandom(0.8, 1.2),
                    radius: 10
                }));
            }
        }
    }
};

var Utils = {
    ... // 省去其他代碼
    rangeRandom: function(min, max) {
        const diff = max - min;
        return min + Math.random() * diff;
    }
};


4.讓粒子動(dòng)起來

終于到了激動(dòng)人心的時(shí)候了促绵,在這一步中攒庵,我們就讓粒子滿屏的動(dòng)起來。

第一步:添加move方法败晴,更新粒子的x, y坐標(biāo)浓冒。需要注意的是,在每次更新完粒子的坐標(biāo)以后尖坤,需要檢測(cè)是否有碰到墻壁稳懒。如果有的話,需要改變粒子的運(yùn)動(dòng)方向糖驴。

var ParticleEffect = {
     // ... 省去其他代碼
    move: function() {

        var windowSize = Utils.getWindowSize();

        this.particles.forEach(function(item) {

            // 更新粒子坐標(biāo)
            item.x += item.vx;
            item.y += item.vy;

            // 如果粒子碰到了左墻壁或右墻壁僚祷,則改變粒子的橫向運(yùn)動(dòng)方向
            if((item.x - item.radius < 0) || (item.x + item.radius > windowSize.width)) {
                item.vx *= -1;
            }

            // 如果粒子碰到了上墻壁或下墻壁,則改變粒子的縱向運(yùn)動(dòng)方向
            if((item.y - item.radius < 0) || (item.y + item.radius > windowSize.height)) {
                item.vy *= -1;
            }
        });
    }
};

第二步:添加draw方法贮缕,控制canvas每次繪制的內(nèi)容(我們先不繪制粒子之間的連線,先讓粒子動(dòng)起來看到效果)俺榆。

var ParticleEffect = {
     // ... 省去其他代碼
    draw: function() {

        var _this = this;
        
        // 每次重新繪制之前感昼,需要先清空畫布,把上一次的內(nèi)容清空
        this.ctx.clearRect(0, 0, windowSize.width, windowSize.height);

        // 繪制粒子
        this.particles.forEach(function(item) {
            item.draw(_this.ctx);
        });

        // TODO: 繪制粒子之間的連線
        
        // 粒子移動(dòng)罐脊,更新相應(yīng)的x, y坐標(biāo)
        this.move();
    }
};

第三步:添加run方法定嗓,使用定時(shí)器蜕琴,不斷重新繪制canvas上的內(nèi)容

var ParticleEffect = {
     // ... 省去其他代碼
    run: function() {
        this.init();
        setInterval(this.draw.bind(this), 1000 / 60);
    }
};

第四步:到這兒就是調(diào)用了,調(diào)用下Particle.run方法就可以讓滿屏的粒子動(dòng)起來了宵溅。

<body>
    <canvas id="canvas">
        <p>your browser doesn't support canvas.</p>
    </canvas>
    <script src="./Particle.js"></script>
    <script>
        window.onload = function() {
          ParticleEffect.run();
        };
    </script>
</body>

5.回過頭來繪制粒子之間的線條

通過觀察不難發(fā)現(xiàn)凌简,在粒子運(yùn)動(dòng)的過程中,兩個(gè)粒子之間的距離比較遠(yuǎn)的時(shí)候是不相連的恃逻,只有當(dāng)兩個(gè)粒子運(yùn)動(dòng)到一定距離范圍之內(nèi)才會(huì)相連雏搂,而遠(yuǎn)了之后細(xì)線又會(huì)斷開。所以實(shí)現(xiàn)起來也不難寇损,只要遍歷所有粒子之間的距離凸郑,只要小于一個(gè)閾值,我們就用canvas畫一條線矛市,把這兩個(gè)粒子連接起來芙沥。具體代碼如下:

var ParticleEffect = {
     // ... 省去其他代碼
    draw: function() {

        // ... 省去其他代碼

        // 繪制粒子之間的連線
        for(var i = 0; i < this.particles.length; i++) {
            for(var j = i + 1; j < this.particles.length; j++) {
                var distance = Math.sqrt(Math.pow(this.particles[i].x - this.particles[j].x, 2) + Math.pow(this.particles[i].y - this.particles[j].y, 2));
                if(distance < 100) {
                    // 這里我們讓距離遠(yuǎn)的線透明度淡一點(diǎn),距離近的線透明度深一點(diǎn)
                    this.ctx.strokeStyle = 'rgba(255,255,255,' + (distance / 100 * .2) + ')';
                    this.ctx.beginPath();
                    this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
                    this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
                    this.ctx.closePath();
                    this.ctx.stroke();
                }
            }
        }
        
    }
};

6.添加粒子與鼠標(biāo)之間的連線

其實(shí)完成上面的這個(gè)步驟浊吏,粒子特效已經(jīng)基本上能跑了而昨。但是我們還可以再添加粒子跟鼠標(biāo)之間的交互效果,比如當(dāng)鼠標(biāo)與粒子之間的距離小于一定閾值時(shí)找田,連接粒子與鼠標(biāo)歌憨。話不多說,看下面的代碼:

var ParticleEffect = {
     // ... 省去其他代碼
     mouseCoordinates: {x: 0, y: 0},
     init: function() {
         
         // ... 省去其他代碼
         
         // 監(jiān)聽鼠標(biāo)的mouseMove事件午阵,記錄下鼠標(biāo)的x,y坐標(biāo)
        window.addEventListener('mousemove', this.handleMouseMove.bind(this), false);
     },
    draw: function() {

        // ... 省去其他代碼

        // 繪制粒子和鼠標(biāo)之間的連線
        for(i = 0; i < this.particles.length; i++) {
            distance = Math.sqrt(Math.pow(this.particles[i].x - this.mouseCoordinates.x, 2) + Math.pow(this.particles[i].y - this.mouseCoordinates.y, 2));
            if(distance < 100) {
                this.ctx.strokeStyle = 'rgba(255,255,255,' + (1 - distance / 100) * .3 + ')';
                this.ctx.beginPath();
                this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
                this.ctx.lineTo(this.mouseCoordinates.x, this.mouseCoordinates.y);
                this.ctx.closePath();
                this.ctx.stroke();
            }
        }
    },
    handleMouseMove: function(event) {

        var x, y;
        event = event || window.event;

        // 處理兼容
        if(event.pageX || event.pageY) {
            x = event.pageX;
            y = event.pageY;
        } else {
            x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
            y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
        }

        this.mouseCoordinates = {x: x, y: y};
    }
};

未完躺孝,待續(xù)...

其實(shí)做到這兒,已經(jīng)能夠看到效果了底桂。但是植袍,還有還有很多可以進(jìn)一步優(yōu)化的地方。比如:粒子動(dòng)效的參數(shù)可配置化籽懦,canvas自適應(yīng)窗口大小于个,requestAnimationFrame代替setInterval,緩存windowSize等等... 這些內(nèi)容將在下一章中再做介紹暮顺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厅篓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捶码,更是在濱河造成了極大的恐慌羽氮,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惫恼,死亡現(xiàn)場(chǎng)離奇詭異档押,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門令宿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叼耙,“玉大人,你說我怎么就攤上這事粒没∩竿瘢” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵癞松,是天一觀的道長(zhǎng)爽撒。 經(jīng)常有香客問我,道長(zhǎng)拦惋,這世上最難降的妖魔是什么匆浙? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮厕妖,結(jié)果婚禮上首尼,老公的妹妹穿的比我還像新娘。我一直安慰自己言秸,他們只是感情好软能,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著举畸,像睡著了一般查排。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抄沮,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天跋核,我揣著相機(jī)與錄音,去河邊找鬼叛买。 笑死砂代,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的率挣。 我是一名探鬼主播刻伊,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼椒功!你這毒婦竟也來了捶箱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤动漾,失蹤者是張志新(化名)和其女友劉穎丁屎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旱眯,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悦屏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年节沦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了键思。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片础爬。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吼鳞,靈堂內(nèi)的尸體忽然破棺而出看蚜,到底是詐尸還是另有隱情,我是刑警寧澤赔桌,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布供炎,位于F島的核電站,受9級(jí)特大地震影響疾党,放射性物質(zhì)發(fā)生泄漏音诫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一雪位、第九天 我趴在偏房一處隱蔽的房頂上張望竭钝。 院中可真熱鬧,春花似錦雹洗、人聲如沸香罐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庇茫。三九已至,卻和暖如春螃成,著一層夾襖步出監(jiān)牢的瞬間旦签,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工寸宏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宁炫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓击吱,卻偏偏與公主長(zhǎng)得像淋淀,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子覆醇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容