微信小程序实现canvas电子签名
一、先看效果
小程序canvas电子签名
二、文档
微信小程序canvas 组件文档
微信小程序canvas API文档
H5Canvas文档
三、分析
1、初始话Canvas容器
2、Canvas触摸事件,bindtouchstart(手指触摸动作开始)、bindtouchmove(手指触摸后移动)、bindtouchend(手指触摸动作结束)、bindtouchcancel(手指触摸动作被打断,如来电提醒,弹窗)
3、记录每次从开始到结束的路径段
4、清除、撤销
四、代码分析
1、页面的布局、Canvas容器的初始化
1、先将屏幕横过来,index.json配置文件,“pageOrientation”: “landscape”
2、wx.getSystemInfoSync() 获取可使用窗口的宽高,赋值给Canvas画布(注意若存在按钮区域、屏幕安全区之类的,需要减去)
// 获取可使用窗口的宽高,赋值给Canvas(宽高要减去上下左右padding的20,以及高度要减去footer区域)wx.createSelectorQuery().select('.footer') // canvas获取节点.fields({node: true, size: true}) // 获取节点的相关信息,node:是否返回节点对应的 Node 实例,size:是否返回节点尺寸.exec((res) => {// 获取手机左侧安全区域(刘海)const deviceInFo = wx.getSystemInfoSync()const canvasWidth = deviceInFo.windowWidth - 20 - deviceInFo?.safeArea?.left || 0const canvasHeight = deviceInFo.windowHeight - res[0].height - 20console.log('canvasWidth', canvasWidth);console.log('canvasHeight', canvasHeight);this.setData({deviceInFo,canvasWidth,canvasHeight})this.initCanvas('init')})
3、通过wx.createSelectorQuery()获取到canvas节点,随即可获取到canvas的上下文实例
// 初始话Canvas画布initCanvas() {let ctx = nulllet canvas = null// 获取Canvas画布以及渲染上下文wx.createSelectorQuery().select('#myCanvas') // canvas获取节点.fields({node: true, size: true}) // 获取节点的相关信息,node:是否返回节点对应的 Node 实例,size:是否返回节点尺寸.exec((res) => { // 执行所有的请求。请求结果按请求次序构成数组// Canvas 对象实例canvas = res[0].node// Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)ctx = canvas.getContext('2d')// Canvas 画布的实际绘制宽高const width = res[0].width;const height = res[0].height;// 获取设备像素比const dpr = wx.getWindowInfo().pixelRatio;// 初始化画布大小canvas.width = width * dpr;canvas.height = height * dpr;// 画笔的颜色ctx.fillStyle = 'rgb(200, 0, 0)';// 指定了画笔(绘制线条)操作的线条宽度ctx.lineWidth = 5// 缩小/放大图像ctx.scale(dpr, dpr)this.setData({canvas, ctx});})},
2、线条的绘制
通过canva组件的触摸事件bindtouchstart、bindtouchmove、bindtouchend、bindtouchcancel结合canvas的路径绘制的方法moveTo(x,y)、lineTo(x,y)、stroke()来实现一段线条的绘制
1、bindtouchstart手指触摸动作开始,结合moveTo(x,y) 用来设置绘图起始坐标的方法确定线段的开始坐标
// 手指触摸动作开始bindtouchstart(event) {let {type, changedTouches} = event;let {x, y} = changedTouches[0];ctx.moveTo(x, y); // 设置绘图起始坐标。},
2、bindtouchend手指触摸动作结束,结合lineTo(x,y) 来绘制一条直线,最后stroke()渲染路径
// 手指触摸动作结束bindtouchend(event) {let {type, changedTouches} = event;let {x, y} = changedTouches[0];ctx.lineTo(x, y);// 绘制ctx.stroke();},
3、但这只是一条直线段,并未实现签名所需的曲线(曲线实质上也是由无数个非常短小的直线段构成)
4、bindtouchmove事件会在手指触摸后移动时,实时返回当前状态
5、那么可否通过bindtouchmove 结合 moveTo ==> lineTo ==> stroke ==> moveTo ==> … 以上一次的结束为下一次的开始这样的方式来实时渲染直线段合并为一个近似的曲线
// 手指触摸后移动 bindtouchmove(event) {let {type, changedTouches} = event;let {x, y} = changedTouches[0];// 上一段终点ctx.lineTo(x, y) // 从最后一点到点(x,y)绘制一条直线。// 绘制ctx.stroke();// 下一段起点ctx.moveTo(x, y) // 设置绘图起始坐标。},
6、归纳封装
// 手指触摸动作开始bindtouchstart(event) {this.addPathDrop(event)},// 手指触摸后移动 bindtouchmove(event) {this.addPathDrop(event)},// 手指触摸动作结束bindtouchend(event) {this.addPathDrop(event)},// 手指触摸动作被打断,如来电提醒,弹窗bindtouchcancel(event) {this.addPathDrop(event)},// 添加路径点addPathDrop(event) {let {ctx, historyImag, canvas} = this.datalet {type, changedTouches} = eventlet {x, y} = changedTouches[0]if(type === 'touchstart') { // 每次开始都是一次新动作// 最开始点ctx.moveTo(x, y) // 设置绘图起始坐标。} else {// 上一段终点ctx.lineTo(x, y) // 从最后一点到点(x,y)绘制一条直线。// 绘制ctx.stroke();// 下一段起点ctx.moveTo(x, y) // 设置绘图起始坐标。}},
3、上一步、重绘、提交
主体思路为每一次绘制完成后都通过wx.canvasToTempFilePath生成图片,并记录下来,通过canvas的drawImage方法将图片绘制到 canvas 上
五、完整代码
1、inde.json
{"navigationBarTitleText": "电子签名","backgroundTextStyle": "dark","pageOrientation": "landscape","disableScroll": true,"usingComponents": {"van-button": "@vant/weapp/button/index","van-toast": "@vant/weapp/toast/index"}
}
2、index.wxml
<!-- index.wxml -->
<view><view class="content" style="padding-left: {{deviceInFo.safeArea.left || 10}}px"><view class="canvas_box"><!-- 定位到canvas画布的下方作为背景 --><view class="canvas_tips">签字区</view><!-- canvas画布 --><canvas class="canvas_content" type="2d" style='width:{{canvasWidth}}px; height:{{canvasHeight}}px' id="myCanvas" bindtouchstart="bindtouchstart" bindtouchmove="bindtouchmove" bindtouchend="bindtouchend" bindtouchcancel="bindtouchcancel"></canvas></view></view><!-- footer --><view class="footer" style="padding-left: {{deviceInFo.safeArea.left}}px"><van-button plain class="item" block icon="replay" bind:click="overwrite" type="warning">清除重写</van-button><van-button plain class="item" block icon="revoke" bind:click="prev" type="danger">撤销</van-button><van-button class="item" block icon="passed" bind:click="confirm" type="info">提交</van-button></view>
</view>
<!-- 提示框组件 -->
<van-toast id="van-toast" />
2、index.less
.content {box-sizing: border-box;width: 100%;height: 100%;padding: 10px;.canvas_box {width: 100%;height: 100%;background-color: #E8E9EC;position: relative;// 定位到canvas画布的下方作为背景.canvas_tips {position: absolute;left: 0;top: 0;width: 100%;height: 100%;font-size: 80px;color: #E2E2E2;font-weight: bold;display: flex;align-items: center;justify-content: center;}// .canvas_content {// width: 100%;// height: 100%;// }}
}
// 底部按钮
.footer {box-sizing: border-box;padding: 20rpx 0;z-index: 2;background-color: #ffffff;text-align: center;position: fixed;width: 100%;box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);left: 0;bottom: 0;display: flex;.item {flex: 1;margin: 0 10rpx;}.scan {width: 80rpx;margin: 0 10rpx;}.moreBtn {width: 150rpx}
}
3、index.js
// index.js
// 获取应用实例
// import request from "../../request/index";
import Toast from '@vant/weapp/toast/toast';const app = getApp()
Page({data: {// expertId: '', // 专家iddeviceInFo: {}, // 设备信息canvasWidth: '', // 画布宽canvasHeight: '', // 画布高canvas: null, // Canvas 对象实例ctx: null, // Canvas 对象上下文实例historyImag: [], // 历史记录,每一笔动作完成后的图片数据,用于每一次回退上一步是当作图片绘制到画布上fileList: [], // 签名后生成的附件initialCanvasImg: '', // 初始画布图,解决非ios设备重设置宽高不能清空画布的问题},onReady() {// 获取可使用窗口的宽高,赋值给Canvas(宽高要减去上下左右padding的20,以及高度要减去footer区域)wx.createSelectorQuery().select('.footer') // canvas获取节点.fields({ node: true, size: true }) // 获取节点的相关信息,node:是否返回节点对应的 Node 实例,size:是否返回节点尺寸.exec((res) => {console.log('res', res);// 获取手机左侧安全区域(刘海)const deviceInFo = wx.getSystemInfoSync()const canvasWidth = deviceInFo.windowWidth - 20 - deviceInFo?.safeArea?.left || 0const canvasHeight = deviceInFo.windowHeight - res[0].height - 20this.setData({deviceInFo,canvasWidth,canvasHeight})this.initCanvas('init')})},onLoad(option) {wx.setNavigationBarTitle({title: '电子签名'})// const {expertId} = option// this.setData({expertId})},// 初始话Canvas画布initCanvas(type) {let ctx = nulllet canvas = nulllet {historyImag, canvasWidth, canvasHeight, deviceInFo, initialCanvasImg} = this.data// 获取Canvas画布以及渲染上下文wx.createSelectorQuery().select('#myCanvas') // canvas获取节点.fields({ node: true, size: true }) // 获取节点的相关信息,node:是否返回节点对应的 Node 实例,size:是否返回节点尺寸.exec((res) => { // 执行所有的请求。请求结果按请求次序构成数组// Canvas 对象实例canvas = res[0].node// Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)ctx = canvas.getContext('2d')// Canvas 画布的实际绘制宽高const width = res[0].widthconst height = res[0].height// 获取设备像素比const dpr = wx.getWindowInfo().pixelRatio// 初始化画布大小canvas.width = width * dprcanvas.height = height * dpr// 画笔的颜色ctx.fillStyle = 'rgb(200, 0, 0)';// 指定了画笔(绘制线条)操作的线条宽度ctx.lineWidth = 5// 如果存在历史记录,则将历史记录最新的一张图片拿出来进行绘制。非ios时直接加载一张初始的空白图片if(historyImag.length !== 0 || (deviceInFo.platform !== 'ios' && type !== 'init')) {// 图片对象const image = canvas.createImage()// 图片加载完成回调image.onload = () => {// 将图片绘制到 canvas 上ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight)}// 设置图片srcimage.src = historyImag[historyImag.length - 1] || initialCanvasImg;}// 缩小/放大图像ctx.scale(dpr, dpr)this.setData({canvas, ctx})// 保存一张初始空白图片if(type === 'init') {wx.canvasToTempFilePath({canvas,png: 'png',success: res => {// 生成的图片临时文件路径const tempFilePath = res.tempFilePaththis.setData({initialCanvasImg: tempFilePath})},})}})},// 手指触摸动作开始bindtouchstart(event) {this.addPathDrop(event)},// 手指触摸后移动 bindtouchmove(event) {this.addPathDrop(event)},// 手指触摸动作结束bindtouchend(event) {this.addPathDrop(event)},// 手指触摸动作被打断,如来电提醒,弹窗bindtouchcancel(event) {this.addPathDrop(event)},// 添加路径点addPathDrop(event) {let {ctx, historyImag, canvas} = this.datalet {type, changedTouches} = eventlet {x, y} = changedTouches[0]if(type === 'touchstart') { // 每次开始都是一次新动作// 最开始点ctx.moveTo(x, y) // 设置绘图起始坐标。} else {// 上一段终点ctx.lineTo(x, y) // 从最后一点到点(x,y)绘制一条直线。// 绘制ctx.stroke();// 下一段起点ctx.moveTo(x, y) // 设置绘图起始坐标。}// 每一次结束或者意外中断,保存一份图片到历史记录中if(type === 'touchend' || type === 'touchcancel') {// 生成图片// historyImag.push(canvas.toDataURL('image/png'))wx.canvasToTempFilePath({canvas,png: 'png',success: res => {// 生成的图片临时文件路径const tempFilePath = res.tempFilePathhistoryImag.push(tempFilePath)this.setData(historyImag)},})}},// 上一步prev() {this.setData({historyImag: this.data.historyImag.slice(0, this.data.historyImag.length - 1)})this.initCanvas()},// 重写overwrite() {this.setData({historyImag: []})this.initCanvas()},// 提交confirm() {const {canvas, historyImag} = this.dataif(historyImag.length === 0) {Toast.fail('请先签名后保存!');return}// 生成图片wx.canvasToTempFilePath({canvas,png: 'png',success: res => {// 生成的图片临时文件路径const tempFilePath = res.tempFilePath// 保存图片到系统wx.saveImageToPhotosAlbum({filePath: tempFilePath,})// this.beforeRead(res.tempFilePath)},})},// // 图片上传// async beforeRead(tempFilePath) {// const that = this;// wx.getImageInfo({// src: tempFilePath,// success(imageRes) {// wx.uploadFile({// url: '', // 仅为示例,非真实的接口地址// filePath: imageRes.path,// name: 'file',// header: {token: wx.getStorageSync('token')},// formData: {// ext: imageRes.type// },// success(fileRes) {// const response = JSON.parse(fileRes.data);// if (response.code === 200) {// that.setData({// fileList: [response.data]// })// that.submit();// } else {// wx.hideLoading();// Toast.fail('附件上传失败');// return false;// }// },// fail(err) {// wx.hideLoading();// Toast.fail('附件上传失败');// }// });// },// fail(err) {// wx.hideLoading();// Toast.fail('附件上传失败');// }// })// },// 提交// submit() {// const {fileList} = this.data// wx.showLoading({title: '提交中...',})// request('post', '', {// fileIds: fileList.map(item => item.id),// }).then(res => {// if (res.code === 200) {// wx.hideLoading();// Toast.success('提交成功!');// setTimeout(() => {// wx.navigateBack({delta: 1});// }, 1000)// }// })// },
})
相关文章:
微信小程序实现canvas电子签名
一、先看效果 小程序canvas电子签名 二、文档 微信小程序canvas 组件文档 微信小程序canvas API文档 H5Canvas文档 三、分析 1、初始话Canvas容器 2、Canvas触摸事件,bindtouchstart(手指触摸动作开始)、bindtouchmove(手指触摸…...

【SpringCloud】Seata微服务事务
Seata微服务事务 分布式事务问题:本地事务分布式事务演示分布式事务问题:示例1 分布式事务理论CAP定理一致性可用性分区容错矛盾 Base理论解决分布式事务的思路 初识SeataSeata的架构部署TC服务微服务集成Seata引入依赖配置TC地址 其他服务 动手实践XA模…...
重新阅读《马说》,感悟“伯乐相马”背后的被选择与选择的大智慧
“初闻不识曲中意,再听已是曲终人”。世有伯乐,然后有千里马。千里马常有,而伯乐不常有。无论你是考研考公等考试大军中的一员,还是已步入社会的打工人或者领导,当你面临被人选择或者选择人时,皆可从《马说…...

深入拆解TomcatJetty(三)
深入拆解Tomcat&Jetty(三) 专栏地址:https://time.geekbang.org/column/intro/100027701 1 Tomcat组件生命周期 Tomcat如何如何实现一键式启停 Tomcat 架构图和请求处理流程如图所示: 对组件之间的关系进行分析,…...
MySQL 实现简单的性能优化
一:硬件优化 更高的网络带宽:在处理大规模的远程请求时可以提高MySQL服务器的响应速度; 更大的内存空间:有助于缓存更多的数据库数据,减少磁盘I/O操作,提高整体性能; 换用企业级SSD࿱…...

AB包资源管理器
简介 ABMgr(Asset Bundle Manager)类是一个用于管理 Unity 中 AssetBundle 资源加载的管理器。它通过字典缓存和管理加载的 AB 包,同时支持同步和异步加载。还包含了卸载和清理 AB 包的方法。 功能解析: 主包加载与依赖管理&…...

Centos7源报错问题
原因:是因为centos7在024年6月份停止维护,导致默认镜像不能使用,更改镜像即可mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/…...

Openlayers高级交互(2/20):清除所有图层的有效方法
Openlayers项目中,经常会放置很多的图层,在业务操作的时候,会做出删除所有图层的行为。这里面给出了一个详细的方法,能够有效的解决 清除所有图层的问题。 效果图 专栏名称内容介绍Openlayers基础实战 (72篇ÿ…...

黑马JavaWeb-day02
什么是JavaScript? JavaScript:简称Js,是一门跨平台、面向对象的脚本语言。是用来控制网页行为的,它能使网页可交互 JavaScript和Java是完全不同的语言,无论是概念还是设计。但是基础语法类似。 JavaScript JavaScript引入方式…...
laravel清除不同缓存
1、清除应用程序缓存: php artisan cache:clear2、清除路由缓存: php artisan route:cache3、清除配置缓存: php artisan config:cache4、清除编译后的视图文件: php artisan view:clear5、清除事件和监听器缓存: ph…...
【Git】解决分支冲突、分支合并、版本回退、版本管理
解决本地冲突 1. 合并分支 假设你正在 main 分支上,想要合并 feature 分支。 git checkout main git merge feature如果两个分支都对同一文件做了不同的修改,Git 会提示你有冲突,并显示冲突文件。 2. 查看冲突文件 使用以下命令查看冲突…...
linux file结构体与inode结构体
在 Linux 系统中,inode 结构体和 file 结构体都是与文件系统相关的重要数据结构。它们各自承担着不同的角色,帮助操作系统管理文件和目录。以下是它们的异同点: inode 结构体 1.定义:inode(索引节点)是文件…...

探索迷宫的奥秘:用 C++ 打造你的迷宫游戏之旅!
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...

JSON 注入攻击 API
文章目录 JSON 注入攻击 API"注入所有东西"是"聪明的"发生了什么? 什么是 JSON 注入?为什么解析器是问题所在解析不一致 JSON 解析器互操作性中的安全问题处理重复密钥的方式不一致按键碰撞响应不一致JSON 序列化(反序列化)中的不一致 好的。JSON 解析器…...

MyBatis入门程序之客户添加、更新与删除
【图书介绍】《SpringSpring MVCMyBatis从零开始学(视频教学版)(第3版)》-CSDN博客 《SpringSpring MVCMyBatis从零开始学 视频教学版 第3版整合开发实战快速开发与项目实战框架技术精讲与整合案例 计算机与互联网 编程语言与程序…...

查缺补漏----数据结构树高总结
① 对于平衡二叉树而言,树高的规律: 高度为h的平衡二叉树的含有的最少结点数(所有非叶节点的平衡因子均为1): n01,n11,n22 含有的最多结点数: (高度为h的满二叉树含有的结点数) ②…...

jenkins添加新服务
jenkins添加新服务 新建item 添加流水线 node{def envname "ENVIRONMENT:1234-dev"def projectGitUrl http://xxxxx/xxxxxx/12345.gitdef imageServer harbor.xxxxx.com //镜像仓库地址def projectAppName 12345-applicationdef projectGitBranch dev//git分…...
网络连接设备的功能与应用概述
目录 一、集线器 二、交换机 三、网桥 四、路由器 五、集线器、交换机、网桥与路由器的比较 备注 一、集线器 定义: 集线器(Hub)是一种物理层设备,它提供多个端口,用于将多个计算机或其他网络设备连接在一起&am…...

【SpringCloud】04-Gateway网关登录校验
1. 网关请求处理流程 2. 网关过滤器 3. 网关实现登录校验 Component // 参数构造器 RequiredArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered {private final AuthProperties authProperties;private final JwtTool jwtTool;private final A…...
FFmpeg 库的简要说明
FFmpeg 库的简要说明: libavutil 功能:提供一系列通用工具函数,旨在简化开发流程。 主要用途: 随机数生成器:用于生成随机数,适用于各种应用。 数据结构:提供常用的数据结构(如链表…...

业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...