canvas核心技術-如何繪製圖片和文字

三隻小羊發表於2018-07-27

這篇是學習和回顧canvas系列筆記的第三篇,完整筆記詳見:canvas 核心技術

通過上一篇canvas核心技術-如何繪製圖形的學習,我們知道了如何繪製任意多邊形以及圖片的填充規則。在canvas中應用比較多的還有繪製圖片和文字。這篇文章,我們就來詳細聊聊圖片和文字的繪製。

圖片

在canvas中,我們可以把一張圖片直接繪製到canvas上,跟使用img標籤類似,不同的是,圖片是繪製到canvas畫布上的,而非獨立的html元素。canvas提供了drawImage方法來繪製圖片,這個方法可以有三種形式的用法,如下,

  • void drawImage(image,dx,dy);直接將圖片繪製到指定的canvas座標上,圖片由image傳入,座標由dx和dy傳入。
  • void drawImage(image,dx,dy,dw,dh);同上面形式,只不過指定了圖片繪製的寬度和高度,寬高由dw和dh傳入。
  • void drawImage(image,sx,sy,sw,sh,dx,dy,dw,dh);這個是最複雜,最靈活的使用形式,第一引數是待繪製的圖片元素,第二個到第五個引數,指定了原圖片上的座標和寬高,這部分割槽域將會被繪製到canvas中,而其他區域將忽略,最後四個引數跟形式二一樣,指定了canvas目標中的座標和寬高。

根據引數個數,我們會分別呼叫不同形式的drawImage,第一種形式最簡單,就是將原圖片直接繪製到目標canvas指定座標處,圖片寬高就是原圖片寬高,不會縮放。第二種形式呢,指定了目標canvas繪製區域的寬高,那麼圖片最終被繪製在canvas上的寬高被固定了,圖片會被縮放,如果指定的dw和dh與原圖片的寬高不是等比咧的,圖片會被壓縮或者拉伸變形。第三種形式,分別指定了原圖片被繪製的區域和目標canvas中的區域,通過sx,sy,sw,sh我們可只選擇原圖片中某一部分割槽域,也可以指定完整的圖片,通過dx,dy,dw,dh我們待繪製的目標canvas區域。

let img = document.createElement('img'); //建立img元素
img.src = './learn9/google.png'; //指定img的src
img.addEventListener(
  'load',
  () => {
    ctx.drawImage(img, 0, 0); // 將img元素呼叫drawImage(img,dx,dy)繪製出來
  },
  false,
);
複製程式碼

canvas核心技術-如何繪製圖片和文字

上面這個示例,這張Google圖片的原始大小是544*184,而canvas區域的大小是預設的300*150。我們呼叫了第一種形式,直接將圖片繪製到canvas的座標原點處,圖片沒有被縮放,超出了canvas區域,超出的部分,會被canvas忽略的。有一點需要注意的是,我是在圖片的onload事件中才開始繪製的,因為圖片沒有載入完畢,直接繪製圖片是無效的。下面的程式碼示例,我都將只貼出onload事件裡的程式碼,圖片載入部分程式碼都相同,就省略了。

let canvasWidth = canvas.width; //獲取canvas寬度
let canvasHeight = canvas.height; //獲取canvas高度
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); 
複製程式碼

canvas核心技術-如何繪製圖片和文字

我們把目標canvas區域指定為canvas的寬高,圖片總是會被繪製在整個canvas中,同時也可以看到繪製出來的圖片變形了。我們可以通過計算出原圖片的寬高比,根據canvas目標區域的寬度來計算出canvas目標區域的高度,或者根據canvas目標區域的高度來計算出canvas目標區域的寬度。

let imgWidth = img.width; //獲取圖片的寬度
let imgHeight = img.height; //獲取圖片的高度
let targetWidth = canvasWidth; //指定目標canvas區域的寬度
let targetHeight = (imgHeight * targetWidth) / imgWidth; //計算出目標canvas區域的高度
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
複製程式碼

canvas核心技術-如何繪製圖片和文字

從圖可以看到,根據圖片寬高比計算出來的目標canvas區域,最終,圖片繪製出來的效果是等比例縮放,沒有變形。

我們再來看看最為複雜,且最為靈活的第三種方式。使用這種方式,我們可以把Google這張圖片中的紅色的那個o部分繪製出來。

ctx.drawImage(img, 143, 48, 90, 90, 0, 0, 90, 90);
複製程式碼

canvas核心技術-如何繪製圖片和文字

Google這張圖中,紅色字母o在原圖片中的座標是(143,48),寬高是90*90,我們簡單的把這個字母繪製在了canvas的(0,0)座標處,寬高也是90*90。可以再來複雜點,把這個紅色的字母o,讓它的高度跟canvas的高度一樣,且等比例放大寬度,且圓心正好在canvas中心,實現如下,

let oWidth = 90; //獲取字母o的寬度
let oHeight = 90; //獲取字母o的高度
let targetHeight = canvas.height; //指定目標canvas區域的高度
let targetWidth = (oWidth * targetHeight) / oHeight; //計算出目標canvas區域的寬度
let targetX = (canvas.width - targetWidth) / 2; //移動目標canvas座標X
ctx.drawImage(img, 143, 48, oWidth, oHeight, targetX, 0, targetWidth, targetHeight);
複製程式碼

canvas核心技術-如何繪製圖片和文字

drawImage返回的第一引數image,不僅可以是圖片元素,實際上還可以是canavs元素,video元素。常見的離屏canvas的使用,依就是將離屏不可見的canvas繪製到當前顯示螢幕canvas上。離螢幕canvas這一部分將會在後續遊戲部分中說到,這裡不詳細說了。

影像畫素

跟圖片繪製有關的函式還有3個,它們分別是getImageDataputImageDatacreateImageData。這些函式是直接可以改變影像中某一個具體的畫素值,從而可以對圖片做一些操作,比如濾鏡。

我們先來看看getImageData方法,它的呼叫方式是let imgData = ctx.getImageData(sx,sy,sw,sh),接受四個引數,表示canvas區域的某一個矩形區域,這個矩形區域的左上角座標是(sx,sy),寬高是sw 和sh,它的返回值是一個ImageData型別的物件,包含的屬性有widthheightdata

  • ImageData.width,無符號長整型,表示這個影像區域的畫素的寬度。

  • ImageData.height,無符號長整型,表示這個影像區域的畫素的高度。

  • ImageData.data,一個Uint8ClampedArray陣列,陣列裡每4個單元,表示一個畫素值。一個像數值用RGBA表示的,這4個單元分別表示R,G,B,A,表示意思是紅,綠,藍,透明度,取值範圍是0~255。

需要注意的是,如果我們在呼叫ctx.getImageData(sx,sy,sw,sh),參數列示的矩形區域超出了canvas的區域,那麼超出的部分將是用黑色的透明度為0的RGBA值表示,也就是(0,0,0,0)。

let imgWidth = img.width; //獲取圖片的寬度
let imgHeight = img.height; //獲取圖片的高度
let targetWidth = canvasWidth; //指定目標canvas區域的寬度
let targetHeight = (imgHeight * targetWidth) / imgWidth; //計算出目標canvas區域的高度
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
console.log(`canvas.width = ${canvasWidth}`);
console.log(`canvas.height = ${canvasHeight}`);
console.log(imgData);
複製程式碼

canvas核心技術-如何繪製圖片和文字

可以看到,我們的canvas預設寬高是300*150,通過ctx.getImageData獲取整個canvas區域的畫素資料值,得到的ImageData的裝置畫素的寬高也是300*150 ,Imagedata.data 的陣列的長度是180000,這個是因為,這個imgData的畫素數是300*150,而每個畫素是由4個分量表示的,所以300*150*4 = 180000了。

當我們通過getImageData得到canvas某一個矩形區域的畫素資料之後,我們可以通過改變這個imageData.data陣列裡的顏色分量值,再將改變後的ImageData通過putImageData方法繪製到canvas上。putImageData的用法有2種呼叫形式,如下,

  • ctx.putImageData(imgData,dx,dy),這種方式,將imgData繪製到canvas區域(dx,dy)座標處,繪製到canvas的區域的矩形大小就是imgData的矩形的大小。
  • ctx.putImageData(imgData,dx,dy,dirtyX,dirtyY,dirtyW,dirtyH),不僅指定了canvas區域(dx,dy),也指定了imgData髒資料區域的(dirtyX,dirtyY)和寬高dirtyW,dirtyH。這種形式,可以只將imgData種某一塊區域繪製到canvas上。
let canvasWidth = canvas.width;
let canvasHeight = canvas.height;
let img = document.createElement('img');
img.src = './learn9/google.png';
img.addEventListener(
 'load',
 () => {
   let imgWidth = img.width; //獲取圖片的寬度
   let imgHeight = img.height; //獲取圖片的高度
   let targetWidth = canvasWidth; //指定目標canvas區域的寬度
   let targetHeight = (imgHeight * targetWidth) / imgWidth; //計算出目標canvas區域的高度
   ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
   //操作ImageData畫素資料
   let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
   oprImageData(imgData, (r, g, b, a) => {
     if (a === 0) {
       return [r, g, b, 255]; //將透明的黑色畫素值改變為不透明
     }
     return [r, g, b, a];
   });
   //將imgData繪製到canvas的中心。超出canvas區域將被自動忽略
   ctx.putImageData(imgData, canvasWidth / 2, canvasHeight / 2);
 },
 false,
);

// 遍歷畫素資料
function oprImageData(imgData, oprFunction) {
 let data = imgData.data;
 for (let i = 0, l = data.length; i < l; i = i + 4) {
   let pixel = oprFunction(data[i], data[i + 1], data[i + 2], data[i + 3]);
   data[i] = pixel[0];
   data[i + 1] = pixel[1];
   data[i + 2] = pixel[2];
   data[i + 3] = pixel[3];
 }
}
複製程式碼

canvas核心技術-如何繪製圖片和文字

上面,我們遍歷了ImageData中data陣列,並將透明度為0的畫素值的透明度變為1(255/255=1)。在遍歷畫素陣列時,我們每便利一次,i 的值加4,這個是因為一個畫素值是用4個陣列單元值表示的,分別為R,G,B,A,我們可以只改變某一個畫素值的某一個分量值,例如透明度。

ctx.putImageData(imgData, canvasWidth / 2, canvasHeight / 2, 79, 27, 50, 50);
複製程式碼

canvas核心技術-如何繪製圖片和文字

我們通過指定了ImageData中髒資料區域,只繪製了紅色字母o,其他部分忽略。上面在呼叫putImageData之前,我們通過遍歷畫素資料改變了部分畫素值的透明度,這種可以操作畫素值的方式,在影像處理等領域是非常有用的,例如常見的影像灰度和反相顏色等。

//操作ImageData畫素資料
let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
ctx.clearRect(0, 0, canvasWidth, canvasHeight); //清除canvas
oprImageData(imgData, (r, g, b, a) => {
    return [255 - r, 255 - g, 255 - b, a]; //反相顏色
});
ctx.putImageData(imgData, 0, 0);
複製程式碼

canvas核心技術-如何繪製圖片和文字

將顏色分量的RGB值都用255減去原顏色分量值,可以看到,Google每個字母的顏色都與原圖片的顏色不一樣了。這個在改變每個顏色分量的值,用不通的邏輯計算,就可以得到不同的處理後的圖片。

//操作ImageData畫素資料
let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
ctx.clearRect(0, 0, canvasWidth, canvasHeight); //清除canvas
oprImageData(imgData, (r, g, b, a) => {
    let avg = (r + g + b) / 3;
    return [avg, avg, avg, a]; //灰度
});
ctx.putImageData(imgData, 0, 0);
複製程式碼

canvas核心技術-如何繪製圖片和文字

通過取RGB的平均值,原圖片的每個字母都是灰色的了,當然,在計算的時候,可以給每個分量加一個係數,例如公式let avg = 0.299r + 0.587g + 0.114b,具體應用可以檢視Grayscale

最後來看看createImageData,這個很好理解了,就是建立一個ImageData 物件了,有兩種形式,如下,

  • ctx.createImageData(width,height),可以指定寬高,建立一個ImageData物件,ImageData.data中的畫素值都是一個透明的黑色,也就是(0,0,0,0)。
  • ctx.createImageData(imgData),可以指定一個已經存在的ImageData 物件來建立一個新的ImageData物件,新建立的ImageData物件的寬高與引數中的ImageData 的寬高一樣,但是畫素值就不一樣了,新建立出來的ImageData的畫素值都是透明的黑色,也就是(0,0,0,0)。

文字

在canvas中,我們不僅可以繪製圖形,圖片,還可以繪製文字。繪製文字比較簡單了,先設定當前ctx的畫筆的文字樣式,例如,字型大小,字型樣式,對其方式等,跟css中比較相似。

跟文字相關的方法有三個,如下,

  • strokeText(text,x,y,maxWidth?),用描邊的形式繪製指定的文字text,其中也指定了繪製的座標(x,y), 還有最後一個可選引數,最大的寬度,如果所繪製的文字超過了指定的maxWidth,則文字會按照最大的寬度來繪製,那麼文字之間的間距就將減少,文字可能被壓縮。
  • fillText(text,x,y,maxWidth?),同strokeText一樣,只不過,是用填充的形式繪製文字,其引數含義一樣。
  • measureText(text),在當前的文字樣式下,測量繪製文字text會佔據的寬度值,返回一個物件,這個物件有一個width屬性。主要注意的是,必須先設定文字樣式,再來測量才是準確的。

跟文字直接相關的屬性設定,如下,

  • font,同css中含義一樣,可以指定文字的字型大小,字型集,字型樣式等。但在canvas中,line-height被強制設定為normal,會忽略其他設定的值。
  • textAlign,設定文字的水平對其方式,可選值有:leftrightcenterstartend。預設值是start。各個含義參見textAlign取值
  • textBaseline,設定文字的垂直對齊方式,可選值有:tophangingmiddlealphabeticideographicbottom。預設值是alphabetic。各個含義參見textBaseline取值

當然了,還有一些其他的屬性也會影響到文字最終繪製出來的效果,比如給當前ctx新增陰影效果,或者設定fillStyle的樣式可以是圖片或者漸變等。這些算是全域性的屬性設定,會影響到canvas所有其他的繪製,而不僅僅是文字,所以在這裡,就不詳細討論了。

let textAligns = ['left', 'right', 'center', 'start', 'end']; //textAlign的取值
let colors = ['red', 'blue', 'green', 'orange', 'blueviolet']; //描邊顏色
ctx.font = '18px sans-serif'; //設定font
for (let [index, textAlign] of textAligns.entries()) {
  ctx.save();
  ctx.textAlign = textAlign; // 設定textAlign
  ctx.strokeStyle = colors[index]; //設定描邊顏色
  ctx.strokeText(textAlign, width / 2, 20 + index * 30); //使用描邊繪製文字
  ctx.restore();
}
複製程式碼

canvas核心技術-如何繪製圖片和文字

我們把textAlign的各個屬性全都設定了一遍,看到startleft的效果一樣,endright的效果一樣,這個是因為startend是與當前本地文字開始方向有關的,如果是左到右開始,那麼startleft一樣,而如果是右到左開始,那麼start是與right效果一樣了。

let textBaselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];
let colors = ['red', 'blue', 'green', 'orange', 'blueviolet', 'cyan']; //描邊顏色
ctx.font = '18px sans-serif'; //設定font
for (let [index, textBaseline] of textBaselines.entries()) {
  ctx.save();
  ctx.textBaseline = textBaseline; // 設定textBaseline
  ctx.strokeStyle = colors[index]; //設定描邊顏色
  ctx.strokeText('abj', 10 + index * 50, height / 2); //使用描邊繪製文字
  ctx.restore();
}
複製程式碼

canvas核心技術-如何繪製圖片和文字

我們又把textBaseline的各個值全設定了一遍,看到的效果如上圖。用到最多的應該是topmiddlealphabeticbottom了,其中預設值是alphabetic

measureText在實際業務中也是用到比較多的一個方法了,這個方法可以測量出在當前設定的文字樣式下,繪製指定的text會佔據的寬度。特別是在繪製表格資料,或者一些分析圖時,需要繪製說明提示性文字,但是又想根據當前滑鼠位置來決定文字繪製的座標,以免超出canvas可見區域。這個方法使用比較簡單,會返回一個帶有width屬性的物件,這個width屬性值就是測量出來的結果。在canvas沒有測量文字高度的方法,然而,在實際時,常常會以W字母測量出來的寬度值加上一點點,就可以大致認為是當前文字的高度值了。

ctx.font = '18px sans-serif'; //設定font,一定得先設定font屬性,才能測量準確
let textWidth = ctx.measureText('W').width;
let textHeight = textWidth + textWidth / 6;
console.log(`當前文字W的寬度:${textWidth}`);
console.log(`當前文字W的高度:${textHeight}`);
複製程式碼

小結

這篇文章主要是學習了canvas中如何使用drawImage來繪製圖片,以及如何使用getImageDataputImageData來對影像畫素值做處理,比如常見的圖片灰度處理,或者反相顏色等。也回顧了在canvas中繪製文字的一些相關方法和屬性,這些知識在css中比較類似,理解起來也比較容易和簡單。

相關文章