2023年6月4日

vue-canvas海报绘制与保存图片

作者 admin

前段时间做了一个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适合引用,在浏览器内长按会存在不同表现方式。