快2024了,你还不会手写签名吗?

公众号【码农爱摸鱼】,专注于在摸鱼中愉快的工作和学习~

手写签名(H5\小程序)

手写签名主要是用canvas实现。通过监听手指触碰屏幕事件来完成(touchstart、touchmove、touchcancel)

H5:

这里是用的vue封装一个手写签名的组件 话不多说直接上代码 html:

xml 复制代码
<template>
  <div class="hand_paint" @touchmove.prevent>
    <div class="hand_content">
      <div class="sign_box" id="signBox">
        <canvas id="signCanvas"></canvas>
      </div>

      <div class="btn_boxs">
        <div @click="clear()">重签</div>
        <div @click="save()">确定</div>
      </div>
    </div>
  </div>
</template>

js:

kotlin 复制代码
data() {
  return {
    image: "",
    mousePressed: false, // 是否触碰画板
    c: "",
    ctx: "",

    lastX: 0,
    lastY: 0,
  }
},
mounted() {
  this.image = "";
  this.mousePressed = false;

  this.ctx = document.getElementById("signCanvas").getContext("2d");
  this.c = document.getElementById("signCanvas");
  var signBox = document.getElementById("signBox");
  console.log('clientWidth', signBox.clientWidth)

  this.c.width = signBox.clientWidth; // 设置宽度
  this.c.height = signBox.clientHeight; // 设置高度
  console.log('this.c', this.c.width)

  // 监听touchstart事件,touchmove事件,touchcancel事件等事件
  this.InitThis();
},
methods: {
  InitThis() {
    // 触摸屏
    var _this = this;
    const WIDTH = document.body.clientWidth
    this.c.addEventListener("touchstart", function(event){
      if (event.targetTouches.length == 1) {
        var touch = event.targetTouches[0];
        _this.mousePressed = true;

        _this.Draw(
          touch.pageY - this.offsetLeft,
          WIDTH - this.offsetTop - touch.pageX,
          false
        );
      }
    }, false );
    this.c.addEventListener("touchmove", function(event){
      if (event.targetTouches.length == 1) {
        var touch = event.targetTouches[0];
        if (_this.mousePressed) {
          _this.Draw(
            touch.pageY - this.offsetLeft,
            WIDTH - this.offsetTop - touch.pageX,
            true
          );
        }
      }
    }, false );
    this.c.addEventListener("touchcancel", function(event){
      if (event.targetTouches.length == 1) {
        _this.mousePressed = false;
      }
    }, false );
  },
  // 绘画
  Draw(x, y, isDown) {
    if (isDown) {// 开始移动手指
      this.ctx.beginPath();
      this.ctx.strokeStyle = "#000"; // 颜色
      this.ctx.lineWidth = 3; // 线宽
      this.ctx.lineJoin = "round"; // 拐角类型
      this.ctx.lineMax = 10; // 设置画笔最大线宽
      this.ctx.lineMin = 3; // 设置画笔最小线宽
      this.ctx.linePressure = 1.2; // 设置画笔笔触压力
      this.ctx.smoothness = 30; // 设置画笔笔触大小变化的平滑度
      this.ctx.moveTo(this.lastX, this.lastY);
      this.ctx.lineTo(x, y);
      this.ctx.closePath();
      this.ctx.stroke();
    }
    this.lastX = x;
    this.lastY = y;
  },
  // 清空画板
  clear() {
    this.ctx.setTransform(1, 0, 0, 1, 0, 0);
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
  },
  // 提交签名
  save() {
    this.checkEmpty(); // 非空验证
  },
  checkEmpty() {
    var c = document.getElementById("signCanvas"); // 获取canvas对象
    if (this.isCanvasBlank(c)) {
      return;
    } else {
      var image = this.c.toDataURL("image/png"); // 得到生成后的签名base64位  url 地址
      // console.log(image); // 打印图片base64 url
      this.dataURLtoFile(image, 'signature.png')
    }
  },
  dataURLtoFile(dataurl, filename) {//将base64转换为文件,dataurl为base64字符串,filename为文件名(必须带后缀名,如.jpg,.png)
    var arr = dataurl.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = window.atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while(n--){
      u8arr[n] = bstr.charCodeAt(n);
    }
    let file = new File([u8arr], filename, {type:mime});
    this.aggSignUpload(dataurl, file)
  },
  aggSignUpload(dataurl, file){
    let _this = this

    let formData = new FormData()
    formData.append('file', file);

    // 调用后端接口,通过文件流的方式上传图片
    // 成功回调中将图片base64传回父组件用作展示
        _this.$emit('submit', dataurl)

  },
  // 验证canvas画布内容是否为空
  isCanvasBlank(canvas) {
    var blank = document.createElement("canvas"); // 创建一个空canvas对象
    blank.width = canvas.width;
    blank.height = canvas.height;
    return canvas.toDataURL() == blank.toDataURL(); // 比较值相等则为空
  },
},

css: 由于H5无法完美的强制让浏览器横屏,所以这里使用媒体查询来达到横屏的效果。

css 复制代码
@media screen and (orientation: portrait) { // 当屏幕处于纵向时
  .hand_paint {
    position: absolute;
    width: 100vh;
    height: 100vw;
    top: 0;
    left: 100vw;
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
    transform-origin: 0% 0%;
  }
}

小程序:

小程序与H5不同,没有封装为组件,做成了一个公共页面。因为可以在page的json文件中设置:

横屏 "pageOrientation": "landscape"

竖屏 "pageOrientation": "portrait" (默认)

自动切换横竖屏 "pageOrientation": "auto"

如果是用的uni-app,则:

json 复制代码
{
  "path" : "路径",
  "style" : {
    "navigationBarTitleText": "手写签名",
    "pageOrientation": "landscape"  // 属性值与上面一致
  }
}

注意!!!小程序在横屏的时候,页面可视区域发生了变化,所以在写css样式的时候需要注意。

这里可以使用vmin。vmin是一种视窗单位,也是相对单位,取的是视图窗口宽度百分比vw和高度百分比vh的较小值。使用vmin的好处是可以让小程序页面不论是横屏还是竖屏,字体大小都能保持一致。

我开发小程序用的uni-app,首先声明一个scss函数:

perl 复制代码
@function tovmin($rpx){//$rpx为需要转换的字号
    @return #{$rpx * 100 / 750}vmin;
}

使用时就把原本单位为rpx的数值带入函数即可,例如:40rpx ==> tovmin(40);

和H5的canvas不同,小程序只需要改成这样:

kotlin 复制代码
<canvas id="signCanvas" canvas-id="signCanvas" class="sign_canvas" disable-scroll @touchstart='handlerTouchstart' @touchmove='handlerTouchmove' @touchend='handlerTouchend' @touchcancel='handlerTouchend'></canvas>

disable-scroll这个属性是为了防止在手指滑动时页面发生移动 。 js:

kotlin 复制代码
data() {
    return {
      mousePressed: false, // 是否触碰画板
      c: "",
      ctx: "",
      
	  arrX: [],
	  arrY: [],
	  arrZ: [],
	  isPaint: false, // 是否绘画
			
	  type: ''
    }
},
onReady() {
	this.mousePressed = false;
	
		
	this.ctx = uni.createCanvasContext('signCanvas');
	this.ctx.lineWidth = 3; // 线宽
	this.ctx.lineCap = 'round'; // 线条端点类型
	this.ctx.lineJoin = 'round'; // 线条拐角类型
	this.ctx.globalAlpha = 1; // 透明度
	console.log('ctx',this.ctx)
},
methods: {
	handlerTouchstart(event){
		this.arrX = []
		this.arrY = []
		this.arrZ = []
		if (event.changedTouches.length == 1) {
			 var touch = event.changedTouches[0];
			 // console.log('touchstart',event)
			 this.mousePressed = true;
			
			 this.arrZ.push(0);
			 this.arrX.push(Math.floor(touch.x));
			 this.arrY.push(Math.floor(touch.y));
		}
	},
	handlerTouchmove(event){
		if (event.changedTouches.length == 1) {
			 var touch = event.changedTouches[0];
			 if (this.mousePressed) {
			   this.arrZ.push(1);
			   this.arrX.push(Math.floor(touch.x));
			   this.arrY.push(Math.floor(touch.y));
					
				// 绘画
				for (var i = 0; i < this.arrZ.length; i++) {
					if (this.arrZ[i] == 0) {
						this.ctx.moveTo(this.arrX[i], this.arrY[i])
					} else {
						this.ctx.lineTo(this.arrX[i], this.arrY[i])
					};
				};
				this.ctx.stroke();
				this.ctx.draw(true);
				this.isPaint = true
			 }
		}
			
			
	},
	handlerTouchend(event){
		if (event.changedTouches.length == 1) {
			 this.mousePressed = false;
		}
	},
    // 清空画板
    clearArea() {
			let canvasw, canvash;
			const res = uni.getSystemInfoSync();

			canvasw = res.windowWidth * 1.2; //设备宽度
			canvash = res.windowHeight * 1.2; //设备高度
			console.log('canvasw',canvasw)
			console.log('canvash',canvash)

			this.arrX = []
			this.arrY = []
			this.arrZ = []
			this.isPaint = false
			this.ctx.clearRect(0, 0, canvasw, canvash);
			this.ctx.draw(true);
    },
    // 提交签名
    saveSign() {
            let _this = this
			if (!this.isPaint) { // 画布为空
				return false;
			};
			//生成图片
			const fileManager = wx.getFileSystemManager();
			uni.showLoading({title: '加载中', mask: true})
			uni.canvasToTempFilePath({
				x: 0,
				y: 0,
				canvasId: 'signCanvas',
				success: function (res) {
					//将图片转换为base64 的格式
					uni.hideLoading()
					let base64 = 'data:image/jpg;base64,' + fileManager.readFileSync(res.tempFilePath, 'base64');
					// console.log(base64)
					//图片路径
					// console.log('tempFilePath', res.tempFilePath)
					_this.uploadImg(res.tempFilePath, base64)
				}
			})
    },
    // 上传图片文件
	uploadImg(tempFilePath, base64){
		uni.showLoading({
			 title: '上传中'
		});
		let _this = this
		return new Promise((resolve, reject) => {
			uni.uploadFile({
				method: "post",
				header: {
					"token": userinfo.token,
					"Content-Type": "multipart/form-data"
				},
				url: '接口地址',
				filePath: tempFilePath, // 生成的文件路径
				name: 'file',
				formData: '请求参数',
				success: (res) => {
					uni.hideLoading()
					console.log('res', res)
				},
				fail(err) {
					uni.hideLoading()
					console.log('err', err)
				}
			})
		})
	},
},

我是摸鱼君,你的【三连】就是摸鱼君创作的最大动力,如果本篇文章有任何错误和建议,欢迎大家留言!

文章持续更新,可以微信搜索 【码农爱摸鱼】关注公众号第一时间阅读。

相关推荐
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
前端小超超1 小时前
vue3 ts项目结合vant4 复选框+气泡弹框实现一个类似Select样式的下拉选择功能
前端·javascript·vue.js
大叔是90后大叔1 小时前
vue3中查找字典列表中某个元素的值
前端·javascript·vue.js
幸运小圣1 小时前
Vue3 -- 项目配置之prettier【企业级项目配置保姆级教程2】
前端·vue.js·vue
ZJ_.2 小时前
Electron 沙盒模式与预加载脚本:保障桌面应用安全的关键机制
开发语言·前端·javascript·vue.js·安全·electron·node.js
竹秋…2 小时前
element-plus <el-date-picker>日期选择器踩坑!!!!
javascript·vue.js·elementui