uniapp封裝canvas組件無腦繪制保存小程序分享海報
正文
小程序分享海報想必大家都做過,受微信的限制,無法直接分享小程序到朋友圈(雖然微信開發(fā)者工具基礎(chǔ)庫從2.11.3開始支持分享小程序到朋友圈,但目前仍處于Beta中),所以生成海報仍然還是主流方式,通常是將設(shè)計稿通過canvas繪制成圖片,然后保存到用戶相冊,用戶通過圖片分享小程序
但是,如果不是對canvas很熟悉的話,每次都要去學(xué)習(xí)canvas的Api,挺麻煩的。我只想“無腦”的完成海報的繪制,就像是把每一個元素固定定位一樣,告訴它你要插入的是圖片、還是文字,然后再傳入坐標(biāo)、寬高就能把在canvas繪制出內(nèi)容。
怎么做呢?接著往下看(注:本文是基于uniapp Vue3搭建的小程序?qū)崿F(xiàn)的海報功能)

配置項
| 屬性 | 說明 | 可選值 |
|---|---|---|
| type | 元素類型 | image、text、border、block(一般用于設(shè)置背景色塊) |
| left | 元素距離canvas左側(cè)的距離 | 數(shù)字或者center,center表示水平居中,比如10、'center' |
| right | 元素距離canvas右側(cè)的距離 | 數(shù)字,比如10 |
| top | 元素距離canvas頂部的距離 | 數(shù)字,比如10 |
| bottom | 元素距離canvas底部的距離 | 數(shù)字,比如10 |
| width | 元素寬度 | 數(shù)字,比如20 |
| height | 元素高度 | 數(shù)字,比如20 |
| url | type為image時的圖片地址 | 字符串 |
| color | type為text、border、block時的顏色 | 字符串,比如#333333 |
| content | type為text時的文本內(nèi)容 | 字符串 |
| fontSize | type為text時的字體大小 | 數(shù)字,比如16 |
| radius | type為image、block時圓角,200表示圓形 | 數(shù)字,比如10 |
| maxLine | type為text時限制最大行數(shù),超出以…結(jié)尾 | 數(shù)字,比如2 |
| lineHeight | type為text時的行高,倍數(shù) | 數(shù)字,比如1.5,默認(rèn)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
},
// 長按掃碼 > 瀏覽臻品 > 獲取權(quán)益
{
type: 'text',
content: '長按掃碼 > 瀏覽臻品 > 獲取權(quán)益',
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
}
]
// 調(diào)用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
}
})
// 導(dǎo)出方法給父組件用
defineExpose({
onDraw(options, callback) {
createPoster.call(
// 當(dāng)前上下文
proxy,
// canvas相關(guān)信息
{
id: 'myCanvas',
width: props.width,
height: props.height
},
// 元素集合
options,
// 回調(diào)函數(shù)
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]
// 最大行數(shù):maxLine 字體大?。篺ontSize 行高:lineHeight
// 類型 顏色 left right top bottom 寬 高 圓角 圖片 文本內(nèi)容
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
// 根據(jù)字體大小計算出寬度
myCanvas.setFontSize(fontSize)
// 內(nèi)容寬度:傳了寬度就去寬度,否則取字體本身寬度
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有多少項則就有多少行
//如果數(shù)組長度大于2,現(xiàn)在只需要顯示兩行則只截取前兩項,把第二行結(jié)尾設(shè)置成'...'
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組件無腦繪制保存小程序分享海報的詳細(xì)內(nèi)容,更多關(guān)于uniapp封裝canvas的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS數(shù)據(jù)分析數(shù)據(jù)去重及參數(shù)序列化示例
這篇文章主要為大家介紹了JS數(shù)據(jù)分析數(shù)據(jù)去重及參數(shù)序列化示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
TypeScript中Module使用區(qū)別及模塊路徑解析規(guī)則
這篇文章主要為大家介紹了TypeScript中Module使用區(qū)別及模塊路徑解析規(guī)則,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
前端JavaScript徹底弄懂函數(shù)柯里化curry
隨著主流JavaScript中函數(shù)式編程的迅速發(fā)展, 函數(shù)柯里化在許多應(yīng)用程序中已經(jīng)變得很普遍。 了解它們是什么,它們?nèi)绾喂ぷ饕约叭绾纬浞掷盟鼈兎浅V匾?。本篇文章小編九向大家詳?xì)介紹JavaScript函數(shù)柯里化,需要的小伙伴可以參考下面文字內(nèi)容2021-09-09
THREE.js添加多個castShadow光源報錯解決及原因分析
這篇文章主要介紹了THREE.js添加多個castShadow的光源報錯解決及原因分析2023-06-06
Server-sent?events實時獲取服務(wù)端數(shù)據(jù)技術(shù)詳解
這篇文章主要為大家介紹了Server-sent?events實時獲取服務(wù)端數(shù)據(jù)技術(shù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02

