小程序海报制作

156 阅读6分钟

样式

	<view class="poster-share">
		<!-- 自定义导航栏 -->
		<!-- <ayi-topbar title="商品海报" background-color="#f5f5f5" color="#282828" :border="false"></ayi-topbar> -->

		<!-- 海报图片 -->
		<view class="picture">
			<canvas class="canvas" v-if="attr.isShow" canvas-id="cvs"></canvas>
			<image class="p-img" v-else :src="attr.tempImg" mode="aspectFill"></image>
		</view>

		<!-- 保存图片 -->
		<view class="keep">
			<view class="tip">保存至相册可分享到朋友圈</view>
			<view class="btn" @click="saveImg">保存图片</view>
		</view>
	</view>
</template>

<script lang="ts" setup>
import { reactive, nextTick, ref } from "vue";
import Canvas from "./lib/canvas";
// import ayiTopbar from "@/components/yll1024335892-ayi-topbar/index.vue"
import avatarPng from "@/static/yll1024335892-page-24/avatar.png";
import goodsPng from "@/static/yll1024335892-page-24/goods.png";
import codePng from "@/static/yll1024335892-page-24/code.png";
const attr = reactive({
	isShow: true,
	name: "无语",
	tempImg: "",
	cvs: null
});
const imageurl = ref("");
const saveImg = () => {
	uni.showToast({
		title: "请长按图片保存!",
		icon: "none",
		duration: 2000
	});
	uni.saveImageToPhotosAlbum({
		filePath: imageurl.value,
		success: () => {
			uni.showToast({
				title: "已保存到相册,快去访问吧!",
				icon: "none"
			});
		}
	});

	attr.cvs.createImage().then((res: any) => {
		console.log(res);

		uni.saveImageToPhotosAlbum({
			filePath: res,
			success: () => {
				uni.showToast({
					title: "已保存到相册,快去访问吧!",
					icon: "none"
				});
			}
		});
	});
};

const init = () => {
	attr.cvs = new Canvas("cvs");
	console.log(attr.cvs);
	return new Promise((_resolve, _reject) => {
		attr.cvs
			.div({
				x: 0,
				y: 0,
				radius: 8,
				backgroundColor: "#fff"
			})
			.image({
				x: 15,
				y: 18,
				height: 33,
				width: 33,
				mode: "aspectFill",
				url: avatarPng
			})
			.text({
				x: 54,
				y: 22.5,
				height: 24,
				text: "洛克" + attr.name,
				overflow: "ellipsis",
				color: "#FF674E"
			})
			.text({
				x: 140,
				y: 22.5,
				width: 192,
				height: 24,
				text: "1",
				color: "#282828"
			})
			.image({
				x: -8,
				y: -10,
				height: 520,
				width: 355,
				mode: "aspectFill",
				url: goodsPng
			})
			.text({
				x: 15,
				y: 40,
				width: 10,
				height: 24,
				overflow: "ellipsis",
				lineClamp: 2,
				fontSize: 18,
				lineHeight: 18,
				text: "免费赠送的小猫咪千万不要随意抛弃",
				color: "#fff"
			})
			.image({
				x: 232.5,
				y: 393,
				height: 69,
				width: 69,
				mode: "aspectFill",
				url: codePng
			})
			.text({
				x: 15,
				y: 467,
				width: 192,
				height: 22,
				fontSize: 22,
				text: "¥3.14",
				color: "#FF674E",
				textDecoration: "underline", // 下划线
				textStyle: "stroke" // 空心字体
			})
			.text({
				x: 204,
				y: 472,
				width: 126,
				height: 14,
				fontSize: 14,
				text: "长按识别二维码访问",
				color: "#B1B1B1"
			})
			.draw()
			.then(() => {
				nextTick(() => {
					attr.cvs.createImage().then((res: string) => {
						console.log(res);
						imageurl.value = res;
						uni.downloadFile({
							url: res,
							success: (data) => {
								if (data.statusCode === 200) {
									attr.tempImg = data.tempFilePath;
									attr.isShow = false;
								}
							}
						});
					});
				});
			});
	});
};
init();
</script>

<style lang="scss">
page {
	background: #f5f5f5;
}

.poster-share {
	.picture {
		padding: 30rpx;
		width: 690rpx;
		height: 1014rpx;
		box-sizing: border-box;
		margin: 0 auto;

		.canvas {
			width: 345px;
			height: 507px;
		}

		.p-img {
			width: 100%;
			height: 100%;
		}
	}

	.keep {
		margin-top: 50rpx;

		.tip {
			text-align: center;
			font-size: 28rpx;
			color: #b1b1b1;
		}

		.btn {
			margin: 0 auto;
			margin-top: 10rpx;
			width: 276rpx;
			height: 84rpx;
			line-height: 84rpx;
			font-size: 32rpx;
			text-align: center;
			color: #ffffff;
			background: #ff674e;
			border-radius: 42rpx;
		}
	}
}
</style>

ts部分

export function isString(value) {
	return typeof value === "string";
}
const isObject = (value) => {
	return value != null && (typeof value == "object" || typeof value == "function");
};
export function isEmpty(val: any) {
	if (typeof val == "boolean") {
		return false;
	}
	if (typeof val == "number") {
		return false;
	}
	if (val instanceof Array) {
		if (val.length == 0) return true;
	} else if (val instanceof Object) {
		if (JSON.stringify(val) === "{}") return true;
	} else {
		if (val == "null" || val == null || val == "undefined" || val == undefined || val == "") return true;
		if ((val + "").replace(/\s*/g, "") == "") return true;
		return false;
	}
	return false;
}
const getObjType = (obj) => {
	const toString = Object.prototype.toString;
	const map = {
		"[object Boolean]": "boolean",
		"[object Number]": "number",
		"[object String]": "string",
		"[object Function]": "function",
		"[object Array]": "array",
		"[object Date]": "date",
		"[object RegExp]": "regExp",
		"[object Undefined]": "undefined",
		"[object Null]": "null",
		"[object Object]": "object"
	};
	if (obj instanceof Object) {
		return "element";
	}
	return map[toString.call(obj)];
};
/**
 * @description 深度克隆
 * @param {object} obj 需要深度克隆的对象
 * @returns {*} 克隆后的对象或者原值(不是对象)
 */
export function cloneDeep(data) {
	const type = getObjType(data);
	let obj;
	if (type === "array") {
		obj = [];
	} else if (type === "object") {
		obj = {};
	} else {
		//不再具有下一层次
		return data;
	}
	if (type === "array") {
		for (let i = 0, len = data.length; i < len; i++) {
			obj.push(cloneDeep(data[i]));
		}
	} else if (type === "object") {
		for (let key in data) {
			obj[key] = cloneDeep(data[key]);
		}
	}
	return obj;
}
// 渲染参数
declare interface RenderOptions {
	x: number;
	y: number;
	height?: number;
	width?: number;
	[key: string]: any;
}

// 文本渲染参数
declare interface TextRenderOptions extends RenderOptions {
	text: string;
	color?: string;
	fontSize?: number;
	textAlign?: "left" | "right" | "center";
	overflow?: "ellipsis";
	lineClamp?: number;
	letterSpace?: number;
	lineHeight?: number;
}

// 图片渲染参数
declare interface ImageRenderOptions extends RenderOptions {
	mode: "aspectFill" | "aspectFit";
	url: string;
	radius?: number;
}

// 块渲染参数
declare interface DivRenderOptions extends RenderOptions {
	radius?: number;
	backgroundColor?: string;
	border?: {
		width: number;
		color: string;
	};
}

// 导出图片参数
declare interface CreateImageOptins {
	x?: number;
	y?: number;
	width?: number;
	height?: number;
	destWidth?: number;
	destHeight?: number;
	fileType?: "jpg" | "png";
	quality?: number;
}

class Canvas {
	ctx: any;
	canvasId: any;
	scope: any;
	renderQuene: any;
	imageQueue: any;

	constructor(canvasId: string) {
		// 绘图上下文
		this.ctx = null;

		// canvas id
		this.canvasId = canvasId;

		// 当前页面作用域
		const { proxy }: any = getCurrentInstance();
		this.scope = proxy;

		// 渲染队列
		this.renderQuene = [];

		// 图片队列
		this.imageQueue = [];

		// 创建画布
		this.create();
	}

	// 创建画布
	create() {
		this.ctx = uni.createCanvasContext(this.canvasId, this.scope);
		return this;
	}

	// 块
	div(options: DivRenderOptions) {
		let render = () => {
			this.divRender(options);
		};
		this.renderQuene.push(render);
		return this;
	}

	// 文本
	text(options: TextRenderOptions) {
		let render = () => {
			this.textRender(options);
		};
		this.renderQuene.push(render);
		return this;
	}

	// 图片
	image(options: ImageRenderOptions) {
		let render = () => {
			this.imageRender(options);
		};
		this.imageQueue.push(options);
		this.renderQuene.push(render);
		return this;
	}

	// 绘画
	draw(save = false) {
		return new Promise((resolve) => {
			let next = () => {
				this.render();
				this.ctx.draw(save, () => {
					resolve(true);
				});
			};

			if (!isEmpty(this.imageQueue)) {
				this.preLoadImage().then(next);
			} else {
				next();
			}
		});
	}

	// 生成图片
	createImage(options?: CreateImageOptins): Promise<string> {
		return new Promise((resolve, reject) => {
			let data = {
				canvasId: this.canvasId,
				...options,
				success: (res: any) => {
					// #ifdef MP-ALIPAY
					resolve(res.apFilePath);
					// #endif

					// #ifndef MP-ALIPAY
					resolve(res.tempFilePath);
					// #endif
				},
				fail: reject
			};

			// #ifdef MP-ALIPAY
			this.ctx.toTempFilePath(data);
			// #endif

			// #ifndef MP-ALIPAY
			uni.canvasToTempFilePath(data, this.scope);
			// #endif
		});
	}

	// 保存图片
	saveImage(options?: CreateImageOptins) {
		uni.showLoading({
			title: "图片下载中..."
		});
		this.createImage(options).then((path: any) => {
			return new Promise((resolve) => {
				uni.hideLoading();
				uni.saveImageToPhotosAlbum({
					filePath: path,
					success: () => {
						uni.showToast({
							title: "保存图片成功"
						});
						resolve(path);
					},
					fail: (err) => {
						// #ifdef MP-ALIPAY
						uni.showToast({
							title: "保存图片成功"
						});
						// #endif

						// #ifndef MP-ALIPAY
						uni.showToast({
							title: "保存图片失败",
							icon: "none"
						});
						// #endif
					}
				});
			});
		});
	}

	// 预览图片
	previewImage(options?: CreateImageOptins) {
		this.createImage(options).then((url: string | any) => {
			uni.previewImage({
				urls: [url]
			});
		});
	}

	// 下载图片
	downLoadImage(item: any) {
		return new Promise((resolve, reject) => {
			if (!item.url) {
				return reject("url 不能为空");
			}

			// 处理base64
			// #ifdef MP
			if (item.url.indexOf("data:image") >= 0) {
				let extName = item.url.match(/data\:\S+\/(\S+);/);
				if (extName) {
					extName = extName[1];
				}
				const fs = uni.getFileSystemManager();
				const fileName = Date.now() + "." + extName;
				// @ts-ignore
				const filePath = wx.env.USER_DATA_PATH + "/" + fileName;

				return fs.writeFile({
					filePath,
					data: item.url.replace(/^data:\S+\/\S+;base64,/, ""),
					encoding: "base64",
					success: () => {
						item.url = filePath;
						resolve(filePath);
					}
				});
			}
			// #endif

			// 是否网络图片
			const isHttp = item.url.includes("http");

			uni.getImageInfo({
				src: item.url,
				success: (result) => {
					item.sheight = result.height;
					item.swidth = result.width;

					if (isHttp) {
						item.url = result.path;
					}

					resolve(item.url);
				},
				fail: (err) => {
					console.log(err, item.url);
					reject(err);
				}
			});

			return 1;
		});
	}

	// 预加载图片
	async preLoadImage() {
		await Promise.all(this.imageQueue.map(this.downLoadImage));
	}

	// 设置背景颜色
	setBackground(options: any) {
		if (!options) return null;

		let backgroundColor;

		if (!isString(options)) {
			backgroundColor = options;
		}

		if (isString(options.backgroundColor)) {
			backgroundColor = options.backgroundColor;
		}

		if (isObject(options.backgroundColor)) {
			let { startX, startY, endX, endY, gradient } = options.backgroundColor;
			const rgb = this.ctx.createLinearGradient(startX, startY, endX, endY);
			for (let i = 0, l = gradient.length; i < l; i++) {
				rgb.addColorStop(gradient[i].step, gradient[i].color);
			}
			backgroundColor = rgb;
		}

		this.ctx.setFillStyle(backgroundColor);

		return this;
	}

	// 设置边框
	setBorder(options: any) {
		if (!options.border) return this;

		let { x, y, width: w, height: h, border, radius: r } = options;

		if (border.width) {
			this.ctx.setLineWidth(border.width);
		}

		if (border.color) {
			this.ctx.setStrokeStyle(border.color);
		}

		// 偏移距离
		let p = border.width / 2;

		// 是否有圆角
		if (r) {
			this.drawRadiusRoute(x - p, y - p, w + 2 * p, h + 2 * p, r + p);
			this.ctx.stroke();
		} else {
			this.ctx.strokeRect(x - p, y - p, w + 2 * p, h + 2 * p);
		}

		return this;
	}

	// 设置缩放,旋转
	setTransform(options: any) {
		if (options.scale) {
		}
		if (options.rotate) {
		}
	}

	// 带有圆角的路径绘制
	drawRadiusRoute(x: number, y: number, w: number, h: number, r: number) {
		this.ctx.beginPath();
		this.ctx.moveTo(x + r, y, y);
		this.ctx.lineTo(x + w - r, y);
		this.ctx.arc(x + w - r, y + r, r, 1.5 * Math.PI, 0);
		this.ctx.lineTo(x + w, y + h - r);
		this.ctx.arc(x + w - r, y + h - r, r, 0, 0.5 * Math.PI);
		this.ctx.lineTo(x + r, y + h);
		this.ctx.arc(x + r, y + h - r, r, 0.5 * Math.PI, Math.PI);
		this.ctx.lineTo(x, y + r);
		this.ctx.arc(x + r, y + r, r, Math.PI, 1.5 * Math.PI);
		this.ctx.closePath();
	}

	// 裁剪图片
	cropImage(mode: "aspectFill" | "aspectFit", width: number, height: number, sWidth: number, sHeight: number, x: number, y: number) {
		let cx, cy, cw, ch, sx, sy, sw, sh;
		switch (mode) {
			case "aspectFill":
				if (width <= height) {
					let p = width / sWidth;
					cw = width;
					ch = sHeight * p;
					cx = 0;
					cy = (height - ch) / 2;
				} else {
					let p = height / sHeight;
					cw = sWidth * p;
					ch = height;
					cx = (width - cw) / 2;
					cy = 0;
				}
				break;
			case "aspectFit":
				if (width <= height) {
					let p = height / sHeight;
					sw = width / p;
					sh = sHeight;
					sx = x + (sWidth - sw) / 2;
					sy = y;
				} else {
					let p = width / sWidth;
					sw = sWidth;
					sh = height / p;
					sx = x;
					sy = y + (sHeight - sh) / 2;
				}
				break;
		}
		return { cx, cy, cw, ch, sx, sy, sw, sh };
	}

	// 获取文本内容
	getTextRows({ text, fontSize = 14, width = 100, lineClamp = 1, overflow, letterSpace = 0 }: any) {
		let arr: any[] = [[]];
		let a = 0;

		for (let i = 0; i < text.length; i++) {
			let b = this.getFontPx(text[i], { fontSize, letterSpace });

			if (a + b > width) {
				a = b;
				arr.push(text[i]);
			} else {
				// 最后一行且设置超出省略号
				if (overflow == "ellipsis" && arr.length == lineClamp && a + 3 * this.getFontPx(".", { fontSize, letterSpace }) > width - 5) {
					arr[arr.length - 1] += "...";
					break;
				} else {
					a += b;
					arr[arr.length - 1] += text[i];
				}
			}
		}

		return arr;
	}

	// 获取单个字体像素大小
	getFontPx(text: string, { fontSize = 14, letterSpace }: any) {
		if (!text) {
			return fontSize / 2 + fontSize / 14 + letterSpace;
		}

		let ch = text.charCodeAt(0);

		if ((ch >= 0x0001 && ch <= 0x007e) || (0xff60 <= ch && ch <= 0xff9f)) {
			return fontSize / 2 + fontSize / 14 + letterSpace;
		} else {
			return fontSize + letterSpace;
		}
	}

	// 渲染块
	divRender(options: DivRenderOptions) {
		this.ctx.save();
		this.setBackground(options);
		this.setBorder(options);
		this.setTransform(options);

		// 区分是否有圆角采用不同模式渲染
		if (options.radius) {
			let { x, y } = options;
			let w = options.width || 0;
			let h = options.height || 0;
			let r = options.radius || 0;
			// 画路径
			this.drawRadiusRoute(x, y, w, h, r);
			// 填充
			this.ctx.fill();
		} else {
			this.ctx.fillRect(options.x, options.y, options.width, options.height);
		}
		this.ctx.restore();
	}

	// 渲染文本
	textRender(options: TextRenderOptions) {
		let { fontSize = 14, textAlign, width, color = "#000000", x, y, letterSpace, lineHeight = 14 } = options || {};

		this.ctx.save();

		// 设置字体大小
		this.ctx.setFontSize(fontSize);

		// 设置字体颜色
		this.ctx.setFillStyle(color);

		// 获取文本内容
		let rows = this.getTextRows(options);

		// 获取文本行高
		let lh = lineHeight - fontSize;

		// 左偏移
		let offsetLeft = 0;

		// 字体对齐
		if (textAlign && width) {
			this.ctx.textAlign = textAlign;

			switch (textAlign) {
				case "left":
					break;
				case "center":
					offsetLeft = width / 2;
					break;
				case "right":
					offsetLeft = width;
					break;
			}
		}

		// 逐行写入
		for (let i = 0; i < rows.length; i++) {
			let d = offsetLeft;

			if (letterSpace) {
				for (let j = 0; j < rows[i].length; j++) {
					// 写入文字
					this.ctx.fillText(rows[i][j], x + d, (i + 1) * fontSize + y + lh * i);

					// 设置偏移
					d += this.getFontPx(rows[i][j], options);
				}
			} else {
				// 写入文字
				this.ctx.fillText(rows[i], x + offsetLeft, (i + 1) * fontSize + y + lh * i);
			}
		}

		this.ctx.restore();
	}

	// 渲染图片
	imageRender(options: ImageRenderOptions) {
		this.ctx.save();

		if (options.radius) {
			// 画路径
			this.drawRadiusRoute(options.x, options.y, options.width || options.swidth, options.height || options.sHeight, options.radius);
			// 填充
			this.ctx.fill();
			// 裁剪
			this.ctx.clip();
		}
		let temp = cloneDeep(this.imageQueue[0]);

		if (options.mode) {
			let { cx, cy, cw, ch, sx, sy, sw, sh } = this.cropImage(options.mode, temp.swidth, temp.sheight, temp.width, temp.height, temp.x, temp.y);
			switch (options.mode) {
				case "aspectFit":
					this.ctx.drawImage(temp.url, sx, sy, sw, sh);
					break;
				case "aspectFill":
					this.ctx.drawImage(temp.url, cx, cy, cw, ch, temp.x, temp.y, temp.width, temp.height);
					break;
			}
		} else {
			this.ctx.drawImage(temp.url, temp.x, temp.y, temp.width || temp.swidth, temp.height || temp.sheight);
		}
		this.imageQueue.shift();
		this.ctx.restore();
	}

	// 渲染全部
	render() {
		this.renderQuene.forEach((ele: any) => {
			ele();
		});
	}
}

export default Canvas;