微信小程序实现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 || 0
     const canvasHeight = deviceInFo.windowHeight - res[0].height - 20
     console.log('canvasWidth', canvasWidth);
     console.log('canvasHeight', canvasHeight);
     this.setData({
       deviceInFo,
       canvasWidth,
       canvasHeight
     })
     this.initCanvas('init')
   })

3、通过wx.createSelectorQuery()获取到canvas节点,随即可获取到canvas的上下文实例

  // 初始话Canvas画布
  initCanvas() {
    let ctx = null
    let 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.data
    let {type, changedTouches} = event
    let {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: '', // 专家id
    deviceInFo: {}, // 设备信息
    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 || 0
        const canvasHeight = deviceInFo.windowHeight - res[0].height - 20
        this.setData({
          deviceInFo,
          canvasWidth,
          canvasHeight
        })
        this.initCanvas('init')
      })
  },
  onLoad(option) {
    wx.setNavigationBarTitle({
      title: '电子签名'
    })
    // const {expertId} = option
    // this.setData({expertId})
  },
  // 初始话Canvas画布
  initCanvas(type) {
    let ctx = null
    let canvas = null
    let {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].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
        // 如果存在历史记录,则将历史记录最新的一张图片拿出来进行绘制。非ios时直接加载一张初始的空白图片
        if(historyImag.length !== 0 || (deviceInFo.platform !== 'ios' && type !== 'init')) {
          // 图片对象
          const image = canvas.createImage()
          // 图片加载完成回调
          image.onload = () => {
            // 将图片绘制到 canvas 上
            ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight)
          }
          // 设置图片src
          image.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.tempFilePath
              this.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.data
    let {type, changedTouches} = event
    let {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.tempFilePath
          historyImag.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.data
    if(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)
  //     }
  //   })
  // },
})

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++第八讲:STL--stack和queue的使用及模拟实现

C第八讲&#xff1a;STL--stack和queue的使用及模拟实现 1.stack的使用2.queue的使用3.栈和队列OJ题3.1题目1&#xff1a;最小栈3.2题目2&#xff1a;栈的压入、弹出序列3.3题目3&#xff1a;逆波兰表达式求值3.4题目4&#xff1a;用栈实现队列 4.栈的模拟实现5.队列的模拟实现…

某大型液压企业干部职业化项目纪实

某大型液压企业干部职业化项目纪实 ——引入三级职能分解&#xff0c;监督检查标准化 【导读】 企业逐渐发展&#xff0c;人员规模逐渐扩大的同时&#xff0c;中层管理者的数量也大幅增加&#xff0c;但是&#xff0c;管理人员增加了&#xff0c;管理问题却越来越多。公司很…

国产标准数字隔离器的未来---克里雅半导体

标准数字隔离器是电信号隔离技术的重要组成部分&#xff0c;近年来取得了重大进展。随着工业自动化、汽车电子和电信等行业对更高性能的需求不断增长&#xff0c;国内数字隔离器制造商正在稳步赶上全球标准。本文讨论了数字隔离器技术的新兴趋势、材料创新的影响&#xff0c;以…

3.cpp基本数据类型

cpp基本数据类型 1.cpp基本数据类型 1.cpp基本数据类型 C基本数据类型和C语言的基本数据类型差不多 注意bool类型&#xff1a;存储真值 true 或假值 false&#xff0c;C语言编译器C99以上支持。 C语言的bool类型&#xff1a;要添加 #include <stdbool.h>头文件 #includ…

考研读研生存指南,注意事项

本视频&#xff0c;涉及考研读研的方方面面&#xff0c;从考研初试→复试面试→研究生生活→导师相处→论文专利写作混毕业&#xff0c;应有尽有。有了他&#xff0c;你的研究生生涯稳了。 读研考研注意事项&#xff0c;研究生生存指南。_哔哩哔哩_bilibili 一、考研初试注意事…

“声音”音源设置和音效播放

学习如何使用音效系统&#xff0c;背景音乐和其他特别的音效&#xff0c;跳跃攻击等等 学习如何在unity当中使用整套的音效系统&#xff0c;使用之前&#xff0c;我们先来确定一下我们要使用的音乐和音效&#xff0c;在Unity Asset Store当中搜索&#xff0c;添加到我们的unit…

ICP许可证网站模板审核专用下载

ICP许可证网站模板审核专用下载 在当今的数字化时代&#xff0c;互联网的合规性变得尤为重要&#xff0c;特别是在中国。ICP许可证&#xff0c;即互联网信息服务业务经营许可证&#xff0c;是经营性网站必须持有的合法证件。为了帮助网站快速达到合规要求&#xff0c;选择合适…

出海IAA产品如何提升广告展示率?

大家好&#xff0c;我是牢鹅&#xff01;对于出海有做IAA的开发者来说&#xff0c;收益的增长至关重要。而广告收益&#xff0c;又与广告展示率息息相关。 牢鹅根据自身经验和AdMob的一些公开资料&#xff0c;总结了下面几点和提升广告展示率的方法&#xff0c;大家可以对照进…

在不支持AVX的linux上使用PaddleOCR

背景 公司的虚拟机CPU居然不支持avx, 默认的paddlepaddle的cpu版本又需要有支持avx才行,还想用PaddleOCR有啥办法呢? 是否支持avx lscpu | grep avx 支持avx的话,会显示相关信息 如果不支持的话,python运行时导入paddle会报错 怎么办呢 方案一 找公司it,看看虚拟机为什么…

数字图像处理的概念(二)

一 图像处理的概念 1 图像处理的内容 它是研究图像的获取、传输、存储、变换、显示、理解与综合利用的一门崭新学科。根据抽象程度不同可分为三个层次&#xff1a;狭义图像处理、图像分析和图像理解。如图 1.2.1 所示。 具体而言&#xff0c;数字图像处理的内容包括 图像的数…

【OceanBase探会】云与 AI 赋能一体化数据库的创新之旅

前言 哈喽&#xff0c;大家好&#xff0c;我是不叫猫先生&#xff0c;非常荣幸受邀参加2024年10月23日的「OceanBase2024年度发布会」&#xff0c;感受这场数据库技术的盛宴。 在云和 AI 时代&#xff0c;构建一体化数据库已成为现代数据架构的核心。随着数据量的激增和应用场…

Linux系统块存储子系统分析记录

1 Linux存储栈 通过网址Linux Storage Stack Diagram - Thomas-Krenn-Wiki-en&#xff0c;可以获取多个linux内核版本下的存储栈概略图&#xff0c;下面是kernel-4.0的存储栈概略图&#xff1a; 2 存储接口、传输速度 和 协议 2.1 硬盘 《深入浅出SSD&#xff1a;固态存储核心…

信息安全工程师(69)数字水印技术与应用

前言 数字水印技术是一种在数字媒体中嵌入特定信息的技术&#xff0c;这些信息可以是版权信息、元数据等。 一、数字水印技术的定义与原理 数字水印技术&#xff08;Digital Watermarking&#xff09;是将一些标识信息&#xff08;即数字水印&#xff09;直接嵌入数字载体&…

ASP.NET Core开发Chatbot API

本文介绍基于ASP.NET Core的Chatbot Restful API开发&#xff0c;通过调用大语言模型的SDK&#xff0c;完成一个简单的示例。并且通过容器化进行部署. 安装 首先需要安装.NET环境&#xff0c;笔者在Ubuntu 22.04通过二进制包进行安装&#xff0c;Windows和Mac下都有installer…

DDR Study - LPDDR Write and Training

参考来源&#xff1a;JESD209-4B&#xff0c;JESD209-4E LPDDR Initial → LPDDR Write Leveling and DQ Training → LPDDR Read and Training → LPDDR Write and Training → LPDDR Clock Switch → PIM Technical Write Command 基于JEDEC标准中可以看到Write Timing信息…

LC专题:图

文章目录 133. 克隆图 133. 克隆图 题目链接&#xff1a;https://leetcode.cn/problems/clone-graph/?envTypestudy-plan-v2&envId2024-spring-sprint-100 又一次写到这个题目&#xff0c;思路仍然不清晰&#xff0c;给我的感觉是使用递归解题&#xff0c;但是递归具体…

基于springboot企业微信SCRM管理系统源码带本地搭建教程

系统是前后端分离的架构&#xff0c;前端使用Vue2&#xff0c;后端使用SpringBoot2。 技术框架&#xff1a;SpringBoot2.0.0 Mybatis1.3.2 Shiro swagger-ui jpa lombok Vue2 Mysql5.7 运行环境&#xff1a;jdk8 IntelliJ IDEA maven 宝塔面板 系统与功能介绍 基…

雷池社区版有多个防护站点监听在同一个端口上,匹配顺序是怎么样的

如果域名处填写的分别为 IP 与域名&#xff0c;那么当使用进行 IP 请求时&#xff0c;则将会命中第一个配置的站点 以上图为例&#xff0c;如果用户使用 IP 访问&#xff0c;命中 example.com。 如果域名处填写的分别为域名与泛域名&#xff0c;除非准确命中域名&#xff0c;否…

顶点着色网格转换为 UV 映射的纹理化网格

https://dylanebert-instanttexture.hf.space/ 简介 顶点着色是一种将颜色信息直接应用于网格顶点的简便方法。这种方式常用于生成式 3D 模型的构建&#xff0c;例如InstantMesh。然而&#xff0c;大多数应用程序更偏好使用 UV 映射的纹理化网格。 InstantMeshhttps://hf.co/sp…

3D-IC——超越平面 SoC 芯片的前沿技术

“3D-IC”&#xff0c;顾名思义是“立体搭建的集成电路”&#xff0c;相比于传统平面SoC&#xff0c;3D-IC引入垂直堆叠芯片裸片&#xff08;die&#xff09;和使用硅通孔&#xff08;TSV&#xff09;等先进封装技术&#xff0c;再提高性能、降低功耗和增加集成度方面展现了巨大…