第 12 章 - 簡易的 Canvas 動畫
上次更新日期 2020/12/xx
Canvas 不只可以進行畫布的圖片繪製,也可以進行動畫的製作!而且效率還不錯。基本上,透過時間函數 setInterval 或 setTimeout 都可以製作動畫。同時,也能透過 .requestAnimatinFrame 這個方法來處理,都具有相當不錯的展示效果喔。
學習目標:
12.1: 用 Canvas 做簡單動畫
我們前一章簡單接觸了 Canvas 的繪圖,使用畫布的概念,就可以在某個位置上面進行繪圖了。既然如此, 如果我將這個畫布持續不斷的改變內容,當然就會變成一個動畫了!概念上面就是這麼簡單。那要如何改變內容呢? 很簡單啊!透過 setInterval 或者是 setTimeout 去呼叫某支函數,就可以達到這個功能了!
- 星空背景的輪轉
我們在前一章節裡面有處理到靜態的星空樣式,不過,這個樣式最終還是得要放置到遊戲當中。我們需要讓這個遊戲的背景可以有點像在移動, 那可以透過 setInterval 搭配陣列來處置即可,過程也不是這麼難啦!只要將計算所得的 x, y, r 值存放到一個二維陣列裡面, 很快就可以處理完畢了!
- 將 unit11-2-3.php 另存新檔為 unit12-1-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 修改 mymain 函數的相關內容:
- mycan 與 myfig 都會被沿用,所以不要加上 var 了,設計成為全域變數
- 設計 mystars = [] 設計為陣列,且為全域變數 (不要加上 var 即可)
- 在計算迴圈之前,加上 .save() 存放既有的設定,避免被迴圈內的其他動作影響到目前的畫布屬性
- 在計算星星位置的 for 迴圈內:
- 計算完畢 x, y, r 之後,宣告 mystars[i] = [] 做成二維陣列
- 讓 mystars[i][0] 是 x ,然後是 y 與 r,紀錄起來就可以了!
- 完成迴圈後,使用 .restore() 回復原本的參數
- 使用 timer = setInterval("showme()",100) 加入定期執行的函數
- 開始設計 showme() 函數的內容
- 重複清除畫布 (.fillRect(...)),讓畫布保持黑色底
- 設計 .save() 儲存畫布參數
- 將 mymain 的 for 迴圈內容整個貼過來,然後進行部份資料修改:
- 讓 x 成為 mystars[i][0];,同理,設計好 y 與 r 的內容
- 使用判斷式,如果 y 大於 300 時, y=y-300,讓星星從頭出現!
- 更新陣列值, mystars[i][1]=y 即可。
- 使用 .restore() 回復預設屬性
開始執行之後,畫布會一直更新,你就可以看到星星往下移動了!
- 將 unit11-2-5.php 另存新檔為 unit12-1-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 透過前一題的技巧,讓 canvas 畫布變成動態之後,放置成為遊戲區的底圖,讓底圖會一直變動~且內容隨機, 每次玩的時候,星空圖都不會一樣!你也可以修改一下 x, y 軸的增減效果,讓星星移動不見得是往下喔!可以隨便你自己變化!
透過這簡單的設計,你的遊戲動態處理起來,就有趣多了!不再是死死的樣子!
- 與時間有關的設計 - 使用變形移動位置也相當重要
與時間有關的設計,就用手錶當範例最好。一般手錶有時針、分針、秒針三個指針,然後又有刻度。我們繪製圖時,有幾個考量的點需要先注意:
- Canvas 以正右方為 0 度角,但是時鐘、手錶以正上方為 0 度角
- Canvas 以左上方座標為原點,但是時鐘、手錶以中心點 (圓心) 為原點
上面的問題我們可以透過 .translate 變更原點,以 .rotate 變更角度即可。那至於時針、分針、秒針的設計呢?假設整個圓是 1 , 那麼時針、分針、秒針對這個圓有什麼意義呢?當時針為 hr、分針為 min 而秒針為 sec 變數時:
- 時針會在 (hr+min/60+sec/3600)/12 的位置上;
- 分針會在 (min+sec/60)/60 的位置上
- 秒針會在 (sec/60) 的位置上。
接下來,就讓我們來了解一下手錶的設計:
- 建立新檔為 unit12-1-3.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- HTML 原始碼的部份,加入一個 h1 標籤,加入一個 canvas 標籤且 id 為 mycan 即可。
- 當網頁載入完成後,會立刻執行 mymain 函數
- mymain 函數會執行 myclock 函數
- 設計 myclock 函數內容:
- 先取得現在的時間:
- 設計名為 now 的變數,內容為 Date() 取得的時間物件
- 設計 hh 變數,內容為 now 的 .getHours() 方法,會是一個 0~23 的數值
- 與 hh 相似,設計 mm, ss 分別為分 (0~59) 與秒 (0~59) 的數值
- 開始設計 canvas 畫布:
- 設計 mycan 變數,內容為取得 mycan ID 那個元素的控制權
- 設計 myfig 變數,內容為 mycan 的畫布功能
- 讓 mycan 的寬度與高度均為 400 像素,且擁有 1px solid gray 的框線
- 設計一些鐘錶相關的設定值,比較重要的統一設定:
- 以 .translate(200,200) 讓原點移動到正中央,記得 200 的原因是因為前面設定 400 的方塊寬高之故。
- 以 .rotate(-0.5*Math.PI) 讓 0 度角逆時針轉動 90 度,因此正上方會是 0 度角。
- 設計線段的形式為 round 圓角
- 開始設計鐘錶最外框,定義出錶面的範圍:
- 先用 .save() 儲存預設的環境
- 框線設定為 10 像素
- 畫圓形,因為原點已經在中央,因此在座標 (0,0),畫出半徑 120 的圓 ( 0~2π 角度),畫出框線即可
- 使用 .restore() 恢復預設環境設定。
- 開始設計 60 個小刻度,很簡單,就是每隔 2π/60 度角畫一個小線段,就會成為外圍刻度了:
- 先用 .save() 儲存預設的環境
- 設計框線厚度為 3 像素
- 設計一個 for 迴圈,跑 60 次,內容為 (1)開始路徑 (2)旋轉角度 2*π/60 (3)向右移動到 105 像素,亦即 (105,0) 的座標 (4)劃線到 (100,0) 座標,亦即畫一個 5 像素的短線 (5)結束路徑 (6)畫框
- 使用 .restore() 恢復預設環境設定。
- 完成上述的資料後,就有小刻度,現在來畫出時針的 12 大刻度
- 先用 .save() 儲存預設的環境
- 設計框線厚度為 5 像素
- 設計一個 for 迴圈,跑 12 次,內容為 (1)開始路徑 (2)旋轉角度 2*π/12 (3)向右移動到 105 像素,亦即 (105,0) 的座標 (4)劃線到 (85,0) 座標,亦即畫一個 20 像素的短線 (5)結束路徑 (6)畫框
- 使用 .restore() 恢復預設環境設定。
- 接下來可以開始繪製時針了:
- 因為時針是 12 小時制,因此,若 hh 大於等於 12 時, hh 需要減去 12 才行
- 先用 .save() 儲存預設的環境
- 設計 rate 變數,內容為 (hh+mm/60+ss/3600)/12 ,亦即時針在一個圓上面所轉動的比例,例題上面說明解釋的項目相同。
- 設計旋轉角度為 2*π*rate 這樣的角度
- 設計線寬為 10 像素
- (1)開始路徑 (2)向左移動到 (-20,0) (3)劃線到 (70,0) 的位置,所以會有 90 像素的寬線段 (4)結束路徑
- 設計線段樣式為深藍色,然後劃線
- 使用 .restore() 恢復預設環境設定。
- 接下來描繪分針與秒針,使用與時針相同的方式,但是 rate 計算角度不同,而且分針應該要 110 像素長,秒針應該要 125 像素長。
- 先取得現在的時間:
- 如果錶面可以正確的呈現出目前的時間,那麼就可以在 mymain 函數內加上每秒鐘執行一次的 setInterval 功能,這樣你的鐘錶就能運作了!
12.2: 加上音效
動畫或遊戲,通常需要有點聲音比較好玩!舉例來說,剛剛我們設計的鐘錶畫面,如果轉針開始動的時候,就有聲音的話, 不就很生動了!很好,這時需要使用 audio 這個效果來處理。不過,很可惜的是,目前 chrome 的設計中,要讓使用者開啟音效才有辦法發音, 否則為了避免干擾使用者,因此預設所有的聲音都是靜音狀態喔!
- 加上音效的方法
要憑空加入音效,可以使用底下的方式處理即可:
myclick = new Audio("images/xxx.mp3"); myclick.play();
只是,這個 play() 應該是『不會運作』的!如前所述,你的 chrome 瀏覽器預設應該不會啟用音效功能。那如何處理? 還好啦,就讓使用者同意播放即可!底下我們做個練習來瞧一瞧你就知道了。
- 將 unit12-1-3.php 另存新檔為 unit12-2-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- HTML 原始碼的部份,加入類似底下的字樣處理
<p><button onclick="openaudio()">開啟音效</button></p>
- 增加名為 openaudio() 的函數,內容大致上就如同上面的設計,你可以依據文末的連結去下載音效,或者直接 從這裡下載 一個簡單的音效
- 在 myclick() 函數的最底下,加上這行指令:
if ( typeof(laser) != 'undefined' ) laser.play();
一開始是沒有音效的,因此,你不能將 laser.play() 直接加到 myclock() 裡面去,否則 myclock 分析到該段程式碼,可能就會終止... 當然可以使用 try 來處理,不過,還是主動加判斷式比較單純。透過使用者 click 的功能,就可以順利的建立好 laser 變數,然後開始發音...
- 射擊遊戲
玩射擊遊戲沒有音效怎麼可以?所以,請自行前往你自己喜歡的音效網下載短音效,一個是發射砲彈,一個是炸裂 UFO 的音效。 你也可以從底下的例題中去下載。但是對於音效,我們應該是需要理解一下!如果同一個音效被同時播放時,可能會有一些衝突的問題。 因此,最簡單的方法,就是讓該音效停止,並且將播放位置調回開始處,重新撥一次,就沒問題!類似這樣:
laser.pause(); laser.currentTime = 0; laser.play();
如果需要聲音一直循環播放 (例如背景音樂),就得要使用底下的屬性了:
laster.loop = true;
- 將 unit12-1-2.php 另存新檔為 unit12-2-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 你可以下載發砲聲與爆炸聲與水聲,然後放置到你的網頁上,準備等等來應用。
- 在 mymain 函數裡面加上 laser, bomb, water 等 3 個變數,都是 Audio 物件,內容分別是上述的砲聲, 爆炸聲與背景音。
- 在 firenow 函數的最底下,以上述三行的方式,讓發射砲彈產生聲音。
- 在 hit = 1 的情境下,以上述三行的方式,讓爆炸產生聲音。
- 背景音比較特別,因為還是得要讓使用者按下鍵盤開始玩遊戲之後,才給予持續不斷的背景音樂,否則就不播放。
因此,應該是要在 kdown() 函數底下進行,而且,需要的動作是:
- 判斷 water.loop 是否不等於 true
- 若是不等於 true 時,則設定 water.loop 為 true 之後,開始播放。
接下來玩玩看,你就會發現遊戲變得有趣多了!甚至你也應該要加入過關音樂等等~讓你的遊戲變得生動活潑些!
12.3: 一些動畫範例
還有一些很特別的動畫也可以簡單的透故 Canva 來製作,舉例來說,製作太陽、地球、月亮的運轉關係,可以簡單的這樣繪製喔:
- 首先,在一個正方形的空間,讓原點移動到中央的部份,這樣就能夠以太陽為中心點來定為了。
- 再來,開始設計正中央的太陽,讓這個黃色圓具有陰影,就會有點像太陽光!
- 然後設計地球軌道示意圖,這時需要使用框線而不是填滿的設計
- 進行地球的旋轉偏移 (這時需要以太陽為原點中心,所以這裡先旋轉)
- 讓原點移動到太陽正右方的軌道點上面,這樣就可以在這個原點上面設計地球的相關資料
- 設計藍色圓,就可以假裝為地球
- 設計月球軌道
- 進行月球的旋轉偏移 (這時則是以地球為中心去旋轉了!)
- 將原點再次移動到軌道上,就可以設計月球了。
基本的設計概念就是這樣,都與前一章談到的『變形』關係很高!因為我們要做的是『動畫』的效果,在這裡我們預計讓地球繞太陽一圈要花 60 秒, 也就是一分鐘才轉一圈,你當然可以每一秒鐘轉 2π/60 這樣,不過就會顯得很不連續。因此,我們連毫秒也拿出來用,因此,每個一分鐘內的秒數, 就會有 second + millisecond 的模樣。因此,旋轉的角度就會是 second/60 + millisecond/60000 這樣!你得知道, millisecond 是 0~999 毫秒, 轉成秒鐘,就得要除以 1000 啊!
- 建立新檔 unit12-3-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 在 body 內,新增一個 h1 的標題,以及一個 canvas 的標籤,同時給予 id 為 mycan
- 當整個網頁載入之後,就會主動執行 mymain 函數
- 在 mymain 函數內,執行一次 mysolar() 函數即可。
- 設計 mysolar 函數:
- 設計數個等等會用到的時間參數:
- 設計名為 now 的變數,內容為 new Date() 函數的結果,會取得目前的時間。
- 設計一分鐘內轉動的圓速率,rate1 為地球公轉一圈,內容為 new.getSeconds()/60 + now.getMillisecond()/60000
- 設計月球公轉速率 rate2,內容為 rate1 * 365.25 / 27.323,其中 365.25 為地球公轉一圈的日數,27.323 為月球公轉一圈的日數。
- 設計畫布相關的資料
- 設計名為 mycan 的變數,內容為取得 mycan 這個 canvas 標籤的控制權
- 設計名為 myfig 的變數,內容為 canvas 的畫布內容
- 設計 mycan 的寬度與高度,都是 400 像素,一個方方正正的方塊
- 先給予框線,比較好知道位置所在
- 填滿 (0,0) 到 (400,400) 為黑色的矩形空間,因為是宇宙,所以是黑色!
- 改變原點到 (200,200) 這個位置上 (.translate 的用途)
- 開始設計正中央的太陽部份,包括圓形、陰影、黃色等樣式,以及地球軌道的繪製:
- 先用 .save() 將預設的狀態儲存起來
- 以座標 (0,0) 繪製 10 像素半徑的圓
- 給予陰影模糊到 15 像素的模樣 (像素值請自訂)
- 給予陰影顏色為黃色
- 填滿黃色,就得到正中央的太陽模樣。
- 回復預設值 (.restore) 避免陰影的干擾
- 開始新路徑描繪
- 在圓心 (0,0) 給予 130 像素的半徑,繪製一個圓
- 給予框線 (strokeStyle) 為灰色,越淡越好
- 繪製框線
- 繪製地球
- 先讓地球以太陽為中心,旋轉 (2π*rate1) 的角度
- 將圓心向右移動到 (130, 0) 這個地球軌道上
- 開始新路徑繪製
- 在圓心 (0,0) 的地方,繪製一個 5 像素半徑的圓
- 結束新路徑繪製
- 填滿一個藍色的圓
- 開始新路徑繪製
- 圓心 (0,0) 繪製一個 20 像素半徑的圓
- 填滿框線為灰色的圓,作為月球軌道示意圖
- 繪製月球
- 先讓月球以地球為中心,旋轉 (2π*rate2) 的角度
- 將圓心向右移動到 (20,0) 這個月球的軌道上
- 開始新路徑的繪製
- 在圓心 (0,0) 的位置上面繪製一個半徑 3 像素的圓
- 結束路徑繪製
- 填滿銀色 (silver) 的圓球
- 設計數個等等會用到的時間參數:
- 若一切順利,在 mymain 裡面,增加 setInterval 的函數來每 0.05 秒執行一次 mysolar 函數即可。
- 使用 window.requestAnimationFrame(functionname) 取代時間參數
上面的例題中,我們使用的是每 0.05 秒跑一次函數,因此,每秒鐘會更新畫布次數為 (1/0.05 = 20) 幅。後來的 canvas 提供另一個模式, 就是 requestAnimationFrame 這個東東!這個玩意兒可以在每秒鐘更新達到 60 幅的畫布更新次數,而且不會耗用太多系統資源! 使用的方法很簡單,就在繪製圖示的最後面 (以上面來說,使用 mysolar() 函數的最後一行) 加上即可! 至於 functionname 就是繪製的名稱,亦即是 mysolar 之意。
// 先移除 setInterverl 的設計
function mymain() {
mysolar();
// timer = setInterval("mysolar()", 50);
}
// 這時才加入 window.requestAnimationFrame
function mysolar() {
....
window.requestAnimationFrame(mysolar);
}
- 產生圓餅圖的計算
有時候,我們可能需要輸出使用者需要的圓餅圖,是即時需要的,不是固定的數值。這個時候可以透過使用者輸入表單,然後, 透過 Canvas 就可以處理掉圓餅圖的設計了!想法很簡單:
- 讓使用者輸入所需要的輸入框數量
- 讓使用者在輸入框填入需要的數值資訊
- 產生圓餅圖:
- 取得所有的數值
- 將所有的數值加總
- 將每一筆資料算比例
- 以迴圈設計扇形圖案,且設計完成之後,務必要進行適當的旋轉。
- 建立新檔 unit12-3-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 將 這個檔案的內容貼上來
- 建立 createinput() 函數內容:
- 取得 ninput 的數值
- 根據 ninput 的數值設計迴圈,建立出需要的輸入框,輸入框的樣式如下:
輸入總額:<input type="text" name="myval" value="10" > 元
- 最終將輸入框的資料加入到 showinput 元件的 innerHTML 內。
- 設計 setcolor() 函數,目的在產生 r, g, b 三顏色
- 分別設計 r, g, b 三種強度,每種強度設計都一樣,取得 0~255 之間的整數即可。
- 設計 showme() 函數:
- 設計 mycan 變數,內容為取得 mycan 元素控制權
- 設計 myinp 變數,內容為取得 myval 元素控制權
- 設計 mytotal 變數,數值預設為 0,目的為取得所有數值的總和
- 以迴圈的方式,以 myinp 的數量為迴圈最大值,取對 myinp 的數值加總,加總的結果為 mytotal
- 設計 myratio 為陣列
- 以迴圈的方式,以 myinp 的數量為迴圈最大值,以 myinp 的數值除以 mytotal 來設計 myratio 的數值,亦即每個輸入框的佔比
- 設計 mycan 的寬、高都是 400 像素
- 設計 myfig 變數,內容即是 mycan 的畫布特性
- 變動原點到 200, 200 正中央座標
- 旋轉角度,讓 0 度變成在畫面正上方 (向左邊逆時針轉 90 度角)
- 進入迴圈,以 myratio 這個陣列的數值為最大值來設計:
- 執行 setcolor() 取得顏色 r, g, b 三個數值
- 以 .beginPath() 開始路徑繪製
- 將座標移動到 (0,0) 原點
- 設計扇形,座標 (0,0),半徑 150 像素,由 0 度到 myratio*2*Math.PI 的角度
- 結束路徑繪製
- 填滿的格式樣式為 rgb 的顏色 (帶入 r, g, b)
- 線條格式為黑色
- 填滿與畫框線
- 旋轉 myratio*2*Math.PI 角度。
...
12.4: 課後作業
動態圖示大部分還是與 setInterval 有關啦!如果要加上其他的互動機制,只要透過 .addEventListener 之類的方法來處理即可。
- 12-4-1、手錶錶面資料更豐富
將剛剛 unit12-1-3.php 的檔案拿出來改,加上月份、星期與日期的資料,讓你的錶面顯示可以更加豐富!一眼看出正確資訊! 最終完成圖要有點像這樣:
- 12-4-2、射飛碟遊戲更生動
拿出第九章 9-5-2 的飛碟遊戲,以 12-2-2 的例題為基準,使用之前多重飛碟的處理效果,去重新設計你的遊戲畫面。 這次請加上許多特效:
- 重新描繪砲台,讓砲台也是使用 canvas 的圖畫,可以直接拿前一章的飛機圖示來處理
- 可以的話,自行上網找尋你喜歡的音效來取代
- 如果砲台被打爆,請關閉背景音效,然後播放失敗音效。
- 如果你清理完畢戰場 (打掉所有的飛碟,且來不及產生新飛碟),關閉背景音效,播放過關音效。
12.5: 參考資料
- 一個簡單的 Canvas 與 SVG 比較說明:https://css-tricks.com/when-to-use-svg-vs-when-to-use-canvas/
- CSS、Canvas、SVG 動畫介紹:https://animateyourhtml5.appspot.com/pres/index.html?lang=en#1
- http://wiki.cs.vsb.cz/images/5/5d/TAMZ-new-L6.pdf
- 基礎動畫:https://developer.mozilla.org/zh-TW/docs/Web/API/Canvas_API/Tutorial/Basic_animations
- 免費音效下載: https://taira-komori.jpn.org/freesoundtw.html
- https://codepen.io/jlong64/pen/jwJpc
- https://cloudinary.com/blog/creating_html5_animations