一个充满心酸、一波三折与不断实践的签名组件诞生的过程

154 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天
最近在写vue3 组件库
( 众白:切,又是经典的重复造轮子,菜鸡,不看了)

伤心.jpg

那么这个组件的起源是这样子滴,那天,月黑风高,晴空万里,(没睡醒猪脑过载中....)
我正静心地造着我的组件库轮子,组长给我丢了个需求,因为移动端有签名的操作,所以需要一个签名的组件,我一想,就这?不就是一个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 啊。
组长:话是这么说没错,但是如果我前面签好了两个字,我失误了,但我不想重开,我只想上天再给我一次机会,让我回到上一步,如果非要加个次数,我希望是一万次。

???你说加功能就加功能呗,怎么还演起来了。

我:是什么让你这么执着于撤回上一步呢
组长:你看我这字写得,手抖画错了下一步,这么好的字,这不可惜了吗

组长签名.jpg

我定睛一看
我:哇塞,组长,你的字真的好哇塞哦,点点如刀,笔笔似桃,好似瀑布直下三千尺般顺畅,又有重峦叠嶂不老松般的韧劲。
组长眉毛一挑,嘴角微扬:快做吧,这个月的绩效我帮你提了
我:好的好的,谢谢组长,我对组长您的敬佩如长江流水绵绵不绝,巴拉巴拉巴拉...

说完这一整套,我开始考虑起这个撤回功能的思路,撤回,就是抹除上一步的痕迹,首先想到是使用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' ,出来的效果却不尽人意,点击一次撤回,覆盖的部分不是非常完整。

globalCompositeOperation 撤回.jpg
于是灵活多变的我又开始寻找其他API,很快,我精锐的目光定位clearRect(),该API 可以通过把像素设置为透明以达到擦除一个矩形区域的目的

重来.jpg

理论上来说和我撤回上一步的想法非常契合,话不多说,开搞:
//  采用重叠画布擦除的方式,失败
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,效果依旧不尽人意,以小区域的方法去清除,清除不干净,扩大区域又会影响其他笔划

clear 撤回.jpg

呜呜呜,跟狗啃一样,还不如上一个呢,这也不行,还是进厂上班吧我

进厂.jpg

正难过之际,突然又想到,撤回上一步,回到上一步,emmm,应该是我一开始的思路不对(哇,这个人思想出了问题),撤回使用擦除的方法行不通,那我就曲线救国,通过记录绘制过程,然后移除上一步的绘制,再重新绘制剩余的部分。ok,话不多说,开始实践出真知:
//  首先我们除了需要记录下触屏开始,移动,同样要记录下结束触屏的终止点
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,以上便是这个集重开,撤回于一身的签名组件的诞生过程,说不尽的心酸与菜鸡主人的不断实践下得出来的产物。希望对大家有那么一丢丢的帮助,有啥好的方法的话,可以留言给我,非常感谢!!!

爱你.jpg

如果有什么写的不好或者不对的地方,偷偷私信我就好了,别让我太尴尬,哈哈哈哈哈。