您现在的位置: 微信小程序 > 微信小程序开发 > 教程 >

Canvas绘图在微信小程序中的应用:生成个性化海报

来源:微信小程序 编辑:Yiyongtong.com 发布时间:2018-10-07 09:28热度:
一、Canvas应用的背景(个人理解)及基础语法
背景
从2012年开始,微信那个时候用户的积累的量已经非常大了,推出公众号,当然大屏智能手机在那个时候也流行,传统的大众媒体逐步消亡,像微信公众号这样的新媒体盛行。企业的广告投入开始从电视等传统媒体向基于圈层文化的新媒体精准营销转移,甚至很多企业尤其互联网企业开始思考如何利用用户的自传播这种方式去宣传企业、实现商业目标。而用户的自传播很好的途径就是生产个性化的海报。举个最常见的例子,我第一次使用Keep是因为在朋友圈看到朋友分享她运动量的一个截图,当时在我看来非常酷,有心率脉搏呀、时速运动量啊、消耗的卡路里等,还有一个二维码,然后我就点了下载了Keep,这整个获客成本几乎为0,秒秒钟就多了一个用户。而实现这一过程的技术手段就可以用canvas。所以,canvas的盛行,与企业的精准营销和用户的自传播有很大的关系。
如极客时间的一些实现案例:



大家看第一张图的话是在2017年末的时候,Qcon全球软件开发大会预热阶段的海报。然后我们为程序员做了一个生成2018年关键字的一张海报,文案都非常有趣啊。第二张的话是在2018年元旦的时候做的极客时间助手,这个小程序当初主要是为程序员做的2018年新年签。那面就是一些极客时间的专栏,包括用户留言,你留言随手可以生成一张海报,可以转发等等大概就是这样。

基础语法
Canvas本质是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素,默认大小为300像素×150像素(宽×高,像素的单位是px),通过JavaScript上下文对象动态创建图像。比如,画线、画矩形、涂颜色甚至生成带二维码的海报。原理就是一笔一笔的画,画一条横线,再画一条横线等等,就是不断地创建路径、绘制路径,然后把这个路径封闭起来可以涂色之类的,他的底层的封装就是放到一个数组里形成一个路径的数组,将这个数组传到js底层的一个方法,然后去绘制。

举个栗子:画一个头像




首先,你需要把这张图片画canvas上面,比如说你画你这个头像就是正方形,就在(0,0)开始画一个图片。那么你在这个图片的中心,作为原点,然后你画一个圆形。然后你再利用canvas语法画一个圆弧,在这个圆弧路径以外设置不可见以内设置可见,这个时候就形成了一个圆形头像。

  <canvas id="canvas" width="300" height="300"></canvas>
  <script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    const img = new Image()
    img.onload = function() {
      circleImg(ctx, img, 100, 100, 50)
    }
    img.src="https://avatar-static.segmentfault.com/289/811/2898115528-58c35e9b79717_big64"
    function circleImg(ctx, img, x, y, r) {
      ctx.save()
      let d = 2 * r
      let cx = x + r
      let cy = y + r
      ctx.arc(cx, cy, r, 0, 2 * Math.PI)
      ctx.stroke();
      ctx.clip()
      ctx.drawImage(img, x, y, d, d)
      ctx.restore()
    }

    // 微信小程序中的[canvas](https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html)与HTML5的canvas在语法有些区别,比如API就不一样,
    // 另外小程序中的canvas因为是原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法覆盖原生组件
  </script>

二、常用的"生成海报"的方式

我们会经常在朋友圈看到什么算命、性格分析、测算你的智商、情商等等这些东西,都是由用户分享出一张图片(海报),这个图片就是用canvas做成的,上面画了二维码,二维码是一个数组两个或循环嵌套画小黑点用户识别这个二维码之后就进入他的程序,经过程序跑出来的测试结果啊什么的,点保存的时候,就会生成一张个性海报明白。怎么生成这种个性化海报呢?

2.1 字符串模板

此处应有案例

主要实现:与服务端约定好数据格式-->前端做好模板-->服务端用第三方工具渲染返回到客户端img
首先与服务端约定好数据格式,比如关键字是什么、头像URL、昵称等等,把所有放数据格式的地方用{{{}}}嵌套,告诉后端位置;然后,将前端模拟数据抠去,比如user.tags,把这一段html的字符串模板给到服务端,最后服务端拿到数据通过html2canvas这样的第三方工具把图片渲染返回给客户端展示,让用户可以长按这张图片保存到手机相册。这是比较传统的方式早些年基本上都是通过这种方式。
有什么弊端呢?
一是第三方工具维护不及时、不支持flex布局、ES6等语法,二是调试不方便,三是高并发的时候会出问题,特别是生成的是高清无码的海报的时候

2.2 canvas绘制

案例: '极客时间小助手'小程序
主要实现:前端直接通过canvas生成海报
摇晃手机抽取新年签跳到第一个页面,需要绘制头像、关键字以及保存按钮,黄色的保存按钮其实就是呃一张透明的png图片,把它画上去。那在这个button上面儿需要固定一个宽高和它差不多大小的一个空的、透明的div,在这个div上加点击事件,这个事件就是调第二张要保存的那个canvas。第二张这个是没有保存按钮的但有二维码。带二维码的这张canvas放哪里呢?一种方案是定位,给一个特别大的top或left,让它不显示在屏幕里边;另一个方案是层级,预览的这张canvas在真正要保存canvas图片之上,但是会有问题。手机浏览器版本低的话,定了层级不管用,一些安卓手机也会有问题,有时候会浮上来没被盖住。
当然,如果要实现保存高清图的话,还是需要处理的,那就是放大,不过这个是笨方法。最优的方法是拆解这张图像,确保导出的canvas是最高清的,而且对用户来说也是最省流量的。
解析:进到首页其实关键字在本地就随机取完了,在首页index.js中的onShow方法中就通过wx.getStorageSync缓存了要画的元素,比如关键字(这里是图片)、关键字解析语(也是图片,毕竟微信小程序的canvas不支持字体)等等。摇一摇触发重力感应事件wx.onAccelerometerChange监听里面的事件,获取用户授权拿到头像并跳转到poster页面。直接就开始画两张图片,一张有二维码的(shakepage1),一张有button的(shakepage2),这里二维码是'死码',button也是在图片的基础上覆盖一个view,画完之后调canvasToTempFilePath保导出那张带码的,此时带码的这张通过css设置visibility: hidden隐藏起来。点击按钮触发saveImageToPhotosAlbum将导出的这张 图片保存到手机相册,这里需要授权相应的要做一些处理,比如用户拒绝授权之后再次点击需要 wx.showModal再次请用户授权。基本代码如下:(详细源码))

      wx.canvasToTempFilePath({
        x: 0,
        y: 0,
        width: this.data.screenWidth,
        height: this.data.screenHeight,
        destWidth: this.data.screenWidth * this.data.pixelRatio,  // pixelRatio为设备的像素比  
        destHeight: this.data.screenHeight * this.data.pixelRatio,
        canvasId: "canvasid",
        success: function(e) {
          console.log(e)
          this.setData({
            bjtempFilePath: e.tempFilePath  // 拿到要保存的图片路径
          }, function() {});
        },
        fail: function(e) {
          console.log(e);
        }
      })
  onUserSaveImageRight: function () {
    console.log("-click-");
    var _this = this;
    if (!wx.saveImageToPhotosAlbum) return wx.showModal({
      title: "提示",
      content: "当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。"
    }), void console.log("version low");
    wx.getSetting({
      success: function (res) {
        res.authSetting["scope.writePhotosAlbum"] ? (console.log("1-已经授权《保存图片》权限"), _this.saveimgfn()) : wx.authorize({
          scope: "scope.writePhotosAlbum",
          success: function () {
            console.log("用户对相册-授权成功"), _this.saveimgfn();
          },
          fail: function () {
            wx.showModal({
              title: "提示",
              content: "请您授权保存到系统相册",
              showCancel: !1,
              success: function (res) {
                res.confirm && wx.openSetting({
                  success: function (res) {
                    res.authSetting["scope.writePhotosAlbum"] ? setTimeout(function () {
                      _this.saveimgfn();
                    }, 500) : wx.showModal({
                      title: "提示",
                      content: "您未授权,无法将海报保存到相册,你可以截屏得到海报,或者再次点击'保存海报'按钮并授权",
                      showCancel: !1
                    });
                  }
                });
              }
            });
          }
        });
      }
    });
  },
  saveimgfn: function () {
    var filePath = this.data.bjtempFilePath;
    console.log(filePath), filePath ? wx.saveImageToPhotosAlbum({
      filePath: filePath,
      success: function (res) {
        wx.showToast({
          title: "保存成功",
          icon: "success",
          duration: 1500
        });
      },
      fail: function () {
        wx.showToast({
          title: "保存失败",
          icon: "fail",
          duration: 1500
        });
      }
    }) : this.saveImage()

三、极客时间小程序-生成各种海报的解决方案

微信小程序canvas与HTM5的canvas对比

  1. 微信小程序canvas中层级z-index失效,小程序中canvas拥有最高级,无法二次设置;
  2. 微信小程序canvas不支持字体功能,特殊字体只能用图片代替;
  3. 微信小程序canvas不支持绘制在线图片,需要下载再绘制(安全域名的锅)
  4. 微信小程序canvas可以实现不同尺寸屏幕自适应
    var rpx;
    //获取屏幕宽度,获取自适应单位
    wx.getSystemInfo({
      success: function(res) {
        rpx = res.windowWidth/750
      },
    })
    // 在绘制方法中将参数乘以相对单位即可实现自适应
    const s = wx.createCanvasContext("canvas")
    s.drawImage(Url, 0, 0, 265 * rpx, 262.5 * rpx) 

如何导出高清海报、如何封装;

wx.canvasToTempFilePath({
    canvasId: 'image-save',
    x: 0,
    y: 0,
    success: res => {
       wx.saveImageToPhotosAlbum({
         filePath: res.tempFilePath,
         success: () => {
           this.setData({saving: false})
           utils.success('保存成功')
           setTimeout(() => {wx.navigateBack()}, 500)
         },
         fail: err => {
           this.setData({saving: false})
           wx.getSetting({
             success: res => {
               if(!res.authSetting || !res.authSetting['scrop.writePhotoAlbum']){
                 wx.openSetting()
               }
             }
           })
         }
       })
    }
})