小程序分享海报,由于微信的限制,暂时无法直接分享小程序到朋友圈(虽然微信开发者工具基础库从2.11.3开始支持分享小程序到朋友圈,但目前仍处于Beta中)所以生成海报仍然还是主流方式
主体思路:将设计稿通过canvas生成图片,然后保存到用户相册,用户再通过图片分享小程序
属性 | 说明 | 可选值 |
---|---|---|
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) } }) }