今天要介紹的是canvas對圖形對象的操作,包括圖像、視頻繪制,和操作像素對象的方法。
圖片/視頻的繪制
在canvas中,我們可以通過 drawImage() 的方法來繪制圖片或視頻文件,其語法為:
ctx.drawImage( img, clip_x, clip_y, clip_w, clip_h, x, y, width, height );
其中紅色的參數為可選項,它們的含義如下:
⑴ 我們先來看下最簡單的形式 ctx.drawImage(img, x, y):
<canvas id="myCanvas" width="300" height="300" style="border:solid 1px #CCC;"> 您的瀏覽器不支持canvas,建議使用最新版的Chrome </canvas> <script> var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,30,30); //在畫布坐標(30,30)的位置繪制圖片 } </script>
注意如同我們在第一章說講到的,應當等圖片onload之后才執行繪圖代碼,防止代碼在圖片加載到之前就執行。效果如下:
⑵ 我們也可以通過添加 width 和 height 參數來縮放圖片:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,30,30,250,150); //在畫布坐標(30,30)的位置繪制一張寬度為250,高度為150的圖片 }
⑶ 我們把裁剪圖片的參數 clip_x, clip_y, clip_w, clip_h 也都加上:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,10,20,300,300,30,30,250,150); //在畫布坐標(30,30)的位置繪制一張寬度為250、高度150的圖片,這種圖片是在img上坐標為(10,20)的位置所裁剪出來的寬高均為300的區域 }
注意這里被拉伸的圖片已經不再是一開始的那張原始圖了,而是原始圖在其坐標(10,20)處開始裁剪到的寬高均為300的區域,也就是把這個裁剪到的區域,再伸縮為寬250、高150。
把參數全部用上雖然感覺有點繁瑣,但它可以實現像css sprite的效果,從而有效減少圖片文件請求數量,進而減少我們要寫img.onload=.... 的次數。
說到裁剪我們順便說說另一個canvas方法 clip() ,它是更地道的“裁剪”方法,在使用它之前需要繪制一個閉合路徑(比如一個rect),使用clip()之后的繪制語句所繪制的對象只能顯示被裁剪的區域(就一開始定義的那個閉合路徑里的區域,類似PS的蒙板、Flash里的遮罩層):
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); ctx.rect(60,60,100,100); //繪制裁剪區域(一個矩形) ctx.clip(); //設置上一個閉合路徑為裁剪蒙板 var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,10,20); }
我們說回一開始講的 drawImage() 方法,它有一個蠻屌的功能——獲取和繪制視頻當前圖像,這里提供下3wschool的案例。
利用這個功能,再配合ImageData對象的方法,我們甚至可以用來替換綠屏視頻的綠色背景。至於什么是ImageData對象,這是我們接下來要講的地方。
ImageData你可以理解為“含像素數據的圖形對象”,“像素數據”指的是該圖形對象上的每一個有序的像素的數據,每個像素都有它對應的顏色數據(RGBA值)。
我們可以通過 createImageData(width,height) 方法來創建一個ImageData對象,然后通過 putImageData(imgData,x,y) 方法把ImageData對象放到畫布上:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); //創建一個寬為200,高為100的ImageData對象 ctx.putImageData(imgData,50,60); //將上述創建的ImageData對象放到畫布坐標(50,60)的位置
運行上述代碼,不會有任何圖形顯示出來,因為我們僅僅創建了一個沒有任何數據的ImageData對象。如何給ImageData對象賦予像素數據、定義其每一點像素的顏色呢?處理這問題要用到ImageData對象的 .data 屬性。
我們要知道,一個圖形對象上的每一點像素都是從上到下一行一行(每一行里又是從左到右)有序地排列着的,而每一個像素又有四個數值(RGBA)表示它的顏色。
比如下方有一個非常簡單的圖形對象(假設我把它放大了75倍,方便查看),它一共只有四個像素點,這四個像素點的RGBA數值分別是(255,255,0,255)、(0,255,64,255)、(43,149,255,255)、(236,103,100,51) :
那么這個圖形對象的“像素數據”可以看為一個數組: [255,255,0,255,0,255,64,255,43,149,255,255,236,103,100,51]
也就是把四個像素的RGBA數據依次拼起來。當然這里只是一個非常簡單的例子,常規的圖像可能有幾千幾萬個像素,但它們的像素數據都遵循這種存儲方式。
而ImageData對象的 .data 屬性正是返回這么一個存儲像素數據的數組(沒錯就是數組,故有length屬性)。我們可以這樣進一步完善上方的代碼:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0;i<imgData.data.length;i+=4) //遍歷ImageData對象的每一個像素點,並給它們上色 { imgData.data[i+0]=255; imgData.data[i+1]=100; imgData.data[i+2]=0; imgData.data[i+3]=255; } ctx.putImageData(imgData,50,60);
此處我們給該ImageData對象的每一個像素都賦值了RGBA(255,100,0,255)的顏色,執行效果如下:
我們試着不要繪制純色的ImageData對象,來個多彩的:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0,t=255;i<imgData.data.length;i+=4) //遍歷ImageData對象的每一個像素點,並給它們上色 { if(t<=0) t=255; imgData.data[i+0]=255-t; imgData.data[i+1]=t; imgData.data[i+2]=255-t; imgData.data[i+3]=255; --t; } ctx.putImageData(imgData,50,60);
效果如下:
其實 putImageData() 方法還有四個可選參數,可以用來裁剪ImageData對象上的指定區域。其全部參數為:
ctx.putImageData( imgData, x, y, clip_X, clip_Y, clip_Width, clip_Height);
clip_X,clip_Y分別表示相對於ImageData對象的裁剪起始點坐標,clip_Width, clip_Height表示要裁剪的矩形區域寬高。例如上面的例子我們可以稍微裁剪一下,裁剪成正方形吧:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0,t=255;i<imgData.data.length;i+=4) { if(t<=0) t=255; imgData.data[i+0]=255-t; imgData.data[i+1]=t; imgData.data[i+2]=255-t; imgData.data[i+3]=255; --t; } ctx.putImageData(imgData,60,60,50,0,100,100); //裁剪imgData上坐標為(50,0)且寬高均為100px的矩形區域,並在畫布(60,60)的坐標上畫出來
每一個ImageData對象都有其 width 和 height 屬性,對應其寬度和高度:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0;i<imgData.data.length;i+=4) { imgData.data[i+0]=255; imgData.data[i+1]=100; imgData.data[i+2]=255; imgData.data[i+3]=255; } ctx.putImageData(imgData,50,60); console.log("寬度是" + imgData.width + ",高度是" + imgData.height ); //輸出其寬高
另外介紹下獲取已有ImageData對象的兩個方式,首先是直接用 createImageData( imgData ) 的方式來獲取已有的ImageData對象的尺寸,注意這里只會獲取其尺寸,不會把已有對象的像素數據也復制了:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0;i<imgData.data.length;i+=4) { imgData.data[i+0]=255; imgData.data[i+1]=100; imgData.data[i+2]=255; imgData.data[i+3]=255; } ctx.putImageData(imgData,50,10); var imgData2=ctx.createImageData(imgData); //新建一個尺寸與已有的imgData一致的新ImageData對象imgData2,注意是不會復制其像素數據的 for (var i=0;i<imgData2.data.length;i+=4) //給imgData2上色 { imgData2.data[i+0]=155; imgData2.data[i+1]=200; imgData2.data[i+2]=155; imgData2.data[i+3]=155; } ctx.putImageData(imgData2,50,160);
另一種方法才算是地道的獲取、復制已有ImageData對象的方法,即 getImageData() 方法,該方法返回一個 ImageData 對象,此對象拷貝了畫布指定矩形區域的像素數據,其語法如下:
var newImgData=ctx.getImageData( x, y, width, height );
其中參數 x,y 分別表示要從畫布上開始復制的起始點坐標,width,height 分別表示要復制矩形區域的寬度和高度。我們來個示例:
<body> <img id="img" src="http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg" /> <canvas id="myCanvas" width="300" height="600" style="border:solid 1px #CCC;"> 您的瀏覽器不支持canvas,建議使用最新版的Chrome </canvas> <script> var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = document.getElementById("img"); img.onload = function(){ ctx.drawImage(img,10,10); var imgData=ctx.getImageData(0,0,c.width,c.height); ctx.putImageData(imgData,0,300); } </script> </body>
執行上述代碼時發現
var imgData=ctx.getImageData(0,0,c.width,c.height);
ctx.putImageData(imgData,0,300);
這兩句沒有起任何作用,且Chrome報錯“Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.”:
這是何處導致的問題?我們的代碼寫錯或寫漏了什么么?或者圖片不能作為ImageData對象來獲取?
其實不然,我們的代碼沒有特別的錯誤,而圖片或者視頻文件都可以算作ImageData對象,之所以會發送這個錯誤,是因為我們在canvas上放置了一張跨域的圖片。
一旦canvas發現你繪制了一張跨域的圖片時,它就會認為此時的畫布是"tainted"、被污染的,從而不允許你操作該圖片的像素,從而防止多種類型的XSS/CSRF攻擊。
對於此問題的詳細描述可以查看這里,而解決此問題的辦法是在服務器的環境下來運行代碼(當然圖片也要放到項目目錄下作為本地文件)。
我們使用tomcat/IIS/wamp等服務器來運行我們的項目,便可成功執行、得到我們想要的效果:
在上面代碼的基礎上,我們可以來執行一個有趣的效果,它類似於制圖軟件中將一張圖片顏色“取反”,也就是說假如圖片上某一點像素顏色是RGBA(255,0,100,255),取反后該像素的RGBA變為(0,255,155,255)。注意透明度Alpha是保持原值的。我們可以這樣寫代碼:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = document.getElementById("img"); img.onload = function(){ ctx.drawImage(img,10,10); var imgData=ctx.getImageData(0,0,c.width,c.height); for (i=0; i<imgData.width*imgData.height*4;i+=4) { imgData.data[i]=255-imgData.data[i]; imgData.data[i+1]=255-imgData.data[i+1]; imgData.data[i+2]=255-imgData.data[i+2]; imgData.data[i+3]=255; } ctx.putImageData(imgData,0,300); }
效果如下:
關於圖像對象操作的介紹我們就講到這,最后來個有趣也實用的應用。還記得我們在前面提到的,可以利用 drawImage 和 ImageData 的方法來替換屏幕的綠色背景么?
在MSDN有這么一段介紹:
從兩個視頻中讀寫像素到另一個視頻中所需的代碼要求使用兩個視頻、兩個畫布和一個最終畫布。一次捕捉視頻上的一幀,然后繪制到兩個單獨的畫布上。這樣允許讀回數據。
其中畫布1和畫布2分別用來繪制兩個視頻當前幀的畫面(注意這倆個畫布我們設其樣式visibility:hidden,即不可見):
ctxSource1.drawImage(video1, 0, 0, videoWidth, videoHeight);
ctxSource2.drawImage(video2, 0, 0, videoWidth, videoHeight);
然后我們可以輕松從這兩個畫布已繪制出來的圖像並轉為ImageData對象:
currentFrameSource1 = ctxSource1.getImageData(0, 0, videoWidth, videoHeight);
currentFrameSource2 = ctxSource2.getImageData(0, 0, videoWidth, videoHeight);
最后從瀏覽綠屏的像素數組中搜索綠色像素,如果找到,代碼將用背景場景中的像素替換所有綠色像素:
for (var i = 0; i < n; i++) { // Grab the RBG for each pixel: r = currentFrameSource1.data[i * 4 + 0]; g = currentFrameSource1.data[i * 4 + 1]; b = currentFrameSource1.data[i * 4 + 2]; // If this seems like a green pixel replace it: if ( (r >= 0 && r <= 59) && (g >= 74 && g <= 144) && (b >= 0 && b <= 56) ) // Target green is (24, 109, 21), so look around those values. { pixelIndex = i * 4; currentFrameSource1.data[pixelIndex] = currentFrameSource2.data[pixelIndex]; currentFrameSource1.data[pixelIndex + 1] = currentFrameSource2.data[pixelIndex + 1]; currentFrameSource1.data[pixelIndex + 2] = currentFrameSource2.data[pixelIndex + 2]; currentFrameSource1.data[pixelIndex + 3] = currentFrameSource2.data[pixelIndex + 3]; } }
MSDN還專門提供了一個實例(點我查看),查看該頁面源碼即可獲得全部代碼,有興趣的朋友可以研究下。
本章就講到這里,下一章將介紹canvas常用的變形轉換功能,共勉~