How does the applet generate posters to share friends

  html5, javascript, Small program

The project needs have been written for some time, but I still want to come back and sum up. First, I want to review and optimize the project. Second, I want to make a record of the pit to avoid similar problems in the future.

Demand

Using WeChat’s powerful social ability to achieve fission through small programs, new users are drawn.
The generated poster is as follows

clipboard.png

Demand analysis

1. The api provided by the official applet can be directly shared and forwarded to WeChat group to open the applet
2. Use the applet to generate posters, save pictures to photo albums, and share them with friends. Users can pay attention to the public number by identifying the two-dimensional code or open the applet to achieve the purpose of fission.

Implementation plan

I. Analysis of How to Realize

I believe everyone should have a similar puzzle, that is, how to draw a poster according to the design of the product. In fact, I didn’t know how to do it at that time. I seriously thought that I had to draw a picture through canvas, so that users can save this picture in the photo album and share it with friends. However, the picture to be drawn has not only words but also numbers, pictures, two-dimensional codes and so on and is alive. How can this be dynamically generated? After careful consideration, it is necessary to draw the text, numbers and background picture onto the canvas bit by bit, thus finally synthesizing a picture through api and exporting it to the mobile phone photo album.

II. Problems to be Solved

1. Dynamic acquisition and drawing of two-dimensional codes (including how to generate small program two-dimensional codes, public number two-dimensional codes, and opening web page two-dimensional codes)
2. How to draw the background map and obtain the picture information
3. Save the finished picture to the local photo album
4. Handle whether the user cancels authorization to save to the album.

III. Implementation Steps

Here, I specifically write down the problems raised above and describe the approximate implementation process.

(1) First, the canvas was created. I set the canvas position to negative to prevent it from being displayed on the page. Because I tried to dynamically display a nd hide the canvas by judging the conditions, there would be problems during drawing. Therefore, this method was adopted and the size of the canvas must be set here.

<canvas canvas-id="myCanvas" style="width: 690px;  height:1085px;  position: fixed;  top: -10000px;"  ></canvas>

(2) after creating the canvas, draw the background map first, because the background map I am in the local, so get < canvas > component canvas-id attribute, throughcreateCanvasContextCreate the canvas’s drawing context CanvasContext. UsedrawImageDraw an image to the canvas. The first parameter is the local address of the image. The latter two parameters are the X-axis and Y-axis of the image re lative to the upper left corner of the canvas. The last two parameters are to set the width and height of the image.

const ctx = wx.createCanvasContext('myCanvas')
 
 ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)

(3) After creating the background map, draw the head portrait, characters and numbers on the background map. viagetImageInfoIn order to obtain the information of the avatar, it is necessary to note that the download domain name must be configured before the acquired network image can take effect, which is specifically configured in the background settings of the applet.

To obtain the head portrait address, first measure the size of the head portrait in the canvas and the coordinates of the x-axis and y-axis. the result[0] here is a picture address I returned by using promise package.

let headImg = new Promise(function (resolve) {
 wx.getImageInfo({
 src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`,
 success: function (res) {
 resolve(res.path)
 },
 fail: function (err) {
 console.log(err)
 wx.showToast({
 Title:' network error please try again',
 icon: 'loading'
 })
 }
 })
 })
 
 Leavatarurl _ width = 60//width of portrait drawn
 Avatarurl_heigth = 60, // height of avatar drawn
 Avatarurl_x = 28, // Position of the head portrait drawn on the canvas
 avatarurl_y = 36;  //The position of the head portrait drawn on the canvas
 
 ctx.save();  //Save the status first so that you can draw the circle before using it.
 ctx.beginPath();  //Start drawing
 //draw a circle first. the first two parameters determine the coordinates of the center of the circle (x,y). the third parameter is the radius of the circle. the fourth parameter is the drawing direction. the default is false, i.e. clockwise
 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
 ctx.clip();  //Draw a circle and cut any shape and size in the original canvas.  Once an area is cut, all subsequent drawings will be limited to the cut area.
 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth);  //Push to Remove Pictures

Here is an example of how to draw text. For example, to draw the following “word”, I need to dynamically obtain the total width of the previous words, so as to set the X-axis coordinates of the “word”. Here I originally wanted to passmeasureTextTo measure the width of the font, but the width value obtained for the first time on iOS side is incorrect. I also raised this issue in WeChat developer community.bug, so I want to use another method to achieve, is to obtain the width of a word under normal circumstances, and then multiplied by the total number of words to obtain the total width, kiss try is ok.

clipboard.png

let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
 ctx.font = 'normal normal 30px sans-serif';
 ctx.setFillStyle('#ffffff')
 Ctx.fillText ('word', allReading, 150);

(4) drawing the public number two-dimensional code is the same as obtaining the head portrait. it returns the network address of the picture through the interface before passing throughgetImageInfoAcquiring public number two-dimensional code picture information

(5) how to draw small program code, the specific official website document also gives infinite generationSmall program code interface, any applet page can be opened through the generated applet, and the two-dimensional code is permanent and effective. The specific application scenario of which applet two-dimensional code interface is called ca n be seen in detail. In other words, the front end can call the applet code returned by the back end interface through the transfer of parameters, and then draw it on the canvas (the same as the drawing head picture and public number two-dimensional code written above)

Ctx.drawImage ('local address of applet code', x-axis, y-axis, width, height)

⑥ Turn canvas into picture and return the picture address after drawing finally

wx.canvasToTempFilePath({
 canvasId: 'myCanvas',
 success: function (res) {
 Candastotempfilepath = res.tempfilepath//the returned picture address is saved to a global variable
 that.setData({
 showShareImg: true
 })
 wx.showToast({
 Title:' drawing succeeded',
 })
 },
 fail: function () {
 wx.showToast({
 Title:' drawing failed',
 })
 },
 complete: function () {
 wx.hideLoading()
 wx.hideToast()
 }
 })

⑦ Save to system photo album; First judge whether the user opens the user authorized photo album and process the results under different conditions. For example, if the user authorizes according to the normal logic, there is no problem. However, if some users click to cancel authorization, what should they do? If not, there will be some problems. Therefore, when the user clicks to cancel the authorization, a pop-up box will prompt him. When he clicks again, he will jump to the settings to guide the user to open the authorization, thus achieving the purpose of saving to the album sharing circle of friends.

//Obtain whether the user opens the user authorized photo album
 if (!  openStatus) {
 wx.openSetting({
 success: (result) => {
 if (result) {
 if (result.authSetting["scope.writePhotosAlbum"] === true) {
 openStatus = true;
 wx.saveImageToPhotosAlbum({
 filePath: canvasToTempFilePath,
 success() {
 that.setData({
 showShareImg: false
 })
 wx.showToast({
 Title:' The picture was saved successfully, go and share it with friends ~',
 icon: 'none',
 duration: 2000
 })
 },
 fail() {
 wx.showToast({
 Title:' save failed',
 icon: 'none'
 })
 }
 })
 }
 }
 },
 fail: () => { },
 complete: () => { }
 });
 } else {
 wx.getSetting({
 success(res) {
 //If not, obtain authorization
 if (!  res.authSetting['scope.writePhotosAlbum']) {
 wx.authorize({
 scope: 'scope.writePhotosAlbum',
 success() {
 openStatus = true
 wx.saveImageToPhotosAlbum({
 filePath: canvasToTempFilePath,
 success() {
 that.setData({
 showShareImg: false
 })
 wx.showToast({
 Title:' The picture was saved successfully, go and share it with friends ~',
 icon: 'none',
 duration: 2000
 })
 },
 fail() {
 wx.showToast({
 Title:' save failed',
 icon: 'none'
 })
 }
 })
 },
 fail() {
 //If the user refuses or does not have authorization, open the authorization window again.
 openStatus = false
 Log ('Please Set Allow Access to Albums')
 wx.showToast({
 Title:' Please set permission to access photo albums',
 icon: 'none'
 })
 }
 })
 } else {
 //If there is, save it directly.
 openStatus = true
 wx.saveImageToPhotosAlbum({
 filePath: canvasToTempFilePath,
 success() {
 that.setData({
 showShareImg: false
 })
 wx.showToast({
 Title:' The picture was saved successfully, go and share it with friends ~',
 icon: 'none',
 duration: 2000
 })
 },
 fail() {
 wx.showToast({
 Title:' save failed',
 icon: 'none'
 })
 }
 })
 }
 },
 fail(err) {
 console.log(err)
 }
 })
 }

Summary

So far all the steps have been implemented. When drawing, I will encounter some data returned from the background by asynchronous request, so I used promise, async and await to package it to ensure that the exported picture information is complete. In the process of drawing, we did encounter some pits. For example, the scale of the pictures exported at the beginning is not right, the width of the text measured by measureText is not right, and the color of the text on the exported pictures may be wrong and poor if drawn many times (possibly due to network reasons). If you also encounter some pits, you can discuss them together and make a record. The complete code is attached below.

import regeneratorRuntime from '../../utils/runtime.js' // 引入模块
const app = getApp(),
  api = require('../../service/http.js');
var ctx = null, // 创建canvas对象
    canvasToTempFilePath = null, // 保存最终生成的导出的图片地址
    openStatus = true; // 声明一个全局变量判断是否授权保存到相册

// 获取微信公众号二维码
  getCode: function () {
    return new Promise(function (resolve, reject) {
      api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => {
        console.log(res, '获取微信公众号二维码')
        if (res.code == 200) {
          console.log(res.content, 'codeUrl')
          resolve(res.content)
        }
      }).catch(err => {
        console.log(err)
      })
    })
  },

  // 生成海报
  async createCanvasImage() {
    let that = this;
    // 点击生成海报数据埋点
    that.setData({
      generateId: '点击生成海报'
    })
    if (!ctx) {
      let codeUrl = await that.getCode()
      wx.showLoading({
        title: '绘制中...'
      })
      let code = new Promise(function (resolve) {
        wx.getImageInfo({
          src: codeUrl,
          success: function (res) {
            resolve(res.path)
          },
          fail: function (err) {
            console.log(err)
            wx.showToast({
              title: '网络错误请重试',
              icon: 'loading'
            })
          }
        })
      })
      let headImg = new Promise(function (resolve) {
        wx.getImageInfo({
          src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`,
          success: function (res) {
            resolve(res.path)
          },
          fail: function (err) {
            console.log(err)
            wx.showToast({
              title: '网络错误请重试',
              icon: 'loading'
            })
          }
        })
      })

      Promise.all([headImg, code]).then(function (result) {
        const ctx = wx.createCanvasContext('myCanvas')
        console.log(ctx, app.globalData.ratio, 'ctx')
        let canvasWidthPx = 690 * app.globalData.ratio,
          canvasHeightPx = 1085 * app.globalData.ratio,
          avatarurl_width = 60, //绘制的头像宽度
          avatarurl_heigth = 60, //绘制的头像高度
          avatarurl_x = 28, //绘制的头像在画布上的位置
          avatarurl_y = 36, //绘制的头像在画布上的位置
          codeurl_width = 80, //绘制的二维码宽度
          codeurl_heigth = 80, //绘制的二维码高度
          codeurl_x = 588, //绘制的二维码在画布上的位置
          codeurl_y = 984, //绘制的二维码在画布上的位置
          wordNumber = that.data.wordNumber, // 获取总阅读字数
          // nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度
          // allReading = ((nameWidth + 375) - 325) * 2 + 380;
          // allReading = nameWidth / app.globalData.ratio + 325;
          allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
        console.log(wordNumber, wordNumber.toString().length, allReading, '获取总阅读字数的宽度')
        ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)
        ctx.save(); // 先保存状态 已便于画完圆再用
        ctx.beginPath(); //开始绘制
        //先画个圆   前两个参数确定了圆心 (x,y) 坐标  第三个参数是圆的半径  四参数是绘图方向  默认是false,即顺时针
        ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
        ctx.clip(); //画了圆 再剪切  原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
        ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片

        ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.setFontSize(28); // 文字字号
        ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字

        ctx.font = 'normal bold 44px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText(wordNumber, 325, 153); // 绘制文字

        ctx.font = 'normal normal 30px sans-serif';
        ctx.setFillStyle('#ffffff')
        ctx.fillText('字', allReading, 150);

        ctx.font = 'normal normal 24px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText('打败了全国', 26, 190); // 绘制文字

        ctx.font = 'normal normal 24px sans-serif';
        ctx.setFillStyle('#faed15'); // 文字颜色
        ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比

        ctx.font = 'normal normal 24px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText('的小朋友', 205, 190); // 绘制孩子百分比

        ctx.font = 'normal bold 32px sans-serif';
        ctx.setFillStyle('#333333'); // 文字颜色
        ctx.fillText(that.data.singIn, 50, 290); // 签到天数

        ctx.fillText(that.data.reading, 280, 290); // 阅读时长
        ctx.fillText(that.data.reading, 508, 290); // 听书时长

        // 书籍阅读结构
        ctx.font = 'normal normal 28px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText(that.data.bookInfo[0].count, 260, 510); 
        ctx.fillText(that.data.bookInfo[1].count, 420, 532); 
        ctx.fillText(that.data.bookInfo[2].count, 520, 594); 
        ctx.fillText(that.data.bookInfo[3].count, 515, 710); 
        ctx.fillText(that.data.bookInfo[4].count, 492, 828); 
        ctx.fillText(that.data.bookInfo[5].count, 348, 858); 
        ctx.fillText(that.data.bookInfo[6].count, 212, 828); 
        ctx.fillText(that.data.bookInfo[7].count, 148, 726); 
        ctx.fillText(that.data.bookInfo[8].count, 158, 600); 

        ctx.font = 'normal normal 18px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText(that.data.bookInfo[0].name, 232, 530); 
        ctx.fillText(that.data.bookInfo[1].name, 394, 552); 
        ctx.fillText(that.data.bookInfo[2].name, 496, 614); 
        ctx.fillText(that.data.bookInfo[3].name, 490, 730); 
        ctx.fillText(that.data.bookInfo[4].name, 466, 850); 
        ctx.fillText(that.data.bookInfo[5].name, 323, 878); 
        ctx.fillText(that.data.bookInfo[6].name, 184, 850); 
        ctx.fillText(that.data.bookInfo[7].name, 117, 746); 
        ctx.fillText(that.data.bookInfo[8].name, 130, 621); 

        ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像
        ctx.draw(false, function () {
          // canvas画布转成图片并返回图片地址
          wx.canvasToTempFilePath({
            canvasId: 'myCanvas',
            success: function (res) {
              canvasToTempFilePath = res.tempFilePath
              that.setData({
                showShareImg: true
              })
              console.log(res.tempFilePath, 'canvasToTempFilePath')
              wx.showToast({
                title: '绘制成功',
              })
            },
            fail: function () {
              wx.showToast({
                title: '绘制失败',
              })
            },
            complete: function () {
              wx.hideLoading()
              wx.hideToast()
            }
          })
        })
      })
    }
  },

  // 保存到系统相册
  saveShareImg: function () {
    let that = this;
    // 数据埋点点击保存学情海报
    that.setData({
      saveId: '保存学情海报'
    })
    // 获取用户是否开启用户授权相册
    if (!openStatus) {
      wx.openSetting({
        success: (result) => {
          if (result) {
            if (result.authSetting["scope.writePhotosAlbum"] === true) {
              openStatus = true;
              wx.saveImageToPhotosAlbum({
                filePath: canvasToTempFilePath,
                success() {
                  that.setData({
                    showShareImg: false
                  })
                  wx.showToast({
                    title: '图片保存成功,快去分享到朋友圈吧~',
                    icon: 'none',
                    duration: 2000
                  })
                },
                fail() {
                  wx.showToast({
                    title: '保存失败',
                    icon: 'none'
                  })
                }
              })
            }
          }
        },
        fail: () => { },
        complete: () => { }
      });
    } else {
      wx.getSetting({
        success(res) {
          // 如果没有则获取授权
          if (!res.authSetting['scope.writePhotosAlbum']) {
            wx.authorize({
              scope: 'scope.writePhotosAlbum',
              success() {
                openStatus = true
                wx.saveImageToPhotosAlbum({
                  filePath: canvasToTempFilePath,
                  success() {
                    that.setData({
                      showShareImg: false
                    })
                    wx.showToast({
                      title: '图片保存成功,快去分享到朋友圈吧~',
                      icon: 'none',
                      duration: 2000
                    })
                  },
                  fail() {
                    wx.showToast({
                      title: '保存失败',
                      icon: 'none'
                    })
                  }
                })
              },
              fail() {
                // 如果用户拒绝过或没有授权,则再次打开授权窗口
                openStatus = false
                console.log('请设置允许访问相册')
                wx.showToast({
                  title: '请设置允许访问相册',
                  icon: 'none'
                })
              }
            })
          } else {
            // 有则直接保存
            openStatus = true
            wx.saveImageToPhotosAlbum({
              filePath: canvasToTempFilePath,
              success() {
                that.setData({
                  showShareImg: false
                })
                wx.showToast({
                  title: '图片保存成功,快去分享到朋友圈吧~',
                  icon: 'none',
                  duration: 2000
                })
              },
              fail() {
                wx.showToast({
                  title: '保存失败',
                  icon: 'none'
                })
              }
            })
          }
        },
        fail(err) {
          console.log(err)
        }
      })
    }
  },