持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天
最近在写vue3 组件库
( 众白:切,又是经典的重复造轮子,菜鸡,不看了)
那么这个组件的起源是这样子滴,那天,月黑风高,晴空万里,(没睡醒猪脑过载中....)
我正静心地造着我的组件库轮子,组长给我丢了个需求,因为移动端有签名的操作,所以需要一个签名的组件,我一想,就这?不就是一个canvas 搞定的事吗,哼,港港单单啦,组长你就放一百个心,交给我了。
说干就干,首先理一下开发思路,首先建立一个canvas 画布,然后监听画布上的触屏,移动,判断移出canvas 画布外停止绘制,最后就是结束触屏,停止绘画。思路整理完毕,接下来就是实际开发:
触屏与移动绘制
移动端触屏事件大家都懂,@touchstart,大家时间都有限,这点就不用我水文解读了吧 (其实是技术不到位,怕解读不好)。不懂的妹妹可以找我,我们彻夜长谈,男妹妹勿扰,找就举报。话不多说,直接上代码:
// 这里呢为了方便,就把start,move 事件合在一起,后面去判断触屏类型就行啦
const touchStartFun = (e: TouchEvent) => {
e.preventDefault();
const canvasObj = signatureCanvasRef.value;
if (!canvasObj) {
return false;
}
if (e.touches.length) {
const trajectory = {
x: e.touches[0].clientX - canvasObj.getBoundingClientRect().left,
y: e.touches[0].clientY - canvasObj.getBoundingClientRect().top,
type: e.type,
};
if (e.type === 'touchstart') {
drawStart(trajectory);
} else if (e.type === 'touchmove') {
drawMove(trajectory, true);
}
}
};
绘制笔划
在drawStart() 方法上记录初始点位置
// 触屏开始
const drawStart = (trajectory: pointsType) => {
startX = trajectory.x;
startY = trajectory.y;
strokePoint(trajectory);
};
// 触屏移动
const drawMove = (trajectory: pointsType, isSave: Boolean) => {
strokePoint(trajectory);
startX = trajectory.x;
startY = trajectory.y;
};
// 开始绘制
const strokePoint = (trajectory: pointsType) => {
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(trajectory.x, trajectory.y);
ctx.stroke();
ctx.closePath();
};
以上就是一个移动端签名组件的实现过程啦,哼,我就说嘛,港港单单,顺便再给他加上一个重开的按钮,以防待会组长找我茬:
// 做个重开按钮的click 事件,clearRect()将像素设置为透明对画布进行擦除
const clearPoint = () => {
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
};
铛铛,完成!跑一下效果,可以,审查一下代码,完工,兴冲冲地交给组长,本来以为到这里就结束了,今天又可以无惊无险,又到六点。
没想到过了一会,组长过来跟我说,你这个签名组件,有个小问题,如果签名错了怎么办。
哼哼,还好我早有准备。
我:有个重开按钮啊,直接/remake 啊。
组长:话是这么说没错,但是如果我前面签好了两个字,我失误了,但我不想重开,我只想上天再给我一次机会,让我回到上一步,如果非要加个次数,我希望是一万次。
???你说加功能就加功能呗,怎么还演起来了。
我:是什么让你这么执着于撤回上一步呢
组长:你看我这字写得,手抖画错了下一步,这么好的字,这不可惜了吗
我定睛一看
我:哇塞,组长,你的字真的好哇塞哦,点点如刀,笔笔似桃,好似瀑布直下三千尺般顺畅,又有重峦叠嶂不老松般的韧劲。
组长眉毛一挑,嘴角微扬:快做吧,这个月的绩效我帮你提了
我:好的好的,谢谢组长,我对组长您的敬佩如长江流水绵绵不绝,巴拉巴拉巴拉...
说完这一整套,我开始考虑起这个撤回功能的思路,撤回,就是抹除上一步的痕迹,首先想到是使用canvas 中的globalCompositeOperation ,globalCompositeOperation 这个API 主要是在绘制图形时,在现有画布上,控制旧图形与新图形合成之间的操作类型。
然后撤回的话呢,我就使用destination-out 这个属性值(destination-out:将现有内容保持在新图形不重叠的地方),就是我绘制一条与背景同色的线,覆盖上一条即将撤回的轨迹。思路有了,那么接下来就是实践出真知:
首先之前的代码不变,在其基础上,我们先将绘画轨迹记录下来:
// 触屏开始
const drawStart = (trajectory: pointsType) => {
// 将globalCompositeOperation 初始化回默认属性source-over
ctx.globalCompositeOperation = 'source-over';
// 颜色回调为初始颜色
ctx.strokeStyle = '#000000';
startX = trajectory.x;
startY = trajectory.y;
strokePoint(trajectory);
// 记录下绘画轨迹
points.unshift(trajectory);
};
// 开始绘制
const strokePoint = (trajectory: pointsType) => {
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(trajectory.x, trajectory.y);
ctx.stroke();
ctx.closePath();
// 记录轨迹
points.unshift(trajectory);
};
// 擦除
const withdrawPoint = () => {
const index = points.findIndex(item => item.type === 'touchstart');
const pointsWith = points.splice(0, index + 1);
pointsWith.forEach((item: pointsType) => {
withdrawPointItem(item);
});
};
// 采用重叠画布擦除的方式
const withdrawPointItem = (trajectory: pointsType) => {
// 切换globalCompositeOperation 属性值
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
// 随意取一不为透明的颜色
ctx.strokeStyle = 'red';
ctx.moveTo(startX, startY);
ctx.lineTo(trajectory.x, trajectory.y);
ctx.stroke();
ctx.closePath();
startX = trajectory.x;
startY = trajectory.y;
};
emmm,理论行通了,但是ctx.globalCompositeOperation = 'destination-out' ,出来的效果却不尽人意,点击一次撤回,覆盖的部分不是非常完整。
于是灵活多变的我又开始寻找其他API,很快,我精锐的目光定位clearRect(),该API 可以通过把像素设置为透明以达到擦除一个矩形区域的目的
// 采用重叠画布擦除的方式,失败
const withdrawPointItem = (trajectory: pointsType) => {
// 矩形宽高就取线条的粗细,我的为3
ctx.clearRect(trajectory.x, trajectory.y, 3, 3);
ctx.beginPath();
// 线条颜色就取透明色,同样在开始绘制时换回初始线条颜色
ctx.strokeStyle = 'transparent';
ctx.moveTo(startX, startY);
ctx.lineTo(trajectory.x, trajectory.y);
ctx.stroke();
ctx.closePath();
startX = trajectory.x;
startY = trajectory.y;
};
emmm,效果依旧不尽人意,以小区域的方法去清除,清除不干净,扩大区域又会影响其他笔划
呜呜呜,跟狗啃一样,还不如上一个呢,这也不行,还是进厂上班吧我
// 首先我们除了需要记录下触屏开始,移动,同样要记录下结束触屏的终止点
const drawEnd = () => {
points.unshift({
x: startX,
y: startY,
type: 'touchend',
});
};
// 移除上一步的轨迹后重新绘制
const withdrawPoint = () => {
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
// 每次开始触屏记录一个type 为' touchstart ' 的标识
const index = points.findIndex(item => item.type === 'touchstart');
points.splice(0, index + 1);
points.forEach((item: pointsType) => {
// 获取到终止点
if (item.type === 'touchend') {
startX = item.x;
startY = item.y;
}
// 调用移动轨迹去绘制,加一个状态是决定要不要将轨迹保存下来
drawMove(item, false);
});
};
// 触屏移动绘制 isSave => 如果是撤回上一步,就不必保存到轨迹中
const drawMove = (trajectory: pointsType, isSave: Boolean) => {
strokePoint(trajectory);
startX = trajectory.x;
startY = trajectory.y;
if (isSave) {
points.unshift(trajectory);
}
};
那么,这个最终结果可不可行呢,当然是可行的,否则现在的我就该是灵活就业状态了(不信的大家可以动手试试哦),呜呜呜...
ok,以上便是这个集重开,撤回于一身的签名组件的诞生过程,说不尽的心酸与菜鸡主人的不断实践下得出来的产物。希望对大家有那么一丢丢的帮助,有啥好的方法的话,可以留言给我,非常感谢!!!