2023 uniapp( vue3 + TS )使用canvas生成海报并保存,taro/微信小程序也适用

news2025/1/13 13:49:37

有段时间没写vue了,有点生疏了......

1、代码有注释,完整代码如下

<template>
	<view class="page">
		<canvas class="canvas" v-if="isShow" :style="{width:`${canvasWidth}px`,height:`${canvasHeight}px`}"
			:canvas-id="canvasId"></canvas>
		<view class="tool">
			<u-button @click="nextImg" :custom-style="buttonCustomStyle" text="下一张"
				color="linear-gradient(to right, #232526, #414345)"></u-button>
			<u-button @click="save" :custom-style="buttonCustomStyle" text="保存图片"
				color="linear-gradient(to right, rgb(66, 83, 216), rgb(213, 51, 186))"></u-button>

		</view>
	</view>
</template>

<script setup lang="ts">
	import { onMounted, reactive, ref, watch } from "vue";
	import { getImgBase64, drawRoundedRect, drawText, getSystemInfo } from "@/utils/canvas";
	import { imageList } from './utils'
	import type { ImageListType } from './utils'
	import { getAuthorize } from "@/utils";
	// 按钮样式
	const buttonCustomStyle = reactive({
		width: '49%',
	});
	// const color = '#f7f6ff'
	const offsetNumber : number = 20;
	const listHeight : number = 66;
	const context = ref(null)
	const pageColor = ref(imageList[0].pageColor)
	const canvasId = ref<string>(String(`canvasId${new Date().getTime()}`))
	const isShow = ref<boolean>(false)
	const imageObj = ref<ImageListType>(imageList[0])
	const imgTemp = ref<string>(wx.env.USER_DATA_PATH + `/${new Date().getTime()}-temp.png`) //图片存储的临时路径
	const canvasWidth = ref<number>(0)// canvas宽度
	const canvasHeight = ref<number>(0)// canvas宽度
	const canvasWidthRate = ref<number>(0.8)//canvas基于屏幕宽度占比,随便设置,我这里设置占屏幕宽度的百分之八十
	// 绘制图片,并转成base64
	const drawBgImg = async () => {
		// 图片临时地址重新赋值,避免图片加载路径都是一样的
		imgTemp.value = wx.env.USER_DATA_PATH + `/${new Date().getTime()}-temp.png`
		const ctx = context.value
		const { imgUrl, imageWidth, imageHeight } = await getImgBase64({
			src: imageObj.value.imageSrc,
			canvasWidth: canvasWidth.value,
			filePath: imgTemp.value
		})
		canvasHeight.value = canvasHeight.value + imageHeight //重新计算画布总高度
		// 填充背景矩阵
		drawRoundedRect(ctx, 0, 0, canvasWidth.value, canvasHeight.value, {
			fillColor: imageObj.value.bgColor,
			leftBottom: true,
			leftTop: true,
			rightBottom: true,
			rightTop: true,
			r: 5
		})
		// 图片如何居中呢?
		// 当前画布宽度 - 图片宽度 = 空出来的间隙
		// 图片居中 = 空出来的间隙 / 2 = x 轴起点 x = canvasWidth.value - imageWidth) / 2
		ctx.drawImage(imgUrl, (canvasWidth.value - imageWidth) / 2, offsetNumber, imageWidth, imageHeight);
	}
	// 绘制文字
	const drawTxt = async () => {
		const ctx = context.value
		const offsetY = canvasHeight.value
		// 填充矩形背景
		drawRoundedRect(ctx, 0, canvasHeight.value, canvasWidth.value, listHeight, {
			fillColor: imageObj.value.bgColor,
		});
		// 绘制文字
		drawText({
			ctx,
			text: imageObj.value.name,
			fillStyle: '#000000',
			fontSize: 22,
			x: (canvasWidth.value / 2),
			y: offsetY + 25,
			center: true
		})
		// 绘制文字
		drawText({
			ctx,
			text: imageObj.value.desc,
			fillStyle: '#000000',
			fontSize: 16,
			x: (canvasWidth.value / 2),
			y: offsetY + 25 + 25,
			center: true
		})
		//画布高度重新计算
		canvasHeight.value = canvasHeight.value + listHeight
	}
	// 初始化canvas
	const initCanvas = async () => {
		try {
			uni.showLoading()
			const { windowWidth } : any = await getSystemInfo()
			const ctx = context.value
			canvasHeight.value = offsetNumber;//初始化高度
			canvasWidth.value = windowWidth * canvasWidthRate.value//初始化宽度
			await drawBgImg()
			await drawTxt()
			ctx.draw()
		} finally {
			uni.hideLoading()
		}

	}
	onMounted(() => {
		isShow.value = true
	})
	watch(isShow, () => {
		if (isShow.value) {
			canvasId.value = String(`canvasId${new Date().getTime()}`)//由于存在多次绘制,避免ID重复,需要重新赋值
			context.value = uni.createCanvasContext(canvasId.value)//由于存在多次绘制,所以每次都重新创建新的画布实例
			initCanvas()

		}

	})
	// 下一张图
	const nextImg = () => {
		isShow.value = false
		imageObj.value = imageList[imageObj.value.id + 1] ?? imageList[0]
		pageColor.value = imageObj.value.pageColor
		setTimeout(() => {
			isShow.value = true
		}, 100)
	}
	// 图片添加到相册
	const addIamgeToAlbum = async () => {
		try {
			const { tempFilePath } = await uni.canvasToTempFilePath({
				canvasId: canvasId.value,
			});
			console.log('tempFilePath', tempFilePath);
			await uni.saveImageToPhotosAlbum({
				filePath: tempFilePath
			})
			uni.showToast({
				title: '保存成功!'
			})
		} catch {

		}

	}
	// 保存
	const save = async () => {
		try {
			uni.showLoading()
			// 获取用户设置
			const { authSetting } = await uni.getSetting()
			// 没有权限的时候
			if (!authSetting['scope.writePhotosAlbum']) {
				await getAuthorize('scope.writePhotosAlbum', {
					title: '请授权保存到相册',
					callback: addIamgeToAlbum
				})
				return
			}
			addIamgeToAlbum()
		} catch (err) {
		} finally {
			uni.hideLoading()
		}

	}
</script>

<style lang="scss" scoped>
	.page {
		background-color: v-bind(pageColor);
		height: 100vh;
		overflow: hidden;

		.canvas {
			margin: 20px auto 0;
		}

		.tool {
			width: 100%;
			display: flex;
			align-items: center;
			justify-content: center;
			position: fixed;
			bottom: 0;
			left: 0;
			right: 0;
		}
	}
</style>

1.1、工具函数 - canvas.ts

//utils/canvas.ts
const fs = uni.getFileSystemManager()

// 将Base64写入本地文件
const base64WriteFile = (filePath : string, data : string) => {
	return new Promise((resolve, reject) => {
		fs.writeFile({
			filePath,
			data,
			encoding: 'base64',
			success: (res) => {
				resolve(res);
			},
			fail: (err) => {
				reject(err);
			},
		});
	});
};
// 参数的类型校验
type GetImgBase64Type = {
	src : string;//图片地址(本地/在线地址)
	canvasWidth : number;//画布宽度
	filePath : string//临时路径
}
// 加载图片地址,生成base64并写入临时路径中
export const getImgBase64 = async (params : GetImgBase64Type) => {
	const { src, canvasWidth, filePath } = params
	try {
		// 获取图片信息:地址、宽高
		const imgInfo = await uni.getImageInfo({
			src,
		});
		// 计算图片在画布中的宽度
		const imageWidth = canvasWidth * 0.8;//随便定的,多少px都行
		// // 根据比例计算图片在画布中的高度
		const scaleFactor = Number((imageWidth / imgInfo.width).toFixed(2));
		// 根据比例计算图片高度
		const imageHeight = imgInfo.height * scaleFactor;
		// 生成base64
		const base64 : any = fs.readFileSync(imgInfo.path, 'base64')
		// 写入本地
		await base64WriteFile(filePath, base64)
		const currentImgInfo = await uni.getImageInfo({
			src: filePath,
		});
		return {
			imageWidth,
			imageHeight,
			imgUrl: currentImgInfo.path
		}
	} catch (err) {
		console.log('err', err);
	}

};

type DrawRoundedRectParamsType = {
	leftTop ?: boolean;
	leftBottom ?: boolean;
	rightTop ?: boolean;
	rightBottom ?: boolean;
	fillColor ?: string;
	r ?: number;
};
// canvas 绘制自定义圆角矩形
export const drawRoundedRect = (
	ctx : any,
	x : number,
	y : number,
	w : number,
	h : number,
	params ?: DrawRoundedRectParamsType,
) => {
	const {
		leftTop = false,
		leftBottom = false,
		rightTop = false,
		rightBottom = false,
		fillColor = 'transparent',
		r = 0,
	} = params || {};
	ctx.save(); // 保存当前绘图状态 防止虚线影响其他图形
	ctx.beginPath();
	ctx.setFillStyle(fillColor);
	ctx.setStrokeStyle('transparent');
	ctx.moveTo(x + r, y);
	// 绘制上边线和左上角圆弧
	if (leftTop) {
		ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5);
		ctx.lineTo(x, y);
	} else {
		ctx.moveTo(x, y + r);
		ctx.lineTo(x, y);
		ctx.lineTo(x + r, y);
	}
	ctx.lineTo(x + w - r, y);
	// 绘制上边线和右上角圆弧
	if (rightTop) {
		ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2);
	} else {
		ctx.lineTo(x + w - r, y);
		ctx.lineTo(x + w, y);
		ctx.lineTo(x + w, y + r);
	}

	ctx.lineTo(x + w, y + h - r);

	// 绘制下边线和右下角圆弧
	if (rightBottom) {
		ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5);
	} else {
		ctx.lineTo(x + w, y + h - r);
		ctx.lineTo(x + w, y + h);
		ctx.lineTo(x + w - r, y + h);
	}

	ctx.lineTo(x + r, y + h);

	// 绘制下边线和左下角圆弧
	if (leftBottom) {
		ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI);
	} else {
		ctx.lineTo(x + r, y + h);
		ctx.lineTo(x, y + h);
		ctx.lineTo(x, y + h - r);
	}

	ctx.lineTo(x, y + r);

	// 绘制左边线和左上角圆弧
	if (leftTop) {
		ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5);
		ctx.moveTo(x + r, y);
	} else {
		ctx.moveTo(x, y + r);
		ctx.lineTo(x, y);
		ctx.lineTo(x + r, y);
	}
	ctx.fill();
	ctx.closePath();
	ctx.stroke();
	ctx.restore(); // 恢复之前的绘图状态
};
type DrawTextConfigType = {
	ctx : any;
	fillStyle : string;//填充颜色
	fontSize : number//文字大小
	text : string;//在画布上输出的文本
	x : number;//绘制文本的左上角x坐标位置
	y : number//绘制文本的左上角y坐标位置
	center ?: boolean
}
// 绘制文本
export const drawText = (config : DrawTextConfigType) => {
	const { fillStyle, fontSize, x, y, text, ctx, center = false } = config
	ctx.setFillStyle(fillStyle);
	ctx.setFontSize(fontSize);
	if (center) {
		ctx.textAlign = 'center';//文字水平居中
	}
	ctx.fillText(text, x, y);
}
// 获取当前设备信息
export const getSystemInfo = () => {
	return new Promise((resolve) => {
		uni.getSystemInfo({
			success(res) {
				resolve(res)
			},
		})
	})
}

1.2、工具函数 - index.ts

//utils/index.ts
// 获取用户授权
type GetAuthorizeType = {
	title ?: string;//授权弹框描述
	callback ?: () => void//成功的回调
}
export const getAuthorize = (scope : string, params : GetAuthorizeType) => {
	const { title = '请开启授权', callback } = params
	return new Promise(() => {
		uni.authorize({
			scope,
			success: () => {
				callback?.()
			},
			fail: () => {
				// 如果用户点了拒绝,需要弹框提示再次授权
				uni.showModal({
					title,
					success() {
						uni.openSetting();
					},
				});
			}
		})
	})
}

1.3、图片列表函数

// ./utils/index.ts
export type ImageListType = {
	id : number;
	name : string
	desc : string
	imageSrc : string
	bgColor : string
	pageColor : string
}
export const imageList : ImageListType[] = [
	{
		id: 0,
		name: '那维莱特',
		desc: '潮水啊,我已归来!',
		imageSrc: '../../static/那维莱特.jpg',
		bgColor: '#b2d4ff',
		pageColor: '#d9e9ff',
	},
	{
		id: 1,
		name: '东方镜',
		desc: '太阳之下,诸世皆影!',
		imageSrc: '../../static/镜.jpg',
		bgColor: '#ffdecd',
		pageColor: '#fff3ed',
	},
	{
		id: 2,
		name: '魈',
		desc: '你去吧,我会在这里等你。',
		imageSrc: '../../static/魈.png',
		bgColor: '#f1ddff',
		pageColor: '#fbf4ff',
	},
	{
		id: 3,
		name: '琴团长',
		desc: '我以此剑起誓,必将胜利献给你!',
		imageSrc: '../../static/琴.jpg',
		bgColor: '#e6e4ff',
		pageColor: '#f7f6ff',
	},
]

2、效果如下

                                

                

3、添加相册授权

根据各自框架添加授权即可,比如uniapp在manifest.json下

    "mp-weixin" : {
        "appid" : "你的微信appid",
        "setting" : {
            "urlCheck" : false
        },
        "usingComponents" : true,
         /* 授权 */
		"permission": {
			"scope.writePhotosAlbum": {
				"desc": "请授权保存到相册"
			}
		}
    },

4、项目地址

我的项目地址,点击跳转

5、问题总汇

5.1、为什么本地图片/在线图片真机不显示等?

将所有用到的图片转 base64 展示,参考上面工具函数中的 getImgBase64()

5.2、多文本如何换行?

参考下面地址  使用canvas画布时多行文本应该怎么换行? | 微信开放社区  

5.3、多次绘制出现白屏等?

比如以弹框的形式多次点击生成等情况,首先要确保每个canvas-idID的实例不能重复。可以参考我上面标题1中的代码。

5.4、当ctx.draw()后需要立马回去临时路径做 image预览时,画布生成的内容不全?

5.4.1、前提情景

由于 canvas 的层级比较高,做预览的时候会遮住其他的view等标签。而且样式或拖拽等也不好处理,花费时间肯定更多一点,这个时候需要用 <image src="图片的临时路径" mode=""></image> 代替 canvas 做展示。

5.4.2、解决

改写ctx.draw()为如下:

		ctx.draw(
				false,
				setTimeout(async () => {
					//在这里生成临时路径
					const { tempFilePath } = await uni.canvasToTempFilePath({
						canvasId: canvasId.value,
					});
					console.log('tempFilePath', tempFilePath);
					await uni.saveImageToPhotosAlbum({
						filePath: tempFilePath
					})
				}, 100),
			);

由于绘制可能需要更长的时间,通过延时器即可解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1127698.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法学习(四)将一颗二叉搜索树转排序的双向链表

描述 输入一棵二叉搜索树&#xff0c;将该二叉搜索树转换成一个排序的双向链表。如下图所示 数据范围&#xff1a;输入二叉树的节点数 0 \le n \le 10000≤n≤1000&#xff0c;二叉树中每个节点的值 0\le val \le 10000≤val≤1000 要求&#xff1a;空间复杂度O(1)O(1)&#x…

无障碍阅读他人开源项目结构:看完本文,你将信心满满

先看看阿里是怎么约定的 我印象中&#xff0c;以前在看《阿里巴巴Java开发手册》时&#xff0c;好像有关于工程结构和应用分层相关的内容&#xff0c;于是我回翻了一下&#xff0c;果然有&#xff1a; 它这里面讲的内容大概就是&#xff1a;关于一个正常的企业项目里一种通用的…

项目中拖拽元素,可以使用html的draggable属性,当然也可以用第三方插件interact

项目中拖拽元素&#xff0c;可以使用html的draggable属性&#xff0c;当然也可以用第三方插件interact 一、安装二、引用三、使用 一、安装 npm install interactjs二、引用 import interact from interactjs三、使用 <div class"drag_box"> &…

Linux虚拟网络设备—Veth Pair

veth是Virtual Ethernet Device的缩写&#xff0c;是一种成对出现的Linux虚拟网络接口设备。它最常用的功能是用于将不同的Linux network namespaces 命名空间网络连接起来&#xff0c;让二个namespaces之间可以进行通信。我们可以简单的把veth pair理解为用一根网线&#xff0…

2023年【加氢工艺】免费试题及加氢工艺在线考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 加氢工艺免费试题考前必练&#xff01;安全生产模拟考试一点通每个月更新加氢工艺在线考试题目及答案&#xff01;多做几遍&#xff0c;其实通过加氢工艺在线考试很简单。 1、【单选题】《中华人民共和国职业病防治法…

ES6初步了解Symbol的用法

ES6中为我们新增了一个原始数据类型Symbol&#xff0c;让我为大家介绍一下吧&#xff01; Symbol它表示是独一无二的值 Symbol要如何创建 第一种创建方式&#xff1a; let sy Symbol()第二种创建方式&#xff1a; let sy Symbol.for()具体独一无二在哪呢&#xff1f;它们的地…

阻抗导纳控制理解

书籍《Modern Robotics - Mechanics , Planning, and Control》中关于阻抗控制和导纳控制的部分&#xff1a; 下面结合上边的内容谈一谈我对导纳控制的理解。 1、质量-弹簧-阻尼 首先&#xff0c;不论是阻抗控制&#xff0c;还是导纳控制&#xff0c;他们同根同源&am…

计算机网络_03_tcp/ip四层模型

文章目录 1.为什么会有tcp/ip?2.tcp/ip是什么?3.为什么会有tcp/ip四层模型?4.tcp/ip四层模型介绍 1.为什么会有tcp/ip? 早期的计算机(计算机网络没有出现之前)几乎都是各自为战, 各种操作系统厂家百花齐放, 市面上的大部分计算机使用的都是不同的操作系统, 为每个人提供定…

【rk3568-linux】 rk3568x_linux-- 编译说明

概述 一个好的安装教程能够帮助开发者完成更便捷、更快速的开发。书山有路勤为径&#xff0c;学海无涯苦作舟。我是秋知叶i、期望每一个阅读了我的文章的开发者都能够有所成长。 开发环境 开发环境&#xff1a;ubuntu18 文章目录 概述开发环境一、选择型号二、全自动编译三、…

基于蜉蝣算法的无人机航迹规划-附代码

基于蜉蝣算法的无人机航迹规划 文章目录 基于蜉蝣算法的无人机航迹规划1.蜉蝣搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用蜉蝣算法来优化无人机航迹规划。 1.蜉蝣搜索算法 …

一个工作三年的前端是如何做性能优化的

你是怎么做性能优化的&#xff1f;关于这一个问题&#xff0c;也是我们前端开发程序员经常会讨论到的问题&#xff0c;接下来这篇文章将总结一下前端方面的性能优化及方式。 为什么要做性能优化 性能优化是为了提高网页的加载速度和相应速度&#xff0c;给用户带来更好的体验…

asp.net在线考评系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net在线考评系统是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使用c#语言开发 asp.net 在线考评系统 二、功能介绍 本…

Python自动处理pptx:新建、另存、添加幻灯片、添加标题、插入文本图片图形、提取文本

Python-pptx库是一个用于创建、更新和读取Microsoft PowerPoint .pptx 文件的Python库。它允许我们使用Python脚本自动化PowerPoint文件的创建、更新和读取操作&#xff0c;是一个非常方便自动化处理PPTX的工具。 安装 pip install python-pptx创建 from pptx import Prese…

记一次公司项目上的常规渗透测试

授权项目上的渗透测试&#xff0c;漏洞有网站弱口令—存储型XSS—文件上传。 前言 本文由知识星球《网络安全情报攻防站》星友堂主投稿&#xff0c;感谢投稿。授权项目下常规的渗透测试。欢迎朋友们积极投稿&#xff0c;投稿有奖励。天冷了来领奶茶钱 正文 网站首页 首页啥都没…

前端koa搭建服务器(保姆级教程)——part1

目录 koa简介前端项目搭建koa环境第一步&#xff1a;新建项目第二步&#xff1a;环境初始化&#xff0c;安装依赖初始化项目&#xff0c;生成package.json文件安装koa依赖安装koa-router 路由管理依赖安装dotenv 环境变量依赖安装nodemon 热启动依赖 第三步&#xff1a;代码调用…

黄金代理这么多,怎么选?

目前&#xff0c;现货黄金代理已成为了市场中成熟的模式&#xff0c;我们只要在搜索引擎上搜索如何在市场中开户&#xff0c;会搜到各种各样的黄金代理&#xff0c;其中更是不乏服务非常优秀的。部分投资者早就接受了黄金代理的存在&#xff0c;并且率先开始在黄金代理中进行开…

【单调栈】503. 下一个更大元素 II、42. 接雨水

提示&#xff1a;努力生活&#xff0c;开心、快乐的一天 文章目录 503. 下一个更大元素 II&#x1f4a1;解题思路&#x1f914;遇到的问题&#x1f4bb;代码实现&#x1f3af;题目总结 42. 接雨水&#x1f4a1;解题思路&#x1f914;遇到的问题&#x1f4bb;代码实现&#x1f3…

反射Java

反射是获取摸个类的所有对象 构造器 &#xff1a;成员变量&#xff1a; 成员方法 获取方式 package Reflect;//获取class类的对象public class Test1Clacc {public static void main(String[] args)throws Exception {Class c1Student.class;System.out.println(c1.getName(…

神奇代码备份恢复工具逸事与操作指南

文章目录 一&#xff0c;序二&#xff0c;逸事三&#xff0c;为什么今天要提这个工具四&#xff0c;操作界面1. 文章发表者备份项目步骤2. 文章发表者恢复项目操作步骤3. 文章阅读者恢复项目步骤 五&#xff0c;附件1. 示例备份文件2. 神奇代码备份恢复工具源码备份 一&#xf…

淘宝商品详情API接口(标题|主图|SKU|价格|商品销量)

Taobao.item_get-获得淘宝商品详情接口&#xff0c;淘宝商品详情数据接口是淘宝开放平台提供的一种API接口&#xff0c;通过调用该接口&#xff0c;可以获取淘宝商品详情信息。该接口支持多种编程语言&#xff0c;包括Java、PHP、Python等。在使用淘宝商品详情API接口时&#x…