创建canvas绘制图片的组件 - 代码如下
<template>
<view>
<canvas canvas-id="canvas" :style="{'width': width + 'px', 'height': height + 'px'}" style="position: fixed; left: -9999px; top: -9999px;"></canvas>
</view>
</template>
<script>
export default {
name: "drawImage",
props: {
// 绘制图片的尺寸
imageSize: {
type: Object,
default: () => {},
},
// canvas绘制的数据
canvasData: {
type: Array,
default: () => [],
},
// 是否开始绘制
isDraw: {
type: Boolean,
default: false,
},
},
data() {
return {
// 屏幕宽度
screenWidth: 0,
// canvas画布的宽度
width: 0,
// canvas画布的高度
height: 0,
// 当前图片放大倍数 - 清晰度
count: 2,
};
},
mounted() {
// 这段代码主要是为了防止uni-app在app端IOS的问题,因为在IOS 画布过大可能会导致绘制空白 - 可以自行调整
// #ifdef APP-PLUS
if(uni.getSystemInfoSync().platform === 'ios') {
this.count = 1.8;
}
// #endif
},
watch: {
// 监听是否开始绘制
isDraw: async function(newVal) {
if(newVal) {
this.getSystemInfo();
this.getImageByCanvasData(this.canvasData);
}
}
},
methods: {
/** 获取系统信息 */
getSystemInfo() {
const { screenWidth } = uni.getSystemInfoSync();
this.width = this.imageSize.width * this.count;
this.height = this.imageSize.height * this.count;
this.screenWidth = screenWidth;
},
/**
* 通过数据绘制图片
* @param {array} data canvas绘制的数组
* 格式:每一项的数据
* { type: 'rect', attr: { color: '', x, y, width, height, radian_1, radian_2, radian_3, radian_4 } }
* { type: 'image', attr: { image: '', x, y, width, height, radian_1, radian_2, radian_3, radian_4 } }
* { type: 'text', attr: { text: '', x, y, color, size, weight, writingMode } }
* */
async getImageByCanvasData(data) {
// 获取canvas上下文对象
const context = uni.createCanvasContext("canvas", this);
// 清空画布
context.clearRect(0, 0, this.width * this.count, this.height * this.count);
for(const item of data) {
// 判断类型
if(item.type === 'rect') {
// 绘制圆边矩形
this.drawRoundRectangular(
context,
item.attr.color,
item.attr.x * this.count,
item.attr.y * this.count,
item.attr.width * this.count,
item.attr.height * this.count,
item.attr.radian_1 ? item.attr.radian_1 * this.count : 0,
item.attr.radian_2 ? item.attr.radian_2 * this.count : -1,
item.attr.radian_3 ? item.attr.radian_3 * this.count : -1,
item.attr.radian_4 ? item.attr.radian_4 * this.count : -1
);
}
else if(item.type === 'image' && item.attr.image) {
// 绘制圆边图片
await this.drawRoundImageToCanvas(
context,
item.attr.image,
item.attr.x * this.count,
item.attr.y * this.count,
item.attr.width * this.count,
item.attr.height * this.count,
item.attr.radian_1 ? item.attr.radian_1 * this.count : 0,
item.attr.radian_2 ? item.attr.radian_2 * this.count : -1,
item.attr.radian_3 ? item.attr.radian_3 * this.count : -1,
item.attr.radian_4 ? item.attr.radian_4 * this.count : -1
);
}
else if(item.type === 'text' && item.attr.text) {
// 绘制文本
this.drawTextToCanvas(
context,
item.attr.text,
item.attr.x * this.count,
item.attr.y * this.count,
item.attr.color,
parseInt(item.attr.size ? item.attr.size * this.count : 16 * this.count),
item.attr.weight,
item.attr.writingMode ? item.attr.writingMode : 'initial'
);
}
}
// 绘制图片
context.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: 'canvas',
x: 0,
y: 0,
width: this.width,
height: this.height,
destWidth: this.width,
height: this.height,
success: res => {
this.$emit("generateImageSuccessful", res.tempFilePath);
},
}, this);
});
},
/**
* 绘制文本
* @param {string} context Canvase的实例
* @param {string} text 文本内容
* @param {number} x 矩形的x坐标
* @param {number} y 矩形的y坐标
* @param {number} color 文本颜色
* @param {number} size 字体的大小
* @param {string} weight 字体的粗细
* @param {string} writingMode 字体的排列方式 - initial 水平 tb 垂直
* */
drawTextToCanvas(context, text, x, y, color = '#000', size = 16, weight = '400', writingMode = 'initial') {
context.fillStyle = color;
context.font = `normal ${weight} ${size}px sans-serif`;
if(writingMode === 'tb') {
const temp = text.split("");
for(let i = 0; i < temp.length; i++) {
context.fillText(temp[i], x, i * size + y);
}
}
else {
// 判断是否有换行符
const temp = text.split("\n");
for(let i = 0; i < temp.length; i++) {
context.fillText(temp[i], x, i * size + y + i * size * 0.2); // i * size * 0.2 增加换行的间距
}
}
},
/**
* 绘制圆边矩形
* @param {string} context Canvase的实例
* @param {string} color 填充的颜色
* @param {number} x 矩形的x坐标
* @param {number} y 矩形的y坐标
* @param {number} width 矩形的宽度
* @param {number} height 矩形的高度
* @param {number} height 图片的高度
* @param {number} radian_1 弧度大小 - radian_1 右上 的弧度, 1个参数代表全部
* @param {number} radian_2 弧度大小 - radian_2 右下 的弧度
* @param {number} radian_3 弧度大小 - radian_3 左下 的弧度
* @param {number} radian_4 弧度大小 - radian_4 左上 的弧度
* */
drawRoundRectangular(context, color, x, y, width, height, radian_1 = 0, radian_2 = -1, radian_3 = -1, radian_4 = -1) {
context.save();
this.drawRoundPath(context, x, y, width, height, radian_1, radian_2, radian_3, radian_4);
context.setFillStyle(color);
context.fill();
context.restore();
},
/**
* 绘制圆角图片
* @param {string} context Canvase的实例
* @param {string} image 图片地址
* @param {number} x 图片的x坐标
* @param {number} y 图片的y坐标
* @param {number} width 图片的宽度
* @param {number} height 图片的高度
* @param {number} radian_1 弧度大小 - radian_1 右上 的弧度, 1个参数代表全部
* @param {number} radian_2 弧度大小 - radian_2 右下 的弧度
* @param {number} radian_3 弧度大小 - radian_3 左下 的弧度
* @param {number} radian_4 弧度大小 - radian_4 左上 的弧度
* */
async drawRoundImageToCanvas(context, image, x, y, width, height, radian_1 = 0, radian_2 = -1, radian_3 = -1, radian_4 = -1) {
context.save();
this.drawRoundPath(context, x, y, width, height, radian_1, radian_2, radian_3, radian_4);
context.drawImage(await this.handleNetworkImgaeTransferTempImage(image), x, y, width, height);
context.restore();
},
/**
* 绘制圆边路径
* @param {string} context Canvase的实例
* @param {number} x 图片的x坐标
* @param {number} y 图片的y坐标
* @param {number} width 图片的宽度
* @param {number} height 图片的高度
* @param {number} radian_1 弧度大小 - radian_1 右上 的弧度, 1个参数代表全部
* @param {number} radian_2 弧度大小 - radian_2 右下 的弧度
* @param {number} radian_3 弧度大小 - radian_3 左下 的弧度
* @param {number} radian_4 弧度大小 - radian_4 左上 的弧度
* */
drawRoundPath(context, x, y, width, height, radian_1 = 0, radian_2 = -1, radian_3 = -1, radian_4 = -1) {
// 设置弧度
radian_2 = radian_2 === -1 ? radian_1 : radian_2;
radian_3 = radian_3 === -1 ? radian_1 : radian_3;
radian_4 = radian_4 === -1 ? radian_1 : radian_4;
// 创建路径 - 绘制带圆边的矩形
context.beginPath();
context.moveTo(x + width / 2, y);
context.arcTo(x + width, y, x + width, y + height, radian_1);
context.arcTo(x + width, y + height, x, y + height, radian_2);
context.arcTo(x, y + height, x, y, radian_3);
context.arcTo(x, y, x + width, y, radian_4);
// 关闭路径 - 结束绘制
context.closePath();
context.strokeStyle = "transparent";
context.stroke();
context.clip();
},
/** 将网络图片变成临时图片 */
handleNetworkImgaeTransferTempImage(url) {
return new Promise(resolve => {
if(url.indexOf('http') === 0) {
uni.downloadFile({
url,
success: res => {
resolve(res.tempFilePath);
}
});
}
else {
resolve(url);
}
});
},
}
}
</script>
<style scoped lang="scss">
</style>
在页面中的使用
<template>
<view class="sharePoster">
<view class="content">
<!-- 显示一下绘制完成后的路径 -->
<image :src="tempImage" style="width: 375px; height: 667px;"></image>
<Index :isDraw="isDraw" :canvasData="canvasData" :imageSize="{width: 375, height: 667}"
@generateImageSuccessful="generateImageSuccessful" />
<view class="btn2" @click="saveAlbum">保存到相册</view>
</view>
</view>
</template>
<script>
import Index from "../index/index.vue"
export default {
components: {
Index
},
data() {
return {
option: {
title: '详情'
},
poster: "",
// 是否开始绘制
isDraw: false,
/**
* 需要绘制的图片数据 - 具体参数需要看组件内的
* { type: 'rect', attr: { color: '', x, y, width, height, radian_1, radian_2, radian_3, radian_4 } }
* { type: 'image', attr: { image: '', x, y, width, height, radian_1, radian_2, radian_3, radian_4 } }
* { type: 'text', attr: { text: '', x, y, color, size, weight, writingMode } }
* */
canvasData: [],
// 临时路径
tempImage: "",
goodsList:[
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.81"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.82"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.83"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.84"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.85"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.86"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.87"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.88"
},
{
image:'/static/images/hb.png',
text:'商品名字商品',
price:"¥12.89"
},
]
};
},
onLoad(option) {
this.option = option
// this.getCreatPoster(this.option.id)
this.canvasData = [
// { type: 'rect', attr: { color: '#fff', x: 0, y: 0, width: 375, height: 667 } },
{ type: 'image', attr: { image: '/static/images/hb.png', x: 0, y: 0, width: 375, height: 667, } },
{ type: 'text', attr: { text: this.getString('开心一族漂亮家园推荐语推', 7), x: 110, y: 80, color: 'red', size: 20, weight: '500', } },
{ type: 'text', attr: { text: this.getString('推荐语推荐语推荐语推荐语推荐语推荐语推荐语推荐语', 13), x: 90, y: 108, color: '#000', size: 14, } },
...this.getGoodsArr(),
{ type: 'text', attr: { text: '共3个品种', x: 150, y: 515, color: '#000', size: 14, } },
{ type: 'image', attr: { image: '/static/images/hb.png', x: 150, y: 545, width: 75, height: 75, } },
{ type: 'text', attr: { text: '扫码查看海报详情', x: 130, y: 640, color: '#fff', size: 14, } },
];
this.isDraw = true;
},
methods: {
getGoodsArr(){
let data = []
let canvasData = []
let text_x=80,img_x=75,img_y=125
for (let i = 0; i < this.goodsList.length; i++) {
if (data[parseInt(i / 3)] instanceof Array) {
data[parseInt(i / 3)].push(this.goodsList[i]);
} else {
let temp = [this.goodsList[i]];
data[parseInt(i / 3)] = temp;
}
}
data.forEach((item,index)=>{
let canvasData_init =[]
item.forEach((item1,index1)=>{
canvasData_init.push(...[
{ type: 'image', attr: { image: item1.image, x: img_x*(index1+1), y: img_y*(index+1), width: 75, height: 75, } },
{ type: 'text', attr: { text: this.getString(item1.text, 4), x: text_x*(index1+1), y: img_y*(index+1)+90, color: '#000', size: 14, } },
{ type: 'text', attr: { text: this.getString(item1.price, 6), x: text_x*(index1+1), y: img_y*(index+1)+110, color: 'red', size: 14, } },
])
})
console.log("canvasData_init",canvasData_init)
canvasData.push(...canvasData_init)
})
return canvasData
},
getString(string, num) {
if (string.length > num) {
string = string.substring(0, num) + "..."
}
return string
},
/** 绘制成功后的回调 - 返回一个临时路径 */
generateImageSuccessful(image) {
this.tempImage = image;
},
getCreatPoster(id) {
this.$u.api.getCreatPoster({ id }).then(res1 => {
this.poster = res1.poster
})
},
saveImageToPhotosAlbum() {
uni.saveImageToPhotosAlbum({
filePath: this.tempImage,
success: function () {
uni.showToast({
title: '保存成功,请从相册选择再分享',
icon: "none",
duration: 2000
})
},
fail: err => {
uni.showToast({
title: '保存失败',
icon: "none",
duration: 1000
})
}
});
},
saveAlbum() {
uni.getSetting({//获取用户的当前设置
success: (res) => {
if (res.authSetting['scope.writePhotosAlbum']) {//验证用户是否授权可以访问相册
this.saveImageToPhotosAlbum();
} else {
uni.authorize({//如果没有授权,向用户发起请求
scope: 'scope.writePhotosAlbum',
success: () => {
this.saveImageToPhotosAlbum();
},
fail: () => {
uni.showToast({
title: "请打开保存相册权限,再点击保存相册分享",
icon: "none",
duration: 3000
});
setTimeout(() => {
uni.openSetting({//调起客户端小程序设置界面,让用户开启访问相册
success: (res2) => {
}
});
}, 3000);
}
})
}
}
})
},
},
};
</script>
<style lang="scss">
.sharePoster {
background: #F5F5F5;
min-height: 100vh;
padding-bottom: 30rpx;
image {
width: 100%;
}
}
.btn2 {
width: 90%;
text-align: center;
padding: 18rpx 0;
background: linear-gradient(270deg, rgba(96, 138, 252, 1) 0%, rgba(96, 186, 252, 1) 100%);
border-radius: 190rpx;
color: #fff;
font-size: 28rpx;
margin: 30rpx auto;
}
</style>