移动端使用canvas
做手写板应用场景应该还是很常见的,比如做签名。。。。本文将解决以下几个问题:
- 判断用户到底有没有签名,如果没有签名却点击完成按钮就提示请签名
- 只保存签名区域。例如
canvas
画布是500×500的,但是签名只用了100×100的区域,那么我们需要只截取100*100的区域保存 - 阻止手机浏览器的默认事件。默认情况下比如说安卓,手指右滑可能是返回上一层,例如苹果,手指下滑可能回弹效果,又例如微信里面也是会有回弹效果。对于用户签名时,这些默认事件需要禁止。
- 将
canvas
保存成File
文件使用formData
上传到服务器
解决方案:
- 每个
canvas
实例都有一个toDataURL
属性,将canvas
转换成base64
,然后对比是否相同,进而判断有没有签名。 canvas
上下文有getImageData
和putImageData
方法可以帮助解决第二个问题的裁剪canvas
问题,还有就是通过pageX
和pageY
不断的计算用户触摸点位的最大值和最小值- 使用
preventDefault
阻止默认事件触发 - 也就是将
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
这是效果图