微信小程序实现canvas电子签名

embedded/2024/10/22 6:57:57/

一、先看效果

小程序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)//     }//   })// },
})

http://www.ppmy.cn/embedded/129498.html

相关文章

【ARM】ARM架构参考手册_Part A CPU(1)

目录​​​​​​​ 1.1 关于ARM架构 1.1.1 ARM寄存器 1.1.2 异常 1.1.3 状态寄存器 1.2 ARM指令集 1.2.1 分支指令 1.2.2 数据处理指令 算术/逻辑指令 比较指令 乘法指令 计算前导零指令 1.2.3 状态寄存器传送指令 1.2.4 加载和存储指令 加载和存储寄存器 加载…

c++就业 创建新的设计模式

virtual自然生成虚函数表&#xff08;一维数组记录了虚函数地址 通过偏移可以调相对应的方法&#xff09; vp 编译的时候地址自然会赋值给相对应的对象 如何体现多态 没有虚函数重写 那么就是早绑定 就比如subject会转换成base类型 p指向base对象 有虚函数就是晚绑定 p指向subj…

LlamaIndex核心概念查询管道(Query Pipelines)简介

LlamaIndex 查询管道简介 概述 LlamaIndex提供了一个声明性查询API&#xff0c;允许您将不同的模块链接在一起&#xff0c;以便在数据上编排从简单到高级的工作流。 这是以QueryPipeline抽象为中心的。装入各种模块&#xff08;从llm到提示符&#xff0c;再到检索器&#xf…

自由学习记录(12)

综合实践 2D的Shape&#xff0c;Tilemap都要导包的&#xff0c;编辑器也要导包&#xff0c;。。和2d沾边的可能3d都要主动导包 应该综合的去运用&#xff0c;不见得Tilemap就很万能&#xff0c;如果要做什么顶方块的有交互反应的物体&#xff0c; 那直接拖Sprite会更方便一些…

边缘计算网关助力煤矿安全远程监控系统

煤矿开采环境复杂&#xff0c;危险程度高&#xff0c;每一次事故都带给行业血淋淋的教训&#xff0c;安全问题也是政府与行业亟待解决的难题。伴随着技术的发展&#xff0c;煤矿智能化成为行业探索的新方向&#xff0c;降低安全风险也是智能化的重要目标之一。防微杜渐是安全生…

【OpenCV】人脸识别方法

代码已上传GitHub&#xff1a;plumqm/OpenCV-Projects at master EigenFace、FisherFace、LBPHFace 这三种方法的代码区别不大所以就一段代码示例。 EigenFace与FisherFace 1. 将人脸图像展开为一维向量&#xff0c;组成训练数据集 2. PCA&#xff08;EigenFace&#xff09;或…

Feature Browser Page Feature Browser 页面

Feature Browser 页面允许通过提供包含相关设置的结构化功能列表来轻松自定义 Grid Control。此页面如下图所示。 在树 View 中选择特定功能会导致过滤属性网格&#xff0c;以便仅显示与此功能相关的属性和事件。例如&#xff0c;在上图中&#xff0c;仅显示与 XtraGrid 的 Pre…

Unity学习日志-API

Untiy基本API 角度旋转自转相对于某一个轴 转多少度相对于某一个点转练习 角度 this.transform.rotation(四元数)界面上的xyz(相对于世界坐标) this.transform.eulerAngles;相对于父对象 this.transform.localEulerAngles;设置角度和设置位置一样&#xff0c;不能单独设置xz…