资讯专栏INFORMATION COLUMN

小程序如何生成海报分享朋友圈

Lemon_95 / 2031人阅读

摘要:项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。需求利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。

项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。

需求

利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。
生成的海报如下

需求分析

1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序
2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的

实现方案 一、分析如何实现

相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。

二、需要解决的问题

1、二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码)
2、背景图如何绘制,获取图片信息
3、将绘制完成的图片保存到本地相册
4、处理用户是否取消授权保存到相册

三、实现步骤

这里我具体写下围绕上面所提出的问题,描述大概实现的过程

①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。

②创建好画布之后,先绘制背景图,因为背景图我是放在本地,所以获取 组件 canvas-id 属性,通过createCanvasContext创建canvas的绘图上下文 CanvasContext 对象。使用drawImage绘制图像到画布,第一个参数是图片的本地地址,后面两个参数是图像相对画布左上角位置的x轴和y轴,最后两个参数是设置图像的宽高。

const ctx = wx.createCanvasContext("myCanvas")

ctx.drawImage("/img/study/shareimg.png", 0, 0, 690, 1085)

③创建好背景图后,在背景图上绘制头像,文字和数字。通过getImageInfo获取头像的信息,这里需要注意下在获取的网络图片要先配置download域名才能生效,具体在小程序后台设置里配置。

获取头像地址,首先量取头像在画布中的大小,和x轴Y轴的坐标,这里的result[0]是我用promise封装返回的一个图片地址

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"
            })
          }
        })
      })
      
let avatarurl_width = 60, //绘制的头像宽度
    avatarurl_heigth = 60, //绘制的头像高度
    avatarurl_x = 28, //绘制的头像在画布上的位置
    avatarurl_y = 36; //绘制的头像在画布上的位置
    
    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); // 推进去图片
    
这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。

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

④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息

⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的)

ctx.drawImage("小程序码的本地地址", x轴, Y轴, 宽, 高)

⑥最终绘制完把canvas画布转成图片并返回图片地址

        wx.canvasToTempFilePath({
            canvasId: "myCanvas",
            success: function (res) {
              canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里
              that.setData({
                showShareImg: true
              })
              wx.showToast({
                title: "绘制成功",
              })
            },
            fail: function () {
              wx.showToast({
                title: "绘制失败",
              })
            },
            complete: function () {
              wx.hideLoading()
              wx.hideToast()
            }
          })

⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。

// 获取用户是否开启用户授权相册
    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)
        }
      })
    }
总结
至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码
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)
        }
      })
    }
  },

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104132.html

相关文章

  • 程序如何生成海报分享朋友

    摘要:项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。需求利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。摘要: 小程序开发必备技能啊... 原文:小程序如何生成海报分享朋友圈 作者:小白 Fundebug经授权转载,版权归原作者所有。 项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优...

    lemon 评论0 收藏0
  • Canvas绘图在微信程序中的应用:生成个性化海报

    摘要:解析进到首页其实关键字在本地就随机取完了,在首页中的方法中就通过缓存了要画的元素,比如关键字这里是图片关键字解析语也是图片毕竟微信小程序的不支持字体等等。 一、Canvas应用的背景(个人理解)及基础语法 背景 从2012年开始,微信那个时候用户的积累的量已经非常大了,推出公众号,当然大屏智能手机在那个时候也流行,传统的大众媒体逐步消亡,像微信公众号这样的新媒体盛行。企业的广告投入开始...

    vpants 评论0 收藏0
  • Laravel 生成程序图文海报最佳方案之一

    摘要:微信小程序官方并未提供分享到朋友圈的方法,所以目前基本整个行业都是使用生成图文海报发到朋友圈,然后识别太阳码进入到小程序。背景图片和微信头像合成后清晰度不够。 微信小程序官方并未提供分享到朋友圈的方法,所以目前基本整个行业都是使用生成图文海报发到朋友圈,然后识别太阳码进入到小程序。 通过谷歌或者百度有很多同学已经提供了一些解决方案,但是在我们使用后效果并不是很理想,主要体现在以下方面:...

    piglei 评论0 收藏0
  • 教你如何在低成本、有限的时间里策划裂变100社群_怎么做微信防封群

    摘要:如何在低成本有限的时间里策划一场合格的裂变活动呢常做裂变的朋友可能回到个词裂变系数投入与产出比。存在成本的裂变活动吗答案存在。在这条新规则宣布后,裂变分享率增加到了。优点进一步验证用户的朋友圈,防止浑水摸鱼。 ...

    BLUE 评论0 收藏0

发表评论

0条评论

Lemon_95

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<