想學canvas?那必定要看看這篇文章

canvas簡介

在學習一項新技術以前,先了解這項技術的歷史發展及成因會幫助咱們更深入的理解這項技術。javascript

歷史上,canvas最先是由Apple Inc. 提出的,在Mac OS X webkit中建立控制板組件使用,而在canvas稱爲HTML草案及標準以前,咱們是經過一些替代方式去繪圖的,好比爲人所詬病的Flash,以及很是強大的SVG(Scalable Vector Graphics,可伸縮的矢量標記圖),還有隻能在IE(IE 5.0以上的版本)中使用的VML(Vector Markup Language,矢量可標記圖)。甚至於有些前端可使用div+css來完成繪圖。css

總的來講,沒有canvas的時候,在瀏覽器繪製圖形是比較複雜的,而在canvas出現以後,繪製2D圖形相對變得容易了。html

NOTE: 用div繪製一些簡單的圖形,如矩形,圓形,三角形,梯形,倒也算是沒那麼複雜。

但canvas也有缺點。由於canvas本質上是一個與 分辨率相關位圖畫布 ,也就註定了在不一樣分辨率下,canvas繪製的內容顯示的時候會有所不一樣。此外,canvas繪製的內容 不屬於任何DOM元素 ,在瀏覽器的元素查看器中也找不到,那天然沒法檢測鼠標點擊了canvas中的哪一個內容,很顯然,這兩方面,canvas都是不如SVG的。前端

舉個例子:若是使用CSS設置canvas元素的尺寸,那可能會致使繪製出來的圖形變得扭曲,如長方形變正方形,圓形變橢圓等,這是由於畫布尺寸和元素尺寸是不同的,畫布會自動適應元素的尺寸,若是兩者是成比例的,那麼畫布就會等比例縮放,不會出現扭曲。

這麼說來,canvas有這麼明顯的缺點,那直接使用SVG豈不是更好?java

No,聽過一句話嗎?沒有完美的方案,只有適不適合。es6

SVG是基於XML的,那麼就說明,SVG裏面的元素均可以認爲是 DOM元素 ,能夠啓用DOM操做,同時,SVG中每一個繪製的圖像均被視爲對象,若SVG對象屬性變化,瀏覽器會自動重現圖形。web

以上是SVG的優點,但經過這個優點,咱們也能發現一些問題:算法

  1. 一般,過分使用DOM的應用都會變得很慢,因此,複雜的SVG會致使渲染速度變慢。可是像地圖這類的應用,首選是SVG。
  2. 瀏覽器的重排發生在瀏覽器窗口發生變化,元素尺寸位置變化,字體變化等等。
  3. 即便能夠啓用DOM操做,但DOM操做的代價仍是比較昂貴的(DOM和JS的實現是分開的)。

回到主題。canvas

canvas是經過JavaScript進行2D圖形的繪製,而 <canvas> 標籤自己是沒有任何繪製能力的,它僅僅是一個容器。在繪製時,canvas是逐像素的進行渲染的,一旦圖形繪製完成,該元素就再也不被瀏覽器所關注(腳本執行結束,繪製的圖形也不屬於DOM)。瀏覽器

值得注意的是,在HTML標準(whatwg標準)中明確的指出: Authors should not use the canvas element in a document when a more suitable element is available. 因此,不要濫用元素。

canvas目前幾乎被全部的瀏覽器支持,可是IE 9.0 以前的版本不支持 canvas元素

canvas基本使用

canvas是一個HTML元素,因此要使用canvas,首先須要:

<canvas id="canvas" width="600" height="300">
    當前瀏覽器不支持canvas
</canvas>

在第一行HTML代碼中能夠看到兩個屬性:widthheight ,它指明瞭畫布的寬高,在上文中提到過,不要使用CSS規定尺寸,由於當CSS規定的尺寸和畫布尺寸比例不一致時,沒法成比例縮放,致使繪製出來的圖形變得扭曲。在沒有設置畫布大小時,canvas默認會初始化成300px * 150px的畫布。

「當前瀏覽器不支持canvas」是元素的內容,但他只是做爲一個後備內容(即fallback content),只有當瀏覽器不支持canvas時,這個內容纔會被顯示出來。

canvas元素自己沒有繪製能力,只是做爲一個容器,因此須要經過JavaScript這類腳本進行繪製:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

上面的HTML+JS代碼是使用canvas所必須的,不管要繪製什麼內容,這幾行代碼不可缺乏。

getContext() 是canvas元素提供的方法,用於獲取繪製上下文(或者說渲染上下文,The rendering context),他只有一個參數:上下文格式。這裏傳入2d 表示獲取2D圖像繪製環境。因爲getContext是canvas元素提供的方法,故咱們能夠經過檢測getContext方法的存在性來檢查瀏覽器的支持性。

context變量的類型是 CanvasRenderingContext2D

渲染上下文很差理解,能夠理解爲畫圖用的筆刷。

在畫布中如何肯定繪製的位置?是座標。

在canvas中,畫布的左上角爲原點,橫軸爲x軸表示寬,縱軸爲y軸表示高1。原點的位置是能夠移動的,咱們暫時不考慮原點的移動問題。

w3c school 中,將canvas提供的繪製API大體分爲如下幾種2

  1. 顏色、樣式、陰影
  2. 線條樣式
  3. 矩形
  4. 路徑
  5. 轉換
  6. 文本
  7. 圖像繪製
  8. 像素操做
  9. 合成
  10. 其餘

canvas組合示例

在上面這個例子中,包含了矩形,圓形,線,文字及「文字」幾大塊內容,細講下去,會涉及到很多API,會使得本文變得很長,並且沒有必要,值得一提的是貝塞爾曲線,這是二維圖形應用程序的數學曲線,通常的矢量圖形軟件就是經過它來精確畫出曲線的,貝塞爾曲線是計算機圖形學中至關重要的參數曲線3

一次貝塞爾曲線

二次貝塞爾曲線

三次貝塞爾曲線

以上圖片按順序分別是一次貝塞爾曲線,二次貝塞爾曲線,三次貝塞爾曲線。從圖中,能夠很清楚的看到,一次貝塞爾曲線其實是一條直線。固然,還有更高階次的曲線,不過canvas只提供了二次和三次貝塞爾曲線。

以二次貝塞爾曲線的API爲例:

quadraticCurveTo(cp1x, cp1y, x, y);

(cp1x, cp1y)表示控制點座標,(x, y)表示結束點座標。這裏還缺乏一個起始點座標,假設是(x0, y0),那這個(x0, y0)是誰?

就是在調用 quadraticCurveTo 函數時,context(繪製上下文)所處的座標。舉個例子:

var cxt = canvas.getContext('2d'); // 認爲canvas已經獲取到
cxt.beginPath();
cxt.moveTo(120, 90);
cxt.quadraticCurveTo(130, 80, 130, 70);
cxt.quadraticCurveTo(115, 70, 115, 50);
cxt.quadraticCurveTo(115, 30, 155, 30);
cxt.quadraticCurveTo(195, 30, 195, 50);
cxt.quadraticCurveTo(195, 70, 155, 70);
cxt.quadraticCurveTo(135, 90, 120, 90);
cxt.stroke();

這段代碼運行結果就是一個對話框(在第一張圖片中體現),能夠看到,在調用二次貝塞爾曲線以前,咱們設置了起點,即,將筆刷移動到座標(120, 90),在以後調用中,都是之前一次貝塞爾曲線的終點做爲本次曲線的起點。

這時候可能會有人問:我去掉這個moveTo的調用是否是就畫不出來了?若是後續是調用lineTo函數,那還真就畫不出來了。可是別忘了,還有一次貝塞爾曲線,這就是條直線,他是以(cp1x, cp1y)爲起點,(x,y)爲終點的一條直線。因此說,去掉moveTo後,只會影響到第一條曲線的繪製。可是若是刪除最後一行代碼stroke(),那麼程序運行結束時,在瀏覽器上啥都看不到。

由此,咱們應該思考另外一個問題:爲何stroke()函數是必須的呢?

其實,canvas是一種基於狀態的繪製,依照此,能夠將canvas提供的API分爲兩種:狀態設置,具體繪製。

stroke()fill()等函數就是將內容繪製到canvas畫布容器中的函數。

arc()lineTo()rect()等函數就是設置筆刷狀態的函數。

在那種玄幻類型的電影、電視劇裏面就常常能看到某個道士虛空畫符,畫完以後往前一推,就印在了對應的符或者人身上了。

道士虛空畫符,這個過程就像是canvas設置筆刷狀態的過程。

往前一推,這個就是具體的繪製了,怎麼繪製咱不知道,反正這符是畫上去了。(前文提到過,canvas是 逐像素渲染 的)

「文字」的繪製,注意,這個文字是打了引號的,普通文字,咱們繪製只須要調用fillText()便可,而這裏所指的文字是點陣字體,在單片機或者LCD這類程序中,經過點亮一系列的點,顯示出文字或圖案,點亮的過程較爲複雜,能夠簡單的理解爲LCD上的像素點置爲1時點亮該點,爲0時不點亮(實際可能相反)。那麼canvas這裏的「文字」繪製也是同樣的道理,經過創建文字對應的字體庫,當須要繪製某個文字的時候,在字體庫中找到對應的文字點陣,而後將點陣中標誌爲1的位置點亮(填充)便可。

實際操做時,可能並非點亮這麼簡單,你可能會想要製做出更酷的內容,用圓形去填充,用矩形去填充,甚至說想要製做出動態爆炸的效果,這時候就牽扯到一些其餘的計算了。

矩形填充

上圖是一個用矩形填充的示例,數字對應8x8的點陣。

canvas的高級動畫

先思考一個問題,假設如今咱們已經學會了繪製一個圓形的方法,如今要求作出一個和物理學相關的動畫:平拋運動。

如今該如何去實現呢?

可能看到這個問題的時候,有些人瞬間懵圈了:我就學了個繪製圓的函數,你就讓我模擬這麼高難度的動畫,你這分明是想謀害鄭!

可能也有人會想到,平拋運動,在高中物理學中學到過,基本都只是研究一個小球的問題,在2維平面中,這小球徹底能夠視做一個圓,可不就只須要學會畫圓就好了?

經此,咱們繼續往下思考,在平拋運動中的小球,假設水平方向設有初始速度v0,除了重力外,不受到其餘外力影響,也即存在一個重力加速度g(爲了計算簡單,咱們能夠簡單的設爲g = 10m/s^2),同時豎直方向沒有初速度vh(或稱vh = 0;),以下圖:

平拋運動

從圖中,咱們能夠看到一些頗有意思的現象,如:小球的水平方向恰好和canvas畫布的橫軸一致,豎直方向也和縱軸方向保持一致。

而後由平拋運動對應的物理公式:

// 豎直方向無初速度,水平方向沒有外力
x = v0 * t; // 水平方向位移
h = 1/2 * g * t * t; // 豎直方向位移

// 豎直方向有初速度
h = vh * t - 1/2 * g * t * t; // 豎直方向位移

發現(x, h)和canvas上的座標(x, y)是一致的,並且咱們也不是在作物理題,也就是說,v0, t, g, vh這些參數都是已知的,咱們惟一須要作的就是,計算出任意時刻的(x, h),也即小球在canvas上的座標(x, y)。

分析結束,咱們如今能夠獲得小球在任意時刻的位置座標,那麼咱們也就能夠在畫布上畫出來任意時刻的小球。

針對上面的分析,可能會有人說:你這不對,你這個應該是具備特殊性的吧,小球未必是從左邊拋出去的,從右邊也能夠啊,向上拋也能夠。

的確,上面的分析只是取出了其中一個比較特殊的狀態來研究,限於篇幅(以及本文主題是canvas而非物理),沒有推廣到更通常的結論,但其實,這些分析已經足夠了,不管是位移仍是速度,他都是矢量,帶有方向,那麼咱們不妨規定:以canvas的座標軸,數值增長的方向爲正向,那麼從右邊拋出,能夠認爲是反向,能夠表示爲-v0 ,最終經過計算位移的公式,能夠獲得正確的座標(但這時候算座標x是比較麻煩的,不能直接使用上述公式)。

分析這麼多,說點兒咱最關心的實現。

在以前的分析中,咱們知道想求小球任意時刻所在位置座標,須要的參數有:v0, t, g, vh。這些參數應該存放在哪裏呢?怎麼設計這個數據結構?

咱們固然能夠直接將這些參數設爲全局變量,但這顯然是不合適的,這些參數裏,惟一適合設爲全局變量的是重力加速度g。而v0, t, vh這些都應該是小球自身的「屬性」,因此咱們應該將其抽象成一個類。

function Ball(r, v0, vh, t) {
    this.r = r;
    this.v0 = v0;
    this.vh = vh;
    this.t = t;
    this.x = 0;
    this.h = 0;
    
    this.calcX = function() { /* 計算水平位移 */ }
    this.calcH = function() { /* 計算豎直位移 */ }
}

var ball = { x: 0, h: 0, r: 10, v0: 0, vh: 0, g: 10};
// 重力加速度不管是做爲全局變量仍是小球屬性,都可

// es6以後
class Ball {
    constructor();
}

以上三種方式,各有各的好處,選擇一個合適的方式便可。

「你這說物理我就頭大,有沒有更簡單的?」

更簡單也有啊,反正並無要求100%還原物理學場景:

var ball = { x: 0, y: 0, r: 10, vx: 5, vy: 0, g: 5 };
setInterval(() => {
    ball.vy += ball.g; // 豎直方向速度增長
    ball.y += ball.vy; // 豎直方向位移
    ball.x += ball.vx; // 水平方向位移
    cxt.clearRect(0, 0, 800, 300);
    cxt.beginPath();
    cxt.fillStyle = 'black';
    cxt.arc(ball.x, ball.y, ball.r, 0, 2*Math.PI);
    cxt.fill();
}, 50);

OK,結束了。

這就是高級一點的動畫。可能在學幾個函數,這個動畫會更炫一點。好比學完矩形填充再掌握一點rgba的知識,你能夠作個「尾巴」出來,即長尾效應。具體只須要將上述代碼中的cxt.clearRect()替換成:

cxt.fillStyle = 'rgba(255, 255, 255, 0.2)';
cxt.fillRect(0, 0, 800, 300);

這就能顯得我們編碼能力很厲害的樣子。

作到這一步仍是不知足:小球一個勁兒的向下掉,這動畫沒一下子就沒了。

不要緊,我們能夠作「碰撞檢測」啊。好像又是一個高大上的詞彙,但實際上也沒什麼高大上的,若是基於本節第一部分的分析,那咱還得考慮一下碰撞形成的動量損失的問題,挺複雜的。

可是簡化版就好說了啊。小球碰到上/下邊界,豎直方向速度反向,同時速率減半。左右邊界能夠有相似的處理。

if (ball.r + ball.x > canvas_width) {
    ball.vx *= -0.5
}
if (ball.r + ball.y > canvas_height) {
    ball.vy *= -0.5;
}
NOTE:碰撞檢測在這裏指的是「 邊界檢測」,小球落到邊界的時候再繼續下落顯然是沒有意義的,由於後面的動畫我們是看不到的。因此要麼碰到邊界就中止,要麼從新開始,或者進行其餘處理,總之,不能出現無心義的動畫。

像之前玩的貪吃蛇,會有各類牆的存在,控制的小蛇在碰到牆的時候,遊戲就失敗了,或者說沒有牆的時候,小蛇會從另外一個方向出來。

小結

說了這麼多,你會發現,本文不只沒有直接的羅列不一樣的DEMO來介紹函數,更是在儘可能避免過多的介紹canvas中的API。

我的看來,canvas其實就是一個函數庫,他和咱們平時使用的那些什麼forEach,splice,split,map,reduce沒什麼區別,都是封裝好了直接用的,查一查函數手冊就能夠了解用法了,多用幾回就會比較熟悉了。

剛進大學的時候,專業課老師就告訴咱們,程序=算法+數據結構,即便到如今,也有不少人在強調這一點。若是你有心,再回想一下上一節內容,在分析平拋運動的時候,我本質上是在考慮算法問題;在設計小球的類時,考慮了面向對象,但更多的是在考慮數據結構的問題,在考慮了這些內容的基礎上,我纔開始了具體的實現。

以爲不錯,不妨關注一下?

參考資料:


  1. MDN文檔
  2. HTML 5 Canvas參考手冊
  3. 貝塞爾曲線
相關文章
相關標籤/搜索