canvas 海报

85 阅读1分钟

1、页面代码

<template>
	<view class="demo">
		<canvas :style="{ width: canvasW + 'px', height: canvasH + 'px' }" canvas-id="myCanvas" id="myCanvas"></canvas>
	</view>
</template>

<script>
	import {
		drawTextVertical
	} from './canvas';
	export default {
		components: {},
		data() {
			return {
				canvasW: 0, // 画布宽
				canvasH: 0, // 画布高
				SystemInfo: {}, // 设备信息
				goodsImg: {}, // 主图
				ratio: 0, // 比率
			}
		},
		async onLoad() {
			// 获取设备信息,主要获取宽度,赋值给canvasW 也就是宽度:100%
			this.SystemInfo = await this.getSystemInfo();
			// 获取主图,APP端会返回图片的本地路径(H5端只能返回原路径)
			this.goodsImg = await this.getImageInfo(
				'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fd1.lanrentuku.com%2Fupload%2Fimg%2Fpy5s1c%2F202011022342%2F5fa028e595edf.jpg&refer=http%3A%2F%2Fd1.lanrentuku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1672207134&t=97d23e38c9db282abb73632ad9953367'
			);

			console.log('--goodsImg--', this.goodsImg)

			// this.canvasW = this.SystemInfo.windowWidth/ this.SystemInfo.pixelRatio; // 画布宽度
			// this.canvasH = (this.goodsImg.height + 50)/this.SystemInfo.pixelRatio; // 画布高度 = 主图高度 + 文字图片的间距(大概50)

			this.canvasW = this.SystemInfo.windowWidth; // 画布宽度
			this.canvasH = this.goodsImg.height; // 画布高度

			if (this.goodsImg.errMsg == 'getImageInfo:ok' && this.SystemInfo
				.errMsg == 'getSystemInfo:ok') {
				console.log('ok')
				uni.showToast({
					icon: 'loading',
					mask: true,
					duration: 1000,
					title: '海报绘制中',
				});

				setTimeout(() => {
					let ctx = uni.createCanvasContext('myCanvas', this);

					// 填充背景色,白色
					ctx.setFillStyle('red'); // 默认白色
					// ctx.fillRect(0, 0, this.canvasW, this.canvasH) // fillRect(x,y,宽度,高度)

					// 绘制主图
					ctx.drawImage(this.goodsImg.path, 0, 0, this.canvasW, this
						.canvasW) // drawImage(图片路径,x,y,绘制图像的宽度,绘制图像的高度)

					/**
					 *   感谢:李文龙大夫
					 *   妙手回春,华佗再世
					 *   龙吴路赠送
					 */

					ctx.font = `normal 400 ${(18 * this.ratio).toFixed(0)}px my-font3`;
					// ctx.setFontSize(16) // 字号
					ctx.setFillStyle('#ecdc40') // 颜色
					// ctx.fillText('¥' + this.price, 10, this.canvasW + 75); // (文字,x,y)  

					drawTextVertical(ctx, '感谢', (this.canvasW - this.getDistance(30)), this.getDistance(15),
						22 * this.ratio)
					ctx.fillText(':', (this.canvasW - this.getDistance(30)), this.getDistance(26));
					drawTextVertical(ctx, '李文龙大夫', (this.canvasW - this.getDistance(30)), this.getDistance(33),
						22)

					ctx.setFontSize(32 * this.ratio) // 字号
					drawTextVertical(ctx, '华佗再世', (this.canvasW - this.getDistance(40)), this.getDistance(28),
						45)
					drawTextVertical(ctx, '妙手回春', (this.canvasW - this.getDistance(55)), this.getDistance(28),
						45)

					ctx.font = `normal 400 ${(16 * this.ratio).toFixed(0)}px my-font1`;
					// ctx.setFontSize(16)
					ctx.setFillStyle('#ecdc40')

					drawTextVertical(ctx, '龙吴路赠送', this.getDistance(35), this.getDistance(56), 13, 'bottom')

					let dateText = this.converToDate('2022-11-28')
					// drawTextVertical(ctx, dateText, this.getDistance(29), this.getDistance(42), 13)
					drawTextVertical(ctx, dateText, this.getDistance(29), this.getDistance(78), 13, 'bottom')



					ctx.draw(true, (ret) => { // draw方法 把以上内容画到 canvas 中。
						// console.log(ret)
						uni.showToast({
							icon: 'success',
							mask: true,
							title: '绘制完成',
						});

						const that = this;

						uni.canvasToTempFilePath({ // 保存canvas为图片
							x: 0,
							y: 0,
							canvasId: 'myCanvas',
							fileType: 'png',
							quality: 1, //图片质量
							success(res) {
								// console.log(res.tempFilePath, 'canvas生成图片地址11')
							},
							complete(res) {
								// console.log(res.tempFilePath, 'canvas生成图片地址11')

								// that.downloadImg(res.tempFilePath)

								// 预览
								// uni.previewImage({
								// 	current: 0, //预览图片的下标
								// 	urls: [res
								// 		.tempFilePath] //预览图片的地址,必须要数组形式,如果不是数组形式就转换成数组形式就可以
								// })

								// 在H5平台下,tempFilePath 为 base64, // 图片提示跨域 H5保存base64失败,APP端正常输出临时路径
								// console.log(res)
							},
						})
					});
				}, 500)
			} else {
				console.log('err')
			}
		},
		methods: {
			// 获取图片信息
			getImageInfo(image) {
				return new Promise((req, rej) => {
					uni.getImageInfo({
						src: image,
						success: function(res) {
							// console.log('获取图片信息',res)
							req(res)
						},
					});
				})
			},

			getDistance(num, minNum = 0) {
				// num = num / 100;
				num = num / 100;
				let windowWidth = this.SystemInfo.windowWidth // 可使用窗口宽度
				let data = windowWidth * num;
				if (windowWidth > data > minNum) {
					return data
				} else {
					return minNum
				}
			},

			// 获取设备信息
			getSystemInfo() {
				return new Promise((req, rej) => {
					uni.getSystemInfo({
						success: (res) => {
							// console.log('获取设备信息',res)
							// console.log('获取设备信息',res.windowWidth,res.windowHeight)
							this.ratio = res.windowWidth / 375;

							console.log('--this.ratio--', this.ratio)
							req(res)
						}
					});
				})
			},

			converToDate(date) {
				let today = new Date();
				let chinese = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
				let y = today.getFullYear().toString();
				let m = (today.getMonth() + 1).toString();
				let d = today.getDate().toString();
				let result = "";
				for (var i = 0; i < y.length; i++) {
					result += chinese[y.charAt(i)];
				}
				result += "年";
				if (m.length == 2) {
					if (m.charAt(0) == "1") {
						result += ("十" + chinese[m.charAt(1)] + "月");
					}
				} else {
					result += (chinese[m.charAt(0)] + "月");
				}
				if (d.length == 2) {
					result += (chinese[d.charAt(0)] + "十" + chinese[d.charAt(1)] + "日");
				} else {
					result += (chinese[d.charAt(0)] + "日");
				}
				return result;
			},
			downloadImg(url) {
				// const that = this;
				// // #ifdef H5
				// var $code_img = document.getElementById('code_img');
				// console.log($code_img.src)
				// // #endif
				uni.downloadFile({
					url: url,
					success: (res) => {
						if (res.statusCode === 200) {
							console.log(res.tempFilePath);
							this.saveImg(res.tempFilePath)
						}
					}
				});
			},
			saveImg(url) {
				var oA = document.createElement("a");
				oA.download = ''; // 设置下载的文件名,默认是'下载'
				oA.href = url;
				document.body.appendChild(oA);
				oA.click();
				oA.remove(); // 下载之后把创建的元素删除
			},
		},
	}
</script>

<style>
	.my-font1 {
		font-family: my-font1
	}

	.my-font2 {
		font-family: my-font2
	}

	.my-font3 {
		font-family: my-font3
	}
</style>

2、垂直绘制文本 (drawTextVertical)方法

function drawTextVertical(context, text, x, y, interval, direction) {
	direction = direction || 'top'
	let arrText = text.split('');
	let arrWidth = arrText.map(function(letter) {
		return interval || 20;
		// 这里为了找到那个空格的 bug 做了许多努力,不过似乎是白费力了
		// const metrics = context.measureText(letter);
		// console.log(metrics);
		// const width = metrics.width;
		// return width;
	});
	let align = context.textAlign;
	let baseline = context.textBaseline;
	if (align == 'left') {
		x = x + Math.max.apply(null, arrWidth) / 2;
	} else if (align == 'right') {
		x = x - Math.max.apply(null, arrWidth) / 2;
	}
	if (baseline == 'bottom' || baseline == 'alphabetic' || baseline == 'ideographic') {
		y = y - arrWidth[0] / 2;
	} else if (baseline == 'top' || baseline == 'hanging') {
		y = y + arrWidth[0] / 2;
	}
	context.textAlign = 'center';
	context.textBaseline = 'middle';
	// 开始逐字绘制
	arrText.forEach(function(letter, index) {
		// // 确定下一个字符的纵坐标位置
		// var letterWidth = arrWidth[index];
		// 是否需要旋转判断
		let code = letter.charCodeAt(0);
		if (code <= 256) {
			context.translate(x, y);
			// 英文字符,旋转90°
			context.rotate(90 * Math.PI / 180);
			context.translate(-x, -y);
		} else if (index > 0 && text.charCodeAt(index - 1) < 256) {
			// y修正
			y = y + arrWidth[index - 1] / 2;
		}
		context.fillText(letter, x, y);
		// 旋转坐标系还原成初始态
		context.setTransform(1, 0, 0, 1, 0, 0);
		// 确定下一个字符的纵坐标位置
		let letterWidth = arrWidth[index];
		console.log('====', direction)
		if (direction == 'top') {
			y = y + letterWidth;
		} else {
			y = y - letterWidth;
		}
	});
	// 水平垂直对齐方式还原
	context.textAlign = align;
	context.textBaseline = baseline;
}
module.exports = {
	drawTextVertical: drawTextVertical
}

3、效果图

image.png

4、其他npm包

vue-canvas-poster-fixleft

Vue Canvas Poster 一个通过 css 属性画 canvas 图片的轻量级的 vue 组件。

@ubbcou/vue-canvas-poster

vue-canvas-poster 的 vue3版本。

vue-canvas-poster-yufan

一个基于vue+canvas通过类CSS数据生成canvas图片的组件。