標籤:
俗話說:發圖不留種,菊花萬人捅!我這裡想延伸一下:教學不給例,說你是傻逼!哎呀,還挺押韻,嘻嘻,開個玩笑!
我們都講了四期API的知識了,估計大家看的也是枯燥的很啊,前面的小執行個體也是太簡單,簡直不解渴啊,但是也不能一口氣就吃成一個胖子,下面再給大家來一個小執行個體,給大家提提神!
前面在講畫圓的時候,給大家留了一個思考,或者說是一個坑吧,就是如何來畫一個扇形?我們知道畫圓的方法是無法一下子就能畫出一個扇形的,我當時提供了一個方法,不知道大家是否有印象,沒印象沒關係,我再複述一遍:就是如果我畫了一個圓弧,然後在圓心畫2條線,分別連在圓弧的起始點和結束點,那麼這不就是一個圓嗎?那麼這個方法到底能不能畫出一個圓呢?其實我也不知道,那我們就試一下:
第一步,畫一個圓弧:
//將原點移到100,100的位置ctx.translate(100, 100);//畫一個圓弧ctx.arc(0,0,100,30*Math.PI/180, 60*Math.PI/180);
ctx.stroke();
此時就是我們熟悉的畫圓弧的方法,現在要開始畫線了,這才是關鍵,我們先分析一下,磨刀不費砍柴工嘛!
一條直線是由2個點構成,現在我們知道圓心這點,第2個點就是圓弧的起始點和結束點,那麼這2的座標我們怎麼得到呢?我們畫圖來分析一下:
我們大概就是要畫這樣一個扇形,畫的比較醜,將就看一下,如果我們按照數學思路,根據角度公式一通算,或許能得到這2個點的座標,但是我感覺我自己想想都覺得這運算該有多麼的複製,我數學不好,不願意算,有沒有一種簡便的方法呢,可以不用那麼的懂腦筋的?(抱歉,我的“記憶體”明顯感到不足)我有一個大膽的假設,就是如果這個扇形不是這樣斜著的,而是一邊的水平的,比如說這樣:
那麼我就可以很容易的得到第一條線段了,就是橫座標上的那條線,你不懂?好吧,是這樣的,圓心座標我們知道,半徑我們知道,那弧的起點座標就很容易啦,懂了吧,那另一條線怎麼弄呢?如果你剛看完上一期API的知識,我們趁熱,很容易就能想到rotate()方法,就是我們再畫一條圓心到起點的線,圓弧的角度我們是知道的,好,我們將新畫的這條線選擇圓弧的角度,那不就到終點了嗎?我靠,我TM太機智了!我們來試一下:
//圓弧ctx.save();ctx.translate(100, 100);ctx.arc(0,0,100,0, 30*Math.PI/180);ctx.restore();//第一條線ctx.save();ctx.moveTo(100,100);ctx.lineTo(200,100);ctx.restore();//第二條線ctx.save();ctx.translate(100, 100);ctx.moveTo(0,0);ctx.rotate(30*Math.PI/180);ctx.lineTo(100,0);ctx.stroke();ctx.restore();
哇塞,簡直眼前一亮啊,果然可以啊,按照這思路,如果我們現在把這個扇形旋轉一個角度,這個角度=第二條線的角度-第一條線的角度,那麼得到的不就是前面我們需要的那個扇形嗎?但是前面的代碼直接旋轉只能旋轉圓弧,線旋轉不了,我們修改一下:
還是看這張圖吧,我們可以換一個思路,如果圓弧畫在目標位置,然後畫2條角度為0的2條線,再旋轉到圓弧的起始點和結束點,不就行了嗎?(因為圓弧的起始角度和結束角度我們是知道的)試試吧:
//將原點設定100,100位置ctx.translate(100,100);//原點在100,100,則圓心設為0,0 ——> 100,100的位置ctx.arc(0,0,100,30*Math.PI/180,60*Math.PI/180);//save(),restore()是為了防止角度旋轉的汙染ctx.save();ctx.rotate(30*Math.PI/180);ctx.moveTo(0,0);ctx.lineTo(100,0);ctx.restore();ctx.rotate(60*Math.PI/180);ctx.moveTo(0,0);ctx.lineTo(100,0);ctx.stroke();
哎呀,真的可以啊,哈哈,有人會問,你的第一步為什麼是設定原點呢,為什麼不用moveTo來設定起始點呢?好問題,因為畫布的預設原點在0,0的位置上,如果用moveTo來設定起始點,原點依然還在0,0的位置,上一節API我們將變換的時候講到,變換是以原點為基準點的,即使你設定了起始點,但是起始點不是原點的話,圖形旋轉依然會圍繞0,0點旋轉然後自轉,得到的圖形就不知道是什麼圖形了,偏差的角度就很難矯正,對此還是不太明白的同學可以自己寫一個例子體驗一樣,或許理解更深刻一點,這裡就作為練習題,不在這裡寫了!
上面的代碼還是可以最佳化的,比如說畫第一條線的時候,我們用到了save()和restore(),其作用不只是可以防止外面的屬性或方法對裡面的繪製產生影響,它的本質意思是save()儲存當前環境的狀態,restore()返回之前儲存路徑的狀態,這是什麼意思,舉個栗子,save()就像是在一個迷宮的入口,restore()就想是這個迷宮的出口,但是發現這裡就是迷宮的路口,出了迷宮,在迷宮裡具體是怎麼走的,根本不知道,這就可以防止你的外部因素來影響你走迷宮的路線,那有一個細節大家要注意,就是當你進去的這個門,你出來的時候還是這個門,恩,這個就可以利用了,這就相當於是畫筆的觸點了,還原觸點,我們看一下還原的觸點在什麼地方:
//將原點設定100,100位置ctx.translate(100,100);//原點在100,100,則圓心設為0,0 ——> 100,100的位置ctx.arc(0,0,100,30*Math.PI/180,60*Math.PI/180);//save(),restore()是為了防止角度旋轉的汙染ctx.save();ctx.rotate(30*Math.PI/180);ctx.moveTo(0,0);ctx.lineTo(100,0);ctx.restore();ctx.rotate(60*Math.PI/180);ctx.lineTo(100,0);ctx.stroke();
居然得到的是這樣的結果,從第2張圖可以看出還原的觸點的位置在圓弧的初始點,其實這裡我們是忽略了一個問題,就是線在旋轉的時候,是從它的起點為圓心旋轉的,而上面的代碼是,第一條線從圓心開始,到圓弧的起點(旋轉過後),自然現在的起點就是圓弧的起點了,第二條線怎麼畫,它旋轉的結果都不是我們想要的了,所以這裡我們需要特別的注意,現在我們將第一條直線的起點設在(r,0)的位置,旋轉後就到了圓弧的起始點,然後在畫到圓心地方,那現在的起始點就是圓心了,再畫一條線到圓弧,就哦了,現在我們再來一次:
//將原點設定100,100位置ctx.translate(100,100);//原點在100,100,則圓心設為0,0 ——> 100,100的位置ctx.arc(0,0,100,30*Math.PI/180,60*Math.PI/180);//save(),restore()是為了防止角度旋轉的汙染ctx.save();ctx.rotate(30*Math.PI/180);ctx.moveTo(100,0);ctx.lineTo(0,0);ctx.restore();ctx.rotate(60*Math.PI/180);ctx.lineTo(100,0);ctx.stroke();
看,這就是我們想要的圖形,所以,上面所犯的幾個錯都是比較容易犯的錯,需要特別的注意!
根據這個原理,我們其實還可以用另外一種方式,就是充分使用觸點的作用,怎麼講,當我們再畫圓弧的時候,畫完之後其觸點在圓弧的結束位置,如此的天賜良機,為何不直接將這個觸點作為起點,畫一條到圓心的線,不就可以少旋轉一次嗎?然後再畫第二條線,簡直感覺省時省力,我們看看效果吧:
//將原點設定100,100位置ctx.translate(100,100);//原點在100,100,則圓心設為0,0 ——> 100,100的位置ctx.arc(0,0,100,30*Math.PI/180,60*Math.PI/180);//以圓弧終點為起點畫直線ctx.lineTo(0,0);ctx.rotate(30*Math.PI/180);//以0,0為起點畫直線ctx.lineTo(100,0);ctx.stroke();
你看,用這個理論,就連save(),restore()都可以省了,因為就只有一個旋轉,代碼也少了好多,效果還一樣,哈哈,為了能重複使用,我們需要把他封裝一下:
第一種:
CanvasRenderingContext2D.prototype.sector = function(x,y,r,sDeg,eDeg){ this.save(); this.translate(x,y); this.beginPath(); this.arc(0,0,r,sDeg*Math.PI/180,eDeg*Math.PI/180); this.save(); this.rotate(sDeg*Math.PI/180); this.moveTo(r,0); this.lineTo(0,0); this.restore(); this.rotate(eDeg*Math.PI/180); this.lineTo(r,0); this.restore(); return this; } ctx.sector(100,100,100,30,60).stroke(); ctx.sector(100,100,100,90,120).fill(); ctx.sector(100,100,100,160,180).stroke();
第二種:
CanvasRenderingContext2D.prototype.sector = function(x,y,r,sDeg,eDeg){ this.save(); this.translate(x,y); this.beginPath(); this.arc(0,0,r,sDeg*Math.PI/180,eDeg*Math.PI/180); this.lineTo(0,0); this.rotate(sDeg*Math.PI/180); this.lineTo(r,0); this.restore(); return this; } ctx.sector(100,100,100,30,60).stroke(); ctx.sector(100,100,100,90,120).fill(); ctx.sector(100,100,100,160,180).stroke();
你以為這樣就完了嗎?當我們充分理解canvasAPI的基礎知識的時候,我們還會得到另外一種方式來畫扇形,簡直6到爆!哈哈哈!究竟是什麼呢?我們接著往下看:
前面的基礎知識講到畫圓的時候,我們講到了beginPath()和closePath(),有人會說,這不就是開始路徑和封閉路徑嗎?這跟畫扇形有什麼關係?沒錯,你說的一點都沒錯,好,現在請大聲跟我念:封閉路徑!封閉路徑!封閉路徑!重要事情說3遍,現在你的心裡是不是有了那麼一點感覺,沒錯,不要覺得害羞,不要覺得壓抑,就是它,就是它,大聲把它說出來,就是這感覺,什嗎?你什麼感覺都沒有,此處有表情,好吧,我來告訴你我的感覺:
上面有一處說,為什麼要用translate,而不要moveTo,是因為我們需要旋轉,所以就需要原點,現在如果我們不需要旋轉,而是正常的畫圖,那麼我們就不需要原點,我們就可以用moveTo,好了,如果我們配合beginPath()和closePath(),就會將一個圓弧封閉起來,想想我們在講畫三角形的時候的那段折線是怎麼變成三角形的,沒錯,現在是否有了一點感覺?還是木有?好吧,我們來看一個栗子:
ctx.beginPath();//定義起點ctx.moveTo(100,100);//以起點為圓心,畫一個半徑為100的圓弧ctx.arc(100,100,100,30*Math.PI/180, 60*Math.PI/180);ctx.closePath();ctx.stroke();
看看,寥寥數行,就畫出了一個扇形,對不上面的映像,是不是一樣的?我們封裝一下:
CanvasRenderingContext2D.prototype.sector = function(x,y,r,angle1,angle2){ this.save(); this.beginPath(); this.moveTo(x,y); this.arc(x,y,r,angle1*Math.PI/180,angle2*Math.PI/180,false); this.closePath(); this.restore(); return this; } ctx.sector(100,100,100,30,60).stroke(); ctx.sector(100,100,100,90,120).fill(); ctx.sector(100,100,100,160,180).stroke();
效果都一樣,只是思路不一樣,或許還有別的方式來畫扇形,如果大家有更好的方法,希望能留下你的代碼,大家互相學習一下!
扇形的方法有了,具體用哪個可以依據自己的喜好,我就按照第3種來寫一個小應用,餅圖:
CanvasRenderingContext2D.prototype.sector = function(x,y,r,angle1,angle2){ this.save(); this.beginPath(); this.moveTo(x,y); this.arc(x,y,r,angle1*Math.PI/180,angle2*Math.PI/180,false); this.closePath(); this.restore(); return this; } ctx.fillStyle = ‘red‘; ctx.sector(200,200,100,30,150).fill(); ctx.fillStyle = ‘green‘; ctx.sector(200,200,100,150,270).fill(); ctx.fillStyle = ‘blue‘; ctx.sector(200,200,100,270,390).fill();
再寫一個扇形倒計時:
CanvasRenderingContext2D.prototype.sector = function(x,y,r,angle1,angle2){ this.save(); this.beginPath(); this.moveTo(x,y); this.arc(x,y,r,angle1*Math.PI/180,angle2*Math.PI/180,false); this.closePath(); this.restore(); return this; } var angle = 0; var timer = null; ctx.fillStyle = ‘green‘; setInterval(function(){ angle+=5; ctx.sector(200,200,100,0,angle).fill(); if(angle == 360){ clearInterval(timer); } },200);
你以為我只是寫幾個例子給你看嗎?你還是太年輕了,之所以要丟出這2個例子,是為了擴充一下思路,我們可以在這些效果上面加一點什麼東西,效果是否就不一樣了,舉個例子,第一個餅圖,如果我們在中間加一個白色的圓,會怎麼樣?
CanvasRenderingContext2D.prototype.sector = function(x,y,r,angle1,angle2){ this.save(); this.beginPath(); this.moveTo(x,y); this.arc(x,y,r,angle1*Math.PI/180,angle2*Math.PI/180,false); this.closePath(); this.restore(); return this; } ctx.fillStyle = ‘red‘; ctx.sector(200,200,100,30,150).fill(); ctx.fillStyle = ‘green‘; ctx.sector(200,200,100,150,270).fill(); ctx.fillStyle = ‘blue‘; ctx.sector(200,200,100,270,390).fill(); ctx.fillStyle = ‘#fff‘; ctx.sector(200,200,80,0,360).fill();
看,這效果是不是就變成另外一個效果了,比如說第二個效果,我們也加一個白色的圓,看有什麼效果:
CanvasRenderingContext2D.prototype.sector = function(x,y,r,angle1,angle2){ this.save(); this.beginPath(); this.moveTo(x,y); this.arc(x,y,r,angle1*Math.PI/180,angle2*Math.PI/180,false); this.closePath(); this.restore(); return this; } var angle = 0; var timer = null; setInterval(function(){ angle+=5; ctx.fillStyle = ‘green‘; ctx.sector(200,200,100,0,angle).fill(); ctx.fillStyle = ‘#fff‘; ctx.sector(200,200,80,0,360).fill(); if(angle == 360){ clearInterval(timer); } },200);
看看,這效果是不是可以做很多的效果,當然,因為沒有加動畫效果,現在的效果很生硬,需要大家來完善,只要你腦洞開啟,其實扇形還是能做出很多非常炫酷的效果的,當然了,好的效果都是需要打磨的,在此只是拋磚引玉,如果大家有更好,更炫酷的效果,請不吝分享一下,今天就講到這裡,謝謝大家的支援!
canvas實踐小執行個體二 —— 扇形