前端图片合并
上一篇中说到了电子签名,需求是用户签完名需要把名字放在某一个需要签名的位置,这里采用canvas进行图片的合并操作:
话不多说,直接上代码
<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: [], //画图路径队列
};
},
watch: {
drawPathQueue(newVal, oldVal) {
// 绘制单行文字
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 + 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('') + '...';
}
// 打印
for (let i = 0; i < drawArr.length; i++) {
tempOptions.y = tempOptions.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 (newVal.length === this.list.length) {
try {
// console.log('生成的队列:' + JSON.stringify(newVal));
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)
}
}
},
counter(newVal, oldVal) {
if (newVal === 0) {
this.ctx.draw()
/* draw完不能立刻转存,需要等待一段时间 */
setTimeout(() => {
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)
}, 1000)
}
}
},
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++) {
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
this.drawPathQueue.push(current)
}
})
}
},
saveImg() {
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
// 在H5平台下,tempFilePath 为 base64
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
console.log('save success');
}
});
}
})
}
}
}
</script>
<style lang="scss" scoped>
.canvas {
position: fixed;
top: 100rpx;
left: 750rpx;
}
</style>
上面是组件的格式
调用如下:
<poster v-if="list.length" :list="list" background-color="#FFF" :width="750" :height="1334" @on-success="posterSuccess" ref="poster"></poster>
import Poster from '@/components/zhangyuhao-poster/Poster.vue'
components:{
Poster
},
mounted(){ this.list = [{ type: 'image',//类型 // path替换成你自己的图片,注意需要在小程序开发设置中配置域名 path: 'https://xxx.com/cns4.jpg',//图片地址 x: 0,//开始位置 y: 0,//开始位置 width: 750,//宽 height: 750//搞 }, { type: 'image', path: e, x: 750-400, y: 400, width: 200, height: 200 } ] }, methods:{ posterError(err) { console.log(err) }, posterSuccess(url) { // 生成成功,会把临时路径在这里返回 console.log(url) } }
ok,详细的代码解释可以到https://ext.dcloud.net.cn/plugin?id=4611

浙公网安备 33010602011771号