天天看點

Canvas簡介

Canvas API提供了一個通過JavaScript和HTML的<canvas>來繪制圖形的方式,用于動畫,遊戲通話,資料可視化,圖檔編輯等方面。

Canvas API主要聚焦于2D圖形,同樣使用<canvas>元素的WebGL API則用于繪制硬體加速的2D和3D圖形。

1 基礎執行個體

下面這個簡答的例子在畫布上繪制一個綠色的長方形。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Canvas</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'green';
    ctx.fillRect(10, 10, 150, 100);
</script>
</body>
</html>       

結果如下:

Canvas簡介

2 基本用法

2.1 canvas元素

<canvas id="tutorial" width="150" height="150"></canvas>       

<canvas>元素看起來和<img>元素很像,唯一不同的是它沒有src,alt這樣的屬性,實際上<canvas>标簽隻有兩個屬性,width和height。這些都是可選的,同樣可以使用dom properties來設定。如不指定寬度和高度,Canvas會初始化為寬度為300像素,高度為150像素。也可以使用css來定義大小,但在繪制圖像會伸縮以适應它的架構尺寸:如果css的尺寸與初始畫布的比例不一緻,它會出現扭曲。

注意:如果繪制出來的圖像是扭曲的,嘗試使用width和height屬性為<canvas>明确規定寬高,而不是使用css。

id屬性不是<canvas>元素特有的,而是每一個HTML元素都預設具有的屬性,給<canvas>标簽設定一個id屬性友善在腳本中找到它。

<canvas>元素可以像任何一個普通圖像一樣(有margin,padding,border,background等屬性)被設計。然而,這樣的樣式不會影響在Canvas中的實際圖像,後面會講到。當開始時沒有為canvas規定樣式規則,其将會完全透明。

替換内容

<canvas>元素與<img>标簽的不同之處在于,就像<video>,<audio>,或者<picture>元素一樣,很容易定義一些替代的内容。由于某些較老的浏覽器(尤其是IE9之前版本的IE浏覽器)或者文本浏覽器不支援HTMLcanvas元素,在這些浏覽器上應該總是能展示替代内容。

這非常簡答,我們隻是在<canvas>标簽中提供了替換内容。不支援<canvas>的浏覽器會忽略容器并在其中渲染替代内容。而支援<canvas>的浏覽器會忽略在容器中包含的内容,并且隻是正常渲染canvas。

舉個例子,我們可以提供對canvas内容的文字描述或者動态生成内容相對應的圖檔,如下所示:

<canvas id="stockGraph" width="150" height="150">
    current stock price: $3.15 +0.15
</canvas>
<canvas id="clock" width="150" height="150">
    <img src="./../images/pic1.jpg" width="150" height="150" alt=""/>
</canvas>       

</canvas>标簽不可省

與<img>元素不同,<canvas>元素需要結束标簽</canvas>。如果結束标簽不存在,則文檔其餘部分會被認為是替代内容,将不會顯示出來。

如果不需要替代内容,一個簡單的<canvas id="foo"></canvas>在所有支援canvas的浏覽器中都是完全相容的。

2.2 渲染上下文(The rendering context)

<canvas>元素創造了一個固定大小的畫布,它公開了一個或多個渲染上下文,其可以用來繪制和處理要展示的内容。我們将注意力放在2D渲染上下文中。其它種類的上下文也提供了不同種類的渲染方式,比如,WebGL使用了基于OpenGL ES的3D上下文。

canvas期初是空白的。為了展示,首先腳本需要找到渲染上下文,然後在它的上面繪制。<canvas>元素有一個叫做getContext()的方法,這個方法是用來獲得渲染上下文和它的繪畫功能。getContext()有一個參數,指定上下文的格式。對于2D圖像而言,如文本,可以使用CanvasRenderingContext2D。

var canvas = document.getElementById('tutorial');
var ctx = canvas.getContext('2d');       

代碼第一行通過使用getElementById()方法為<canvas>元素得到dom對象,一旦有了元素對象,就可以使用它的getContext()方法來通路繪畫上下文。

2.3 檢查支援特性

替換内容是用在浏覽器不支援<canvas>标簽的場合。通過簡單的測試getContext()方法的存在,腳本可以檢查程式設計支援性。上面的代碼可以改成下面這樣:

var canvas = document.getElementById('tutorial');

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}       

2.4 一個模闆骨架

這是一個最簡單的模闆,我們可以把它看做之後例子的起點。

<html>
  <head>
    <title>Canvas tutorial</title>
    <script type="text/javascript">
      function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
          var ctx = canvas.getContext('2d');
        }
      }
    </script>
    <style type="text/css">
      canvas { border: 1px solid black; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="150" height="150"></canvas>
  </body>
</html>       

上面腳本中包含一個叫draw()的函數,當頁面加載結束的時候會執行這個函數。通過使用在文檔上加載事件來完成。隻要頁面加載結束,這個函數或者是類似這樣的,同樣可以使用window.setTimeOut(),window.setInterval()或者其他任何處理程式來調用。最後,這個例子最後生成了一個空白的canvas。

2.5 一個簡單的例子

一開始,我們看個簡單的例子,繪制了兩個長方形,其中跟一個有着alpha透明度。我們在接下來的例子中探讨它是如何工作的。

<html>
 <head>
  <script type="application/javascript">
    function draw() {
      var canvas = document.getElementById("canvas");
      if (canvas.getContext) {
        var ctx = canvas.getContext("2d");

        ctx.fillStyle = "rgb(200,0,0)";
        ctx.fillRect (10, 10, 55, 50);

        ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
        ctx.fillRect (30, 30, 55, 50);
      }
    }
  </script>
 </head>
 <body onload="draw();">
   <canvas id="canvas" width="150" height="150"></canvas>
 </body>
</html>       

最後運作它的結果是這樣的:

Canvas簡介

3 使用canvas繪制圖形

這裡我們來學習如何使用canvas繪制矩形,三角形,直線,圓弧和曲線。

3.1 格栅 

在我們開始畫圖之前,先了解一下畫布栅格以及坐标空間。上面1.2.5中的HTML模闆有個寬150px,高150px的canvas元素。如下圖所示,canvas元素預設被網格所覆寫。通常來說,網格的一個單元相當于canvas元素中的已像素。栅格的起點為左上角(坐标為(0, 0))。所有元素的位置都相對于遠點定位。是以途中藍色正方形左上角的坐标為距離左邊(X軸)x像素,距離上邊(Y軸)y像素,坐标為(x, y)。

3.2 繪制矩形

不同于SVG,<canvas>隻支援兩種形式的圖形繪制,矩形和路徑(由一系列的點連成的線段)。所有其他類型的圖形都是通過一條或者多條路徑組合而成的。不過我們擁有衆多路徑生成方法讓複雜圖形的繪制成為可能。

canvas提供三種方式繪制矩形:

fillRect(x, y, width, height):繪制一個填充的矩形

strokeRect(x, y, width, height):繪制一個矩形的邊框

clearRect(x, y, width, height):清除指定矩形區域,讓清除部分完全透明

上面的方法中都包含了相同的參數。x與y指定了在canvas畫布上所繪制的矩形的左上角(相對與原點)的坐标。width和height設定矩形的尺寸。

矩形(Rectangular)例子

下面看看三個方法調用的效果

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    ctx.fillRect(25, 25, 100, 100);
    ctx.clearRect(45, 45, 60, 60);
    ctx.strokeRect(50, 50, 50, 50);
  }
}       
Canvas簡介

fillRect()繪制了一個邊長為100px的黑色正方形。clearRect()函數從正方形的中心開始擦除一個60*60px的正方形,接着使用strokeRect()在清除的區域内生成一個50*50的正方形邊框。 

接下來我們能夠看到clearReact()的兩個可選方法,然後我們會知道如何改變渲染圖形的填充顔色及描邊顔色。

不同于下面介紹的路徑函數(path function),以上的三個函數繪制之後會馬上顯現在canvas上,即時生效。

3.3 繪制路徑

圖形的基本元素是路徑。路徑是通過不同顔色和寬度的線段或曲線連成的不同形狀的點的集合。一個路徑,甚至一個子路徑,都是閉合的。使用路徑繪制圖形需要一些額外的不住。

  1. 首先,需要建立路徑起點。
  2. 然後使用畫圖指令去畫出路徑。
  3. 之後把路徑封閉。
  4. 一旦路徑生成,就能通過描邊或者填充路徑區域來渲染圖形。

下面是所要用到的函數:

beginPath()

建立一條路徑,生成之後,圖形繪制指令被指向到路徑上生成路徑。

closePath()

閉合路勁過之後圖形繪制指令有重新指向上下文中。

stroke()

通過線條來繪制圖形輪廓。

fill()

通過填充路徑的内容區域生成實心的圖形。

生成路徑的第一步叫做beginPath()。本質上,路徑是由很多子路徑構成,這些子路徑都是在一個清單中,所有的子路徑(線,弧形,等)構成圖形。而每次這個方法調用之後,裡诶包清空重置,然後我們可以重新繪制新的圖形。

注意:目前路徑為空,即調用beginPath()之後,或者canvas剛健的時候,第一條路徑構造指令通常被視為是moveTo(),無論實際上是什麼。出于這個原因,你幾乎總是要在設定路徑之後專門指定起始位置。

第二步就是調用函數指定繪制路徑。

第三,就是閉合路徑closePath(),不是必須的。這個方法會通過繪制一條從目前點到開始點的直線來閉合圖形。如果目前點就是開始點,即圖形已經閉合了,該函數什麼也不做。

注意:當你調用fill()函數時,所有沒有閉合的形狀都會自動閉合,是以不需要調用closePath()函數,但是調用stroke()時不會自動閉合。

繪制一個三角形

例如,繪制三角形的代如下:

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                ctx.beginPath();
                ctx.moveTo(75, 50);
                ctx.lineTo(100, 75);
                ctx.lineTo(100, 25);
                ctx.fill();
            }
        }       

輸出如下:

Canvas簡介

移動筆觸

一個非常有用的函數,而這個函數實際上并不能畫出任何東西,也是上面所描述的路徑清單的一部分,這個函數就是moveTo()。或者可以想象一下在紙上作業,一支鋼筆或者鉛筆的筆尖從一個點到另一個點的移動過程。

moveTo(x, y)

将筆觸移動到指定的坐标(x, y)上。

當canvas初始化或者beginPath()調用後,通常會使用moveTo()函數。通常會使用moveTo()函數設定為起點。也可以使用moveTo()繪制一些不連續的路徑。看下面笑臉的例子。我們使用moveTo()方法第地方使用紅線标記。

// 繪制一個笑臉
        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');
                ctx.beginPath();

                // 畫一個圓
                ctx.arc(75, 75, 50, 0, Math.PI * 2, true)
                ctx.stroke()

                // 嘴巴
                ctx.moveTo(110, 75)
                ctx.arc(75, 75, 35, 0, Math.PI, false)

                // 左眼
                ctx.moveTo(65, 65)
                ctx.arc(60, 65, 5, 0, Math.PI * 2, true)

                // 右眼
                ctx.moveTo(95, 65)
                ctx.arc(90, 65, 5, 0, Math.PI * 2, true)
                ctx.stroke()
            }
        }       

最後結果如下:

Canvas簡介

繪制直線,需要用到的方法lineTo()

lineTo(x, y)

繪制一條從目前位置到指定位置(x, y)的直線。

該方法有兩個參數:x,y,代表坐标系中直線結束的點。開始點和之前繪制路徑有關,之前路徑結束點就是接下來的開始點。開始點也可以通過moveTo()函數改變。

下面例子繪制兩個三角形,一個是填充的,一個是描邊的。 

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                // 填充三角形
                ctx.beginPath();
                ctx.moveTo(25, 25);
                ctx.lineTo(105, 25);
                ctx.lineTo(25, 105);
                ctx.fill();

                // 描邊三角形
                ctx.beginPath();
                ctx.moveTo(125, 125);
                ctx.lineTo(125, 45);
                ctx.lineTo(45, 125);
                ctx.closePath();
                ctx.stroke();
            }
        }       

結果從調用beginPah()函數準備繪制一個新的形狀路徑開始。然後使用moveTo函數移動到目标位置上。繪制結果如下:

Canvas簡介

填充與描邊三角形步驟有所不同,上面提到,因為路徑使用填充(fill)時,路徑自動閉合,使用描邊(stroke)則不會閉合路徑。如果沒有添加閉合路徑closePath()到描邊三角形中,則隻繪制了兩條線,并不是一個完整的三角形。

圓弧

繪制圓弧或者圓,使用arc方法。當然可以使用arcTo(),不過後者不是那麼可靠,這裡不作介紹。

arc(x, y, radius, startAngle, endAngle, anticlockwise)arc(x, y, radius, startAngle, endAngle, anticlockwise)

畫一個以(x,y)為圓心,radius為半徑的圓弧(圓),從startAngle開始到endAngle結束,按照anticlockwise給定的方向(預設是順時針)來生成圓軌迹。

arcTo(x1, y1, x2, y2, radius)

根據給定的控制點和邊境華一段圓弧,再已直線連接配接兩個控制點。

arc方法有6個參數,x,y為繪制的圓弧坐在圓上的圓心坐标。radius為半徑。startAngle以及endAngle參數用弧度定義了開始以及結束的弧度。這些都是以x軸為基準。參數articlockwise為一個布爾值,為true時,是逆時針方向,否則順時針方向。

注意:arc()函數中表示角的機關是弧度,不是角度。角度和弧度的js表達式:弧度=(Math.PI/180)*角度

下面的例子繪制12個不同的角度以及填充的圓弧。

下面兩個for循環,生成圓弧的行列(x,y)坐标。每一段圓弧的開始都調用beginPath()。代碼中,每個圓弧的參數都是可變的,實際程式設計中,不需要這樣做。

(x,y)坐标是可變的。半徑(radius)和開始角度(startAngle)都是固定的。結束角度(endAngle)在第一列開始時是180度(半圓)然後每列增加90度。最後一列形成一個完整的圓。

在第一行,第三行是順時針的圓弧中使用clockwise語句,第二行,第四行是逆時圓弧中使用anticlockwise。if語句讓第一行,第二行描邊圓弧,下面兩行填充路徑。

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');
                for (var i = 0; i < 4; i++) {
                    for (var j = 0; j < 3; j++) {
                        ctx.beginPath();
                        var x = 25 + j * 50; // x 坐标值
                        var y = 25 + i * 50; // y 坐标值
                        var radius = 20; // 圓弧半徑
                        var startAngle = 0; // 開始點
                        var endAngle = Math.PI + (Math.PI * j) / 2; // 結束點
                        var anticlockwise = i % 2 === 0; // 順時針或逆時針

                        ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

                        if (i > 1) {
                            ctx.fill();
                        } else {
                            ctx.stroke();
                        }
                    }
                }
            }
        }       

 繪制結果如下:

Canvas簡介

二次貝塞爾曲線和三次貝塞爾曲線

下面例子介紹很有用的路徑,貝塞爾曲線。二次以及三次貝塞爾曲線都很有用,用來繪制複雜有規律的圖形。

quadraticCurveTo(cp1x, cp1y, x, y)

繪制二次貝塞爾曲線,cp1x,cp2x為一個控制點,x,y為結束點。

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

繪制三次貝塞爾曲線,cp1x,cp1y為控制點一,cp2x,cp2y為控制點二,x,y為結束點。

下圖能購很好的描述二者之間的關系,二次貝塞爾曲線有一個開始點(藍色),一個結束點(藍色),以及一個控制點(紅色),而三次貝塞爾曲線有兩個控制點。

參數x,y在這兩個方法中都是結束點坐标。cp1x,cp1y為坐标中的第一個控制點,cp2x,cp2y為坐标中的第二個控制點。

使用二次以及三次貝塞爾曲線有一定難度,因為不同于像Adobe IIlustrators這樣的矢量軟體,我們所繪制的曲線沒有給我們提供直接的視覺回報。這個繪制複雜的圖形變得十分困難。在下面的例子中,我們會繪制一些簡單的有規律的圖形。

二次貝塞爾曲線

這個例子使用多個貝塞爾曲線來渲染對話氣泡。

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                // 二次貝塞爾曲線
                ctx.beginPath();
                ctx.moveTo(75, 25);
                ctx.quadraticCurveTo(25, 25, 25, 62.5);
                ctx.quadraticCurveTo(25, 100, 50, 100);
                ctx.quadraticCurveTo(50, 120, 30, 125);
                ctx.quadraticCurveTo(60, 120, 65, 100);
                ctx.quadraticCurveTo(125, 100, 125, 62.5);
                ctx.quadraticCurveTo(125, 25, 75, 25);
                ctx.stroke();
            }
        }       
Canvas簡介

 三次貝塞爾曲線

這個例子使用三次貝塞爾曲線繪制心型。

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                //三次貝塞爾曲線
                ctx.beginPath();
                ctx.moveTo(75, 40);
                ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
                ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
                ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
                ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
                ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
                ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
                ctx.fill();
            }
        }      
Canvas簡介

矩形

直接在畫布上繪制矩形的三個額外方法,此外,也有rect()方法,将一個矩形路徑增加到目前路徑上。

rect(x, y, width, height)

繪制一個左上角坐标為(x,y),寬,高為width和height的矩形。

該方法執行的時候,moveTo()方法i自動設定坐标參數(0,0)。也就是說,目前筆觸自動重置回預設坐标。 

組合使用

目前為止,每一個李子東的圖形都隻用到一種類型的路徑,然後繪制一個圖形沒有顯示使用數量及類型。是以在最後一個例子中,我們組合使用各種路徑來重制一款著名的遊戲。

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                roundedRect(ctx, 12, 12, 150, 150, 15);
                roundedRect(ctx, 19, 19, 150, 150, 9);
                roundedRect(ctx, 53, 53, 49, 33, 10);
                roundedRect(ctx, 53, 119, 49, 16, 6);
                roundedRect(ctx, 135, 53, 49, 33, 10);
                roundedRect(ctx, 135, 119, 25, 49, 10);

                ctx.beginPath();
                ctx.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7, false);
                ctx.lineTo(31, 37);
                ctx.fill();

                for (var i = 0; i < 8; i++) {
                    ctx.fillRect(51 + i * 16, 35, 4, 4);
                }

                for (i = 0; i < 6; i++) {
                    ctx.fillRect(115, 51 + i * 16, 4, 4);
                }

                for (i = 0; i < 8; i++) {
                    ctx.fillRect(51 + i * 16, 99, 4, 4);
                }

                ctx.beginPath();
                ctx.moveTo(83, 116);
                ctx.lineTo(83, 102);
                ctx.bezierCurveTo(83, 94, 89, 88, 97, 88);
                ctx.bezierCurveTo(105, 88, 111, 94, 111, 102);
                ctx.lineTo(111, 116);
                ctx.lineTo(106.333, 111.333);
                ctx.lineTo(101.666, 116);
                ctx.lineTo(97, 111.333);
                ctx.lineTo(92.333, 116);
                ctx.lineTo(87.666, 111.333);
                ctx.lineTo(83, 116);
                ctx.fill();

                ctx.fillStyle = "white";
                ctx.beginPath();
                ctx.moveTo(91, 96);
                ctx.bezierCurveTo(88, 96, 87, 99, 87, 101);
                ctx.bezierCurveTo(87, 103, 88, 106, 91, 106);
                ctx.bezierCurveTo(94, 106, 95, 103, 95, 101);
                ctx.bezierCurveTo(95, 99, 94, 96, 91, 96);
                ctx.moveTo(103, 96);
                ctx.bezierCurveTo(100, 96, 99, 99, 99, 101);
                ctx.bezierCurveTo(99, 103, 100, 106, 103, 106);
                ctx.bezierCurveTo(106, 106, 107, 103, 107, 101);
                ctx.bezierCurveTo(107, 99, 106, 96, 103, 96);
                ctx.fill();

                ctx.fillStyle = "black";
                ctx.beginPath();
                ctx.arc(101, 102, 2, 0, Math.PI * 2, true);
                ctx.fill();

                ctx.beginPath();
                ctx.arc(89, 102, 2, 0, Math.PI * 2, true);
                ctx.fill();
            }
        }

        // 封裝的一個用于繪制圓角矩形的函數.
        function roundedRect(ctx, x, y, width, height, radius) {
            ctx.beginPath();
            ctx.moveTo(x, y + radius);
            ctx.lineTo(x, y + height - radius);
            ctx.quadraticCurveTo(x, y + height, x + radius, y + height);
            ctx.lineTo(x + width - radius, y + height);
            ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
            ctx.lineTo(x + width, y + radius);
            ctx.quadraticCurveTo(x + width, y, x + width - radius, y);
            ctx.lineTo(x + radius, y);
            ctx.quadraticCurveTo(x, y, x, y + radius);
            ctx.stroke();
        }       

繪制結果如下:

Canvas簡介

 這裡不詳細講解上面的代碼,重點是繪制上下文中使用到了fillStyle屬性,以及封裝函數(例子中的roundedReact())。封裝函數對與減少代碼量以及複雜程度十分有用。後面會講到fillStyle更多細節,這裡僅僅是改變顔色,有預設的黑色到白色,然後又是黑色。

1.3.4 Path2D對象

正如前面的例子可以看到,可以使用一些列的路徑和繪畫指令來把對象"畫"在畫布上。為了簡化代碼和提高性能,Path2D對象已經可以在新版本的浏覽器中使用,用來緩存或記錄繪畫指令,這樣能快速地回顧路徑。

如何産生一個Path2D對象呢?

Path2D()

Path2d()會傳回一個初始化的Path2D對象(可能将某一個路徑作為變量-建立一個它的副本,或者将一個包含SVG path資料的字元串作為變量)。

new Path2D();     // 空的Path對象
        new Path2D(path); // 克隆Path對象
        new Path2D(d);    // 從SVG建立Path對象            

所有路徑方法比如moveTo,reac,arc或者quadraticCurveTo等,如我們前面見過的,都可以在Path2D中使用。

Path2D API添加了addPath作為将path結合起來的方法。當你想從幾個元素中來建立對象時,這将很實用,比如:

Path2D.addPath(path [, transform])

添加一條路徑到目前路徑(可能添加了一個變換矩陣)

Path2D示例

在這個例子中,我們創造了一個矩形和一個圓,他們都被村委Path2D對象,後面再排上用場。随着新的Path2D API産生,幾種方法也相應地被更新來使用Path2D對象而不是目前路徑。在這裡,待參數的stroke和fill可以把對象畫在畫布上。

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                var rectangle = new Path2D();
                rectangle.rect(10, 10, 50, 50);

                var circle = new Path2D();
                circle.moveTo(125, 35);
                circle.arc(100, 35, 25, 0, 2 * Math.PI);

                ctx.stroke(rectangle);
                ctx.fill(circle);
            }
        }       
Canvas簡介

使用SVG Paths

新的Path2D API有另一個強大的特點,就是使用SVG path data來初始化canvas上的路徑。這樣在擷取路徑時可以以SVG或者canvas的方式來重用他們。

這條路徑将先移動到(M10 10)然後再水準移動80個機關(h 80),然後下移80個機關(v 80),接着左移80個機關(h -80),再回起點(Z)。

var p = new Path2D("M10 10 h 80 v 80 h -80 Z");      

4 使用樣式和顔色

上面隻用到預設的線條和填充樣式,這裡探讨canvas的全部可選項,來繪制更加吸引人的内容。

4.1 顔色Colors

到目前位置,我們隻看到過繪制内容的方法。如果要想給圖形上色,有兩個重要的屬性可以做到:fillStyle,strokeStyle。

fillStyle = color::設定圖形的填充顔色

strokeStyle = color:設定圖形輪廓的顔色

color可以是css顔色值字元串,漸變對象或者是圖案對象。預設情況下線條和填充顔色都是黑色的(css值是#000000)

注意:一旦設定了strokeStyle或者fillStyle的值,那麼這個新值就會成為新繪制的圖形的預設值。如果想要給每個圖形上不同的顔色,需要重新設定fillStyle或strokeStyle的值。

你輸入的值應該是符合css3标準的有效字元串,下面的例子都表示同一種顔色。

// 這些 fillStyle 的值均為 '橙色'
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";       

fillStyle示例

在本示例中,我們會再度用兩層for循環來繪制方格陣列,每個方格不同的顔色。結果如下圖,但是實作所用的代碼沒有那麼複雜。使用兩個變量i,j來為每個方格産生唯一的RGB色彩值,其中僅修改紅色和綠色通道的值,而保持藍色通道的值不變。可以通過修改這些顔色通道的值來産生各種各樣的顔色闆。通過增加漸變的頻率,還可以繪制出類似Photoshop中的調色闆。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  for (var i=0;i<6;i++){
    for (var j=0;j<6;j++){
      ctx.fillStyle = 'rgb(' + Math.floor(255-42.5*i) + ',' + 
                       Math.floor(255-42.5*j) + ',0)';
      ctx.fillRect(j*25,i*25,25,25);
    }
  }
}       
Canvas簡介

strokeStyle示例

這個示例與上面的有點類似,單這裡用到的是strokeStyle屬性,畫的不是方格,而是用arc來畫圓。

function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    for (var i=0;i<6;i++){
      for (var j=0;j<6;j++){
        ctx.strokeStyle = 'rgb(0,' + Math.floor(255-42.5*i) + ',' + 
                         Math.floor(255-42.5*j) + ')';
        ctx.beginPath();
        ctx.arc(12.5+j*25,12.5+i*25,10,0,Math.PI*2,true);
        ctx.stroke();
      }
    }
  }       
Canvas簡介

4.2 透明度

除了繪制實色圖形,還可以用canvas來繪制半透明的圖形。通過設定globalAlpha屬性或者使用一個半透明的顔色作為輪廓或填充的樣式。

globalAlpha = transparencyValue:這個屬性影響到canvas中所有圖形的透明度,有效的值範圍是0.0(完全透明)到1.0(全完不透明),預設值是1.0.

globalAlpha屬性在需要繪制大量擁有相同透明度的圖形的時候相當高效。不過我認為下面的方法可操作性更強一些。

因為strokeStyle和fillStyle屬性接受符合css3規範的顔色值,那麼我們可用rgba對象來設定具有透明度的顔色。

// 指定透明顔色,用于描邊和填充樣式
ctx.strokeStyle = "rgba(255,0,0,0.5)";
ctx.fillStyle = "rgba(255,0,0,0.5)";       

rgba()方法與rgb()方法類似,就多了一個用于設定色彩透明度的參數。它的有效範圍是從0.0(完全透明)到1.0(完全不透明)。

globalAlpha執行個體

在這個例子裡,用四色格作為北京,設定globalAlpha為0.2後,在上面畫一系列半徑遞增的透明圓。最終結果是一個徑向漸變的效果。圓疊加得越多,原先所畫的透明度會約低。通過增加循環次數,畫更多的圓,從中心到邊緣部分,背景會呈現逐漸消失的效果。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  // 畫背景
  ctx.fillStyle = '#FD0';
  ctx.fillRect(0,0,75,75);
  ctx.fillStyle = '#6C0';
  ctx.fillRect(75,0,75,75);
  ctx.fillStyle = '#09F';
  ctx.fillRect(0,75,75,75);
  ctx.fillStyle = '#F30';
  ctx.fillRect(75,75,75,75);
  ctx.fillStyle = '#FFF';

  // 設定透明度值
  ctx.globalAlpha = 0.2;

  // 畫半透明圓
  for (var i=0;i<7;i++){
      ctx.beginPath();
      ctx.arc(75,75,10+10*i,0,Math.PI*2,true);
      ctx.fill();
  }
}       
Canvas簡介

rgba()執行個體

下面這個例子和上面類似,不過不是畫圓,而是畫矩形。這裡還可以看出,rgba()可以分别設定輪廓和填充樣式,因而具有更好的可操作性和靈活性。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');

  // 畫背景
  ctx.fillStyle = 'rgb(255,221,0)';
  ctx.fillRect(0,0,150,37.5);
  ctx.fillStyle = 'rgb(102,204,0)';
  ctx.fillRect(0,37.5,150,37.5);
  ctx.fillStyle = 'rgb(0,153,255)';
  ctx.fillRect(0,75,150,37.5);
  ctx.fillStyle = 'rgb(255,51,0)';
  ctx.fillRect(0,112.5,150,37.5);

  // 畫半透明矩形
  for (var i=0;i<10;i++){
    ctx.fillStyle = 'rgba(255,255,255,'+(i+1)/10+')';
    for (var j=0;j<4;j++){
      ctx.fillRect(5+i*14,5+j*37.5,14,27.5)
    }
  }
}       
Canvas簡介

1.4.2 線型 Line styles

可以通過一系列的屬性來設定線的樣式。

lineWidth = value:設定線條的寬度

lineCap = type:設定線條末端樣式

lineJoin = type:設定線條與線條間接合處的樣式

miterLimit = value:顯示當兩條線相交時交接處最大的長度,所謂交接處長度(斜接長度)是指線條交界處内角頂點到外角頂點的長度

getLineDash():傳回一個包含目前虛線樣式,長度為非負偶數的數組

lineDashOffset = value:設定虛線樣式的起始偏移量

通過下面的例子可能會更加容易的了解。

lineWidth屬性的例子

這個屬性設定目前繪線的粗細。屬性值必須為正數,預設值是1.0。

線寬是指給定路徑的中心到兩邊的粗細。換句話說就是在路徑的兩邊各繪制線寬的一半。因為畫布的坐标并不和像素直接對應,當需要獲得精确的水準或者垂直線的時候要特别注意。

在下面的例子中,用遞增的寬度繪制了10條直線。最左邊的線寬1.0機關。并且最左邊的以及所有寬度為奇數線并不能精确呈現,這就是因為路徑定位的問題。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        for (var i = 0; i < 10; i++) {
            ctx.lineWidth = 1 + i;
            ctx.beginPath();
            ctx.moveTo(5 + i * 14, 5);
            ctx.lineTo(5 + i * 14, 140);
            ctx.stroke();
        }
    }       

要想獲得精确的線條,必須對線條是如何描繪出來的有所了解。見下圖,用網格來代表canvas的坐标格,每一格對應螢幕上的一個像素點。在第一個圖中,填充了(2,1)至(5,5)的矩形,整個區域的邊界剛好落在像素的邊緣上,這樣可以得到的矩形有清晰的邊緣。

Canvas簡介

如果想繪制一條從(3,1)到(3,5),寬度是1.0的線條,會得到像第二幅圖一樣的結果。實際填充區域(深藍色部分)僅僅延伸至路徑兩旁各一半像素。而這半個像素又會以近似的方式進行渲染,這意味這那些像素隻是部分着色,結果是以實際筆觸顔色一半色調的顔色來填充整個區域(淺藍色和深藍的部分)。這就是上例中為何寬度為1.0的線并不準确的原因。

要解決這個問題,必須對路徑控制得更加精确。1.0的線條會在路徑兩邊延伸半像素,那麼第三幅那樣繪制從(3.5,1)到(3.5,5)的線條,其邊緣正好落在像素邊界,填充出來就是準确的1.0的線條。

注意:在這個豎線的例子中,其Y坐标剛好落在網格線上,否則斷點上同樣會出現半渲染的像素點(這種行為取決于目前的lineCap風格,它預設為butt;可以通過lineCap樣式設定為square正方形,來得到與技術寬度線的半像素坐标一緻的筆畫,這樣端點輪廓外邊框将自動擴充以完全覆寫整個像素格)。

隻有路徑的起點,終點受詞影響:如果一個路徑是通過closePath()來封閉的,它沒有起點和終點;相反的情況下,路徑上的所有端點都與上一個點相連,下一段路徑使用點錢的lineJoin設定(預設為miter),如果路徑是水準或垂直的話,會導緻相連路徑的外輪廓根據焦點自動延伸,是以渲染出的路徑輪廓會覆寫整個像素格。

對于那些寬度為偶數的線條,每一邊的像素都是整數,那麼想要其路徑是落在像素點之間(如從(3,1)到(3,5))而不是在像素點的中間。同樣,注意到那個例子的垂直線條,其Y坐标剛好落在網格線上,如果不是的話,斷點上同樣會出現半渲染的像素點。

雖然開始處理可縮放的2D圖形時會很痛苦,的那是及早注意到像素網格與路徑位置之間的關系,可以確定圖形在經過縮放或者是其他任何變形後都可以保持看上去蠻好:線寬為1.0的垂線在放大2倍後,會變成線寬為2.0,并且出現在它應該出現的位置上。

lineCap屬性的例子

屬性lineCap的值決定了線段斷點顯示的樣子。它可以為下面三種的之一:butt,round和square。預設是butt。

在這個例子中,繪制了三條直線,分别賦予不同的lineCap值。還有兩條輔助線,為了可以看得更清除他們之間的差別,三條線的起點重點都落在輔助線上。

最左邊的線使預設的butt。可以注意到它與輔助線齊平。中間是round的效果,斷點處加上了半徑為一半線寬的半圓。右邊是square的效果,端點處加上了等寬且高度為一半線寬的方塊。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        var lineCap = ['butt', 'round', 'square'];

        // 建立路徑
        ctx.strokeStyle = '#09f';
        ctx.beginPath();
        ctx.moveTo(10, 10);
        ctx.lineTo(140, 10);
        ctx.moveTo(10, 140);
        ctx.lineTo(140, 140);
        ctx.stroke();

        // 畫線條
        ctx.strokeStyle = 'black';
        for (var i = 0; i < lineCap.length; i++) {
            ctx.lineWidth = 15;
            ctx.lineCap = lineCap[i];
            ctx.beginPath();
            ctx.moveTo(25 + i * 50, 10);
            ctx.lineTo(25 + i * 50, 140);
            ctx.stroke();
        }
    }       
Canvas簡介

linJoin屬性的例子

lineJoin的屬性值決定了圖形中兩線端連接配接處所顯示的樣式。它可以是這三種之一:round,bevel和miter。預設是miter。這裡用三條折線來做例子,分别設定不同的lineJoin值。最上面一條是round的效果,外邊角處被磨圓了,圓的半徑等于線寬。中間和最下面一條分别是bevel和miter的效果。當值是miter的時候,線段會在連接配接處外側延伸直至相交玉于一點,延伸效果受下面要介紹的miterLimit屬性的制約。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        var lineJoin = ['round', 'bevel', 'miter'];
        ctx.lineWidth = 10;
        for (var i = 0; i < lineJoin.length; i++) {
            ctx.lineJoin = lineJoin[i];
            ctx.beginPath();
            ctx.moveTo(-5, 5 + i * 40);
            ctx.lineTo(35, 45 + i * 40);
            ctx.lineTo(75, 5 + i * 40);
            ctx.lineTo(115, 45 + i * 40);
            ctx.lineTo(155, 5 + i * 40);
            ctx.stroke();
        }
    }       
Canvas簡介

miterLimit屬性的例子

就如上一個例子中的miter效果,線段外側邊緣會延伸交彙于一點上。線段直接夾角比較大的,交點不會太遠,但當夾角減少時,交點距離會呈指數級增大。miterLimit屬性就是用來設定外延交點與連接配接點的最大距離,如果交點距離大于此值,連接配接效果變成了bevel。

手動改變miterLimit的值,觀察其影響效果。藍色輔助線顯示鋸齒折線段的起點與終點所在的位置。

Canvas簡介

使用虛線

使用setLineDash方法和lineDashOffset屬性來指定虛線樣式,setLineDash方法接受一個數組,來指定線段與間隙的交替;lineDashOffset屬性設定其實偏移量。它往往應用在計算機圖形程式選區工具動效中。它可以幫助使用者通過動畫的邊界來區分圖像背景選區邊框。在本教程的後面部分,你可以學習如何實作這一點和其他基本的動畫。

在下面這個例子中,我們要建立一個螞蟻線的效果。

var ctx = document.getElementById('canvas').getContext('2d');
    var offset = 0;

    function draw() {
        ctx.clearRect(0,0, canvas.width, canvas.height);
        ctx.setLineDash([4, 2]);
        ctx.lineDashOffset = -offset;
        ctx.strokeRect(10,10, 100, 100);
    }

    function march() {
        offset++;
        if (offset > 16) {
            offset = 0;
        }
        draw();
        setTimeout(march, 20);
    }
    march();       
Canvas簡介

4.3 漸變Gradients

就好像一般的繪圖軟體一樣,我們可以用線性或者徑向的漸變來填充或者描邊。我們用下面的方法建立一個canvasGradient對象,并且指派給圖形的fillStyle和strokeStyle屬性。

createLinearGradient(x1, y1, x2, y2):createLinearGradient方法接受4個參數,表示漸變的起點(x1, y1)與終點(x2, y2)。

createRadiaGradient(x1, y1, r1, x2, y2, r2):createRadialGradient方法接受6個參數,前三個定義一個以(x1, y1)為原點,半徑為r1的圓,後三個參數則定義一個以(x2, y2)為原點半徑為r2的圓。

var lineargradient = ctx.createLinearGradient(0,0,150,150);
var radialgradient = ctx.createRadialGradient(75,75,0,75,75,100);       

建立出canvasGradient對象後,我們就可以用addColorStop方法給它上色了。

gradient.addColorStop(position, color):addColorStop方法接受2個參數,position參數必須是一個0.0至1.0之間的數值,表示漸變中顔色所在的相對位置。例如,0.5表示顔色會出現在正中間。color參數必須是一個有效的css顔色值,例如(#FFF,rgba(0, 0, 0, 1))等等。

可以根據需要it安家任意多個色标(color stops)。下面是最簡單的線性黑白漸變的例子。

var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
    lineargradient.addColorStop(0, 'white');
    lineargradient.addColorStop(1, 'black');       

createLinearGradient的例子

這裡,設定兩種不同的漸變,第一種是背景色漸變,可以給同一位置設定兩種顔色,可以用這個實作突變的效果,就像這裡從白色到綠色的突變。一般情況下,色标的順序是無所謂順序的,但是色标位置重複時,順序就變得非常重要了。是以注意保持色标定義順序和它理想的順序一緻。

第二種漸變,并不是從0.0位置開始定義色标,因為那并不是那麼嚴格的。在0.5處設一黑色色标,漸變會預設為從起點到色标之間都是黑色。

你會發現,strokeStyle和fillStyle屬性都可以接受canvasGradient對象。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        // Create gradients
        var lingrad = ctx.createLinearGradient(0, 0, 0, 150);
        lingrad.addColorStop(0, '#00ABEB');
        lingrad.addColorStop(0.5, '#fff');
        lingrad.addColorStop(0.5, '#26C000');
        lingrad.addColorStop(1, '#fff');
        var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95);
        lingrad2.addColorStop(0.5, '#000');
        lingrad2.addColorStop(1, 'rgba(0,0,0,0)');
        // assign gradients to fill and stroke styles
        ctx.fillStyle = lingrad;
        ctx.strokeStyle = lingrad2;
        // draw shapes
        ctx.fillRect(10, 10, 130, 130);
        ctx.strokeRect(50, 50, 50, 50);
    }       
Canvas簡介

createRadialGradient的例子

在這個例子中定義4個不同的徑向漸變。可以控制漸變的起始和結束點,是以我們可以是實作一些更加複雜的效果。(經典的徑向漸變是隻有一個中心店,簡單地由中心向外圍的圓擴張)

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        // 建立漸變
        var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
        radgrad.addColorStop(0, '#A7D30C');
        radgrad.addColorStop(0.9, '#019F62');
        radgrad.addColorStop(1, 'rgba(1,159,98,0)');

        var radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50);
        radgrad2.addColorStop(0, '#FF5F98');
        radgrad2.addColorStop(0.75, '#FF0188');
        radgrad2.addColorStop(1, 'rgba(255,1,136,0)');

        var radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40);
        radgrad3.addColorStop(0, '#00C9FF');
        radgrad3.addColorStop(0.8, '#00B5E2');
        radgrad3.addColorStop(1, 'rgba(0,201,255,0)');

        var radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90);
        radgrad4.addColorStop(0, '#F4F201');
        radgrad4.addColorStop(0.8, '#E4C700');
        radgrad4.addColorStop(1, 'rgba(228,199,0,0)');

        // 畫圖形
        ctx.fillStyle = radgrad4;
        ctx.fillRect(0, 0, 150, 150);
        ctx.fillStyle = radgrad3;
        ctx.fillRect(0, 0, 150, 150);
        ctx.fillStyle = radgrad2;
        ctx.fillRect(0, 0, 150, 150);
        ctx.fillStyle = radgrad;
        ctx.fillRect(0, 0, 150, 150);
    }       
Canvas簡介

這裡讓起點稍微偏離終點,這樣可以達到一種球狀3D效果。但最好不要讓裡面的圓與外圓部分交疊,那樣會産生什麼樣的效果就不好說了。4個徑向漸變效果的最後一個色标是透明的,如果想要兩色标直接的過渡柔和一些,隻要兩個顔色值一緻就可以了。代碼裡看不出來,是因為用了兩種不同的顔色表示方法,其實是相同的,#019F62=rgba(1,159,98,1)。

4.4 圖案樣式Patterns

上面我們使用循環來實作圖案的效果,其實有一個更加簡單方式:createPattern。

createPattern(image, type):該方法接受兩個參數。Image可以是一個Image對象的引用,或者另一個canvas對象。Type必須是下面的字元串之一:repeat,repeat-x,repeat-y和norepeat。

注意:canvas對象作為Image參數在FireFox1.5(Gecko 1.8)中是無效的。

圖案的應用跟漸變很類似,建立一個pattern之後,賦給fillStyle或strokeStyle屬性即可。

var img = new Image();
img.src = 'someimage.png';
var ptrn = ctx.createPattern(img,'repeat');      
注意:與drawImage有點不同,你需要确認image對象已經裝載完畢,否則圖案可能效果不對。

createPattern的例子

在最後的例子中,床架一個圖案然後賦給了fillStyle屬性。唯一要注意的是,使用Image對象的onload handle來確定設定圖案之間圖像已經裝載完畢。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        // 建立新 image 對象,用作圖案
        var img = new Image();
        img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
        img.onload = function () {
            // 建立圖案
            var ptrn = ctx.createPattern(img, 'repeat');
            ctx.fillStyle = ptrn;
            ctx.fillRect(0, 0, 150, 150);
        }
    }       
Canvas簡介

4.5 陰影Shadows

shadowOffsetX=float,shadowOffsetY=float:shadowOffsetX和shadowOffsetY用來設定陰影在X軸和Y軸的延伸距離,他們是不受變換矩陣所影響的。負值表示陰影會往上或左延伸,正值表示會往下或右延伸,他們預設值都為0。

shadowBlur=float:shadowBlur用于設定陰影的模糊程度,其數值并不跟相熟數量挂鈎,也不受變換矩陣的影響,預設為0。

shadowColor=color:shadowColor是标準的CSS顔色值,用于設定陰影顔色效果,預設是全透明的黑色。

文字陰影的例子

這個例子繪制了帶陰影效果的文字。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowBlur = 2;
        ctx.shadowColor = "rgba(0, 0, 0, 0.5)";

        ctx.font = "20px Times New Roman";
        ctx.fillStyle = "Black";
        ctx.fillText("Sample String", 5, 30);
    }       
Canvas簡介

4.6 canvas填充規則

當我們用到fill(或者clip和isPointinPath)可以選擇一個填充規則,該填充規則根據某處在路徑的外面或者裡面來決定該處是否被填充,這對于自己與自己路徑相交或者路徑被嵌套的時候非常有用。

兩個可能的值:

  • “nonzero”:non-zero winding rule,預設值
  • “evenodd”:even-odd winding rule

這個例子,我們用填充規則evenodd

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.beginPath();
        ctx.arc(50, 50, 30, 0, Math.PI * 2, true);
        ctx.arc(50, 50, 15, 0, Math.PI * 2, true);
        ctx.fill("evenodd");
    }       
Canvas簡介

5. 繪制文本

5.1 繪制文本

canvas提供兩種方式來繪制文本:

filltext(text, x, y, [, maxWidth]):在指定的(x,y)位置填充指定的文本,繪制的最大寬度是可選的

strokeText(text, x, y, [, maxWidth]):在指定的(x,y)位置繪制文本邊框,繪制的最大寬度是可選的

一個填充文本是例子

文本用目前填充方式被填充

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.font = "48px serif";
        ctx.fillText("Hello world", 10, 50);
    }       
Canvas簡介

 一個文本邊框的例子

文本用目前的邊框樣式被繪制

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.font = "48px serif";
  ctx.strokeText("Hello world", 10, 50);
}       
Canvas簡介

5.2 有樣式的文本

上面使用font屬性來使文本比預設尺寸大一些,還有更多的屬性來改變canvas文本的樣式。

font=value:用來繪制文本的樣式,這個字元串和css font屬性相同。預設的字型是10px sans-serif

textAlign=value:文本對其選項,可選的值有start,end,left,right,center。預設值是start

textBaseline=value:基線對齊選項,可選的值包括:top,hanging,middle,alphabetic,ideographic,bottom。預設的值是alphabetic。

direction=value:文本方向,可能的值:ltr,rtl,inherit。預設值inherit。

如果使用過css,這些屬性應該很熟悉了。

textBaseline例子

function draw() {
        var ctx = document.getElementById("canvas").getContext('2d')
        ctx.font = "48px serif"
        ctx.textBaseline = "hanging"
        ctx.strokeText("hello world", 0, 100)
    }       
Canvas簡介

 5.3 預測文本寬度

當需要獲得更多文本細節時,可以使用下面方法測量文本。

measureText():方法傳回一個TextMetrics對象的寬度,所在像素,這些展現文本特性的屬性。

下面這段代碼展示如何測量文本來擷取它的寬度:

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  var text = ctx.measureText("foo"); // TextMetrics object
  text.width; // 16;
}       

6. 使用圖像

canvas更有意思的一項特性就是圖像操作能力。可以用于動态的圖像合成或者作為圖形的背景,以及遊戲界面(Sprites)等等。浏覽器支援的格式的任意的外部圖檔都可以使用,比如PNG,GIF,JPEG等。甚至可以将同一個頁面中其他canvas元素生成的圖檔作為圖檔源。

引入圖檔都canvas需要兩個基本步驟:

  1. 獲得一個指向HtmlImageElement對象或者另一個canvas元素的引用作為源,也可以通過提供一個URL的方式來使用圖檔。
  2. 使用drawImage()函數将圖檔繪制到畫布上。

6.1 獲得需要繪制的圖檔

canvas的API可以使用下面這些類型中的一種作為圖檔的源:

HTMLImageElement:這些圖檔是由Image()函數構造出來的,或者任何的<img>元素。

HTMLVideoElement:用一個HTML的<video>元素作為圖檔原,可以從适配中抓取目前幀作為一個圖像。

HTMLCanvasElement:可以使用另一個<canvas>元素作為圖檔源。

ImageBitmap:這是一個高性能的位圖,可以低延遲地繪制,他可以從上述所有源以及其他幾種源中生成。

這些源統一由CanvasImageSource類型來引用。有幾種方式可以擷取到我們需要在canvas上使用的圖檔。

使用相同頁面内的圖檔

我們可以使用下面方法中的一種擷取與canvas同一個頁面中的圖檔的引用:

  • document.images集合
  • document.getElementByTagName()方法
  • 如果知道指定圖檔的ID,可以使用document.getElementById()方法擷取這個圖檔

使用其他域名下的圖檔

在HTMLImageElement上使用crossOrigin屬性,可以請求加載其他域名上的圖檔。如果圖檔的伺服器允許跨域通路這個圖檔,那麼可以使用這個圖檔而不污染canvas,否則這個圖檔将會污染canvas。

使用其他canvas元素

和引用頁面内的圖檔類似,用document.getElementByTagName或者document.getElementById方法來擷取其他canvas元素,但是引入的應該是已經準備好的canvas。

一個常用的應用就是将第二個canvas作為另一個更大的canvas的縮略圖。

由零開始建立圖像

或者我們可以是用腳本建立一個新的HTMLImageElement對象。要實作這個方法,可以使用Image構造函數。

var img = new Image();   // 建立一個<img>元素
img.src = 'myImage.png'; // 設定圖檔源位址      

當上面腳本執行後,圖檔開始加載。

若調用drawImage時,圖檔沒有裝載完,那麼什麼都不會發生(在一些舊的浏覽器中會抛出異常)。是以應該使用load事件來保證不會在圖檔加載完成之前使用這個圖檔。

var img = new Image();   // 建立img元素
img.onload = function(){
  // 執行drawImage語句
}
img.src = 'myImage.png'; // 設定圖檔源位址       

如果隻用到一張圖檔的話,這已經足夠了,但是一旦需要不止一張圖檔,情況就變得複雜了,當時圖檔預加載的政策不在這裡讨論。

通過data:url方式嵌入圖像

我們還可以通過data:url的方式來引用圖像,Data urls允許使用一串Base64編碼的字元串的方式來定義一個圖檔,如下:

img.src = 'data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==';       

其優點是圖檔内容可以直接使用,無須再到伺服器兜一圈。(還有一個優點是,可以将css,JavaScript,html和圖檔全部封裝在一起,前一起來比較友善)缺點是沒辦法緩存圖檔,大圖檔,高清圖檔的話内嵌url資料會非常長。

使用視訊幀

還可以使用<video>中的幀,即使是不可見的video。例如,如果有一個id為myvideo的<video>元素,可以這樣做:

function getMyVideo() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    return document.getElementById('myvideo');
  }
}       

它将為這個視訊傳回HTMLVideoElement對象,正如我們前面說到的,可以使用它作為canvas圖檔源。

6.2 繪制圖檔

一旦獲得了源圖檔對象,我們可以使用drawImage來将圖像渲染到canvas裡。drawImage有三種重載,下面是最基礎的一種。

drawImage(image, x, y):其中image是image或者canvas對象,x, y是其在目标canvas中的起始坐标。

一個簡單的例子

下面是一個例子,我們用一個外部的圖像作為一個折線圖的背景圖。這樣我們就不必繪制複雜的背景圖了,省下不少代碼。這裡隻用到一個image對象,于是就在它的onload事件響應函數中繪制折線圖。drawImage方法将背景放置在canvas的左上角(0,0)處。

function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    var img = new Image();
    img.onload = function(){
      ctx.drawImage(img,0,0);
      ctx.beginPath();
      ctx.moveTo(30,96);
      ctx.lineTo(70,66);
      ctx.lineTo(103,76);
      ctx.lineTo(170,15);
      ctx.stroke();
    }
    img.src = 'images/backdrop.png';
  }       

看起來結果是這樣的:

Canvas簡介

6.2 縮放Scaling

drawImage方法的了一種重載變種是增加了兩個用于控制圖像在canvas中縮放的參數。

drawImage(image, x, y, width, height):這個方法多了2個參數,width和height,這兩個參數用來控制當向canvas畫入時應該縮放的大小。

平鋪圖像

在這個例子中,用一張圖檔,像背景一樣在canvas中重複平鋪。實作起來也很簡單,隻要循環鋪開經過縮放的圖檔即可。如下面代碼,第一層for循環做行重複,第二層是做列重複的。圖像大小被縮放到原來的三分之一,50x38px。這種方法很好達到背景圖案的效果,在下面的例子中可以看到。

注意:圖像很可能因為大幅度的縮放而變得模糊,如果圖像中有文字,那麼最好還是不要縮放,因為那樣處理後很可能因為圖檔模糊,使圖像中的文字無法辨認。
function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        var img = new Image();
        img.onload = function () {
            for (var i = 0; i < 4; i++) {
                for (var j = 0; j < 3; j++) {
                    ctx.drawImage(img, j * 50, i * 38, 50, 38);
                }
            }
        };
        img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';
    }      

所謂縮放就是設定圖檔的大小,這樣這樣圖檔的寬和高可能和原始的寬,高比例不同,這樣圖檔會出現拉伸,伸縮效果。這個例子就是從固定位置,按照固定大小,在canvas上繪制圖像。結果如下:

Canvas簡介

6.3 切片Slicing

drawImage方法第三個重載有8個參數,用于控制做切片顯示的。

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight):第一個參數是圖像源,和上面的是相同的,都是一個圖像或另一個canvas的應用。其他的參數參照下圖了解,前4個參數是定義圖像源的切片位置和大小,後4個參數是定于切片的目标顯示位置和大小。

Canvas簡介

切片是做圖像合成的強大工具。假設有一張圖像,包含了所有圖檔元素(比如,一張圖檔上有,前進按鈕,後退按鈕,首頁按鈕),可以用這種方式來合成完整的圖像。例如想畫一張圖示,而手頭隻有包含所有必需文字的PNG檔案,那麼可以輕松的根據實際資料的需要來改變最終顯示的圖示。這方法的另一個好處是你不需要單獨裝載每一個圖像。

一個相框的例子

在這個例子中,繼續使用上面的犀牛圖檔,不過通過切片将它放在一個相框圖檔中。相框圖檔是一個24位的PNG圖檔,中間有一個透明的陰影。因為24位PNG圖像包含一個完整的8位alpha通道,不像GIF和8未的PNG圖像,它可以放在任何背景上,而不用擔心遮罩。

Canvas簡介

下面代碼思路是在畫布上先用犀牛圖像作為源畫一次,再用相框作為源畫一次。

注意這裡沒有通過建立新對象來加載圖像,而是将他們作為<img>标簽直接包含在HTML源代碼中,并從中檢索圖像。通過display隐藏犀牛和相框。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas-image</title>
</head>
<body>
<canvas id="canvas" width="150" height="150"></canvas>
<div style="display:none;">
    <img id="source" src="https://mdn.mozillademos.org/files/5397/rhino.jpg" width="300" height="227">
    <img id="frame" src="https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png" width="132" height="150">
</div>
<script>
    function draw() {
        var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');

        // Draw slice
        ctx.drawImage(document.getElementById('source'), 33, 71, 104, 124, 21, 20, 87, 104);

        // Draw frame
        ctx.drawImage(document.getElementById('frame'), 0, 0);
    }
    draw();
</script>
</body>
</html>       
Canvas簡介

美術館的例子

通過canvas切片,既然可以把犀牛放在相框裡,那多個頭像放在相框中就是一個美術館的效果。在這個例子中,将建立一個小型的美術館。畫廊由多個圖像的表格組成。加載頁面時canvas将為每個圖像插入一個元素,并在其周圍繪制一個架構。

每個圖像有固定的寬度和高度,周圍繪制的架構也是如此。可以增強腳本,使其使用圖像的寬度和高度來調整架構擺放位置。

代碼的邏輯如下,周遊document.images容器并響應的添加新的canvas元素。唯一需要注意的是使用Node.insertBefore方法。insertBefore是元素(圖像)的父節點(表單元)的方法,在此之前我們要插入新節點(canvas元素)。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>album</title>
    <style>
        body {
            background: 0 -100px repeat-x url(https://mdn.mozillademos.org/files/5415/bg_gallery.png) #4F191A;
            margin: 10px;
        }
        img { display: none; }
        table { margin: 0 auto; }
        td { padding: 15px; }
    </style>
</head>
<body onload="draw();">
<table>
    <tr>
        <td><img src="https://mdn.mozillademos.org/files/5399/gallery_1.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5401/gallery_2.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5403/gallery_3.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5405/gallery_4.jpg"></td>
    </tr>
    <tr>
        <td><img src="https://mdn.mozillademos.org/files/5407/gallery_5.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5409/gallery_6.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5411/gallery_7.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5413/gallery_8.jpg"></td>
    </tr>
</table>
<img id="frame" src="https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png" width="132" height="150"/>
<script>
    function draw() {
        // 周遊頁面中的image
        for (var i = 0; i < document.images.length; i++) {
            // 相框就算了
            if (document.images[i].getAttribute('id') != 'frame') {
                // 建立一個canvas
                canvas = document.createElement('canvas');
                canvas.setAttribute('width', 132);
                canvas.setAttribute('height', 150);
                // 在插入在每個隐藏的圖檔前面
                document.images[i].parentNode.insertBefore(canvas,document.images[i]);
                ctx = canvas.getContext('2d');
                // 在canvas中畫這個圖檔
                ctx.drawImage(document.images[i], 15, 20);
                // 在canvas中畫這個像框
                ctx.drawImage(document.getElementById('frame'), 0, 0);
            }
        }
    }
</script>
</body>
</html>       

效果如下:

Canvas簡介

 6.4 控制圖像的縮放行為

如前所述,縮放圖像可能會由于縮放過程而導緻圖檔模糊,變形。在上下文context中imageSmoothingEnabled屬性來控制縮放圖像時是否平滑,預設值是true,圖檔被裁剪時保持平滑。設定方式如下:

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;       

7. 變形Transformations

前面的内容中讨論了canvas網格和坐标空間。目前為止,我們隻根據需要改變預設網格的大小。通過使用變形這種強大的方式可以将圖形源旋轉,縮放。

7.1 狀态的儲存和恢複

在深入變形之前,先了解兩個方法,這兩個方法是實作複雜繪圖必不可少的。

save():儲存canvas的所有狀态

restore():從最近使用的場景中恢複canvas的狀态

每一次調用save()方法的時候,canvas狀态就會儲存在一個棧中,目前繪圖狀态保被推入棧頂。一個繪圖狀态由下面内容組成:

  • 目前應用的變形(例如,移動,旋轉,伸縮)
  • 以下一些屬性的值:strokeStyle,fillStyle,globalAlpha,lineWidth,lineCap,lineJoin,miterLimit,lineDashOffset,shadowOffsetX,shadowOffsetY,shadowBlur,shadowColor,globalCompositeOperation,font,textAlign,textBaseline,direction,imageSmoothingEnabled
  • 目前裁剪路徑,下面将會介紹

可以任意次數的調用save()方法。每次調用restore()方法最後一次儲存的繪圖狀态從棧頂彈出,左右狀态被重新裝載為次棧頂的狀态。

一個儲存和恢複canvas狀态的例子

下面這個例子說明在繪制一系列的矩形的時候狀态是如何儲存和恢複的。

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        ctx.fillRect(0, 0, 150, 150);   // 使用預設狀态繪制一個矩形
        ctx.save();                  // 儲存狀态

        ctx.fillStyle = '#09F';      // 修改設定
        ctx.fillRect(15, 15, 120, 120); // 使用新設定繪制矩形

        ctx.save();                  // 儲存狀态
        ctx.fillStyle = '#FFF';      // 修改設定
        ctx.globalAlpha = 0.5;
        ctx.fillRect(30, 30, 90, 90);   // 使用新設定繪制矩形

        ctx.restore();               // 恢複之前狀态
        ctx.fillRect(45, 45, 60, 60);   // 使用恢複之後的狀态繪圖

        ctx.restore();               // 再次恢複之前狀态
        ctx.fillRect(60, 60, 30, 30);   // 使用恢複之後的狀态繪圖
    }
    draw()       
Canvas簡介

7.2 移動Translation

先看移動的第一個方法translate()。這個方法可以把canvas從起始位置移動到網格坐标中的不同位置。

translate(x, y):移動canvas在網格上的位置。x坐标表示水準位移,y坐标表示垂直位移。如下圖

Canvas簡介

 在做一些變形之前,最好儲存canvas狀态。調用restore方法從最近使用的場景中恢複canvas狀态要比反向轉換回到原來狀态要容易的多。并且,如果在一個循環中執行translate()移動但沒有儲存和恢複canvas狀态,很有可能因為繪圖在canvas邊緣外部而丢失調。

一個轉化的例子

這個例子說明canvas轉化的好處。沒有translate()轉換,可能所有的矩形都在同一個位置(0, 0)繪圖。translate()方法還允許我們自由地将矩形放在畫布任何位置,而不必手動調用fillRect()方法來調整坐标位置。這使得它更容易了解和使用。

在下面的例子中,有9次調用fillRect()方法,在兩個for循環中分别調用三次。在每次循環中,canvas都會移動一次,畫一個矩形,然後canvas狀态傳回到初始狀态。注意得益于translae()轉移,每次調用fillRect()都使用相同的坐标來繪圖。

function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    for (var i = 0; i < 3; i++) {
      for (var j = 0; j < 3; j++) {
        ctx.save(); // 儲存位置
        ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)'; // 設定樣式
        ctx.translate(10 + j * 50, 10 + i * 50); // 轉移
        ctx.fillRect(0, 0, 25, 25); // 畫圖
        ctx.restore(); // 從最近的狀态中恢複狀态
      }
    }
  }       
Canvas簡介

7.3 旋轉Rotating

第二個變形是旋轉rotate(),使用這個方法可以從源位置開始旋轉canvas。

rotate(angle):根據角度順時針旋轉畫布

旋轉的中心位置總是從canvas的原點開始。如果需要改變旋轉中心,需要調用translate()方法來轉移。

一個旋轉的例子

在這個例子中,我們先使用rotate将矩形從中心位置旋轉,然後使用translate()方法将矩形從原點移動位置。

注意:角度轉換成弧度的計算公式(Mah.PI/180)*degrees
function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');

    // 儲存狀态
    ctx.save();
    // 藍色矩形
    ctx.fillStyle = '#0095DD';
    ctx.fillRect(30, 30, 100, 100);
    ctx.rotate((Math.PI / 180) * 25);
    // 綠色矩形
    ctx.fillStyle = '#4D4E53';
    ctx.fillRect(30, 30, 100, 100);
    ctx.restore();

    // 右邊矩形,從中心位置旋轉
    ctx.fillStyle = '#0095DD';
    ctx.fillRect(150, 30, 100, 100);

    ctx.translate(200, 80); // 轉移位置
                            // x = x + 0.5 * width
                            // y = y + 0.5 * height
    ctx.rotate((Math.PI / 180) * 25); // 旋轉
    ctx.translate(-200, -80); // 位置回歸

    // 畫矩形
    ctx.fillStyle = '#4D4E53';
    ctx.fillRect(150, 30, 100, 100);
  }       
Canvas簡介

7.4 伸縮

下一個變形是伸縮,我們使用伸縮來增加或減少畫布網格中的大小機關。使用縮放可以按比例放大或縮小畫布。

scale(x, y):水準按x縮放畫布機關,垂直按y縮放畫布機關。兩個參數都是實數,小于1的值将按比例縮小,大于1的值将會放大,值為1則保持相同大小。

x,y的值若為負數可以實作軸鏡像(例如,使用translate(0, canvas.height); scale(1, -1)可以實作笛卡爾坐标系統,原點在左下角。)

預設情況下,canvas中一個機關是一像素。如果scale(0.5, 0.5),所有像素都是原來大小的一半,是以圖形就是原來一半大小。同理,如果參數設定為2,像素大小是正常大小的2倍,圖像将為原來大小的2倍。

一個伸縮的例子

function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');

    // 畫一個隽星,并x軸放大19倍,y軸縮放3倍
    ctx.save();
    ctx.scale(10, 3);
    ctx.fillRect(1, 10, 10, 10);
    ctx.restore();

    // 水準鏡像
    ctx.scale(-1, 1);
    ctx.font = '48px serif';
    ctx.fillText('MDN', -135, 120);
  }       
Canvas簡介

7.5 變形Transforms

最後一個方法使用transform方法允許對轉換矩陣進行修改。

transform(m11, m12, m21, m22, dx, dy):這個方法是将目前的變形矩陣乘以一個基于自身參數的矩形。如果任意一個參數是無限大,變形矩陣也必須被标記為無限大,否則會抛出異常。

這個函數的參數的含義如下:

m11:水準方向的縮放

m12:水準方向的傾斜偏移

m21:豎直方向的傾斜偏移

m22:豎直方向的縮放

dx:水準方向的移動

dy:豎直方向的移動

setTransform(m11, m12, m21, m22, dx, dy):這個方法會将目前的變形矩陣重置為機關矩陣,然後用相同的參數調用transform方法。如果任意一個參數是無限大,那麼變形矩陣也必須被标記為無限大,否則會抛出異常。從根本上來說,這個方法是取消了目前變形,然後設定為指定的變形,一步完成。

resetTransform():重置為目前變形為機關矩陣,它和調用下面的語句行為是一緻的:ctx.setTransform(1, 0, 0, 1, 0, 0)

transform/setTransform的例子

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        var sin = Math.sin(Math.PI / 6);
        var cos = Math.cos(Math.PI / 6);
        ctx.translate(100, 100);
        var c = 0;
        for (var i = 0; i <= 12; i++) {
            c = Math.floor(255 / 12 * i);
            ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
            ctx.fillRect(0, 0, 100, 10);
            ctx.transform(cos, sin, -sin, cos, 0, 0);
        }

        ctx.setTransform(-1, 0, 0, 1, 100, 100);
        ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
        ctx.fillRect(0, 50, 100, 100);
    }       
Canvas簡介

8. 合成與裁剪

上面的例子中,我們總是将一個圖形畫在另一個圖形紙上,對于更多的情況,僅僅這樣是不夠的。比如對于合成圖形來說,繪制順序會有限制。不過我們可以用globalCompositeOperation屬性來改變這種情況。此外clip屬性允許我們隐藏不想看到的部分。

8.1 globalCompositeOperation

不僅可以在已有圖形後面畫新圖形,還可以用來遮蓋指定區域,清除畫布中的某些部分(清除不僅限于矩形,像clearRect()方法做的那樣)以及其他的操作。

globalCompositeOperation=type:這個屬性設定了在畫新圖形時采用的遮蓋政策,其值是一個辨別12種遮蓋方式的字元串。

下面看一個綜合的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Compositing</title>
</head>
<body>
<script>
    // 全局變量
    var canvas1 = document.createElement("canvas");
    var canvas2 = document.createElement("canvas");
    // 頁面元素
    var gco = ['source-over', 'source-in', 'source-out', 'source-atop',
        'destination-over', 'destination-in', 'destination-out', 'destination-atop',
        'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay', 'darken',
        'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
        'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'].reverse();
    var gcoText = [
        '這是預設設定,并在現有畫布上下文之上繪制新圖形。',
        '新圖形隻在新圖形和目标畫布重疊的地方繪制。其他的都是透明的。',
        '在不與現有畫布内容重疊的地方繪制新圖形。',
        '新圖形隻在與現有畫布内容重疊的地方繪制。',
        '在現有的畫布内容後面繪制新的圖形。',
        '現有的畫布内容保持在新圖形和現有畫布内容重疊的位置。其他的都是透明的。',
        '現有内容保持在新圖形不重疊的地方。',
        '現有的畫布隻保留與新圖形重疊的部分,新的圖形是在畫布内容後面繪制的。',
        '兩個重疊圖形的顔色是通過顔色值相加來确定的。',
        '隻顯示新圖形。',
        '圖像中,那些重疊和正常繪制之外的其他地方是透明的。',
        '将頂層像素與底層相應像素相乘,結果是一幅更黑暗的圖檔。',
        '像素被倒轉,相乘,再倒轉,結果是一幅更明亮的圖檔。',
        'multiply和screen的結合,原本暗的地方更暗,原本亮的地方更亮。',
        '保留兩個圖層中最暗的像素。',
        '保留兩個圖層中最亮的像素。',
        '将底層除以頂層的反置。',
        '将反置的底層除以頂層,然後将結果反過來。',
        '螢幕相乘(A combination of multiply and screen)類似于疊加,但上下圖層互換了。',
        '用頂層減去底層或者相反來得到一個正值。',
        '一個柔和版本的強光(hard-light)。純黑或純白不會導緻純黑或純白。',
        '和difference相似,但對比度較低。',
        '保留了底層的亮度(luma)和色度(chroma),同時采用了頂層的色調(hue)。',
        '保留底層的亮度(luma)和色調(hue),同時采用頂層的色度(chroma)。',
        '保留了底層的亮度(luma),同時采用了頂層的色調(hue)和色度(chroma)。',
        '保持底層的色調(hue)和色度(chroma),同時采用頂層的亮度(luma)。'].reverse();
    // 全局尺寸
    var width = 320;
    var height = 340;
    // 頁面加載事件,頁面主程式
    window.onload = function () {
        var lum = {
            r: 0.33,
            g: 0.33,
            b: 0.33
        };
        // 設定canvas尺寸
        canvas1.width = width;
        canvas1.height = height;
        canvas2.width = width;
        canvas2.height = height;
        lightMix()
        colorSphere();
        runComposite();
        return;
    };
    // 建立canvas
    function createCanvas() {
        var canvas = document.createElement("canvas");
        canvas.style.background = "url(" + op_8x8.data + ")";
        canvas.style.border = "1px solid #000";
        canvas.style.margin = "5px";
        canvas.width = width / 2;
        canvas.height = height / 2;
        return canvas;
    }
    //
    function runComposite() {
        var dl = document.createElement("dl");
        document.body.appendChild(dl);
        while (gco.length) {
            var pop = gco.pop();
            var dt = document.createElement("dt");
            dt.textContent = pop;
            dl.appendChild(dt);
            var dd = document.createElement("dd");
            var p = document.createElement("p");
            p.textContent = gcoText.pop();
            dd.appendChild(p);

            var canvasToDrawOn = createCanvas();
            var canvasToDrawFrom = createCanvas();
            var canvasToDrawResult = createCanvas();

            var ctx = canvasToDrawResult.getContext('2d');
            ctx.clearRect(0, 0, width, height)
            ctx.save();
            ctx.drawImage(canvas1, 0, 0, width / 2, height / 2);
            ctx.globalCompositeOperation = pop;
            ctx.drawImage(canvas2, 0, 0, width / 2, height / 2);
            ctx.globalCompositeOperation = "source-over";
            ctx.fillStyle = "rgba(0,0,0,0.8)";
            ctx.fillRect(0, height / 2 - 20, width / 2, 20);
            ctx.fillStyle = "#FFF";
            ctx.font = "14px arial";
            ctx.fillText(pop, 5, height / 2 - 5);
            ctx.restore();

            var ctx = canvasToDrawOn.getContext('2d');
            ctx.clearRect(0, 0, width, height)
            ctx.save();
            ctx.drawImage(canvas1, 0, 0, width / 2, height / 2);
            ctx.fillStyle = "rgba(0,0,0,0.8)";
            ctx.fillRect(0, height / 2 - 20, width / 2, 20);
            ctx.fillStyle = "#FFF";
            ctx.font = "14px arial";
            ctx.fillText('existing content', 5, height / 2 - 5);
            ctx.restore();

            var ctx = canvasToDrawFrom.getContext('2d');
            ctx.clearRect(0, 0, width, height)
            ctx.save();
            ctx.drawImage(canvas2, 0, 0, width / 2, height / 2);
            ctx.fillStyle = "rgba(0,0,0,0.8)";
            ctx.fillRect(0, height / 2 - 20, width / 2, 20);
            ctx.fillStyle = "#FFF";
            ctx.font = "14px arial";
            ctx.fillText('new content', 5, height / 2 - 5);
            ctx.restore();

            dd.appendChild(canvasToDrawOn);
            dd.appendChild(canvasToDrawFrom);
            dd.appendChild(canvasToDrawResult);

            dl.appendChild(dd);
        }
    };
    // 公用函數
    var lightMix = function () {
        var ctx = canvas2.getContext("2d");
        ctx.save();
        ctx.globalCompositeOperation = "lighter";
        ctx.beginPath();
        ctx.fillStyle = "rgba(255,0,0,1)";
        ctx.arc(100, 200, 100, Math.PI * 2, 0, false);
        ctx.fill()
        ctx.beginPath();
        ctx.fillStyle = "rgba(0,0,255,1)";
        ctx.arc(220, 200, 100, Math.PI * 2, 0, false);
        ctx.fill()
        ctx.beginPath();
        ctx.fillStyle = "rgba(0,255,0,1)";
        ctx.arc(160, 100, 100, Math.PI * 2, 0, false);
        ctx.fill();
        ctx.restore();
        ctx.beginPath();
        ctx.fillStyle = "#f00";
        ctx.fillRect(0, 0, 30, 30)
        ctx.fill();
    };
    // 公用函數
    var colorSphere = function (element) {
        var ctx = canvas1.getContext("2d");
        var width = 360;
        var halfWidth = width / 2;
        var rotate = (1 / 360) * Math.PI * 2; // per degree
        var offset = 0; // scrollbar offset
        var oleft = -20;
        var otop = -20;
        for (var n = 0; n <= 359; n++) {
            var gradient = ctx.createLinearGradient(oleft + halfWidth, otop, oleft + halfWidth, otop + halfWidth);
            var color = Color.HSV_RGB({H: (n + 300) % 360, S: 100, V: 100});
            gradient.addColorStop(0, "rgba(0,0,0,0)");
            gradient.addColorStop(0.7, "rgba(" + color.R + "," + color.G + "," + color.B + ",1)");
            gradient.addColorStop(1, "rgba(255,255,255,1)");
            ctx.beginPath();
            ctx.moveTo(oleft + halfWidth, otop);
            ctx.lineTo(oleft + halfWidth, otop + halfWidth);
            ctx.lineTo(oleft + halfWidth + 6, otop);
            ctx.fillStyle = gradient;
            ctx.fill();
            ctx.translate(oleft + halfWidth, otop + halfWidth);
            ctx.rotate(rotate);
            ctx.translate(-(oleft + halfWidth), -(otop + halfWidth));
        }
        ctx.beginPath();
        ctx.fillStyle = "#00f";
        ctx.fillRect(15, 15, 30, 30)
        ctx.fill();
        return ctx.canvas;
    };
    // 計算顔色 HSV (1978) = H: Hue / S: Saturation / V: Value
    Color = {};
    Color.HSV_RGB = function (o) {
        var H = o.H / 360,
            S = o.S / 100,
            V = o.V / 100,
            R, G, B;
        var A, B, C, D;
        if (S === 0) {
            R = G = B = Math.round(V * 255);
        } else {
            if (H >= 1) H = 0;
            H = 6 * H;
            D = H - Math.floor(H);
            A = Math.round(255 * V * (1 - S));
            B = Math.round(255 * V * (1 - (S * D)));
            C = Math.round(255 * V * (1 - (S * (1 - D))));
            V = Math.round(255 * V);
            switch (Math.floor(H)) {
                case 0:
                    R = V;
                    G = C;
                    B = A;
                    break;
                case 1:
                    R = B;
                    G = V;
                    B = A;
                    break;
                case 2:
                    R = A;
                    G = V;
                    B = C;
                    break;
                case 3:
                    R = A;
                    G = B;
                    B = V;
                    break;
                case 4:
                    R = C;
                    G = A;
                    B = V;
                    break;
                case 5:
                    R = V;
                    G = A;
                    B = B;
                    break;
            }
        }
        return {
            R: R,
            G: G,
            B: B
        };
    };
    var createInterlace = function (size, color1, color2) {
        var proto = document.createElement("canvas").getContext("2d");
        proto.canvas.width = size * 2;
        proto.canvas.height = size * 2;
        proto.fillStyle = color1; // top-left
        proto.fillRect(0, 0, size, size);
        proto.fillStyle = color2; // top-right
        proto.fillRect(size, 0, size, size);
        proto.fillStyle = color2; // bottom-left
        proto.fillRect(0, size, size, size);
        proto.fillStyle = color1; // bottom-right
        proto.fillRect(size, size, size, size);
        var pattern = proto.createPattern(proto.canvas, "repeat");
        pattern.data = proto.canvas.toDataURL();
        return pattern;
    };
    var op_8x8 = createInterlace(8, "#FFF", "#eee");
</script>
</body>
</html>       

8.2 剪切路徑

剪切路徑和普通的canvas圖形差不多,不同的是它的作用是遮罩,用來隐藏不需要的部分。如下圖所示。紅色邊的五角星就是剪切路徑,所有在路徑意外的部分不會在canvas上繪制出來。

Canvas簡介

如果和上面介紹的globalCompositeOperation屬性比較,它可以實作和source-in和source-atop差不多的效果。差別是剪切路徑不會在canvas上繪制東西,而且它永遠不受新圖形的影響。這些特性使得它在特定區域裡會制圖型很好用。

在上面繪制圖形一節裡,介紹了stroke和fill方法,這裡介紹第三種方法clip。

clip():将目前正在建構的路徑轉換為目前的裁剪路徑。

我們使用clip()方法來建立一個新的裁剪路徑。

預設情況下,canvas有一個與它自身一樣大的剪切路徑(也就是沒有剪切效果)。

clip的例子

在這個例子裡,用一個圓形的剪切路徑來限制随機星星的繪制區域。首先畫一個和canvas一樣大小的黑色矩形作為背景,然後移動原點至中心點。然後使用clip方法建立一個弧形的剪切路徑。剪切路徑也屬于canvas狀态的一部分,可以被儲存起來。代碼如下:

function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.fillRect(0, 0, 150, 150);
        ctx.translate(75, 75);

        // Create a circular clipping path
        ctx.beginPath();
        ctx.arc(0, 0, 60, 0, Math.PI * 2, true);
        ctx.clip();

        // draw background
        var lingrad = ctx.createLinearGradient(0, -75, 0, 75);
        lingrad.addColorStop(0, '#232256');
        lingrad.addColorStop(1, '#143778');

        ctx.fillStyle = lingrad;
        ctx.fillRect(-75, -75, 150, 150);

        // draw stars
        for (var j = 1; j < 50; j++) {
            ctx.save();
            ctx.fillStyle = '#fff';
            ctx.translate(75 - Math.floor(Math.random() * 150),
                75 - Math.floor(Math.random() * 150));
            drawStar(ctx, Math.floor(Math.random() * 4) + 2);
            ctx.restore();
        }

    }

    function drawStar(ctx, r) {
        ctx.save();
        ctx.beginPath()
        ctx.moveTo(r, 0);
        for (var i = 0; i < 9; i++) {
            ctx.rotate(Math.PI / 5);
            if (i % 2 == 0) {
                ctx.lineTo((r / 0.525731) * 0.200811, 0);
            } else {
                ctx.lineTo(r, 0);
            }
        }
        ctx.closePath();
        ctx.fill();
        ctx.restore();
    }       
Canvas簡介

如果我們在建立新裁剪路徑時想要儲存原來的裁剪路徑,我們需要做的就是儲存一下canvas的狀态。

簡介路徑建立之後所有出現在它裡面的東西才會畫出來。在畫線性漸變時我們就會注意到這一點。然後繪制處50顆随機分布的(經過縮放)星星,讓然也隻有才裁剪路徑裡的星星才能繪制出來。

9. 基本的動畫

由于我們是通過JavaScript去操控<canvas>對象,這樣實作一些互動動畫是很容易的,這裡先了解一些基本的動畫。

canvas動畫最大的限制是圖像一旦繪制出來,它就一直保持這個狀态。如果要移動它,就不得不對所有的東西(包含之前的)進行重繪。重繪是相當費時的,而且性能很依賴于電腦的速度。

9.1 動畫的基本步驟

可以通過一下的步驟來畫出一幀:

  1. 清空canvas:除非接下來要畫的内容會完全填充canvas(例如背景圖),否則需要清空所有。最簡單的做法就是調用clearRect方法。
  2. 儲存canvas狀态:如果改變一些會影響canvas狀态的設定(例如樣式,變形等),并且在繪制每幀的時候儲存初始狀态,則需要儲存原始狀态。
  3. 畫動畫的形狀:進行實際幀繪制的步驟。
  4. 重置canvas狀态:如果已經儲存了canvas的狀态,可以先恢複它,然後重繪一下。

 9.2 操控動畫

在canvas上繪制内容是用canvas提供的或者自定義的方法,通常,我們僅僅在腳本執行結束後才能看到結果,比如說在for循環中完成動畫是不太可能的。是以為了實作動畫,需要一些可以定時執行重繪的方法。有兩種方法可以實作這樣的動畫操控。首先可以通過setInterval和setTimeout方法來繪制在設定的時間點上執行的重繪。

預定更新

有三個函數可以在預定時間段後執行一個指定的函數:window.setInterval(),window.setTimeout(),window.requestAnimationFrame()。

  • setInterval(function, delay):每延遲delay指定的毫秒數,就執行指定的函數function
  • setTimeout(function, delay):延遲delay指定的毫秒數後開始執行函數function
  • requestAnimationFrame(callback):高速浏覽器希望執行一個動畫,并在重繪之前,請求浏覽器執行一個特定的函數來更新動畫

如果不需要和使用者互動,可以使用setInterval方法,它可以定期執行指定的代碼。如果需要做一個遊戲,可以使用鍵盤或者滑鼠事件配合setTimeout來實作。通過設定監聽事件,可以捕捉使用者的互動,并執行相應的動作。

下面的動畫,采用window.requestAnimationFrame()實作動畫效果。這個方法提供了更加平緩并且更加有效的方式執行動畫,當系統準備好了重繪條件的時候,才調用繪制動畫幀。一般每秒回調函數執行60次,也有可能被降低。

9.3 太陽系的動畫

在這個例子裡,我們做一個小型的太陽系模拟動畫:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>anmation</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
  var sun = new Image();
  var moon = new Image();
  var earth = new Image();

  function init() {
    sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
    moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
    earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
    window.requestAnimationFrame(draw);
  }

  function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');

    ctx.globalCompositeOperation = 'destination-over';
    ctx.clearRect(0, 0, 300, 300); // clear canvas

    ctx.fillStyle = 'rgba(0,0,0,0.4)';
    ctx.strokeStyle = 'rgba(0,153,255,0.4)';
    ctx.save();
    ctx.translate(150, 150);

    // 地球
    var time = new Date();
    ctx.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds());
    ctx.translate(105, 0);
    ctx.fillRect(0, -12, 50, 24); // 陰影
    ctx.drawImage(earth, -12, -12);

    // Moon
    ctx.save();
    ctx.rotate(((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds());
    ctx.translate(0, 28.5);
    ctx.drawImage(moon, -3.5, -3.5);
    ctx.restore();

    ctx.restore();

    ctx.beginPath();
    ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // 地球軌道
    ctx.stroke();

    ctx.drawImage(sun, 0, 0, 300, 300);

    window.requestAnimationFrame(draw);
  }

  init();
</script>
</body>
</html>       
Canvas簡介

9.4 一個時鐘動畫

下面這個動畫實作一個時鐘,展示目前的時間。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>clock</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
    function clock() {
        var now = new Date();
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.save();
        ctx.clearRect(0, 0, 150, 150);
        ctx.translate(75, 75);
        ctx.scale(0.4, 0.4);
        ctx.rotate(-Math.PI / 2);
        ctx.strokeStyle = 'black';
        ctx.fillStyle = 'white';
        ctx.lineWidth = 8;
        ctx.lineCap = 'round';

        // 時針
        ctx.save();
        for (var i = 0; i < 12; i++) {
            ctx.beginPath();
            ctx.rotate(Math.PI / 6);
            ctx.moveTo(100, 0);
            ctx.lineTo(120, 0);
            ctx.stroke();
        }
        ctx.restore();

        // 分針
        ctx.save();
        ctx.lineWidth = 5;
        for (i = 0; i < 60; i++) {
            if (i % 5 !== 0) {
                ctx.beginPath();
                ctx.moveTo(117, 0);
                ctx.lineTo(120, 0);
                ctx.stroke();
            }
            ctx.rotate(Math.PI / 30);
        }
        ctx.restore();

        var sec = now.getSeconds();
        var min = now.getMinutes();
        var hr = now.getHours();
        hr = hr >= 12 ? hr - 12 : hr;

        ctx.fillStyle = 'black';

        // 畫時針
        ctx.save();
        ctx.rotate(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec);
        ctx.lineWidth = 14;
        ctx.beginPath();
        ctx.moveTo(-20, 0);
        ctx.lineTo(80, 0);
        ctx.stroke();
        ctx.restore();

        // 畫分針
        ctx.save();
        ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
        ctx.lineWidth = 10;
        ctx.beginPath();
        ctx.moveTo(-28, 0);
        ctx.lineTo(112, 0);
        ctx.stroke();
        ctx.restore();

        // 畫秒針
        ctx.save();
        ctx.rotate(sec * Math.PI / 30);
        ctx.strokeStyle = '#D40000';
        ctx.fillStyle = '#D40000';
        ctx.lineWidth = 6;
        ctx.beginPath();
        ctx.moveTo(-30, 0);
        ctx.lineTo(83, 0);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
        ctx.stroke();
        ctx.fillStyle = 'rgba(0, 0, 0, 0)';
        ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
        ctx.fill();
        ctx.restore();

        ctx.beginPath();
        ctx.lineWidth = 14;
        ctx.strokeStyle = '#325FA2';
        ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
        ctx.stroke();

        ctx.restore();

        window.requestAnimationFrame(clock);
    }

    window.requestAnimationFrame(clock);
</script>
</body>
</html>       
Canvas簡介

9.4 一個全景圖的例子

在這個例子中實作一個從左到右滾動的全景圖。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="800" height="300"></canvas>
<script>
    var img = new Image();

    // 全局變量
    img.src = 'https://mdn.mozillademos.org/files/4553/Capitan_Meadows,_Yosemite_National_Park.jpg';
    var CanvasXSize = 800;
    var CanvasYSize = 200;
    var speed = 30; // 播放速度
    var scale = 1.05;
    var y = -4.5; // 水準位移

    // 主程式
    var dx = 0.75;
    var imgW;
    var imgH;
    var x = 0;
    var clearX;
    var clearY;
    var ctx;

    img.onload = function() {
        imgW = img.width * scale;
        imgH = img.height * scale;

        if (imgW > CanvasXSize) {
            // 如果圖檔比canvas大
            x = CanvasXSize - imgW;
        }
        if (imgW > CanvasXSize) {
            // 圖檔比canvas寬
            clearX = imgW;
        } else {
            clearX = CanvasXSize;
        }
        if (imgH > CanvasYSize) {
            // 圖檔比canvas寬
            clearY = imgH;
        } else {
            clearY = CanvasYSize;
        }

        // 擷取canvas
        ctx = document.getElementById('canvas').getContext('2d');

        // 設定播放速度
        return setInterval(draw, speed);
    }

    function draw() {
        ctx.clearRect(0, 0, clearX, clearY); // 清空

        // 如果圖檔比canvas小
        if (imgW <= CanvasXSize) {
            // 重置
            if (x > CanvasXSize) {
                x = -imgW + x;
            }
            // 畫額外的圖像1
            if (x > 0) {
                ctx.drawImage(img, -imgW + x, y, imgW, imgH);
            }
            // 畫額外的圖像2
            if (x - imgW > 0) {
                ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
            }
        }

        // 圖像比canvas大
        else {
            // 重置
            if (x > (CanvasXSize)) {
                x = CanvasXSize - imgW;
            }
            // 畫額外圖像
            if (x > (CanvasXSize-imgW)) {
                ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
            }
        }
        // 畫圖像
        ctx.drawImage(img, x, y,imgW, imgH);
        // 移動
        x += dx;
    }
</script>

</body>
</html>       

結果如下: 

Canvas簡介

10. 進階動畫

上面我們制作了基本動畫以及了解讓繪圖移動的方法,這一部分将會對動畫有更深入的了解,并對運動有更深入的了解,并添加一些實體軌迹讓動畫看起來更加進階。

10.1 繪制小球

這裡繪制一個小球用于動畫學習。下面代碼先先建立一個畫布。

<canvas id="canvas" width="600" height="300"></canvas>       

首先要畫一個context(畫布場景) 。為了畫處這個球,再建立一個包含一些相關屬性以及drwa()函數的ball對象來完成繪制。

var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');

    var ball = {
        x: 100,
        y: 100,
        radius: 25,
        color: 'blue',
        draw: function() {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fillStyle = this.color;
            ctx.fill();
        }
    };
    ball.draw();       

這裡沒有什麼特别的,小球實際上是一個簡單的圓形,使用arc函數畫出。

10.2 添加速率

有了小球,現在添加一些基本動畫,這裡使用上面提到的window.requestAnimationFrame()方法,控制動畫。小球依舊靠添加速度矢量進行移動。在每一幀裡,依舊使用clear清理掉之前幀裡舊的圓形。

var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var raf = null;

    var ball = {
        x: 100,
        y: 100,
        vx: 5,
        vy: 2,
        radius: 25,
        color: 'blue',
        draw: function() {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fillStyle = this.color;
            ctx.fill();
        }
    };

    function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ball.draw();
        ball.x += ball.vx;
        ball.y += ball.vy;
        raf = window.requestAnimationFrame(draw)
    }

    canvas.addEventListener("mouseover", function (e) {
        raf = window.requestAnimationFrame(draw)
    })

    canvas.addEventListener("mouseout", function (e) {
        window.cancelAnimationFrame(raf);
    })

    ball.draw();       

10.3 邊界

如果沒有任何碰撞檢測,小球很快就會超出畫布。是以需要檢查小球的x和y位置是否超出畫布尺寸以及是否需要将速度矢量翻轉。為了這麼做,需要把下面的檢查代碼添加進入draw函數中:

if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
  ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
  ball.vx = -ball.vx;
}       

預覽一下,包滑鼠放在canvas範圍内部,效果是這樣的:

Canvas簡介

10.4 加速度

為了使動作效果看起來更加真實,可以像這樣處理速度,例如:

ball.vy *= .99;
ball.vy += .25;       

這會減少垂直方向上的速度,是以小球隻會在地闆上彈跳。

10.5 長尾效果

現在,我們使用的是clearRect函數幫助我們清除前一幀動畫。如果用一個半透明的fillRect函數取而代之,就可以輕松實作長尾效果。

ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);       
Canvas簡介

10.6 添加滑鼠控制

為了更好的控制小球,我們可以使用mousemove事件讓它跟随滑鼠活動。下面例子中,click事件會釋放小球然後讓它重新跳起。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;
var running = false;
var ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 1,
  radius: 25,
  color: 'blue',
  draw: function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
};
function clear() {
  ctx.fillStyle = 'rgba(255,255,255,0.3)';
  ctx.fillRect(0,0,canvas.width,canvas.height);
}
function draw() {
  clear();
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;

  if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
    ball.vx = -ball.vx;
  }

  raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener('mousemove', function(e){
  if (!running) {
    clear();
    ball.x = e.clientX;
    ball.y = e.clientY;
    ball.draw();
  }
});
canvas.addEventListener('click',function(e){
  if (!running) {
    raf = window.requestAnimationFrame(draw);
    running = true;
  }
});
canvas.addEventListener('mouseout', function(e){
  window.cancelAnimationFrame(raf);
  running = false;
});
ball.draw();       

11. 像素操作

到目前為止,還沒有介紹canvas畫布真是像素原理,事實上可以通過ImageData對象操作像素資料,直接讀取或将資料寫入該對象中。稍後将詳細介紹如何控制圖像使其平滑(非鋸齒)以及如何從canvas畫布中儲存圖像。

11.1 ImageData對象

ImageData對象中存儲着canvas對象的像素資料,它包含下面幾個隻讀屬性:

  • width:圖檔寬度,機關是像素
  • height:圖檔高度,機關是像素
  • data:Uint8ClampedArray類型的一維數組,包含着RGBA格式的證書資料,範圍在0至255之間(包含255)

data屬性傳回一個Uint8ClampeArray,它可以被使用作為檢視初始像素資料。每個像素使用4個1bytes值(按照紅,綠,藍和透明值的順序,就是rgba格式)來代表。每個顔色值部分用0至255來代表。每個部分被配置設定到一個在數組内連續的索引,左上角像素的紅色部分在數組索引0為止。像素從左到右被處理,然後往下,周遊整個數組。

Uint8ClampedArray包含高度 X 寬度 X 4bytes資料,索引值從0到(高度X寬度X4)- 1,例如要讀取圖檔中位于第50行,第200列的像素的藍色部分,代碼如下:

blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2];       

根據行,列讀取某像素點的RGBA值的公式:

imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 0/1/2/3];       

可能會使用Uint8ClampedArray.length屬性來讀取像素數組的大小(以bytes為機關):

var numBytes = imageData.data.length;       

11.2 得到場景像素資料

為了獲得一個包含為了獲得一個包含畫布場景像素資料的ImageData對象,可以使用getImage()方法。

var myImageData = ctx.getImageData(left, top, width, height);       

這個方法傳回一個ImageData對象,它代表了畫布區域的對象資料,此畫布的四個角落分别表示為(left, top), (left + width, top),(left, top + height)以及(left + width, top + height)四個點。這些坐标點呗設定為畫布坐标空間元素。

注意:任何畫布意外的元素都被傳回成一個透明的ImageData對象

顔色選擇器

在這個例子中,我們會使用getImageData()去展示滑鼠光标下的顔色。為此,我們需要得到目前滑鼠的位置,記為layerX和layerY,然後去查詢getImageData()給我們提供的在哪個位置的像素數組裡面的像素資料。最後我們使用數組資料去設定背景顔色和div的文字顔色。注意這裡不能使用遠端圖檔,不然會報錯:Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="300" height="200"></canvas>
<div id="color" style="width: 300px; height: 200px;"></div>
<script>
  var img = new Image();
  // img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';
  img.src = './../images/rhino.jpg';
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');
  img.onload = function () {
    ctx.drawImage(img, 0, 0);
    img.style.display = 'none';
  };
  var color = document.getElementById('color');

  function pick(event) {
    var x = event.layerX;
    var y = event.layerY;
    var pixel = ctx.getImageData(x, y, 1, 1);
    var data = pixel.data;
    var rgba = 'rgba(' + data[0] + ', ' + data[1] +
      ', ' + data[2] + ', ' + (data[3] / 255) + ')';
    color.style.background = rgba;
    color.textContent = rgba;
  }
  canvas.addEventListener('mousemove', pick);
</script>
</body>
</html>       

 效果如下:

Canvas簡介

11.3 在canvas畫布中寫入像素資料

可以使用putImageData()方法在canvas的畫布中寫入一個像素資料。

ctx.putImageData(myImageData, dx, dy);       

參數dx,dy表示要繪制的像素點在坐标系中x軸和y軸距離。舉例,下面代碼在坐标左上角位置畫一個點。

ctx.putImageData(myImageData, 0, 0);       

灰階變換和反轉顔色

在這個例子中,周遊所有像素以更改它們的值,然後使用putImageData()将修改後的像素畫在畫布上。invert函數隻是簡單的用255減去目前顔色值。grayscale函數隻是簡單的使用紅色,綠色,藍色值的平均值。還可以使用一個權重平均值,例如:公式x = 0.299r + 0.587g + 0.114b

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="300" height="200"></canvas><br/>
<button id="grayscalebtn">GrayScale</button>
<button id="invertbtn">Invert</button>

<script>
    var img = new Image();
    img.src = './../images/rhino.jpg';
    img.onload = function () {
        draw(this);
    };

    function draw(img) {
        var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        img.style.display = 'none';
        var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        var data = imageData.data;
        var invert = function () {
            for (var i = 0; i < data.length; i += 4) {
                data[i] = 255 - data[i];            // 紅色
                data[i + 1] = 255 - data[i + 1];    // 綠色
                data[i + 2] = 255 - data[i + 2];    // 藍色
            }
            ctx.putImageData(imageData, 0, 0);
        };
        var grayscale = function () {
            for (var i = 0; i < data.length; i += 4) {
                var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
                data[i] = avg; // red
                data[i + 1] = avg; // green
                data[i + 2] = avg; // blue
            }
            ctx.putImageData(imageData, 0, 0);
        };
        var invertbtn = document.getElementById('invertbtn');
        invertbtn.addEventListener('click', invert);
        var grayscalebtn = document.getElementById('grayscalebtn');
        grayscalebtn.addEventListener('click', grayscale);
    }
</script>
</body>
</html>       
Canvas簡介

11.4 縮放和抗鋸齒

 在drawImage()方法,畫布重疊和imageSmoothingEnable屬性的幫助下,我們可以放大顯示圖檔以及看到的詳細内容。

我們得到滑鼠的位置并裁減出距左和上5像素,距右下5像素的圖檔。然後我們将這幅圖複制到另一個畫布,并将圖檔調整到我們想要的大小。在縮放畫布裡,我們将10X10像素的對原畫布裁減調整為20X20,代碼如下:

zoomctx.drawImage(canvas, 
                  Math.abs(x - 5), Math.abs(y - 5),
                  10, 10, 0, 0, 200, 200);       

因為反鋸齒是預設啟用的,我們可能想要關閉它以看到清楚的像素。可以通過切換CheckBox來看到imageSmoothingEnable屬性的效果。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>imageSmoothingEnabled</title>
</head>
<body>
<canvas id="canvas" width="200" height="200"></canvas>
<canvas id="zoom" width="200" height="200"></canvas>
<br/>
<input type="checkbox" id="smoothbtn"/><label for="smoothbtn">Enable image smoothing</label>
<script>
  var img = new Image();
  img.src = './../images/rhino.jpg';
  img.onload = function () {
    draw(this);
  };
  function draw(img) {
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    img.style.display = 'none';
    var zoomctx = document.getElementById('zoom').getContext('2d');

    var smoothbtn = document.getElementById('smoothbtn');
    var toggleSmoothing = function (event) {
      zoomctx.imageSmoothingEnabled = this.checked;
      zoomctx.mozImageSmoothingEnabled = this.checked;
      zoomctx.webkitImageSmoothingEnabled = this.checked;
      zoomctx.msImageSmoothingEnabled = this.checked;
    };
    smoothbtn.addEventListener('change', toggleSmoothing);

    var zoom = function (event) {
      var x = event.layerX;
      var y = event.layerY;
      zoomctx.drawImage(canvas,
        Math.abs(x - 5),
        Math.abs(y - 5),
        10, 10,
        0, 0,
        200, 200);
    };
    canvas.addEventListener('mousemove', zoom);
  }
</script>
</body>
</html>       
Canvas簡介

11.5 儲存圖檔

 HTMLCanvasElement提供一個toDataUrl()方法,此方法在儲存圖檔的時候非常有用。它傳回一個包含類型參數規定的圖像個數的資料連結。圖檔分辨率是96dpi。

canvas.toDataUrl('image/png'):預設設定,建立一個png圖檔。

canvas.toDataUrl('image/jpeg', quality):建立一個jpg圖檔,可以設定第二個參數,從0到1,0表示圖檔比較粗糙,但是檔案比較小,1表示品質最好。當從畫布中生成一個資料連結,例如可以将它放在任何<image>元素,或者将它放在一個有download屬性的超連結裡用于儲存到本地。

也可以從畫布中建立一個Blob對象。

canvas.toBlob(callback, type, encoderOptions):從畫布圖檔中儲存Blob對象。

12. 點選區域和無障礙通路

<canvas>标簽隻是一個位圖,不提供任何已經繪制在上面的對象的資訊。canvas的内容不能像語義化的HTML一樣暴露一些協助工具。一般來說,應該避免在互動類型的網站或者App上使用canvas。下面讨論的内容可以讓canvas互動更加容易。

12.1 内容相容

<canvas></canvas>标簽裡的内容可以被一些不支援canvas的浏覽器提供相容。這對殘疾使用者裝置很有用(比如螢幕閱讀器),這樣它們可以讀取并解釋DOM裡的子節點。

<canvas> 
  <h2>Shapes</h2> 
  <p>A rectangle with a black border. 
   In the background is a pink circle. 
   Partially overlaying the <a href="http://en.wikipedia.org/wiki/Circle" onfocus="drawCircle();" onblur="drawPicture();">circle</a>. 
   Partially overlaying the circle is a green 
   <a href="http://en.wikipedia.org/wiki/Square" onfocus="drawSquare();" onblur="drawPicture();">square</a> 
   and a purple <a href="http://en.wikipedia.org/wiki/Triangle" onfocus="drawTriangle();" onblur="drawPicture();">triangle</a>,
   both of which are semi-opaque, so the full circle can be seen underneath.</p> 
</canvas>        

12.2 ARIA規則

Accessible Rich Internet Applications (ARIA) 定義了讓Web内容和Web應用更容易被有身體缺陷的人擷取的辦法。你可以用ARIA屬性來描述canvas元素的行為和存在目的。詳情見ARIA和 ARIA 技術。

12.3 點選區域(hit region)

判斷滑鼠坐标是否在canvas上一個特定區域裡一直是個有待解決的問題。hit region API讓你可以在canvas上定義一個區域,這讓無障礙工具擷取canvas上的互動内容成為可能。它能讓你更容易地進行點選檢測并把事件轉發到DOM元素去。這個API有以下三個方法(都是實驗性特性,請先在浏覽器相容表上确認再使用)

CanvasRenderingContext2D.addHitRegion():在canvas上添加一個點選區域。

CanvasRenderingContext2D.removeHitRegion():在canvas上移除指定id的點選區域。

CanvasRenderingContext2D.clearHitRegion():移除canvas上所有點選區域。

可以把一個點選區域添加到路徑裡并檢測MouseEvent.region屬性來測試滑鼠有沒有點選這個區域。

<canvas id="canvas"></canvas>
<script>
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.arc(70, 80, 10, 0, 2 * Math.PI, false);
ctx.fill();
ctx.addHitRegion({id: "circle"});

canvas.addEventListener("mousemove", function(event){
  if(event.region) {
    alert("hit region: " + event.region);
  }
});
</script>       

很不幸,目前chrome版本并不支援這個試驗中的特性。

Canvas簡介
Canvas簡介

addHitRegion()方法可以帶一個control選項可以把事件轉發到指定的元素(canvas裡的)上。

ctx.addHitRegion({control: element});       

12.4 焦點圈

當用鍵盤控制時,焦點圈是一個可以幫我們在頁面上快速導航的标記。要在canvas上繪制焦點圈,可以使用drawFocusIfNeeded屬性。

CanvasRenderingContext2D.drawFocusIfNeeded():如果給定的的元素擷取了焦點,這個方法會目前路徑畫焦點圈。另外,scroolPathIntoView()可以讓一個元素獲得焦點的時候在螢幕上可見(滾動到元素所在的區域) 。

CanvasRenderingContext2D.scrollPathIntoView():把目前路徑或者一個指定的路徑滾動到顯示區域内。

13. canvas的優化

<canvas>元素是衆多網絡2D圖像渲染标準之一。被廣泛應用于遊戲及複雜圖像可視化中。然而,随着網站和應用将canvas網布越來越複雜,性能開始成為問題。這裡有一些canvas畫布元素優化的建議,保證使用時渲染性能。

13.1 性能優化建議

在離屏canvas上預渲染想死的圖形或者重複對象

如果發現每一幀裡有好多複雜的圖畫運算,可以考慮建立一個離屏canvas,将圖像在這個畫布上畫一次(或者每當圖像改變時畫一次),然後在每幀上畫處視線以外的這個畫布。

myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");
myEntity.render(myEntity.offscreenContext);       

避免使用浮點數的坐标點,使用整數

當畫一個有浮點數的對象時,會發生子像素渲染。

ctx.drawImage(myImage, 0.3, 0.5);       

浏覽器為了達到抗鋸齒的效果會做額外的運算。為了避免這種情況,保證在調用drawImage()函數時,使用Math.floor()函數對所有坐标點取整。 

不要在drawImage時縮放圖像

有些動畫會出現這種情況,一部分元素不斷地改變或者移動,而其他的元素,例如外觀,永遠不變。這種情況的一種優化方法是用多個畫布建立不同的層次。

例如:可以在最頂層建立一個外觀層,而且僅僅在使用者輸入的時候被畫出。可以建立一個遊戲層,在這個遊戲層上面會有不斷更新的元素和一個背景層,用于繪制那些較少更新的元素。

<div id="stage">
  <canvas id="ui-layer" width="480" height="320"></canvas>
  <canvas id="game-layer" width="480" height="320"></canvas>
  <canvas id="background-layer" width="480" height="320"></canvas>
</div>
 
<style>
  #stage {
    width: 480px;
    height: 320px;
    position: relative;
    border: 2px solid black
  }
  canvas { position: absolute; }
  #ui-layer { z-index: 3 }
  #game-layer { z-index: 2 }
  #background-layer { z-index: 1 }
</style>       

使用CSS設定大的背景圖 

如果像大多數遊戲那樣,有一個靜态的背景,可以使用一個div元素,給background特性設定圖檔,以及将它置于畫布元素之後。了這麼做可以避免在每一幀畫布上繪制大圖。

使用CSS transforms特性縮放畫布

CSS Transform特性由于調用GPU,是以更快捷。最好的情況是,不要将小畫布放大,而是去将大畫布縮小。例如Firefox系統,目标分辨率是480X320px。

var scaleX = canvas.width / window.innerWidth;
var scaleY = canvas.height / window.innerHeight;

var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);

stage.style.transformOrigin = '0 0'; //scale from top left
stage.style.transform = 'scale(' + scaleToFit + ')';       

關閉透明度

如果遊戲使用畫布而且不需要透明,可以使用HtmlElement.getContext()建立一個繪制上下文把alpha選項設定為false。這個選項可以幫助浏覽器進行内部優化。

var ctx = canvas.getContext('2d', { alpha: false });       

其他優化建議

  • 将畫布的函數調用集合到一起(例如:畫一條折線,而不要畫多條分開的直線)
  • 避免不必要的畫布狀态改變
  • 渲染畫布中不同點,而非整個更新狀态
  • 盡量避免使用shadowBlur特性
  • 盡量避免text rendering
  • 使用不同的辦法清除畫布(clearRect(),fillRect(),刁征canvas大小)
  • 使用windows.requestAnimationFrame()調用動畫,而非window.setInterval() 

作者:

Tyler Ning

出處:http://www.cnblogs.com/tylerdonet/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,如有問題,請微信聯系冬天裡的一把火

Canvas簡介

繼續閱讀