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

有段时间没写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基于屏幕宽度占比,随便设置,我这里设置占屏幕宽度的百分之八十// 绘制图片,并转成base64const drawBgImg = async () => {// 图片临时地址重新赋值,避免图片加载路径都是一样的imgTemp.value = wx.env.USER_DATA_PATH + `/${new Date().getTime()}-temp.png`const ctx = context.valueconst { 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) / 2ctx.drawImage(imgUrl, (canvasWidth.value - imageWidth) / 2, offsetNumber, imageWidth, imageHeight);}// 绘制文字const drawTxt = async () => {const ctx = context.valueconst 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}// 初始化canvasconst initCanvas = async () => {try {uni.showLoading()const { windowWidth } : any = await getSystemInfo()const ctx = context.valuecanvasHeight.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 = falseimageObj.value = imageList[imageObj.value.id + 1] ?? imageList[0]pageColor.value = imageObj.value.pageColorsetTimeout(() => {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 } = paramstry {// 获取图片信息:地址、宽高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;// 生成base64const 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 } = configctx.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 } = paramsreturn 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 : stringdesc : stringimageSrc : stringbgColor : stringpageColor : 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.mzph.cn/news/116154.shtml

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

相关文章

spring cloud Eureka集群模式搭建(IDEA中运行)《二》

上一篇集群配置文件完善 上一篇博客&#xff0c;想必大家都学会了Eureka集群模式的搭建和运行&#xff0c;针对上一篇的配置文件进行了优化&#xff0c;在这里分享给大家。上一篇主要有3个配置文件&#xff0c;分别对应3个不同的服务&#xff0c;这种形式配置文件分别写在了不…

uni-app 小宠物 - 会说话的小鸟

在 template 中 <view class"container"><view class"external-shape"><view class"face-box"><view class"eye-box eye-left"><view class"eyeball-box eyeball-left"><span class"…

Learning Open-World Object Proposals without Learning to Classify(论文解析)

Learning Open-World Object Proposals without Learning to Classify 摘要1 介绍2 相关工作3 方法3.1 基线3.2 基于纯定位的对象性3.3. 对象定位网络 (OLN)4 实验4.1跨类泛化4.2.开放世界类不可知检测4.3更多的跨数据集泛化4.3.1 Objects365 泛化4.3.2 EpicKitchens 的泛化4.4…

LeetCode LCR 179. 查找总价格为目标值的两个商品

和为 s 的两个数字 题目链接 LCR 179. 查找总价格为目标值的两个商品 购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况&#xff0c;返回任一结果即可。 示例 1&#xff1a; 输入&#xff1a;price [3, 9, 12, …

Hadoop3教程(三十六):(生产调优篇)企业开发场景中的参数调优案例概述

文章目录 &#xff08;170&#xff09;企业开发场景案例HDFS参数调优MapReduce参数调优YARN参数调优执行程序 参考文献 &#xff08;170&#xff09;企业开发场景案例 这章仅做兴趣了解即可。 需求&#xff1a;从1G数据中&#xff0c;统计每个单词出现次数。服务器3台&#x…

【通览一百个大模型】Baize(UCSD)

【通览一百个大模型】Baize&#xff08;UCSD&#xff09; 作者&#xff1a;王嘉宁&#xff0c;本文章内容为原创&#xff0c;仓库链接&#xff1a;https://github.com/wjn1996/LLMs-NLP-Algo 订阅专栏【大模型&NLP&算法】可获得博主多年积累的全部NLP、大模型和算法干货…

Delphi 编程实现拖动排序并输出到文档

介绍&#xff1a;实现拖动排序功能&#xff0c;并将排序后的内容输出到文档中。我们将使用 Delphi 的组件来创建一个界面&#xff0c;其中包括一个 Memo 控件用于输入内容&#xff0c;一个 ListBox 控件用于显示排序后的内容&#xff0c;并且提供按钮来触发排序和输出操作。 代…

常用Win32 API的简单介绍

目录 前言&#xff1a; 控制控制台程序窗口的指令&#xff1a; system函数&#xff1a; COORD函数&#xff1a; GetStdHandle函数&#xff1a; GetConsoleCursorInfo函数&#xff1a; CONSOLE_CURSOR_INFO函数&#xff1a; SetConsoleCursorInfo函数&#xff1a; SetC…

docker自动构建jar镜像,自动发布最新镜像的简单shell脚本

使用docker自动构建spring boot jar&#xff0c;自动发布最新镜像的简单shell脚本。一般在docker上部署流程问为&#xff1a; 构建docker镜像 -> 停止旧版本 -> 杀掉旧版本 -> 部署最新镜像。 1、maven/mvd/gradle 打包spring boot jar 2、编写Dockerfile&#xff0c…

读书笔记:Effective C++ 2.0 版,条款37(不要重新定义继承而来的非虚函数)、条款38(不重新定义继承而来的缺省参数值)

条款37: 决不要重新定义继承而来的非虚函数 非虚函数在编译期静态绑定&#xff0c;容易出错。 概念上也不合适。 任何条件下都要禁止重新定义继承而来的非虚函数。 条款38: 决不要重新定义继承而来的缺省参数值 继承一个有缺省参数值的虚函数。 虚函数是动态绑定而缺省参数值是…

Java面试题-Java核心基础-第十一天(注解)

目录 一、注解是什么&#xff1f; 二、注解的作用&#xff1f; 三、Java中的内置注解有哪些&#xff1f; 四、如何自定义一个注解&#xff1f; 五、JDK8中的新特性 一、注解是什么&#xff1f; 注解就是一种可以标注在类、属性、方法、方法参数等结构上面的一种特殊“注释…

解决“您点击的链接已过期”;The Link You Followed Has Expired的问题

今天WP碰到一个坑。无论发布文章还是更新插件、更换主题都是这么一种状态“您点击的链接已过期”&#xff1b;The Link You Followed Has Expired 百度出来的答案都是修改post_max_size 方法1. 通过functions.php文件修复 这种方法更容易&#xff0c;只需将以下代码添加到Wor…

Flutter和SwiftUI比较

0.语言 SwiftUI 毫无疑问是Swift语言编写&#xff0c; 在2019年正式推出&#xff0c;目前最新是Swift 5.9 (2023年9月)&#xff0c;由Apple公司维护和发行&#xff1b; 该编程语言发明人已离职Apple。 语言官网&#xff1a;https://developer.apple.com/swift/ 最好用Xcode编…

maven仓库改国内源

今天准备复现漏洞环境&#xff0c;发现太慢&#xff0c;需要配置国内源 file -> settings 搜索maven 修改settings.xml&#xff0c;这里的需要修改两个文件 1.上图的settings.xml文件 2.idea的maven模块 settings.xml文件将原来的注释掉&#xff0c;然后把阿里的添加上&…

【前端设计模式】之调停者模式(中介者模式)

调停者模式是一种行为设计模式&#xff0c;它通过引入一个调停者对象来集中处理一组对象之间的交互。调停者模式的目标是减少对象之间的直接通信&#xff0c;从而降低耦合度&#xff0c;并且使代码更易于维护和扩展。 调停者模式特性 将对象之间的通信集中在一个调停者对象中…

Web前端—Flex布局:标准流、浮动、Flex布局、综合案例(短视频首页解决方案)

版本说明 当前版本号[20231024]。 20231024初版 目录 文章目录 版本说明目录Flex布局01-标准流02-浮动基本使用产品区域布局HTML标签CSS样式 清除浮动场景搭建额外标签法单伪元素法双伪元素法overfow法 03-Flex布局Flex组成主轴对齐方式侧轴对齐方式修改主轴方向弹性伸缩比弹…

【Spring Cloud】seata分布式事务官方入门案例(导读2)

文章目录 1. 准备seata环境1.1. 生产环境启动seata1.2. 采用代码启动seata 2. 开始测试3. 附录3.1. 如果第一次配置seata压缩包3.2. 常见问题参考 本文是针对官方seata入门文章https://sca.aliyun.com/zh-cn/docs/2022.0.0.0/user-guide/seata/quick-start的 补充说明&#xf…

第3章 指令级并行及其利用

3.1 指令级并行&#xff1a;概念和挑战 1985年之后几乎所有处理器都使用流水线来使指令能重叠执行。由于指令可以并行执行&#xff0c;所有指令之间的这种可能得重叠称为指令级并行ILP。 ILP大体有两种实现方法&#xff1a; 1. 依靠硬件来动态发现并实现并行&#xf…

基于C语言 --- 自己写一个通讯录

C语言程序设计笔记---039 C语言之实现通讯录1、介绍C/C程序的内存开辟2、C语言实现通讯录2.1、ContactMain.c程序大纲2.2、Contact2.h2.3、Contact2.c2.3.1 InitContact( )初始化通讯录函数2.3.2 AddContact( )添加联系人和CheckCapaticy( )检查容量函数2.3.3、ShowContact( )显…

项目架构落地之需求分析(一)

目标 尽可能全面准确、全面、深入理解业务&#xff08;5W2H&#xff09;识别重难点业务理清业务流程和功能点识别非功能需求和质量约束 一.需求调研 派什么人&#xff1a; 懂业务头脑灵活懂技术擅长沟通业务经验多情商高 调研之前&#xff1a; 做功课假想系统 调研中&am…