AxeSlide軟件項目梳理 canvas繪圖系列知識點整理
背景介紹
我們的軟件支持插入gif圖片,並且展示在軟件里是動態的,例如插入下面這張gif圖。
在軟件里顯示的同樣是這樣的動態效果:
那么這張動態的圖是怎么繪制到canvas上面的呢,如果只是像繪制一張普通圖片用context.drawImage(img,x,y),這樣繪制出來的只是當前顯示到img標簽的一個靜態畫面。
下面介紹我們項目中使用的方法:
1. 解析gif文件信息
安裝Node.js的gify-parse模塊,該模塊用於解析gif文件信息的API。
具體使用和介紹參見: https://www.npmjs.com/package/gify-parse
我們讀取上面那張gif圖到buffer然后用該模塊解析出的結果如下圖:
注意:解析出來的結果有點小問題,寬高的值是顛倒的
利用上面圖1中的gifInfo信息,我們用animated=true判定這張圖確實是gif圖,它是由24張圖組成,每張圖的寬高為384*288
利用上面圖2中的delay這個屬性值,它表示兩張圖變換的間隔時間,在接下來的第3步繪制大圖到canvas中會用到這個屬性。
2. 拼大圖
我們的思路就是把gif中包含的24張圖拼成一張大圖片,拼大圖我們利用canvas,將24張圖挨個繪制到臨時的一個canvas上面,最后將canvas保存成本地png文件。
下面的代碼用來計算我們的畫布tempCanvas的寬高:
1 var tempCavas = <HTMLCanvasElement>document.createElement("canvas"); 2 //canvas元素的寬在大約40000的時候,將無法進行繪圖 3 //設置30000為最大值 4 var shouldWidth = gifInfo.width * gifInfo.images.length; 5 if (shouldWidth > 30000) { 6 tempCavas.width = Math.floor(30000 / gifInfo.width) * gifInfo.width; 7 tempCavas.height = Math.ceil(gifInfo.images.length / Math.floor(30000 / gifInfo.width))*gifInfo.height; 8 }else { 9 tempCavas.width = shouldWidth; 10 tempCavas.height = gifInfo.height; 11 }
gify-parse模塊只解析出來了寬高等一部分有用信息那么,不能得到每張具體的圖片。
我們需要引入gif模塊,https://github.com/liufangfang/gif 從這里下載即可,該模塊很簡單只有一個函數function(gifSrcPath, callBack) {},傳入gif圖片文件路徑和一個回調函數,回調函數接收錯誤信息和每個幀存儲到本地的圖片路徑callBack(null, pathList)。
下面就看我們的回調函數如何利用這個文件列表files:
1)基本思路就是通過createElement("img")創建IMG標簽
2)img.onload之后將圖片繪制到canvas.context上,當然繪制的位置是需要根據當前圖片是gif圖中第幾幀位置去計算的
3)繪制完最后一張后,將canvas轉換成圖片信息保存到本地
cxt.drawImage(tempImage, 0, 0, gifInfo.width, gifInfo.height, startx, starty, gifInfo.width, gifInfo.height);
1 require('gif')(path,(error, files: Array<string>) => { 2 if (error) { 3 Logger.setErrLog(LogCode.image, "文件:File,方法:node_modules-gif,異常信息:" + error); 4 callBack(null); 5 } 6 files.forEach((file, index) => { 7 var targetDir = FileSytem.imageTempDir + id + index + ".jpg"; 8 9 FileSytem.copySync(file, targetDir); 10 11 try 12 { 13 var tempImage = <HTMLImageElement>document.createElement("img"); 14 var tempImageSrc = targetDir; 15 tempImage.id = index.toString(); 16 tempImage.src = tempImageSrc; 17 tempImage.onload = (ev: Event) => { 18 try 19 { //計算該張圖片繪制到canvas上的位置 20 var atWidth = gifInfo.width * Number(tempImage.id); 21 var startx = atWidth % tempCavas.width; 22 var starty = (atWidth / tempCavas.width | 0) * gifInfo.height; 23 24 cxt.drawImage(tempImage, 0, 0, gifInfo.width, gifInfo.height, startx, starty, gifInfo.width, gifInfo.height); 25 FileSytem.remove(tempImageSrc, null); 26 ev.target = null; 27 loadCounter++; 28 if (gifInfo.images.length == loadCounter) { 29 var dataBuffer = new Buffer(tempCavas.toDataURL("image/png").replace(/^data:image\/\w+;base64,/, ""), 'base64'); 30 var dataPath = FileSytem.imageDir + id + ".png"; 31 FileSytem.fileSaveSync(dataPath, dataBuffer); 32 callBack(dataPath, tempCavas.width); 33 tempCavas.width = 0; 34 tempCavas.height = 0; 35 } 36 } 37 catch (e) { 38 Logger.setErrLog(LogCode.image, "文件:File,方法:gifToPng_1,異常信息:" + e); 39 callBack(null); 40 } 41 } 42 } 43 catch (e) { 44 Logger.setErrLog(LogCode.image, "文件:File,方法:gifToPng_2,異常信息:" + e); 45 callBack(null); 46 } 47 }); 48 });
最后我們拼成的一張大圖如下,如果幀數多或者較寬,因為我們設置了最寬30000px 所以就會出現多行的大圖。
3.將大圖繪制到canvas
插入gif到生成大圖的過程已經寫清楚了,那么怎么利用這張大圖來繪制到canvas形成一張動態的效果圖呢?
之前我們提到過插入的任何元素都繼承自commonElement類,Image是繼承commonElement,我們針對gif圖插入的功能專門有一個類GifImage,而它繼承自Image。這個類里面有個最主要的函數:將大圖中的每一部分一張張的循環繪制,具體代碼如下:
1 private drawGif() { 2 if (this.element && this.context) { 3 var lastFrame = this.gifInfo.images[this.currentFrame % this.gifInfo.images.length] 4 var nowTime = this.tempNowTime || Date.now(); 5 if (nowTime- this.lastDrawTime >= lastFrame.delay) {//控制繪制的速度 6 this.currentFrame++; 7 this.lastDrawTime = nowTime; 8 } 9 var frameNum = this.currentFrame % this.gifInfo.images.length;//計算是該繪制第幾張圖 10 11 this.context.save(); 12 this.rotate(); 13 //計算截取大圖某一部分繪制到畫布的其實坐標 14 var atWidth = this.gifInfo.width * frameNum; 15 var startx = atWidth % this.totalWidth; 16 var starty = (atWidth / this.totalWidth | 0) * this.gifInfo.height; 17 this.context.drawImage(this.element, startx, starty, this.gifInfo.width, this.gifInfo.height, this.config.translate.x, this.config.translate.y, this.config.width, this.config.height); 18 this.context.restore(); 19 } 20 }