Canvas 2D 學習筆記

前言

HTML5 的 canvas 元素就像是網頁上的一塊畫布,利用 JavaScript 可以在這個元素上繪圖、合成照片,並且透過時間的改變在畫布上繪製不同圖形可以建立動畫...等等,canvas 元素最早是由 Apple 為 Mac OS X Dashboard 所提出,目前所有主流瀏覽器都已支援。

canvas 元素由 HTML 標準定義,W3C 還制定了 HTML Canvas 2D Context 標準,該標準規定在 canvas 元素上繪圖的 API,透過 API 公開的方法和屬性就可以實現繪圖,這個 API 核心是 CanvasRenderingContext2D 物件的使用,CanvasRenderingContext2D 可以透過表示 canvas 元素的 DOM 物件的 getContext() 方法,並直接把字串 “2d” 作為唯一的參數傳遞給它而取得。

首先,瞭解一下動畫基本原理,動畫以人類視覺的原理為基礎,如果快速檢視一系列相關的靜態影像,那麼會感覺到這是一個連續的運動。動畫的基本原理與電影的原理相同,都是利用人類視覺的「視覺殘留」,人眼在某個物像消失後,仍可使該物像在視網膜上滯留 0.1~0.4 秒左右,電影膠卷以每秒 24 格畫面勻速轉動,一系列靜態畫面會因視覺殘留作用造成一種連續的視覺印象,產生逼真動畫。

  1基本用法

                    <canvas id="tutorial" width="150" height="150"></canvas>
                
<canvas> 只有 width 與 height 這兩個屬性,這兩個屬性非必須、能透過 DOM 屬性設定,若沒有設定,預設寬為 300px、高為 150px,可以用 CSS 強制設定元素尺寸,當渲染時,影像會縮放以符合元素尺寸。
*如果繪圖結果看起來有些扭曲,可以試著修改 <canvas> 自身的寬高

當沒有套用樣式規定時,<canvas> 會被初始成全透明。

錯誤時回應的替代內容(Fallback content)

因為舊版瀏覽器(特別是 IE9 以前)都不支援 canvas 元素,當不支援 <canvas> 瀏覽器時會直接忽略 <canvas>,因此在 <canvas> 下瀏覽器認識的替代內容則會被瀏覽器解析顯示,所以可以準備一段 canvas 內容的說明文字或 canvas 繪圖完成後的靜態圖片:
                    <canvas id="stockGraph" width="150" height="150">
                        瀏覽器如果不支援 canvas 元素,就顯示這行文字
                    </canvas>
                    
                    <canvas id="stockGraph" width="150" height="150">
                        <img src="images/clock.png width="150" height="150">
                    </canvas>
                

渲染環境(rendering context)

canvas 元素產生一個固定大小的繪圖畫布,這個畫布有一或多個渲染環境,不同環境可能會提供不同型態的渲染方式,例如 WebGL 使用 OpenGL ES 的 3D 環境,這裡我們主要討論 2D 渲染環境,getContext() 方法是傳回一個用於在畫布上繪圖的上下文環境,參數 contextID 指定了想要在畫布上繪製的影像類型,當傳遞字串 “2d” 意思是進行二維繪圖,就會導致這個方法傳回一個上下文物件 CanvasRenderingContext2D 物件,該物件定義二維繪圖 API:
                    var canvas = document.getElementById('tutorial');
                    var ctx = canvas.getContext('2d');
                
目前最新瀏覽器都支援 canvas 元素實現 3D 繪圖呈現,getContext() 方法允許傳遞一個 “webgl” 字串參數傳回一個 WebGLRenderingContext 物件用於 3D 繪圖。一些瀏覽器也可以傳遞自訂的參數值,例如一些版本的 FireFox 可以傳遞 “moz-3d” 以及 “experimental-webgl” 作為參數。

支援性檢查

利用檢查 getContext() 方法檢查是否支援 canvas 元素:
                    var canvas = document.getElementById('tutorial');
                    if(canvas.getContext){
                        var ctx = canvas.getContext('2d');
                        // drawing code here
                    }else{
                        //canvas-unsupported code here
                    }
                
範例:01_基本用法

  2實用功能

讓 Canvas 鋪滿瀏覽器視窗

經常需要將 Canvas 填滿瀏覽器視窗,需用 CSS 設定 canvas 為 block:
                    <style>
                        html, body{
                            margin: 0;
                            padding:0;
                        }
                        canvas{
                            display:block;
                        }
                    </style>
                
然後用 JavaScript 來動態調整畫布長寬及 resize:
                    //該函數用來改變畫布大小
                    function resizeCanvas(){
                        var canvas = document.getElementById('_2DCanvas');    //取得 canvas
                            canvas.setAttribute('width', window.innerWidth);       //改變寬度
                            canvas.setAttribute('height', window.innerHeight);     //改變高度
                    }
                    //每當視窗改變時也要呼叫 resizeCanvas() 函數用來改變畫布的大小
                    window.onresize = resizeCanvas;
                

禁用選取 Canvas

通常還需禁止選取 Canvas 功能,避免像手機觸控時選取到 DOM,而阻礙互動進行:
                    //禁止滑鼠選取 DOM 元素
                    document.onselectstart = function(){
                        return false
                    };
                
範例:02_Canvas 在專案上的實用功能 / 01_實用功能

儲存 Canvas 圖像資料

一般在用戶端預設右鍵另存圖片都是 PNG 格式,若要傳到伺服端需用 canvas.toDataURL() 方法取得 base64 編碼的圖片資料:
                    canvas.toDataURL([type, ...])
                
第一個參數 type 控制傳回圖片的類型,例如 png、 jpg 等,預設值為 image/png,如果提供的參數值所表示的圖片類型不被支援,那麼會使用預設值,其後可以有很多參數,根據參數 type 的設定不同,如果該參數是 image/jpeg,那第二個參數是 0.0~1.0 之間的數字,用於定義圖片品質等級。
                    //影像輸出為 base64 壓縮字串,預設為 image/png
                    var data = canvas.toDataURL();
                    //刪除字串前的提示訊息 "data:image/png;base64"
                    var base64 = data.subString(22);
                
範例:02_Canvas 在專案上的實用功能 / 02_輸出圖像資料
傳回圖片 base64 的編碼資料!

範例:02_Canvas 在專案上的實用功能 /03_調整Canvas解析度
調整符合裝置的解析~

  3繪製基本圖型

W3C 制定的 Canvas 2D Context 標準提供了一組用來在畫布上繪製圖型的 API,可用的物件、方法和屬性非常豐富,該標準可以使用濟面描述語言(Interface Description Language,簡稱 IDL)來描述。

了解座標

在開始繪圖之前,我們先了解畫布的座標系,畫布的原點在元素的左上角,水平是 x 軸、垂直是 y 軸,沿原點向右、向下是正值,向左向上是負值,與傳統的笛卡爾座標系不同:

矩形(rectangle)

不同於SVG,<canvas> 只支援一種原始圖形,矩形,矩形共有三個繪圖函數:
                    //畫出一個填滿的矩形
                    fillRect(x, y, width, height)   
                    
                    //畫出一個矩形的邊框
                    strokeRect(x, y, width, height)
                    
                    //清除指定矩形區域內的內容,使其變為全透明
                    clearRect(x, y, width, height)  
                
這三個函數都接受一樣的參數:x, y 代表從原點出發的座標位置,width, height 代表矩形的寬高。 範例:03_繪製圖形 / 01_rect_矩形

路徑(path)

使用路徑畫圖的步驟如下:
  1. 先產生路徑
    第一部呼叫 beginPath() 產生一個路徑,路徑會被存在一個次路徑清單中,這些次路徑集合起來就形成一塊圖形,每次呼叫這個方法,次路徑清單就會被重設,然後便能畫另一個新圖形。
    *當目前路徑為空(例如接著呼叫 beginPath() 完後)或是在一個新畫布上,不論為何,第一個路徑繪圖指令總是 moveTo(),因為每當重設路徑後,你幾乎都會需要設定繪圖起始點。
  2. 用繪圖指令畫出路徑
    呼叫各式方法來實際設定繪圖路徑。
  3. 結束路徑
    非必要的一步,就是呼叫closePath()。這個方法會在現在所在點到起始點間畫一條直線以閉合圖形,如果圖形已經閉合或是只含一個點,這個方法不會有任何效果。
    *當呼叫 fill(),任何開放的圖形都會自動閉合,所以不需要再呼叫 closePath(),但是stroke() 並非如此。
一旦路徑產生後便可用筆畫或填滿方式來渲染生成。
                    //產生一個新路徑,產生後再使用繪圖指令來設定路徑
                    beginPath()
                    
                    //閉合路徑好讓新的繪圖指令來設定路徑
                    closePath()
                
以下是路徑 API 的繪圖指令:
                    //畫出圖形的邊框
                    stroke()
                    
                    //填滿路徑內容區域來產生圖形
                    fill()
                
moveTo() 不會畫任何圖形,卻是路徑清單的一部份,作用大概像是把筆從紙上一點提起來,然後放到另一個點,當初始化畫布或是呼叫 beginPath(),通常會想要使用 moveTo() 來指定起始點,也可以用 moveTo() 畫不連結的路徑:
                    //移動畫筆到指定的 (x, y) 座標點
                    moveTo(x, y)
                
用 lineTo() 方法畫直線:
                    //從目前繪圖點畫一條直線到指定的 (x, y) 座標點
                    lineTo(x, y)
                
範例:03_繪製圖形 / 02_line_線

弧形(arc)

arc() 方法是以一個中心點和半徑來畫圓弧路徑,五個參數分別是:x, y 代表圓心座標點;radius 代表半徑;startAngle, endAngle 代表沿著弧形曲線上的起始點與結束點的弧度;弧度測量是相對於 x 軸,anticlockwise 為 true 代表逆時針作圖、false 代表順時針作圖:
                    arc(x, y, radius, startAngle, endAngle, anticlockwise) 
                
arc() 方法用的是弧度(radians)而非角度(degrees),如果要在弧度與角度之間換算,可利用以下程式碼:
                    radians = (Math.PI/180)*degrees
                
範例:03_繪製圖形 / 03_arc_圓弧
範例:03_繪製圖形 / 04_arc_圓弧_圓形
範例:03_繪製圖形 / 05_arc_圓弧_扇形

弧線(arcTo)

arcTo() 方法使用一個目標點和一個半徑來畫弧線路徑,四個參數分別是:x1, y1 定義 P1 座標;x2, y2 定義 P2 座標;radius 代表半徑:
                    arcTo(x1, y1, x2, y2, radius)
                
增加給路徑的圓弧是具有指定半徑圓的一部份,該圓弧有一個點與目前位置點(start)到 P1 的線段相切,還有一個點和從 P1 到 P2 的線段相切,這兩個切點就是圓弧的起點和終點,圓弧繪製的方向就是連接這兩個點的最短圓弧的方向。
範例:03_繪製圖形 / 06_arcTo_弧線_圓形
範例:03_繪製圖形 / 07_arcTo_弧線_圓角矩形

路徑繞排

在建立路徑時,還要注意繪製路徑的方向,路徑方向被稱為「路徑繞排」。
繪圖方法的座標參數的順序確定了繞排方向:
  • 順時鐘路徑為正向繞排
  • 逆時鐘路徑為反相繞排
當路徑相交時,繞排規則十分重要,繞排規則將確定重疊區域的填充規則,Canvas 是使用「非零規則(nonZero)」的繞排規則,依靠繞排方向來確定是否填充相交路徑定義的區域,有兩種狀況:
  • 當相交路徑的繞排方向不同時,不填充所定義的區域
  • 當相交路徑的繞排方向相同時,將填充本來不填充的區域
如果影像進行動畫處理或要用在 3D 物件上的紋理影像發生重疊時,繞排規則會變得非常重要!
可以使用「計數」方式來確定是否填充,規則如下:
  • 正向繞排路徑將獲得設定值 +1
  • 反向繞排路徑將獲得設定值 -1
以形狀上閉合區域中的一點為起點,繪製一個從該點向外無限延伸的線條,使用該線條與路徑相交的次數以及這些路徑的組合值來確定填充。

對於非零繞排,如果計數為 0,則不填充相交區域,如果計數為其他數,則填充相交區域,組合該線條與路徑相交的次數,如果相交路徑的繞排方式相同,那麼路徑的組合值就是 2 或 -2,不為 0,則填充相交區域,不然組合值為 0,則不填充相交區域。 範例:03_繪製圖形 / 08_路徑繞排
範例:03_繪製圖形 / 09_正弦波和餘弦波

二次貝茲曲線(quadratic curve)與三次貝茲曲線(bezier curve)

二次與三次貝茲曲線是另一個種可以構成複雜有機圖形的路徑,二次和三次的差別是(貝茲曲線的起點和終點以藍色標示):二次貝茲曲線只有一個控制點;三次貝茲曲線有兩個控制點。
                    //從目前起始點畫一條二次貝茲曲線到 (x, y) 指定的終點
                    //控制點由 (cp1x, cp1y) 指定
                    quadraticCurveTo(cp1x, cp1y, x, y)
                    
                    //從目前起始點畫一條三次貝茲曲線到 (x, y) 指定的終點
                    //控制點由 (cp1x, cp1y) 和 (cp2x, cp2y) 指定
                    bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) 
                
*其中比較需要注意的是可以自己定義一個的函數,來執行常重複的繪圖工作,減少程式碼數量與複雜度。 範例:03_繪製圖形 / 10_quadraticCurveTo_二次貝茲曲線
範例:03_繪製圖形 / 11_quadraticCurveTo_二次貝茲曲線_圓形
範例:03_繪製圖形 / 12_bezierCurveTo_三次貝茲曲線
範例:03_繪製圖形 / 13_bezierCurveTo_三次貝茲曲線_橢圓

範例:03_繪製圖形 / 14_path_路徑_直線
範例:03_繪製圖形 / 15_path_路徑_曲線
範例:03_繪製圖形 / 16_path_路徑_圓角
範例:03_繪製圖形 / 17_path_路徑_形狀
範例:03_繪製圖形 / 18_path_路徑_橢圓曲線

Path2D 物件(Path2D)

Path2D 相較於上述物件,是比較新的功能,配合新的 Path2D API 儲存路徑,亦能簡化 Canvas 繪圖碼並達到更快的執行速度,有三種方式可建立 Path 2D 物件:
                    new Path2D();	 //空路徑
                    new Path2D(path);  //複製另一個路徑
                    new Path2D(d);     //複製 SVG 路徑資料
                
第三種方式將 SVG 路徑納入建構,特別方便好用,可以重複使用 SVG 路徑,直接在 Canvas 描繪相同的輪廓:
                    var p = new Path2D("M10 10 h 80 v 80 h -80 Z");
                
*其中比較需要注意的是可以自己定義一個的函數,來執行常重複的繪圖工作,減少程式碼數量與複雜度。 範例:03_繪製圖形 / 19_Path2D

  4套用樣式

繪製線條和填充實,可以使用 strokeStyle 與 fillStyle 屬性來分別指定線條、填充的呈現,屬性值可以是顏色字串(單色)、CanvasGradient 物件(漸層色)、CanvasPattern 物件(模式),以及基本設定的透明度、陰影,線條則有自己的樣式。

單色(color)

單色也有好幾種寫法:
                    ctx.fillStyle = "orange"; 
                    ctx.fillStyle = "#FFA500"; 
                    ctx.fillStyle = "rgb(255,165,0)"; 
                    ctx.fillStyle = "rgba(255,165,0,1)";
                
顏色名稱的關鍵字列表(約 147 色)可從這裡查詢:http://www.colors.commutercreative.com/ 範例:04_套用樣式 / 01_顏色

透明度(alpha)

透過設定 globalAlpha 屬性或是以半透明顏色值設定 strokeStyle 與 fillStyle 屬性,可以畫半透明的圖形:
                    globalAlpha = transparencyValue
                
允許值介於 0.0(全透明)到1.0(不透明),預設值為 1.0。
*一旦設定後,之後畫布上畫的所有圖形的不透明度都會套用此設定值。 範例:04_套用樣式 / 02_透明度

漸層色(gradient)

透過設定 fillStyle 和 strokeStyle 屬性為 canvasGradient 漸層物件,可以畫出線性和放射狀的漸層顏色。

線性漸層(linear Gradient):
                    createLinearGradient(x1, y1, x2, y2)
                    //產生線性漸層物件,漸層起點為 (x1, y1),終點為 (x2, y2)
                
放射性漸層(Radial Gradient):
                    createRadialFradient(x1, y1, r1, x2, y2, r2)
                    //產生放射狀漸層物件,第一個圓的圓心在 (x1, y1)、半徑為 r1,第二個圓的圓心在 (x2, y2)、半徑為 r2
                
一旦產生 canvasGradient 漸層物件,用 addColorStop() 方法添加顏色:
                    gradient.addColorStop(position, color)
                
於 gradient 漸層物件建立一個顏色點,其中 color 是顏色字串、position 介於 0.0 到 1.0 之間,定義該顏色在漸層中的相對位置,呼叫這個方法會指定當進行到設定的位置時,漸層需要完全轉變成設定的顏色。 範例:04_套用樣式 / 03_漸層色

圖樣(patterns)

呼叫 createPattern() 會產一個可重複的畫布樣式物件,其中 image 是CanvasImageSource 類別物件:
                    createPattern(image, type)
                    //image 是 CanvasImageSource 類別物件,像是 HTMLImageElement
                    //Type 是字串,定義如何產生樣式,有 repeat、repeat-x、repeat-y、no-repeat
                
*注意 onload 事件處理器,這是為了確保影像載入完成後再進行,不像 drawImage() 方法,呼叫 createPattern() 方法前影像必須要先載入完成,否則可能圖像的程生會有問題。 範例:04_套用樣式 / 04_圖樣

陰影(shadow)

要產生陰影只需要四個屬性:
                    shadowOffsetX = float
                    //代表陰影從物件延伸出來的水平距離,預設為0,不受變形矩陣影響
                    shadowOffsetY = float
                    //代表陰影從物件延伸出來的垂直距離,預設為0,不受變形矩陣影響
                    shadowBlur = float
                    //代表陰影模糊大小範圍,預設為0,不受變形矩陣影響,不等同於像素值
                    shadowColor = >color<
                    //CSS顏色值,代表陰影顏色,預設為全透明。
                
shadowOffsetX 和 shadowOffsetY 會決定陰影延伸大小,若是為正值,則陰影會往右(沿 X 軸)和往下(沿 Y 軸)延伸,若是為負值,則會往正值相反方向延伸。 範例:04_套用樣式 / 05_陰影

線條樣式

有幾種屬性可以設定線條樣式,用 lineWidth 屬性指定線條寬度粗細,lineCap 屬性來指定端點如何繪製,lineJoin 屬性指定線條間結合處如何結合:
                    lineWidth = value
                    //設定線條寬度
                    lineCap = type
                    //設定線條端點的樣式
                    lineJoin = type
                    //設定線條間接合處的樣式
                    miterLimit = value
                    //限制當兩條線相交時交接處最大長度;所謂交接處長度(miter length)是指線條交接處內角頂點到外角頂點的長度
                
lineWidth 屬性決定線條寬度,預設 1.0,必須大於 0.0,線條寬度的起算點是中央,越寬會越往兩旁各延伸一半的設定寬度,由於繪畫的最小計量單位是像素,所以指定一個浮點數會被重新計算後再應用於繪圖,以效能考慮,不建議使用浮點數作為線條粗細值
先來了解一下繪圖路徑的產生方式:
  1. 第一張紅色區域的邊際正好符合像素間的邊際,所以會產生清晰影像!
  2. 第二張有一條 1.0 px 的直線,不過線條寬度起算點使從繪圖路徑中央開始往兩旁各延伸一半的設定寬度,所以繪圖路徑兩旁的像素格只有一半會被填滿深藍色,另一半則會經由計算填入近似色(淺藍色),結果整個像素格並非全部填入相同顏色,而產生出邊緣較模糊的線條,因此奇數寬度直線會因為繪圖路徑位置關係而比較模糊。
  3. 第三張是為了避免模糊,必須精準設定繪圖路徑位置,因此將點往左移動 0.5 px,就可以得到填滿像素的線。
為了避免模糊,繪圖路徑最好是落在整數座標點上。
雖然處裡 2D 繪圖縮放有些麻煩,但只要仔細計算像素格和繪圖路徑位置,縱使進行圖像縮放或變形,圖像輸出還是可以保持正確,例如一條寬 1.0 px 的直線,只要位置計算正確,放大兩倍後會變成一條 2 個像素寬的清晰直線,而且還是會保持正確位置。

lineCap 屬性指定線條的端點如何繪製,共有三種合法值,預設為 butt:
  1. butt
    表示線條端點沒有線蓋,端點是平直且和線條方向正交,這條線條在端點之外沒有擴充。
  2. round
    表示端點帶有一個半圓形的線蓋,半圓直徑等於線條粗細,餅且線條在端點之外擴充了線條粗細的一半。
  3. square
    表示線條帶有一個矩形線蓋,這個值和 butt 一樣,但線條擴充了自己寬度的一半。
lineJoin 屬性決定兩個連接區端(如線條、弧形或曲線)如何連結(對於長度為零,亦即終點和控制點為同一點的圖形無效),共有三個屬性,其中 miter 為預設值:
  1. miter
    表示斜交型連接,向外延伸連結區段外緣直到相交於一點,形成菱形區域,而 miterLimit 屬性會影響 miter 屬性。
  2. round
    代表圓弧形連接樣式。
  3. miter
    代表斜面型連接樣式,在連結區段的共同終點處填滿一個三角形區域,將原本的外接角處形成一個切面。
miterLimit 屬性決定相交指數,只有在當設定 miter 時有效,斜面可能太長變得不協調,可以用 miterLimit 屬性為斜面的長度設定一個上限,這個屬性工作表示斜面長度和線條長度的比值,預設是 10,表示一個斜面長度不應該超過寬度的 10 倍,如果斜面到達這個長度,它就變斜角了! 範例:04_套用樣式 / 06_線條樣式

  5繪製文字

Canvas 提供在畫布上繪製文字,可以將文字繪製成輪廓線條,也可以繪製成填充,除了設定圖形相關屬性(例如顏色、線條粗細、陰影等),還可以定義文字樣式,基本使用為:
                    fillText(text, x, y [, maxWidth])
                    //設定文字內容和位置
                    strokeText(text, x, y [, maxWidth]) 
                    //設定文字框的內容和位置
                
另外有三種屬性用來設定文字樣式:
  • 字型(font)
  • 水平對齊方式(textAlign)
  • 基準線對齊方式(textBaseline)

設定字型

                    font = value
                
屬於 font 設定字型,與 CSS 的 font 屬性設定相同,但不支援互依性質屬性質,例如 inhert、initial,也不支援 line-height,如果有這些關鍵字,會自動被忽略:
                    ctx.font = '12px/14px sans-serif';
                    //定義 line-height 為 14px,但被強制為 normal,實際等於 '12px sans-serif'
                    ctx.font = '80% sans-serif';
                    //設定 font-size 為父元素的 80%,會首先計算為像素
                    ctx.font = 'x-large "New Century Schoolbook", serif';
                    //設定 font-size 為 x-large 也會首先計算為像素、字體為 "New Century Schoolbook", serif
                    ctx.font = 'bold italic large Palatino, serif';
                    //設定 font-weight 為粗體、font-style 為斜體、字體為 large Palatino, serif
                    ctx.font = 'normal small-caps 120%/120% fantasy';
                    //設定 font-variant 為小型大寫、font-size 為父元素的 120%、字體為 fantasy                    
                
範例:05_繪製文字 / 01_文字基本繪製

設定文字水平對齊方式

                    textAlign = value
                
參數包含 start、end、left、right、center,預設為 start,textAlign 屬性值會牽涉到繪製文字的起始點座標:
                    //以下三種起始點是文字左邊緣,且實際的對齊方式是靠左對齊
                    textAlign = 'left';
                    textAlign = 'start';    //同時文字方向是 ‘ltr’
                    textAlign = 'end';      //同時文字方向是 ‘rtl’
                    //以下三種起始點是文字右邊緣,且實際的對齊方式是靠右對齊
                    textAlign = 'right';
                    textAlign = 'start';    //同時文字方向是 ‘rtr’
                    textAlign = 'end';      //同時文字方向是 ‘ltr’

                    //以下三種起始點是文字水平中心,且實際的對齊方式是置中對齊
                    textAlign = 'center';
                
範例:05_繪製文字 / 02_基準設定

設定文字水平對齊方式

                    textBaseline = value
                
參數包含 top、hanging、middle、alphabetic、ideographic、bottom,預設為 alphabetic。

最常使用的基準線對齊方式就是 alphabetic,就是平常我們用到的 baseline,是西歐自行用到的概念,是一個標準,除此之外,常用的垂直對齊為 top、middle、bottom。

另外兩種基準線對齊方式,ideographic 即表意文字,是 CJK 字型(中日韓字型)用到的概念;hanging 是印度系文字自行用到的概念,字型有點像是懸掛在一個基準線上,藏語字型也是一個 hanging 字型,處理西歐字型和 CJK 字型時不會有關。

W3C 標準提供了下圖,來說明這幾個基準線的位置,前幾個字元都是西歐文字,中間兩個是 CJK 文字,最後一個是印度系文字:

設定文字方向

                    direction = value
                
參數包含 ltr、rtl、inherit,預設為 inherit(目前測不出結果)。

測量繪製文字的寬度

                    var oTextMetrics = ctx.measureText(text);
                    oTextMetrics.width
                
參數 text 定義要測量的文字字串,因為繪圖方法沒有提供文字換行功能,因此需要測量文字的寬度來確定換行,進一步將過長的文字繪製在多行上。 範例:05_繪製文字 / 03_多行設定

  6繪製影像

畫布 API 能接受以下資料型態作為影響來源:
  • HTMLImageElement
    用 Image() 建構成的影像或是 <img> 元素。
  • HTMLVideoElement
    用 HTMLVideoElement 元素作影像來源,抓取影片目前的影像畫格當作影像使用。
  • HTMLCanvasElement
    用另一個 HTMLCanvasElement 元素當影像來源。
  • ImageBitmap
    可以被快速渲染的點陣圖(bitmap),點陣圖能由上述所有來源產生。
還有好幾種方法能夠取得影像用於畫布:
  • 使用同一份網頁上的影像
    透過 document.images、document.getElementsByTagName() 方法、document.getElementById() 方法取得影像。
  • 使用來自其他網域的影像
    Using the crossOrigin attribute on an 透過 <htmlimageelement> 的 crossOrigin 屬性,可以要求從另一個網域載入影像來使用,若是寄存網域(thehosting domain)准許跨網路存取該影像,那麼便可以使用它而不污染(taint)畫布,反之,使用該影像會污染畫布(taint the canvas)。
  • 使用其他畫布元素
    如同取得其他影像,一樣能用 document.getElementsByTagName() 或document.getElementById() 方法取得其他畫布元素,但是在使用之前請記得來源畫布上已經有繪上圖了。
    使用其他畫布元素作為影像來源有很多有用的應用用途,其中之一便是建立第二個小畫布作為另一個大畫布的縮小影像。
  • 創造全新的影像
    產生新的 HTMLImageElement 物件也能當作影像來源,可以用 Image() 來建構一個新影像元素:
                                var img = new Image();    //產生新 img 元素
                                img.src = 'myImage.png';  //設定圖片路徑
                            
    在影像載入完成前呼叫 drawImage() 不會有任何效果,甚至某些瀏覽器還會拋出例外狀況,所以要透過載入事件來避免這類問題:
                                var img = new Image();    //產生新 img 元素
                                img.addEventListener("load", function() {
                                    //圖片載入完畢後,再畫圖
                                }, false);
                                img.src = 'myImage.png';  //設定圖片路徑
                            
    若是只要載入一份影像,可以用上面的方法,不過當需要載入、追蹤多個影像時,就需要更好的方法了,可以參考 JavaScript Image Preloader。
  • 以 data:URL 嵌入影像
    利用 data: url,透過 data URL 直接將影像定義成 Base64 編碼的字串,然後嵌入程式碼之中:
                                var img_src = 'data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==';
                            
    data URL 的好處是立即產生影像,不用再和伺服器連線,另一個好處是能夠將影像包入 CSS, JavaScript, HTML 之中,讓影像更具可攜性;壞處則是影像不會被快取起來,而且對大影像來說編碼後的 URL 會很長、檔案也會相對比較大一些。
  • 從影片中擷取每一幀
    使用 <video> 元素中的影片的影片畫格(縱使影片為隱藏),例如現在我們有一個 ID為 “myvideo” 的 <video> 元素:
                                function getMyVideo() {
                                    var canvas = document.getElementById('canvas');
                                    if (canvas.getContext) {
                                        var ctx = canvas.getContext('2d');
                                        return document.getElementById('myvideo');
                                    }
                                }
                            
    上面的方法會回傳一個 HTMLVideoElement 的影像物件,這個物件可以被視為CanvasImageSource 類別的物件來使用,可以參考 html5Doctor的“video + canvas = magic” 一文。

影像繪圖

取得影像來源後可以用 drawImage() 方法將影像渲染到畫布上:
                    drawImage(image, x, y)
                    //從座標點 (x, y) 開始畫上 imag e參數指定的來源影像(CanvasImageSource)
                

影像縮放

第二種型態是在後面新增兩個參數,就可以縮放影像:
                    drawImage(image, x, y, width, height)
                    //當放置影像於畫布上時,會按照參數 width(寬)、height(高)來縮放影像                        
                

影像切割

第三種型態接受 9 個參數,其中 8 個讓我們從原始影像中切出一部份影像、縮放並畫到畫布上:
                    drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 
                    //image 參數是來源影像物件,(sx, sy) 代表在來源影像中以 (sx, sy) 座標點作為切割的起始點,sWidth 和 sHeight 代表切割寬和高,(dx, dy)代表放到畫布上的座標點,dWidth 和 dHeight 代表縮放影像至指定的寬和高                        
                
前四個參數定義了來源影像上切割的起始點和切割大小,後四個參數定義了畫到畫布上的位置和影像大小,切割影像最常使用在 Sprite 圖片,就不用多次載入檔案,進而加強效能。

影像動態

canvas 的 drawImage() 不但可以將 img 元素、另一個 canvas 元素作為影像來源,也可以將 video 元素作為影像源,這就為動態處理視訊內容提供了其他可能,在「10_繪製像素」會有詳細範例 :
                    drawImage(video, x, y)
                
範例:06_繪製影像 / 01_影像三種多載形式

  7變形效果

畫布狀態儲存與復原

使用變形效果前,有兩個不可或缺的方法:
                    save();
                    //儲存現階段畫布完整階段
                    restore();
                    //復原最近一次儲存的畫布狀態
                
每一次呼叫 save(),畫布狀態便會存進一個堆疊(stack)中,包含:
  • 曾經套用過的變形效果,如 translate, rotate 和 scale。
  • 繪圖樣式,如 strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 屬性的屬性值。
  • 目前截圖路徑。
呼叫 save() 的次數不限,而每一次呼叫 restore(),最近一次儲存的畫布狀態便會從堆疊中被取出,然後還原畫布到此畫布狀態。 範例:07_變形效果 / 01_儲存與復原

移動畫布

第一個變形效果方法是 translate(),用來移動畫布,原先畫布的原點在網格 (0, 0) 位置,移動畫布使其原點移到 (x, y) 位置(如圖)。
                    translate(x, y)
                    //移動網格上的畫布,其中 x 代表水平距離、y 代表垂直距離
                
建議最好在做任何變形效果前先儲存一下畫布狀態,當我們需要復原先前的狀態時,只需要呼叫一下 restore() 即可。 範例:07_變形效果 / 02_translate_移動畫布

旋轉畫布

第二個變形效果方法是 rotate(),以畫布原點作中心旋轉畫布,可以呼叫 translate() 方法來移動旋轉中心(亦即畫布原點)(如圖)。
                    rotate(x) 
                    //以畫布原點為中心,順時針旋轉畫布 x 弧度(弧度 = Math.PI * 角度 / 180)
                
範例:07_變形效果 / 03_rotate_旋轉畫布

縮放畫布

第三個變形效果方法是 scale(),用來縮放圖形,畫布網格預設前進 1 單位等於前進 1px,所以縮小 0.5 倍,就會變成前進 0.5 的像素大小,亦即縮小圖像一半大小,反之,放大 2 倍將放大圖像 2 倍。
                    scale(x, y)
                    //x 代表縮放畫布水平網格單位 x 倍,y 代表縮放畫布垂直網格單位 y 倍,輸入 1.0 不會造成縮放。如果輸入負值會造成座標軸鏡射,假設輸入 x 為 -1,那麼原本畫布網格 X 軸上的正座標點都會變成負座標點、負座標點則變成正座標點。
                
範例:07_變形效果 / 04_scale_縮放畫布

變形矩陣

最後一個方法是設定變形矩陣,藉由改變變形矩陣可以營造各種變形效果,包含先前所提到的rotate, translate, scale。
                    transform(a, b, c, d, e, f)
                    setTransform(a, b, c, d, e, f)
                
呼叫 transform() 會拿目前的變形矩陣乘以下圖的 3 乘 3 矩陣:
  • a 代表水平縮放,影響在縮放或旋轉影像時沿 X 軸的像素定位
  • b 代表水平傾斜,影響在旋轉或傾斜影像時沿 Y 軸的像素定位
  • c 代表垂直傾斜,影響在旋轉或傾斜影像時沿 X 軸的像素定位
  • d 代表垂直縮放,影響在縮放或旋轉影像時沿 Y 軸的像素定位
  • e 代表水平移動,沿 X 軸移動每個點的距離
  • f 代表垂直移動,沿 Y 軸移動每個點的距離
轉換的公式如下圖: 但這兩種方法還是有些不同:
  • transform() 方法
    可以應用矩陣轉換,每個轉換都將更改目前矩陣的屬性,所以可以有效合併重疊多個轉換
  • setTransform() 方法
    也可以應用轉換,但該方法不能重疊,他會將轉換重置,然後再應用目前設定,也稱為恆等矩陣(Identiy matrix,預設矩陣),等於復原目前矩陣再輸入參數呼叫transform()。
所以矩陣的轉換,跟先前提到的變形功能可以做轉換:
轉換 方法 矩陣值 相等於
平移 translate(tx, ty) transform(1, 0, 0, 1, tx, ty)
縮放 scale(sx, sy) transform(sx, 0 , 0, sy, 0 , 0)
旋轉 rotate(q) transform(cos(q), sin(q), -sin(q), cos(q), 0, 0)
傾斜剪裁 transform()
setTransform()
transform(1, 0, tan(a), 1, 0, 0)
類似水平傾斜(skewX)的概念

transform(1, tan(a), 0, 1, 0, 0)
類似垂直傾斜(skewY)的概念
範例:07_變形效果 / 05_transform_矩陣轉換

  8合成效果

混合模式

利用 globalCompositeOperation 屬性設定混合上下重疊圖形的顏色,還可以將新圖形遮蓋部分區域、清除畫布部分區域(不同於 clearRect() 只能清除矩形區域):
                    globalCompositeOperation = type
                    //type 字串可指定以下 12 種合成設定之一,每一種合成設定均將套用到新繪製的圖形上
                
下面列出 globalCompositeOperation 屬性可能的值、說明及圖形結果 (藍色是舊圖形、紅色為新圖形):
source-over
將新圖形畫在舊圖形之上,為預設值
destination-over
將新圖形畫在舊圖形之下
source-in
只保留重疊的新圖形區域,其餘變透明
destination-in
只保留重疊的舊圖形區域,其餘變透明
source-out
只保留非重疊的新圖形區域,其餘變透明
destination-out
只保留非重疊的舊圖形區域,其餘變透明
source-atop
在舊圖形之上,蓋上重疊的新圖形區域
destination-atop
在新圖形之上,蓋上重疊的舊圖形區域
lighter
重疊區域的顏色,由相加而得
darker
重疊區域的顏色,由相減而得
*此屬性值已從畫布規格中移除,不再支援
xor
重疊區域設為透明
copy
移除舊圖形,只保留新圖形
範例:08_合成效果 / 01_混合模式

剪裁路徑

裁剪路徑如同遮罩一樣,會蓋掉不需要的部分(如圖)。 紅邊星星是我們的裁剪路徑,在路徑區域以外部分都不會出現在畫布上,可以發現 source-in 和 source-atop 這兩種構圖組合所達到的效果類似,其中最大差異在於裁剪路徑不需加入新圖形,消失的部分也不會出現在畫布上。

剪裁區域是累加性的,呼叫 clip() 可將目前路徑和目前繪製區域取交集,產生一個新的區域。

目前沒有一個方法可以直接把目前的剪裁區設並為畫布範圍,要做到這點,必須儲存和恢復畫布的整個圖形狀態:
                    clip()
                    //轉換目前繪圖路徑為裁剪路徑。
                
呼叫 clip() 除了會替代 closePath() 來關閉路徑之外,還會轉換目前填滿或勾勒繪圖路徑為裁剪路徑。 >canvas< 畫布預設有一個等同於本身大小的裁剪路徑,等同於無裁剪效果。 範例:08_合成效果 / 02_裁剪路徑

  9繪製動畫

使用 Canvas 僅能繪製靜態的圖片,但是透過每隔一段時間就繪製一幅不同的圖片就可以實現動畫!每一幅圖片就是動畫中的一格,繪製一格畫面需要以下幾個步驟:
  1. 清空畫布
    除了不變的背景畫面,所有先前畫的圖案都要先清除,可透過 clearRect() 方法達成。
  2. 儲存畫布狀態
    如果想要保留不變的畫面,就需要儲存畫布原始狀態。
  3. 畫出畫面
    重新繪製動畫格。
  4. 恢復畫布狀態
    復原畫布狀態,然後重繪下一格。

控制動畫

我們需靠每隔一段時間繪圖來產生動畫,傳統 JavaScript 無非就是使用以下方法實現動畫:
                    setInterval(function(){
                        //每隔 delay 毫秒,執行動作...
                    }, delay);   
                    setTimeout(function(){
                        //過 delay 毫秒後,執行動作...
                    }, delay); 
                
這兩種方法對簡單的動畫方法適用,但隨著現在對使用者體驗的要求越來越高,這兩種方法已經不太適用,加上即使看不到網頁(當網頁標籤不是目前瀏覽標籤或瀏覽器最小化時),動畫也會不停繪製。

為解決此問題瀏覽器廠商和 W3C 開始制定標準,提供一個對動畫格統一管理,並提供監聽的 API,進一步為建立動畫提供一種更平順、高效能的方法,稱作 WindowAnimationTiming:
                    requestAnimationFrame(callback)
                    //告訴瀏覽器執行動畫時,要求瀏覽器在重繪下一張畫面之前,呼叫 callback 函數來更新動畫,callback 通常每秒鐘執行 60 次,當在背景執行時,次數會更低
                
WindowAnimationTiming 有兩種優勢:
  1. 只進行 1 次繪製
    在同一影格中,對 DOM 的所有操作只進行一次版面配置和繪製,把原本需要多次進行的版面配置和繪製最佳化成一次。
  2. 不繪製隱藏的元素
    如果動畫元素被隱藏,就不需再繪製,例如當切換另一個網頁或最小化時,瀏覽器就會停止動畫,表示能減少 CPU 和記憶體的消耗。
基本方法就是呼叫 window.requestAnimationFrame() 方法:
                    var animationStartTime;
                    function animate(timeStamp){
                        //執行一個操作
                        document.getElementById('animated').style.left = (timeStamp - animationStartTime) % 500 + 'px';
                        //再次呼叫 requestAnimationFram() 方法循環執行,進一步實現動畫
                        window.requestAnimationFrame(animate);
                    }
                    
                    function start(){
                        animationStartTime = Date.now();
                        window.requestAnimationFrame(animate);
                    }
                
瀏覽器支援的相容性:
                    (function(){
                        var requestAnimationFrame = window.requestAnimationFrame
                                        || window.mozRequestAnimationFrame
                                        || window.webkitRequestAnimationFrame
                                        || function(callback){
                                            return window.setTimeout(callback, 1000/24);
                                        };
                        var cancelAnimationFrame = window.cancelAnimationFrame
                                        || window.mozCancelAnimationFrame
                                        || window.webkitCancelAnimationFrame
                                        || window.clearTimout;
                        window.requestAnimationFrame = requestAnimationFrame;
                        window.cancelAnimationFrame = cancelAnimationFrame;
                    })();
                
但如果想製作互動 , 可以使用鍵盤或滑鼠事件來控制動畫,有需要可以搭配使用 setTimeout() 函數。

高精確度時間

可以對 SVG、Canvas、CSS...設定回呼函數動畫,回呼函數有一個傳入的 timeStamp 參數,該參數表示呼叫回呼函數的時間,它是一個 DOMTimeStamp 類型的值,是從導覽開始時開始測量的時間值,此時間值可以直接與 Data.now() 進行比較,但大多瀏覽器將 timeStamp 參數實現為 DOMHighResTimeStamp 類型的值,表示是從導覽開始時開始測量的高精確度時間值,DOMHighResTimeStamp 以毫秒為單位,精確到千分之一毫秒,此時間值不直接與 Date.now() 進行比較,應使用 window.performance.now 取得目前的高精確度時間值

下面示範如何回呼函數中的時間戳記參數:
                    function animate(timeStamp){
                        var progress;
                        if(start === null) start = timestamp;
                        progress = timestamp - start;
                        if(對 progress 一些比較){
                            requestAnimationFrame(step);
                        }
                    }
                
範例:09_繪製動畫 / 01_setTimeout_基本移動球
範例:09_繪製動畫 / 02_setTimeout_太極旋轉動畫
範例:09_繪製動畫 / 03_setTimeout_時鐘
範例:09_繪製動畫 / 04_setInterval_預載進度動畫
範例:09_繪製動畫 / 05_setInterval_往右移動的循環圖片
範例:09_繪製動畫 / 06_requestAnimationFrame_基本移動球
範例:09_繪製動畫 / 07_requestAnimationFrame_太陽系時鐘
範例:09_繪製動畫 / 08_requestAnimationFrame_邊緣反彈球
範例:09_繪製動畫 / 09_requestAnimationFrame_物理彈跳球
範例:09_繪製動畫 / 10_requestAnimationFrame_殘影效果的彈跳球
範例:09_繪製動畫 / 11_requestAnimationFrame_圖片精靈動畫和控制FPS

10繪製像素

ImageData 物件代表 canvas 區中最基礎的像素,該物件儲存了影像像素值,每個物件有三個屬性:
                    imagedata.width	//傳回 ImageData 寬度
                    imagedata.height	//傳回 ImageData 高度
                    imagedata.data	//data 屬性質是一個 CanvasPixelArray 物件(Uint8ClampedArray),該物件用於儲存 width*height*4 個像素值,是一個一維陣列
                
每個像素點都是 | R | G | B | a | 組成,分別是 R、G、B 值和 alpha 值,範圍都是 0~255,像素的順序從左到右、從上到下,依序儲存。

例如讀取第 200 欄、第 50 列的值:
                    blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2];
                

使用 Uint8ClampedArray.length 屬性來讀取影像 pixel 的陣列大小:
                    var numBytes = imageData.data.length;
                
範例:10_繪製像素 / 01_createImageData_畫紅色半透明方塊圖

創造 ImageData 物件

使用 createImageData() 方法創造一個全新空白的 ImageData 物件,所有像素都是透明黑色 rgba(0,0,0,0),有兩種方式可以建立:
                    imagedata = context.createImageData(sw, sh); 	//分別指定寬高
                    imagedata = context.createImageData(imagedata);	//指定一個 ImageData 物件,僅取得物件的寬高,不會複製物件本身,主要用來參考尺寸大小
                
如果沒有指定參數,就會拋出 NOT_SUPPORTED_ERR 例外;如果參數值是無限大或 NaN,或只有一個值為 null 的參數,方法也會拋出 NOT_SUPPORTED_ERR 例外。

得到 pixel 資料的內容

用 getImageData() 方法,傳回一個包含指定矩形範圍內的圖像資料的 ImageData 物件:
                    imagedata = context.getImageData(sx, sy, sw, sh);	
                    //分別指矩形左上角的座標、寬高                        
                
如果指定參數值是無限大或 NaN,會拋出 NOT_SUPPORTED_ERR 例外;如果指定寬高的參數為 0,就會拋出 INDEX_SIZE_ERR 例外。
※注意在 Chrome 本機使用 getImageData() 方法取得圖片像素資料時會拋出例外,這是 JavaScript 相同來源策略造成的,從 Web 伺服器上使用時不會有這個問題!
※在 ImageData 物件中,任何超出 canvas 外的 pixels 皆會返回透明的黑色的形式。

在內容中寫入 pixel 資料

使用 putImageData() 方法將 ImageData 物件中的自訂圖像資料繪畫到畫布上,如果中途讀取參數被提供,那麼就僅僅繪製指定的矩形範圍內的資料。

屬性 globalAlpha 和屬性 globalCompositeOperation 以及陰影設定在該方法呼叫終將被忽略,也就是說,混合、透明度、陰影相關的像素將被整個取代:
                    imagedata = context.putImageData(imagedata, dx, dy[, dirtyX, dirtyY, dirtyWidth, dirtyHeight]);	
                    //分別指定一個 ImageData 物件、繪製開始點的座標,其他為可選,指定要繪製的 ImageData 物件中影像的起始點座標、寬高                        
                
如果第一個參數是 null,那麼就會拋出 TYPE_MISMATCH_ERR 例外;如果其他參數是無限大,就會拋出 NOT_SUPPORTED_ERR 例外。

儲存圖像資料

使用像素繪製很常需要儲存圖片的功能,所以這邊除了再提醒寫法,也補充更詳細的新方法:
                    //影像輸出為 base64 壓縮字串的 png 圖像,此為預設,也可不寫
                    canvas.toDataURL('image/png')
                    //若輸出 jpg 圖像,還可以選擇圖像品質 0~1
                    canvas.toDataURL('image/jpeg', quality)
                    //使用 Blob 物件影像輸出
                    canvas.toBlob(callback, type, encoderOptions)                    
                
前兩者都是轉成 Base64 碼,可以把圖像放到 <image> 元素裡,然後使用連結直接下載即可!

至於 Blob 是一種二進位形式儲存方式,跟 Base64 編碼相比,只有一個短短的 GUID,真正的內容被儲存在瀏覽器記憶體中,Object URL像個號碼牌,憑著它可以向瀏覽器提領內容,由於物件內容被儲存在瀏覽器記憶體,Object URL 的生命週期就像 JavaScript 變數一樣,需等到網頁載入後,透過 URL.createObjectURL() 為 Blob 物件建立 Object URL,網頁結束後自動失效:
  • 第一個參數是回傳結果後執行。
  • 第二個參數是影像格式,如果沒有指定類型,預設為 image/png、96 dpi 解析度。
  • 第三個參數是影像品質(0~1),針對 image/jpg 和 image/webp 的參數,其他格式會自動忽略。
但因為 Blob 資料碼短、容量小,在 CDN 上也有加速功能,大多用在處理圖片跨域、隱藏視頻原路徑等等,不是拿來給用戶端下載的。 範例:10_繪製像素 / 01_createImageData_畫紅色半透明方塊圖
範例:10_繪製像素 / 02_圖片顏色反相
範例:10_繪製像素 / 03_色相分解與合成
範例:10_繪製像素 / 04_讀取圖片像素的顏色值
範例:10_繪製像素 / 05_圖片垂直水平翻轉
範例:10_繪製像素 / 06_圖片效果切換
範例:10_繪製像素 / 07_色版混合器
範例:10_繪製像素 / 08_放大像素
範例:10_繪製像素 / 09_儲存圖像
範例:10_繪製像素 / 10_影片與canvas鏡射
範例:10_繪製像素 / 11_匯入影片轉黑白
範例:10_繪製像素 / 12_匯入影片加濾鏡
範例:10_繪製像素 / 13_影片alpha色版去背

11建立人機互動

DOM 事件處理方法在 Canvas 繪製的圖形環境中已不再適用,無論 Canvas 上繪製多少圖形,都是一個整體,不可單獨取得,所以無法替某個圖形增加事件,實際上點擊的都是整個 <canvas>。

滑鼠事件

Canvas 由於其像素繪圖的本質,只能在 canvas 元素節點去處理,然後識別滑鼠指標發生在 Canvas 內部的哪一個圖形上,以下為步驟重點:
  1. 計算滑鼠指標的位置
    要獲得滑鼠指標在 Canvas 畫布上的位置,需要找到一個符合每個瀏覽器相容性的方法,目前只有 clientX、clientY、screeX、screeY 是完全相容,IE 9 以上才支援!
  2. 為 canvas 元素綁定事件
    canvas 元素支援的 DOM 滑鼠事件包含 click、mousedown、mouseup、mousemove、mouseover 或是裝置上的 touchstart、touchend、touchmove 等等。
  3. 判斷事件觸發與物件位置和圖形範圍
    當有了事件物件的座標位置,就要判斷 Canvas 裡的圖形是否覆蓋了這個座標,檢測圖形有兩種方法:對於規則(矩形、圓、橢圓、三角形)的圖形透過檢測滑鼠點擊的位置來確定是否被點擊;對於不規則的圖形只能使用 isPointInPath() 方法。
    使用 isPointInPath() 方法檢測一個座標是否在路徑內,路徑本身上的點也被認為是位於路徑內,但需注意該方法僅對目前路徑有效,如果 Canvas 中繪製了多個圖形,也只能檢測最後繪製的圖形,解決的方法是:當點擊事件發生時,重繪所有圖形路徑,每繪製一個路徑就使用 isPointInPath() 方法判斷事件座標是否在該圖形覆蓋範圍內。
  4. 判斷是否重疊
    如果有多個圖形存在重疊狀況,必須循環重繪來獲得事件圖形物件,確定哪個圖形位於最頂層。
    最好的方法是建立物件導向開發模型,所有圖形必須要建立對應的物件,來記錄他們所在的位置、大小、z-index,對應的物件放到一個陣列裡並按 z-index 排序,這樣當滑鼠事件觸發後,就可以按照 z-index 的順序來檢測滑鼠座標在不在某個區域內。
範例:11_建立人機互動 / 01_滑鼠事件基本檢測指標位置
範例:11_建立人機互動 / 02_滑鼠事件檢測形狀
範例:11_建立人機互動 / 03_滑鼠事件檢測路徑點
範例:11_建立人機互動 / 04_滑鼠拖曳物件
範例:11_建立人機互動 / 05_滑鼠繪圖板

鍵盤事件

相較滑鼠事件,鍵盤事件就比較簡單,無需計算目前圖形路徑,只要透過 window 物件來實現 Canvas 鍵盤事件的監聽,包含 keydown、keyup、keyCode 等等。 範例:11_建立人機互動 / 06_鍵盤事件移動位置

觸控事件

在 IOS3.2 和 Android2.1 之後的瀏覽器都支援觸控事件,但少數系統瀏覽器不支援,這時可以使用滑鼠事件模擬,觸控事件包含 touchstart、touchmove、touchend、touchcancel,一樣也有 clientX、clientY、screenX、screenY 屬性。 範例:01_基本用法

12物件導向註冊

在使用 Canvas 繪圖時,在畫布上繪製的是一個圖形的組合,每個圖形都是一個物件,並且很多圖形具有相同的抽象象徵,因此我們能用物件導向程式設計(OOP)開發繪圖、為圖形註冊監聽事件,工作就會變得更加簡便、結構化、具擴充性。

例如我們可以建立類別,包含它的屬性和方法,所以首先建立自訂委派的類別 EventDispatcher,讓所有自訂的 JavaScript 類別都可以繼承該類別,實現自訂物件委派事件。 範例:12_物件導向註冊

13碰撞和拾取檢測

滑鼠取得一個圖形物件的操作被稱為拾取(Picking),也就是目前要操作的圖形物件,檢視兩個圖形物件是否有相交則為碰撞檢測。

拾取檢測

依據範例封裝的方式,都是將物件都放置一個陣列變數 displayList 中,這是一個顯示清單,在陣列中的索引號就是繪圖層級,索引號越大,圖形就越在上層

然後利用 drawScene() 函數中檢查 displayList 陣列按順序繪製每個圖形,在呼叫 drawScene() 函數時,如果傳遞座標參數,就表示是滑鼠點擊位置,透過 isPointInPath() 方法檢測這個位置是否在圖形內,並根據層級關係設定屬性 isTarget 的值,當多個圖形都在點擊範圍內時,最上面的圖形物件屬性 isTarget 為 true,表示這個圖形為目前拾取的物件,每次點擊都會呼叫 drawScene() 函數重繪,改變每個 isTarget 的值,每次點擊時,仍要檢查 displayList 陣列。 範例:13_碰撞和拾取檢測 / 01_拾取圖形物件

碰撞檢測

檢視兩個圖形物件是否有相交,是遊戲開發以及其他互動操作的開發中必備的,目前存在很多用於碰撞檢測的演算法,但由於網頁執行速度的問題,太複雜的演算法會導致很多問題,因此很多遊戲在檢測碰撞都會兩種方式:
  • 使用矩形檢測
    為圖形物件定義一個專門用於碰撞檢測的屬性,屬性值就是一個包圍物件的無形矩形框,只須檢測兩個圖形物件的矩形框是否相交即可。 範例:13_碰撞和拾取檢測 / 02_使用矩形包圍體檢測碰撞
  • 使用像素檢測
    使用矩形包圍體計算十分快速,但精度稍缺,因此想實現高精度的碰撞檢測,可以在做完矩形碰撞後再進行逐像素檢測,減少部分運算量。
    檢測的方式就是在一個畫布分別畫兩個圖形,檢測同一位置都有像素,就表示相交。
    像素檢測方式一般會使用另一個隱藏的 canvas 元素來繪製要檢測的物件,可以加快速度和加強呈現效果,稱為「離屏繪製」。 範例:13_碰撞和拾取檢測 / 03_使用像素檢測碰撞

14最佳化方法

除了使用進階動畫計時器 requestAnimationFrame 可以有效加強使用者體驗,進一步減少資源佔用,其他與繪製無關的複雜計算應交給 Web Worker 來提高效率,針對不同應用,還有其他特殊技巧可以提高性能。

暫存圖繪製

暫存圖繪製又稱為預先繪製、多引擎繪製,透過 JavaScript 程式建立一個 canvas 元素而不增加到 DOM 裡的畫布,被稱為「暫存圖畫布(Off-Screen Canvas)」,首先把需要畫的內容畫到這個暫存畫布上,然後將畫好的內容繪製到需要呈現的 canvas 元素上:
                    var offscreen_canvas = document.createElement('canvas');
                    offscreen_canvas.width = 64;
                    offscreen_canvas.height = 64;
                    var offscreen_context = offscreen_canvas.getContext('2d');
                    drawSomthing(offscreen_context);
                
暫存圖的 canvas 寬高控制得越小越好,能剛好繪製需要的圖形,盡量是繪製後不會再改變的靜態內容,這個暫存圖 canvas 實際就是充當了一個圖形的快取作用,需要的時候,使用 drawImage() 方法繪製到目前 canvas 元素:
                    var canvas.document.getElementById('_2dCanvas');
                    var ctx = canvas.getContext('2d');
                    ctx.drawImage(offscreen_canvas, 0, 0);
                
使用暫存圖 canvas 快取已經載入的圖片可以提高效能,在目前 canvas 上畫暫存圖 canvas,而非畫 Image 物件,但是如果圖片過多,需要很多暫存 canvas,效能也會降低
另一件事是,除了用 clearRect() 方法清空畫布之外,還可以將畫布長寬設定為 0 來清空內容,然後再重置為原來的大小,好處是在垃圾收集器還沒回收該 Canvas 物件時,瀏覽器就可以先釋放它的快取,就可以避免瀏覽器因為快取佔用太多而不得不強制垃圾收集,但這個方法最好使用在暫存圖繪製 Canvas,不適用在 DOM 中的 Canvas(因為瀏覽器還要重新配置 2 次)。

使用多個 Canvas 實現多場景

在處理動畫時,經常遇到「圖層」的問題,可以建立多個畫布讓某些元件隔離,使撰寫和偵錯程式變得容易。

而且畫布是一個矩形的透明區域,可以透過 CSS 讓畫布重疊一起,例如在一個遊戲中,上層 canvas 元素用來繪製計分系統,另一個則用來繪製遊戲操作內容,將兩者配合起來以實現遊戲功能。

對於靜態無變化的背景,甚至可以在容器元素的 background-image 指定場景。

行動裝置注意事項

行動裝置與桌機效能相去甚遠,考慮到行動裝置的效能限制要注意:
  • Canvas 不宜過大
    Canvas 會依據螢幕寬度增減資源和記憶體頻寬的消耗,所以當畫布越大,資源會消耗越多,要特別斟酌使用。
  • 引用圖片不宜過大
    正常情況下,不要讓圖片在畫布上時再縮放,引入最合適的大小,圖片資源的解析度跟 Canvas 解析度也應保持一致。

其他最佳化注意事項

  • 減少浮點數
    因為 Canvas 上每個點都是一個像素,如果計算結果是浮點數也會在最後轉成整數,因此在進行像素等級的操作時盡量用整數,這樣可以減少運算量。
  • 線條粗細、圖片寬高用偶數
    因為奇數會導致線條平分在兩個像素中,此時核心會對這個進行處理,修正一個像素中。
  • 重繪畫布時盡量少改變繪圖範圍
    Canvas API 在動畫方面有一個主要限制:一旦在畫布上建立圖形,將無法改變它,要移動圖形就必須重新繪製。
  • 避免使用陰影相關屬性
    像是 shadowColor 屬性、shadowBlur 屬性、shadowOffsetX 和 shadowOffsetY 屬性會消耗大量資源,要注意盡量不要使用。

結語

目前 Canvas 被大量運用在商業網站上,應用的範圍很廣,像是動態影片、廣告橫幅、視覺特效、濾鏡處理、複雜圖形和物理運算、使用者編輯操作介面、圖像產生器、影片客製互動、2D 遊戲...等等。

但是試過以上原生 Canvas 的寫法後,一定會覺得這根本沒辦法短時間開發吧!!??所以大部分基於開發效率,還是會使用框架來撰寫!例如廣告橫幅,在商業應用上較常由 Adobe Animate CC 或是 Google Web Designer 等軟體產出 Canvas 碼,像是 Adobe Animate CC 就是使用 Create.js 框架轉化,而在前端,我大致區分以下三種 2D Canvas 網站框架:

會這麼區分是因為這三大類型也會區分為不同產業的工程師使用(最後一個甚至可以說是給藝術家使用),「動畫圖形開發」意旨一般商業型網站,包含動畫和操作介面;而遊戲開發較偏向遊戲公司開發 HTML5 遊戲時使用;至於 p5.js 的語法較偏向幾何圖形的操縱,主要還可以跟大量的外掛結合,例如 OpenCL, Webduino 等等,也是可以用在商業行開發,但大多是藝術家拿來做其他創作用。

開發建議如下:
  • 不需在畫布上點擊互動、主要以視覺效果為主
    原生 Canvas 開發並不容易,但是在較簡單的功能像是「靜態合圖」、「畫筆功能」等情況下,會建議使用原生寫法,或是需要大量由前端自行運算的視覺,或許原生的「像素繪製」會比較方便。
  • 需要在畫布上點擊互動、主要以動畫或編輯為主
    如果是偏向大量動畫和點擊式的互動,建議使用動畫相關型的框架,因為大多 Canvas 框架都有物件註冊功能,選取物件會比較方便,且針對動畫常搭配 Tween.js 套件,對於習慣使用 TweenMax 的前端來說比較容易。
  • 主要需要畫布內的元素做碰撞計算和其他物理效果
    屬遊戲類網站,最好使用遊戲引擎類的框架,因這類框架會有內建的碰撞和遊戲相關效果。

雖然說有框架可以用......
但實際上,很多視覺效果和功能還是要靠數學運算...orz 其實自己的開發經驗,有時還覺得 2D Canvas 比 3D 還難寫,因為 2D Canvas 的開發範圍太大,並沒有像 3D 需要的功能這麼明確,所以反倒 3D 框架已經把必備功能都寫好,而 2D 框架幾乎還是要自己去做運算~如果不能憑自己的數學能力寫出來,就請多多參考網友們的範例,並加入自己的修改吧!ˊ_>ˋ

後續會推出實戰應用的範例喔~
未來有空也會慢慢釋出框架筆記和較進階的寫法!
有任何心得也歡迎跟我討論交流! 謝謝 :D

Domo.

DEMO MEMO