微信小程序手写签名

微信小程序手写签名组件

该组件基于signature_pad封装,signature_pad本身是web端的插件,此处将插件代码修改为小程序端可用。

signature_pad.js

javascript 复制代码
/*!
 * 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

html 复制代码
<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自定义路径映射

javascript 复制代码
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.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

css 复制代码
.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

html 复制代码
<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

css 复制代码
	.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

json 复制代码
{
  "usingComponents": {
    "signature": "/components/signature/signature"
  }
}
相关推荐
丁总学Java4 小时前
微信小程序,点击bindtap事件后,没有跳转到详情页,有可能是app.json中没有正确配置页面路径
微信小程序·小程序·json
mosen8685 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
qq22951165026 小时前
微信小程序的汽车维修预约管理系统
微信小程序·小程序·汽车
小飞哥liac16 小时前
微信小程序的组件
微信小程序
stormjun17 小时前
Java基于微信小程序的私家车位共享系统(附源码,文档)
java·微信小程序·共享停车位·私家车共享停车位小程序·停车位共享
Bessie23419 小时前
微信小程序eval无法使用的替代方案
微信小程序·小程序·uni-app
shenweihong20 小时前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
1 天前
微信小程序运营日记(第四天)
微信小程序·小程序
qq22951165021 天前
小程序Android系统 校园二手物品交换平台APP
微信小程序·uni-app
一只不会编程的猫2 天前
微信小程序配置
微信小程序·小程序