JavaScript圖形例項:Canvas API
1.Canvas概述
Canvas API(畫布)用於在網頁實時生成影象,並且可以操作影象內容,基本上它是一個可以用JavaScript操作的點陣圖(bitmap)。
要使用HTML5在瀏覽器視窗中繪製圖形,首先需要在HTML檔案中新建一個canvas網頁元素。一般方法如下:
<canvas id="myCanvas" width="400" height="300">
您的瀏覽器不支援canvas!
</canvas>
上面這段程式碼,表示建立了一個名為“myCanvas”的canvas網頁元素,它就是一塊畫布,該畫布的寬為400,高為300。有了這塊畫布,我們就可以使用JavaScript編寫程式,利用Canvas API在這塊畫布上繪製圖形。如果所用瀏覽器不支援Canvas API,則就會顯示canvas標籤中間的文字——“您的瀏覽器不支援canvas!”。
每個canvas網頁元素都有一個對應的context物件(上下文物件),Canvas API定義在這個context物件上面。為了在canvas上繪製圖形,必須先得到一個畫布上下文物件的引用。為此,使用JavaScript編寫程式段如下:
var canvas = document.getElementById('myCanvas'); // 取得網頁中的畫布物件
var ctx = canvas.getContext('2d'); // 得到畫布上下文物件ctx
上面程式碼中,getContext方法指定引數2d,表示該canvas物件用於生成2D圖案(即平面圖案)。如果引數是3d,就表示用於生成3D影象(即立體圖案)。
當使用一個canvas元素的getContext(“2d”)方法時,返回的是CanvasRenderingContext2D物件,其內部表現為笛卡爾平面座標。這就是Canvas畫布提供的一個用來作圖的平面空間,該空間的每個點都有自己的座標,x表示橫座標,y表示縱座標。原點(0, 0)位於畫布左上角,x軸的正向是原點向右,y軸的正向是原點向下。
每一個canvas元素僅有一個上下文物件。得到了這個上下文物件,就可以利用這個物件的屬性和方法進行圖形繪製了。
2.繪圖方法
2.1 繪製路徑
在Canvas API中,上下文CanvasRenderingContext2D物件提供了一系列與圖形繪製相關的屬性和方法。其中,與路徑繪製相關的方法如下:
void beginPath(); // 開始繪製路徑
void closePath(); // 結束路徑繪製
void moveTo(in float x, in float y); // 設定線段的起點
void lineTo(in float x, in float y); // 設定線段的終點
void bezierCurveTo(in float cp1x, in float cp1y, in float cp2x, in float cp2y, in float x, in float y); // 繪製一條三次貝塞爾曲線
void quadraticCurveTo(in float cpx, in float cpy, in float x, infloat y); // 繪製一條二次貝塞爾曲線
void stroke(); // 給透明的線段著色,從而完成線段繪製
void fill(); // 給閉合路徑填充顏色,填充色由fillStyle屬性指定
與繪製路徑相關的屬性有:
attribute float lineWidth; // 線的寬度,預設為1
attribute any strokeStyle; // 著色的顏色,預設為black(黑色)
attribute any fillStyle; // 填充顏色,預設為black(黑色)
attribute DOMString lineCap; // 線段的箭頭樣式,僅有三個選項:butt(預設值)、round、square,其他值忽略
下面通過幾個例子來說明繪製路徑的方法和屬性的使用。
例1 繪製一條從(20,20)到(200,20)的一條紅色橫線。
<!DOCTYPE html>
<head>
<title>繪圖方法的使用</title>
<script type="text/javascript">
function draw(id)
{
var canvas=document.getElementById(id);
if (canvas==null)
return false;
var ctx=canvas.getContext('2d');
ctx.beginPath(); // 開始路徑繪製
ctx.moveTo(20, 20); // 設定路徑起點,座標為(20,20)
ctx.lineTo(200, 20); // 繪製一條到(200,20)的直線
ctx.lineWidth = 1.0; // 設定線寬
ctx.strokeStyle = "#FF0000"; // 設定線條顏色為紅色
ctx.stroke(); // 進行線的著色,這時整條線才變得可見
}
</script>
</head>
<body onload="draw('myCanvas');">
<canvas id="myCanvas" width="400" height="300" style="border:3px double #996633;">
</canvas>
</body>
</html>
將上述HTML程式碼儲存到一個html文字檔案中,再在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以看到在畫布中繪製出一條長為180的紅色橫線。
下面的例子中我們不再給出完整的HTML檔案內容,只給出JavaScript編寫的與圖形繪製直接相關的程式碼。例如例1原始檔中加了註釋的6條語句。讀者需要自己試一試時,只需把下列各例給出的程式碼去覆蓋例1中的6條註釋語句,其餘部分保持不變即可。
在繪製路徑時,moveto和lineto方法可以多次使用。最後,還可以使用closePath方法,自動繪製一條當前點到起點的線段,形成一個封閉圖形,省卻使用一次lineto方法。
例2 在畫布中繪製一個紅色邊框的直角三角形和一個藍色邊框的等腰三角形。
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(20,100); // 垂直直角邊
ctx.lineTo(70,100); // 水平直角邊
ctx.lineTo(20,20); // 斜邊
ctx.strokeStyle="red";
ctx.stroke(); // 進行著色,使得線段可見
ctx.beginPath();
ctx.moveTo(40,120);
ctx.lineTo(20,180); // 左邊的腰
ctx.lineTo(60,180); // 底邊
ctx.closePath(); // 右邊的腰是通過自動封閉繪製得到
ctx.strokeStyle="blue";
ctx.stroke();
例3 線段箭頭的三種樣式的比較。
ctx.lineWidth=10;
ctx.strokeStyle="red";
ctx.beginPath();
ctx.lineCap='butt';
ctx.moveTo(100,50);
ctx.lineTo(250,50);
ctx.stroke();
ctx.beginPath();
ctx.lineCap='round';
ctx.moveTo(100,80);
ctx.lineTo(250,80);
ctx.stroke();
ctx.beginPath();
ctx.lineCap='square';
ctx.moveTo(100,110);
ctx.lineTo(250,110);
ctx.stroke();
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖1所示的三條線段。其中,第1根線段箭頭樣式為“butt”(為預設值),線段的頭和尾都是長方形,也就是不做任何的處理;第2根線段箭頭樣式為“round”,線段的頭和尾都增加一個半圓形的箭頭;第3根線段的樣式為“square”,線段的頭和尾都增加一個長方形,長度為線寬一半,高度為線寬。
圖1 繪製的3根紅色線段
例4 繪製紅綠藍3個實心三角形。
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(20,100);
ctx.lineTo(70,100);
ctx.lineTo(20,20);
ctx.fillStyle="red";
ctx.fill(); // 填充紅色三角形
ctx.beginPath();
ctx.moveTo(40,120);
ctx.lineTo(20,180);
ctx.lineTo(60,180);
ctx.closePath();
ctx.fillStyle="green";
ctx.fill(); // 填充綠色三角形
ctx.beginPath();
ctx.moveTo(70,20);
ctx.lineTo(120,180);
ctx.lineTo(140,150);
ctx.fillStyle="blue";
ctx.fill(); // 填充藍色三角形
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖2所示的三個實心三角形。
圖2 三個實心三角形
通過上面的示例,我們可以知道,填充的形狀應該是封閉的路徑。如果路徑未關閉,那麼 fill() 方法會從路徑結束點到開始點之間新增一條線,以關閉該路徑,然後填充該路徑。例如,圖2中藍色三角形構成的路徑並未關閉,呼叫fill()時,會自動新增直線關閉。
貝賽爾曲線(Bezier curve)是計算機圖形學中相當重要的引數曲線。Canvas API中提供了兩個繪製貝塞爾曲線的方法。其中:
quadraticCurveTo() 方法用於繪製一條二次貝塞爾曲線。
二次貝塞爾曲線需要兩個點。第一個點(cpx,cpy)是用於二次貝塞爾計算中的控制點,第二個點(x,y)是曲線的結束點。曲線的開始點是當前路徑中最後一個點。如果路徑不存在,需要使用 beginPath() 和 moveTo() 方法來定義開始點。
bezierCurveTo() 方法用於繪製一條三次貝塞爾曲線。
三次貝塞爾曲線需要三個點。前兩個點(cp1x,cp1y)和(cp2x,cp2y)是用於三次貝塞爾計算中的控制點,第三個點(x,y)是曲線的結束點。曲線的開始點是當前路徑中最後一個點。如果路徑不存在,同樣需要使用 beginPath() 和 moveTo() 方法來定義開始點。
例5 繪製一條二次貝塞爾曲線和一條三次貝塞爾曲線。
ctx.beginPath();
ctx.moveTo(20,20);
ctx.quadraticCurveTo(20,100,200,20);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(20,120);
ctx.bezierCurveTo(20,220,200,180,200,120);
ctx.stroke();
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖3所示的兩條貝塞爾曲線。
圖3 兩條貝塞爾曲線
例6 使用多個貝塞爾曲線來繪製一個對話氣泡。
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();
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖4所示的對話氣泡。
圖4 對話氣泡
例7 使用多個貝塞爾曲線來繪製一個紅心。
ctx.fillStyle="red";
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();
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖5所示的紅心圖案。
圖5 紅心
例8 通過迴圈繪製一個9行9列的棋盤。
ctx.strokeStyle="red";
ctx.lineWidth=3;
ctx.beginPath();
for (i=50;i<=450;i+=50)
{
ctx.moveTo(i,50);
ctx.lineTo(i,450);
ctx.moveTo(50,i);
ctx.lineTo(450,i);
}
ctx.stroke();
為顯示完整的棋盤,請將畫布的寬和高均設定為500。在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖6所示的棋盤。
圖6 棋盤
2.2 繪製矩形
在Canvas API中,上下文CanvasRenderingContext2D物件提供的與矩形繪製相關的方法如下:
void rect(in float x, in float y, in float w, in float h); // 建立一個矩形路徑
void clearRect(in float x, in float y, in float w, in float h); // 清除給定矩形區域的內容
void fillRect(in float x, in float y, in float w, in float h); // 填充給定的矩形區域
void strokeRect(in float x, in float y, in float w, in float h); // 繪製給定的矩形邊框
這個幾個方法中給定的四個引數分別為矩形左上角頂點的x座標、y座標,以及矩形的寬w和高h。
例9 繪製紅綠藍三個矩形邊框。
ctx.beginPath();
ctx.lineWidth="8";
ctx.strokeStyle="red";
ctx.rect(15,15,150,150);
ctx.stroke(); // 繪製紅色矩形
ctx.beginPath();
ctx.lineWidth="3";
ctx.strokeStyle="#00FF00";
ctx.strokeRect(30,30,40,50); // 繪製綠色矩形
ctx.beginPath();
ctx.lineWidth="8";
ctx.strokeStyle="blue";
ctx.moveTo(80,50);
ctx.lineTo(80,120);
ctx.lineTo(140,120);
ctx.lineTo(140,50);
ctx.closePath();
ctx.stroke(); // // 繪製藍色矩形
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖7所示的三個矩形。
圖7 三個矩形邊框
例10 繪製一個邊長為100的正方形,邊框採用藍色,內部用紅色填充。
ctx.fillStyle="red";
ctx.strokeStyle="blue";
ctx.lineWidth=2;
ctx.fillRect(50,50,100,100);
ctx.strokeRect(50,50,100,100);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖8所示的正方形。
圖8 正方形
在沒有進行座標旋轉的情況下,採用rect()和strokeRect()方法繪製的矩形一定是兩條邊與x軸平行,兩條邊與y軸平行。若要繪製與座標軸不平行的矩形,可以採用繪製4條線的方法完成。
例11 繪製矩形中的矩形。要求矩形裡的矩形其頂點在外面矩形的中點上。
var x = [50,50,250,250];
var y = [50,250,250,50];
ctx.strokeStyle="red";
ctx.lineWidth=3;
for (i=1;i<=4;i++)
{
ctx.beginPath();
ctx.moveTo(x[0],y[0]);
for (k=1;k<=3;k++)
ctx.lineTo(x[k],y[k]);
ctx.closePath();
ctx.stroke();
var tx=x[0];
var ty=y[0];
for (k=0;k<3;k++)
{
x[k]=(x[k]+x[k+1])/2;
y[k]=(y[k]+y[k+1])/2;
}
x[3]=(tx+x[3])/2;
y[3]=(ty+y[3])/2;
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖9所示的正方形。
圖9 正方形中的正方形
例12 繪製國際象棋棋盤。
for (i=0;i<8;i++)
{
for (j=0;j<8;j++)
{
if ((i+j)%2==0)
ctx.fillStyle = 'black';
else
ctx.fillStyle= 'white';
ctx.fillRect(j*50,i*50,50,50);
}
}
為顯示完整的棋盤,請將畫布的寬和高均設定為400。在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖10所示的國際象棋棋盤。
圖10 國際象棋棋盤
例13 根據給定資料繪製柱狀圖。
var data = [100, 50, 20, 30, 100];
var colors = [ "red","orange", "yellow","green", "blue"];
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width,canvas.height);
for(var i=0; i<data.length; i++)
{
var dp = data[i];
ctx.fillStyle = colors[i];
ctx.fillRect(25+i*50, 280-dp*2, 50, dp*2);
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖11所示的柱狀圖。
圖11 柱狀圖
2.3 繪製圓形和扇形
在Canvas API中,上下文CanvasRenderingContext2D物件提供的與圓形和弧等繪製相關的方法如下:
void arc(in float x, in float y, in float radius, in float startAngle, in float endAngle, in boolean anticlockwise);
void arcTo(in float x1, in float y1, in float x2, in float y2, in float radius);
其中,arc方法用來繪製扇形。引數x和y是圓心座標,radius是半徑,startAngle和endAngle則是扇形的起始角度和終止角度(以弧度表示),anticlockwise表示作圖時應該逆時針畫(true)還是順時針畫(false)。
arcTo() 方法用於在畫布上建立介於兩個切線之間的弧/曲線。繪製出子路徑最後一個點(x0,y0)和(x1,y1)以及(x1,y1)和(x2,y2)構成的兩條直線間半徑為radius的最短弧線,並用直線連線(x0,y0)
例14 繪製圓弧。
ctx.strokeStyle="red";
ctx.fillStyle="orange";
for(var i=0;i<3;i++)
{
for(var j=0;j<4;j++)
{
ctx.beginPath();
x = 50+j*100;
y = 50+i*100;
radius = 45;
startAngle = 0;
endAngle = Math.PI/2+(Math.PI*j)/2;
anticlockwise = i%2==0 ? false : true;
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
if (i>1)
ctx.fill();
else
ctx.stroke();
}
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖12所示的圓弧。
圖12 圓弧及填充
通過這個例子還可以加深理解:當呼叫fill()函式時,所有沒有閉合的形狀都會自動閉合,因此可以不呼叫closePath()函式。但是呼叫stroke()時不會自動閉合。
例15 繪製9個大小不一的圓。
ctx.fillStyle="red";
ctx.lineWidth=1;
for (var i=1;i<10;i++)
{
ctx.beginPath();
ctx.arc(i*20,i*20,i*10,0,Math.PI*2,true);
ctx.closePath();
ctx.fillStyle='rgba(255,0,0,0.25)';
ctx.fill();
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖13所示的圖形。
圖13 逐漸放大的圓
例16 根據給定資料繪製餅圖。
var data = [100, 50, 20, 30, 100];
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width,canvas.height);
var colors = [ "red","orange", "yellow","green", "blue"];
var total = 0;
for(var i=0; i<data.length; i++)
total += data[i];
var prevAngle = 0;
for(var i=0; i<data.length; i++)
{
var fraction = data[i]/total;
var angle = prevAngle + fraction*Math.PI*2;
ctx.fillStyle = colors[i];
ctx.beginPath();
ctx.moveTo(150,150);
ctx.arc(150,150, 100, prevAngle, angle, false);
ctx.lineTo(150,150);
ctx.fill();
ctx.strokeStyle = "black";
ctx.stroke();
prevAngle = angle;
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖14所示的餅圖。
圖14 餅圖
2.4 繪製文字
在Canvas API中,上下文CanvasRenderingContext2D物件提供的與文字繪製相關的方法如下:
void fillText(in DOMString text, in float x, in float y, optionalin float maxWidth);
void strokeText(in DOMString text, in float x, in float y, optionalin float maxWidth);
這兩個方法用來繪製文字,它的前三個引數分別為文字內容、起點的x座標、y座標。其中:fillText方法為繪製填充的文字;strokeText方法為對文字進行描邊,不填充內部區域,通常用來新增空心字。
與文字相關的屬性有:
attribute DOMString font; // 設定字型,預設為10px sans-serif
attribute DOMString textAlign; // 設定對齊方式,有"start", "end", "left", "right", "center"等,預設為"start"
attribute DOMString textBaseline; //設定文字對齊基線,有"top", "hanging", "middle", "alphabetic", "ideographic", "bottom" 等取值,預設為”alphabetic"
例17 在畫布上新增兩行文字。
ctx.font = "Bold 50px 隸書";
ctx.fillStyle = "Black";
ctx.fillText("我們是中國人", 10, 50);
ctx.strokeText("我們熱愛我們的祖國", 10, 150);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖15所示的文字。
圖15 文字繪製
注意:fillText方法不支援文字斷行,即所有文字出現在一行內。所以,如果要生成多行文字,只有呼叫多次fillText方法。
例18 繪製包含資料說明的柱狀圖。
var data = [100, 50, 20, 30, 100];
var colors = [ "red","orange", "yellow","green", "blue"];
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width,canvas.height);
for(var i=0; i<data.length; i++)
{
var dp = data[i];
ctx.fillStyle = colors[i];
ctx.fillRect(25+i*50, 280-dp*2, 50, dp*2);
}
ctx.fillStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(25,10);
ctx.lineTo(25,280);
ctx.lineTo(290,280);
ctx.stroke();
ctx.fillStyle = "black";
for(var i=0; i<6; i++)
{
ctx.fillText((5-i)*20 + "",4, i*40+80);
ctx.beginPath();
ctx.moveTo(25,i*40+80);
ctx.lineTo(30,i*40+80);
ctx.stroke();
}
var labels = ["JAN","FEB","MAR","APR","MAY"];
for(var i=0; i<5; i++)
ctx.fillText(labels[i], 40+ i*50, 290);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖16所示的包含資料說明的柱狀圖。
圖16 包含資料說明的柱狀圖
還可以為文字等設定陰影。
例19 為文字設定陰影。
ctx.shadowOffsetX = 3; // 設定水平位移
ctx.shadowOffsetY = 3; // 設定垂直位移
ctx.shadowBlur = 2; // 設定模糊度
ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; // 設定陰影顏色
ctx.font = "50px 宋體";
ctx.fillStyle = "Black";
ctx.fillText("我們是中國人", 10, 50);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖17所示的文字陰影效果。
圖17 文字陰影
2.5 圖形剪下
在Canvas API中,上下文CanvasRenderingContext2D物件提供了一個用於圖形剪下的方法。
void clip();
剪下(clip)路徑和普通的 canvas 圖形差不多,不同的是它的作用是遮罩,用來隱藏沒有遮罩的部分,如圖18所示。紅邊五角星就是裁切路徑,所有在路徑以外的部分都不會在 canvas 上繪製出來。預設情況下,canvas 有一個與它自身一樣大的剪下路徑(也就是沒有剪下效果)。
圖18 剪下示意圖
例20 一個簡單的剪下示例。
ctx.fillStyle = 'red';
ctx.fillRect(0,0,400,300);
ctx.beginPath();
ctx.moveTo(200,50);
ctx.lineTo(100,250);
ctx.lineTo(300,250);
ctx.closePath();
ctx.lineWidth = 10;
ctx.stroke();
ctx.clip();
ctx.fillStyle = 'yellow';
ctx.fillRect(0,0,400,150);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖19所示的三角形剪下。
圖19 三角形剪下
在這個例子中,先畫了一個與 canvas 一樣大小(寬400,高300)的紅色方形作為背景,然後用 clip方法建立一個三角形的剪下路徑。剪下路徑建立之後,所有出現在它裡面的東西才會畫出來。這樣在其後繪製高度為畫布一半的矩形填充時,只有三角形剪下路徑裡面的內容才會繪製出來。
設定了剪下區域之後,無論在Canvas上繪製什麼,只有落在剪下區域內的那部分才能得以顯示,其餘都會被遮蔽掉。
例21 cilp方法的進一步理解。
// 繪製第一個圓
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.arc(200, 100, 100, 0, Math.PI * 2, false);
ctx.fill();
// 繪製第二個圓
ctx.beginPath();
ctx.fillStyle = 'blue';
ctx.arc(100, 150, 100, 0, Math.PI * 2, false);
ctx.fill();
// 繪製第三個圓
ctx.beginPath();
ctx.fillStyle = 'green';
ctx.arc(300, 150, 100, 0, Math.PI * 2, false);
ctx.fill();
// 繪製第四個圓
ctx.beginPath();
ctx.fillStyle = 'brown';
ctx.arc(200, 200,100, 0, Math.PI * 2, false);
ctx.fill();
ctx.lineWidth = 10;
ctx.strokeStyle='black';
ctx.stroke();
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖20所示的4個圓。
圖20 沒有使用clip方法的4個圓
若在第3個圓繪製後插入一條語句“ctx.clip();”,則在畫布中繪製出如圖21所示的圖形。從圖21可以看出第4個圓(棕色的),只有落在第3個圓中的部分被繪製出來。
圖21 在第3個圓之後使用clip()方法
若clip()方法往上移,放到第2個圓的後面,則在畫布中繪製出如圖22所示的圖形。從圖22可以看出第3個圓(藍色的)完全沒有被繪製出來,因為第3個圓與第2個圓相切,沒有交集;第4個圓(棕色的)只有落在第2個圓中的部分被繪製出來。
圖22 在第2個圓之後使用clip()方法
若再把clip()方法往上移,放到第1個圓的後面,則在畫布中繪製出如圖23所示的圖形。從圖23可以看出,第1個圓為剪下區域,第2、3、4個圓只有落在第1個圓中的部分才被繪製出來。
圖23 在第1個圓之後使用clip()方法
當使用剪下函式clip()進行繪圖後,可能需要取消該剪下區域或者重新定義剪下區域。在Canvas中,可以通過save()函式和restore()函式來實現。在構建剪下區域之前儲存狀態,完成剪下區域內的繪圖之後進行狀態讀取。
例如,在例21的程式中,在第2個圓繪製後插入語句“ctx.save();”和“ctx.clip();”,在第3個圓繪製後插入語句“ctx.restore();”,則在畫布中繪製出如圖24所示的圖形。從圖24可以看出,第3個圓(藍色的)完全沒有被繪製出來,因為第3個圓與第2個圓相切,沒有交集;第4個圓(棕色的)全部被繪製出來,此時取消了剪下區域。
圖24 在第3個圓繪製後取消剪下區域
例22 採用clip實現簡單的探照燈效果。
<!DOCTYPE html>
<head>
<title>簡單探照燈</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var rot=10;
var canvas=document.getElementById('myCanvas');
var ctx=canvas.getContext('2d');
setInterval("draw()",100);
function draw()
{
ctx.clearRect(0,0,400,400);
ctx.save();
ctx.fillStyle="black";
ctx.fillRect(0,0,400,400);
ctx.beginPath();
ctx.arc(rot,200,40,0,Math.PI*2,true);
ctx.closePath();
ctx.fillStyle="white";
ctx.fill();
ctx.clip();
ctx.font="bold 45px 隸書";
ctx.textAlign="center";
ctx.textBaseline="middle";
ctx.fillStyle="#FF0000";
ctx.fillText("中國北京歡迎您!",200,200);
ctx.restore();
rot=rot+10;
if (rot>400) rot=10;
}
</script>
</body>
</html>
在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以看到在畫布中呈現出如圖25所示的簡單探照燈效果。
圖25 簡單的探照燈
3.圖形變換
在圖形學中,可以對圖形進行平移、縮放和旋轉等變換操作。
在Canvas API中,上下文CanvasRenderingContext2D物件提供的與圖形變換相關的方法如下:
void translate(in float x, in float y); // 平移Canvas的原點到指定的座標點(x,y)
void rotate(in float angle); // 按給定的弧度angle順時針旋轉
void scale(in float x, in float y); // 按給定的縮放倍率進行縮放
void setTransform(in float m11, in float m12, in float m21, infloat m22, in float dx, in float dy); // 將當前轉換重置為單位矩陣
void transform(in float m11, in float m12, in float m21, in floatm22, in float dx, in float dy); // 按矩陣進行變換
在進行圖形變換前先儲存上下文環境(狀態)是一個良好的習慣。大多數情況下,呼叫 restore()方法比手動恢復原先的狀態要簡單得多。例如,在一個迴圈中做平移操作但沒有儲存和恢復canvas 的狀態,很可能到最後會發現有些東西不見了,那是因為它很可能已經超出 canvas 範圍以外了。
3.1 save和restore方法
save方法用於儲存上下文環境,restore方法用於恢復到上一次儲存的上下文環境。
在Canvas中,每個上下文物件都包含一個繪圖狀態的堆,繪圖狀態包含下列內容:
(1)當前的變換矩陣;
(2)當前的剪下區域(clip);
(3)當前的屬性值:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、 lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、 strokeStyle、textAlign、textBaseline等。
例23 save方法和restore方法的簡單應用示例。
ctx.fillStyle = "red";
ctx.fillRect(10,10,80,80);
ctx.save();
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 5;
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.fillStyle = "blue";
ctx.fillRect(100,10,80,80);
ctx.restore();
ctx.fillRect(200,10,80,80);
ctx.fillStyle = "orange";
ctx.fillRect(300,10,80,80);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖26所示的4個矩形。
圖26 4個矩形
這4個矩形中,第1個矩形填充紅色,之後儲存狀態(填充色為紅色),第2個矩形是一個有黑色陰影的填充色為藍色的矩形;接著,使用restore方法,恢復了儲存前的設定,繪製了一個沒有陰影的填充色為紅色的第3個矩形,第4個矩形是一個填充色為橙色的矩形,也沒有陰影。
若去掉程式碼中的“ctx.restore();”語句,不恢復狀態,則繪製的4個矩形如圖27所示。體會圖27與圖26的區別。
圖27 不執行“ctx.restore();”繪製的4個矩形
3.2 translate、scale和rotate方法
translate() 方法實現座標平移,例如,進行ctx.translate(dx,dy);後,座標原點移到(dx,dy)處,這樣程式繪圖時給出的座標值(x,y),相對於canvas預設的座標原點(0,0),應該為(x+dx,y+dy)。
例24 translate簡單應用示例。
ctx.fillStyle = "red";
ctx.fillRect(10,10,80,80);
ctx.fillStyle = "blue";
ctx.translate(80,80);
ctx.fillRect(10,10,80,80);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖26所示的2個矩形。從圖中看出,在座標位置 (10,10) 處繪製一個紅色填充矩形後,將座標原點平移到(80,80),這樣再次繪製填充藍色的矩形從位置 (90,90) 處開始繪製。
圖28 平移後的藍色矩形
例25 連續座標平移的示例。
ctx.fillStyle = 'rgba(255,0,0,0.5)';
for (i = 0; i<5; i++)
{
ctx.translate(50,50);
ctx.fillRect(0,0,100,100);
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖29所示的5個矩形。
圖29 座標平移後的5個矩形
scale() 方法實現圖形的縮放。需要注意的是使用scale方法對繪圖進行縮放後,所有之後的繪圖也會被縮放,包括座標定位也會被縮放。例如,執行ctx.scale(2,2)後,繪圖將定位於距離畫布左上角兩倍遠的位置。
例26 scale簡單應用示例。
ctx.fillStyle = "red";
ctx.fillRect(10,10,80,80);
ctx.save();
ctx.fillStyle = "blue";
ctx.scale(2,2);
ctx.fillRect(50,10,80,80);
ctx.restore();
ctx.fillStyle = "green";
ctx.scale(0.5,0.5);
ctx.fillRect(10,200,80,80);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖30所示的3個矩形。從圖中看出,在座標位置 (10,10) 處繪製一個紅色填充矩形後,ctx.scale(2,2);將繪圖放大兩倍,這樣繪製填充藍色的矩形從位置 (100,20) 處開始繪製,其寬和高均是紅色填充矩形的2倍;恢復上下文環境後,ctx.scale(0.5,0.5);將繪圖縮小1倍,這樣繪製填充綠色的矩形從位置 (5,100) 處開始繪製,其寬和高均是紅色填充矩形的一半。
圖30 矩形的縮放
若去掉程式碼中的“ctx.restore();”語句,不恢復狀態,則繪製的3個矩形如圖31所示。體會圖31與圖30的區別。綠色矩形的大小之所以與紅色矩形一樣,是因為一個東西放大2倍後再縮小1倍,正好恢復原樣。
通過這個示例一定得明白,圖形變換的設定一定是在前一個狀態的基礎上進行的。因此在進行圖形變換時,根據需要通過save()方法儲存狀態,restore()方法恢復狀態是非常重要的。
圖31 不執行“ctx.restore();”繪製的3個矩形
例27 連續圖形放大的示例。
ctx.fillStyle = 'rgba(255,0,0,0.5)';
for (i = 0; i<4; i++)
{
ctx.scale(2,2);
ctx.fillRect(5,5,10,10);
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖32所示的4個矩形。
圖32 依次放大後的4個矩形
rotate()方法實現圖形的旋轉,其中引數給出的旋轉角度angle以弧度計。如需將角度degress轉換為弧度,可以使用 degrees*Math.PI/180 公式進行計算。
例28 將矩形旋轉45°。
ctx.fillStyle = "red";
ctx.fillRect(150,50,80,80);
ctx.fillStyle = "blue";
ctx.rotate(45*Math.PI/180);
ctx.fillRect(150,50,80,80);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖33所示的2個矩形,其中藍色矩形順時針旋轉了45°。
圖33 順時針旋轉了45°的藍色矩形
例29 連續旋轉的矩形。
ctx.translate(200,200);
ctx.fillStyle = 'rgba(255,0,0,0.5)';
for (i = 0; i<6; i++)
{
ctx.rotate(Math.PI/3);
ctx.fillRect(0,0,100,50);
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖34所示的6個矩形。
圖34 旋轉的矩形
3.3 transform 和setTransform方法
transform() 方法是通過變換矩陣實現變換。例如,語句ctx.transform(a,b,c,d,e,f);中包含6個引數,其中:
a 水平縮放繪圖
b 水平傾斜繪圖
c 垂直傾斜繪圖
d 垂直縮放繪圖
e 水平移動繪圖
f 垂直移動繪圖
通過transform() 方法可以縮放、旋轉、移動並傾斜當前的繪圖環境。
例如,ctx.translate(dx,dy)可以用
context.transform(0,1,1,0,dx,dy);
或 context.transform(1,0,0,1,dx,dy); 來替代。
又例如, ctx.transform(0.95,0,0,0.95,30,30);
可以替代 ctx.translate(30,30);
ctx.scale(0.95.0.95); 同時縮放和平移。
例30 transform() 方法簡單應用示例。
ctx.fillStyle="red";
ctx.fillRect(0,0,150,50)
ctx.transform(1,0.5,0,1,30,10);
ctx.fillStyle="blue";
ctx.fillRect(0,0,150,50);
ctx.transform(1,0.5,-0.5,1,30,10);
ctx.fillStyle="green";
ctx.fillRect(0,0,150,50);
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖35所示的3個矩形。由圖可以看出,藍色矩形水平方向傾斜了,綠色矩形在水平方向和垂直方向上都傾斜了。
圖35 應用transform() 方法後繪製的矩形
setTransform()方法也是通過變換矩陣實現變換,它與transform()的使用方法類同,區別在於:每次呼叫 setTransform() 時,都會重置前一個變換矩陣然後構建新的矩陣;二每次呼叫 transform() 時,它都會在前一個變換矩陣基礎上構建變換矩陣。
例如,將例30中的第2個transform 方法“ctx.transform(1,0.5,-0.5,1,30,10);”改寫為“ctx.setTransform(1,0.5,-0.5,1,30,10);”,則在瀏覽器視窗中繪製出如圖36所示的圖形。由圖可知,藍色矩形基本被綠色矩形覆蓋,只露出一個小角。這是因為繪製藍色矩形之前,使用的是setTransform() 方法,會重置變換矩陣,而不是在繪製藍色矩形的變換矩陣基礎上進行,因此藍色矩形與綠色矩形水平傾斜一致,水平與垂直方向平移也一致,只是綠色矩形多進行了垂直傾斜,因此將藍色矩形露出一小角。
圖36 應用setTransform() 方法後繪製的矩形
例31 通過變換繪製螺旋。
ctx.translate(200,50);
for(var i=0; i<50; i++)
{
ctx.save();
ctx.transform(0.95,0,0,0.95,30,30);
ctx.rotate(Math.PI/12);
ctx.beginPath();
ctx.fillStyle = 'rgba(255,0,0,0.5)';
ctx.arc(0,0,50,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
}
在瀏覽器中開啟包含這段JavaScript程式碼的html檔案,可以看到在畫布中繪製出如圖37所示的螺旋圖形。
圖37 螺旋圖
4.影象處理
canvas更有意思的一項特性就是影象操作能力。可以用於動態的影象合成或者作為圖形的背景,以及遊戲介面等等。瀏覽器支援的任意格式的外部圖片都可以使用,比如PNG、GIF或者JPEG。
4.1 影象繪製
drawImage() 方法可以向畫布上繪製影象、畫布或視訊。
要在畫布上繪製圖片,可以使用drawImage方法。該方法有三種不同的呼叫形式:
(1)ctx.drawImage(img,x,y); // 在畫布上定點陣影象
(2)ctx.drawImage(img,x,y,width,height);
// 在畫布上定點陣影象,並規定影象的寬度和高度
(3)ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
// 剪下影象,並在畫布上定位被剪下的部分。
其中,各引數的說明如下:
img 規定要使用的影象、畫布或視訊。
sx 可選。開始剪下的 x 座標位置。
sy 可選。開始剪下的 y 座標位置。
swidth 可選。被剪下影象的寬度。
sheight 可選。被剪下影象的高度。
x 在畫布上放置影象的 x 座標位置。
y 在畫布上放置影象的 y 座標位置。
width 可選。要使用的影象的寬度。(伸展或縮小影象)
height 可選。要使用的影象的高度。(伸展或縮小影象)
在使用drawImage()方法將影象繪製到畫布上之前,要獲得需要繪製的影象。canvas的API可以使用影象源的型別有:
(1)HTMLImageElement。
這些圖片或者由Image()函式構造出來的,或者頁面中的某一個<img>元素。
(2)HTMLVideoElement。
用一個HTML的 <video>元素作為圖片源,可以從視訊中抓取當前幀作為一個影象。
(3)HTMLCanvasElement。
可以使用另一個 <canvas> 元素作為圖片源。這種型別的一個常用的應用就是將第二個canvas作為另一個大的 canvas 的縮圖。
獲取到需要在canvas上繪製的圖片的方式有以下幾種:
(1)使用相同頁面內的圖片或其它 canvas 元素。
通過document.images 集合、document.getElementsByTagName 方法或者 document.getElementById 方法來獲取頁面內的圖片(如果已知圖片元素的 ID)。
使用其它 canvas 元素和引用頁面內的圖片類似地,用 document.getElementsByTagName 或 document.getElementById 方法來獲取其它 canvas 元素。
例32 在畫布中繪製頁面上的圖片。
<!DOCTYPE html>
<head>
<title>在畫布中繪製頁面上的圖片</title>
<script type="text/javascript">
function draw(id)
{
var canvas=document.getElementById(id);
if (canvas==null)
return false;
var ctx=canvas.getContext('2d');
var img=document.getElementById("myImg");
ctx.drawImage(img,10,10);
}
</script>
</head>
<body onload="draw('myCanvas');">
<img src="aaa.jpg" id="myImg">
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
</body>
</html>
在瀏覽器中開啟儲存這段HTML程式碼的html檔案,可以看到在畫布中出現如圖38所示的頁面。頁面中,左邊的圖片是頁面中的img元素,右邊是在畫布中繪製的左邊的圖片。
圖38 圖片繪製(一)
若將上面程式中的“ctx.drawImage(img,10,10);”改寫為“ctx.drawImage(img,50,50,200,200,20,20,250,150);”,則在瀏覽器中顯示如圖39所示的內容。右邊canvas中繪製的圖片是左邊原圖的部分剪下,並進行了水平拉伸。
圖39 圖片繪製(二)
(2)建立影象。
用指令碼建立一個新的 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'; // 設定圖片源地址
例33 編寫如下的HTML檔案內容。
<!DOCTYPE html>
<head>
<title>建立影象物件使用圖片進行繪製</title>
<script type="text/javascript">
function draw(id)
{
var canvas=document.getElementById(id);
if (canvas==null)
return false;
var ctx=canvas.getContext('2d');
var image = new Image();
image.src = 'aaa.jpg';
image.onload=function(){
ctx.drawImage(image,10,10);
}
}
</script>
</head>
<body onload="draw('myCanvas');">
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
</body>
</html>
在瀏覽器中開啟儲存這段HTML程式碼的html檔案,可以看到在畫布中出現如圖40所示的頁面。
圖40 建立影象物件使用圖片進行繪製
(3)通過 data: url 方式嵌入影象。
Data urls 允許用一串 Base64 編碼的字串的方式來定義一個圖片。其優點就是圖片內容即時可用,無須再到伺服器兜一圈。缺點就是影象沒法快取,圖片大的話內嵌的 url 資料會相當的長。
4.2 畫素級操作
在Canvas API中,上下文CanvasRenderingContext2D物件提供了三個方法用於畫素級操作:createImageData, getImageData, 和putImageData。
(1)createImageData()方法。
createImageData() 方法建立新的空白ImageData 物件。新物件的預設畫素值 transparent black。
ImageData物件儲存了影象畫素值。每個物件有三個屬性:width,height和data。data 屬性型別為CanvasPixelArray,用於儲存width*height*4個畫素值。每一個畫素有RGB值和透明度alpha值(其值為 0 至255,包括alpha在內)。畫素的順序從左至右,從上到下,按行儲存。其中:
R - 紅色 (0-255)
G - 綠色 (0-255)
B - 藍色 (0-255)
A - alpha 通道 (0-255;0 是透明的,255 是完全可見的)
transparent black 表示 (0,0,0,0)。
畫素值以陣列形式存在,由於每個畫素有4條資訊,因此陣列的大小是 ImageData 物件的4倍。可以使用 ImageDataObject.data.length獲得陣列的大小。
createImageData() 方法有兩種呼叫形式:
1) var imgData=ctx.createImageData(width,height);
以指定的尺寸(以畫素計)建立新的 ImageData 物件,其中引數width給定ImageData 物件的寬度,height給定ImageData 物件的高度。
2)var imgData=ctx.createImageData(imageData);
建立與指定的另一個 ImageData 物件尺寸相同的新 ImageData 物件(不會複製影象資料)。
(2)putImageData()方法。
putImageData() 方法用於將影象資料(從指定的 ImageData 物件)放回畫布上。其呼叫格式為:
ctx.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
各引數的說明如下:
imgData 規定要放回畫布的 ImageData 物件。
x ImageData 物件左上角的 x 座標,以畫素計。
y ImageData 物件左上角的 y 座標,以畫素計。
dirtyX 可選。水平值(x),以畫素計,在畫布上放置影象的位置。
dirtyY 可選。垂直值(y),以畫素計,在畫布上放置影象的位置。
dirtyWidth 可選。在畫布上繪製影象所使用的寬度。
dirtyHeight 可選。在畫布上繪製影象所使用的高度。
例34 createImageData和putImageData方法的簡單應用。
var imgData=ctx.createImageData(100,100);
for (var i=0;i<imgData.data.length;i+=4)
{
imgData.data[i+0]=255;
imgData.data[i+1]=0;
imgData.data[i+2]=0;
imgData.data[i+3]=255;
}
ctx.putImageData(imgData,10,10);
上述程式碼執行後,會在畫布中繪製一個紅色矩形,矩形的左上角座標為(10,10),寬度和高度均為100。
(3)getImageData()方法。
getImageData方法可以用來讀取(複製)Canvas的內容,返回一個ImageData物件,包含了每個畫素的資訊。其呼叫形式為:
var imgData=ctx.getImageData(x,y,width,height);
各引數描述如下:
x 開始複製的左上角位置的 x 座標。
y 開始複製的左上角位置的 y 座標。
width 將要複製的矩形區域的寬度。
height 將要複製的矩形區域的高度。
imgData物件有一個data屬性,它的值是一個一維陣列。該陣列的值依次是每個畫素的紅、綠、藍、alpha通道值,每個值的範圍是0~255。可以通過操作這個陣列的值達到操作影象的目的。修改這個陣列以後,使用putImageData方法將陣列內容重新繪製在Canvas上。
例35 通過操作畫素陣列的值來操作影象。
<!DOCTYPE html>
<head>
<title>通過操作畫素陣列的值來操作影象</title>
<script type="text/javascript">
function draw(id1,id2)
{
var canvas1=document.getElementById(id1);
var ctx1=canvas1.getContext('2d');
var canvas2=document.getElementById(id2);
var ctx2=canvas2.getContext('2d');
var imgData1=ctx1.createImageData(100,100);
for (i=0;i<imgData1.data.length;i+=4)
{
imgData1.data[i+0]=255;
imgData1.data[i+1]=0;
imgData1.data[i+2]=0;
imgData1.data[i+3]=255;
}
ctx1.putImageData(imgData1,10,10);
var imgData2=ctx1.getImageData(10,10,100,100);
for (i=imgData2.data.length/4;i<imgData2.data.length*3/4;i+=4)
{
imgData2.data[i+0]=0;
imgData2.data[i+2]=255;
}
ctx2.putImageData(imgData2,10,10);
}
</script>
</head>
<body onload="draw('myCanvas1','myCanvas2');">
<canvas id="myCanvas1" width="200" height="200" style="border:3px double #996633;">
</canvas>
<canvas id="myCanvas2" width="200" height="200" style="border:3px double #996633;">
</canvas>
</body>
</html>
在瀏覽器中開啟儲存這段HTML程式碼的html檔案,可以看到在瀏覽器視窗中顯示如圖41所示的內容。
圖41 通過操作畫素陣列的值來操作影象的示例
關於更多的Canvas API的知識,可以參閱檔案“HTML 5 Canvas 參考手冊”(https://www.w3school.com.cn/tags/html_ref_canvas.asp)。