# 小程序实现海报分享功能

小程序实现图片分享功能,主要是通过canvas画布把元素一个一个画上去,最后通过画布转图片方法把图片保持到相册中。这其中有几个比较关键的点需要去实现。

# canvas尺寸问题

canvas宽高可以在元素上直接设置,或者通过js设置,但是canvas的宽高和css设置的宽高是不一样的,css设置的大小是设置的画布窗口显示的大小,但是底层画布的大小是没有改变的,默认是300*150的,所以有时只设置的css宽高绘制是变形的,只有设置画布的大小和css设置的窗口大小一样才不会变形。

在小程序中需要根据dpr来设置,保证屏幕大小适配。

<canvas class="canvas" type="2d" id="myCanvas"></canvas>


Page({
  data: {
    canvasImg: '',
    canvasDom: null,
    context: null,
  },
  onReady() {
    this.initCanvas()
  },
  //初始化画布
  initCanvas() {
    const query = this.createSelectorQuery()
    query
      .select('#myCanvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        const canvasDom = res[0].node
        const context = canvasDom.getContext('2d')
        // 设置真实宽高
        const dpr = wx.getSystemInfoSync().pixelRatio
        const canvasW = res[0].width * dpr
        const canvasH = res[0].height * dpr
        canvasDom.width = canvasW
        canvasDom.height = canvasH
        this.setData({
          canvasDom,
          context,
          canvasW,
          canvasH,
        })
      })
  }
})


// CSS
.index-canvas {
  width: 750rpx;
  height: 1000rpx;
  canvas {
    width: 100%;
    height: 100%;
  }
}

# 绘制图片

把图像绘制到画布上,可以使用drawImage()方法,如果是本地图片可以通过createImage()方法创建一个图片,然后直接绘制就行了,如果是通过手机拍照或者获取手机相册可以通过临时路径tempImagePath进行绘制。

// 创建一个图片对象
const url = '../../assets/images/bg.png'
drawImage(url, x, y, w, h) {
  const { context, canvasDom } = this.data
  const image = canvasDom.createImage();
  image.src = url;
  image.onload = () => {
    // 把获取图片大小不动的绘制到 (0, 0) 的位置上
    // context.drawImage(image, 0, 0);
    // 把图片绘制到画布位置(0, 0)大小为w, h的区域上 
    context.drawImage(image, x, y, w, h)
  }
},

# 绘制网络图片

如果是网络图片(允许访问的)是没办法直接绘制,如果后端给我们一个动态图片,我们该如何绘制到canvas上呢,上面我们提到可以使用临时地址,那么如果是网络地址,我们可以使用downloadFile()方法把图片地址下载下来,获取到临时路径在绘制到canvas上。

// 下载图片
downloadFile(url) {
  return new Promise((resolve, reject) => {
    wx.downloadFile({
      url,
      success: (res) => {
        console.log('下载成功', res.tempFilePath);
        resolve(res.tempFilePath)
      },
      fail: (err) => {
        console.log('下载失败', err);
        reject(err)
      }
    })
  })
},


// 保存海报
async saveCanvasImage() {
  const imgUrl = await this.downloadFile(xxx)
  this.drawImage(imgUrl, 0, 0, 100, 100)
}

# 文本进行换行

单行文本可以直接通过fillText()方法直接绘制,如果文本超多canvas宽度是不会自动换行的,我们需要借助measureText()方法计算文本的宽度,如果超过canvas宽度则截取文本进行换行绘制。

// 绘制文本
drawText(text, x, y, maxWidth, lineHeight) {
  const { context } = this.data;


  // 字符分隔为数组
  const arrText = text.split('');
  let line = '';
  
  for (let n = 0; n < arrText.length; n++) {
    const testLine = line + arrText[n];
    const metrics = context.measureText(testLine);
    const testWidth = metrics.width;
    if (testWidth > maxWidth && n > 0) {
      // 设置字体样式
      // context.font = "bold 30px";
      context.fillText(line, x, y);
      line = arrText[n];
      y += lineHeight;
    } else {
      line = testLine;
    }
  }
  context.fillText(line, x, y);
},

# 保存图片

将canvas转成图片需要使用canvasToTempFilePath()方法,然后通过wx.saveImageToPhotosAlbum()方法将图片保存到手机相册。

// 将canvas转为图片并保存
wx.canvasToTempFilePath({
  canvas: canvasDom,
  width: canvasW,
  height: canvasH,
  destWidth: canvasW, // 一定要设置 否则输出图片是原本的pixelRatio倍导致图片不全
  destHeight: canvasH,
  fileType: 'jpg', // 图片类型
  // quality: 1, // 图片质量
  success: (res) => {
    console.log('生成成功', res.tempFilePath);
    wx.saveImageToPhotosAlbum({
      filePath: res.tempFilePath, // 临时文件路径或永久文件路径 (本地路径) ,不支持网络路径
      success: (res) => {
        console.log('保存海报成功', res)
      },
      fail: (err) => {
        console.log('保存海报成功', err)
      }
    })
  }
})

# 示例

// html
<view class="index">
  <view class="index-canvas">
    <canvas class="canvas" type="2d" id="myCanvas" width="750" heigth="1000"></canvas>
  </view>
  <button type="primary" disabled="{{saveLoad}}" bindtap="saveCanvasImage">生成海报</button>
</view>


// js
Page({
  data: {
    saveLoad: false,
    canvasDom: null,
    context: null,
    canvasW: 0,
    canvasH: 0,
    textY: 0,
  },
  onLoad() {
    // this.getCardList()
  },
  onReady() {
    this.initCanvas()
  },
  //初始化画布
  initCanvas() {
    const query = this.createSelectorQuery()
    query
      .select('#myCanvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        const canvasDom = res[0].node
        const context = canvasDom.getContext('2d')
        const dpr = wx.getSystemInfoSync().pixelRatio
        const canvasW = res[0].width * dpr
        const canvasH = res[0].height * dpr
        canvasDom.width = canvasW
        canvasDom.height = canvasH
        this.setData({
          canvasDom,
          context,
          canvasW,
          canvasH,
        })
        // this.drawImage()
        // this.downloadFile()
      })
    },
    
  // 创建一个图片对象
  drawImage(url, x, y, w, h) {
    const { context, canvasDom } = this.data
    const image = canvasDom.createImage();
    image.src = url;
    image.onload = () => {
      // 将图片绘制到canvas上
      context.drawImage(image, x, y, w, h)
    }
  },


  // 下载图片
  downloadFile(url) {
    return new Promise((resolve, reject) => {
      wx.downloadFile({
        url,
        success: (res) => {
          console.log('下载成功', res.tempFilePath);
          resolve(res.tempFilePath)
        },
        fail: (err) => {
          console.log('下载失败', err);
          reject(err)
        }
      })
    })
  },


  // 绘制文本
  drawText(text, x, y, maxWidth, lineHeight) {
    const { context } = this.data;


    // 字符分隔为数组
    const arrText = text.split('');
    let line = '';
    
    for (let n = 0; n < arrText.length; n++) {
      const testLine = line + arrText[n];
      const metrics = context.measureText(testLine);
      const testWidth = metrics.width;
      if (testWidth > maxWidth && n > 0) {
        // 设置字体样式
        // context.font = "bold 30px";
        context.fillText(line, x, y);
        line = arrText[n];
        y += lineHeight;
      } else {
        line = testLine;
      }
    }
    // 绘制后的高度
    this.setData({
      textY: y
    })
    context.fillText(line, x, y);
  },


  // 保存海报
  async saveCanvasImage() {
    const that = this
    const { context, canvasDom, canvasW, canvasH } = this.data
    this.setData({
      saveLoad: true
    })
    // 绘制分享图片
    const imgUrl = await this.downloadFile('https://via.placeholder.com/200x300')
    this.drawImage(imgUrl, 150, 30, 200, 300)
    // 设置文字字号及字体
    context.font = '30px'
    // 设置画笔颜色
    context.fillStyle = '#000'
    context.fillText('我是分享标题', 50, 350)
    const desc = '我是商品详情介绍,我是商品详情介绍,我是商品详情介绍,我是商品详情介绍。'
    this.drawText(desc, 50, 380, canvasW - 50, 20)
    context.fillText('分享得好礼', 50, this.data.textY + 30)


    // 将canvas转为为图片
    wx.canvasToTempFilePath({
      canvas: canvasDom,
      width: canvasW,
      height: canvasH,
      destWidth: canvasW, // 一定要设置 否则输出图片是原本的pixelRatio倍导致图片不全
      destHeight: canvasH,
      fileType: 'jpg', // 图片类型
      // quality: 1, // 图片质量
      success: (res) => {
        that.setData({
          saveLoad: false
        })
        console.log('生成成功', res.tempFilePath);
        wx.saveImageToPhotosAlbum({
          filePath: res.tempFilePath, // 临时文件路径或永久文件路径 (本地路径) ,不支持网络路径
          success: (res) => {
            console.log('保存海报成功', res)
          },
          fail: (err) => {
            console.log('保存海报成功', err)
          }
        })
      },
      fail: (err) => {
        that.setData({
          saveLoad: false
        })
        console.log('生成错误', err)
      }
    })
  },
})


// css
.index {
  width: 100%;
  height: 100vh;
  padding-top: 20rpx;
  background-color: #fff;
  .index-canvas {
    width: 600rpx;
    height: 800rpx;
    margin: 0 auto;
    canvas {
      width: 100%;
      height: 100%;
    }
  }
}