使用 html2canvas 和 gif.js 实现表情符号制作
使用html2canvas和gif.js实现表情包制作
记录一下过年前做的一个需求:
用户输入名字,然后生成表情包。
上面的“XX给您”(静态)是根据输入的名字变化,而下面的小人(动态的)只能切换男版或者女版。
设计师给到我的是两张动图,我的想法是取到两张动图的每一帧(随便搜一个网站把动图传上去就能取到了),用户输入文字后,将文字与每一帧图片合成转换为新的帧,然后再将新的帧全部连起来形成动图。
先大概了解一下html2canvas和gif.js的用法,这两个库都很简洁,可以很快地上手。
html2canvas
很简单,只要将需要转换的dom元素传入html2canvas(element, options)方法中即可。
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});
options中比较常用到的属性是backgroundColor:画布背景颜色。
gif.js
gif.js可以直接将image/canvas,或者ctx中获得帧,得到gif
它有四个核心方法:
//1.创建GIF实例
var gif = new GIF({
workers: 2, //启用两个worker
quality: 10, //图像质量
workerScript: './gif.worker.js' //worker
transparent:'#fff' //这个在背景图片是透明时会用到
})
//2.向gif中添加帧图片,参数可以是image、canvas、ctx
gif.addFrame(frame, {
copy: true, //复制像素数据
delay: 80, //帧停留时间80ms
//还有其他选项如width、height,用来设置图片的宽高
})
//3.转换完成后的回调
gif.on('finished',function(blob) {
//do something
})
//4.启动实例(开始生成gif)
gif.render();
1.将文字与图片拼凑在一起
用户输入名字,点击预览后,将文字与每一帧图片拼凑在一起形成新的帧图片。
要获取每一帧,就需要将每一帧放到页面上,然后通过html2canvas转换为图片(或者canvas或者其上下文)
在这里我将文字与第一帧图片放到页面的临时dom结构中,将该dom结构转为ctx后,再将图片替换为下一帧,以此类推。
这个临时dom结构如下:
<div v-cloak id="temp">
<div id="temp-text" ref="tempText" style="opacity: 0;">{{name}}给您</div>
</div>
由于这部分对于用户来说是不可见的,于是设置了绝对定位,并且设置z-index为-1,避免对页面的原本布局产生影响。
这里不可以用opacity、visibility或display来隐藏元素,因为会导致html2canvas绘制出来的是空白的。
2.创建Gif实例
var running = false; //标记worker的状态
var gifBlob = ''; //存放gif的blob格式
var gifBase = ''; //存放gif的base64格式
var gif = new GIF({
workers: 2,
quality: 10,
workerScript: './js/gif.worker.js',
transparent: '#fff'
});
//转换成功后的操作,主要是做预览,并方便用户进行保存和转发。
gif.on('finished', function (blob) {
//生成图片
gifBlob = blob;
//将图片转为base64并展示到页面中
blobToDataURI(gifBlob,(result) => {
gifBase = result;
document.getElementById('pic').src = gifBase;
document.getElementById('gifHidden').src = gifBase;
})
running = false;
})
3.将拼凑后的dom结构转为canvas,并添加到Gif实例中,转为gif
/*
* imgList:原始动图(未拼凑时)的序列,需要根据选择的性别传入不同的值
* temp:存放用于转换为canvas的临时dom节点
* onerror:错误处理函数
*/
function loadGif(imgList, temp, onerror) {
if(running == true) {onerror();return;} //防止用户多次点击,如果worker正在运行,通过onerror方法提示用户正在生成中。
running = true;
let imgImage = new Image();
let count = 1;
function imageToCanvas(img,src) { //img为序列帧
temp.appendChild(img);//将帧插入临时dom
img.crossorigin = "anonymous"
img.src = src;
img.onload = function() { //图片加载成功事件
html2canvas(document.getElementById('temp'),{backgroundColor: 'transparent'}).then(canvas => {
canvas.style.backgroundColor = "transparent"
let ctx = canvas.getContext('2d', { //获取二维渲染上下文
alpha: true
})
gif.addFrame(ctx, { //这里我用了上下文,因为直接添加画布,会导致生成的图片有锯齿。
copy: true,
delay: 80,
width: Math.floor(canvas.width),
height: Math.floor(canvas.height)
});
//移除图片
img.remove();
count ++;
if(count === 6) { //一共有5帧,count等于6时开始转换
gif.render();
}else {
imgImage = null;
imgImage = new Image();
//开始转换下一帧
imageToCanvas(imgImage, imgList[count - 1])
}
})
}
}
imageToCanvas(imgImage, imgList[0])
}
以上就是整个的实现过程。
再记录下过程中遇到的一些问题
1.首先是一开始生成的动图背景是白色的,但是我原图都是透明底色。那问题肯定是出在从html转canvas或者从canvas转gif的过程。
然后我将html转canvas后的每一张图片都显示出来,发现转换后的图片背景颜色都是白色,问题应该就是出在这里。查了文档之后,
尝试在options中加入backgroundColor: 'transparent',即:
html2canvas(document.getElementById('temp'),{backgroundColor: 'transparent'}).then(...)
设置完了之后再试一次,发现背景颜色变成了黑色。。。
再查看每一帧图片,都是透明底色的,那就是在转gif时出了问题。
再去看了gif.js的文档,发现有个参数:background、transparent
background在文档中的解释是:background color where source image is transparent。看到这个之后我激动了一下,那我把这个属性设置成transparent不就可以了?嗯。。显然还是不行,背景还是黑色。
于是我看了看另一个属性。transparent在文档中的解释是:透明的16进制色值,看完后一脸懵逼,这几个词都懂,但是组合在一起,就不知道这个属性有什么鬼用,但是隐约觉得跟我这次要解决的问题有关。所以我又修改了代码
成功!根据这个结果,我猜测了一下这个属性的意思应该是将底色为该值的颜色变为透明。因为当我不设置背景色时,为白色,然后添加transparent: '#fff'后,底色就是透明了,也就是将白色变为了透明。
2.解决了背景颜色透明的问题,又出现了生成的图片尺寸在ios13系统下不一样的问题。捣鼓了很久,一直以为是浏览器兼容问题。
最后发现ios13的dpr=3,即时生成的图片宽高与dpr=2一致,但内容不全。那么就需要将图片尺寸等比例放大。通过将宽高(根据dpr换算)传入构造函数解决:
var gif = new GIF({
workers: 2,
quality: 10,
workerScript: './js/gif.worker.js',
transparent: '#fff'
/*
原本的width是7.9rem和9.4rem,为了兼容dpr=3的设备,进行了换算
*/
width: 7.9 * window.rem * window.devicePixelRatio / 2,
height: 9.4 * window.rem * window.devicePixelRatio / 2,
});
3.最后一个问题,还是出现在ios13系统下,生成的动图是斜的。
我将每一帧的图片都显示到页面上 ,发现每一帧都是正常的,所以应该是从帧图片转换到gif的过程中出问题的。
问题在于在执行git.addFrame时,因为传的是canvas上下文,在gif.js源码中,会默认将每一帧的 宽高设置为构造函数传入的宽高,按道理从构造函数中传入的宽高已经经过换算,所以我不清楚内部是做了什么转换,导致图片尺寸出了问题。
最后的解决方案:修改了源码,从addFrame的options参数中传入宽高(画布的宽高),作为帧序列图片的宽高。问题得到解决。因为每一帧的大小都一样,所以从构造函数中传入的宽高也已经没用了,也就是问题3与问题2最后只需要这一处修改就可以解决:
gif.addFrame(ctx, {
copy: true,
delay: 80,
width: Math.floor(canvas.width),
height: Math.floor(canvas.height)
});
上一篇: 剖析月饼代理原则,快速见效