小程序 canvas 2d 新接口 绘制带小程序码的海报图

songfens 2020-03-28

截止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。

最终效果

小程序 canvas 2d 新接口 绘制带小程序码的海报图

下面就让我们抽丝剥茧,细细剖析。

以下代码均在官方开发者工具下编写


步骤:

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再获取小程序二维码。其实我也还没做到这。: )

对文中有不理解的地方,欢迎留言探讨。创作不易,转载请留下出处。

Promise

相关推荐