今天我們要來製作一個可以掃描出敵人的動態雷達圖,讓隱藏在地圖深處的隨機敵人現身。本次工具主要透過 Canvas 物件,將網頁當作畫布繪製各式不同的圖形,而在觀念上與上一次的時鐘是有些相似的,都運用了三角函數的概念,不過會有一些延伸的知識點,像是如何將相似的圖形以模組化的方式來撰寫,以及敵人在被掃略線掃到時,要如何顯現後再漸漸地消失。
透過此次教學,你會學到
- 認識 Canvas,並將網頁在當作畫布一樣繪製線條、顏色與形狀
- 學習觀察相似物件的特性,以模組化的方式呈現,減少程式碼的撰寫
認識常用的 Canvas 屬性
在 Canvas 中,每一次繪圖都是分為一個個區段的,在執行步驟上可分為三個步驟,這裡以像是操作機台白話的方式來做比擬。
- 按下繪圖開始樣式按鈕
- 設定所要畫的圖形,像是圖形或是線條,可以是單一或是多個都沒問題
- 確認上述設定圖形沒問題,開始畫圖,將圖形顯示在畫面上
上述三個步驟對應到 Canvas 的屬性分別是:
- 繪圖開始 :
beginPath()
- 圖形設計 : 在圖形設計上可分為兩種,一種實心呈現色塊的圖形,另一種則是單一線條,或是以單一線條所構成的中空圖形,常見的畫圖方式有以下:
- 畫正方形
rect(x 位置,y 位置,寬度,高度)
- 畫弧形
arc(x 位置,y 位置,半徑,起始角度,終點角度)
- 畫線條時會有兩個屬性搭配使用,分別是
moveTo(x 位置,y 位置)
,這僅會移動畫筆,但不畫線,而另一個則是lineTo(x 位置,y 位置)
,這則是會以下筆的方式移動到特定位置。
fillStyle
用於指定色塊,而strokeStyle
則是指定線條顏色。 - 畫正方形
- 開始畫圖 : 這裡同樣在兩種不同的圖形有特別的指定方式,在色塊上是
fill()
,而線條則是stroke()
在正式進入本章的主題前,老闆找到了一個線上繪製數學圖形的網站,嘗試以不同的方式來說明極座標的概念。在先前的文章〈來用可怕的三角函數做網頁吧!Part 1及Part 2〉說明了以極座標表示位置的方式,而在本次範例中,一樣會使用到極座標概念,想要回顧上次的教學內容,歡迎點選上面文章連結複習一下再開始!
在圖1-1中,看到網頁中的步驟三定義的變數 t,用於表示角度,而步驟四則顯示極座標 (cos(t), sin(t))
的表示方式畫出圓點,當移動 t 的拉桿時,同時也代表角度正在改變,可以看到在畫面中的點以座標 (0, 0)
的位置為中心,在周圍以半徑為 1 單位的距離移動。
接著在圖 1-2 中的步驟五寫了一個定義圓的方程式,讓前面所提的圓心軌跡顯示出來。
而步驟六則定義了 r 也就是圓的半徑範圍,預設上 r 的半徑大小為 1 ,所以可以看到拖拉變數 t 也就是角度範圍時,圓點就在圓形軌跡上面移動,而當改變半徑 r 時,則會改變整個圓的大小。
為了讓圓點可以在圓的軌跡上,而不是固定在半徑為 1 的範圍中,所以在圖 1-3 的步驟四中,在 x 與 y 的座標位置都乘上了半徑 r,這樣一來,當 r 的大小有變化時,不僅圓的大小會改變,可以看到圓點距離中心點的位置也在改變。
在步驟四中所呈現的就是點在座標系統的呈現方式,其涵蓋了兩個變數,分別是距離中心點位置的半徑 r 以及角度 t。
前置作業
設定 Code Pen
在 Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、Js 中引入 Jquery。
引入雲端字形
在此次範例會使用到外部字體來作為搭配,讓作品更好看。
- 首先進入到 Google Fonts 中,搜尋 Abel 後進入頁面。
- 在 Styles 區塊中的右側有加號 Select this style,點選後在右側會跑出視窗。
- 在整個視窗的右側中會出現 Use on the web 的視窗,點選 import 視窗,複製下列網址 https://fonts.googleapis.com/css2family=Abel&family=Open+Sans&family=Oswald&display=swap
- 開啟 Code Pen 中的 css 設定,將剛剛所選的連結貼在add another resource新增的欄位中,並點選儲存,這樣一來就可以使用了。
上述都設定好後,就要正式進入主題囉!
一、基礎版面
為了可以畫圖,所以需要一張畫布,那就是 Canvas,並指定 ID 為 #myCanvas。另外放置了訊息,分別是標題,以及一些訊息,這裡先暫時以 temp 作為代稱,這個在後面會更改為顯示掃到敵人時,敵人所在的角度與位置。
//HTML canvas#myCanvas .info h1 Boss, CODING Please p.message temp
接下來在樣式上做初始設定,我們希望是滿版的網頁,所以在長與寬都設置為 100%,而在預設上內距與外距會跑出來,但這些我們也不要,所以在 padding 與 margin 上設置為 0。
屬性 overflow 則是決定當物件超出原本的畫面時,該怎麼處理物件的顯示方式,這裡選擇 hidden,代表超出範圍的即隱藏起來。在字體上則是使用先前在 google font 所引入的 Abel。
//CSS html, body //填滿視窗 width: 100% height: 100% padding: 0 margin: 0 overflow: hidden font-family: Abel
現在畫面上仍看不到 canvas 的蹤影,所以指定背景顏色 #333 讓它顯示出來,所以發現它小小一個,但我們希望它撐滿整個版面,不過這個效果老闆選擇後續在 js 修改,這裡僅先調整訊息的位置。
訊息要放置在畫面的左下角,所以將它的定位改為絕對定位,並讓它離下方與左側各距離 50px。在字體的顏色上,敵人的訊息顏色是使用老闆特調的黃金色 rgb(185, 147, 98),標題的話則是白色,雖然標題暫時隱藏了,不過待會背景會設置為深色,就會看見標題了。而在這兩行字的間距上老闆希望可以距離近一些,所以將兩者 margin 都拿掉設置為 0。
//CSS html, body //填滿視窗 width: 100% height: 100% padding: 0 margin: 0 overflow: hidden font-family: Abel canvas background-color: #333 .info position: absolute left: 50px bottom: 50px h1 color: white letter-spacing: 3px margin: 0 .message margin: 0 color: rgb(185, 147, 98)
二、基礎版面樣式設置
畫圖的第一步就是要取得畫布這個元素,在預設上我們有引入 Jquery,所以可以使用錢字號$的方式來抓取元素,所以這裡以錢字號加上在 html 中所設定 canvas 的 ID – #myCanvas,並且記得加上第零個的位置,這樣才會是 html 的元素。
接著處理這張畫布的渲染環境,由於是要在平面的範圍上作圖,所以使用 c.getContext("2d")
來存取的繪圖區域。
有了畫布後,要指定畫布的長寬,讓它可以撐滿整個畫面,所以創建兩個變數分別是 ww
與 wh
來記錄畫面上的寬度與長度。另外,由於後續需要將主要的物件放置在畫面中央,所以也需要創建一個名為 center 的變數來記錄中心點的位置。
接著建立 getWindowSize()
函數來指定畫布長寬與中心點,透過錢字號抓取 window 網頁元件,並取其視窗長度與高度的屬性 outerWidth()
與 outerHeight()
,放入到變數 ww
, wh
中。有了長寬的數值後,就可以將它指定為畫布的長與寬了,而中心點的位置則是將兩者數值都除以 2。
設定好函數後,記得在下面呼叫一次剛剛所撰寫的函式,才會呈現所寫的效果。不過這裡有個問題,當我們拉動網頁視窗時,畫面並不會隨之更新,原因在於函式僅執行了一次,為了解決這個問題,所以需要加上 $(window).resize(getWindowSize)
,代表著當畫面有重新改變大小時,會重複執行一次 getWindowSize()
這個函式。
//JavaScript var c = $("#myCanvas")[0]; var ctx = c.getContext("2d"); var ww, wh; var center = { x: 0, y: 0 }; function getWindowSize() { //設定大小 ww = $(window).outerWidth(); wh = $(window).outerHeight(); c.width = ww c.height = wh //重新設定中心點 center = { x: ww / 2, y: wh / 2 }; } getWindowSize(); //設定當網頁尺寸變動的時候要重新抓跟設定大小、中心 $(window).resize(getWindowSize);
三、繪製一個矩形
設定好畫布後,接著要嘗試在畫布上繪製圖形,這裡先來畫畫看一個正方形。建立一個名為 draw()的函式,裡面 ctx 也就剛剛所抓取的畫布名稱,而rect 則代表要繪製一個矩形,參數分別為 (x 起始位置,y 起始位置,寬度,長度)。為了讓它可以呈現出動態的效果,所以使用 setInterval(draw, 10),設定每十毫秒就執行一次 draw(),並且創建一個數值會向上遞增的變數 time 放到 x 位置中,這樣一來物件每十秒就會向前方移動一單位的距離。
//JavaScript setInterval(draw, 10) var time = 0; function draw() { time += 1; ctx.rect(20 + time, 20, 150, 100); ctx.stroke(); }
但是這個時候會發現,舊的元素並沒有被清除掉。解決這個問題的方法為,在每一次繪圖的時候,也重新再一次指定背景。這邊可以注意到繪製填滿圖形與線條是不一樣的,若是要填滿圖形是 ctx.fill()
,而繪製線條的話則是 ctx.stroke()
,詳細的原理在第四步驟會做說明。
//JavaScript function draw() { time += 1; ctx.fillStyle = "#fff" ctx.beginPath(); ctx.rect(0, 0, 500, 500); ctx.fill(); ctx.rect(0 + time, 0, 50, 50); ctx.stroke(); }
這樣子呈現的效果就沒有問題了,不過我們是要在整張畫布上作畫,當然需要再改變畫布的大小,為了確保可完整地覆蓋背景,所以設置一個很大的數值覆蓋在背景上,並將原本測試使用的矩型移除掉。
//JavaScript function draw() { time += 1; //更新為整張畫布大小為黑色+放大 ctx.fillStyle = "#111" ctx.beginPath(); ctx.rect(-2000, -2000, 4000, 4000); ctx.fill(); }
這是現在畫面上所呈現的樣子,雖然看上去跟步驟二所呈現出的效果是一樣的,但是現在的這個背景會不斷更新,我們接下來將圖形繪製上去的時候,也才不會造成圖形疊加在一起的問題。
四、畫垂直線
背景設定好後,就要來繪圖囉,不過在開始之前,要先來說明在 canvas 中的座標系統:在canvas 中,當增加 y 數值的時候,會發現物體往畫面的下方移動,這是 canvas 預設的座標系統。而我們所熟悉的座標系統中,X 數值增加是向右,而 Y 數值增加則是向上,所以這裡要調整一下,在 sass 中改變 Y 軸的軸向。
//CSS canvas background-color: #333 transform: scaleY(-1)
//JavaScript function getWindowSize() { ... center = { x: ww / 2, y: wh / 2 }; ctx.restore(); ctx.translate(center.x, center.y); }
接下來就是要繪製 x 軸與 y 軸,線條是從左邊至右邊以及從下面至上面。在畫線上會需要使用到兩個指定,分別是 moveTo
與 lineTo
,可以想像你手中現在拿著一支筆,moveTo
代表手移動至該點的位置,但是不接觸紙張,而lineTo
則是從現在這個位置,將畫筆在紙上移動至 lineTo
所指定的位置上,所以繪製 x 軸上就是先移動至畫面的左側 (-ww / 2, 0)
的位置,接著移動到 (ww / 2, 0)
的點上,而 y 軸也是相同的道理。
這邊為何會需要在最後加上 stroke()
呢 ? 原因在於,如果每下達一個指令時,渲染機制就馬上執行畫線的話,這樣是很吃效能的,所以系統是預設讓我們在新增完所有路徑的時候,再使用 stroke()
將剛剛所指定的路徑繪製出,同樣的概念也適用於 fill()
。
//JavaScript function draw() { time += 1; ctx.fillStyle = "#111" ctx.beginPath(); ctx.rect(-2000, -2000, 4000, 4000); ctx.fill(); // 畫座標軸 ctx.strokeStyle = "rgba(255,255,255,0.5)"; //x ctx.moveTo(-ww / 2, 0); ctx.lineTo(ww / 2, 0); //y ctx.moveTo(0, -wh / 2); ctx.lineTo(0, wh / 2); ctx.stroke(); }
五、利用極座標畫線條
建立好座標軸後,就要來開始挑戰本章的大魔王 – 動態掃略線。首先第一步是要以極座標來畫線,會需要兩個重要的數值,分別是線條的長度 r 以及線條的角度 deg。
線條要以中心點為起點,所以將畫筆移動圓點 (0, 0)位置,接著是移動至另一個端點,也就是( r * cos(角度), r * sin(角度) )
,但是此時會發現畫面上所呈現的效果怎麼跟預想的不太一樣,看上去很明顯並非是 45 度。
//JavaScript var color_gold = "185,147,98"; function draw() { ... // 以極座標方式繪製線條 ctx.strokeStyle = "rgba(" + color_gold + ",1)"; var r = 100; var deg = 45; ctx.moveTo(0, 0); ctx.lineTo(r * Math.cos(deg), r * Math.sin(deg)); ctx.stroke(); }
原因在於口語上我們表達會是 90度、180度,但是實際上它的單位會是弧度,比如說 180 度相當於是角度 PI,其數值的大小為 3.14,而非 180,所以這裡需要進行單位上的轉換,建立一個 deg_to_pi 變數,並將 π 除上 180,接著我們在 console 裡面試試看呈現出的結果,可以看到角度乘上 deg_to_pi 時,就會是正確的徑度數值,這樣一來後續在定義角度的時候,我們就可以用熟悉的角度來去定義囉。
下面將定義的變數 deg_to_pi 與角度 deg 做相乘後,所呈現出就會是正確的徑度數值與角度。
//JavaScript var color_gold = "185,147,98"; var deg_to_pi = Math.PI / 180; //新增轉換定義 function draw() { ... // 以極座標方式繪製線條 ctx.strokeStyle = "rgba(" + color_gold + ",1)"; var r = 100; var deg = 45; ctx.moveTo(0, 0); ctx.lineTo(r * Math.cos(deg_to_pi * deg), r * Math.sin(deg_to_pi * deg)); ctx.stroke(); }
做到這邊,確實有達成所希望的效果沒錯,不過如果每次要設定點的位置時,都需要寫一長串的話似乎有點麻煩,所以老闆這邊習慣寫一個可以計算點位置的函數,只需要傳入兩個參數,分別是長度以及角度後,就可以得到一個物件,裡面包含點的 x 位置與 y 位置。
除了更改點的呈現方式之外,這裡有個小地方要注意,就是會發現所有的線條,包含前面設定的 xy 軸都變成了金色,原因在於繪圖系統又重新將它們漆上了金色,為了可以與路徑的設定切分,需要加上一個另一個的 beginPath()
告知繪圖系統要建立一個新的路徑。
//JavaScript var color_gold = "185,147,98"; var deg_to_pi = Math.PI / 180 // 1. 更改為 function 從極座標轉串成點 function Point(r, deg) { return { x: r * Math.cos(deg * deg_to_pi), y: r * Math.sin(deg * deg_to_pi) } } function draw() { ... ctx.strokeStyle = "rgba(" + color_gold + ",1)"; var r = 100; var deg = 45; var newpoint = Point(r, deg) // 2.以函數取得 newPoint, ctx.beginPath(); // 3. 為了避免前面的軸線再次重新被描一次而變成金色,所以這裡要加上 beginPath ctx.moveTo(0, 0); ctx.lineTo(newpoint.x, newpoint.y); ctx.stroke(); }
六、扇形掃描線
扇型的掃描線我們不使用 arc 來繪製,是因為無法達成透明度變化的效果,而要構成掃描線的樣式,可以透過每一個單位的線條或是三角形組合而成,這裡先以較為簡單的線條方式來繪製看看。
每次角度改變一度,就繪製一條線條,所以創建一個 for 迴圈,迴圈執行的次數 line_deg_len
也就是弧形的角度大小,在線條角度上以 time
這個變數的數值為基準減去迴圈中的 i
值,這樣就有 100 個連續相差 1 的角度數值,另外因為 time
是會隨時間變化的,連帶著這 100 個角度值也會不斷變化,進而生成了動態的旋轉扇形。
在透明度的設定上,則是在每一條線畫的時候就指定個別的透明度,不過由於透明度的值是 0~1,我們不能直接放 i
值,而是要放 i / line_deg_len
。
//JavaScript function draw() { ... ctx.strokeStyle = "rgba(" + color_gold + ",1)"; var r = 200; var deg = time; var newpoint = Point(r, deg) var line_deg_len = 100; // 弧線的角度 for (var i = 0; i < line_deg_len; i++) { var deg = (time - i) var newpoint = Point(r, deg) ctx.beginPath(); ctx.strokeStyle = "rgba(" + color_gold + "," + (i / line_deg_len) + ")"; ctx.moveTo(0, 0); ctx.lineTo(newpoint.x, newpoint.y); ctx.stroke(); } }
現在有一個漸層且會動態旋轉的扇形了,但是有兩個地方怪怪的需要調整:一個是它旋轉的方向反了,透明度較低的線條在前方;另一個則是在以線條的方式繪製下,會有明顯紋路的問題,所以接下來我們要改以畫三角形的方式來試試。
三角形相較於線條會複雜一些些,但是原理上是一樣的,只是線條是兩個點形成一條線,而三角形是三個點形成一個面,相較於線條的單一角度 time - i
,還需要另一個相鄰的角度time - i - 1
,並以這兩個角度來計算出每一個三角形的兩個頂點位置後,再將頂點與 (0, 0) 的位置連接起來。在最後要將原先用於線條的 stroke()
改為用於色塊的 fill()
。這樣透過以小三角型色塊的方式呈現,相較於一條條的線條,在視覺上看起來會更加滑順。
接下來處理透明度的方向問題,由於 i
是由 0 開始的,以至於在一開始線條的透明度為 0,這裡有個小技巧就是以 1 減去原先設定的數值,這樣順序就會從由小至大變成由大至小了。
//JavaScript function draw() { ... // 改用三角形畫圖 var line_deg_len = 100; for (var i = 0; i < line_deg_len; i++) { // var deg = (time-i) // var newpoint = Point(r, deg) var deg1 = (time - i - 1) var deg2 = (time - i) var point1 = Point(r, deg1) var point2 = Point(r, deg2) var opacity = 1 - (i / line_deg_len) ctx.beginPath(); ctx.fillStyle = "rgba(" + color_gold + "," + opacity + ")"; ctx.moveTo(0, 0); ctx.lineTo(point1.x, point1.y); ctx.lineTo(point2.x, point2.y); // ctx.stroke(); ctx.fill() } }
七、敵人系統
在第七章敵人系統中是比較大的章節,因此將其拆分成四個小節來做說明,分別有:
- 如何隨機地產生一組敵人
- 如何將隨機的敵人擺放至畫面上
- 如何判定掃略線掃到敵人,並且敵人會做出相對應的變化
- 調整敵人樣式,使敵人看起來更完整
在開始製作敵人系統的這個章節前,要先來建立一個 Color 的函式,透過傳遞一個參數代表透明度來指定需要的顏色,後續也會比較方便。
//JavaScript // 建立 Color 函數來做使用 function Point(r, deg){...} function Color(opacity) { return "rgba(" + color_gold + "," + opacity + ")"; } function draw(){...}
7-1 隨機產生敵人
首先創建一個長度為 10 的陣列,並且在裡面放入空的陣列,在圖A中可以看出 enemies
的值為一排空陣列。
接著透過 map 函數做陣列中元素的轉換,將原本空的陣列放入兩個值,分別為 x 與 y,其結果可在圖B中所見。而實際上我們需要創建的資訊一共有三個,分別是半經 r 、角度 deg 以及透明度 opacity,如圖C。
由於位置是隨機產生的,所以在半徑與角度上都是使用 Math.random()
,這裡值得一提的是,角度我們可以直接使用熟悉的 0~360 的角度系統,原因是在於我們使用先前已經寫好了 Point()
函數取得點的位置,而函數中也已經處理好角度轉換的問題了。
// 建立十個空物件的寫法 (圖A) var enemies = Array(10).fill({}) // 若建立十個物件,物件中 key 為 X、Y (圖B) var enemies = Array(10).fill({}).map( function (obj) { return { x: 5, y: 5 } }) // 我們要繪製敵人則是需要建立十個空陣列,物件中 key 為半徑 r、角度 deg、透明度 opacity (圖C) var enemies = Array(10).fill({}).map( function (obj) { return { r: Math.random() * 200, deg: Math.random() * 360, opacity: 1 } })
7-2 敵人在畫面上顯示
隨機產生出敵人後,接著就是將敵人依序顯示在畫面上。
除了使用 for 迴圈來去存取陣列中的物件外,另一種方式則是使用 forEach
,這裡使用 obj 來當作每一個元素的代稱,在每一個 obj 中都有三個屬性可做存取,分別是半徑 r 以及角度 deg以及透明度 opacity。由於這裡是要計算出點的位置,所以僅需要前面兩個值,再放入之前所創建的 Point 函數,便可得到該點所在的確切位置 obj_point
了。
敵人先以圓形來做為表示,要畫圓的方式是使用 arc ,其語法為
arc (中心點 x 位置,中心點 y 位置,圓的半徑,起始角度,終點角度)
由於是要畫完整的圓,所以在角度上設定為 0 至 2π。
//JavaScript function draw(){ ... enemies.forEach(function (obj) { //本體 ctx.fillStyle = Color(1); var obj_point = Point(obj.r, obj.deg); ctx.beginPath(); ctx.arc( obj_point.x, obj_point.y, 10, 0, 2 * Math.PI ); ctx.fill(); }) }
7-3 當線條角度與敵人一樣時,將敵人透明度變成 1
前面只是確認每一個點確實有被設定,而且都有顯示出來。接著要來實作掃略線的功能,當線條與敵人重疊時,敵人才會顯示,並在一段時間後再次消失。
首先要先定義掃略線,在前面的步驟中,定義了 time
這個變數,在每次執行一次 draw()
時,數值便會向上加 1,而且透過 time 定義角度後,將扇形掃略的效果所畫出來,我們要定義掃略線當下的位置,也是使用到 time 這個變數。
不過前面有提到 time 會隨著執行時間不斷地增加,但是角度的範圍僅限於 0~360,所以需要將 time 取 360 的餘數time % 360
,讓數值維持在 0~360 之間。
有了掃略線 line_deg
的角度後,接著就是跟敵人的角度來做比較,當兩者的角度差小於一定數值的時候,就代表兩者有重複到了,在這裡老闆以兩者設為兩者的距離取絕對值後小於 1 ,若是符合條件,便將透明度變為 1。而在敵人出現後,要讓他漸漸消失,等待下一次被掃略後出現,只需要將其透明度乘上一個小於 1 即可,透明度的值便會漸漸從 1 趨近於 0。
//JavaScript var enemies = Array(10).fill({}).map( function (obj) { return { r: Math.random() * 200, deg: Math.random() * 360, opacity: 0 // 1. 敵人透明度設為 0 } }) function draw() { var line_deg = time % 360; // 2. 定義掃略線 enemies.forEach(function (obj) { //本體 ctx.fillStyle = Color(obj.opacity); var obj_point = Point(obj.r, obj.deg); ctx.beginPath(); ctx.arc( obj_point.x, obj_point.y, 10, 0, 2 * Math.PI ); ctx.fill(); // 3. 判定掃略線與敵人位置 if (Math.abs(obj.deg - line_deg) <= 1) { obj.opacity = 1; } obj.opacity *= 0.99 }) }
7-4 修改敵人樣式
目前敵人的樣式上只有單個圓圈而已,顯得有些單調,我們想將敵人的符號加上叉叉,以及一個有動態向外擴展的圓圈。在開始之前,先將原先的圈圈大小做調整,將半徑為 10 大小縮小為 4。
叉叉為兩個線交疊而形成的,而線條的長短代表叉叉的大小。老闆先是定義了 x_size
這數值大小,這是繪製線條值所移動的距離,也代表叉叉的大小。兩條線分別是從左下至右上以及右下至左上,中心點的位置與圓圈同為 (obj_point.x obj_point.y)
,要移動至左下角時,在 X 座標與 Y 座標的值都是減去 x_size
,而右上角的點則是都加上 x_size
,其餘另外兩個點則以此類推。
接著來畫向外擴張的圈圈,這裡可以直接複製上面的開始畫圈圈的程式碼,並改成 strokeStyle
以線條的方式呈現。一個向外擴展的圈圈代表半徑的大小隨著時間在不斷變化的,在這裡當然是可以再寫一個變數來代表動態的半徑大小,但是其實我們已經有現成的變數可以使用,也就是透明度,所以不需要再額外寫一個。那就將半徑乘上 obj.opacity
吧,此時會發現圈圈反而是從外向內縮小,原因在於 obj.opacity
就是從 1 趨近於 0 漸漸越來越小的。
其解法就是將 1 除上 obj.opacity
後,當透明度值越小,所得到相對應的數值會越大,這裡要特別注意的是,由於在除法中除上 0 是沒有意義的,所以需要加上一個極小的數值來避免掉這樣的情況。
// 圈圈由外向內 ctx.arc(point.x, point.y, 20*opacity , 0, 2 * Math.PI); // 圈圈由內向外 ctx.arc(point.x, point.y, 20*(1/(obj.opacity + 0.001)); , 0, 2 * Math.PI);
以下是改變了實心圓大小、加上叉叉以及向外擴展圈圈的完成程式碼
enemies.forEach(function (obj) { //本體 ctx.fillStyle = Color(obj.opacity); var obj_point = Point(obj.r, obj.deg); ctx.beginPath(); ctx.arc( obj_point.x, obj_point.y, 4, 0, 2 * Math.PI // 1. 半徑縮小為 4 ); ctx.fill(); if (Math.abs(obj.deg - line_deg) <= 1) { obj.opacity = 1; } obj.opacity *= 0.99 // 2. 畫叉叉 ctx.strokeStyle = Color(obj.opacity); var x_size = 6; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(obj_point.x - x_size, obj_point.y - x_size); ctx.lineTo(obj_point.x + x_size, obj_point.y + x_size); ctx.moveTo(obj_point.x + x_size, obj_point.y - x_size); ctx.lineTo(obj_point.x - x_size, obj_point.y + x_size); ctx.stroke(); // 3. 往外消失的圓線 ctx.strokeStyle = Color(obj.opacity); // 線條是strokeStyle ctx.lineWidth = 1; var point = Point(obj.r, obj.deg); var r = 20 * (1 / (obj.opacity + 0.001)); ctx.beginPath(); ctx.arc(point.x, point.y, r , 0, 2 * Math.PI); ctx.stroke(); // 線條是stroke() }
八、修改左下角文字
在掃略線掃到敵人後,除了讓敵人顯現外,當然也要將敵人的位置標示出來。在判定掃略線與敵人碰觸到的判斷式中,用 jquery 的方式抓取左下角顯示文字的元素 .message
,再填上要顯示的文字,分別是距離中心的半徑長度以及角度。在預設上系統會顯示非常多位數的小數點,這裡可以透過 toFixed(3)
來限制小數點所呈現的位數,括號內的數值即代表要呈現小數點幾位數,這樣一來讓視覺上比較好看一些。
if (Math.abs(obj.deg - line_deg) <= 1){ obj.opacity=1; $(".message").text("Detected: "+obj.r.toFixed(3) +" at "+ obj.deg.toFixed(3) + "deg"); }
九、外圍刻度
在外圍的刻度上要先定義幾個四個變數來使用,分為是
split
: 將圓形切分的份數。feature
: 每隔幾度要以比較長的線條呈現,就像家裡時鐘整點位置的線條會長一點。tart_r
: 距離中心點的起始位置。外圍刻度要為在掃略線的外圍,所以數值比掃略線的 200 再多一些。len
: 線條的長度。
在角度的要特別注意需要轉換,deg=(i/split)*360
,這是因為在切分上只有切成了 120 份而非 360 份,所以 i
值並非代表角度,而是要先以i/split
判斷是第幾份,再將數值乘上 360 才會是正確的角度。後續則是將靠內側的點以及靠外側的點計算出來後,再相連就可以囉。
接著再使用判斷式 i % feature == 0
,也就是當每隔 15 個單位時,將線條的長度以及粗度都設定得比其他線條的數值更大一些。
function draw(){ ... ctx.strokeStyle=Color(1); var split=120; var feature=15; var start_r=230; var len=5; for(var i=0;i<split;i++){ ctx.beginPath(); // 角度要轉換成 360 var deg=(i/split)*360; // 如果在大刻度上就變粗變長 if (i % feature == 0) { len = 10; ctx.lineWidth = 3; } else { len = 5; ctx.lineWidth = 1; } // 轉換內側點以及外側點的位置 var point1=Point(start_r,deg); var point2=Point(start_r+len,deg); ctx.moveTo(point1.x,point1.y); ctx.lineTo(point2.x,point2.y); ctx.stroke(); } }
十、畫龍點睛的線條
剩下還有三個不同的線條需要繪製,分別是最內側的虛線、與掃略線半徑相同的實線,以及最外圍由兩個超過四分之一弧形所組成的外框。在最後一個步驟中,要嘗試使用函式帶入參數的方式來一次繪製三種不同的線條,這也是本次範例中相當精彩的地方。
由於一個圓為 360 度,所以設定一個 1 到 360 的 迴圈,並且透過先前寫過的點位置的函數轉換,轉換成 point,再將這 360個連接在一起。其中從外部所傳進來的參數 r 所代表的為半徑的大小。
function draw(){ ... function CondCircle(r) { ctx.beginPath(); ctx.strokeStyle = Color(1); for (var i = 0; i <= 360; i++) { var point = Point(r, i); ctx.lineTo(point.x, point.y); } ctx.stroke(); } CondCircle(300) }!
以上是實線的呈現方式,那虛線呢 ? 來看看下圖吧 !
想像拿的一支畫筆,當從每一個點前往下一個點的位置時,也就是 i 至i+1,可以選擇這一次是要用 lineTo 下筆畫線,還是用 moveTo 不要畫線只要移動就好。上面線條是的虛與實是剛好以 1:1 的方式繪製,那如果是要以其他比例,像是下圖呢 ?
假設圖片中所呈現有畫線與無畫線的比例是 4:1 ,那就是抓成五等分,有其中四分要畫線,而一份不須畫線,而這可以使用取餘數的方式來完成,下面實作就以每 180 度為一個區塊,在每一個區塊中其中的 90 度以畫線,另外的 90 度則不會畫線。
function draw(){ ... function CondCircle(r) { ctx.beginPath(); ctx.lineWidth = 1; ctx.strokeStyle = Color(1); for (var i = 0; i <= 360; i++) { var point = Point(r, i); if (i % 180 < 90) { ctx.lineTo(point.x, point.y); } else { ctx.moveTo(point.x, point.y); } } ctx.stroke(); } CondCircle(300) }!
有了使用餘數來畫線的概念後,接著就是要來集大成的時候了。要將決定畫不畫線的地方也就是 i % 180 < 90
改為傳送一個函式進來的方式來進行判斷,這裡命名為 func_cond
,另外增加一個可以設定線條粗度的變數lwidth
。
原本的四分之一弧形取餘數的數值不變,但而外加上了 time,使其可以產生出動態的效果。最內側的虛線則是每個區塊為三等份,其中一等分不畫線,所以回傳值為 (deg % 3) < 1
,而與掃略相同的圓圈是實線,代表每一條線條都要連起來,在回傳值上都是 true。
function draw(){ ... function CondCircle(r, lwidth, func_cond) { ctx.beginPath(); ctx.lineWidth = lwidth; ctx.strokeStyle = Color(1); for (var i = 0; i <= 360; i++) { var point = Point(r, i); if (func_cond(i)) { ctx.lineTo(point.x, point.y); } else { ctx.moveTo(point.x, point.y); } } ctx.stroke(); } // CondCircle(300) // 最外圍的四分之一弧形 CondCircle(300, 2, function (deg) { return ((deg + time / 10) % 180) < 90; }); // 最內側的虛線 CondCircle(100, 1, function (deg) { return (deg % 3) < 1; }); // 與掃略線相同長度的實線 CondCircle(200, 1, function (deg) { return true; }); }!
回顧
在本次範例中,可以發現一整個作品幾乎都是由 Canvas 所繪製出來的,就讓我們一起來回顧製作的流程吧
- 建構畫面上所需要的文字與本章主角 Canvas
- 抓取 Canvas 物件,並設定畫面長寬可隨頁面大小做更動
- 在 Canvas 繪製一個動態圖形,並調整底層背景樣式,讓所繪製圖形不會疊加在一起
- 正式開始繪製所需要要的圖形,從建立 X 軸與 Y 軸,再從單一的線條到以多個線條來形成掃描扇形
- 建立敵人系統是此次最重要的章節,隨機生成敵人的位置,並與掃略線做搭配,以及設定掃到敵人時所需要呈現的樣式
- 加上外圍刻度
- 以模組化的方式畫出中內外的線條三種不同的線條
透過這次範例讓我們見識到 Canvas 厲害之處,原來它真的就像畫筆一樣,可以畫出這麼美感與創意兼具的作品,想要看更多動態網頁、互動藝術作品,你可以加入老闆的動畫互動網頁程式入門 (HTML/CSS/JS)或是動畫互動網頁特效入門(JS/CANVAS),或來訂閱老闆 youtube 頻道吧!
此篇直播筆記由幫手 阮柏燁 協助整理