微信小程序手写签名组件
该组件基于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>
<canvas
type="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">
<canvas
id="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() {
try {
const { windowWidth, windowHeight, pixelRatio } = wx.getWindowInfo();
this.setData({
width: windowWidth - 60, // 减去按钮区域
height: windowHeight,
dpr: Math.max(pixelRatio || 1, 2),
})
} catch (e) { }
this.init()
}
},
/**
* 组件的方法列表
*/
methods: {
init() {
this.createSelectorQuery().select('#signature').fields({ node: true, size: true }).exec((res) => {
const { width, height, dpr } = this.data
const canvas = res[0].node
const ctx = canvas.getContext('2d')
canvas.width = width * dpr
canvas.height = height * dpr
ctx.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.node
const { width, height } = this.data
const ctx = canvas.getContext('2d')
canvas.width = height
canvas.height = width
ctx.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"
}
}