uniapp封装canvas组件无脑绘制保存小程序分享海报

更新时间:2022-08-08 10:46:39

小程序分享海报想必大家都做过,受微信的限制,无法直接分享小程序到朋友圈(虽然微信开发者工具基础库从2.11.3开始支持分享小程序到朋友圈,但目前仍处于Beta中),所以生成海报仍然还是主流方式,通常是将设计稿通过canvas绘制成图片,然后保存到用户相册,用户通过图片分享小程序

但是,如果不是对canvas很熟悉的话,每次都要去学习canvas的Api,挺麻烦的。我只想“无脑”的完成海报的绘制,就像是把每一个元素固定定位一样,告诉它你要插入的是图片、还是文字,然后再传入坐标、宽高就能把在canvas绘制出内容。

怎么做呢?接着往下看(注:本文是基于uniapp Vue3搭建的小程序实现的海报功能)

b1ce741fe55e15e6b67c794a84534011_20220807091515012.png

配置项

type元素类型image、text、border、block(一般用于设置背景色块)left元素距离canvas左侧的距离数字或者center,center表示水平居中,比如10、'center'right元素距离canvas右侧的距离数字,比如10top元素距离canvas顶部的距离数字,比如10bottom元素距离canvas底部的距离数字,比如10width元素宽度数字,比如20height元素高度数字,比如20urltype为image时的图片地址字符串colortype为text、border、block时的颜色字符串,比如#333333contenttype为text时的文本内容字符串fontSizetype为text时的字体大小数字,比如16radiustype为image、block时圆角,200表示圆形数字,比如10maxLinetype为text时限制最大行数,超出以…结尾数字,比如2lineHeighttype为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)
}
  })
}

uniappcanvas