小程序canvas的那些事
发布于 4 年前 作者 leichao 3094 次浏览 来自 分享

背景

业务场景需要在__小程序__内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二:

  • 服务端合成:直接返回给前端图片URL
  • 客户端合成:客户端利用canvas绘制

在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器__CPU__浪费。
下面主要谈谈**客户端(canvas)**合成的过程。

实现思路

  1. __小程序端__发起请求,获取需动态展示的数据;
  2. 利用__canvas绘制画布__;
  3. __导出图片__保存到相册。

小技巧&那些坑

理想很丰满,现实很骨感。

实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。

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还有一些不足。所以应注意两点:

  1. 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等);
  2. 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。

ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。

8 回复

客户端合成尺寸过大会闪退

我们也是base64绘制的,二进制字节流前端太重了

我的小程序用的也是wxml2canvas

导出清晰图片」这块因为pixelRatio=3导致输出的图片扩大了3*3倍,数据也都变化了。请问有没有办法,可以保证像素数据不变?

烦请问如何先发布小程序

transform相关样式如何操作绘制?类似transform:rotate(135deg) scale(1.2);

绘制base64图片那里,有没有试过用微信自带的wx.base64ToArrayBuffer转成ArrayBuffer然后用wx.canvasPutImageData绘制到画布上?

回到顶部