天天看點

canvas技術概述

canvas簡介

在學習一項新技術之前,先了解這項技術的曆史發展及成因會幫助我們更深刻的了解這項技術。

曆史上,canvas最早是由Apple Inc. 提出的,在Mac OS X webkit中建立控制闆元件使用,而在canvas稱為HTML草案及标準之前,我們是通過一些替代方式去繪圖的,比如為人所诟病的Flash,以及非常強大的SVG(Scalable Vector Graphics,可伸縮的矢量标記圖),還有隻能在IE(IE 5.0以上的版本)中使用的VML(Vector Markup Language,矢量可标記圖)。甚至于有些前端可以使用div+css來完成繪圖。

總的來說,沒有canvas的時候,在浏覽器繪制圖形是比較複雜的,而在canvas出現之後,繪制2D圖形相對變得容易了。

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

但canvas也有缺點。因為canvas本質上是一個與 分辨率相關 的 位圖畫布 ,也就注定了在不同分辨率下,canvas繪制的内容顯示的時候會有所不同。此外,canvas繪制的内容 不屬于任何DOM元素 ,在浏覽器的元素檢視器中也找不到,那自然無法檢測滑鼠點選了canvas中的哪個内容,很顯然,這兩方面,canvas都是不如SVG的。

舉個例子:如果使用CSS設定canvas元素的尺寸,那可能會導緻繪制出來的圖形變得扭曲,如長方形變正方形,圓形變橢圓等,這是因為畫布尺寸和元素尺寸是不一樣的,畫布會自動适應元素的尺寸,如果二者是成比例的,那麼畫布就會等比例縮放,不會出現扭曲。

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

No,聽過一句話嗎?沒有完美的方案,隻有适不适合。

SVG是基于XML的,那麼就說明,SVG裡面的元素都可以認為是 DOM元素 ,可以啟用DOM操作,同時,SVG中每個繪制的圖像均被視為對象,若SVG對象屬性變化,浏覽器會自動重制圖形。

以上是SVG的優勢,但通過這個優勢,我們也能發現一些問題:

通常,過度使用DOM的應用都會變得很慢,是以,複雜的SVG會導緻渲染速度變慢。但是像地圖這類的應用,首選是SVG。

浏覽器的重排發生在浏覽器視窗發生變化,元素尺寸位置變化,字型變化等等。

即使可以啟用DOM操作,但DOM操作的代價還是比較昂貴的(DOM和JS的實作是分開的)。

回到主題。

canvas是通過JavaScript進行2D圖形的繪制,而  标簽本身是沒有任何繪制能力的,它僅僅是一個容器。在繪制時,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           

在第一行HTML代碼中可以看到兩個屬性:width 和 height ,它指明了畫布的寬高,在上文中提到過,不要使用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]:

顔色、樣式、陰影

線條樣式

矩形

路徑

轉換

文本

圖像繪制

像素操作

合成

其他

在上面這個例子中,包含了矩形,圓形,線,文字及“文字”幾大塊内容,細講下去,會涉及到不少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沒什麼差別,都是封裝好了直接用的,查一查函數手冊就可以了解用法了,多用幾次就會比較熟悉了。

剛進大學的時候,專業課老師就告訴我們,程式=算法+資料結構,即使到現在,也有很多人在強調這一點。如果你有心,再回想一下上一節内容,在分析平抛運動的時候,我本質上是在考慮算法問題;在設計小球的類時,考慮了面向對象,但更多的是在考慮資料結構的問題,在考慮了這些内容的基礎上,我才開始了具體的實作。

參考資料:

MDN文檔 ↩︎

HTML 5 Canvas參考手冊 ↩︎

貝塞爾曲線 ↩︎

原文位址

https://www.cnblogs.com/keepsmart/p/12611659.html

繼續閱讀