移动端使用canvas做签名板

移动端使用canvas做手写板应用场景应该还是很常见的,比如做签名。。。。本文将解决以下几个问题:

  1. 判断用户到底有没有签名,如果没有签名却点击完成按钮就提示请签名
  2. 只保存签名区域。例如canvas画布是500×500的,但是签名只用了100×100的区域,那么我们需要只截取100*100的区域保存
  3. 阻止手机浏览器的默认事件。默认情况下比如说安卓,手指右滑可能是返回上一层,例如苹果,手指下滑可能回弹效果,又例如微信里面也是会有回弹效果。对于用户签名时,这些默认事件需要禁止。
  4. canvas保存成File文件使用formData上传到服务器

解决方案:

  1. 每个canvas实例都有一个toDataURL属性,将canvas转换成base64,然后对比是否相同,进而判断有没有签名。
  2. canvas上下文有getImageDataputImageData方法可以帮助解决第二个问题的裁剪canvas问题,还有就是通过pageXpageY不断的计算用户触摸点位的最大值和最小值
  3. 使用preventDefault阻止默认事件触发
  4. 也就是将base64转换成blob类型的过程

react为例子看一下基本结构 components/WordPad/index.tsx

import React, { useEffect, useRef, useImperativeHandle, forwardRef } from "react";

const WordPad = (props, ref) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
  let minY = Infinity, maxY = 0;
  let minX = Infinity, maxX = 0;
  const findYMinAndMax = (y: number) => {
    minY = Math.min(minY, y)
    maxY = Math.max(maxY, y)
  }
  const findXMinAndMax = (x: number) => {
    minX = Math.min(minX, x)
    maxX = Math.max(maxX, x)
  }
  useEffect(() => {
    let canvasEl = canvasRef.current!
    let canvasInstance = canvasEl.getContext('2d')!;
    canvasEl.width = window.innerWidth;
    canvasEl.ontouchstart = function (e: TouchEvent) {
      e.preventDefault && e.preventDefault();
      let touchOptions = e.touches[0];
      canvasInstance.beginPath();
      // 记录用户当前书写的点位
      let x = touchOptions.pageX - canvasEl.offsetLeft;
      let y = touchOptions.pageY - canvasEl.offsetTop;
      findYMinAndMax(y)
      findXMinAndMax(x)
      canvasInstance.moveTo(x, y);
      canvasEl.ontouchmove = function (e) {
        let touchOptions = e.touches[0];
        // 记录用户当前书写的点位
        let x = touchOptions.pageX - canvasEl.offsetLeft;
        let y = touchOptions.pageY - canvasEl.offsetTop;
        findYMinAndMax(y)
        findXMinAndMax(x)
        canvasInstance.lineTo(x, y);
        canvasInstance.stroke();
        e.preventDefault && e.preventDefault();
      };
    }

    const documentTouchend = function () {
      canvasEl.ontouchmove = null;
    };
    document.addEventListener('touchend', documentTouchend)
    return () => {
      canvasEl.ontouchstart = canvasEl.ontouchmove = null
      document.removeEventListener('touchend', documentTouchend)
    }
  }, [])

  useImperativeHandle(ref, () => ({
    // 清空canvas
    clear: () => {
      canvasRef.current?.getContext('2d')?.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
      minY = minX = Infinity, maxY = maxX = 0;
    },
    getUrl: () => {
      let canvas = canvasRef.current!
      // 频繁使用getImageData方法可能存在性能问题, 所以需要设置{ willReadFrequently: true },但是我设置了之后好像没效果,还是有警告
      let context = canvas.getContext("2d", { willReadFrequently: true })!;
      let canvasTmp = document.createElement('canvas')
      let initUrl = canvasTmp?.toDataURL("image/png");
      if (minY !== Infinity && minX !== Infinity) {
        /* 
            找到用户在画布上具体的签名位置,并抠出来
            下面的 +20和-10主要是为了留一些白边
        */
        canvasTmp.width = maxX - minX + 20
        canvasTmp.height = maxY - minY + 20
        canvasTmp.getContext("2d")?.putImageData(context.getImageData(minX - 10, minY - 10, canvasTmp.width, canvasTmp.height), 0, 0);
      }
      let curUrl = canvasTmp?.toDataURL("image/png");
      // 保存最初始的URL和当前URL,方便后续判断用户是否有签名
      return [initUrl, curUrl]
    }
  }))

  return (
    <canvas style={{ 'backgroundColor': 'skyblue' }} ref={canvasRef} height="500" />
  )
}
export default forwardRef(WordPad)

在pages里面使用,我在里面使用了tailwind

import { useRef } from "react";
import WordPad from "../components/WordPad"

export default function Signature() {
  const wordPadRef = useRef(null)

  const clear = () => {
    wordPadRef.current?.clear()
  }
  const submit = async () => {
    let [origin, current] = wordPadRef.current?.getUrl()
    if (origin === current) {
      alert('请签名')
      return
    }
    /* 转blob */
    let arr = current.split(","),
      mime = arr[0].match(/:(.*?);/)[1], // 此处得到的为文件类型
      bstr = atob(arr[1]), // 此处将base64解码
      n = bstr.length,
      u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }

    let blob = new Blob([u8arr], { type: mime })
    let url = URL.createObjectURL(blob)
    window.open(url, "_blank")
  }
  return (
    <>
      <WordPad ref={wordPadRef}></WordPad>
      <div className="px-4 py-3 text-right sm:px-6 h-20 left-0 right-0 w-full"></div>
      <div className="bg-gray-50 px-4 py-3 text-right sm:px-6 fixed bottom-0 left-0 right-0 w-full pb-8">
        <button
          style={{
            border: "1px solid rgb(79 70 229 / var(--tw-bg-opacity))",
            color: 'rgb(79 70 229 / var(--tw-bg-opacity))'
          }}
          onClick={() => clear()}
          className="inline-flex justify-center rounded-md  py-2 px-6 text-base font-semibold text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mr-3"
        >
          清除签名
        </button>
        <button
          onClick={() => submit()}
          className="inline-flex justify-center rounded-md bg-indigo-600 py-2 px-6 text-base font-semibold text-white hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
        >
          提交签名
        </button>
      </div>
    </>
  )
}

完成代码在这里:https://github.com/chaochaoer/signature

这是效果图
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值