1705741420270.png
这里还需要注意,宽高属性需要和css中宽高属性保持相同比例,否则绘画会出现扭曲情况
初始化画布创建完成了,接下来需要实现画笔功能,这时候就需要结合CanvasContext绘图上下文对象预设画笔属性以及后续绘图需要用到的坐标轴
// SignaturePad.jsx
let ctx = null;
let startX = 0;
let startY = 0;
const SignaturePad = () => {
const initCanvas = () => {
// 创建 canvas 的绘图上下文 CanvasContext 对象
ctx = Taro.createCanvasContext('signature');
// 设置描边颜色
ctx.setStrokeStyle('#000000');
// 设置线条的宽度
ctx.setLineWidth(4);
// 设置线条的端点样式
ctx.setLineCap('round');
// 设置线条的交点样式
ctx.setLineJoin('round');
};
useEffect(() => {
initCanvas();
return () => {
ctx = null;
};
}, []);
...
}
绘画
所有准备工作完成,然后就是如何实现绘画功能了。想要实现绘画,要对 canvas 有所了解,canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角,坐标为 (0,0) 。所有元素的位置都相对于原点来定位。所以图中蓝色方形左上角的坐标为距离左边(X 轴)x 像素,距离上边(Y 轴)y 像素,坐标为 (x, y)
Canvas_default_grid.png
Canvas相关属性
image.png
了解了基础知识,我们就基本知道如何实现了。通过onTouchStart确定画笔开始坐标,onTouchMove获取用户在canvas内的绘画路径,将路径上所有的点都填充上颜色。
// 是否绘画过
const isPaint = useRef(false)
const canvasStart = (e) => {
startX = e.touches[0].x;
startY = e.touches[0].y;
// 开始创建一个路径
ctx.beginPath();
};
const canvasMove = (e) => {
if (startX !== 0 && !isPaint.current) {
isPaint.current = true;
}
const { x, y } = e.touches[0];
// 把路径移动到画布中的指定点,不创建线条
ctx.moveTo(startX, startY);
// 增加一个新点,然后创建一条从上次指定点到目标点的线
ctx.lineTo(x, y);
// 画出当前路径的边框
ctx.stroke();
// 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中
ctx.draw(true);
startX = x;
startY = y;
};
const canvasEnd = () => {
ctx.closePath();
};
return (
<Canvas
id="signature"
canvasId="signature"
className="canvas"
onTouchStart={canvasStart}
onTouchMove={canvasMove}
onTouchEnd={canvasEnd}
onTouchCancel={canvasEnd}
width="343"
height="180"
disableScroll // 禁止屏幕滚动以及下拉刷新
/>
)
添加操作
到这里,基础的绘画已经完成了,但是我们是需要将生成的签名保存到服务端的,所以还需要有一个确定操作。
const createImg = async () => {
if (!isPaint.current) {
Taro.showToast({
title: '签名内容不能为空!',
icon: 'none',
});
return false;
}
// 把画布内容导出成图片,返回文件路径
const { FilePath } = await ctx.toTempFilePath();
// 这里就可以做拿到路径的后续操作了
// ...
};
有了确定操作,假如用户签错名字了想要重写,还需要一个清除操作。
let canvasw = 0;
let canvash = 0;
// 获取 canvas 的尺寸(宽高)
const getCanvasSize = () => {
nextTick(() => {
// 小程序查询节点信息方法
const query = Taro.createSelectorQuery();
query
.select('#signature')
.boundingClientRect()
.exec(([rect]) => {
canvasw = rect.width;
canvash = rect.height;
});
});
};
useEffect(() => {
getCanvasSize();
...
}, []);
const clearDraw = () => {
startX = 0;
startY = 0;
// 清除画布上在该矩形区域内的内容
ctx.clearRect(0, 0, canvasw, canvash);
ctx.draw(true);
setIsPaint(false);
};
到这里,一个基础的签名板已经完成了,但是还有一些可以优化的地方,下面我们将继续对它进行一些优化。
清空虽然能解决用户写错的问题,但是只撤回上一笔对用户体验来说是更好的。我们可以创建一个history用于记录用户每一次绘画,然后通过getImageData获取canvas区域隐含的像素数据,将其push()到history中,在触发撤回操作时,将最新一条数据pop()同时清空画布,再通过putImageData将history最后一条像素数据绘制到画布上,这样就能实现撤回效果。
const history = useRef([]);
const canvasEnd = async () => {
ctx.closePath();
const res = await ctx.getImageData({ x: 0, y: 0, width: canvasw, height: canvash });
history.current.push(res);
};
// 撤回
const revoke = () => {
if (!history.current.length) return;
history.current.pop();
if (!history.current.length) {
ctx.clearRect(0, 0, canvasw, canvash);
ctx.draw(true);
return;
}
ctx.putImageData(history.current[history.current.length - 1]);
};