canvas初探實踐-第一篇

白雲飄飄發表於2018-06-05

新增canvas元素

作為新手,比如我,探索canvas是在頁面上加個canvas元素開始的。

<style type="text/css">
  #id {
    border: 1px dashed gray;
  }
</style>
<canvas id="canvas">不支援</canvas>
複製程式碼

重新整理頁面後,頁面上就能看到一個虛線框,這個就是預設大小的canvas元素了。 把canvas寬度和高度設定大些,在css裡定義好寬度和高度後,事實上並沒有效果。解決的辦法直接在html屬性中定義。

<style type="text/css">
  #id {
    border: 1px dashed gray;
  }
</style>
<canvas id="canvas" width=500 height=500>不支援</canvas>
複製程式碼

繪製一條直線

canvas只是一張白紙,而繪製的筆由 document.getElementById('canvas').getContext("2d") 物件提供。

const cxt = document.getElementById('canvas').getContext("2d");
複製程式碼

我們從座標原點作為起點(0,0),離起點x軸100,y軸200處作為終點(100,200),繪製一條直線。 首先告訴畫筆移動到起點位置

cxt.moveTo(0, 0);
複製程式碼

然後告訴畫筆,從起點開始畫直線,一直畫到終點位置

cxt.lineTo(100, 200);
複製程式碼

最後告訴畫筆已經確定好點了,可以開始繪製

cxt.stroke();
複製程式碼

新手需要了解,canvas的座標系是 W3C座標系,y軸正方向向下,座標原點在螢幕的左上角。

至此,已經學習了三個重要的 API: moveTo, lineTo, stroke。

繪製多邊形

思考下:多邊形是多條直線按照一定角度連線在一起的,那定義好各個點座標,分別呼叫 moveTo、 lineTo。當然還有個更便捷的方式實現:
lineTo方法是可以重複呼叫的,第一次呼叫lineTo後,畫筆自動移動到終點座標位置,第二次呼叫lineTo後,該起點座標就為上一個終點座標,以此類推。
繪製直角三角形

const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");

cxt.beginPath();
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.lineTo(120, 180);
cxt.closePath();
cxt.stroke();
複製程式碼

解析:
首先確定直角三角形的三個座標點,然後呼叫beginPath, 開始一條新路徑;
接下來畫筆移動到其中一個座標點,從這個座標點開始一直畫直線;
最後呼叫 closePath, 關閉當前路徑,將前面兩條線閉合;
最後呼叫 stroke,繪製直角三角形。

繪製正多邊形
多邊形是按照一定角度連線的,確定了角度就可以通過三角函式來繪製多邊形了,如下:

//正多邊形
/**
 n : 表示n邊型
 dx、dy:表示n邊型中心座標
 size:表示n邊型大小(邊的長度)
*/
function createPolygon(ctx, n, dx, dy, size) {
  //每個角的角度
  const degree = (2 * Math.PI)/n;
  cxt.beginPath();
  for(let i=0; i<n; i++) {
    // 三角函式
    let x = Math.cos(i*degree);
    let y = Math.sin(i*degree);
    cxt.lineTo( x * size + dx, y * size + dy );
  }
  cxt.closePath();
}
cxt.strokeStyle="rgba(255,0,0,0.7)";
// 繪製正五多邊形
createPolygon(cxt, 5, 100, 100, 60);
cxt.stroke();
複製程式碼

提示:canvas只是提供了筆和紙,至於如何繪製各種效果,需要新手複習下數學幾何、物理學知識了。

路徑

它是canvas中非常重要的概念,除了矩形,其它所有基本圖形(包括直線、多邊形、圓形、弧線、貝塞爾曲線)都是以路徑為基礎。

方法 說明
beginPath() 開始一條新的路徑
closePath() 閉合當前路徑
isPointPath() 判斷某一個點是否存在於當前路徑

狀態

一條路徑裡,每一次繪製圖形(呼叫stroke()或fill())時,canvas會檢測整個程式定義的所有狀態,當一個狀態值沒有被改變,canvas就一直使用最初的值。

cxt.beginPath();
// 設定直線的寬度 10px,canvas中是畫素單位(px)
cxt.lineWidth = 10;
// 繪製一條直線,寬度是 10px。 記為 A
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.closePath();
cxt.stroke();

// 繪製一條直線,記為 B
cxt.moveTo(100, 110);
cxt.lineTo(200, 210);
cxt.closePath();
cxt.stroke();

// 繪製一條直線,寬度是 20px。記為 C
cxt.lineWidth = 20;
cxt.moveTo(10, 10);
cxt.lineTo(100,100);
cxt.closePath();
cxt.stroke();

// 繪製一條直線,記為 D
cxt.moveTo(50, 30);
cxt.lineTo(300,200);
cxt.closePath();
cxt.stroke();

// 重新開啟一條路徑,繪製一條直線,記為 E
cxt.beginPath();
cxt.moveTo(90, 90);
cxt.lineTo(00,400);
cxt.closePath();
cxt.stroke();
// 重新開啟一條路徑,繪製一條直線,記為 F
cxt.beginPath();
cxt.lineWidth = 50;
cxt.moveTo(90, 90);
cxt.lineTo(00,400);
cxt.closePath();
cxt.stroke();
複製程式碼

最終效果:線條A、B、C、D、E寬度為 20px,F寬度為50px。
分析:首先我們定義的狀態是線條寬度;

  1. 繪製線條E,我們重新開啟了一條新路徑,呼叫stroke方法開始繪製,這時候線條寬度並非預設值,而是呼叫該stroke方法前最後定義的 20px,因為canvas會檢測整個程式定義的線條寬度。
  2. 我們首先定義的線條寬度是10px, 但是後面又重新定義20px,覆蓋了之前的狀態。
  3. 同一條路徑中的直線A、B、C、D,雖然每次都呼叫stroke方法,但是最終生效的寬度是該路徑中最後一次呼叫stroke方法之前檢測到的狀態。
  4. 不同路徑裡設定的寬度不會覆蓋其它路徑裡的寬度值,所有路徑裡F線條雖然設定了50px,但是其它線條設定的寬度值並未受影響。

結論:狀態值改變時,分兩種情況考慮:

  1. 如果使用 beginPath()開啟一條新的路徑,則不同路徑使用不同的值。
  2. 如果在一條路徑內,則後面的值會覆蓋前面的值。 當然,lineWidth有點特殊,如果後面設定的值小於前面設定的值,最終效果還是顯示前面大的值。

狀態操作-儲存和恢復

canvas上下文提供了 save 和 restore 這一對API,儲存狀態和在上下文中恢復儲存的狀態。
示例:

cxt.lineWidth = 5;
cxt.save();
cxt.beginPath();
// 設定直線的寬度 10px,canvas中是畫素單位(px)
cxt.lineWidth = 20;
// 繪製一條直線,寬度是 10px。 記為 A
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.closePath();
cxt.stroke();

// 繪製一條直線,記為 B
cxt.moveTo(100, 110);
cxt.lineTo(200, 210);
cxt.closePath();
cxt.restore();
cxt.stroke();

複製程式碼

最終效果:直線A的線寬為20px,直線B的線寬為5px。 分析:上面介紹過一個路徑裡,後面的狀態會覆蓋前面的狀態,上個示例中一條路徑裡,只能顯示最後定義的寬。
本次示例中,由於一開始呼叫 save方法,儲存了 lineWidth=5的狀態,繪製直線B時,還原了該狀態,最終呈現了不一樣的效果。
新手需要注意的是,save和restore一般是成對使用的。
canvas狀態的儲存和恢復,主要用於以下三種場合:

  1. 圖形或圖片剪下(clip());
  2. 圖形或圖片變形;
  3. 其它屬性改變的時候:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。

變形操作

我們已經探索了canvas的最基本概念和熟悉了canvas的基本操作流程,這次探索變形操作。

方法 說明
translate() 平移
scale() 縮放
rotate() 旋轉
transform(), setTransform() 變換矩陣

需要注意的是:translate、scale、rotate這三個方法,都是通過變換矩陣 transform 這個方法來實現的。
transfrom方法和setTransform方法功能類似,但兩者間有本質的區別:
每次呼叫transfrom方法,參考的都是上一次變換後的圖形狀態,然後再進行變換;
但是setTransform方法會重置圖形的狀態,然後再進行變換;

首先我們定義一個直角三角形

var cnv = document.getElementById('canvas');
var cxt = cnv.getContext("2d");

//直角三角形
cxt.beginPath();
cxt.lineWidth = 5;
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.lineTo(120, 180);
cxt.closePath();
複製程式碼

然後將這個三角形做變形操作:

  1. 沿著X軸直線移動100px,Y軸直線移動100px;
  2. 圖形在X軸方向縮小0.5倍,Y軸方向放大2倍;
  3. 圖形逆時針旋轉30°。
//... 省略部分是直角三角形程式碼
//變形操作
cxt.translate(50, 50);
cxt.scale(0.5, 2);
cxt.rotate(-30 * Math.PI/180);
複製程式碼

最後執行繪製

//... 省略部分是直角三角形和變形操作程式碼
cxt.stroke();
複製程式碼

變形操作後的影響:
當平移後,座標原點移動到(100,100);
當縮放後,直角三角形的左上角座標,X軸方向縮小一半,Y軸方向放大2倍,圖形高度變為原來的兩倍長,圖形寬度縮小一半,Y軸方向(底)線條高度放大2倍,X軸方向(左右)線條縮小一半;
執行旋轉後,圖形縮放效果改變,同時按照新的原點座標逆時針旋轉30°。

再次繪製一個矩形圖形

//... 省略部分是直角三角形和變形操作程式碼
cxt.beginPath();
cxt.rect(10,10, 100, 100);
cxt.closePath();
cxt.stroke();
複製程式碼

最終效果:矩形也受上面的變形影響。根據之前探索的狀態操作,我們可以讓矩形正常顯示,不受變形影響。

const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");
cxt.save();
//... 省略部分是直角三角形和變形操作程式碼
cxt.restore();
cxt.beginPath();
cxt.rect(10,10, 100, 100);
cxt.closePath();
cxt.stroke();
複製程式碼

最終效果:矩形正常顯示,直角三角形依然是變形後效果。
新手注意:如果變形操作後發現接下來的圖形跟設想的不一致,可以考慮下save() 和restore()方法了。

總結下:
save方法儲存的狀態:變形狀態(變換矩陣)、繪圖狀態、剪下狀態(clip());
save方法不能儲存路徑狀態,想要一個新的路徑,只能呼叫beginPath();
save方法只能儲存狀態,不能儲存圖形,恢復圖形只能通過清除畫布重繪;

變形實踐:圖形旋轉動畫

思路:

  1. 首先圖形以canvas的座標中心為旋轉中心,通過translate方法將座標原點移動到canvas的中心;
  2. 呼叫rotate方法,旋轉指定角度;
  3. 繪製一個矩形,將矩形中心座標設定為座標原點,即矩形左上角座標設定為(-width/2, -height/2);
  4. 呼叫時間間隔函式比如 requestAnimationFrame,修改旋轉角度,不斷清除畫布重繪,實現圖形旋轉動畫。
const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");
const rectWidth = 100;
const rectHeight = 100;

let i = 0;
function rotate() {
  // 角度累加
  i++;
  // 清除畫布
  cxt.clearRect(0,0, cnv.width, cnv.height);
  // 儲存當前狀態
  cxt.save();
  // 座標原點移動到畫布中心點的100px處
  cxt.translate(rectWidth/2+100, rectHeight/2+100);
  // 指定旋轉角度
  cxt.rotate(Math.PI*(i/10));
  // 繪製填充一個藍色的矩形
  cxt.fillStyle="blue";
  cxt.fillRect(-rectWidth/2, -rectHeight/2, rectWidth, rectHeight);
  // 還原狀態,為什麼請檢視上節介紹
  cxt.restore();
  // 執行動畫效果
  requestAnimationFrame(rotate);
  if (i > 360) {
    i = 0;
  }
}
requestAnimationFrame(rotate);
複製程式碼

圓形和漸變

繪製圓形
API:
cxt.arc(圓心座標x, 圓心座標y, 半徑, 開始角度, 結束角度, anticlockwise), anticlockwise為boolen值,true時,表示按逆時針方向繪製,false相反。
例子:繪製一個圓

const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");

function drawBall(x, y, radius, style) {
  cxt.save();
  cxt.beginPath();
  // 圓的開始角度是 0°,結束角度是 360°
  cxt.arc(x, y, radius, 0, 360 * Math.PI/180, false);
  cxt.closePath();
  cxt.fillStyle = style || 'red';
  cxt.fill();
  cxt.restore();
}
// 繪製一個圓,圓心座標(250,250),半徑50px, 填充樣式為 紅顏色
drawBall(250, 250, 50, 'red');

複製程式碼

漸變

  1. 線性漸變 線性漸變,指的是一條直線上進行的漸變。
    API例子示例
let gnt = cxt.createLinearGradient(x1, y1, x2, y2);
gnt.addColorStop(value1, color1);
gnt.addColorStop(value2, color2);
cxt.fillStyle = gnt;
cxt.fill();
複製程式碼

解釋: 想要實現線性漸變,需要以下三個步驟: 1). 呼叫 createLinearGradient 方法建立一個 linearGradient 物件,並賦值給變數gnt;
2). 呼叫 linearGradient 物件(即gnt)的 addColorStop 方法多次,第一次表示漸變開始的顏色,第二次表示漸變結束時的顏色。第三次則以第二次漸變顏色作為開始顏色,進行漸變,以此類推;
3). 把 linearGradient 物件(即gnt)賦值給 fillStyle屬性,並且呼叫fill()方法繪製有漸變色的圖形;
引數說明:
x1、y1表示漸變色開始點的座標,x2、y2表示漸變色結束點的座標,表示繪製從點(x1, y1) 到點(x2, y2) 的線性漸變。
addColorStop 引數:
value表示漸變位置的偏移量,取值為 0~1之間的任意值,color表示漸變顏色,取值為任意顏色值。

開始點和結束點座標之間有以下三種關係:
1). 如果y1和y2相同,表示沿著水平方向從左到右漸變,記作 X1->X2;
2). 如果x1和x2相同,表示沿著垂直方向從左到右漸變,記作 y1->y2;
3). 如果x1和x2不同,y1和y2不同,表示漸變色沿著矩形對角線方向漸變,記作 (x1->x2, y1->y2);

程式碼例子:

橫向的線性漸變, X1->X2

// X軸方向 y1 =y2
cxt.save();
var gnt1 = cxt.createLinearGradient(0, 150, 200, 150);
gnt1.addColorStop(0, 'HotPink');
gnt1.addColorStop(1, 'white');
cxt.fillStyle = gnt1;
cxt.fillRect(0,0, 200,200);
cxt.restore();
複製程式碼

縱向的線性漸變, y1->y2

// Y軸方向 x1 = x2
cxt.save();
var gnt2 = cxt.createLinearGradient(0, 300, 0, 450);
gnt1.addColorStop(0, 'HotPink');
gnt1.addColorStop(1, 'white');
cxt.fillStyle = gnt2;
cxt.fillRect(0,300, 200,200);
cxt.restore();
複製程式碼

對角線的線性漸變,(x1->x2, y1->y2)

cxt.save();
// 矩形對角線方向 x1 != x2, y1 != y2
var gnt3 = cxt.createLinearGradient(300,300, 500, 500);
gnt3.addColorStop(0, 'HotPink');
gnt3.addColorStop(1, 'white');
cxt.fillStyle = gnt3;
cxt.fillRect(300,300, 200,200);
cxt.restore();
複製程式碼
  1. 徑向漸變 徑向漸變,是一種從起點到終點、顏色從內到外進行的圓形漸變(從中間向外拉,像圓一樣)。徑向漸變是圓形漸變或橢圓形漸變,顏色不再沿著一條直線漸變,而是從一個起點向所有方向漸變。 API例子示例
let gnt = cxt.createRadialGradient(x1, y1, r1, x2, y2, r2);
gnt.addColorStop(value1, color1);
gnt.addColorStop(value2, color2);
cxt.fillStyle = gnt;
cxt.fill();
複製程式碼

解釋: 想要實現徑向漸變,需要以下三個步驟: 1). 呼叫 createRadialGradient 方法建立一個 radialGradient 物件,並賦值給變數gnt;
2). 呼叫 radialGradient 物件(即gnt)的 addColorStop 方法多次,第一次表示漸變開始的顏色,第二次表示漸變結束時的顏色。第三次則以第二次漸變顏色作為開始顏色,進行漸變,以此類推;
3). 把 radialGradient 物件(即gnt)賦值給 fillStyle屬性,並且呼叫fill()方法繪製有漸變色的圖形; 引數說明:
(x1, y1)表示漸變開始圓心的座標,r1表示漸變開始圓的半徑。
(x2, y2)表示漸變結束圓心的座標,r2表示漸變結束圓的半徑。
呼叫 createRadialGradient 方法,會從漸變開始的圓心位置(x1, y1)向漸變結束的圓心位置(x2, y2)進行顏色漸變。起點為開始圓心,終點為結束圓心,由起點向終點擴散,直至終點外邊框。 addColorStop 引數:
value表示漸變位置的偏移量,取值為 0~1之間的任意值,color表示漸變顏色,取值為任意顏色值。

程式碼例子:

// 圓形
cxt.save();
cxt.beginPath();
cxt.arc(80,80,50,0, 360 *Math.PI/180, false);
cxt.closePath();
//徑向漸變
var gnt4 = cxt.createRadialGradient(100, 60, 10, 80, 80, 50);
gnt4.addColorStop(0,'white');
gnt4.addColorStop(0.9, 'orange');
gnt4.addColorStop(1, 'rgba(0,0,0,0)');   
cxt.fillStyle = gnt4;
cxt.fill();
cxt.restore();
複製程式碼

最終效果:一個雞蛋黃

圓形、漸變、變形實踐:圓球繞橢圓做圓周運動

思路:

  1. 首先繪製一個圓,給這個圓新增漸變效果,看起來立體感,具體參考上面的程式碼;
  2. 繪製一個橢圓軌跡,首先分別計算橢圓的X軸半徑、Y軸半徑的比率,呼叫 scale(ratioX, ratioY),進行變形操作,最後按照按照比率確定圓的中心座標,繪製一個圓;
  3. 根據橢圓標準方程,結合三角函式,計算出橢圓上任意點的座標;
  4. 呼叫時間間隔函式比如 requestAnimationFrame,修改旋轉角度,不斷清除畫布重繪,實現圓球繞橢圓做圓周運動動畫。
const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");

// 橢圓
function ellipse(cxt, centerX, centerY, radiusX, radiusY) {
  cxt.save();
  let r = (radiusX > radiusY) ? radiusX: radiusY;
  let ratioX = radiusX /r;
  let ratioY = radiusY /r;
  cxt.scale(ratioX, ratioY);
  cxt.beginPath();
  cxt.arc( centerX / ratioX, centerY / ratioY, r, 0, 360 * Math.PI/180, false );
  cxt.closePath();
  cxt.stroke();
  cxt.restore();
}

// 帶漸變色的圓球
function drawBall(cxt,x,y,radius) {
  cxt.save();
  cxt.beginPath();
  cxt.arc(x,y,radius,0, 360*Math.PI/180, false);
  cxt.closePath(); 
  let gnt = cxt.createRadialGradient(x,y,10,x,y,50);
  gnt.addColorStop(0,'white');
  gnt.addColorStop(0.9, 'orange');
  gnt.addColorStop(1, 'rgba(0,0,0,0)');  
  cxt.fillStyle=gnt;
  cxt.fill();
  cxt.restore();
}
// 累加旋轉角度
let angle = 0;
function drawFrame() {
  requestAnimationFrame(drawFrame);
  cxt.clearRect(0,0, cnv.width, cnv.height);
  // 太陽
  drawBall(cxt, cnv.width/2, cnv.height/2, 30 );
  // 橢圓軌跡
  ellipse(cxt, cnv.width/2, cnv.height/2, 200, 100);
  // 橢圓標準方程
  let circlePointX1 = cnv.width/2 + Math.cos(angle) * 200;
  let circlePointY1 = cnv.height/2 + Math.sin(angle) * 100;
  //繞太陽執行的圓球
  drawBall(cxt,circlePointX1, circlePointY1, 30 );
  angle += 0.02;
  if(angle >= 360) {
    angle = 0;
  }
};
drawFrame();

複製程式碼

橢圓任意點的座標公式推導:
橢圓的標準方程: (x/a)² + (y/b)² = 1, 其中 a為橢圓x軸半徑,b為橢圓y軸半徑;
因為: (cosA)² + (sinA)² = 1
根據三角函式,得出
x/a = cosA,y/a = sinA
故:任意點座標為:
x = a * cosA
y = b * sinA
最終公式:

x = centerX + Math.cos(angle) * radiusX;
y = centerY + Math.sin(angle) * radiusY;
複製程式碼

新手探索canvas時,最好還是要複習下數學知識。

結束

至此,新手探索旅程本次結束了,這次探索中,接觸到了canvas元素的初始化,路徑、狀態的概念和操作,以及探索瞭如何操作多線條、矩形、變形、圓形、漸變,動手實踐了相關程式碼,介紹了注意事項和所需的數學知識,可以說canvas的基礎大部分基本都已經探索完畢。
下一次旅行我們將探索圖形畫素操作、陰影效果、文字操作、基本物理動畫實現、事件響應實現、以及探索下常用的邊界檢測和碰撞檢測。

@作者:白雲飄飄(534591395@qq.com)

@github: https://github.com/534591395 歡迎關注我的微信公眾號:

微信公眾號
或者微信公眾號搜尋 新夢想兔,關注我哦。

相關文章