HTML 圖學

HTML <canvas> 元件用於在網頁上,使用 JavaScript 動態繪製圖形。

<canvas> 標籤只是圖形的容器,
可以使用多種方法在Canvas 上繪製路徑、盒、圓、字符以及添加圖像。

下圖是用 <canvas> 創建的:

畫布的創建

一個畫布在網頁中是一個矩形框,通過 <canvas> 元件來繪製.

這是一個基本的空畫布示例:

<canvas id="emptyCanvas" width="200" height="100"></canvas>

注意:預設情況下<canvas>元素沒有邊框和內容。

使用 style 屬性來添加邊框:

<canvas id="myCanvas" width="200" height="100"
  style="border:1px solid #000000;">
</canvas>

使用JavaScript 來繪製圖像

canvas 元件本身是沒有繪圖能力的。 所有的繪製工作必須在JavaScript 內部完成:

畫一條線

先使用 HTML DOM method getElementById(),找到畫布元件。
var c = document.getElementById("myCanvas");

其次,需要一個畫布的繪圖物件。

getContext() 是一個內置的 HTML 物件,具有用於繪製的屬性和方法:

var ctx = c.getContext("2d");

在 Canvas上畫線,我們將使用以下兩種方法:

繪製線條必須使用 stroke() 的方法。
ctx.moveTo(0, 0);
ctx.lineTo(200, 100);
ctx.stroke();

用 fillRect() 畫矩形

fillStyle 屬性可以是 CSS 顏色、漸變或圖案。 預設填充樣式為黑色。
fillRect(x,y,width,height) 方法在畫布上繪製一個填充了填充樣式的矩形:
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#FF0000";
ctx.fillRect(0,0,150,75);

用 arc( ) 畫圓形

使用 arc(x,y,r,s,e) 畫一個圓,
x,y 是圓心的座標,r 是半徑,s 和 e 是起點和終點的角度。
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.arc(95, 50, 40, 0, 2 * Math.PI);
ctx.stroke();
beginPath() 產生一個新路徑,產生後再使用繪圖指令來設定路徑。

用 arc( ) 畫扇形

arc()的第4和5的參數是指從幾度開始的角度,角度請參考右圖。
在下方的程式碼中從210度開始,到330度就可繪製出扇形。

注意第4和5的參數使用弧度。
弧度與角度間換算: radians = (Math.PI/180) * degrees

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.lineWidth = 1;
ctx.fillStyle = "#FF0000"
ctx.beginPath();
ctx.moveTo(95,50);
ctx.lineTo(95-Math.sin(0.1666*Math.PI),50+Math.cos(0.1666*Math.PI) );
ctx.arc(95,50,40,1.1666*Math.PI,(2-0.1666)*Math.PI);
ctx.lineTo(95,50);
ctx.stroke();
ctx.fill();

貝茲曲線(Bezier curve)

二次與三次貝茲曲線(Bezier curves)是另一種可用來構成複雜有機圖形的路徑。
quadraticCurveTo(cp1x, cp1y, x, y) (en-US)

從目前起始點畫一條二次貝茲曲線到x, y指定的終點,控制點由cp1x, cp1y指定。

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) (en-US)

從目前起始點畫一條三次貝茲曲線到x, y指定的終點,控制點由(cp1x, cp1y)和(cp2x, cp2y)指定。

會話框
本例用了數個二次貝茲曲線畫了一個會話框。
    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();
畫心形
這個範例用三次貝茲曲線畫了一個愛心;
    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 繪製文本,重要的屬性和方法如下:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "30px Arial";
ctx.fillText("Hello World", 10, 50);

Canvas - 梯度(Gradient)

可以在矩形, 圓形, 線條, 文本等等設定梯度, 各種形狀可以自己定義不同的顏色。

以下有兩種不同的方式來設置 Canvas 梯度:

一旦有了梯度物件,就必須添加兩個或更多色標(color stop)。

addColorStop()方法指定色標,參數使用坐標來描述,可以是0至1。

使用梯度時,設定 fillStyle 或 strokeStyle 的值為梯度,然後繪製形狀,如矩形,文本,或一條線。

線性梯度(LinearGradient)

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

// 創建梯度物件
var grd = ctx.createLinearGradient(0, 0, 200, 0);
grd.addColorStop(0, "red");
grd.addColorStop(1, "white");

// 用梯度填充
ctx.fillStyle = grd;
ctx.fillRect(10, 10, 150, 80);

徑向梯度(Radical Gradient)

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

// Create gradient
var grd = ctx.createRadialGradient(75, 50, 5, 90, 60, 100);
grd.addColorStop(0, "red");
grd.addColorStop(1, "white");

// Fill with gradient
ctx.fillStyle = grd;
ctx.fillRect(10, 10, 150, 80);

Canvas - 圖像

把圖像放置到畫布上, 使用 drawImage()方法:

drawImage()方法是一個多載 (overload)方法,最基本的型態:

drawImage(image,x,y)

從座標點(x, y)開始畫上image參數指定的來源影像(CanvasImageSource).
window.onload = function() {
  var canvas = document.getElementById("myCanvas");
  var ctx = canvas.getContext("2d");
  var img = document.getElementById("scream");
  ctx.drawImage(img, 10, 10);
};
例題:canvas image

圖像縮放

drawImage()的第二個型態增加了兩個新參數,讓我們在畫布上放置影像的同時並縮放影像。
drawImage(image, x, y, width, height)

當放置影像於畫布上時,會按照參數width(寬)、height(高)來縮放影像。
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';
}

圖像切割

drawImage()第三個型態接受9個參數,其中8個讓我們從原始影像中切出一部分影像、縮放並畫到畫布上。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

image參數是來源影像物件,
(sx, sy)代表在來源影像中以(sx, sy)座標點作為切割的起始點,
sWidth 和 sHeight 代表切割寬和高,
(dx, dy)代表放到畫布上的座標點,
dWidth和dHeight代表縮放影像至指定的寬和高.
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);
}

變形效果

畫布狀態儲存與復原

在使用變形效果等複雜繪圖方法之前,有兩個不可或缺的方法(method)必須要先了解一下:

save() : 儲存現階段畫布完整狀態。
restore() : 復原最近一次儲存的畫布狀態。

可以呼叫save()的次數不限,而每一次呼叫restore(),最近一次儲存的畫布狀態便會從堆疊中被取出,然後還原畫布到此畫布狀態。

範例

本例會畫一連串矩形圖案來說明畫布狀態堆疊是如何運作。
  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);   // 畫一復原前設定的矩形

移動畫布

第一個變形效果方法是translate()。
translate(x, y)
移動網格上的畫布,其中x代表水平距離、y代表垂直距離。

範例

下面程式碼示範了利用 translate() 畫圖的好處。用了drawSpirograph() 函數畫萬花筒類的圖案。
如果沒有移動畫布原點,那麼每個圖案只會有四分之一會落在可視範圍。藉由移動畫布原點我們便可以自由變換每個圖案的位置, 使圖案完整出現,而且省去手動計算調整每個圖案的座標位置。

另外一個draw()函數透過兩個 for 迴圈移動畫布原點、呼叫drawSpirograph()函數、復歸畫布圓點位置共九次。

例題:spirograph

旋轉

本範例呼叫rotate()方法來畫一系列環狀圖案。

我們執行了兩個迴圈來作圖,第一個迴圈決定的圓環個數和該圓環上圓環上圓點的個數的顏色。
第二個迴圈決定了圓環上圓點的個數,每一次作圖前我們都儲存了原始畫布狀態,以便結束時可以復原狀態。

畫布旋轉的弧度則以圓環上圓點的個數決定,像是最內圈的圓環共有六個圓點,所以每畫一個原點,畫布就旋轉60度(360度/6)。
第二的圓環有12個原點,所以畫布一次旋轉度數為30度(360度/12),以此類推。

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

  for (var i=1;i<6;i++){ // 由內而外,六圈
    ctx.save();
    ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';

    for (var j=0; j < i*6; j++){ // 畫各別圓點
      ctx.rotate(Math.PI*2/(i*6));
      ctx.beginPath();
      ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
      ctx.fill();
    }

    ctx.restore();
  }
}

變形矩陣

最後一個方法是設定變形矩陣,藉由改變變形矩陣,我們因此可以營造各種變形效果;其實先前所提到的rotate, translate, scale都是在設定變形矩陣,而這邊的這個方法就是直接去改變變形矩陣。
transform(m11, m12, m21, m22, dx, dy)

呼叫Transform會拿目前的變形矩陣乘以下列矩陣:

m11 	m21 	dx
m12 	m22 	dy
0       0       1
其中m11代表水平縮放圖像,m12代表水平偏移圖像,m21代表垂直偏移圖像,m22代表垂直縮放圖像,dx代表水平移動圖像,dy代表垂直移動圖像。 如果輸入Infinity 值,不會引起例外錯誤,矩陣值會依照輸入設成無限。
setTransform(m11, m12, m21, m22, dx, dy)
復原目前矩陣為恆等矩陣(Identiy matrix,也就是預設矩陣),然後再以輸入參數呼叫transform()。

例題:matrix

動畫

產生一個畫面基本上需要以下步驟 :
  1. 清除畫布: 除了不變的背景畫面,所有先前畫的圖案都要先清除,這個步驟可以透過clearRect()方法達成。
  2. 儲存畫布狀態: 若是想要每一次重新繪圖時畫布起始狀態都是原始狀態,那麼就需要先行儲存畫布原始狀態。
  3. 畫出畫面: 畫出需要畫面。
  4. 復原畫布狀態: 復原畫布狀態以備下次繪圖使用。
一般來說當程式碼執行完畢後才會看到繪圖結果,所以無法靠執行for迴圈來產生動畫。

我們得靠每隔一段時間繪圖來產生動畫:

     setInterval(function, delay)

每隔delay毫秒,執行輸入function(函數)
     setTimeout(function, delay)

過delay毫秒後,執行輸入function(函數)
      requestAnimationFrame(callback)

告訴瀏覽器在執行動畫的時候,要求瀏覽器在重繪下一張畫面之前,
呼叫callback函數來更新動畫

太陽系動畫

本例會產生一個小型太陽系運行動畫。

循環景色

例題:循環景色

圓球彈跳

例題:圓球彈跳

參考資料Basic_animations

可撕布

例題:可撕布

參考資料tearable cloth

blobsallad

例題:blobsallad

參考資料blobsallad

video destruction

例題:BigBuckBunny

參考資料9 Mind-Blowing Canvas Demos

Canvas - 時鐘

1. 創建畫布

<canvas id="canvas" width="400" height="400"
    style="background-color:#333"></canvas>
  // 在網頁創建畫布
<script>
var canvas = document.getElementById("canvas");
  // 由畫布元件,造一畫布物件
var ctx = canvas.getContext("2d");
  // 設定物件內容為平面圖形
var radius = canvas.height / 2;  // 計算半徑
ctx.translate(radius, radius);  // 平移到畫布中心
radius = radius * 0.90  // 留10% 鐘面邊緣
drawClock();

function drawClock() {  // 畫白色鐘面
  ctx.arc(0, 0, radius, 0 , 2 * Math.PI);
  ctx.fillStyle = "white";
  ctx.fill();
}
</script>

2. 畫鐘面

function drawClock() {
  drawFace(ctx, radius);
}

function drawFace(ctx, radius) {
  var grad;
		// 繪製白色圓圈
  ctx.beginPath();
  ctx.arc(0, 0, radius, 0, 2 * Math.PI);
  ctx.fillStyle = 'white';
  ctx.fill();
		// 創建徑向漸變(原始時鐘半徑的 95% 和 105%)
  grad = ctx.createRadialGradient
  			(0, 0 ,radius * 0.95, 0, 0, radius * 1.05);
  grad.addColorStop(0, '#333');
  grad.addColorStop(0.5, 'white');
  grad.addColorStop(1, '#333');
  ctx.strokeStyle = grad;
  ctx.lineWidth = radius*0.1;
  ctx.stroke();
		// 色標創建 3D 效果
  ctx.beginPath();
  ctx.arc(0, 0, radius * 0.1, 0, 2 * Math.PI);
  ctx.fillStyle = '#333';
  ctx.fill();
}

3. 繪製時鐘數字

function drawClock() {
  drawFace(ctx, radius);
  drawNumbers(ctx, radius);
}

function drawNumbers(ctx, radius) {
  var ang;
  var num;
  ctx.font = radius * 0.15 + "px arial";
  ctx.textBaseline = "middle";  // 將文本對齊設置為打印位置的中間
  ctx.textAlign = "center";	//  和中心:
  for(num = 1; num < 13; num++){ // 將打印數字位置計算為半徑的 85%,
    ang = num * Math.PI / 6;		// 每個數字旋轉 30度:
    ctx.rotate(ang);
    ctx.translate(0, -radius * 0.85);
    ctx.rotate(-ang);
    ctx.fillText(num.toString(), 0, 0);
    ctx.rotate(ang);
    ctx.translate(0, radius * 0.85);
    ctx.rotate(-ang);
  }
}

4. 繪製時鐘指針

function drawClock() {
  drawFace(ctx, radius);
  drawNumbers(ctx, radius);
  drawTime(ctx, radius);
}

function drawTime(ctx, radius){
  var now = new Date();
  var hour = now.getHours();
  var minute = now.getMinutes();
  var second = now.getSeconds();
  // 計算時針的角度、長度和寬度
  hour = hour%12;
  hour = (hour*Math.PI/6)+(minute*Math.PI/(6*60))
          +(second*Math.PI/(360*60));
  drawHand(ctx, hour, radius*0.5, radius*0.07);
  // 計算分針的角度、長度和寬度
  minute = (minute*Math.PI/30)+(second*Math.PI/(30*60));
  drawHand(ctx, minute, radius*0.8, radius*0.07);
  // 計算秒針的角度、長度和寬度
  second = (second*Math.PI/30);
  drawHand(ctx, second, radius*0.9, radius*0.02);
}

function drawHand(ctx, pos, length, width) {
  ctx.beginPath();
  ctx.lineWidth = width;
  ctx.lineCap = "round";
  ctx.moveTo(0,0);
  ctx.rotate(pos);
  ctx.lineTo(0, -length);
  ctx.stroke();
  ctx.rotate(-pos);
}

5. 啟動時鐘

啟動時鐘,就是每隔一段時間調用一次 drawClock 函數。
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var radius = canvas.height / 2;
ctx.translate(radius, radius);
radius = radius * 0.90
//drawClock();
setInterval(drawClock, 1000);

參考資料