小程序 canvas 2d 新接口 绘制带小程序码的海报图
截止2020.3.26,小程序官方文档中,有两种绘制方式:Canvas 2D、webGL
文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html
而开发者工具中,官方推荐使用性能更好的2d模式,用法如下所示:
<canvas type="2d" id="myCanvas"></canvas>
但是网上大多数教程都是使用旧的接口,如:
<canvas canvas-id="canvasBox"></canvas>
本着学习和为后来人踩坑的目的,我们来尝试一下新接口,迎接未知的挑战 :)
需要注意的是:官方文档中CanvasContext的一些函数,在Canvas 2d模式下已经失效,这点,官方用了一句话做了描述:
canvas 组件的绘图上下文。CanvasContext 是旧版的接口, 新版 Canvas 2D 接口与 Web 一致。
出处:https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html
举个例子,比如设置填充色:
// 旧方式: ctx.setFillStyle(‘red‘) // 在Canvas 2d 下会报错 // 新方式: ctx.fillStyle = "red";
所以针对新接口的方法,可以参考html5的canvas api。
最终效果

下面就让我们抽丝剥茧,细细剖析。
以下代码均在官方开发者工具下编写
步骤:
wxml文件中,加入canvas标签以及保存按钮:
<canvas type="2d" id="canvasBox"></canvas>
js文件中:
1.设置数据:数据就相当于所有交通的枢纽
data: {
// 数据区,从服务端拿到的数据
name: "作者 Alpiny", // 姓名
phone: "13988887777", // 电话
posterUrl: "https://desk-fd.zol-img.com.cn/t_s1024x1024c5/g5/M00/00/0A/ChMkJlmfw7CIBpnCAAD3xQrT42EAAf9sgAH1ycAAPfd598.jpg", // 海报地址
photoUrl: "https://img2.woyaogexing.com/2020/03/27/3698eb92b78246e99d859f97f4227936!400x400.jpeg", // 头像地址
qrcodeUrl: "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=144549786,228270254&fm=26&gp=0.jpg", // 小程序二维码
// 设置区,针对部件的数据设置
photoDiam: 50, // 头像直径
qrcodeDiam: 80, // 小程序码直径
infoSpace: 13, // 底部信息的间距
saveImageWidth: 500, // 保存的图像宽度
bottomInfoHeight: 100, // 底部信息区高度
tips: "微信扫码或长按了解更多", // 提示语
// 缓冲区,无需手动设定
canvasWidth: 0, // 画布宽
canvasHeight: 0, // 画布高
canvasDom: null, // 画布dom对象
canvas:null, // 画布的节点
ctx: null, // 画布的上下文
dpr: 1, // 设备的像素比
posterHeight: 0, // 海报高
},这里数据分了三类:数据区是后端传送来的数据、设置区是可以定制画面的数据、缓冲区是用来暂存一些临时的数据,无需设置。
2.onReady 钩子中,执行 drawImage 函数
onReady: function () {
this.drawImage()
},放到 onReady里的目的,是为了一进入页面就直接渲染画面。
3.创建drawImage函数,用来选择canvas节点并准备绘图:
// 查询节点信息,并准备绘制图像
drawImage() {
const query = wx.createSelectorQuery() // 创建一个dom元素节点查询器
query.select(‘#canvasBox‘) // 选择我们的canvas节点
.fields({ // 需要获取的节点相关信息
node: true, // 是否返回节点对应的 Node 实例
size: true // 是否返回节点尺寸(width height)
}).exec((res) => { // 执行针对这个节点的所有请求,exec((res) => {alpiny}) 这里是一个回调函数
const dom = res[0] // 因为页面只存在一个画布,所以我们要的dom数据就是 res数组的第一个元素
const canvas = dom.node // canvas就是我们要操作的画布节点
const ctx = canvas.getContext(‘2d‘) // 以2d模式,获取一个画布节点的上下文对象
const dpr = wx.getSystemInfoSync().pixelRatio // 获取设备的像素比,未来整体画布根据像素比扩大
this.setData({
canvasDom: dom, // 把canvas的dom对象放到全局
canvas: canvas, // 把canvas的节点放到全局
ctx: ctx, // 把canvas 2d的上下文放到全局
dpr: dpr // 屏幕像素比
},function(){
this.drawing() // 开始绘图
})
})
// 对以上设置不明白的朋友
// 可以参考 createSelectorQuery 的api地址
// https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html
},看,上面的代码第20行执行了drawing函数,drawimg 函数里制定了绘制的整体流程,下面我们来创建它。
4.创建 drawimg 函数
// 绘制画面
drawing() {
const that = this;
wx.showLoading({title:"生成中"}) // 显示loading
that.drawPoster() // 绘制海报
.then(function () { // 这里用同步阻塞一下,因为需要先拿到海报的高度计算整体画布的高度
that.drawInfoBg() // 绘制底部白色背景
that.drawPhoto() // 绘制头像
that.drawQrcode() // 绘制小程序码
that.drawText() // 绘制文字
wx.hideLoading() // 隐藏loading
})
},这其中要注意的是,为了让最终生成的图片自适应高,所以要提前拿到海报的高度来设置画布,所以,第一步绘制海报是阻塞运行的(采用了Promise来完成阻塞)。
5.创建 drawPoster 函数,绘制海报
// 绘制海报
drawPoster() {
const that = this
return new Promise(function (resolve, reject) {
let poster = that.data.canvas.createImage(); // 创建一个图片对象
poster.src = that.data.posterUrl // 图片对象地址赋值
poster.onload = () => {
that.computeCanvasSize(poster.width, poster.height) // 计算画布尺寸
.then(function (res) {
that.data.ctx.drawImage(poster, 0, 0, poster.width, poster.height, 0, 0, res.width, res.height);
resolve()
})
}
})
},而drawPoster大约第7行,又进行了阻塞,是因为,我们要用拿到的海报数据先设置一下画布,否则直接绘图会导致失败。
6.创建 computeCanvasSize 函数,用来计算画布尺寸
// 计算画布尺寸
computeCanvasSize(imgWidth, imgHeight){
const that = this
return new Promise(function (resolve, reject) {
var canvasWidth = that.data.canvasDom.width // 获取画布宽度
var posterHeight = canvasWidth * (imgHeight / imgWidth) // 计算海报高度
var canvasHeight = posterHeight + that.data.bottomInfoHeight // 计算画布高度 海报高度+底部高度
that.setData({
canvasWidth: canvasWidth, // 设置画布容器宽
canvasHeight: canvasHeight, // 设置画布容器高
posterHeight: posterHeight // 设置海报高
}, () => { // 设置成功后再返回
that.data.canvas.width = that.data.canvasWidth * that.data.dpr // 设置画布宽
that.data.canvas.height = canvasHeight * that.data.dpr // 设置画布高
that.data.ctx.scale(that.data.dpr, that.data.dpr) // 根据像素比放大
setTimeout(function(){
resolve({ "width": canvasWidth, "height": posterHeight }) // 返回成功
},1200)
})
})
},7.创建第4步所需的其他几个函数:drawInfoBg(绘制底部白色背景)、drawPhoto(绘制头像)、drawQrcode(绘制二维码)、drawText(绘制文本)、alpiny(作者本人)
// 绘制白色背景
// 注意:这里使用save 和 restore 来模拟图层的概念,防止污染
drawInfoBg() {
this.data.ctx.save();
this.data.ctx.fillStyle = "#ffffff"; // 设置画布背景色
this.data.ctx.fillRect(0, this.data.canvasHeight - this.data.bottomInfoHeight, this.data.canvasWidth, this.data.bottomInfoHeight); // 填充整个画布
this.data.ctx.restore();
},
// 绘制头像
drawPhoto() {
let photoDiam = this.data.photoDiam // 头像路径
let photo = this.data.canvas.createImage(); // 创建一个图片对象
photo.src = this.data.photoUrl // 图片对象地址赋值
photo.onload = () => {
let radius = photoDiam / 2 // 圆形头像的半径
let x = this.data.infoSpace // 左上角相对X轴的距离
let y = this.data.canvasHeight - photoDiam - 35 // 左上角相对Y轴的距离 :整体高度 - 头像直径 - 微调
this.data.ctx.save()
this.data.ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI) // arc方法画曲线,按照中心点坐标计算,所以要加上半径
this.data.ctx.clip()
this.data.ctx.drawImage(photo, 0, 0, photo.width, photo.height, x, y, photoDiam, photoDiam) // 详见 drawImage 用法
this.data.ctx.restore();
}
},
// 绘制小程序码
drawQrcode() {
let diam = this.data.qrcodeDiam // 小程序码直径
let qrcode = this.data.canvas.createImage(); // 创建一个图片对象
qrcode.src = this.data.qrcodeUrl // 图片对象地址赋值
qrcode.onload = () => {
let radius = diam / 2 // 半径,alpiny敲碎了键盘
let x = this.data.canvasWidth - this.data.infoSpace - diam // 左上角相对X轴的距离:画布宽 - 间隔 - 直径
let y = this.data.canvasHeight - this.data.infoSpace - diam + 5 // 左上角相对Y轴的距离 :画布高 - 间隔 - 直径 + 微调
this.data.ctx.save()
this.data.ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI) // arc方法画曲线,按照中心点坐标计算,所以要加上半径
this.data.ctx.clip()
this.data.ctx.drawImage(qrcode, 0, 0, qrcode.width, qrcode.height, x, y, diam, diam) // 详见 drawImage 用法
this.data.ctx.restore();
}
},
// 绘制文字
drawText() {
const infoSpace = this.data.infoSpace // 下面数据间距
const photoDiam = this.data.photoDiam // 圆形头像的直径
this.data.ctx.save();
this.data.ctx.font = "14px Arial"; // 设置字体大小
this.data.ctx.fillStyle = "#333333"; // 设置文字颜色
// 姓名(距左:间距 + 头像直径 + 间距)(距下:总高 - 间距 - 文字高 - 头像直径 + 下移一点 )
this.data.ctx.fillText(this.data.name, infoSpace * 2 + photoDiam, this.data.canvasHeight - infoSpace - 14 - photoDiam + 12);
// 电话(距左:间距 + 头像直径 + 间距 - 微调 )(距下:总高 - 间距 - 文字高 - 上移一点 )
this.data.ctx.fillText(this.data.phone, infoSpace * 2 + photoDiam - 2, this.data.canvasHeight - infoSpace - 14 - 16);
// 提示语(距左:间距 )(距下:总高 - 间距 )
this.data.ctx.fillText(this.data.tips, infoSpace, this.data.canvasHeight - infoSpace);
this.data.ctx.restore();
},
到此,在开发者工具中,你应该可以预览到画面啦~!
至于保存图片的部分,代码我就不贴了。留一些给大家去思考、探索,学无止境。
至于小程序码图片的获取,不在本文范围内,大致思路是 后端拿着 appid和key 去微信 api 获取 token,然后拿着token再获取小程序二维码。其实我也还没做到这。: )
对文中有不理解的地方,欢迎留言探讨。创作不易,转载请留下出处。