likes
comments
collection
share

小程序 canvas 2d 手写签名

作者站长头像
站长
· 阅读数 13

在最近的小程序项目中,设计师又给我出难题了,需要在小程序里面实现一个手写签名功能,完成后将签名生成文件,上传的服务端去。手写签名不难,难在居然要我生成 svg,还有将签名功能搞得和画板一样,我是不能忍,直呼搞不了。

安卓版手写签名 github.com/silencefly9…

签名功能实际在我之前的安卓开发中用过,支持笔画的前进、后退、全清除以及图片的导出,真就快做到 生成 SVG 的底部了,要不是我不懂 SVG ,就手撕一个根据 path 路径生成 svg 的代码了。

扯远了,小程序和安卓原生比不得,我就只弄了清除和导出功能。

先看看效果

小程序 canvas 2d 手写签名

这个是使用二阶贝塞尔曲线优化后的效果,曲线很流畅。 小程序 canvas 2d 手写签名

这个是没使用贝塞尔曲线优化的效果,笔画很粗糙。

性能和适配

一开始我用的旧版本 canvas 去绘制签名,因为网上有个县城的案例,后面发现用起来卡的一批,而且在有些手机上导出图片还会有没数据问题,根本没法忍。

于是自己学了学 canvas 2d 的用法,结合贝塞尔曲线,做了个性能和流畅度都还可以的手写签名页面。

看看代码

  • WXML
<canvas class="canvas" type="2d" id="myCanvas" bindtouchstart="bindtouchstart" bindtouchmove="bindtouchmove" />
<cover-view class="row button-container">
  <cover-view class="flex1 button " bindtap='clear'>
    清除
  </cover-view>
  <cover-view class="flex1 button margin-left-12" bindtap='export'>
    提交
  </cover-view>
</cover-view>

这里就是放了一个 canvas,加上使用 cover-view做的两个按钮。

  • JSON
{
  "usingComponents": {},
  "pageOrientation": "landscape",
  "navigationBarTitleText": "手写签名",
  "enablePullDownRefresh": false
}

这里设置页面的横向显示,并禁用了下拉刷新,页面横向显示的时候,导出的图片也是横向的,所以并不需要多考虑。

  • WXSS
.canvas {
  height: 100vh;
  width: 100vw;
}

.button-container {
  width: 150rpx;
  height: 30rpx;
  position: absolute;
  top: 16rpx;
  right: 16rpx;
}

.button {
  background: white;
  color: #616161FF;
  box-sizing: border-box;
  border-radius: 20rpx;
  line-height: 30rpx;
  text-align: center;
  border: 1px solid #C9C9C9;
}

这里 canvas 的宽高设置成了屏幕的宽高,使用单位 vh 和 vw,其他没什么说的。

  • Page.js
var app = getApp()

Page({
  data: {
    canvas: null,
    context: null,

    // 前一点
    preX: 0,
    preY: 0
  },

  onShow: function () {
    const that = this;

    // 获取画布
    const query = wx.createSelectorQuery()
    query.select('#myCanvas')
      .fields({
        node: true,
        size: true
      })
      .exec((res) => {
        const canvas = res[0].node
        const ctx = canvas.getContext('2d')

        const dpr = wx.getSystemInfoSync().pixelRatio
        canvas.width = res[0].width * dpr
        canvas.height = res[0].height * dpr
        ctx.scale(dpr, dpr)

        // 设置笔触信息
        ctx.strokeStyle = '#000000'
        ctx.lineWidth = 2
        ctx.font = "20px Arial"

        that.setData({
          canvas: canvas,
          context: ctx
        })
      })
  },

  /**记录开始点 */
  bindtouchstart: function (e) {
    let context = this.data.context
    let curX = e.changedTouches[0].x
    let curY = e.changedTouches[0].y

    context.beginPath()
    context.moveTo(curX, curY)

    this.data.preX = curX
    this.data.preY = curY
  },

  /**记录移动点,刷新绘制 */
  bindtouchmove: function (e) {
    let context = this.data.context
    let preX = this.data.preX
    let preY = this.data.preY
    let curX = e.changedTouches[0].x
    let curY = e.changedTouches[0].y

    let deltaX = Math.abs(preX - curX)
    let deltaY = Math.abs(preY - curY)

    // 相差大于3像素的时候作二阶贝塞尔曲线
    if (deltaX >= 3 || deltaY >= 3) {
      // 前后两点中心点
      let centerX = (preX + curX) / 2
      let centerY = (preY + curY) / 2

      //这里以前一点作为控制点,中心点作为终点,起始点为上一次的中点,很流畅啊!
      context.quadraticCurveTo(preX, preY, centerX, centerY);
      context.stroke();

      this.data.preX = curX
      this.data.preY = curY
    }
  },

  /**清空画布 */
  clear: function () {
    let context = this.data.context
    let canvas = this.data.canvas

    context.clearRect(0, 0, canvas.width, canvas.height);

    context.strokeStyle = '#000000'
    context.lineWidth = 2
    context.font = "20px Arial"
  },

  /**导出图片 */
  export: function () {
    const that = this;
    wx.showLoading({
      title: '正在生成签名...',
    })
    wx.canvasToTempFilePath({
      canvas: that.data.canvas,
      success(res) {
        wx.hideLoading()
        app.log("tempFilePath:" + res.tempFilePath);

        //上传图片
        ...
      },
      fail() {
        wx.hideLoading()
        wx.showToast({
          title: '生成图片失败',
          icon: 'none',
          duration: 1000
        })
      }
    })
  },
})

这里代码也好理解,一个是记录笔触开始,一个是在笔触移动的时候绘制线条,一个是清除画布,一个是导出图片。

绘制曲线

绘制曲线包含两部分,第一部分是获取开始点,开始画笔路径,移动到开始点,并将开始点记录到全局变量中。

第二部分是当手指移动的时候产生一系列点,将这一系列点连起来。这里用到了二阶贝塞尔曲线,一系列点中,以上两点的中心点为起始点,上一点作为控制点,本次最近两点的中心点作为终点,这样绘制出来的曲线很流畅,有预测曲线方向的作用。

清除画布

清除画布使用了 clearRect 函数,注意重新设置笔触信息。

导出图片

千万别被网上成千上万的旧版本的 canvas的 api 干扰了,当你使用 canvas 2d的时候根本就没有 draw 方法了,当然新的方法时要把 canvas 对象传进去。

上传图片

最后得到的图片时一个临时路径,保存在 res.tempFilePath 中,可以直接在 image 组件中使用,当然要上传的话也可以,上传功能看后台接口吧。

结语

经过 canvas 2d 和贝塞尔曲线的优化,我这手写签名虽然还是比较简陋,但是性能上和流畅度上真的比网上绝大部分博客好得多吧。

end

完美撒花