平臺:React
相關(guān)技術(shù): Canvas API ,?requestAnimationFrame API
剛?cè)肼毑痪?,接到一個需求,關(guān)于重構(gòu)課程列表,撇去蛋疼的看源碼修改原有的顯示邏輯之外,當(dāng)時最令我印象深刻的是這個圓形進(jìn)度條的制作過程。
在看到這個需求時,我首先想到能不能用css3進(jìn)行實現(xiàn),畢竟從性能來說css3是較好的,但是發(fā)現(xiàn)這樣最后尾巴的小點沒辦法解決,而且技術(shù)難度貌似不小,最終部門小伙伴說你可以用canvas嘗試一下,并直接給我一個靜態(tài)demo,感恩~,我也趁機(jī)研究了一下canvas api的使用。

首先我們拿到這個圖案先拆分為4個部分,分別說外圓灰色細(xì)圈,外圓橘色細(xì)圈,內(nèi)部橘色圈,以及尾巴的小點, 下面針對一些核心繪制API進(jìn)行分析
這里講一下比較蛋疼的最外側(cè)的圓弧和那個點的開發(fā)。
最外側(cè)的圓弧核心代碼如下:(取context這種基礎(chǔ)咱就不寫了)
????????context.clearRect(0, 0,直徑, 直徑);
? ? ? ? context.beginPath();? ?<=開始繪制 ,包工頭說開始搬磚了
? ? ? ? context.lineWidth = 任意粗細(xì);?<=定義繪制粗細(xì)?
? ? ? ? context.strokeStyle = '定義畫圈的顏色';
? ? ? ? context.arc(直徑 / 2,? ?直徑 / 2,? ? 直徑 / 2 - 2*傳入半徑,? ?0,? ? ?傳入需要的百分比數(shù)值 * 0.02 * Math.PI -?0.5 * Math.PI? ? ?false);?<=重要,繪制圓形路徑
? ? ? ? context.stroke(); <=針對strokeStyle部分結(jié)束繪制
? ? ? ? context.closePath();<=結(jié)束路徑 - 包工頭說下班了
這里主要講一下這里運用context.arc的繪制思路,這里請大家跟我回憶w3school上關(guān)于這個API的用法定義

前兩個不用說,確定中心點,第三個通過傳入一個半徑,通過減去,切分的做法,"漏"出一個傳入值的弧圈,可謂比較討巧,原理看圖好懂點。

而起始角和結(jié)束腳我們需要通過下面這個圖(w3school參考圖(2))說明,起始點0(-0.5 * Math.PI)
結(jié)束點:傳入需要的百分比數(shù)值 * 0.02 * Math.PI -0.5 * Math.PI ,先別急問0.02怎么來的,分析一下。
其實可以理解為-0.5 * Math.PI(也就是1.5 * Math.PI所在位置)就是起點,因為最大值也就是1.5*PI,所以這里的增值最多為-0.5 + x = 1.5? ?x = 2,那么? 2/100 = 0.02份/1%,那么我們上面的公式就是這么來的。

那么上面的圓弧算是開發(fā)完畢了,我們來做一個更難的,那個點的跟蹤計算。
首先先上核心代碼
context.beginPath();
context.strokeStyle= "#FF9C00";
context.lineWidth= 2;
context.fillStyle= "#FF9C00";
let radian= 傳入進(jìn)度 / 100 * 2 * Math.PI - 0.5 * Math.PI;
let x= Math.cos(radian)* (大圓弧直徑 / 2 - 2*裁去的半徑)+ 直徑 / 2(圓心x點);
let y= Math.sin(radian)* (大圓弧直徑 / 2 - 2*裁去的半徑)+ 直徑 / 2(圓心y點);;
context.arc(x, y, 0.8 * r, 0, 2 * Math.PI, false);
context.stroke();
context.fill();
context.closePath();
前四行為起手式級別,上面解釋過,這里不再進(jìn)行解釋,我們主要分析第五行到第七行代碼。
這里最難的是怎么找到這個點的位置(x,y)進(jìn)行擺放,而繪制這個圓點方法如上如法炮制即可,已經(jīng)比較簡單了。
而我們知道數(shù)學(xué)公式里面求圓上的一點的公式
到了JS的Math.cos和Math.sin函數(shù),我們查閱文檔可以知道這兩個函數(shù)中的傳入值都是指的“弧度”而非“角度”,弧度的計算公式為: 2*PI/360*角度;那么你會說這樣最高豈不是有2*Math.PI ? 還記得我們上面的圖嗎,最高也就是1.5*Math.PI,所以我們這里需要手動減去0.5*Math.PI 。因為我們在最上面的需求已經(jīng)可以得到弧度 :傳入進(jìn)度 / 100 * 2 * Math.PI- 0.5 * Math.PI的公式,所以通過它,我們可以得到一個類似xx Math.PI的弧度值。
如果要求算出(x1,y1)在(x0,y0)為圓心的圓上,其所在坐標(biāo),我們可以使用如下的公式得到圓點的位置。
x1 = x0 + r * cos(弧度值)
y1 = y0 + r * sin(弧度值)
那么到此我們終于可以繪制出靜態(tài)的demo圖了,慢著,你以為就結(jié)束了嗎,naive
UI大佬:"要不咱加個動效?"
好吧~向大佬勢力屈服
那么怎么讓它動起來呢?如果你仔細(xì)觀察會發(fā)現(xiàn)我們剛剛實現(xiàn)的繪制過程,都是傳入最終參數(shù)percent來讓它完成繪制的,如果我每一次 precent+1,會有什么情況?不用你猜,對的,他會一次一次的繪制進(jìn)度+1,這時候我突然想到"判斷渲染的條件,動畫,每次+1",腦海里浮現(xiàn)出面試復(fù)習(xí)時經(jīng)常出現(xiàn)的那個巨長的API,requestAnimationFrame,不就是最適合這種場景么.requestAnimationFrame API
實際實現(xiàn)思路就是,初始percent傳0,通過判斷是否達(dá)到percent,如果不是,percent+1繼續(xù)再遞歸調(diào)用,而且他不會產(chǎn)生類似setTimeInterval定時器這種影響頁面性能和事件隊列的副作用。
一段實現(xiàn)代碼
progressRender(context, length, R, r, percent,slogan){
? ? ? ? ?if(slogan!==0) {
? ? ? ? percent += 1;
? ? ? ? if (percent < this.props.progress) {
? ? ? ? ? ? ? requestAnimationFrame(()=> {
? ? ? ? ? ? ? this.progressRender(context, length, R, r, percent)
? ? ? ? })
? ? ?}
}
最終我們能得到如下,ps:隨便找的一個轉(zhuǎn)gif的網(wǎng)站,感覺卡卡的,但是實際還好,比較流暢。

通過這個小需求,不僅有點鍛煉數(shù)學(xué)和邏輯能力,也有點考驗我們對需求應(yīng)變的機(jī)動準(zhǔn)備,如果需求增加了我們要怎么靈活的實現(xiàn)出來,總之,繼續(xù)加油吧~