HTML5 的標準已經出來很久了,但是似乎其中的 Canvas 現在並沒有在太多的地方用到。一個很重要的原因是,Canvas 的標準還沒有完全確定,不適合大規模用於生產環境。但是,Canvas 的優點也是很明顯的,例如在繪製含有大量元素的圖表的時候,SVG 往往因為性能問題而無法勝任,例如我見過的一次技術分享會的抽獎環節,雖然效果比較炫,但因為每個頭像都是DOM,利用CSS3 控制的動畫,導致了效能非常低。此外,隨著硬體效能的提高,視訊截圖、影像處理等功能也逐漸可以在網頁上實現了,大多數網站用的是Flash,但是Flash 在Mac 電腦上效能不高,還需要學習一些額外的知識。 Canvas 則是直接使用 JavaScript 來進行繪圖,對 Mac 友好,所以不失為 Flash 的一個繼承者。
使用 Canvas
說了這麼多,Canvas 究竟是個啥?
英文中 Canvas 的意思是“畫布”,不過這裡說的 Canvas 是 HTML5 中新出的一個元素,開發者可以在上面繪製一系列圖形。 Canvas 在HTML 文件中的寫法很簡單:
其中 id 屬性是所有HTML 元素都可以用的,Canvas 自帶的屬性只有後面兩個(分別控制寬度、高度),沒有其它的了。至於相容性,CanIUse 上面寫了,基礎的功能目前用戶使用的 90% 的瀏覽器都支持,所以大部分情況下還是可以放心使用的。
注意,一定要用 Canvas 隨附的 width 和 height 屬性,請勿使用 CSS 來控制,因為 CSS 控制會造成 Canvas 變形。可以試著與 PhptpShop 對比一下,後者是改變“圖像大小”,前者才是正確的改變“畫布大小”。例如下圖是三張圖片的橫向拼接:最左邊的黑框中是大小為50px * 50px 的原圖;中間是改變了圖像大小為100px * 100px 的效果,圖像變得模糊,但是對於圖像本身來說座標範圍並沒有變大;最右邊才是正確的100px * 100px 的Canvas。
Canvas 絕大部分的繪圖方法都與
我們首先取得到這個元素:
var canvas = document.getElementById('canvas');
然後透過一個方法來取得可以呼叫一切 Canvas API 的入口:
var ctx = canvas.getContext('2d');
看到 2d 是不是很激動地聯想到有沒有 3d 呢?沒有 3d 的寫法,不過如果想要開啟 3D 世界的大門,則可以寫 canvas.getContext('webgl')。然而 WebGL 是基於 OpenGL ES 2.0 的一套標準,與本文是徹底的兩條路,因此這裡就不討論了。
Canvas 中的基本概念
坐標
與數學上常見的笛卡爾坐標係不太相同,Canvas 的坐標係是計算機中常見的坐標系,它長這樣:
畫布的最左上角是(0,0),往右x 增大,往下y 增大,而且x 和y 都是整數(就算在計算過程中不是整數,在繪製的時候也會當作整數處理),單位是像素。
绘图
带大家怀旧一下。不知道有多少同学小时候玩过 logo 语言,在里面你可以控制一只小海龟在一块板子上行走、画画、提笔、落笔。Canvas 中也一样,你需要控制一只画笔的移动和绘制。然而 Canvas 更高级一些,你可以直接利用一些函数来画图,不用去控制那只画笔的位置。
Canvas 中的基本图形
通过上文定义的 ctx 变量可以干许多有意思的事情,我们先看看如何绘制一些基本图形。
线条
我们指定画笔移动到某一点,然后告诉画笔需要从当前这一点画到另一点。我们可以让画笔多次移动、绘制,最后统一输出到屏幕上。例子如下:
ctx.moveTo(10, 10); ctx.lineTo(150, 50); ctx.lineTo(10, 50); ctx.moveTo(10, 20); ctx.lineTo(40, 70); ctx.stroke();
上面的代码中,lineTo 是产生线条用的函数,执行完之后画笔就移到了线条的终点。需要注意的是,线条此时并没有显示在屏幕上,必须调用 stroke 才会显示。这样设计是有道理的,因为向屏幕上输出内容需要耗费大量的资源,我们完全可以先攒够一波 lineTo,最后用 stroke 放一个大的。
路径
绘制路径非常简单,只需要先告诉 ctx 一声“我要开始画路径了”,然后通过各种方法(例如 lineTo)绘制路径。如果需要画一个封闭路径,那就最后告诉 ctx一声:“我画完了,你把它封闭起来吧。”当然,不要忘记利用 stroke 输出到屏幕上。
一个简单的例子:
ctx.beginPath(); ctx.moveTo(10, 10); ctx.lineTo(150, 50); ctx.lineTo(10, 50); ctx.closePath(); ctx.stroke();
如果我不想只描绘路径线条,而是想填充整个路径呢?可以将最后一行的 stroke 改成 fill,这样就跟使用了画图中的油漆桶一样,封闭路径里面的内容就都被填充上颜色了:
ctx.fill();
弧 / 圆形
绘制弧的函数参数比较多:
ctx.arc(圆心 x 坐标, 圆心 y 坐标, 半径, 起始角度, 终止角度, 是否为逆时针);
注意,在 Canvas 的坐标系中,角的一边是以圆心为中心的水平向右的直线。角度单位均为弧度。例如下图,确定了圆心、起始角度(图中标明的锐角)和终止角度(图中标明的钝角),方向为逆时针,于是就有了这么一个弧。如果方向为顺时针,那么就会是一个跟它互补的、非常非常大的弧……
所以如果转了 2π 圈之后,弧就成了圆形,因此也可以使用绘制弧的方式来绘制圆形:
ctx.beginPath(); ctx.arc(圆心 x 坐标, 圆心 y 坐标, 半径, 0, Math.PI * 2, true); ctx.closePath();
最后一个参数随便填(当然也可以不填),因为不管是顺时针还是逆时针,转了 2π 圈之后都是一个圆。
矩形
如果只是想绘制一个横平竖直的矩形,可以使用下面的两个方法:
// 只描边 ctx.strokeRect(左上角 x 坐标, 左上角 y 坐标, 宽度, 高度); // 只填充 ctx.fillRect(左上角 x 坐标, 左上角 y 坐标, 宽度, 高度);
线条样式 / 填充样式
之前绘制的所有图形都是黑色的,但是 Canvas 肯定不止这么一种颜色(不然标准的制定者会被喷的很惨)。事实上,Canvas 可以单独设置线条样式和填充样式,分别使用的是 strokeStyle 和 fillStyle。可能的值有三种:纯色、渐变、图像。既然线条样式与填充样式的使用方法相同,那么下面统一以填充样式为例。如果想设置线条样式,直接将所有的 fillStyle 改成 strokeStyle 即可,里面的参数都不变。
/* 纯色填充 */ // 普通的颜色 ctx.fillStyle = '#0000ff'; // 带有透明度的颜色 ctx.fillStyle = 'rgba(64, 0, 127, 0.5)'; /* 渐变填充 */ // 设置渐变的尺寸(参数分别为起始点的 x 和 y、终止点的 x 和 y) var gradient = ctx.createLinearGradient(0, 0, 170, 0); // 设置过渡色,第一个参数是渐变的位置,第二个参数是颜色 gradient.addColorStop(0, 'magenta'); gradient.addColorStop(0.5, 'blue'); gradient.addColorStop(1.0, 'red'); // 设置填充样式 ctx.fillStyle = gradient; /* 图片填充 */ // 创建图片 var image = new Image; image.src = '/path/to/image.png'; // 创建图片笔触,可以指定图片的平铺方式,这里是横向平铺 var pattern = ctx.createPattern(image, 'repeat-x'); // 设置笔触填充 ctx.fillStyle = pattern;
关于渐变,除了代码中提到的线性渐变以外,还有 createRadialGradient,也就是径向渐变。
设置完填充样式之后,就可以使用 fill 来填充啦!如果设置的是线条样式,那么就可以使用 stroke 来描边。
当然,对于线条样式,还有个额外的方法叫 lineWidth 可以用来控制线条的宽度。
文字
要想在画布上画文字,首先需要知道所使用的字体和字号:
ctx.font = '30px Verdana';
然后就可以通过 strokeText 或者 fillText 来对字体描边或者填充字体。
ctx.strokeText("Hello Coding!", 23, 33); ctx.fillText("Hello Coding!", 23, 66);
图片
在 Canvas 中绘制图片有三种方法:
// 指定绘制位置 ctx.drawImage(image, x, y); // 指定绘制位置和图像宽高 ctx.drawImage(image, x, y, width, height); // 指定剪裁区域、绘制位置和图像宽高 ctx.drawImage(image, sx, sy, swidth, sheight, x, y, width, height);
参数的含义依次如下:
image: 要使用的 Image、Canvas 或 Video sx: 可选,开始剪切的 x 坐标 sy: 可选,开始剪切的 y 坐标 swidth: 可选,被剪切图像的宽度 sheight: 可选,被剪切图像的高度 x: 在画布上放置图像的 x 坐标 y: 在画布上放置图像的 y 坐标 width: 可选,要使用的图像的宽度 height: 可选,要使用的图像的高度
画布设置
细心的同学可能会发现,刚才有些属性是直接对 ctx 变量做设置,例如 ctx.lineWidth,只要设置了它,那么后续画出来的线条全都是这么个宽度。
其实,Canvas 的设置项还有许多,例如我们可以直接移动画布、旋转画布、设置全局的绘制透明度等等。这些设置还可以随时保存和恢复。
要注意的一点是,所有已经画在画布上的东西,是已经定死了的,不管之后再次进行任何设置都不会再改变。这个很像 Windows 下的画图程序。
废话不多说,直接上代码:
// 移动画布,其实就是移动坐标系 ctx.translate(往右移动的量, 往下移动的量); // 旋转画布,旋转中心为坐标系原点 ctx.rotate(顺时针旋转的角度); // 以坐标系原点为中心缩放画布 ctx.scale(横向放大倍数, 纵向放大倍数); // 设置绘制透明度,如果 fillStyle 等属性设置了透明度则会叠加 ctx.globalAlpha(零到一的小数); // 设置全局组合操作 ctx.globalCompositeOperation = 'lighter'; // 保存当前设置 ctx.save(); // 恢复上次保存的设置 ctx.restore();
移动、旋转、缩放其实就是在控制绘图的坐标系,如果你在调用这三个方法的时候,脑子里时刻有一个带刻度的坐标系,效果会非常好。
事實上,Canvas 的座標變換遵循電腦圖形學的知識:變換矩陣。簡單來說,一個座標可以看成是一個矩陣,座標所對應的矩陣乘上變換矩陣就可以實現對座標的變換。為了提升計算的效率,可以先計算出幾種變換複合之後的變換矩陣,然後直接透過 transform 函數對目前座標系進行變換,或透過 setTransform 函數將座標系重設為初始狀態後再進行變換。至於變換矩陣的內容,對於本文來說就有些超綱了。
全域組合操作有點像PhotoShop 裡面的“混合選項”,具體的實現方式還沒有完全確定,目前常見瀏覽器都統一了的實現方式有:source-over、source-atop、destination-over、destination- out、lighter、xor。具體的行為可以看 Mozilla 官方文檔,但是由於標準還未完全確定,因此其它瀏覽器不保證所有的行為都跟 Mozilla 的標準一致。一般來說,比較常見的是 source-over 和 lighter 兩種,這兩種的標準在瀏覽器界也算是無可爭議的。
至於保存和恢復設定就有點好玩了,首先需要了解一個叫做「棧」的東西。
棧是一個一維數組,規定只能從一個方向操作。棧一開始是空的,我們可以從這個方嚮往數組 push 元素,也只能從這個方向把最後一個元素(棧頂元素)pop 出來,除此以外沒有任何多餘的操作。當然,pop 的次數不能多於 push 的次數,因為 pop 到堆疊底部的時候棧裡就已經沒有元素了,此時再 pop 是沒有意義的。棧的用處有很多,例如括號匹配、表達式求值、深度優先搜索,甚至絕大部分語言的函數呼叫都要用到棧。
每次我們調用 save 函數,實際上是將當前的全域設定push 到了一個專門棧上,每次調用 restore 函數的時候將最後一次保存的內容pop 出來並用它覆蓋當前的全局設置,這樣頂棧就是最近一次保存的內容了。保存和恢復在某些情況下很好用,例如我需要畫一個歪著的圖形,然後繼續畫正著的圖形,這樣就可以先調用 save,然後調用 rotate,畫完圖形之後再restore 回來,繼續畫其它的圖形。
其實Canvas 還有許多方法,例如 toDataURL 直接將目前畫布上的內容轉換為十六進位的 data-url,getImageData 直接將影像轉換為RGBA 陣列以供影像處理演算法使用,putImageData 將RGBA 陣列轉換為圖片顯示在畫布上等等。如果配上 JavaScript 的計時更新(最好用 requestAnimationFrame 而不是 setInterval),則可以產生動畫效果。網路上還有許多 Canvas 的函式庫,可以讓程式設計師更簡單地基於 Canvas 編寫屬於自己的特效或功能。在這兒我想說一句話:大家的腦洞有多大,Canvas 的能力就有多強~