众所周知,Canvas 清除画布内容的 API 是 clearRect 方法,且调用该方法 清除的是矩形区域。日常中,用的更多的却是 圆形区域 的橡皮擦。
要实现圆形效果的橡皮擦,就不得不提 Canvas API 中的 clip 裁剪方法了,实现的方案就是根据尺寸大小,绘制相应的圆(arc),接着将这部分内容裁剪,在裁剪的区域内,执行 clearRect 方法,清除掉整个裁剪区域,不影响主屏内容。
Pointerdown
鼠标按下的同时,更新 drawing 状态,并调用自定义 eraser 方法,将该点擦除。
/*** 按下操作.* @param event*/protected pointerdown(event: MouseEvent | PointerEvent | Touch): void { this.drawing = true;this.ctx.lineJoin = this.ctx.lineCap = 'round'; this.ctx.lineWidth = 20; // 擦除 const point = {x: event.clientX, y: event.clientY};this.erase(point);}
Pointermove
同理,不断移动的过程中,不断的调用 eraser 方法擦除相应的点。
/*** 移动.* @param event*/protected pointermove(event: PointerEvent | MouseEvent): void { if (this.drawing) { // 因为第一个点已经在按下的时候擦除 if (this.data.length > 1) { } }}
earse
擦除动作,先调用 save 保存 Canvas 状态,接着调用 clip 方法进行裁剪,clearRect 清除,最后 restore 恢复。
/*** 擦除.* @param point* @param ctx*/protected erase(point: {x: number; y: number;}): void { this.ctx.save();this.ctx.beginPath();this.ctx.arc(point.x, point.y, 10, 0, Math.PI * 2);this.ctx.clip();this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.restore();}
Pointerup
更新 drawing 状态,避免鼠标没有按下的情况下,再次触发了移动事件。
/*** 鼠标抬起.* @param event*/protected pointerup(event: PointerEvent | MouseEvent): void { if (this.drawing) this.drawing = false;}
完了?想法很美好,现实却非常残酷,实际出来的效果,不忍直视。
原因是鼠标在移动过程中,并不会记录所有的移动信息,而是通过取插值的方法拿到鼠标的坐标信息,移动速度太快的时候,根本来不及响应。既然如此,那就手动把不连贯的两个点连起来,再将连起来的这部分裁剪下来清除掉,这样就可以不管两个点连不连贯了,知道这两点坐标即可,如下图所示:
改进 Eraser 方法
求两个圆点之间形成的矩形坐标,这个是关键,关系到夹角 / 弧度的问题,涉及了三角函数(正切 / 正弦 / 余弦之类的这里就不赘述了)。
/*** 擦除(连贯).* 根据两点之间连成一条直线, 再用clip裁剪后擦除.* @param p1* @param p2*/protected erase( p1: {x: number; y: number;}, p2: {x: number; y: number;}): void { const radius = 20; // 求两圆点之间的矩形坐标,这个是重点 const sin = radius * Math.sin(Math.atan((p1.y - p2.y) / (p1.x - p2.x))), cos = radius * Math.cos(Math.atan((p1.y - p2.y) / (p1.x - p2.x)));const lt = {x: p2.x + sin, y: p2.y - cos},lb = {x: p2.x - sin, y: p2.y + cos},rt = {x: p1.x + sin, y: p1.y - cos},rb = {x: p1.x - sin, y: p1.y + cos};// 开始擦除this.ctx.save();this.ctx.beginPath();this.ctx.arc(p1.x, p1.y, radius, 0, Math.PI * 2);this.ctx.closePath();this.ctx.clip();this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.restore();// 用 lineTo 连接四个点, 形成裁剪区域ctx.save();ctx.beginPath();ctx.moveTo(lt.x, lt.y);ctx.lineTo(rt.x, rt.y);ctx.lineTo(rb.x, rb.y);ctx.lineTo(lb.x, lb.y);ctx.closePath();ctx.clip();ctx.restore();}
小结
主要是调用 clip 裁剪后清除,不影响主屏。再者就是移动速度太快的时候,出现不连贯的擦除效果,需要用到三角函数来计算两圆点连成的线,与坐标系之间形成的角度,方便根据两点来绘制矩形的擦除区域,进而再裁剪擦除。这两点实现了,圆形橡皮擦基本就OK了。