2023年6月4日
vue-canvas海报绘制与保存图片
前段时间做了一个web小程序项目,业务上有保存海报并分享需求,所以咱就开始动手拆分-封装。
先看父组件:
<div>
<shareCanvasCom :orderXhrData="orderXhrData" :loginInfo="loginInfo" ref="child" :qrcodePath="qrcodePath"/>
</div>
<script>
import shareCanvasCom from "../components/shareCanvas/index.vue"
export default {
components: {
shareCanvasCom,
},
data() {
return {
orderXhrData:[],//订单信息
loginInfo:{},//用户信息
qrcodePath:'图片url',
}
}
</script>
看子组件
<div>
<div class="canvas-box">
<image v-if="tempFilePath !== ''" :src="tempFilePath" /></image>
<div style="position:relative;" v-if="tempFilePath !== ''">
<image src="../static/images/posterClose.png" ></image>
</div>
</div>
</div>
methods.js
import Apis from '@/common/apis/index'
import GroupBuyingApis from '@/common/apis/shop.js'
let isOneMove = false
export default {
data() {
return {
imgUrl: '',
windowWidth: 0,
windowHeight: 0,
ossKey: '',
tempFilePath: '',
photoPath: '',
// qrcodePath: '',
ctx: null,
cw: 654 / 2, // 原图宽度-20 因为box css 有20左右内边距
ch: 1022 / 2,
multiple: 0,
distance: 0, // 手指移动的距离
distanceDiff: 0,
scale: 1, // 缩放比例
positionx: 0,
positiony: 150,
starx: '',
stary: '',
movex: '',
movey: '',
path1: '../../../static/images/groupOrder/shareBg.png',
banner:'../../../static/images/groupOrder/shareBanner2.png',
wxCode:'',
path2: '',
path2w: 0,
path2h: 0,
};
},
onLoad(options) {
},
async beforeUpdate(){
let app = getApp();
// console.log(app);
this.imgUrl = app.globalData.imgUrl;
this.windowWidth = uni.getSystemInfoSync().windowWidth;
this.windowHeight = uni.getSystemInfoSync().windowHeight;
await this.$nextTick();
// this.photoPath = uni.getStorageSync('eye_photo_path')
this.photoPath ='../../../static/images/groupOrder/shareBg.png'
},
methods: {
async loadingWxCode(){
if(this.qrcodePath){ //typeof this.qrcodePath !== 'undefined' && this.qrcodePath.trim() !== ''
const img = new Image()
img.src = this.qrcodePath;
img.onload = (res) => {
this.drawRoundWxCode()
}
}
},
async drawRoundWxCode(){
//圆角图片
var avatar_width = 75; //绘制的图片宽度
var avatar_heigth = 75; //绘制的图片高度
var avatar_x = 247; //绘制的图片在画布上的位置
var avatar_y = 430; //绘制的图片在画布上的位置
this.ctx.save();
//先绘制一个遮罩层---
this.ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
this.ctx.arc(avatar_width / 2 + avatar_x, avatar_heigth / 2 + avatar_y, avatar_width / 2, 0, Math.PI * 2, false);
this.ctx.fillStyle="#fff";//设置填充颜色
this.ctx.fill();//开始填充
//正式绘制圆角图片
this.ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
this.ctx.arc(avatar_width / 2 + avatar_x, avatar_heigth / 2 + avatar_y, avatar_width / 2, 0, Math.PI * 2, false);
this.ctx.clip();//画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
this.ctx.drawImage(this.qrcodePath+'?t='+ new Date().getTime(), avatar_x+2.5, avatar_y+2.5, avatar_width-5, avatar_heigth-5); // 推进去图片,必须是https图片
this.ctx.restore(); //恢复之前保存的绘图上下文 恢复之前保存的绘图上下午即状态 还可以继续绘制
this.ctx.draw(true)
// this.onSave()
setTimeout(()=> { this.onSaveImages()}, 500)
},
// 开始画图(朋友圈分享图片)
async inCanvas() {
try {
const _this = this
let cw = this.cw
let ch = this.ch
this.ctx = uni.createCanvasContext('ShareCanvas', this)
// this.ctx.setFillStyle('#7f7f7f')
this.ctx.fillStyle = 'rgba(0, 0, 0, 0)';
this.ctx.fillRect(0, 0, cw, ch)
uni.getImageInfo({
src: _this.photoPath,
success(res) {
_this.drawBottomBg(res)
// _this.path2 = res.path
// _this.multiple = res.width / cw
// // 等比例计算出默认宽高
// _this.path2w = _this.multiple > 1 ? cw : res.width;
// _this.path2h = _this.multiple > 1 ? res.height / _this.multiple : res.height;
// _this.ctx.drawImage(_this.path2+'?t='+ new Date().getTime(), _this.positionx, _this.positiony, _this.path2w, _this.path2h)
// 绘制二维码
_this.loadingWxCode()
// 背景
_this.drawPosterBg();
//名称
let userInfo = uni.getStorageSync('userInfo');
if(!userInfo){
userInfo = _this.loginInfo;
}
if(userInfo.name){
_this.ctx.setFillStyle("#666666");
_this.ctx.setFontSize(14);
_this.ctx.setTextAlign("center");
_this.ctx.fillText(userInfo.name, _this.path2w/2, 85);
}
//标题内容
_this.ctx.setFillStyle("#333333");
_this.ctx.setFontSize(18);
_this.ctx.setTextAlign("left");
_this.ctx.fillText('我发现了一个很棒的宝贝!', 59, 115);
// 海报图片
if(!_this.goodsItemData.bannerList || !_this.goodsItemData.bannerList[0]){
_this.ctx.drawImage(_this.banner + '?t='+ new Date().getTime(), 19, 130, 288, 185)
}else{
let bannerData = _this.goodsItemData.bannerList[0].banner;
_this.ctx.drawImage(bannerData + '?t='+ new Date().getTime(), 19, 130, 288, 185)
}
// let bannerData = _this.goodsItemData.bannerList[0];
// //变量存在且子项不等于空字符串
// if(typeof bannerData !== 'undefined' && bannerData.banner.trim() !== ''){
// _this.ctx.drawImage(bannerData.banner+'?t='+ new Date().getTime(), 19, 130, 288, 185)
// }
// 详情
if(_this.goodsItemData.name){
_this.ctx.setFillStyle("#333333");
_this.ctx.font = "PingFang SC bold 16px Arial";
_this.ctx.setTextAlign("left");
_this.ctx.setTextBaseline("middle");
_this.drawText(_this.ctx, _this.goodsItemData.name, 40 / 2, 705 / 2, 380 / 2);
}
//绘制价格
_this.drawPriceGroup()
// 已出售文本
if(_this.goodsItemData.sellNumber){
_this.ctx.setFillStyle("#999999");
// _this.ctx.setFontSize(12);
_this.ctx.font = "PingFang SC 12px Arial";
_this.ctx.setTextAlign("left");
_this.ctx.fillText("已售"+_this.goodsItemData.sellNumber+'份', 45 / 2, 800 / 2);
}
// 绘制用户圆角头像
if(userInfo.avatar){
_this.drawUserHead(userInfo);
}
_this.ctx.draw(true);
},
fail(res){
console.log(res);
}
})
// setTimeout(()=> { this.onSaveImages()}, 1500)
} catch (error) {
console.error(error)
}
},
// 文本换行
drawText(ctx, t, x, y, w) {
let chr = t.split("");
let temp = "";
let row = [];
for (var a = 0; a < chr.length; a++) {
if (ctx.measureText(temp).width >= w) {
row.push(temp);
temp = "";
}
temp += chr[a];
}
row.push(temp);
for (var b = 0; b < row.length; b++) {
ctx.fillText(row[b], x, y + (b + 1) * 22);
}
},
drawBottomBg(res){
let cw = this.cw
let ch = this.ch
this.path2 = res.path
this.multiple = res.width / cw
// 等比例计算出默认宽高
this.path2w = this.multiple > 1 ? cw : res.width;
this.path2h = this.multiple > 1 ? (res.height / this.multiple)+20 : res.height;
// const radius = 5; // 圆角半径
const topLeftRadius = 15; // 左上角圆角半径
const topRightRadius = 15; // 右上角圆角半径
const bottomLeftRadius = 25; // 左下角圆角半径
const bottomRightRadius = 25; // 右下角圆角半径
this.ctx.beginPath();
this.ctx.moveTo(topLeftRadius, 0);
this.ctx.lineTo(this.path2w - topRightRadius, 0);
this.ctx.arcTo(this.path2w, 0, this.path2w, topRightRadius, topRightRadius);
this.ctx.lineTo(this.path2w, this.path2h - bottomRightRadius);
this.ctx.arcTo(this.path2w, this.path2h, this.path2w - bottomRightRadius, this.path2h, bottomRightRadius);
this.ctx.lineTo(bottomRightRadius, this.path2h);
this.ctx.arcTo(0, this.path2h, 0, this.path2h - bottomLeftRadius, bottomLeftRadius);
this.ctx.lineTo(0, topLeftRadius);
this.ctx.arcTo(0, 0, topLeftRadius, 0, topLeftRadius);
this.ctx.closePath(); // 关闭路径
// 将路径设为剪切区域
this.ctx.clip();
this.ctx.drawImage(this.path2+ '?t='+ new Date().getTime(), this.positionx, this.positiony, this.path2w, this.path2h)
},
drawPosterBg(){
let cw = this.cw
let ch = this.ch
const radius = 5; // 圆角半径
this.ctx.beginPath();
this.ctx.moveTo(radius, 0);
this.ctx.lineTo(cw - radius, 0);
this.ctx.arcTo(cw, 0, cw, radius, radius);
this.ctx.lineTo(cw, ch - radius);
this.ctx.arcTo(cw, ch, cw - radius, ch, radius);
this.ctx.lineTo(radius, ch);
this.ctx.arcTo(0, ch, 0, ch - radius, radius);
this.ctx.lineTo(0, radius);
this.ctx.arcTo(0, 0, radius, 0, radius);
this.ctx.closePath(); // 关闭路径
// 将路径设为剪切区域
this.ctx.clip();
// 在剪切区域内绘制图像
this.ctx.drawImage(this.path1+ '?t='+ new Date().getTime(),0, 0, cw, ch);
this.ctx.draw(true)
},
drawUserHead(userInfo){
//圆角头像
var avatar_width = 44; //绘制的头像宽度
var avatar_heigth = 44; //绘制的头像高度
var avatar_x = 146; //绘制的头像在画布上的位置
var avatar_y = 20; //绘制的头像在画布上的位置
this.ctx.save();
this.ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
this.ctx.arc(avatar_width / 2 + avatar_x, avatar_heigth / 2 + avatar_y, avatar_width / 2, 0, Math.PI * 2, false);
this.ctx.clip();//画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
this.ctx.drawImage(userInfo.avatar+'?t='+ new Date().getTime(), avatar_x, avatar_y, avatar_width, avatar_heigth); // 推进去图片,必须是https图片
this.ctx.restore(); //恢复之前保存的绘图上下文 恢复之前保存的绘图上下午即状态 还可以继续绘制
},
drawPriceGroup(){
let priceItem = '';
if(typeof(this.goodsItemData.pintuan_rule) == "undefined" ){
priceItem = this.goodsItemData.price;
}else{
priceItem = this.goodsItemData.pintuan_rule.price;
}
// console.log('item'+priceItem);
var priceInitX = 16;
var pricePosY = 325;
var priceWidth = 16 + 11 * (priceItem.length - 1);
var lineWidth= 11 + 8 * this.goodsItemData.price.length;
// 价格
this.ctx.setFillStyle("#FE654C");
this.ctx.setFontSize(22);
this.ctx.font = "PingFang SC bold 22px Arial";
this.ctx.setTextAlign("left");
this.ctx.setTextBaseline("middle");
const newText = `¥${priceItem}`
this.drawText(this.ctx, newText, priceInitX, pricePosY, priceWidth);
// console.log(newtextwidth)
// 老价格
if(this.goodsItemData.pintuan_rule.price){
var pricePosX = priceInitX + priceWidth + 30; //16+64
this.ctx.setFillStyle("#FE654C");
this.ctx.setFontSize(14);
this.ctx.setTextAlign("left");
this.ctx.setTextBaseline("middle");
this.drawText(this.ctx, `¥${this.goodsItemData.price}`, pricePosX, pricePosY, priceWidth);
// this.ctx.fillText(`¥${this.share_data.old_price}`, pricePosX, pricePosY);
this.ctx.setFillStyle("#FE654C");
// 横线绘制,11宽基础系数,8为每个字符宽度,
this.ctx.fillRect(pricePosX+3,pricePosY+20,lineWidth,1);
// var ptWidth = 16 + 11 * (this.share_data.old_price - 1);
// 拼团价-icon
this.ctx.drawImage(this.ptRect+'?t='+ new Date().getTime(), pricePosX + lineWidth+10, pricePosY+13, 96 / 2, 33 / 2)
}
},
// canvas图片保存到本地
onSaveImages() {
let me = this
uni.canvasToTempFilePath({
canvasId: 'ShareCanvas',
async success(res) {
console.log('success', res)
me.tempFilePath = res.tempFilePath
},
fail(e) {
console.log('fail', e)
}
})
},
},
}
好了 就这些 ,核心部分:
1.使用时先请求接口拿到接口链接,分别进行加载,下载图片到本地,
绘制-通过回调拉起窗口
uni.showLoading({ title: ‘加载中’, mask: true });
const img = new Image()
img.src = userInfo.avatar;
img.onload = async (res) => {
await this.inCanvas()
uni.hideLoading()
//拿不到canvas展示出来回调函数暂时这么写
this.posterState = true; }
2.直接用的图片,会跨域,这里要先进行下载,把图片下到本地:
uni.getImageInfo({ src: _this.photoPath, success(res)
{ //开始绘制 }, fail(res){ //绘制失败 }) }
3.绘制完成导出为指定大小图片,把this.tempFilePath插入标签的src=“”内
let me = this uni.canvasToTempFilePath({ canvasId: ‘shareCanvas’,
async success(res) { onsole.log(‘success’, res)
me.tempFilePath = res.tempFilePath },
fail(e) { console.log(‘fail’, e) } })
4. 与 有区别 ,image更预览纯看,img适合引用,在浏览器内长按会存在不同表现方式。