在我們做的可視化大屏項目中,經常會遇到飛線的效果。 在我們的大屏編輯器中,可以通過拖拽+配置參數的方式很快就能夠實現。下面是我們使用大屏編輯器實現的一個項目效果:
中間地圖就有飛線的效果。
拋開編輯器的快速實現不說,我們大致來說下canvas繪制飛線的大致原理。
貝塞爾曲線
飛線的路徑主要是一個貝塞爾曲線,canvas繪制貝塞爾曲線比較容易。canvas支持繪制二次和三次,在本次示例中,主要還是繪制二次貝塞爾曲線為主。canvas中指定二次貝塞爾曲線路徑的函數如下:
ctx.quadraticCurveTo(cpx, cpy, x, y);
有關貝塞爾曲線的基礎知識,讀者可以自行學習,此處不再贅述。
漸變實現
從圖中,可以看出飛線的效果是淡入的效果,顏色並不是一致的,起點處顏色很淡,終點處顏色就比較濃厚。
怎么樣能夠實現這種效果呢? 答案就是漸變,我們知道,canvas支持線性漸變和放射漸變。但是這兩種漸變似乎都不太適合曲線的路徑。
事實上,我們會考慮使用線性漸變。因為飛線效果中,曲線的彎曲程度都不太大,所以使用線性漸變,曲線造成的差異,人眼是感覺不出來的。
嗯嗯,圖形學就是欺騙的藝術。
只要在線的起點和終點創建一個線性漸變,起點的顏色非透明度是0,終點的非透明度是1即可達到目標。
示例代碼如下:
function createGradient(ctx,p0,p1){
var grd = ctx.createLinearGradient(p0.x,p0.y,p1.x,p1.y);
grd.addColorStop(0,'rgba(255,0,255,0)');
grd.addColorStop(1,'rgba(255,0,255,1)');
return grd;
}
ctx.beginPath();
ctx.moveTo(P0.x,P0.y);
ctx.quadraticCurveTo(Q01.x,Q01.y,B1.x,B1.y);
ctx.lineCap = 'round';
ctx.lineWidth =3;
ctx.strokeStyle = createGradient(ctx,P0,P2);
ctx.shadowColor = 'rgba(255,0,255,1)';
ctx.shadowBlur = 5;
ctx.stroke();
流動效果
流動效果就是線條從起點開始,慢慢飛到終點的效果。 技術角度來說,就是繪制二次曲線百分之幾的一部分,百分比的數值從0增加到1,然后又回到0,周而復始。
代碼如下:
let percent = 0.0;
function render(){
ctx.save();
//按百分比繪制
ctx.restore();
percent += 0.005;
if(percent > 1){
percent = 0.;
}
requestAnimationFrame(render);
}
問題的關鍵在於如何繪制貝塞爾曲線的一部分。 一種思路是使用二次貝塞爾曲線的公式,把曲線分成很多片段來進行模擬,然而這種方式的效率並不高。 其實可以使用插值的方式來獲取一段貝塞爾曲線。代碼如下:
// 參考https://xiaozhuanlan.com/topic/9506147283#section0t
let P0 = startPoint, P1 = controlPoint,P2 = endPoint;
let Q01 = interpolation(P0,P1,percent),
Q11 = interpolation(P1,P2,percent),
B1 = interpolation(Q01,Q11,percent);
function interpolation(P0,P1,t) {
var Q = {
x: P0.x * (1 - t) + P1.x * (t),
y: P0.y * (1 - t) + P1.y * (t),
};
return Q;
}
有關上面插值的原理,可以參考下面的說明,摘取字文章:
https://xiaozhuanlan.com/topic/9506147283#section-5
二次貝塞爾曲線
我們知道二次貝塞爾曲線有三個點P0、P1、P2。二次貝塞爾曲線的表達方程如下:
B(t) = (1-t)2 * P0 + 2t(1-t) * P1 + t2 * P2
其中: $t \in $[0,1]
借助上面一次貝塞爾曲線的計算方法,可以通過以下步驟來確定二次貝塞爾曲線的B(t)點:
- 選定 $t \in $[0,1]
- 通過插值運算法則,在P0和P1所組成的線段上,計算出P0和P1點之間的插值點Q0,其中插值的比例值是t。根據插值規則有:length( P0, Q0 ) = length( P0, P1 ) * t
- 通過插值運算法則,在P1和P2所組成的線段上,計算出P1和P2點之間的插值點Q1,其中插值的比例是t。
- 通過插值運算法則,在Q1和Q2所組成的線段上,計算出P1和P2點之間的插值點B,其中插值的比例是t。
上述過程中計算出來的點B就是在曲線上面點。上述過程如下圖所示:
從圖中可以得出結論:
- 直線(Q0,Q1)和曲線相切於B點。
另外還有隱藏的結論:
- 曲線(P0,B)也是貝塞爾曲線,P0是曲線的起始點,B是曲線的終止點,而Q0是控制點
- 曲線(B,P2)也是貝塞爾曲線,B是曲線的起始點,P2是曲線的終止點,而Q1是控制點
上面兩個結論會很有用,有了這個兩個結論,前面“迭代(分片)”繪制部分貝塞爾的方法,可以用更加簡單的方法替代,這在稍后詳細說明。
如果將t的值從0過渡到1,不斷計算點B,這些點的集合就可以組成一條二次貝塞爾曲線。下面圖形動畫復現了這個效果:
通過上面的方式,就可以繪制流動的飛線效果了,如下圖所示:
加上陰影
默認線條的樣式並不是很好看,如果加上陰影,可以讓效果更加豐滿。 加上陰影也很簡單,代碼如下:
ctx.shadowColor = 'rgba(255,0,255,1)';
ctx.shadowBlur = 5;
最終的飛線效果參考下圖:
結語
如果對可視化感興趣,可以和我交流,微信541002349. 如果你對我們的大屏編輯器產品或者3d組態編輯器感興趣,可以加我微信交流,或者發郵件也可以,terry.tan@servasoft.com。 另外關注公眾號“ITMan彪叔” 可以及時收到更多有價值的文章。