微信小程序手写签名组件
该组件基于signature_pad封装,signature_pad本身是web端的插件,此处将插件代码修改为小程序端可用。
signature_pad.js
/*!* Signature Pad v5.0.3 | https://github.com/szimek/signature_pad* (c) 2024 Szymon Nowak | Released under the MIT license*/
!(function (t, e) {"object" == typeof exports && "undefined" != typeof module? (module.exports = e()): "function" == typeof define && define.amd? define(e): ((t ="undefined" != typeof globalThis? globalThis: t || self).SignaturePad = e());
})(this, function () {"use strict";class t {constructor(t, e, i, n) {if (isNaN(t) || isNaN(e))throw new Error(`Point is invalid: (${t}, ${e})`);(this.x = +t),(this.y = +e),(this.pressure = i || 0),(this.time = n || Date.now());}distanceTo(t) {return Math.sqrt(Math.pow(this.x - t.x, 2) + Math.pow(this.y - t.y, 2));}equals(t) {return (this.x === t.x &&this.y === t.y &&this.pressure === t.pressure &&this.time === t.time);}velocityFrom(t) {return this.time !== t.time? this.distanceTo(t) / (this.time - t.time): 0;}}class e {static fromPoints(t, i) {const n = this.calculateControlPoints(t[0], t[1], t[2]).c2,s = this.calculateControlPoints(t[1], t[2], t[3]).c1;return new e(t[1], n, s, t[2], i.start, i.end);}static calculateControlPoints(e, i, n) {const s = e.x - i.x,o = e.y - i.y,r = i.x - n.x,h = i.y - n.y,a = (e.x + i.x) / 2,c = (e.y + i.y) / 2,d = (i.x + n.x) / 2,l = (i.y + n.y) / 2,u = Math.sqrt(s * s + o * o),v = Math.sqrt(r * r + h * h),_ = u + v == 0 ? 0 : v / (u + v),p = d + (a - d) * _,m = l + (c - l) * _,g = i.x - p,w = i.y - m;return { c1: new t(a + g, c + w), c2: new t(d + g, l + w) };}constructor(t, e, i, n, s, o) {(this.startPoint = t),(this.control2 = e),(this.control1 = i),(this.endPoint = n),(this.startWidth = s),(this.endWidth = o);}length() {let t,e,i = 0;for (let n = 0; n <= 10; n += 1) {const s = n / 10,o = this.point(s,this.startPoint.x,this.control1.x,this.control2.x,this.endPoint.x),r = this.point(s,this.startPoint.y,this.control1.y,this.control2.y,this.endPoint.y);if (n > 0) {const n = o - t,s = r - e;i += Math.sqrt(n * n + s * s);}(t = o), (e = r);}return i;}point(t, e, i, n, s) {return (e * (1 - t) * (1 - t) * (1 - t) +3 * i * (1 - t) * (1 - t) * t +3 * n * (1 - t) * t * t +s * t * t * t);}}class i {constructor() {try {this._et = new EventTarget();} catch (t) {this._et = document;}}dispatchEvent(t) {return this._et.dispatchEvent(t);}}class n extends i {constructor(t, e = {}) {var i, s, o;super(),(this.canvas = t),(this._drawingStroke = !1),(this._isEmpty = !0),(this._lastPoints = []),(this._data = []),(this._lastVelocity = 0),(this._lastWidth = 0),(this._handleMouseDown = (t) => {this._isLeftButtonPressed(t, !0) &&!this._drawingStroke &&this._strokeBegin(this._pointerEventToSignatureEvent(t));}),(this._handleMouseMove = (t) => {this._isLeftButtonPressed(t, !0) && this._drawingStroke? this._strokeMoveUpdate(this._pointerEventToSignatureEvent(t)): this._strokeEnd(this._pointerEventToSignatureEvent(t), !1);}),(this._handleMouseUp = (t) => {this._isLeftButtonPressed(t) ||this._strokeEnd(this._pointerEventToSignatureEvent(t));}),(this._handleTouchStart = (t) => {1 !== t.touches.length ||this._drawingStroke ||(t.cancelable && t.preventDefault(),this._strokeBegin(this._touchEventToSignatureEvent(t)));}),(this._handleTouchMove = (t) => {1 === t.touches.length &&(t.cancelable && t.preventDefault(),this._drawingStroke? this._strokeMoveUpdate(this._touchEventToSignatureEvent(t)): this._strokeEnd(this._touchEventToSignatureEvent(t), !1));}),(this._handleTouchEnd = (t) => {0 === t.touches.length &&(t.cancelable && t.preventDefault(),this._strokeEnd(this._touchEventToSignatureEvent(t)));}),(this._handlePointerDown = (t) => {this._isLeftButtonPressed(t) &&!this._drawingStroke &&(t.preventDefault(),this._strokeBegin(this._pointerEventToSignatureEvent(t)));}),(this._handlePointerMove = (t) => {this._isLeftButtonPressed(t, !0) && this._drawingStroke? (t.preventDefault(),this._strokeMoveUpdate(this._pointerEventToSignatureEvent(t))): this._strokeEnd(this._pointerEventToSignatureEvent(t), !1);}),(this._handlePointerUp = (t) => {this._isLeftButtonPressed(t) ||(t.preventDefault(),this._strokeEnd(this._pointerEventToSignatureEvent(t)));}),(this.velocityFilterWeight = e.velocityFilterWeight || 0.7),(this.minWidth = e.minWidth || 0.5),(this.maxWidth = e.maxWidth || 2.5),(this.throttle = null !== (i = e.throttle) && void 0 !== i ? i : 16),(this.minDistance =null !== (s = e.minDistance) && void 0 !== s ? s : 5),(this.dotSize = e.dotSize || 0),(this.penColor = e.penColor || "black"),(this.backgroundColor = e.backgroundColor || "rgba(0,0,0,0)"),(this.compositeOperation = e.compositeOperation || "source-over"),(this.canvasContextOptions =null !== (o = e.canvasContextOptions) && void 0 !== o ? o : {}),(this._strokeMoveUpdate = this.throttle? (function (t, e = 250) {let i,n,s,o = 0,r = null;const h = () => {(o = Date.now()),(r = null),(i = t.apply(n, s)),r || ((n = null), (s = []));};return function (...a) {const c = Date.now(),d = e - (c - o);return ((n = this),(s = a),d <= 0 || d > e? (r && (clearTimeout(r), (r = null)),(o = c),(i = t.apply(n, s)),r || ((n = null), (s = []))): r || (r = setTimeout(h, d)),i);};})(n.prototype._strokeUpdate, this.throttle): n.prototype._strokeUpdate),(this._ctx = t.getContext("2d", this.canvasContextOptions)),this.clear();}clear() {const { _ctx: t, canvas: e } = this;(t.fillStyle = this.backgroundColor),t.clearRect(0, 0, e.width, e.height),t.fillRect(0, 0, e.width, e.height),(this._data = []),this._reset(this._getPointGroupOptions()),(this._isEmpty = !0);}fromDataURL(t, e = {}) {return new Promise((i, n) => {const s = new Image(),o = e.ratio || window.devicePixelRatio || 1,r = e.width || this.canvas.width / o,h = e.height || this.canvas.height / o,a = e.xOffset || 0,c = e.yOffset || 0;this._reset(this._getPointGroupOptions()),(s.onload = () => {this._ctx.drawImage(s, a, c, r, h), i();}),(s.onerror = (t) => {n(t);}),(s.crossOrigin = "anonymous"),(s.src = t),(this._isEmpty = !1);});}toDataURL(t = "image/png", e) {return ("number" != typeof e && (e = void 0), this.canvas.toDataURL(t, e));}isEmpty() {return this._isEmpty;}fromData(t, { clear: e = !0 } = {}) {e && this.clear(),this._fromData(t, this._drawCurve.bind(this), this._drawDot.bind(this)),(this._data = this._data.concat(t));}toData() {return this._data;}_isLeftButtonPressed(t, e) {return e ? 1 === t.buttons : !(1 & ~t.buttons);}_pointerEventToSignatureEvent(t) {return {event: t,type: t.type,x: t.x,y: t.y,pressure: "pressure" in t ? t.pressure : 0,};}_touchEventToSignatureEvent(t) {const e = t.changedTouches[0];return {event: t,type: t.type,x: e.x,y: e.y,pressure: e.force,};}_getPointGroupOptions(t) {return {penColor: t && "penColor" in t ? t.penColor : this.penColor,dotSize: t && "dotSize" in t ? t.dotSize : this.dotSize,minWidth: t && "minWidth" in t ? t.minWidth : this.minWidth,maxWidth: t && "maxWidth" in t ? t.maxWidth : this.maxWidth,velocityFilterWeight:t && "velocityFilterWeight" in t? t.velocityFilterWeight: this.velocityFilterWeight,compositeOperation:t && "compositeOperation" in t? t.compositeOperation: this.compositeOperation,};}_strokeBegin(event) {this._drawingStroke = !0;const i = this._getPointGroupOptions()const n = Object.assign(Object.assign({}, i), { points: [] })this._data.push(n);this._reset(i);this._strokeUpdate(event);}_strokeUpdate(t) {if (!this._drawingStroke) return;if (0 === this._data.length) return void this._strokeBegin(t);const e = this._createPoint(t.x, t.y, t.pressure),i = this._data[this._data.length - 1],n = i.points,s = n.length > 0 && n[n.length - 1],o = !!s && e.distanceTo(s) <= this.minDistance,r = this._getPointGroupOptions(i);if (!s || !s || !o) {const t = this._addPoint(e, r);s ? t && this._drawCurve(t, r) : this._drawDot(e, r),n.push({ time: e.time, x: e.x, y: e.y, pressure: e.pressure });}}_strokeEnd(t, e = !0) {this._drawingStroke &&(e && this._strokeUpdate(t),(this._drawingStroke = !1));}_reset(t) {(this._lastPoints = []),(this._lastVelocity = 0),(this._lastWidth = (t.minWidth + t.maxWidth) / 2),(this._ctx.fillStyle = t.penColor),(this._ctx.globalCompositeOperation = t.compositeOperation);}_createPoint(e, i, n) {return new t(e , i, n, new Date().getTime());}_addPoint(t, i) {const { _lastPoints: n } = this;if ((n.push(t), n.length > 2)) {3 === n.length && n.unshift(n[0]);const t = this._calculateCurveWidths(n[1], n[2], i),s = e.fromPoints(n, t);return n.shift(), s;}return null;}_calculateCurveWidths(t, e, i) {const n =i.velocityFilterWeight * e.velocityFrom(t) +(1 - i.velocityFilterWeight) * this._lastVelocity,s = this._strokeWidth(n, i),o = { end: s, start: this._lastWidth };return (this._lastVelocity = n), (this._lastWidth = s), o;}_strokeWidth(t, e) {return Math.max(e.maxWidth / (t + 1), e.minWidth);}_drawCurveSegment(t, e, i) {const n = this._ctx;n.moveTo(t, e), n.arc(t, e, i, 0, 2 * Math.PI, !1), (this._isEmpty = !1);}_drawCurve(t, e) {const i = this._ctx,n = t.endWidth - t.startWidth,s = 2 * Math.ceil(t.length());i.beginPath(), (i.fillStyle = e.penColor);for (let i = 0; i < s; i += 1) {const o = i / s,r = o * o,h = r * o,a = 1 - o,c = a * a,d = c * a;let l = d * t.startPoint.x;(l += 3 * c * o * t.control1.x),(l += 3 * a * r * t.control2.x),(l += h * t.endPoint.x);let u = d * t.startPoint.y;(u += 3 * c * o * t.control1.y),(u += 3 * a * r * t.control2.y),(u += h * t.endPoint.y);const v = Math.min(t.startWidth + h * n, e.maxWidth);this._drawCurveSegment(l, u, v);}i.closePath(), i.fill();}_drawDot(t, e) {const i = this._ctx,n = e.dotSize > 0 ? e.dotSize : (e.minWidth + e.maxWidth) / 2;i.beginPath(),this._drawCurveSegment(t.x, t.y, n),i.closePath(),(i.fillStyle = e.penColor),i.fill();}_fromData(e, i, n) {for (const s of e) {const { points: e } = s,o = this._getPointGroupOptions(s);if (e.length > 1)for (let n = 0; n < e.length; n += 1) {const s = e[n],r = new t(s.x, s.y, s.pressure, s.time);0 === n && this._reset(o);const h = this._addPoint(r, o);h && i(h, o);}else this._reset(o), n(e[0], o);}}}return n;
});
//# sourceMappingURL=signature_pad.umd.min.js.map
组件代码
这里封装展示的是横向签名,但其实画布是竖向,最后获取的是将画布旋转-90
度的图片。
signature.wxml
<page-container show="{{show}}" position="right" bind:afterleave="pageLeave"><view hidden="{{!show}}" class="signature-wrap"><view class="actions-wrap"><view class="actions"><button type="default" class="sign-button" bindtap="tapUndo">撤销</button><button type="warn" class="sign-button" bindtap="tapClear">清除</button><button type="primary" class="sign-button" bindtap="tapConfirm">完成</button></view></view><canvastype="2d"id="signature"class="signature"style="width:{{width}}px; height:{{height}}px;"disable-scroll="{{true}}"bindtouchstart="handleTouchStart"bindtouchmove="handleTouchMove"bindtouchend="handleTouchEnd"></canvas><!-- 旋转图片canvas容器,不在页面上展示 --><view class="offscreen"><canvasid="targetSignature"type="2d"style="width:{{height}}px; height:{{width}}px;"/></view></view>
</page-container>
signature.js 这里需要注意,我的引用路径是'@/static/signature_pad'
,这种写法需要在app.json
处配置resolveAlias自定义路径映射
import SignaturePad from '@/static/signature_pad'Component({/*** 组件的属性列表*/properties: {show: false},/*** 组件的初始数据*/data: {signature: null,width: 0,height: 0,dpr: 1},lifetimes: {ready() {const { windowWidth, windowHeight, pixelRatio } = wx.getWindowInfo()this.setData({width: windowWidth - 60, // 减去按钮区域height: windowHeight,dpr: Math.max(pixelRatio || 1, 2),}, () => {this.init()})}},/*** 组件的方法列表*/methods: {init() {this.createSelectorQuery().select('#signature').fields({ node: true, size: true }).exec((res) => {const { width, height, dpr } = this.dataconst canvas = res[0].nodeconst ctx = canvas.getContext('2d')canvas.width = width * dprcanvas.height = height * dprctx.scale(dpr, dpr)const signature = new SignaturePad(canvas, {ratio: dpr,minWidth: 1,maxWidth: 4,backgroundColor: '#fff'});this.setData({signature})})},handleTouchStart(e) {this.data.signature._handleTouchStart(e)},handleTouchMove(e) {this.data.signature._handleTouchMove(e)},handleTouchEnd(e) {this.data.signature._handleTouchEnd(e)},tapClear() {this.data.signature.clear()},tapUndo() {let data = this.data.signature.toData()if (data) {data.pop()this.data.signature.fromData(data)}},async tapConfirm() {let isEmpty = this.data.signature.isEmpty()if (isEmpty) {return wx.showToast({title: '未签名',icon: 'none'})}const base64Url = this.data.signature.toDataURL()const targetSign = await this.getRotateImage(base64Url)this.triggerEvent('confirm', targetSign)},// 获取旋转后的图片getRotateImage(url) {return new Promise((resolve, reject) => {const query = this.createSelectorQuery()query.select('#targetSignature').node(res => {let canvas = res.nodeconst { width, height } = this.dataconst ctx = canvas.getContext('2d')canvas.width = heightcanvas.height = widthctx.clearRect(0, 0, height, width)ctx.translate(0, width)ctx.rotate(-Math.PI / 2)const image = canvas.createImage()image.onload = () => {ctx.drawImage(image, 0, 0, width, height)// 如果只需要base64,只取这部分就可以const rotatedSign = canvas.toDataURL()ctx.clearRect(0, 0, height, width)resolve(rotatedSign)// 如果需要上传文件等相关处理,写到本地临时文件后做你自己的处理// wx.canvasToTempFilePath({// canvas,// success(res) {// resolve(res.tempFilePath)// }// })}image.src = url}).exec()})}}
})
signature.wxss
.signature-wrap {width: 100vw;height: 100vh;display: flex;z-index: 99;background-color: #fff;border-top: 2rpx solid #eee;
}
.actions-wrap {width: 60px;display: flex;justify-content: center;align-items: flex-end;padding-bottom: 320rpx;border-right: 2rpx solid #eee;
}
.actions {white-space: nowrap;transform: rotate(90deg);display: flex;
}
.actions .sign-button {width: 160rpx;margin-left: 20rpx;
}
.offscreen {position: fixed;left: 9999px;
}
调用组件
index.wxml
<view class="row"><view class="label">签名</view><view class="value" bind:tap="tapSignature"><image wx:if="{{signImg}}" class="sign-img" src="{{signImg}}" mode="heightFix" /><text wx:else class="input">请点击签名</text></view>
</view><!-- 签名组件 -->
<signature wx:if="{{showSign}}" show="{{showSign}}" bindconfirm="confirmSign" bindcancel="cancelSign"></signature>
index.wxss
.row {display: flex;align-items: center;padding: 16rpx 30rpx;border-bottom: 2rpx solid #f2f2f2;
}
.row .label {flex-shrink: 0;
}
.row .value {flex: 1;display: flex;justify-content: flex-end;
}
.row .value .input {color: #999;
}
.row .value .sign-img {height: 80rpx;
}
index.json
{"usingComponents": {"signature": "/components/signature/signature"}
}