學習HTML5 Canvas這一篇文章就夠了

linvic發表於2019-02-16

一、canvas簡介

<canvas>HTML5 新增的,一個可以使用指令碼(通常為JavaScript)在其中繪製影象的 HTML 元素。它可以用來製作照片集或者製作簡單(也不是那麼簡單)的動畫,甚至可以進行實時視訊處理和渲染。

​ 它最初由蘋果內部使用自己MacOS X WebKit推出,供應用程式使用像儀表盤的構件和 Safari 瀏覽器使用。 後來,有人通過Gecko核心的瀏覽器 (尤其是MozillaFirefox),OperaChrome和超文字網路應用技術工作組建議為下一代的網路技術使用該元素。

Canvas是由HTML程式碼配合高度和寬度屬性而定義出的可繪製區域。JavaScript程式碼可以訪問該區域,類似於其他通用的二維API,通過一套完整的繪圖函式來動態生成圖形。

​ Mozilla 程式從 Gecko 1.8 (Firefox 1.5)開始支援 <canvas>, Internet Explorer 從IE9開始<canvas> 。Chrome和Opera 9+ 也支援 <canvas>

二、Canvas基本使用

2.1 <canvas>元素

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

<canvas>看起來和<img>標籤一樣,只是 <canvas> 只有兩個可選的屬性 width、heigth 屬性,而沒有 src、alt 屬性。

​ 如果不給<canvas>設定widht、height屬性時,則預設 width為300、height為150,單位都是px。也可以使用css屬性來設定寬高,但是如寬高屬性和初始比例不一致,他會出現扭曲。所以,建議永遠不要使用css屬性來設定<canvas>的寬高。

###替換內容

​ 由於某些較老的瀏覽器(尤其是IE9之前的IE瀏覽器)或者瀏覽器不支援HTML元素<canvas>,在這些瀏覽器上你應該總是能展示替代內容。

​ 支援<canvas>的瀏覽器會只渲染<canvas>標籤,而忽略其中的替代內容。不支援 <canvas> 的瀏覽器則 會直接渲染替代內容。

用文字替換:

<canvas>
    你的瀏覽器不支援canvas,請升級你的瀏覽器
</canvas>

<img> 替換:

<canvas>
    <img src="./美女.jpg" alt=""> 
</canvas>

結束標籤</canvas>不可省

<img>元素不同,<canvas>元素需要結束標籤(</canvas>)。如果結束標籤不存在,則文件的其餘部分會被認為是替代內容,將不會顯示出來。

2.2 渲染上下文(Thre Rending Context)

<canvas>會建立一個固定大小的畫布,會公開一個或多個 渲染上下文(畫筆),使用 渲染上下文來繪製和處理要展示的內容。

​ 我們重點研究 2D渲染上下文。 其他的上下文我們暫不研究,比如, WebGL使用了基於OpenGL ES的3D上下文 (“experimental-webgl”) 。

var canvas = document.getElementById('tutorial');
//獲得 2d 上下文物件
var ctx = canvas.getContext('2d');

2.3 檢測支援性

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>
    <style type="text/css">
        canvas {
            border: 1px solid black;
        }
    </style>
</head>
<canvas id="tutorial" width="300" height="300"></canvas>
</body>
<script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
      	var ctx = canvas.getContext("2d");
      	//開始程式碼
        
    }
    draw();
</script>
</html>

2.5 一個簡單的例子

繪製兩個長方形。

<html>
<head>
    <title>Canvas tutorial</title>
    <style type="text/css">
        canvas {
            border: 1px solid black;
        }
    </style>
</head>
<canvas id="tutorial" width="300" height="300"></canvas>
</body>
<script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        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);
    }
    draw();
</script>
</html>

三、繪製形狀

3.1 柵格(grid)和座標空間

​ 如下圖所示,canvas元素預設被網格所覆蓋。通常來說網格中的一個單元相當於canvas元素中的一畫素。柵格的起點為左上角(座標為(0,0))。所有元素的位置都相對於原點來定位。所以圖中藍色方形左上角的座標為距離左邊(X軸)x畫素,距離上邊(Y軸)y畫素(座標為(x,y))。

​ 後面我們會涉及到座標原點的平移、網格的旋轉以及縮放等。

3.2 繪製矩形

<canvas> 只支援一種原生的 圖形繪製:矩形。所有其他圖形都至少需要生成一種路徑(path)。不過,我們擁有眾多路徑生成的方法讓複雜圖形的繪製成為了可能。

canvast 提供了三種方法繪製矩形:

  1. fillRect(x, y, width, height)

    繪製一個填充的矩形

  2. strokeRect(x, y, width, height)

    繪製一個矩形的邊框

  3. clearRect(x, y, widh, height)

    清除指定的矩形區域,然後這塊區域會變的完全透明。

說明:

​ 這3個方法具有相同的引數。

x, y:指的是矩形的左上角的座標。(相對於canvas的座標原點)

width, height:指的是繪製的矩形的寬和高。

function draw(){
    var canvas = document.getElementById('tutorial');
    if(!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.fillRect(10, 10, 100, 50);  //繪製矩形,填充的預設顏色為黑色
    ctx.strokeRect(10, 70, 100, 50);  //繪製矩形邊框
    
}
draw();

ctx.clearRect(15, 15, 50, 25);

四、繪製路徑(path)

​ 圖形的基本元素是路徑。

​ 路徑是通過不同顏色和寬度的線段或曲線相連形成的不同形狀的點的集合。

​ 一個路徑,甚至一個子路徑,都是閉合的。

使用路徑繪製圖形需要一些額外的步驟:

  1. 建立路徑起始點
  2. 呼叫繪製方法去繪製出路徑
  3. 把路徑封閉
  4. 一旦路徑生成,通過描邊或填充路徑區域來渲染圖形。

下面是需要用到的方法:

  1. beginPath()

    新建一條路徑,路徑一旦建立成功,圖形繪製命令被指向到路徑上生成路徑

  2. moveTo(x, y)

    把畫筆移動到指定的座標(x, y)。相當於設定路徑的起始點座標。

  3. closePath()

    閉合路徑之後,圖形繪製命令又重新指向到上下文中

  4. stroke()

    通過線條來繪製圖形輪廓

  5. fill()

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

4.1 繪製線段

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath(); //新建一條path
    ctx.moveTo(50, 50); //把畫筆移動到指定的座標
    ctx.lineTo(200, 50);  //繪製一條從當前位置到指定座標(200, 50)的直線.
    //閉合路徑。會拉一條從當前點到path起始點的直線。如果當前點與起始點重合,則什麼都不做
    ctx.closePath();
    ctx.stroke(); //繪製路徑。
}
draw();

4.2 繪製三角形邊框

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.lineTo(200, 50);
    ctx.lineTo(200, 200);
  	ctx.closePath(); //雖然我們只繪製了兩條線段,但是closePath會closePath,仍然是一個3角形
    ctx.stroke(); //描邊。stroke不會自動closePath()
}
draw();

4.3 填充三角形

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.lineTo(200, 50);
    ctx.lineTo(200, 200);
   
    ctx.fill(); //填充閉合區域。如果path沒有閉合,則fill()會自動閉合路徑。
}
draw();

4.4 繪製圓弧

有兩個方法可以繪製圓弧:

  1. arc(x, y, r, startAngle, endAngle, anticlockwise):

    (x, y)為圓心,以r為半徑,從 startAngle弧度開始到endAngle弧度結束。anticlosewise是布林值,true表示逆時針,false表示順時針。(預設是順時針)

    注意:

    1. 這裡的度數都是弧度。
    2. 0弧度是指的x軸正方形
    radians=(Math.PI/180)*degrees   //角度轉換成弧度
    
  2. arcTo(x1, y1, x2, y2, radius):

    根據給定的控制點和半徑畫一段圓弧,最後再以直線連線兩個控制點。

圓弧案例1:

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.arc(50, 50, 40, 0, Math.PI / 2, false);
    ctx.stroke();
}
draw();

圓弧案例2:

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.arc(50, 50, 40, 0, Math.PI / 2, false);
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(150, 50, 40, 0, -Math.PI / 2, true);
    ctx.closePath();
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(50, 150, 40, -Math.PI / 2, Math.PI / 2, false);
    ctx.fill();

    ctx.beginPath();
    ctx.arc(150, 150, 40, 0, Math.PI, false);
    ctx.fill();

}
draw();

圓弧案例3:

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(50, 50);
  	//引數1、2:控制點1座標   引數3、4:控制點2座標  引數4:圓弧半徑
    ctx.arcTo(200, 50, 200, 200, 100);
    ctx.lineTo(200, 200)
    ctx.stroke();
    
    ctx.beginPath();
    ctx.rect(50, 50, 10, 10);
    ctx.rect(200, 50, 10, 10)
    ctx.rect(200, 200, 10, 10)
    ctx.fill()
}
draw();

arcTo方法的說明:

​ 這個方法可以這樣理解。繪製的弧形是由兩條切線所決定。

​ 第 1 條切線:起始點和控制點1決定的直線。

​ 第 2 條切線:控制點1 和控制點2決定的直線。

其實繪製的圓弧就是與這兩條直線相切的圓弧。

4.5 繪製貝塞爾曲線

4.5.1 什麼是貝塞爾曲線

​ 貝塞爾曲線(Bézier curve),又稱貝茲曲線或貝濟埃曲線,是應用於二維圖形應用程式的數學曲線。

​ 一般的向量圖形軟體通過它來精確畫出曲線,貝茲曲線由線段與節點組成,節點是可拖動的支點,線段像可伸縮的皮筋,我們在繪圖工具上看到的鋼筆工具就是來做這種向量曲線的。

​ 貝塞爾曲線是計算機圖形學中相當重要的引數曲線,在一些比較成熟的點陣圖軟體中也有貝塞爾曲線工具如PhotoShop等。在Flash4中還沒有完整的曲線工具,而在Flash5裡面已經提供出貝塞爾曲線工具。

​ 貝塞爾曲線於1962,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau演演算法開發,以穩定數值的方法求出貝茲曲線。

一次貝塞爾曲線(線性貝塞爾曲線)

​ 一次貝塞爾曲線其實是一條直線。

二次貝塞爾曲線

三次貝塞爾曲線

4.5.2 繪製貝塞爾曲線

繪製二次貝塞爾曲線

quadraticCurveTo(cp1x, cp1y, x, y):

說明:

​ 引數1和2:控制點座標

​ 引數3和4:結束點座標

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(10, 200); //起始點
    var cp1x = 40, cp1y = 100;  //控制點
    var x = 200, y = 200; // 結束點
    //繪製二次貝塞爾曲線
    ctx.quadraticCurveTo(cp1x, cp1y, x, y);
    ctx.stroke();
    
    ctx.beginPath();
    ctx.rect(10, 200, 10, 10);
    ctx.rect(cp1x, cp1y, 10, 10);
    ctx.rect(x, y, 10, 10);
    ctx.fill();
    
}
draw();

繪製三次貝塞爾曲線

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

說明:

​ 引數1和2:控制點1的座標

​ 引數3和4:控制點2的座標

​ 引數5和6:結束點的座標

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(40, 200); //起始點
    var cp1x = 20, cp1y = 100;  //控制點1
    var cp2x = 100, cp2y = 120;  //控制點2
    var x = 200, y = 200; // 結束點
    //繪製二次貝塞爾曲線
    ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
    ctx.stroke();

    ctx.beginPath();
    ctx.rect(40, 200, 10, 10);
    ctx.rect(cp1x, cp1y, 10, 10);
    ctx.rect(cp2x, cp2y, 10, 10);
    ctx.rect(x, y, 10, 10);
    ctx.fill();

}
draw();

五、新增樣式和顏色

​ 在前面的繪製矩形章節中,只用到了預設的線條和顏色。

​ 如果想要給圖形上色,有兩個重要的屬性可以做到。

  1. fillStyle = color

    設定圖形的填充顏色

  2. strokeStyle = color

    設定圖形輪廓的顏色

備註:

1. `color` 可以是表示 `css` 顏色值的字串、漸變物件或者圖案物件。
2. 預設情況下,線條和填充顏色都是黑色。
3. 一旦您設定了 `strokeStyle` 或者 `fillStyle` 的值,那麼這個新值就會成為新繪製的圖形的預設值。如果你要給每個圖形上不同的顏色,你需要重新設定 `fillStyle` 或 `strokeStyle` 的值。

fillStyle

function draw(){
  var canvas = document.getElementById('tutorial');
  if (!canvas.getContext) return;
  var ctx = 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 * 50, i * 50, 50, 50);
    }
  }
}
draw();

strokeStyle

<script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        var ctx = canvas.getContext("2d");
        for (var i = 0; i < 6; i++){
            for (var j = 0; j < 6; j++){
                ctx.strokeStyle = `rgb(${randomInt(0, 255)},${randomInt(0, 255)},${randomInt(0, 255)})`;
                ctx.strokeRect(j * 50, i * 50, 40, 40);
            }
        }
    }
    draw();
    /**
     作者:李振超      4 Jun 2017 12:12
     返回隨機的 [from, to] 之間的整數(包括from,也包括to)
     */
    function randomInt(from, to){
        return parseInt(Math.random() * (to - from + 1) + from);
    }

</script>

Transparency(透明度)

globalAlpha = transparencyValue

​ 這個屬性影響到 canvas 裡所有圖形的透明度,有效的值範圍是 0.0 (完全透明)到 1.0(完全不透明),預設是 1.0。

globalAlpha 屬性在需要繪製大量擁有相同透明度的圖形時候相當高效。不過,我認為使用rgba()設定透明度更加好一些。

line style

1. lineWidth = value

線寬。只能是正值。預設是1.0

起始點和終點的連線為中心,上下各佔線寬的一半

ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.lineWidth = 10;
ctx.stroke();

ctx.beginPath();
ctx.moveTo(110, 10);
ctx.lineTo(160, 10)
ctx.lineWidth = 20;
ctx.stroke()

###2. lineCap = type

線條末端樣式。

共有3個值:

  1. butt:線段末端以方形結束

  2. round:線段末端以圓形結束

  3. square:線段末端以方形結束,但是增加了一個寬度和線段相同,高度是線段厚度一半的矩形區域。

    var lineCaps = ["butt", "round", "square"];
    
    for (var i = 0; i < 3; i++){
        ctx.beginPath();
        ctx.moveTo(20 + 30 * i, 30);
        ctx.lineTo(20 + 30 * i, 100);
        ctx.lineWidth = 20;
        ctx.lineCap = lineCaps[i];
        ctx.stroke();
    }
    
    ctx.beginPath();
    ctx.moveTo(0, 30);
    ctx.lineTo(300, 30);
    
    ctx.moveTo(0, 100);
    ctx.lineTo(300, 100)
    
    ctx.strokeStyle = "red";
    ctx.lineWidth = 1;
    ctx.stroke();
    

3. lineJoin = type

同一個path內,設定線條與線條間接合處的樣式。

共有3個值round, bevelmiter

  1. round

    通過填充一個額外的,圓心在相連部分末端的扇形,繪製拐角的形狀。 圓角的半徑是線段的寬度。

  2. bevel

    在相連部分的末端填充一個額外的以三角形為底的區域, 每個部分都有各自獨立的矩形拐角。

  3. miter(預設)

    通過延伸相連部分的外邊緣,使其相交於一點,形成一個額外的菱形區域。

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

    var lineJoin = ['round', 'bevel', 'miter'];
    ctx.lineWidth = 20;

    for (var i = 0; i < lineJoin.length; i++){
        ctx.lineJoin = lineJoin[i];
        ctx.beginPath();
        ctx.moveTo(50, 50 + i * 50);
        ctx.lineTo(100, 100 + i * 50);
        ctx.lineTo(150, 50 + i * 50);
        ctx.lineTo(200, 100 + i * 50);
        ctx.lineTo(250, 50 + i * 50);
        ctx.stroke();
    }

}
draw();

4. 虛線

setLineDash 方法和 lineDashOffset 屬性來制定虛線樣式. setLineDash 方法接受一個陣列,來指定線段與間隙的交替;lineDashOffset屬性設定起始偏移量.

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    
    ctx.setLineDash([20, 5]);  // [實線長度, 間隙長度]
    ctx.lineDashOffset = -0;
    ctx.strokeRect(50, 50, 210, 210);
}
draw();

備註:

getLineDash():返回一個包含當前虛線樣式,長度為非負偶數的陣列。

六、繪製文字

繪製文字的兩個方法

canvas 提供了兩種方法來渲染文字:

  1. fillText(text, x, y [, maxWidth])

    在指定的(x,y)位置填充指定的文字,繪製的最大寬度是可選的.

  2. strokeText(text, x, y [, maxWidth])

    在指定的(x,y)位置繪製文字邊框,繪製的最大寬度是可選的.

var ctx;
function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    ctx = canvas.getContext("2d");
    ctx.font = "100px sans-serif"
    ctx.fillText("天若有情", 10, 100);
    ctx.strokeText("天若有情", 10, 200)
}
draw();

給文字新增樣式

  1. font = value

    當前我們用來繪製文字的樣式。這個字串使用和 CSS font屬性相同的語法. 預設的字型是 10px sans-serif

  2. textAlign = value

    文字對齊選項. 可選的值包括:start, end, left, right or center. 預設值是 start

  3. textBaseline = value

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

  4. direction = value

    文字方向。可能的值包括:ltr, rtl, inherit。預設值是 inherit。

七、繪製圖片

​ 我們也可以在canvas上直接繪製圖片。

7.1 由零開始建立圖片

建立<img>元素

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

指令碼執行後圖片開始裝載

繪製img

//引數1:要繪製的img  引數2、3:繪製的img在canvas中的座標
ctx.drawImage(img,0,0); 

注意:

​ 考慮到圖片是從網路載入,如果 drawImage 的時候圖片還沒有完全載入完成,則什麼都不做,個別瀏覽器會拋異常。所以我們應該保證在 img 繪製完成之後再 drawImage

var img = new Image();   // 建立img元素
img.onload = function(){
  ctx.drawImage(img, 0, 0)
}
img.src = 'myImage.png'; // 設定圖片源地址

7.2 繪製 img 標籤元素中的圖片

img 可以 new 也可以來源於我們頁面的 <img>標籤

<img src="./美女.jpg" alt="" width="300"><br>
<canvas id="tutorial" width="600" height="400"></canvas>
<script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        var ctx = canvas.getContext("2d");
        var img = document.querySelector("img");
        ctx.drawImage(img, 0, 0);
    }
    document.querySelector("img").onclick = function (){
        draw();
    }

</script>

第一張圖片就是頁面中的<img>標籤

7.3 縮放圖片

drawImage() 也可以再新增兩個引數:

drawImage(image, x, y, width, height)

​ 這個方法多了2個引數:widthheight,這兩個引數用來控制 當像canvas畫入時應該縮放的大小。

ctx.drawImage(img, 0, 0, 400, 200)

7.4 切片(slice)

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

​ 第一個引數和其它的是相同的,都是一個影象或者另一個 canvas 的引用。

其他8個引數:

​ 前4個是定義影象源的切片位置和大小,

​ 後4個則是定義切片的目標顯示位置和大小。

八、狀態的儲存和恢復

Saving and restoring state是繪製複雜圖形時必不可少的操作。

save()和restore()

saverestore 方法是用來儲存和恢復 canvas 狀態的,都沒有引數。

Canvas 的狀態就是當前畫面應用的所有樣式和變形的一個快照。

  1. 關於 save()

    Canvas狀態儲存在棧中,每當save()方法被呼叫後,當前的狀態就被推送到棧中儲存。一個繪畫狀態包括:

  • 當前應用的變形(即移動,旋轉和縮放)

  • strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值

  • 當前的裁切路徑(clipping path

    可以呼叫任意多次 save方法。(類似陣列的push())

  1. 關於restore()

    每一次呼叫 restore 方法,上一個儲存的狀態就從棧中彈出,所有設定都恢復。(類似陣列的pop())

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

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

    ctx.fillStyle = 'red'       // 在原有配置基礎上對顏色做改變
    ctx.fillRect(15, 15, 120, 120); // 使用新的設定繪製一個矩形

    ctx.save();                  // 儲存當前狀態
    ctx.fillStyle = '#FFF'       // 再次改變顏色配置
    ctx.fillRect(30, 30, 90, 90);   // 使用新的配置繪製一個矩形

    ctx.restore();               // 重新載入之前的顏色狀態
    ctx.fillRect(45, 45, 60, 60);   // 使用上一次的配置繪製一個矩形

    ctx.restore();               // 載入預設顏色配置
    ctx.fillRect(60, 60, 30, 30);   // 使用載入的配置繪製一個矩形
}
draw();

九、變形

9.1 translate

translate(x, y)

​ 用來移動 canvas原點到指定的位置

translate方法接受兩個引數。x 是左右偏移量,y 是上下偏移量,如右圖所示。

在做變形之前先儲存狀態是一個良好的習慣。大多數情況下,呼叫 restore 方法比手動恢復原先的狀態要簡單得多。又如果你是在一個迴圈中做位移但沒有儲存和恢復canvas 的狀態,很可能到最後會發現怎麼有些東西不見了,那是因為它很可能已經超出 canvas 範圍以外了。

​ 注意:translate移動的是canvas的座標原點。(座標變換)

var ctx;
function draw(){
    var canvas = document.getElementById('tutorial1');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.save(); //儲存坐原點平移之前的狀態
    ctx.translate(100, 100);
    ctx.strokeRect(0, 0, 100, 100)
    ctx.restore(); //恢復到最初狀態
    ctx.translate(220, 220);
    ctx.fillRect(0, 0, 100, 100)
}
draw();

9.2 rotate

rotate(angle)

​ 旋轉座標軸。

​ 這個方法只接受一個引數:旋轉的角度(angle),它是順時針方向的,以弧度為單位的值。

​ 旋轉的中心是座標原點。

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

  ctx.fillStyle = "red";
  ctx.save();

  ctx.translate(100, 100);
  ctx.rotate(Math.PI / 180 * 45);
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore();

  ctx.save();
  ctx.translate(0, 0);
  ctx.fillRect(0, 0, 50, 50)
  ctx.restore();
}
draw();

9.3 scale

scale(x, y)

​ 我們用它來增減圖形在 canvas 中的畫素數目,對形狀,點陣圖進行縮小或者放大。

scale方法接受兩個引數。x,y分別是橫軸和縱軸的縮放因子,它們都必須是正值。值比 1.0 小表示縮 小,比 1.0 大則表示放大,值為 1.0 時什麼效果都沒有。

​ 預設情況下,canvas 的 1 單位就是 1 個畫素。舉例說,如果我們設定縮放因子是 0.5,1 個單位就變成對應 0.5 個畫素,這樣繪製出來的形狀就會是原先的一半。同理,設定為 2.0 時,1 個單位就對應變成了 2 畫素,繪製的結果就是圖形放大了 2 倍。

9.4 transform(變形矩陣)

transform(a, b, c, d, e, f)

a (m11)

​ Horizontal scaling.

b (m12)

​ Horizontal skewing.

c (m21)

​ Vertical skewing.

d (m22)

​ Vertical scaling.

e (dx)

​ Horizontal moving.

f (dy)

​ Vertical moving.

var ctx;
function draw(){
    var canvas = document.getElementById('tutorial1');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.transform(1, 1, 0, 1, 0, 0);
    ctx.fillRect(0, 0, 100, 100);
}
draw();

十、合成

​ 在前面的所有例子中、,我們總是將一個圖形畫在另一個之上,對於其他更多的情況,僅僅這樣是遠遠不夠的。比如,對合成的圖形來說,繪製順序會有限制。不過,我們可以利用 globalCompositeOperation 屬性來改變這種狀況。

globalCompositeOperation = type

    var ctx;
    function draw(){
        var canvas = document.getElementById('tutorial1');
        if (!canvas.getContext) return;
        var ctx = canvas.getContext("2d");
        
        ctx.fillStyle = "blue";
        ctx.fillRect(0, 0, 200, 200);

        ctx.globalCompositeOperation = "source-over"; //全域性合成操作
        ctx.fillStyle = "red";
        ctx.fillRect(100, 100, 200, 200);
    }
    draw();

</script>

注:下面的展示中,藍色是原有的,紅色是新的。

type `是下面 13 種字串值之一:

##1. source-over(default)

這是預設設定,新影象會覆蓋在原有影象。

##2. source-in

僅僅會出現新影象與原來影象重疊的部分,其他區域都變成透明的。(包括其他的老影象區域也會透明)

##3. source-out

僅僅顯示新影象與老影象沒有重疊的部分,其餘部分全部透明。(老影象也不顯示)

##4. source-atop

新影象僅僅顯示與老影象重疊區域。老影象仍然可以顯示。

##5. destination-over

新影象會在老影象的下面。

##6. destination-in

僅僅新老影象重疊部分的老影象被顯示,其他區域全部透明。

##7. destination-out

僅僅老影象與新影象沒有重疊的部分。 注意顯示的是老影象的部分割槽域。

##8. destination-atop

老影象僅僅僅僅顯示重疊部分,新影象會顯示在老影象的下面。

##9. lighter

新老影象都顯示,但是重疊區域的顏色做加處理

##10. darken

保留重疊部分最黑的畫素。(每個顏色位進行比較,得到最小的)

blue: #0000ff

red: #ff0000

所以重疊部分的顏色:#000000

##11. lighten

保證重疊部分最量的畫素。(每個顏色位進行比較,得到最大的)

blue: #0000ff

red: #ff0000

所以重疊部分的顏色:#ff00ff

##12. xor

重疊部分會變成透明

##13. copy

只有新影象會被保留,其餘的全部被清除(邊透明)

#十一、裁剪路徑

clip()

​ 把已經建立的路徑轉換成裁剪路徑。

​ 裁剪路徑的作用是遮罩。只顯示裁剪路徑內的區域,裁剪路徑外的區域會被隱藏。

​ 注意:clip()只能遮罩在這個方法呼叫之後繪製的影象,如果是clip()方法呼叫之前繪製的影象,則無法實現遮罩。

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

    ctx.beginPath();
    ctx.arc(20,20, 100, 0, Math.PI * 2);
    ctx.clip();
    
    ctx.fillStyle = "pink";
    ctx.fillRect(20, 20, 100,100);
}
draw();

十二、動畫

動畫的基本步驟

  1. 清空canvas

    再繪製每一幀動畫之前,需要清空所有。清空所有最簡單的做法就是clearRect()方法

  2. 儲存canvas狀態

    如果在繪製的過程中會更改canvas的狀態(顏色、移動了座標原點等),又在繪製每一幀時都是原始狀態的話,則最好儲存下canvas的狀態

  3. 繪製動畫圖形

    這一步才是真正的繪製動畫幀

  4. 恢復canvas狀態

    如果你前面儲存了canvas狀態,則應該在繪製完成一幀之後恢復canvas狀態。

控制動畫

我們可用通過canvas的方法或者自定義的方法把影象會知道到canvas上。正常情況,我們能看到繪製的結果是在指令碼執行結束之後。例如,我們不可能在一個 for 迴圈內部完成動畫。

也就是,為了執行動畫,我們需要一些可以定時執行重繪的方法。

一般用到下面三個方法:

  1. setInterval()
  2. setTimeout()
  3. requestAnimationFrame()

##案例1:太陽系

let sun;
let earth;
let moon;
let ctx;
function init(){
    sun = new Image();
    earth = new Image();
    moon = new Image();
    sun.src = "sun.png";
    earth.src = "earth.png";
    moon.src = "moon.png";

    let canvas = document.querySelector("#solar");
    ctx = canvas.getContext("2d");

    sun.onload = function (){
        draw()
    }

}
init();
function draw(){
    ctx.clearRect(0, 0, 300, 300); //清空所有的內容
    /*繪製 太陽*/
    ctx.drawImage(sun, 0, 0, 300, 300);

    ctx.save();
    ctx.translate(150, 150);

    //繪製earth軌道
    ctx.beginPath();
    ctx.strokeStyle = "rgba(255,255,0,0.5)";
    ctx.arc(0, 0, 100, 0, 2 * Math.PI)
    ctx.stroke()

    let time = new Date();
    //繪製地球
    ctx.rotate(2 * Math.PI / 60 * time.getSeconds() + 2 * Math.PI / 60000 * time.getMilliseconds())
    ctx.translate(100, 0);
    ctx.drawImage(earth, -12, -12)

    //繪製月球軌道
    ctx.beginPath();
    ctx.strokeStyle = "rgba(255,255,255,.3)";
    ctx.arc(0, 0, 40, 0, 2 * Math.PI);
    ctx.stroke();

    //繪製月球
    ctx.rotate(2 * Math.PI / 6 * time.getSeconds() + 2 * Math.PI / 6000 * time.getMilliseconds());
    ctx.translate(40, 0);
    ctx.drawImage(moon, -3.5, -3.5);
    ctx.restore();

    requestAnimationFrame(draw);
}

##案例2:模擬時鐘

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body {
            padding: 0;
            margin: 0;
            background-color: rgba(0, 0, 0, 0.1)
        }
        
        canvas {
            display: block;
            margin: 200px auto;
        }
    </style>
</head>
<body>
<canvas id="solar" width="300" height="300"></canvas>
<script>
    init();

    function init(){
        let canvas = document.querySelector("#solar");
        let ctx = canvas.getContext("2d");
        draw(ctx);
    }

    function draw(ctx){
        requestAnimationFrame(function step(){
            drawDial(ctx); //繪製錶盤
            drawAllHands(ctx); //繪製時分秒針
            requestAnimationFrame(step);
        });
    }
    /*繪製時分秒針*/
    function drawAllHands(ctx){
        let time = new Date();

        let s = time.getSeconds();
        let m = time.getMinutes();
        let h = time.getHours();
        
        let pi = Math.PI;
        let secondAngle = pi / 180 * 6 * s;  //計算出來s針的弧度
        let minuteAngle = pi / 180 * 6 * m + secondAngle / 60;  //計算出來分針的弧度
        let hourAngle = pi / 180 * 30 * h + minuteAngle / 12;  //計算出來時針的弧度

        drawHand(hourAngle, 60, 6, "red", ctx);  //繪製時針
        drawHand(minuteAngle, 106, 4, "green", ctx);  //繪製分針
        drawHand(secondAngle, 129, 2, "blue", ctx);  //繪製秒針
    }
    /*繪製時針、或分針、或秒針
     * 引數1:要繪製的針的角度
     * 引數2:要繪製的針的長度
     * 引數3:要繪製的針的寬度
     * 引數4:要繪製的針的顏色
     * 引數4:ctx
     * */
    function drawHand(angle, len, width, color, ctx){
        ctx.save();
        ctx.translate(150, 150); //把座標軸的遠點平移到原來的中心
        ctx.rotate(-Math.PI / 2 + angle);  //旋轉座標軸。 x軸就是針的角度
        ctx.beginPath();
        ctx.moveTo(-4, 0);
        ctx.lineTo(len, 0);  // 沿著x軸繪製針
        ctx.lineWidth = width;
        ctx.strokeStyle = color;
        ctx.lineCap = "round";
        ctx.stroke();
        ctx.closePath();
        ctx.restore();
    }
    
    /*繪製錶盤*/
    function drawDial(ctx){
        let pi = Math.PI;
        
        ctx.clearRect(0, 0, 300, 300); //清除所有內容
        ctx.save();

        ctx.translate(150, 150); //一定座標原點到原來的中心
        ctx.beginPath();
        ctx.arc(0, 0, 148, 0, 2 * pi); //繪製圓周
        ctx.stroke();
        ctx.closePath();

        for (let i = 0; i < 60; i++){//繪製刻度。
            ctx.save();
            ctx.rotate(-pi / 2 + i * pi / 30);  //旋轉座標軸。座標軸x的正方形從 向上開始算起
            ctx.beginPath();
            ctx.moveTo(110, 0);
            ctx.lineTo(140, 0);
            ctx.lineWidth = i % 5 ? 2 : 4;
            ctx.strokeStyle = i % 5 ? "blue" : "red";
            ctx.stroke();
            ctx.closePath();
            ctx.restore();
        }
        ctx.restore();
    }
</script>
</body>
</html>

相關文章