小程序使用canvas生成一张带二维码的海报

403 阅读4分钟

这个需求

做了一个小程序,要将一个商品的信息生成一张海报,并且要把这张海报保存到相册。

刚接到这个任务的时候,还是蛮兴奋的,毕竟以前没有做过,又可以搞搞“新”技术了。

技术要点一:

保存相册功能倒是简单,因为小程序开发文档里已经有了非常详细的介绍,只是调用一个官方API:wx.saveImageToPhotosAlbum (如果使用的是uniapp,那把wx改成uni就好了)

但是,需要注意的是你保存的图片,如果的网络图片(即通过接口请求的图片),那你必须通过wx.getImageInfo方法获取到它的信息,成功回调函数里会返回一个path,将这个path,传给wx.saveImageToPhotosAlbum才行。

wx.getImageInfo({
    src: picUrl,
    success: (res) => {
        if (res.path) {
            wx.saveImageToPhotosAlbum({
                filePath: res.path,
                success: function () {
                  wx.showToast({icon: 'none', title: '保存成功'});
                },
                fail: function (err) {
                  wx.showToast({icon: 'none', title: err});
                },
            });
        } else {
          wx.showToast({icon: 'none', title: '下载失败'});
        }
    }
})

嗯,一切准备就绪,感觉可以上线了......但是,你可能还是太年轻了。

不信,你用手机测试一下——保存成功是成功了,可是,当你打开手机相册查看的时候,你会发现,你相册里最后居然是一张空白的照片,惊不惊喜?意不意外?

我当时反正是挺意外的,为什么呢?

因为,没有认真看文档,里面有一句非常关键的话——网络图片需先配置 download 域名才能生效。

是的,必须到小程序管理后台把这类图片的域名添加到合法域名列表......

嗯,就是这里。

这~就大功告成了!

不过,别高兴得太早,到这里可能只是完成了1/10 而已,还有很多工作要做,因为你的生成的是一张关于产品的海报,海报的信息可不只是一张产品图片。

如何把很多动态信息,组合在一起生成一张图片呢?

真正的技术难点:

一开始,我想到的是html2canvas插件,但是几经尝试后,发现小程序没有dom,貌似无法成功。查了很多资料,也根据他们说的方法一字一句地敲好了代码,可就是不成功......于是,只好放弃。

既然插件没办法用,只好自己写咯,所以很自然地想到了canvas,这里技术本身并不验证,基本上是一些细节问题,所以直接上代码:

createCanvas(){
        const _self = this;
        const ctx = wx.createCanvasContext('shareCanvas', _self);
        // 绘制背景白色
        ctx.fillStyle="#FFFFFF";
        ctx.fillRect(0,0, 640, 840);
        ctx.setFillStyle('#333333');
        ctx.setFontSize(36);
        ctx.fillText('文字', 270, 120);
        // 文字换行显示
        ctx.setFillStyle('#B5B6B7')
        ctx.setFontSize(22);
        const otherName = '假设这是一串很长的文字';
        _self.drawText(ctx, otherName, 150, 770, 22, 290);
        // 二维码绘制
        ctx.drawImage(_self.qrcodeUrlLocal, 90, 210, 460, 460);
        // 头像绘制成圆形
        _self.circleImgTwo(ctx, _self.avatarUrlLocal, 25, 690, 108, 108, 100);
        // 绘制整图
        ctx.draw(false, (()=>{
          setTimeout(() => {
            // 把canvas生成为图片
            _self.tempFileImage()
          },300);
        })())
      }

      // 将头像绘制成圆形
      circleImgTwo(ctx, img, x, y, w, h, r) {
        // 画一个图形
        if (w < 2 * r) r = w / 2;
        if (h < 2 * r) r = h / 2;
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.arcTo(x + w, y, x + w, y + h, r);
        ctx.arcTo(x + w, y + h, x, y + h, r);
        ctx.arcTo(x, y + h, x, y, r);
        ctx.arcTo(x, y, x + w, y, r);
        ctx.closePath();
        ctx.strokeStyle = '#FFFFFF'; // 设置绘制圆形边框的颜色
        ctx.stroke();
        ctx.clip();
        ctx.drawImage(img, x, y, w, h);
      },
      // 文本换行显示
      drawText: function(ctx, str, leftWidth, initHeight, titleHeight, canvasWidth) {
        let lineWidth = 0;
        let lastSubStrIndex = 0; //每次开始截取的字符串的索引
        for (let i = 0; i < str.length; i++) {
          lineWidth += ctx.measureText(str[i]).width;
          if (lineWidth > canvasWidth) {
            ctx.fillText(str.substring(lastSubStrIndex, i), leftWidth, initHeight); //绘制截取部分
            initHeight += 28; //22为 文字大小20 + 2
            lineWidth = 0;
            lastSubStrIndex = i;
            titleHeight += 28;
          }
          if (i == str.length - 1) { //绘制剩余部分
            ctx.fillText(str.substring(lastSubStrIndex, i + 1), leftWidth, initHeight);
          }
        }
        // 标题border-bottom 线距顶部距离
        titleHeight = titleHeight + 10;
        return titleHeight;
      },
      tempFileImage() {
        const _self = this;
        wx.canvasToTempFilePath({
          canvasId: 'shareCanvas',
          success: (res) => {
            // 保存当前绘制图片
            _self.savePic(res.tempFilePath)
          },
          fail: function(err) {
            wx.showToast({
              title: '图片生成失败'
            });
          }
        }, _self)
      },
      savePic(filePath) {
        const _self = this;
        wx.saveImageToPhotosAlbum({
          filePath: filePath,
          success: function() {
            wx.showToast({
              title: '图片保存成功'
            });
          },
          fail: function(e) {
            wx.showToast({
              title: '图片保存失败'
            });
          },
          complete: function(e) {
          }
        });
      }

这里只需要注意一下 drawText()circleImgTwo()就可以了,这是很常用业务逻辑,开发的时候很可能会需要,所以记录一下。特别是ctx.measureText可以计算字体的大小,也就是每个字占用的宽高,用于文字换行的绘制。

对了,别忘了把html代码写上:

<canvas canvas-id="shareCanvas" class="canvas"></canvas>

注意要用canvas-id属性来定义元素。

本来,写到这里呢,基本上已经完成了。

将base64图片绘制到canvas

问题是,生成海报的目的是为了引流,引流就不能没有“码”(小程序码或者二维码),有码其实也没什么,毕竟它也只是一张图片而已,但问题是,这张图片来自后台,而后台返回的是一个base64位的图片。这就没办法使用wx.getImageInfo了。

幸好,官方也提供了方法wx.getFileSystemManager()

const base64Url = '这是一张base64的图片';
const number = Math.random();
const filePath = `${wx.env.USER_DATA_PATH}/pic${number}.png`;
const buffer = wx.base64ToArrayBuffer(base64Url);
wx.getFileSystemManager().writeFile({
	filePath,
	data: buffer,
	encoding: 'base64',
	success: () => {
	  _self.qrcodeUrlLocal = filePath
	}, fail: err => {
	  console.log(err)
	}
})

这里需要强调的是不使用wx.base64ToArrayBuffer也是没问题的,可能是因为官方文档里说基础库2.4.0以后不维护这个接口了吧。总之,这样你的小程序码就变成了本地的图片了,可以画到canvas上去了。

这样就可以生成一张完整的海报了。

请教一个问题

有个延伸的问题一直没有找到答案,如果哪位朋友有解,还望告知啊!

就是制作了圆形头像使用了ctx.clip(),这样在它后面的绘制的元素就必须要在截取框内才能显示了,可是,如果一张海报里需要有多个圆形图的话,那应该如何操作呢?

最后上一张生成后的海报: