小程序canvas的那些事
背景
业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二:
服务端合成:直接返回给前端图片URL
客户端合成:客户端利用canvas绘制
在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。
下面主要谈谈**客户端(canvas)**合成的过程。
实现思路
小程序端发起请求,获取需动态展示的数据;
利用canvas绘制画布;
导出图片保存到相册。
小技巧&那些坑
理想很丰满,现实很骨感。
实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。
promise化
画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。
[代码] let promise1 = new Promise((resolve, reject) => {
this.getData(resolve, reject)
});
let promise2 = new Promise((resolve, reject) => {
this.getSystemInfo(resolve, reject)
});
Promise.all([promise1, promise2]).then(() => {
this.drawCanvas()
}).catch(err => {
console.log(err)
});
[代码]
自适应
1、为了在各个机型下保持大小自适应,需要计算出缩放比:
[代码] getSystemInfo(resolve, reject) {
try {
const res = wx.getSystemInfoSync()
//缓存系统信息
systemInfo = res
//这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375
zoom = res.windowWidth / 750 * 1
resolve()
} catch (e) {
// Do something when catch error
reject("获取机型失败")
}
}
[代码]
2、绘制时进行按缩放比进行缩放,如:
[代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom)
[代码]
绘制网络图片
经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制:
[代码]wx.downloadFile({
url: imgUrl,
success: res => {
if (res.statusCode === 200) {
ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom)
}
}
})
[代码]
绘制base64图片
因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。
解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持):
1、调用fileManager.writeFile存储base64到本地;
2、绘制本地图片。
实现代码如下:
[代码]// 先获得一个文件实例
fileManager = wx.getFileSystemManager()
// 把图片base64格式转存到本地,用于canvas绘制
fileManager.writeFile({
filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`,
data: self.data.qrcode,
encoding: 'base64',
success: () => {
//此处需先调用wx.getImageInfo,方可绘制成功
wx.getImageInfo({
src: `${wx.env.USER_DATA_PATH}/qrcode.png`,
success: () => {
//绘制二维码
ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom)
ctx.draw()
}
})
}
})
[代码]
保存到本地相册
wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。
[代码]canvas2Img(e) {
wx.getSetting({
success(res) {
if (res.authSetting['scope.writePhotosAlbum'] === undefined) {
//呼起授权界面
wx.authorize({
scope: 'scope.writePhotosAlbum',
success() {
save()
}
})
} else if (res.authSetting['scope.writePhotosAlbum'] === false) {
//引导拒绝过授权的用户授权
wx.showModal({
title: '温馨提示',
content: '需要您授权保存到相册的权限',
success: res => {
if (res.confirm) {
wx.openSetting({
success(res) {
if (res.authSetting['scope.writePhotosAlbum']) {
save()
}
}
})
}
}
})
} else {
save()
}
}
})
function save() {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 562*zoom,
height: 792*zoom,
destWidth: 562*zoom*systemInfo.pixelRatio,
destHeight: 792*zoom*systemInfo.pixelRatio,
fileType: 'png',
quality: 1,
canvasId: 'shareImg',
success: res => {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
wx.showModal({
content: '保存成功',
showCancel: false,
confirmText: '确认'
})
},
fail: res => {
if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') {
wx.showModal({
content: '保存到相册失败',
showCancel: false
})
}
}
})
},
fail: () => {
wx.showModal({
content: '保存到相册失败',
showCancel: false
})
}
})
}
[代码]
导出清晰图片
wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。
原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。
这里应该乘以设备像素比,实现如下:
[代码]wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 562*zoom,
height: 792*zoom,
destWidth: 562*zoom*systemInfo.pixelRatio,
destHeight: 792*zoom*systemInfo.pixelRatio,
fileType: 'png',
quality: 1,
canvasId: 'shareImg'
)}
[代码]
特殊字体的绘制
研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。
实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。
[代码]drawNum(num, x, y, w, h) {
return new Promise(function (resolve, reject) {
//这里存储0-9的图片CDN链接
let numMap = []
wx.downloadFile({
url: numMap[num],
success: res => {
if (res.statusCode === 200) {
ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom)
resolve()
}
},
fail: () => {
reject()
}
})
})
}
[代码]
安卓机型图片绘制锯齿化问题
测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。
总结
个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点:
提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等);
若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。
ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。