小程序 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標籤以及保存按鈕:

<view style="width:100%;height:{{canvasHeight}}px;">
    <canvas type="2d" id="canvasBox"></canvas>
</view>

 

 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章