小程序 canvas 使用

我们的小程序中有一个功能,会根据用户信息生成图片,并可以保存到相册。第一次是使用前端发起请求,后端生成图片上传到阿里云后给前端返回图片地址,但是上线后发现图片出来的很慢,平均要6~7秒。第二次是使用小程序的 canvas api 在前端绘制图片,可达到平局在3秒左右,但这个过程也遇到了一些问题,描述如下。

多屏幕适配问题

设计稿是基于 375 * 667 尺寸的,如果是用 image 显示图片,我们可以通过计算或使用适配的单位来进行多屏适配。但是 canvas 中不能使用适配的单位,但是可以获取到屏幕的宽度,自己计算每一个尺寸应该有多大,比较麻烦。更好的做法是在绘制前使用 canvasContext.scale api 进行缩放,后面直接使用设计稿中固定的尺寸绘制即可。

绘制圆形用户微信头像

canvas 中绘制图片时,需要先使用 wx.downloadFile api 下载文件到本地,再使用返回的临时路径绘制图片。注意下载的图片的域名需要添加在 downloadFile 合法域名中,否则 iOS 不显示图片。之后使用 canvasContext.clip api 将图片切成圆形。

居中显示不同样式的文字

图片的中间需要显示“第 xxx 天”,整行文字需要居中显示,但数字的字体比较大,因此无法直接使用 canvasContext.setTextAlign api 进行文字居中设置。可使用 canvasContext.measureText
api 先计算出数字显示需要的宽度,然后将 “第 ” 和 “ 天” 放在计算后的位置居右和距左对齐。

canvas 属于原生组件,层级是最高的,页面中如果有需要弹出 Modal 的地方,是无法显示在 canvas 上面的。经试验,如果使用两个 canvas,是可以达到一个 canvas 在另一个 canvas 上面的,但是因为我们的项目中弹出的 Modal 有一个按钮,因此无法使用这种方式。我们采用了在屏幕的外面绘制图片,绝对定位,top 为负值,然后下载到本地,用 image 标签引用本地路径来显示图片。

完整示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// 图片宽度为 330,该值为图片水平居中的位置
const midpoint = 330 / 2;
Page({
...
onLoad(this: PageData, options: Options) {
const canvasContext = wx.createCanvasContext('clockImage');
this.canvasContext = canvasContext;
// 多屏幕适配问题
canvasContext.scale(
app.globalData.windowWidth / 375,
app.globalData.windowWidth / 375,
);
canvasContext.drawImage(`../../images/clock_bg.png`, 0, 0, 330, 400);
canvasContext.setTextAlign('center');
canvasContext.setFillStyle('white');
canvasContext.setFontSize(16);
canvasContext.fillText(`我在这里学习`, midpoint, 124);
canvasContext.fillText(`坚持学习《占位占位占位占位》`, midpoint, 150);
canvasContext.setFillStyle('rgba(255, 255, 255, 0.15)');
canvasContext.fillRect(15, 258, 300, 50);
canvasContext.setFillStyle('#8FADFF');
canvasContext.setFontSize(14);
canvasContext.setTextAlign('left');
canvasContext.setFillStyle('#fff');
canvasContext.fillText('The truth is that there is the pinnacle of', 22, 278);
canvasContext.fillText('justice in practice is to use the truth.', 22, 298);
canvasContext.setTextAlign('center');
canvasContext.draw();
// 用户头像
this.drawAvatar(wxInfo);
// 二维码
this.drawQrcode();
// 天数
this.drawDays();
},
drawAvatar(this: PageData, wxInfo: WxInfo) {
const canvasContext = this.canvasContext;
wx.downloadFile({
url: 'path to avatar',
success: res => {
// 绘制圆形用户微信头像
canvasContext.beginPath();
canvasContext.arc(midpoint, 40, 24, 0, 2 * Math.PI);
canvasContext.clip();
canvasContext.drawImage(res.tempFilePath, midpoint - 24, 16, 48, 48);
canvasContext.setFontSize(12);
canvasContext.draw(true, () => {
this.setData(
{
showAvatar: true,
canSave: this.data.showDays && this.data.showQrcode,
},
() => {
this.saveTempImage();
},
);
});
},
fail: () => {
this.drawAvatar();
},
});
},
drawQrcode(this: PageData) {
const canvasContext = this.canvasContext;
wx.downloadFile({
url: 'https://res.wx.qq.com/mpres/htmledition/images/mp_qrcode3a7b38.gif',
success: res => {
canvasContext.drawImage(res.tempFilePath, 68, 328, 56, 56);
canvasContext.draw(true, () => {
this.setData(
{
showQrcode: true,
canSave: this.data.showAvatar && this.data.showDays,
},
() => {
this.saveTempImage();
},
);
});
},
fail: () => {
this.drawQrcode();
},
});
},
drawDays(this: PageData) {
const canvasContext = this.canvasContext;
// 居中显示不同样式的文字
canvasContext.setFontSize(48);
const days = '99';
const dayWidth = canvasContext.measureText(days).width / 2;
canvasContext.setFillStyle('#8FADFF');
canvasContext.setFontSize(16);
canvasContext.setTextAlign('right');
canvasContext.fillText('第 ', midpoint - dayWidth, 210);
canvasContext.setFillStyle('#FF9F65');
canvasContext.setFontSize(48);
canvasContext.setTextAlign('center');
canvasContext.fillText(days, midpoint, 210);
canvasContext.setFillStyle('#8FADFF');
canvasContext.setFontSize(16);
canvasContext.setTextAlign('left');
canvasContext.fillText(' 天', midpoint + dayWidth, 210);
canvasContext.draw(true, () => {
this.setData(
{
showDays: true,
canSave: this.data.showAvatar && this.data.showQrcode,
},
() => {
this.saveTempImage();
},
);
});
},
saveTempImage(this: PageData) {
// 下载图片
if (this.data.canSave && !this.data.saving) {
this.setData({
saving: true,
});
wx.canvasToTempFilePath({
canvasId: 'clockImage',
success: res => {
// image 组件的 src 值为 clockImagePath
this.setData({
clockImagePath: res.tempFilePath,
});
this.setData({
saving: false,
});
},
fail: () => {
this.setData({
saving: false,
});
},
});
}
},
});