软件项目技术点(9)——如何将gif动态图拆分绘制
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 }