【帶著canvas去流浪(9)】粒子動畫

来源:https://www.cnblogs.com/dashnowords/archive/2019/05/07/10827576.html
-Advertisement-
Play Games

示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一. 粒子特效 粒子特效一般指密集點陣效果,它並不是canvas獨有的,這個名詞更多出 ...


目錄

示例代碼托管在:http://www.github.com/dashnowords/blogs

博客園地址:《大史住在大前端》原創博文目錄

華為雲社區地址:【你要的前端打怪升級指南】

一. 粒子特效

粒子特效一般指密集點陣效果,它並不是canvas獨有的,這個名詞更多出現在AEcocos2dUnity相關的教程中,並且提供了方便的編輯插件讓使用者可以輕鬆地做出例如煙火,流星,光暈等等動態變化的效果,看起來非常酷炫。如果你接觸過Three.js,會發現三維空間的點陣效果看起來更生動。粒子特效的本質還是一個逐幀動畫,所以我們仍然可以使用上一節中提到的動畫編程範式來實現它。本節的教程將實現下麵這樣一個粒子效果:

這是筆者第5個版本,看起來還挺像回事的吧,本篇中我們將逐步實現這樣一個酷炫的粒子動畫,也邀請你一起來看看開發過程中那些各種令人哭笑不得的問號黑人臉時刻。

二. 開發中遇到的問題

2.1 卡頓

想實現上面的動畫,我們首先要做的是構建一個靜態的粒子點陣,構建的過程並不複雜,無非就是xy兩個方向上以固定間距來畫點。如果我們將單個粒子定義為精靈,而不是粒子群,那麼按照上一節的開發範式,我們會在逐幀動畫的執行函數step中按照如下的方式來更新粒子點陣的狀態:

function step(){
    ...
    particles.map(particle=>{
        particle.update();
        particle.paint();
    })
}

可畫面在粒子點陣動起來後就變得巨卡無比,視覺體驗很差。事實上,每一個精靈狀態的update( )方法僅僅是一些javascript中的計算代碼,執行速度是非常快的,而paint( )方法中會經歷路徑繪製和渲染這兩個階段才能繪製出粒子,這個過程的高頻執行相對來說就會很耗時,當舞臺上的元素數量較少時並不會有什麼問題,但在粒子點陣這樣一個大量精靈元素的場景下,就很容易達到性能飽和。而解決的方式並不複雜,粒子是平鋪在畫紙上的,繪製的先後次序並不會導致畫面覆蓋,我們可以先描繪出所有粒子的路徑(一個小半徑圓圈),接著再最後調用context.stroke()方法一次性將所有粒子的邊線繪製出來,卡頓的問題立刻就解決了。就好像SPA框架中先收集變化並對新舊DOM樹進行diff操作,然後集中進行DOM更新,以取代獨立分散的DOM操作造成的性能損耗。

2.2 軌跡

構建完靜態粒子陣列後,我希望從簡單的特效還是做起,那就是滑鼠移動到某個位置後,就把固定半徑內所有的粒子沿徑向爆炸開來,粒子將沿滑鼠點和初始位置的連線運動。然而效果是上圖那樣的,雖然看起來還挺酷炫的,但它不是我們期望的效果。這裡只是一個低級錯誤,就是在step( )沒有重繪畫布,canvas就像一張畫紙,你所繪製的一切都保留在上面直到你用底色色塊將其覆蓋然後重繪,由於基本的視覺暫留,高速的重繪就成了動畫。

2.3 複位

當我們能夠模擬粒子沿爆炸中心炸開的效果後,就需要考慮如何將其複位。起初筆者試圖用彈簧模型來模擬粒子行為,但是出現的問題就如同上圖那樣,有一部分粒子在初始點附近做起了簡諧振動,通過設置最小複位距離來強制複位也很難做到,如果值太小,總會出現捕獲不到的點,如果值太大,又會造成複位效果失真。其實將複位點作為彈簧模型的平衡點是有問題的,因為簡諧振動在過中點的時候雖然不受力,但其速度卻達到最大,這就使得逐幀動畫之間的位移變化很大,所以才會出現上述的最小複位距離很難確定的問題。

越貼近真實效果,粒子力場模型就會越複雜,如果感興趣,你可以自行建立力場模型來進行模擬。本章的示例代碼中我們採用一種簡化的處理方式,就是在爆炸後,直接將粒子置於一個較遠的位置,並以一個線性遞減的速度來靠近其初始位置,越靠近初始位置速度就越小,當其距離小於最小複位距離時將其歸位。

2.4 防護層

當能夠實現炸開的粒子複位後,最後要實現的效果就是防護圈,你可以想象一個透明的球體被扔進水裡的效果,水在外圍運動卻無法穿透防護進進球體。

筆者首次建模後得到效果是上圖這樣的,使用的模型是一個碰撞衰減模型,也就是將防護層當做鋼體錶面,當粒子在複位過程中進入防護層後,就將其速度向量進行反向,並乘以衰減繫數,當其離開防護層後再重新將速度方向指向初始位置。那麼這個模型有什麼問題呢?其實上面的動畫中已經能夠看出,由於時間間隔的選擇問題,粒子在兩幀之間所移動的距離可能會非常大,導致粒子會突然穿透防護層;另一方面,當爆炸中心移動後,粒子複位時的速度方向和它與爆炸中心的連線可能並不重合,單純地將速度沿原方向取反顯然是失真的。

實際上在防護層邊界的處理上,需要對上述模型進行一些調整。我們換個角度思考一下,假如將防護罩展開成一個平面,那麼粒子的運動軌跡就變得清晰了,如果爆炸中心沒有移動,那麼粒子的複位其實就相當於垂直下落的,如果爆炸中心和複位中心不重合,那麼總可以將小球的速度分解為沿爆炸中心徑向和沿爆炸中心切向,它的運動表現就和具有水平初速度和垂直加速度的物體遇到反彈平面時是一致的,為了簡化模擬處理,當小球即將和防護層碰撞時,可以直接將其沿爆炸中心徑向的速度清零,只保留切向速度,這樣當粒子碰到防護層而無法歸位時,就會沿著防護層錶面運動,這樣粒子就不會穿透防護層了(示例代碼中採用了更簡化的模擬策略,下文會提及)。

2.5 二維向量類

在圖形學的計算中,向量的使用頻率是極高的,在計算距離或是判斷點線面之間的關係等等場景中都會應用到,canvas只是一張畫布,其中的關係和距離等等都需要通過手動計算才能獲得。如果不對常見的向量操作進行封裝,代碼中就會充斥著各種諸如用Math.sqrt(A.x * A.x + A.y * A.y)求模運算這種細節完全暴露的代碼,不僅書寫起來非常繁瑣,閱讀和理解的困難也很高,所以我們需要建立一個二維向量類,把向量的求模,反向,相加,相減等常見操作掛載在原型鏈上,這就使得代碼本身更具有意義,下麵給出一個常見的二維向量類的實現,你可以根據自己的需求對其進行改造,後面的示例中我們也將直接使用這個類:

//二維向量類定義
Vector2 = function(x, y) { this.x = x; this.y = y; };
Vector2.prototype = {
    copy: function() { return new Vector2(this.x, this.y); },
    length: function() { return Math.sqrt(this.x * this.x + this.y * this.y); },
    sqrLength: function() { return this.x * this.x + this.y * this.y; },
    normalize: function() { var inv = 1 / this.length(); return new Vector2(this.x * inv, this.y * inv); },
    negate: function() { return new Vector2(-this.x, -this.y); },
    add: function(v) { return new Vector2(this.x + v.x, this.y + v.y); },
    subtract: function(v) { return new Vector2(this.x - v.x, this.y - v.y); },
    multiply: function(f) { return new Vector2(this.x * f, this.y * f); },
    divide: function(f) { var invf = 1 / f; return new Vector2(this.x * invf, this.y * invf); },
    dot: function(v) { return this.x * v.x + this.y * v.y; }
};

三. 實現講解

本節中針對重點代碼片段進行講解,完整的示例代碼可以從【我的github倉庫】中獲取到。

3.1 粒子類的update方法

/*方法中涉及到的位置相關屬性都是Vector2這個向量類的實例
*所以可以調用原型鏈方法進行向量計算
*/ 
update(){
        
        let nextPos;//模擬下一次落點

        
        const disV = this.pos0.subtract(this.pos);//當前位置到回歸點的向量
        const disL = disV.length();//當前位置和初始點距離

        //1.計算速度(設定最小速度避免出現無限接近卻無法歸位的場景),並模擬下一次落點
        this.velocity = disV.multiply(kv * disL < minV ? minV : kv * disL);
        nextPos = this.pos.add(this.velocity.multiply(dt)); 

        //2.判斷下一次落點是否和當前爆破範圍保護層碰撞
        const disToE = nextPos.subtract(explodeCenter); //從爆破中心指向下一次落點的向量
        const disToEL = disToE.length();
        const disVnext = this.pos0.subtract(nextPos);//下一次落點指向回歸點的向量
        const disLnext = disVnext.length();
        
        if (disToEL < explodeR) {
              //2.1 如果下一次落點會落在當前爆炸中心的範圍內則處理
              nextPos = explodeCenter.add(disToE.normalize().multiply(explodeR * 1.05));
        }else{
              //2.2 如果下一次落點距離回歸點小於最小回收距離則回收
            if (disLnext < resetDistance ) {
                this.pos = this.pos0;
                return;
            }
        }

        //3.確認更新位置
        this.pos = nextPos;      
    }

上面的位置更新策略的難點在於2.1中的計算方法,也就是粒子回歸途中碰到防護層錶面時的處理。為了避開複雜的向量計算,示例代碼中對碰撞的處理是直接改變其下一個落點的位置,而不是通過速度和受力來計算其位置,具體的做法是從當前爆炸中心向下一次落點位置連線生成向量,然後強制將當前粒子置於1.05倍半徑的地方,如下圖所示:

3.2 粒子群的繪製

為了節約渲染時的性能消耗,示例中對逐幀動畫的模式進行了調整,先統一更新粒子狀態,接著繪製所有粒子的路徑,最後一次性調用context.fill方法將粒子渲染出來。

//繪製粒子
function paintParticles() {
    ctx.fillStyle = 'white';
    ctx.beginPath();
    for(let i = 0; i < particles.length; i++){
        for(let j =0; j <particles[i].length; j++){
            //更新粒子狀態
            particles[i][j].update();
            //繪製粒子
            ctx.moveTo(particles[i][j].pos.x,particles[i][j].pos.y);
            ctx.arc(particles[i][j].pos.x,particles[i][j].pos.y,0.9,0,Math.PI*2,false);
        }
    }
    ctx.fill();
}

3.3 爆破層的模擬

粒子是否受到爆破中心的影響相對容易判斷,我們只需要計算粒子當前位置距離爆破中心的距離是否小於設定的爆破層半徑即可,本例中依舊使用直接計算位移的方式來替代基於爆破衝擊力的模擬,當爆破發生時將受到影響的粒子直接沿爆破中心與當前位置連線方向移動至大於爆破半徑的隨即位置。

//爆炸時某個點的影響
function explodePoint(p,center) { 
    let factor= Math.random() * 10;
    let dis = new Vector2(p.pos.x - center.x, p.pos.y - center.y).length();
    //核心點炸開
    if (dis < 0.3 * explodeR) {  
        //初始位置
        p.pos = explodeCenter.add(new Vector2(p.pos.x - center.x, p.pos.y - center.y).normalize().multiply(explodeR*(1+Math.random()*6)));
    } else {
       //非核心點炸至半徑附近
        p.pos = explodeCenter.add(new Vector2(p.pos.x - center.x, p.pos.y - center.y).normalize().multiply(explodeR*(1+Math.random()/10)));
    }
}

其餘的部分都是一些常規的逐幀動畫框架代碼,實現難度並不大,本文不再贅述。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 昨天:android studio安裝好後,在網上學習了一些基礎知識,來豐富自己對android開發的經驗,因為自己在安卓開發這一塊完全就是小白,所以基礎很重要。 看看成果吧: 遇到的問題: 1.在運行時,一個為R的文件經常性報錯,通過ctrl定位發現R為layout文件中所有的綜合,它包含所有文件 ...
  • 發現 jqGrid TreeGrid 載入的數據必須要排序 給了兩種平滑數據模式盡然不內部遞歸 所以改了下源碼加了個數據二次過濾器擴展 數據本該是這樣的 結果沒排序成這樣了 (而且還得是從根節點到子節點整體排序) 1. loadFilters: 數據過濾器擴展 (順帶把ParentId為非字元串類型 ...
  • 雙向數據綁定: 所謂雙向數據綁定是指View(視圖)與Model(模型)之間的綁定:View<=>Model。 View的改變: 通過界面交互使視圖發生改變,如Input框的輸入,Select元素的選擇,scrollBar滾動,瀏覽器視窗大小改變等等。 Model的改變: 如在Ajax, promi ...
  • 首先,我們要瞭解Node.js不是一種語言,它只是一個除了瀏覽器之外的,可以運行js的環境。 其次,Node能做些什麼 ? web伺服器、 命令行工具、 網路爬蟲、 桌面應用程式開發等 3.接下來使用Node.js創建簡單的伺服器 第一種寫法: 第二種寫法: 個人推薦:第二種寫法,可以很好的瞭解。 ...
  • 註意點: position屬性 定義建議元素佈局所用的定位機制 {position:static/absolute/relative/fixed;} static:預設值,沒有定位 absolute:絕對定位元素,相對於static定位以外的第一個父元素進行定位。可以通過left、top、right ...
  • 說起迭代器, 那就要先瞭解迭代模式 迭代模式: 提供一種方法可以順序獲得聚合對象中的各個元素, 是一種最簡單, 也是最常見的設計模式,它可以讓用戶通過特定的介面尋訪集合中的每一個元素 而不用瞭解底層的實現。 迭代器 : 依照迭代模式的思想而實現, 分為內部迭代器和外部迭代器, 內部迭代器: 本身是函 ...
  • 頁面效果: 只是測試了一部分功能,因篇幅有限,不能測試全面,有什麼問題,歡迎留言一起學習! 裡面的正則表達式,參考小編的前幾篇文章,有資源連接的 ...
  • 註意點: 元素類型分為 塊級元素 和 行內元素 塊級元素: 在網頁中以塊的形式顯示,預設情況都會占據一行,兩個相鄰的塊級元素不會出現併列顯示的元素,按照順序自上而下排列。 塊級元素可以定義自己的寬度和長度。 div: dl:與dt、dd搭配使用 form:交互表單 h1-h6:標題 hr:水平線 o ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...