微信小程序之生成图片

当我们开发完自己的小程序后,很想分享给大家让都来关注一下,但是单纯二维码看起来比较单调,不太吸引人!

为了提升吸引力,我们可以把这种用于分享出去的二维码图片做的尽量美观,做成漂亮的图片带二维码的。那怎么来生成带二维码的图片呢?下面是我做的一个生成图片的交互界面
QQ图片20181019151351.jpg

微信小程序也提供了一系列基于canvas的绘图相关API,所以绘制这样的图片还是比较简单易上手的。下面就开始我们的绘制之旅吧!

那怎么来生成一个分享的图片呢?

step1:绘制背景图片

观察合成的分享图,它的组成主要有以下几个部分:一张大的背景图,一本图书封面,书名“Row,row,row your boat”,“扫码听原版音频”文字,以及一个小程序码图片。 最后的两个button“分享给好友”“保存到相册”,只是做交互作用,不绘制到合成图片中去。

那么我们就先找一张图片来当做背景图,将它画到画布上去,代码大致如下:

 ctx.drawImage(this.randomBg, 0, (this.statusBarHeight-44)*scale, 375*scale, bgHeight*scale)
        ctx.draw(true)

step2:绘制图书封面图片

  let coverDx = (250-this.coverSize.width)/2 + 62.5
        let coverDy = this.naviBarBottom + 10 + (250-this.coverSize.height)/2
        if (this.isIos) {
          ctx.setShadow(0, 16*scale, 16*scale, '#D8D8D8')
        }
        ctx.drawImage(this.bookCoverFilePath, coverDx*scale, coverDy*scale, this.coverSize.width*scale, this.coverSize.height*scale)
        ctx.draw(true)

step3:绘制文字

        //绘制文字
        let titleW = 250
        let titleH = 40
        let titleDx = 375/2
        let titleDy = coverDy + 250 + 30 + titleH/2
        for (let b = 0; b < row.length; b++) {
          ctx.fillText(row[b], titleDx*scale, (titleDy + b*titleH)*scale, titleW*scale);
        }
        ctx.draw(true)

step4:绘制小程序码

      const qrDx = 138
        const qrDy = this.screenHeight - 57 - 100
        ctx.drawImage(this.qrcodeFilePath, qrDx*scale, qrDy*scale, 100*scale, 100*scale)
        //等待绘制完毕后输出
        ctx.draw(true, ()=>{
          wx.canvasToTempFilePath({
            canvasId: 'book-img',
            x: 0,
            y: 0,
            width: 375*scale,
            height: this.screenHeight*scale,
            success:(res)=>{
              this.imgOutputPath = res.tempFilePath
            },
            fail() {
              setTimeout(()=>{
                toast('生成分享码失败,请退出重试')
              },500)
            },
            complete:()=>{
              this.imgDone = true
              if (this.shareDone) {
                this.loading = false
              }
            }
          })
        })

我使用的vue.js+mpvue开发的小程序,我先展示一下这个页面的完整代码,由于业务需求,实现的逻辑可能复杂了点:

<template>
  <div class="con" :style="{height: screenHeight+'px'}" @touchmove.stop="disableMove">
    <canvas canvas-id="book-img" class="canvas" :style="{height: (screenHeight*4)+'rpx',background: '#ffffff'}" />
    <canvas canvas-id="book-share" class="canvas" :style="{height: (screenHeight*4)+'rpx',background: '#ffffff'}" />
    <img class="bg" :style="{top: (statusBarHeight-44)*2+'rpx'}" :src="randomBg"/>
    <div class="content"  :style="{'padding-top': statusBarHeight+'px','padding-bottom': (statusBarHeight >20 ? 54:20)+'px'}">
      <div class="header">
        <div class="close-btn" @click="onClickClose">
          <img class="close-img" src="/static/icon_close.png" />
        </div>
        <div class="book-cover-con">
          <div class="book-cover" :style="{width: coverSize.width*2+'rpx', height: coverSize.height*2+'rpx'}">
            <lazy-image :src="book.book_img_thumb" mode="aspectFit" />
          </div>
        </div>
        <div class="book-title line3">{{book.book_name}}</div>
      </div>
      <div class="footer">
       <div class="scan-con">
            <img class="scan-music" src="/static/icon_music_node_black.png" />
        <div class="scan-tip">扫码收听原版音频</div>
          <div class="scan-qrcode">
            <lazy-image :src="qrCodeUrl"/>
          </div>
        </div>
        <div class="btn-con">
          <div class="btn btn-left">
            分享给好友
            <button class="btn-share" open-type="share" />
          </div>
          <div class="btn" @click="onClickSave2Album">保存到相册</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import LazyImage from '@/components/base/LazyImage';
  import {mapGetters} from 'vuex'
  import {
    getQuery,
    toast,
    showLoading,
    hideLoading,
  } from '@/utils'
  import {getQRCode} from '@/api/mini-program'

  export default {
    components: {LazyImage},
    props: {
      book: {
        type: Object
      }
    },
    data() {
      return {
        qrCodeUrl: '',
        bookCoverFilePath: '',
        qrcodeFilePath: '',
        imgOutputPath: '',
        shareOutputPath: '',
        imgDone: false,
        shareDone: false,
        loading: false,
      }
    },
    onUnload() {
      this.qrCodeUrl = ''
      this.bookCoverFilePath = ''
      this.qrcodeFilePath = ''
      this.imgOutputPath = ''
      this.shareOutputPath = ''
      this.imgDone = false
      this.shareDone = false
      this.loading = false
    },
    computed: {
      randomBg() {
        let index = Math.floor(Math.random()*3)%3
        return '/static/book_share_bg' + index + '.png'
      },
      coverSize() {
        let query = getQuery(this.book['book_img_thumb'])
        let width = query.w ? query.w: 250, height = query.h ? query.h:250
        let maxW = 250, maxH = 250
        let size = {}
        if (width > height) {
          size.width = maxW
          size.height = height*maxW/width
        }else {
          size.height = maxH
          size.width = width*maxH/height
        }
        return size
      },
      ...mapGetters('system', ['screenHeight','statusBarHeight','naviBarBottom','isIos'])
    },
    methods: {
      disableMove() {},
      draw() {
        if (this.bookCoverFilePath.length === 0 || this.qrcodeFilePath.length === 0) {return}
        this.drawImg()
        this.drawShare()
      },
      drawImg() {
        const ctx = wx.createCanvasContext('book-img', this)

        const scale = 2
        //bg
        const bgHeight = 812
        ctx.drawImage(this.randomBg, 0, (this.statusBarHeight-44)*scale, 375*scale, bgHeight*scale)
        ctx.draw(true)

        //cover
        let coverDx = (250-this.coverSize.width)/2 + 62.5
        let coverDy = this.naviBarBottom + 10 + (250-this.coverSize.height)/2
        if (this.isIos) {
          ctx.setShadow(0, 16*scale, 16*scale, '#D8D8D8')
        }
        ctx.drawImage(this.bookCoverFilePath, coverDx*scale, coverDy*scale, this.coverSize.width*scale, this.coverSize.height*scale)
        ctx.draw(true)

        //title
        //计算每行文本,需要裁剪时裁剪文本
        let title = this.book.book_name
        let chars = title.split('')
        let temp = ''
        let word = ''
        let row = []
        const maxLine = 3
        ctx.setShadow(0, 0, 0, '#000000')
        ctx.font = `normal bold ${28*scale}px PingFangSC-Semibold`
        ctx.setTextAlign('center')
        ctx.setFillStyle('#484848')
        ctx.setTextBaseline('middle')
        for (let a = 0; a < chars.length; a++) {
          let ascii = title.charCodeAt(a)
          let isLetter = ascii >= 65 && ascii <= 90 || ascii >= 97 && ascii <= 122
          if (isLetter && a !== chars.length-1) {
            word += chars[a]
            continue
          }
          else {
            if (ctx.measureText(temp+word+chars[a]).width < 250*scale) {
              temp += word
              temp += chars[a];
            }
            else {
              row.push(temp);
              temp = word + chars[a];
            }
            word = ''
          }
        }
        row.push(temp);
        if (row.length > maxLine) {
          let rowCut = row.slice(0, maxLine);
          let rowPart = rowCut[maxLine-1];
          let test = "";
          let empty = [];
          for (let a = 0; a < rowPart.length; a++) {
            if (ctx.measureText(test).width < 220*scale) {
              test += rowPart[a];
            }
            else {
              break;
            }
          }
          empty.push(test);
          let group = empty[0] + '...'
          rowCut.splice(maxLine-1, 1, group);
          row = rowCut;
        }

        //绘制文字
        let titleW = 250
        let titleH = 40
        let titleDx = 375/2
        let titleDy = coverDy + 250 + 30 + titleH/2
        for (let b = 0; b < row.length; b++) {
          ctx.fillText(row[b], titleDx*scale, (titleDy + b*titleH)*scale, titleW*scale);
        }
        ctx.draw(true)

        //tip & qrcode
        const iconPath = '/static/icon_music_node_black.png'
        const iconDx = 122
        const iconDy = this.screenHeight - 20 - 22
        ctx.drawImage(iconPath, iconDx*scale, iconDy*scale, 22*scale, 22*scale)

        const tipW = 104
        const tipH = 18
        const tipDx = 148 + tipW/2
        const tipDy = this.screenHeight - 22 - 18/2
        ctx.setFontSize(13*scale)
        ctx.setTextAlign('center')
        ctx.setFillStyle('#919191')
        ctx.setTextBaseline('middle')
        ctx.fillText('扫码收听原版音频', tipDx*scale, tipDy*scale, tipW*scale)

        const qrDx = 138
        const qrDy = this.screenHeight - 57 - 100
        ctx.drawImage(this.qrcodeFilePath, qrDx*scale, qrDy*scale, 100*scale, 100*scale)
        //等待绘制完毕后输出
        ctx.draw(true, ()=>{
          wx.canvasToTempFilePath({
            canvasId: 'book-img',
            x: 0,
            y: 0,
            width: 375*scale,
            height: this.screenHeight*scale,
            success:(res)=>{
              this.imgOutputPath = res.tempFilePath
            },
            fail() {
              setTimeout(()=>{
                toast('生成分享码失败,请退出重试')
              },500)
            },
            complete:()=>{
              this.imgDone = true
              if (this.shareDone) {
                this.loading = false
              }
            }
          })
        })
      },
      drawShare() {
        const ctx = wx.createCanvasContext('book-share', this)

        const scale = 2
        //bg
        const bgHeight = 812
        ctx.drawImage(this.randomBg, 0, 0, 375*scale, bgHeight*scale)
        ctx.draw(true)

        //cover
        let coverW = this.coverSize.width/250*196
        let coverH = this.coverSize.height/250*196
        let coverDx = (196-coverW)/2 + 89
        let coverDy = 25 + (196-coverH)/2
        let coverBottom = coverDy + coverH
        if (this.isIos) {
          ctx.setShadow(0, 16*scale, 16*scale, '#D8D8D8')
        }
        ctx.drawImage(this.bookCoverFilePath, coverDx*scale, coverDy*scale, coverW*scale, coverH*scale)
        ctx.draw(true)

        //slogan
        const tipW = 230
        const tipH = 30
        const tipDx = 375/2
        const tipDy = coverBottom + 25+ 30/2
        ctx.setFontSize(21*scale)
        ctx.setTextAlign('center')
        ctx.setFillStyle('#484848')
        ctx.setTextBaseline('middle')
        ctx.fillText('扫码收听原版音频', tipDx*scale, tipDy*scale, tipW*scale)

        //等待绘制完毕后输出
        ctx.draw(true, ()=>{
          wx.canvasToTempFilePath({
            canvasId: 'book-share',
            x: 0,
            y: 0,
            width: 375*scale,
            height: 375/5*4*scale,
            success:(res)=>{
              this.shareOutputPath = res.tempFilePath
              this.$emit('share', {book: this.book, imgUrl: this.shareOutputPath})
            },
            fail() {
              setTimeout(()=>{
                toast('生成分享码失败,请退出重试')
              },500)
            },
            complete:()=>{
              this.shareDone = true
              if (this.imgDone) {
                this.loading = false
              }
            }
          })
        })
      },
      onClickSave2Album() {
        wx.saveImageToPhotosAlbum({
          filePath: this.imgOutputPath,
          success(res) {
            toast('保存成功')
          },
          fail(res) {
            toast('保存失败,请确认权限是否开启')
            setTimeout(()=>wx.openSetting(),2000)
          }
        })
      },
      onClickClose() {
        this.$emit('close')
      },
    },
    mounted() {
      this.loading = true
      wx.getImageInfo({
        src: this.book.book_img_thumb,
        success:(res)=>{
          this.bookCoverFilePath = res.path
          this.draw()
        },
        fail:(res)=>{
          this.loading = false
          setTimeout(()=>{
            toast('生成分享码失败,请退出重试')
          },500)
        }
      })

      let scene = this.book.is_group === 1 ? `goods_id=${this.book.goods_id}`:`isbn_id=${this.book.isbn_id}`
      let page = 'pages/index/main'
      getQRCode(scene, page, 100*3).then(
        (res)=>{
          let {qrcode_url} = res
          this.qrCodeUrl = qrcode_url
          wx.getImageInfo({
            src: this.qrCodeUrl,
            success:(res)=>{
              this.qrcodeFilePath = res.path
              this.draw()
            },
            fail:(res)=>{
              this.loading = false
              setTimeout(()=>{
                toast('生成分享码失败,请退出重试')
              },500)
            }
          });
        }
      ).catch((err)=>{
        this.loading = false
        setTimeout(()=>{
          toast(err.message)
        },500)
      })
    },
    watch: {
      loading(newVal) {
        newVal ? showLoading():hideLoading()
      }
    }
  }
</script>

<style scoped lang='less'>
  @import "../../styles/variable.less";
  .con {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background: #ffffff;
    overflow: hidden;
    z-index: 1004;
  }
  .bg {
    position: fixed;
    width: 100%;
    height: 691px;
    left: 0;
  }
  .content {
    position: fixed;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    box-sizing: border-box;
  }
  .close-btn {
    box-sizing: border-box;
    width: 44px;
    height: 44px;
    margin-left: 10px;
    padding-top: 12px;
    padding-left: 12px;
  }
  .close-img {
    width: 20px;
    height:20px;
  }
  .book-cover-con {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 250px;
    height: 250px;
    margin-top: 10px;
    margin-left: 62.5px;
  }
  .book-cover {
    box-shadow:0px 16px 16px 0px rgba(0,0,0,0.14);
  }
  .book-title {
    color: #484848;
    font-size:28px;
    font-weight: bold;
    line-height:40px;
    text-align: center;
    height: 120px;
    margin: 30px 62.5px 0;
  }
  .scan-con {
    display: flex;
    flex-direction: row;
    align-items: center;
    width: 100%;
    height: 75px;
  }
  .scan-music {
    width: 22px;
    height: 22px;
    margin-left: 136px;
  }
  .scan-tip {
    color: #919191;
    font-size:13px;
    line-height:18px;
    margin-left: 4px;
  }
  .scan-qrcode {
    margin-left: 10px;
    width: 75px;
    height: 75px;
  }
  .btn-con {
    display: flex;
    flex-direction: row;
    justify-content: center;
    margin-top: 20px;
    width: 100%;
    height: 50px;
  }
  .btn {
    width: 145px;
    height: 50px;
    font-size: 18px;
    line-height:50px;
    color: #484848;
    border-radius:4px;
    border:1px solid rgba(72,72,72,1);
    text-align: center;
    &:active {
      opacity: 0.5;
    }
  }
  .btn-left {
    position: relative;
    margin-right: 15px;
  }
  .btn-share {
    position: absolute;
    opacity: 0;
    color: #00000000;
    background: #00000000;
    background-color: #00000000;
    width: 145px;
    height: 50px;
    border-width: 0px;
    left: 0;
    top: 0;
    padding: 0;
  }
  /*canvas*/
  .canvas {
    position: fixed;
    width: 750px;
    top: -4000px;
    left: 0;
  }
</style>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352