uniapp利用canvas在小程序上面生成图片
Posted on: 2021年12月15日 at 10:05
画布准备
/**
* 开始画图
* @param {object} canvas 画布对象
* @param {object} data 渲染需要用的的数据
* @param {function} callback 完成后的回调
*/
function startDraw(canvas, data, callback) {
// 定义画布大小
canvas.width = 1080
canvas.height = 1826
const ctx = canvas.getContext('2d')
// 清空画布
ctx.clearRect(0, 0, 1080, 1826)
// 根据数据 `data` 开始画图
}
画图片
/**
* 画图片
* @param {object} canvas 画布对象
* @param {string} src 图片地址
* @param {number} x 画图开始位置x
* @param {number} y 画图开始位置y
* @param {number} width 画图宽度
* @param {number} height 画图高度
* @param {number} radius 矩形圆角弧度
* @param {string} fit 图片自适应
* @param {funciton} callback 回调函数
*/
function drawImage(canvas, src, x, y, width, height, radius, fit, callback) {
const ctx = canvas.getContext('2d')
ctx.save()
ctx.globalCompositeOperation = 'source-over'
if (radius > 0) {
ctx.beginPath()
drawRoundedRect(ctx, x, y, width, height, radius)
ctx.clip()
ctx.closePath()
}
const image = canvas.createImage()
image.onload = (res) => {
const imgWidth = image.width
const imgHeight = image.height
let startX = 0
let startY = 0
let imgW = imgWidth
let imgH = imgHeight
if (fit === 'cover') {
// 实现object-fit:cover图片裁切效果
if (imgHeight / imgWidth > height / width) {
imgH = (height * imgWidth) / width
startY = (imgHeight - imgH) / 2
} else {
imgW = (width * imgHeight) / height
startX = (imgWidth - imgW) / 2
}
} else if (fit === 'contain') {
if (imgHeight / imgWidth > height / width) {
imgH = (height * imgWidth) / width
}
}
ctx.drawImage(image, startX, startY, imgW, imgH, x, y, width, height)
ctx.restore()
if (callback) callback(image)
}
image.onerror = (err) => {
showError(err.message || '加载图片失败: ' + src)
}
image.src = src || ''
}
画圆角矩形
/**
* 画圆角矩形
* @param {object} ctx 画笔
* @param {number} x 画图开始位置x
* @param {number} y 画图开始位置y
* @param {number} width 画图宽度
* @param {number} height 画图高度
* @param {number|array} _radius 矩形圆角弧度
*/
function drawRoundedRect(ctx, x, y, width, height, _radius = 0) {
let radius = []
if (Array.isArray(radius)) {
radius = _radius
} else {
radius = [_radius, _radius, _radius, _radius]
}
const [lt, rt, rb, lb] = radius
ctx.moveTo(x + lt, y)
ctx.lineTo(x + width - lt, y)
// 右上角圆角
ctx.arc(x + width - rt, y + rt, rt, 1.5 * Math.PI, 2 * Math.PI)
ctx.lineTo(x + width, y + height - rb)
// 右下角圆角
ctx.arc(x + width - rb, y + height - rb, rb, 0, 0.5 * Math.PI)
ctx.lineTo(x + lb, y + height)
// 左下角圆角
ctx.arc(x + lb, y + height - lb, lb, 0.5 * Math.PI, 1 * Math.PI)
ctx.lineTo(x, y + radius)
// 左上角圆角
ctx.arc(x + lt, y + lt, lt, 1 * Math.PI, 1.5 * Math.PI)
}
画文本
需要支持超出最大宽度后,显示省略号。
根据显示宽度拆分文本
/**
* 根据宽度计算要显示文字
* @param {object} ctx 画笔
* @param {string} text 原始文字
* @param {number} fontSize 字体大小
* @param {boolean} bold 是否加粗
* @param {number} width 文字的容器宽度
* @param {number} maxLines 文字最多显示几行
*/
function splitText(ctx, text = '', fontSize, bold, width, maxLines) {
const textArr = []
let remainText = text
ctx.font = `normal ${bold ? 'bold' : 'normal'} ${fontSize}px sans-serif`
while (textArr.length < maxLines && remainText) {
// while (textArr.length < maxLines && ctx.measureText(remainText).width > width) {
// 估算一行能容纳多少文字
let assumTextNum = Math.floor(width / fontSize)
// 计算假定文字的宽度
let { width: assumTextWidth } = ctx.measureText(remainText.substr(0, assumTextNum))
if (assumTextWidth > width) {
while (assumTextWidth > width) {
assumTextWidth = ctx.measureText(remainText.substr(0, --assumTextNum)).width
}
} else {
while (assumTextWidth <= width && assumTextNum <= remainText.length) {
assumTextWidth = ctx.measureText(remainText.substr(0, ++assumTextNum)).width
}
assumTextWidth = ctx.measureText(remainText.substr(0, --assumTextNum)).width
}
let thisText = remainText.substr(0, assumTextNum)
remainText = remainText.substr(assumTextNum)
if (textArr.length === maxLines - 1 && remainText.length > 0) {
thisText = thisText.substring(0, thisText.length - 2) + '...'
}
textArr.push({
text: thisText,
width: assumTextWidth,
})
}
return textArr
}
渲染文本
/**
* 渲染显示文字
* @param {object} ctx 画笔
* @param {number} x 画图开始位置x
* @param {number} y 画图开始位置y
* @param {number} width 文字容器宽度
* @param {string} text 原始文字
* @param {object} style 文字样式
* @param {number} maxLines 文字最多显示几行
*/
function renderText(ctx, x, y, width, text, style, maxLines) {
if (!text) return
const { fontSize, lineHeight, color, bold } = style
const textArr = splitText(ctx, text, fontSize, bold, width, maxLines)
textArr.forEach((item, index) => {
ctx.fillStyle = color
ctx.textBaseline = 'top'
ctx.fillText(item.text, x, y + index * lineHeight + (lineHeight - fontSize) / 2)
})
}
导出画布
/**
* 导出画布到本地临时文件
* @param {object} canvas 画布
*/
function saveCanvas(canvas, callback) {
uni.canvasToTempFilePath({
canvas,
success: (res) => {
if (res.errMsg === 'canvasToTempFilePath:ok') {
if (callback) callback(res.tempFilePath)
} else {
showError(res.errMsg)
}
},
})
}
保存临时文件到相册
/**
* 保存文件到相册
* @param {string} tempImgPath 临时文件路径
*/
function savePosterFile(tempImgPath) {
if (!tempImgPath) return
uni.saveImageToPhotosAlbum({
filePath: tempImgPath,
success: (res) => {
uni.showToast({
title: '保存成功',
icon: 'success',
})
},
fail: (res) => {
if (res.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
uni.showModal({
title: '提示',
content: '需要您授权保存相册',
confirmText: '去授权',
success: (res) => {
if (res.cancel) return
uni.openSetting({
success: (auth) => {
if (auth.authSetting['scope.writePhotosAlbum']) {
uni.showModal({
title: '提示',
content: '获取权限成功,再次点击图片即可保存',
showCancel: false,
})
} else {
uni.showModal({
title: '提示',
content: '获取权限失败,将无法保存到相册哦~',
showCancel: false,
})
}
},
})
},
})
} else if (res.errMsg === 'saveImageToPhotosAlbum:fail cancel') {
uni.showToast({
title: '已取消',
icon: 'none',
})
} else {
uni.showToast({
title: '保存相册失败',
})
}
},
})
}
可能遇到的问题
提示 Tainted canvases may not be exported.
原因是画布跨域加载了非本域名的图片,导致画布被污染,无法导出为图片。解决办法添加 crossOrigin
属性
image.crossOrigin = 'anonymous'
图片加载的时候报错 CORS
跨域错误
如果服务端没有设置防盗链的话以及其他限制的话,这种情况很可能是浏览器缓存导致的,可以在图片链接后面加上随机数。
image.src = `${url}?${Math.random()}`
作者: wtto
文章标题: uniapp利用canvas在小程序上面生成图片
发表时间: 2021年12月15日
最后更新时间: 2024年2月5日
版权说明:
CC BY-NC-ND 4.0 DEED