uniapp封裝canvas組件無腦繪制保存小程序分享海報

正文

小程序分享海報想必大傢都做過,受微信的限制,無法直接分享小程序到朋友圈(雖然微信開發者工具基礎庫從2.11.3開始支持分享小程序到朋友圈,但目前仍處於Beta中),所以生成海報仍然還是主流方式,通常是將設計稿通過canvas繪制成圖片,然後保存到用戶相冊,用戶通過圖片分享小程序

但是,如果不是對canvas很熟悉的話,每次都要去學習canvas的Api,挺麻煩的。我隻想“無腦”的完成海報的繪制,就像是把每一個元素固定定位一樣,告訴它你要插入的是圖片、還是文字,然後再傳入坐標、寬高就能把在canvas繪制出內容。

怎麼做呢?接著往下看(註:本文是基於uniapp Vue3搭建的小程序實現的海報功能)

配置項

屬性 說明 可選值
type 元素類型 image、text、border、block(一般用於設置背景色塊)
left 元素距離canvas左側的距離 數字或者center,center表示水平居中,比如10、'center'
right 元素距離canvas右側的距離 數字,比如10
top 元素距離canvas頂部的距離 數字,比如10
bottom 元素距離canvas底部的距離 數字,比如10
width 元素寬度 數字,比如20
height 元素高度 數字,比如20
url type為image時的圖片地址 字符串
color type為text、border、block時的顏色 字符串,比如#333333
content type為text時的文本內容 字符串
fontSize type為text時的字體大小 數字,比如16
radius type為image、block時圓角,200表示圓形 數字,比如10
maxLine type為text時限制最大行數,超出以…結尾 數字,比如2
lineHeight type為text時的行高,倍數 數字,比如1.5,默認1.3

一、使用

<template>
  <m-canvas ref="myCanvasRef" :width="470" :height="690" />
  <button @click="createPoster">生成海報</button>
</template>
<script setup>
  import { ref } from 'vue'
  const myCanvasRef = ref()
  function createPoster() {
    // 配置項
    const options = [
      // 背景圖
      {
        type: 'image',
        url: '自行替換',
        left: 0,
        top: 0,
        width: 470,
        height: 690
      },
      // 長按掃碼 > 瀏覽臻品 > 獲取權益
      {
        type: 'text',
        content: '長按掃碼 > 瀏覽臻品 > 獲取權益',
        color: '#333',
        fontSize: 20,
        left: 'center',
        top: 240
      },
      // 小程序碼白色背景
      {
        type: 'block',
        color: '#fff',
        radius: 30,
        left: 'center',
        top: 275,
        width: 245,
        height: 245
      },
      // 小程序碼
      {
        type: 'image',
        url: '自行替換',
        left: 'center',
        top: 310,
        width: 180,
        height: 180
      },
      // 頭像
      {
        type: 'image',
        url: '自行替換',
        radius: '50%',
        left: 'center',
        top: 545,
        width: 50,
        height: 50
      },
      // 昵稱
      {
        type: 'text',
        content: 'Jerry',
        color: '#333',
        fontSize: 20,
        left: 'center',
        top: 625
      }
    ]
    // 調用myCanvas的onDraw方法,繪制並保存
    myCanvasRef.value.onDraw(options, url => {
      console.log(url)
    })
  }
</script>
<style lang="scss" scoped></style>

二、封裝m-canvas組件

<template>
  <canvas class="myCanvas" canvas-id="myCanvas" />
</template>
<script setup>
  import { getCurrentInstance } from 'vue'
  // 引入canvas方法
  import { createPoster } from './canvas'
  const { proxy } = getCurrentInstance()
  // 寬高需要傳哦~
  const props = defineProps({
    width: {
      type: Number,
      required: true
    },
    height: {
      type: Number,
      required: true
    }
  })
  // 導出方法給父組件用
  defineExpose({
    onDraw(options, callback) {
      createPoster.call(
        // 當前上下文
        proxy,
        // canvas相關信息
        {
          id: 'myCanvas',
          width: props.width,
          height: props.height
        },
        // 元素集合
        options,
        // 回調函數
        callback
      )
    }
  })
</script>
<style lang="scss" scoped>
  // 隱藏canvas
  .myCanvas {
    left: -9999px;
    bottom: -9999px;
    position: fixed;
    // canvas寬度
    width: calc(1px * v-bind(width));
    // canvas高度
    height: calc(1px * v-bind(height));
  }
</style>

三、聲明canvas.js,封裝方法

/** @生成海報 **/
export function createPoster(canvasInfo, options, callback) {
  uni.showLoading({
    title: '海報生成中…',
    mask: true
  })
  const myCanvas = uni.createCanvasContext(canvasInfo.id, this)
  var index = 0
  drawCanvas(myCanvas, canvasInfo, options, index, () => {
    myCanvas.draw(true, () => {
      // 延遲,等canvas畫完
      const timer = setTimeout(() => {
        savePoster.call(this, canvasInfo.id, callback)
        clearTimeout(timer)
      }, 1000)
    })
  })
}
// 繪制中
async function drawCanvas(myCanvas, canvasInfo, options, index, drawComplete) {
  let item = options[index]
  // 最大行數:maxLine  字體大小:fontSize  行高:lineHeight
  //    類型    顏色  left  right  top  bottom   寬      高     圓角    圖片  文本內容
  let { type, color, left, right, top, bottom, width, height, radius, url, content, fontSize } = item
  radius = radius || 0
  const { width: canvasWidth, height: canvasHeight } = canvasInfo
  switch (type) {
    /** @文本 **/
    case 'text':
      if (!content) break
      // 根據字體大小計算出寬度
      myCanvas.setFontSize(fontSize)
      // 內容寬度:傳瞭寬度就去寬度,否則取字體本身寬度
      item.width = width || myCanvas.measureText(content).width
      console.log(myCanvas.measureText(content))
      // left位置
      if (right !== undefined) {
        item.left = canvasWidth - right - item.width
      } else if (left === 'center') {
        item.left = canvasWidth / 2 - item.width / 2
      }
      // top位置
      if (bottom !== undefined) {
        item.top = canvasHeight - bottom - fontSize
      }
      drawText(myCanvas, item)
      break
    /** @圖片 **/
    case 'image':
      if (!url) break
      var imageTempPath = await getImageTempPath(url)
      // left位置
      if (right !== undefined) {
        left = canvasWidth - right - width
      } else if (left === 'center') {
        left = canvasWidth / 2 - width / 2
      }
      // top位置
      if (bottom !== undefined) {
        top = canvasHeight - bottom - height
      }
      // 帶圓角
      if (radius) {
        myCanvas.save()
        myCanvas.beginPath()
        // 圓形圖片
        if (radius === '50%') {
          myCanvas.arc(left + width / 2, top + height / 2, width / 2, 0, Math.PI * 2, false)
        } else {
          if (width < 2 * radius) radius = width / 2
          if (height < 2 * radius) radius = height / 2
          myCanvas.beginPath()
          myCanvas.moveTo(left + radius, top)
          myCanvas.arcTo(left + width, top, left + width, top + height, radius)
          myCanvas.arcTo(left + width, top + height, left, top + height, radius)
          myCanvas.arcTo(left, top + height, left, top, radius)
          myCanvas.arcTo(left, top, left + width, top, radius)
          myCanvas.closePath()
        }
        myCanvas.clip()
      }
      myCanvas.drawImage(imageTempPath, left, top, width, height)
      myCanvas.restore()
      break
    /** @盒子 **/
    case 'block':
      // left位置
      if (right !== undefined) {
        left = canvasWidth - right - width
      } else if (left === 'center') {
        left = canvasWidth / 2 - width / 2
      }
      // top位置
      if (bottom !== undefined) {
        top = canvasHeight - bottom - height
      }
      if (width < 2 * radius) {
        radius = width / 2
      }
      if (height < 2 * radius) {
        radius = height / 2
      }
      myCanvas.beginPath()
      myCanvas.fillStyle = color
      myCanvas.strokeStyle = color
      myCanvas.moveTo(left + radius, top)
      myCanvas.arcTo(left + width, top, left + width, top + height, radius)
      myCanvas.arcTo(left + width, top + height, left, top + height, radius)
      myCanvas.arcTo(left, top + height, left, top, radius)
      myCanvas.arcTo(left, top, left + width, top, radius)
      myCanvas.stroke()
      myCanvas.fill()
      myCanvas.closePath()
      break
    /** @邊框 **/
    case 'border':
      // left位置
      if (right !== undefined) {
        left = canvasWidth - right - width
      }
      // top位置
      if (bottom !== undefined) {
        top = canvasHeight - bottom - height
      }
      myCanvas.beginPath()
      myCanvas.moveTo(left, top)
      myCanvas.lineTo(left + width, top + height)
      myCanvas.strokeStyle = color
      myCanvas.lineWidth = width
      myCanvas.stroke()
      break
  }
  // 遞歸邊解析圖片邊畫
  if (index === options.length - 1) {
    drawComplete()
  } else {
    index++
    drawCanvas(myCanvas, canvasInfo, options, index, drawComplete)
  }
}
// 下載並保存
function savePoster(canvasId, callback) {
  uni.showLoading({
    title: '保存中…',
    mask: true
  })
  uni.canvasToTempFilePath(
    {
      canvasId,
      success(res) {
        callback && callback(res.tempFilePath)
        uni.saveImageToPhotosAlbum({
          filePath: res.tempFilePath,
          success() {
            uni.showToast({
              icon: 'success',
              title: '保存成功!'
            })
          },
          fail() {
            uni.showToast({
              icon: 'none',
              title: '保存失敗,請稍後再試~'
            })
          },
          complete() {
            uni.hideLoading()
          }
        })
      },
      fail(res) {
        console.log('圖片保存失敗:', res.errMsg)
        uni.showToast({
          icon: 'none',
          title: '保存失敗,請稍後再試~'
        })
      }
    },
    this
  )
}
// 繪制文字(帶換行超出省略…功能)
function drawText(ctx, item) {
  let { content, width, maxLine, left, top, lineHeight, color, fontSize } = item
  content = String(content)
  lineHeight = (lineHeight || 1.3) * fontSize
  // 字體
  ctx.setFontSize(fontSize)
  // 顏色
  ctx.setFillStyle(color)
  // 文本處理
  let strArr = content.split('')
  let row = []
  let temp = ''
  for (let i = 0; i < strArr.length; i++) {
    if (ctx.measureText(temp).width < width) {
      temp += strArr[i]
    } else {
      i-- //這裡添加瞭i-- 是為瞭防止字符丟失,效果圖中有對比
      row.push(temp)
      temp = ''
    }
  }
  row.push(temp) // row有多少項則就有多少行
  //如果數組長度大於2,現在隻需要顯示兩行則隻截取前兩項,把第二行結尾設置成'...'
  if (row.length > maxLine) {
    let rowCut = row.slice(0, maxLine)
    let rowPart = rowCut[1]
    let text = ''
    let empty = []
    for (let i = 0; i < rowPart.length; i++) {
      if (ctx.measureText(text).width < width) {
        text += rowPart[i]
      } else {
        break
      }
    }
    empty.push(text)
    let group = empty[0] + '...' //這裡隻顯示兩行,超出的用...表示
    rowCut.splice(1, 1, group)
    row = rowCut
  }
  // 把文本繪制到畫佈中
  for (let i = 0; i < row.length; i++) {
    // 一次渲染一行
    ctx.fillText(row[i], left, top + i * lineHeight, width)
  }
}
// 獲取圖片信息
function getImageTempPath(url) {
  return new Promise((resolve) => {
    if (url.includes('http')) {
      uni.downloadFile({
        url,
        success: (res) => {
          uni.getImageInfo({
            src: res.tempFilePath,
            success: (res) => {
              resolve(res.path)
            }
          })
        },
        fail: (res) => {
          console.log('圖片下載失敗:', res.errMsg)
        }
      })
    } else {
      resolve(url)
    }
  })
}

以上就是uniapp封裝canvas組件無腦繪制保存小程序分享海報的詳細內容,更多關於uniapp封裝canvas的資料請關註WalkonNet其它相關文章!

推薦閱讀: