創建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>
image.png
image.png