摘要
本文主要介紹一種WEB形式的煙花(fireworks)效果(圖1所示),該效果基於Canvas實現,巧妙地運用了canvas繪圖的特性,並加入了物理力作用的模擬,使整體效果非常絢麗、逼真。本文從本質上介紹了其實現原理,便於其他可視化愛好者能快速上手。本文從視覺渲染和運動軌跡模擬兩個方面詳細描述了該效果的實現原理及細節。
圖1 - Canvas煙花效果截圖
引言
“東風夜放花千樹。更吹落、星如雨。”——青玉案·元夕。煙花的耀眼只是一瞬,但人們追求美的心卻是永恆。本文所要介紹的效果,是我所見過的最美麗、最符合人的精神圖像的作品。
網上還有一種類似於火星迸發的線條很細的模擬煙花效果,我覺得比本文這一款差多了。如果作為煙花爆竹出售,我感覺它們是一塊錢和十塊錢效果的差別,這是一個不恰當的比喻。
煙花效果的逼真在於其顏色的多彩變幻和運行軌跡的合理性。從現象上看,一道光柱逐漸上升,到達一定高度時停止,同時出現多條弧形的流星效果並逐漸下降。隨着煙花綻放的時間增加,其顏色逐漸暗淡,最終消逝不見。從本質上來看,它是一系列頭部顏色最亮、尾部越來越暗,且軌跡越來淡的線條。從外表上來看,它爆炸前是一條直線,爆炸后變成多條曲線。
本文所描述的實現方式巧妙地利用了Canvas繪圖的技巧,通過合理的重繪和清除策略,完美地模擬了煙花綻放。
正文
一、顏色渲染
雖然其實質是線條的渲染,但對於煙花來講,我們最直觀的感覺是有一個實物飛上天,然后有很多小的實物因爆炸而散落下降。因此我們可在畫布上根據軌跡不斷繪制小圓點,圓點因連續運動而形成線條。再通過不斷重繪畫布,用帶有一定透明度的背景色來渲染畫布整個區域,並且保證新繪制的和原來繪制的圖形都存在,即可達到軌跡顏色逐漸暗淡的效果。
為了清晰描述這一過程,此處把該部分功能獨立出來,我們單獨來看看這個線條效果是怎么來的。
如圖2所示,為了方便查看,我們把線條放大,節奏放慢。可點擊鏈接到Codepen查看源碼。

圖2 - 線條渲染
//// In loop update
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
//// draw on canvas
this.render = function(){
if(this.end)return;
var c = context;
c.save();
c.globalCompositeOperation = 'lighter';
var x = this.pos.x, y = this.pos.y, r = this.size;
var gradient = c.createRadialGradient(x, y, 0.1, x, y, r);
gradient.addColorStop(0.1, "rgba(255, 255, 255 ," + 1 + ")");
gradient.addColorStop(1, "rgba(0, 0, 0, " + 1 + ")");
c.fillStyle = gradient;
c.beginPath();
c.arc(x, y, r, 0, Math.PI * 2, true);
c.closePath();
c.fill();
c.restore();
};
如代碼標注部分(行2、10、13、19)所示,有四點主要內容:畫圓、顏色徑向漸變、繪圖方式、重繪。
1、畫圓。 x, y代表運行軌跡的當前坐標。控制運動的代碼部分只需要負責計算並更新x和y的值。渲染的代碼只負責在該坐標點畫圓並填充。
2、顏色徑向漸變。圓點的填充顏色,其中 createRadialGradient() 函數的第1、2個參數與第4、5個參數分別表示漸變開始和漸變結束的圓心坐標。在這里設置為相同的值,使顏色過渡效果更自然真實。
3、繪圖方式。Canvas的 globalCompositeOperation 屬性決定下一次繪圖時如何將一個源(新的)圖像繪制到目標(已有)的圖像上。此處設置為 'lighter' 使同時顯示源圖像和目標圖像,否則會影響到循環中的畫布重繪。
4、重繪。使用 fillRect() 函數對整個畫布進行清除,同時設置 fillStyle 屬性為帶一定不透明度的背景色。如此每一次清除畫布將使原有的圖形變得暗淡,隨着時間流逝,而最終消失在夜幕背景下,或被最新的煙花軌跡覆蓋。
以上幾點是此次煙花渲染的核心之處,它巧妙運用了Canvas繪圖的特點,完美地用動畫仿真了煙花的綻放。
二、軌跡模擬
煙花軌跡模擬的核心是模擬物體在物理力作用下的運行狀態。在此我們主要考慮方向、速度、重力的影響。煙花在爆炸時向四面八方散開,體現在2D畫布上就是0~360度隨機方向。我們假想在爆炸的一瞬間沖擊力最大,速度最大,然后受阻力影響速度逐漸減小。此外我們需要考慮重力的影響,使其最終大致呈自由落體運動。
為了純粹演示運動軌跡,我們把該部分邏輯獨立出來,用d3.js模擬整個爆炸過程。
如圖3所示,可點擊鏈接到Codepen查看源碼。

圖3 - 軌跡模擬

圖4 - 軌跡瞬間
接下來我們直接看核心源碼:
//// 創建模型列表
function create(){
//// 初始化
circles = [];
for (var i = 0; i < count; i++) {
var particle = new Circle(objs[i], pos[0], pos[1]);
var angle = Math.random() * Math.PI * 2;
var speed = Math.cos(Math.random() * Math.PI / 2) * 15;
particle.vel.x = Math.cos(angle) * speed;
particle.vel.y = Math.sin(angle) * speed;
particle.size = 10;
particle.gravity = 0.2;
particle.resistance = 0.92;
particle.shrink = Math.random() * 0.05 + 0.93;
circles.push(particle);
}
}
//// In Model Class
//// 模型參數更新
this.update = function(){
this.vel.x *= this.resistance; this.vel.y *= this.resistance;
// gravity down
this.vel.y += this.gravity;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
// shrink
this.size *= this.shrink;
this.move();
};
//// 實體位置移動
this.move = function(){
this.object.attr('cx', this.pos.x)
.attr('cy', this.pos.y)
.attr('r', this.size)
;
}
我們抽象出一個物體模型,它的主要屬性是pos(當前位置)、vel(方向參數)、resistance(阻力作用參數)、gravity(重力系數),主要方法是update() 和 move()。可在Codepen查看完整源碼。
以上代碼中, create() 是一個初始化所有爆炸物的方法,總數量count設置為80左右的隨機數,objs是用d3.js添加的svg上的元素集合(在此我們使用圓形circle),模型類的每一個實例的‘object‘屬性對應一個畫布上的元素,在此根據分層思想把update()和move()分開,前者負責邏輯,后者負責效果。
在create里初始化了模型的一系列參數,其中重要的是移動方向,我們逐條來分析:
var angle = Math.random() * Math.PI * 2; 不難理解,就是角度取 0 ~ 360 度的隨機一個方向。取值在0 ~ 6.28之間。
var speed = Math.cos(Math.random() * Math.PI / 2) * 15; 其中括號內的取值在 0 ~ π/2 之間,我們知道在此區間上cos函數的值為正,值域在0 ~ 1之間,故speed的取值在0 ~ 15之間。
particle.vel.x = Math.cos(angle) * speed; 區間0 ~ 2π上,cos函數的取值是-1 ~ 1之間,正負概率均等,參考圖5。
particle.vel.y = Math.sin(angle) * speed; 區間0 ~ 2π上,sin函數的取值是-1 ~ 1之間,正負概率均等。
因此,方向偏移變量vel分布在二維坐標軸的四個象限,各爆炸物的初始坐標離爆炸點的橫縱距離隨機分布在0 ~ 15之間。

圖5 - 三角函數曲線
在update()函數里,修改方向偏移變量,使力度逐漸衰減,並加入重力影響,然后修改物體的當前位置坐標,以及逐漸減小物體在視覺范圍內的尺寸。
this.vel.x *= this.resistance; resistance 的賦值是0.92,故偏移量每次以92%的比例衰減。因此在效果圖中我們看到,物體完全自由落體前移動速度有越來越慢的緩沖效果,類似於cubicOut緩沖。
this.vel.y *= this.resistance;
this.vel.y += this.gravity; gravity是重力系數,所以運動時需要每次沿y軸下方適當偏移。當vel變量的衰減殆盡時,只受重力作用影響,就呈直線下降狀態。(此處物理專業的朋友禁止較真!)
this.pos.x += this.vel.x; 物體位置信息更新。
this.pos.y += this.vel.y;
this.size *= this.shrink; 物體尺寸大小衰減。
this.move(); 視圖層移動。
在這個簡單的軌跡模擬程序里,我們直接使用 setInterval() 函數來實現動畫刷新,也可以使用 requestAnimationFrame() 函數顯得更正式些。
至此,我們的物體物理運動軌跡模擬完成,如圖3所示,盡管沒有什么渲染的成分,看起來依然有煙花綻放的感覺。
總結
本文介紹了一種基於Canvas的煙花效果實現方式,該方式巧妙地利用了Canvas渲染的特性,其動畫受瀏覽器性能的影響較小,並加入物理力作用效果,使動畫整體看起來很逼真,效果很絢麗。本文詳細介紹了其原理及實現細節,有助於其他有相似需求的開發者能迅速使用並改進,實現更完美的作品。
其它
在此聲明,本文所述的方法非本人原創,也沒有找到原作者,在此鳴謝!
基於以上內容的介紹,有興趣的開發者可以使用d3js框架實現整套的完整效果。
若您發現本文所述有失偏駁之處,或有待改進之處,或您有其它想法、意見及建議,請在評論區留言,謝謝!
