JavaScript CSS3

Javascript 動態網頁程式設計 - 上課教材

動畫互動網頁程式設計 > 課程內容 > 第 12 章 - 簡易的 Canvas 動畫

第 12 章 - 簡易的 Canvas 動畫

上次更新日期 2020/12/xx

Canvas 不只可以進行畫布的圖片繪製,也可以進行動畫的製作!而且效率還不錯。基本上,透過時間函數 setInterval 或 setTimeout 都可以製作動畫。同時,也能透過 .requestAnimatinFrame 這個方法來處理,都具有相當不錯的展示效果喔。

學習目標:

12.1: 用 Canvas 做簡單動畫

我們前一章簡單接觸了 Canvas 的繪圖,使用畫布的概念,就可以在某個位置上面進行繪圖了。既然如此, 如果我將這個畫布持續不斷的改變內容,當然就會變成一個動畫了!概念上面就是這麼簡單。那要如何改變內容呢? 很簡單啊!透過 setInterval 或者是 setTimeout 去呼叫某支函數,就可以達到這個功能了!

  • 星空背景的輪轉

我們在前一章節裡面有處理到靜態的星空樣式,不過,這個樣式最終還是得要放置到遊戲當中。我們需要讓這個遊戲的背景可以有點像在移動, 那可以透過 setInterval 搭配陣列來處置即可,過程也不是這麼難啦!只要將計算所得的 x, y, r 值存放到一個二維陣列裡面, 很快就可以處理完畢了!

例題 12-1-1: 讓星空圖示可以移動,主要是向下移動,讓砲台有點像是往上跑的感覺
  1. 將 unit11-2-3.php 另存新檔為 unit12-1-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 修改 mymain 函數的相關內容:
    • mycan 與 myfig 都會被沿用,所以不要加上 var 了,設計成為全域變數
    • 設計 mystars = [] 設計為陣列,且為全域變數 (不要加上 var 即可)
    • 在計算迴圈之前,加上 .save() 存放既有的設定,避免被迴圈內的其他動作影響到目前的畫布屬性
    • 在計算星星位置的 for 迴圈內:
      • 計算完畢 x, y, r 之後,宣告 mystars[i] = [] 做成二維陣列
      • 讓 mystars[i][0] 是 x ,然後是 y 與 r,紀錄起來就可以了!
    • 完成迴圈後,使用 .restore() 回復原本的參數
    • 使用 timer = setInterval("showme()",100) 加入定期執行的函數
  3. 開始設計 showme() 函數的內容
    • 重複清除畫布 (.fillRect(...)),讓畫布保持黑色底
    • 設計 .save() 儲存畫布參數
    • 將 mymain 的 for 迴圈內容整個貼過來,然後進行部份資料修改:
      • 讓 x 成為 mystars[i][0];,同理,設計好 y 與 r 的內容
      • 使用判斷式,如果 y 大於 300 時, y=y-300,讓星星從頭出現!
      • 更新陣列值, mystars[i][1]=y 即可。
    • 使用 .restore() 回復預設屬性

開始執行之後,畫布會一直更新,你就可以看到星星往下移動了!

例題 12-1-2: 將剛剛的技巧放置到射擊遊戲上
  1. 將 unit11-2-5.php 另存新檔為 unit12-1-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 透過前一題的技巧,讓 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) 的位置上。

接下來,就讓我們來了解一下手錶的設計:

例題 12-1-3: 設計一個手錶表面與運作相關
  1. 建立新檔為 unit12-1-3.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. HTML 原始碼的部份,加入一個 h1 標籤,加入一個 canvas 標籤且 id 為 mycan 即可。
  3. 當網頁載入完成後,會立刻執行 mymain 函數
  4. mymain 函數會執行 myclock 函數
  5. 設計 myclock 函數內容:
    1. 先取得現在的時間:
      • 設計名為 now 的變數,內容為 Date() 取得的時間物件
      • 設計 hh 變數,內容為 now 的 .getHours() 方法,會是一個 0~23 的數值
      • 與 hh 相似,設計 mm, ss 分別為分 (0~59) 與秒 (0~59) 的數值
    2. 開始設計 canvas 畫布:
      • 設計 mycan 變數,內容為取得 mycan ID 那個元素的控制權
      • 設計 myfig 變數,內容為 mycan 的畫布功能
      • 讓 mycan 的寬度與高度均為 400 像素,且擁有 1px solid gray 的框線
    3. 設計一些鐘錶相關的設定值,比較重要的統一設定:
      • 以 .translate(200,200) 讓原點移動到正中央,記得 200 的原因是因為前面設定 400 的方塊寬高之故。
      • 以 .rotate(-0.5*Math.PI) 讓 0 度角逆時針轉動 90 度,因此正上方會是 0 度角。
      • 設計線段的形式為 round 圓角
    4. 開始設計鐘錶最外框,定義出錶面的範圍:
      • 先用 .save() 儲存預設的環境
      • 框線設定為 10 像素
      • 畫圓形,因為原點已經在中央,因此在座標 (0,0),畫出半徑 120 的圓 ( 0~2π 角度),畫出框線即可
      • 使用 .restore() 恢復預設環境設定。
    5. 開始設計 60 個小刻度,很簡單,就是每隔 2π/60 度角畫一個小線段,就會成為外圍刻度了:
      • 先用 .save() 儲存預設的環境
      • 設計框線厚度為 3 像素
      • 設計一個 for 迴圈,跑 60 次,內容為 (1)開始路徑 (2)旋轉角度 2*π/60 (3)向右移動到 105 像素,亦即 (105,0) 的座標 (4)劃線到 (100,0) 座標,亦即畫一個 5 像素的短線 (5)結束路徑 (6)畫框
      • 使用 .restore() 恢復預設環境設定。
    6. 完成上述的資料後,就有小刻度,現在來畫出時針的 12 大刻度
      • 先用 .save() 儲存預設的環境
      • 設計框線厚度為 5 像素
      • 設計一個 for 迴圈,跑 12 次,內容為 (1)開始路徑 (2)旋轉角度 2*π/12 (3)向右移動到 105 像素,亦即 (105,0) 的座標 (4)劃線到 (85,0) 座標,亦即畫一個 20 像素的短線 (5)結束路徑 (6)畫框
      • 使用 .restore() 恢復預設環境設定。
    7. 接下來可以開始繪製時針了:
      • 因為時針是 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() 恢復預設環境設定。
    8. 接下來描繪分針與秒針,使用與時針相同的方式,但是 rate 計算角度不同,而且分針應該要 110 像素長,秒針應該要 125 像素長。
  6. 如果錶面可以正確的呈現出目前的時間,那麼就可以在 mymain 函數內加上每秒鐘執行一次的 setInterval 功能,這樣你的鐘錶就能運作了!
鐘錶處理

12.2: 加上音效

動畫或遊戲,通常需要有點聲音比較好玩!舉例來說,剛剛我們設計的鐘錶畫面,如果轉針開始動的時候,就有聲音的話, 不就很生動了!很好,這時需要使用 audio 這個效果來處理。不過,很可惜的是,目前 chrome 的設計中,要讓使用者開啟音效才有辦法發音, 否則為了避免干擾使用者,因此預設所有的聲音都是靜音狀態喔!

  • 加上音效的方法

要憑空加入音效,可以使用底下的方式處理即可:

myclick = new Audio("images/xxx.mp3");
myclick.play();

只是,這個 play() 應該是『不會運作』的!如前所述,你的 chrome 瀏覽器預設應該不會啟用音效功能。那如何處理? 還好啦,就讓使用者同意播放即可!底下我們做個練習來瞧一瞧你就知道了。

例題 12-2-1: 指針轉動產生音效
  1. 將 unit12-1-3.php 另存新檔為 unit12-2-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. HTML 原始碼的部份,加入類似底下的字樣處理
    <p><button onclick="openaudio()">開啟音效</button></p>
    
  3. 增加名為 openaudio() 的函數,內容大致上就如同上面的設計,你可以依據文末的連結去下載音效,或者直接 從這裡下載 一個簡單的音效
  4. 在 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;
例題 12-2-2: 讓遊戲有聲音
  1. 將 unit12-1-2.php 另存新檔為 unit12-2-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 你可以下載發砲聲爆炸聲水聲,然後放置到你的網頁上,準備等等來應用。
  3. 在 mymain 函數裡面加上 laser, bomb, water 等 3 個變數,都是 Audio 物件,內容分別是上述的砲聲, 爆炸聲與背景音。
  4. 在 firenow 函數的最底下,以上述三行的方式,讓發射砲彈產生聲音。
  5. 在 hit = 1 的情境下,以上述三行的方式,讓爆炸產生聲音。
  6. 背景音比較特別,因為還是得要讓使用者按下鍵盤開始玩遊戲之後,才給予持續不斷的背景音樂,否則就不播放。 因此,應該是要在 kdown() 函數底下進行,而且,需要的動作是:
    • 判斷 water.loop 是否不等於 true
    • 若是不等於 true 時,則設定 water.loop 為 true 之後,開始播放。

接下來玩玩看,你就會發現遊戲變得有趣多了!甚至你也應該要加入過關音樂等等~讓你的遊戲變得生動活潑些!

12.3: 一些動畫範例

還有一些很特別的動畫也可以簡單的透故 Canva 來製作,舉例來說,製作太陽、地球、月亮的運轉關係,可以簡單的這樣繪製喔:

  • 首先,在一個正方形的空間,讓原點移動到中央的部份,這樣就能夠以太陽為中心點來定為了。
  • 再來,開始設計正中央的太陽,讓這個黃色圓具有陰影,就會有點像太陽光!
  • 然後設計地球軌道示意圖,這時需要使用框線而不是填滿的設計
  • 進行地球的旋轉偏移 (這時需要以太陽為原點中心,所以這裡先旋轉)
  • 讓原點移動到太陽正右方的軌道點上面,這樣就可以在這個原點上面設計地球的相關資料
  • 設計藍色圓,就可以假裝為地球
  • 設計月球軌道
  • 進行月球的旋轉偏移 (這時則是以地球為中心去旋轉了!)
  • 將原點再次移動到軌道上,就可以設計月球了。

基本的設計概念就是這樣,都與前一章談到的『變形』關係很高!因為我們要做的是『動畫』的效果,在這裡我們預計讓地球繞太陽一圈要花 60 秒, 也就是一分鐘才轉一圈,你當然可以每一秒鐘轉 2π/60 這樣,不過就會顯得很不連續。因此,我們連毫秒也拿出來用,因此,每個一分鐘內的秒數, 就會有 second + millisecond 的模樣。因此,旋轉的角度就會是 second/60 + millisecond/60000 這樣!你得知道, millisecond 是 0~999 毫秒, 轉成秒鐘,就得要除以 1000 啊!

例題 12-3-1: 太陽、地球、月球的軌道與動畫示意
  1. 建立新檔 unit12-3-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 在 body 內,新增一個 h1 的標題,以及一個 canvas 的標籤,同時給予 id 為 mycan
  3. 當整個網頁載入之後,就會主動執行 mymain 函數
  4. 在 mymain 函數內,執行一次 mysolar() 函數即可。
  5. 設計 mysolar 函數:
    1. 設計數個等等會用到的時間參數:
      • 設計名為 now 的變數,內容為 new Date() 函數的結果,會取得目前的時間。
      • 設計一分鐘內轉動的圓速率,rate1 為地球公轉一圈,內容為 new.getSeconds()/60 + now.getMillisecond()/60000
      • 設計月球公轉速率 rate2,內容為 rate1 * 365.25 / 27.323,其中 365.25 為地球公轉一圈的日數,27.323 為月球公轉一圈的日數。
    2. 設計畫布相關的資料
      • 設計名為 mycan 的變數,內容為取得 mycan 這個 canvas 標籤的控制權
      • 設計名為 myfig 的變數,內容為 canvas 的畫布內容
      • 設計 mycan 的寬度與高度,都是 400 像素,一個方方正正的方塊
      • 先給予框線,比較好知道位置所在
      • 填滿 (0,0) 到 (400,400) 為黑色的矩形空間,因為是宇宙,所以是黑色!
    3. 改變原點到 (200,200) 這個位置上 (.translate 的用途)
    4. 開始設計正中央的太陽部份,包括圓形、陰影、黃色等樣式,以及地球軌道的繪製:
      • 先用 .save() 將預設的狀態儲存起來
      • 以座標 (0,0) 繪製 10 像素半徑的圓
      • 給予陰影模糊到 15 像素的模樣 (像素值請自訂)
      • 給予陰影顏色為黃色
      • 填滿黃色,就得到正中央的太陽模樣。
      • 回復預設值 (.restore) 避免陰影的干擾
      • 開始新路徑描繪
      • 在圓心 (0,0) 給予 130 像素的半徑,繪製一個圓
      • 給予框線 (strokeStyle) 為灰色,越淡越好
      • 繪製框線
    5. 繪製地球
      • 先讓地球以太陽為中心,旋轉 (2π*rate1) 的角度
      • 將圓心向右移動到 (130, 0) 這個地球軌道上
      • 開始新路徑繪製
      • 在圓心 (0,0) 的地方,繪製一個 5 像素半徑的圓
      • 結束新路徑繪製
      • 填滿一個藍色的圓
      • 開始新路徑繪製
      • 圓心 (0,0) 繪製一個 20 像素半徑的圓
      • 填滿框線為灰色的圓,作為月球軌道示意圖
    6. 繪製月球
      • 先讓月球以地球為中心,旋轉 (2π*rate2) 的角度
      • 將圓心向右移動到 (20,0) 這個月球的軌道上
      • 開始新路徑的繪製
      • 在圓心 (0,0) 的位置上面繪製一個半徑 3 像素的圓
      • 結束路徑繪製
      • 填滿銀色 (silver) 的圓球
  6. 若一切順利,在 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 就可以處理掉圓餅圖的設計了!想法很簡單:

  • 讓使用者輸入所需要的輸入框數量
  • 讓使用者在輸入框填入需要的數值資訊
  • 產生圓餅圖:
    • 取得所有的數值
    • 將所有的數值加總
    • 將每一筆資料算比例
    • 以迴圈設計扇形圖案,且設計完成之後,務必要進行適當的旋轉。
例題 12-3-2: 互動產生圓餅圖
  1. 建立新檔 unit12-3-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案的內容貼上來
  3. 建立 createinput() 函數內容:
    • 取得 ninput 的數值
    • 根據 ninput 的數值設計迴圈,建立出需要的輸入框,輸入框的樣式如下:
      輸入總額:<input type="text" name="myval" value="10" > 元
      
    • 最終將輸入框的資料加入到 showinput 元件的 innerHTML 內。
  4. 設計 setcolor() 函數,目的在產生 r, g, b 三顏色
    • 分別設計 r, g, b 三種強度,每種強度設計都一樣,取得 0~255 之間的整數即可。
  5. 設計 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: 參考資料