示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一. 任務說明 使用原生 繪製水球圖,這將是一個非常有意思的挑戰任務。水球圖是一種常見 ...
目錄
示例代碼托管在:http://www.github.com/dashnowords/blogs
博客園地址:《大史住在大前端》原創博文目錄
華為雲社區地址:【你要的前端打怪升級指南】
一. 任務說明
使用原生canvasAPI
繪製水球圖,這將是一個非常有意思的挑戰任務。水球圖是一種常見的載入動畫,屬於擴展圖形,在echarts
中使用時需要下載擴展庫(同為擴展庫的還包括文字雲插件和地圖插件,項目地址為https://github.com/ecomfe/echarts-liquidfill)。
二. 重點提示
水球圖的繪製有以下幾個難點:
水波的繪製
水波的繪製實際上是運用簡諧振動公式來模擬的,也就是
x = A*(wt +φ)
,其中振幅A
決定了水波的波紋高低,角頻率w
決定了水波的快慢,相位φ
決定了初始位移差,再加上一些y軸方向的位移偏差和顏色的差異,就可以模擬出不同的水波,接著只需要在幀動畫中不斷改變φ
並重繪曲線,就可以模擬出水波效果了。球形剪裁區域
水波的範圍是不能流出球形的外輪廓的,此處的做法是在繪製水波之前,先使用
context.clip( )
方法將水波的可見繪圖區域控制在水球之內即可,如果還有水球外的圖形需要繪製,記得在每一幀繪製完水波後調用context.restore( )
取消掉之前的剪裁。文字的繪製
如果只是繪製漂浮於水球圖之上的文字,是比較容易實現的,但是如果想要實現一些細節更豐富的效果,並不那麼容易。我們期望實現的效果是,當文字未被水波浸入時,顯示水紋的藍色,而被水浸潤的部分顯示為白色,這樣看起來更加生動。但是繪製起來卻並不容易,如果將文字繪製成藍色,那麼被水淹沒的部分就會消失在水紋中,如果繪製成白色,那麼水紋高度較小時,會完全看不到文字。那麼這樣的渲染文字要如何實現呢?
三. 示例代碼
let options = {
value:0,
a:20,//振幅
pos:[300,300],//水球圖位置
r:160,//水球圖半徑
color:['#2E5199','#1567c8','#1593E7','#42B8F9']//水紋顏色
};
start(options);
/**
* 繪製水球圖
*/
function start(options) {
//移動繪圖坐標至水球圖左邊界點
context.translate(options.pos[0],options.pos[1]);
context.font = 'bold 60px Arial';
context.textAlign='center';
context.textBaseLine = 'baseline';
//計算水球圖繪圖數據
createParams(options);
//開啟幀動畫
requestAnimationFrame(startAnim);
}
//生成水波動畫參數,位置坐標公式為 y = A * (wt + φ)
function createParams(options) {
options.w = [];//存儲水波的角速度
options.theta = [];//存儲每條水波的位移
for(let i = 0; i < 4; i++){
options.w.push(Math.PI /(100 + 20*Math.random()));
options.theta.push(20*Math.random());
}
}
//繪製水波線
function drawWaterLines(options) {
let offset;
let A = options.a;//正弦曲線振幅
let y,x,w,theta;
let r = options.r;
//遍歷每一條水紋理
for(let line = 0; line < 4; line++){
context.save();
//每次繪製時水波的偏移距離
theta = Math.random();
offset = r + A / 2 - (r*19/8 + A) * (options.value / 100 ) + line * r/12;
//獲取正弦曲線計算參數
w = options.w[line];
theta = options.theta[line];
context.fillStyle = options.color[line];
context.moveTo(0,0);
context.beginPath();
//以0.1為步長繪製正弦曲線
for(x = 0; x <= 2*r; x+=0.1){
y = A * Math.sin(w * x + theta) + offset;
//繪製點
context.lineTo(x,y);
}
//繪製為超出水球範圍的封閉圖形
context.lineTo(x,r);
context.lineTo(x - 2 * r,r);
context.lineTo(0, A * Math.sin(theta) - options.height);
context.closePath();
//填充封閉圖形得到一條水波
context.fill();
//截取水波範圍,繪製文字(此處將在後文解釋)
context.clip();
context.fillStyle = 'white';
context.fillText(parseInt(options.value,10) + '%',options.r + 10,10);
context.restore();
}
}
//繪製最底層文字
function drawText1(options) {
context.fillStyle = options.color[0];
context.fillText(parseInt(options.value,10) + '%',options.r + 10,10);
}
//幀動畫迴圈
function startAnim() {
//用位移變化模擬水波
options.theta = options.theta.map(item=>item-0.03);
//用百分比進度計算水波的高度
options.value += options.value > 100 ? 0:0.1;
context.save();
resetClip(options);//剪切繪圖區
drawText1(options);//繪製藍色文字
drawWaterLines(options);//繪製水波線
context.restore();
requestAnimationFrame(startAnim);
}
/**設置水球範圍為剪裁區域
*(本例中並沒有水球以外的部分需要繪製,實際上這裡不需要加入幀動畫迴圈中,只需要在開頭設置一次即可。)
*/
function resetClip(options) {
let r = options.r;
context.strokeStyle = '#2E5199';
context.fillStyle = 'white';
context.lineWidth = 10;
context.beginPath();
context.arc(r, 0, r + 10, 0, 2*Math.PI, false);
context.closePath();
context.fill();
context.stroke();
context.beginPath();
context.arc(r, 0, r, 0, 2*Math.PI, true);
context.clip();
}
瀏覽器中可查看效果:
四. 文字淹水效果的實現
文字淹水效果的繪製實際上是按照如下思路來進行的:
- 首先繪製與最上層水紋顏色一致的文字,這樣在被水淹沒之前,文字都可以以可見的顏色顯示。
- 在繪製水波的過程中,連線完成後調用
context.clip( )
方法將繪圖區域剪裁為所有浸水部分,此時再將填充色設置為白色,接著在同一個位置渲染文字,這樣渲染出的白色文字不會超出水紋的範圍,那麼水紋之外的文字的藍色部分也就被保存在畫布上了。 - 為了避免文字中白色的部分被下一層水紋繪製時截斷,我們需要在每一層水紋繪製後,都重覆步驟2,將該層水紋到水球底部的所有範圍設置為剪裁區域,然後繪製該層水紋以內的白色文字部分,這樣當幾層水紋都繪製完畢後,文字淹水的部分就都會被染成白色。
- 在這樣的繪製方法中,文字的最終效果相當於是逐層繪製出來的片段拼接起來的,每次繪製中能被保存到最後的部分,都只有和當前層的水紋相交的部分。
如果我們將每一層文字的繪製顏色修改一下,就比較容易理解繪製過程:
五. 關於canvas抗鋸齒
如果仔細查看上面的水球外圓,會發現水球圖的外側不是很平整,看起來會有很多鋸齒。網上查到的方法大多是將畫布畫布尺寸(canvas.height
,canvas.width
)調整為元素尺寸(CSS中設置的canvas
元素的尺寸)的3-4倍,希望利用縮放來達到抗鋸齒的作用,但實測的結果卻並沒有明顯改進,利用畫布尺寸來縮放在解決圖像和填充模糊的時候效果較好,但在抗鋸齒方面的作用似乎與線條本身的尺寸仍有關係,不是一種絕對有效的方案。另一種較為有效的方案,是在繪製外圓時增加2px-4px的深色陰影,在視覺上可以很好地弱化鋸齒感。
//在繪製外圓之前添加如下代碼
context.shadowColor = '#2E5199';
context.shadowBlur = 2;
context.shadowOffsetX = 0;
context.shadowOffsetY = 2;
六. 小結
至此,我們在這個系列中完成了所有基本圖表的原生API繪製,一些相對高級的圖表,其繪製過程並不一定很複雜,比如矩形樹圖,繪製起來實際上都是矩形方塊,但卻有助於我們以某種更直觀更具有表現力的方式來觀察數據,例如可視化呈現webpack
的打包結果。數據可視化的基本任務就是讓數據變得可視,這需要我們為想觀察的數據選出恰當的表現方式,這不是純粹靠技術能夠達到的,也需要一些藝術細胞和想象力。但無論如何,這都是一個值得研究的有趣的方向。