示例图片,差不多就这个样子
注意:
1. 先合成一张图
2. 图片 用自己项目中的 上传文件 接口转一下 路径
3. uni.downloadFile 再下载
<template>
<view class="canvas">
<canvas canvas-id="myCanvas" :style="{width: width+'px',height: height+'px'}"></canvas>
</view>
</template>
<!--
list参数说明:
图片渲染:
type: 'image',
x: X轴位置,
y: Y轴位置,
path: 图片路径,
width: 图片宽度,
height: 图片高度,
rotate: 旋转角度
shape: 形状,默认无,可选值:circle 圆形
area: {x,y,width,height} // 绘制范围,超出该范围会被剪裁掉 该属性与shape暂时无法同时使用,area存在时,shape失效
文字渲染:
type: 'text',
x: X轴位置,
y: Y轴位置,
text: 文本内容,
size: 字体大小,
textBaseline: 基线 默认top 可选值:'top'、'bottom'、'middle'、'normal'
color: 颜色
多行文字渲染:
type: 'textarea',
x: X轴位置,
y: Y轴位置,
width:换行的宽度
height: 高度,溢出会展示“...”
lineSpace: 行间距
text: 文本内容,
size: 字体大小,
textBaseline: 基线 默认top 可选值:'top'、'bottom'、'middle'、'normal'
color: 颜色
-->
<script>
export default {
name: "Poster",
props: {
// 绘制队列
list: {
type: Array,
required: true
},
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true
},
backgroundColor: {
type: String,
default: 'rgba(0,0,0,0)'
}
},
emit: ['on-success', 'on-error'],
data() {
return {
posterUrl: '',
ctx: null, //画布上下文
counter: -1, //计数器
drawPathQueue: [], //画图路径队列
};
},
computed: {
// 渲染列表中图片的数量
imgCount() {
return this.list.filter(item => item.type == 'image').length;
},
successImageCount() {
return this.drawPathQueue.filter(item => item.type == 'image' && item.success).length;
}
},
watch: {
drawPathQueue: {
handler(newVal, oldVal) {
// 总图片数量不为零 并且 加载图片数量与总图片数量不相等,不进行绘制
// 没图片或图片都加载完成了,开始绘制
console.log(this.imgCount, this.successImageCount)
if (!this.imgCount && newVal != this.imgCount) {
return;
}
// 绘制单行文字
const fillText = (textOptions) => {
this.ctx.setFillStyle(textOptions.color)
this.ctx.setFontSize(textOptions.size)
this.ctx.setTextBaseline(textOptions.textBaseline || 'top')
this.ctx.fillText(textOptions.text, textOptions.x, textOptions.y)
}
// 绘制段落
const fillParagraph = (textOptions) => {
this.ctx.setFontSize(textOptions.size)
let tempOptions = JSON.parse(JSON.stringify(textOptions));
// 如果没有指定行间距则设置默认值
tempOptions.lineSpace = tempOptions.lineSpace ? tempOptions.lineSpace : 10;
// 获取字符串
let str = textOptions.text;
// 计算指定高度可以输出的最大行数
let lineCount = Math.floor((tempOptions.height || this.height + tempOptions.lineSpace) / (
tempOptions.size +
tempOptions.lineSpace))
// 初始化单行宽度
let lineWidth = 0;
let lastSubStrIndex = 0; //每次开始截取的字符串的索引
// 构建一个打印数组
let strArr = str.split("");
let drawArr = [];
let text = "";
while (strArr.length) {
let word = strArr.shift()
text += word;
let textWidth = this.ctx.measureText(text).width;
if (textWidth > textOptions.width) {
// 因为超出宽度 所以要截取掉最后一个字符
text = text.substr(0, text.length - 1)
drawArr.push(text)
text = "";
// 最后一个字还给strArr
strArr.unshift(word)
} else if (!strArr.length) {
drawArr.push(text)
}
}
if (drawArr.length > lineCount) {
// 超出最大行数
drawArr.length = lineCount;
let pointWidth = this.ctx.measureText('...').width;
let wordWidth = 0;
let wordArr = drawArr[drawArr.length - 1].split("");
let words = '';
while (pointWidth > wordWidth) {
words += wordArr.pop();
wordWidth = this.ctx.measureText(words).width
}
drawArr[drawArr.length - 1] = wordArr.join('') + '...';
}
// 打印
// 记录初始y值
const y = tempOptions.y;
for (let i = 0; i < drawArr.length; i++) {
tempOptions.y = y + tempOptions.size * i + tempOptions.lineSpace * i; // y的位置
tempOptions.text = drawArr[i]; // 绘制的文本
fillText(tempOptions)
}
}
// 绘制背景
this.ctx.setFillStyle(this.backgroundColor);
this.ctx.fillRect(0, 0, this.width, this.height);
/* 所有元素入队则开始绘制 */
if (this.drawPathQueue.length === this.list.length) {
try {
// console.log('生成的队列:' + JSON.stringify(this.drawPathQueue));
console.log('开始绘制...')
for (let i = 0; i < this.drawPathQueue.length; i++) {
for (let j = 0; j < this.drawPathQueue.length; j++) {
let current = this.drawPathQueue[j]
/* 按顺序绘制 */
if (current.index === i) {
/* 文本绘制 */
if (current.type === 'text') {
console.log('绘制文本:' + current.text);
fillText(current)
this.counter--
}
/* 多行文本 */
if (current.type === 'textarea') {
console.log('绘制段落:' + current.text);
fillParagraph(current)
this.counter--
}
/* 图片绘制 */
if (current.type === 'image') {
console.log('绘制图片:' + current.path);
if (current.area) {
// 绘制绘图区域
this.ctx.save()
this.ctx.beginPath(); //开始绘制
this.ctx.rect(current.area.x, current.area.y, current.area.width, current
.area
.height)
this.ctx.clip();
// 设置旋转中心
let offsetX = current.x + Number(current.width) / 2;
let offsetY = current.y + Number(current.height) / 2;
this.ctx.translate(offsetX, offsetY)
let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
this.ctx.rotate(degrees * Math.PI / 180)
this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
current.width, current.height)
this.ctx.closePath();
this.ctx.restore(); // 恢复之前保存的上下文
} else if (current.shape == 'circle') {
this.ctx.save(); // 保存上下文,绘制后恢复
this.ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
let width = (current.width / 2 + current.x);
let height = (current.height / 2 + current.y);
let r = current.width / 2;
this.ctx.arc(width, height, r, 0, Math.PI * 2);
//画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
this.ctx.clip();
// 设置旋转中心
let offsetX = current.x + Number(current.width) / 2;
let offsetY = current.y + Number(current.height) / 2;
this.ctx.translate(offsetX, offsetY)
let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
this.ctx.rotate(degrees * Math.PI / 180)
this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
current.width, current.height)
this.ctx.closePath();
this.ctx.restore(); // 恢复之前保存的上下文
} else {
this.ctx.drawImage(current.path, current.x, current.y, current.width,
current
.height)
}
this.counter--
}
}
}
}
} catch (err) {
console.log(err)
this.$emit('on-error', err)
}
}
},
deep: true
},
counter(newVal, oldVal) {
if (newVal === 0) {
/* draw完不能立刻转存,需要等待一段时间 */
// draw回调的正确用法。。。。 一定要仔细看文档啊
this.ctx.draw(false, () => {
console.log('final counter', this.counter);
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
console.log('in canvasToTempFilePath');
// 在H5平台下,tempFilePath 为 base64
console.log('图片已生成:', res.tempFilePath)
this.posterUrl = res.tempFilePath;
this.$emit('on-success', res.tempFilePath)
},
fail: (res) => {
console.log(res)
}
}, this)
})
}
}
},
mounted() {
this.ctx = uni.createCanvasContext('myCanvas', this)
// this.generateImg()
console.log('mounted')
},
methods: {
create() {
this.generateImg()
},
generateImg() {
console.log('generateimg')
this.counter = this.list.length
this.drawPathQueue = []
/* 将图片路径取出放入绘图队列 */
for (let i = 0; i < this.list.length; i++) {
console.log('for', i)
let current = this.list[i]
current.index = i
/* 如果是文本直接放入队列 */
if (current.type === 'text' || current.type === 'textarea') {
this.drawPathQueue.push(current)
continue
}
/* 图片需获取本地缓存path放入队列 */
uni.getImageInfo({
src: current.path,
success: (res) => {
current.path = res.path;
current.success = true; // 表示加载完成
this.drawPathQueue.push(current)
},
fail: (err) => {
console.error(err)
}
})
}
},
saveImg() {
console.log('保存')
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
console.log(res,'是看是快快快是')
// 在H5平台下,tempFilePath 为 base64
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: (resres) => {
console.log('save success',resres);
}
});
}
})
}
}
}
</script>
<style lang="scss" scoped>
.canvas {
position: fixed;
top: 100rpx;
left: 750rpx;
}
</style>
<template>
<view class="haibaobox">
<image :src="poster" class="bgimg"></image>
<poster ref="poster" :list="list" background-color="#FFF" :width="750" :height="1334" @on-success="posterSuccess" @on-error="posterError"></poster>
<view class="commonBtn u-m-t-32" @click="saveImgFun">保存图片</view>
</view>
</template>
<script>
import Poster from '@/components/Poster'
import baseUrl from "@/api/baseUrl.js"
import {
dtParamInfo,
userInfo
} from "@/api/index/index.js"
export default {
components: {Poster},
data() {
return {
commonInfo:{}, // 获取用户信息,二维码
src:'', // 获取背景图片
poster: '', // 展示在 页面的图片
list: [],
uploadImg:'',//保存到手机的图片路径
}
},
onLoad(option) {},
onShow() {
this.getUserInfo();
},
methods:{
// 用户详情
getUserInfo(){
let data = {
userId: uni.getStorageSync('id')
}
userInfo(data).then(res => {
this.commonInfo = res.data;
this.getimg();
});
},
getimg(){
dtParamInfo({id:3}).then(res => {
this.src = res.data.content;
this.start();
});
},
posterError(err) {
console.log(err,'错误=======')
},
posterSuccess(url) {
// 生成成功,会把临时路径在这里返回
let that = this;
this.poster = url;
console.log(url,'成功======')
// 生成的图片 用 自己项目中的上传文件接口 转一下图片
// 不然 下载图片的时候 会报错 downloadfile fail url scheme is invalid
uni.uploadFile({
url: baseUrl + 'upload',
filePath: that.poster,
name: 'file',
formData: {
file: that.poster
},
header: {},
success: r => {
let imgData = JSON.parse(r.data)
this.uploadImg = imgData.url; // 下载的图片
}
});
},
start() {
// 赋值需要渲染的信息
this.list = [{
type: 'image',
// path替换成你自己的图片,注意需要在小程序开发设置中配置域名
path: this.src,
x: 0,
y: 0,
width: 750,
height: 1260
},
{
type: 'image',
path: this.commonInfo.invitationCode,
x: (750 - 272) / 2,
y: 800,
width: 272,
height: 272
}
];
// // 生成图片
this.$nextTick(() => {
// 要放在$nextTick()里,不然会空白
this.$refs.poster.create();
})
},
// 保存图片
saveImgFun(){
const that = this;
uni.downloadFile({
url: that.uploadImg,
success: res => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function() {
uni.showToast({
title: '保存成功',
duration: 2000
})
},
fail: function() {
uni.showToast({
title: '保存失败',
duration: 2000
})
}
})
} else {
uni.showToast({
title: '保存失败',
duration: 2000
})
}
}
});
}
},
}
</script>
<style lang='scss' scoped>
.haibaobox{
.bgimg{
position: relative;
width:750rpx;
height:85vh;
}
.maimg{
position: absolute;
left:0;
right: 0;
bottom: 260rpx;
width: 272rpx;
height: 272rpx;
margin: 0 auto;
}
}
/* 按钮 */
.commonBtn{
margin: 0 auto;
width: 686rpx;
height: 88rpx;
background: #8D43AA;
border-radius: 44rpx;
font-size: 34rpx;
color: #FFFFFF;
line-height: 88rpx;
text-align: center;
}
</style>