Canvas简历编辑器-选中绘制与拖拽多选交互方案
在之前我们聊了聊如何基于Canvas
与基本事件组合实现了轻量级DOM
,并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级DOM
的基础上,关注于实现选中绘制与拖拽多选交互设计。
- 在线编辑: https://windrunnermax.github.io/CanvasEditor
- 开源地址: https://github.com/WindrunnerMax/CanvasEditor
关于Canvas
简历编辑器项目的相关文章:
- 社区老给我推Canvas,我也学习Canvas做了个简历编辑器
- Canvas图形编辑器-数据结构与History(undo/redo)
- Canvas图形编辑器-我的剪贴板里究竟有什么数据
- Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)
- Canvas简历编辑器-Monorepo+Rspack工程实践
- Canvas简历编辑器-层级渲染与事件管理能力设计
- Canvas简历编辑器-选中绘制与拖拽多选交互方案
选中绘制
我们先来聊一聊最基本的节点点击选中以及拖拽的交互,而在聊具体的代码实现之前,我们先来看一下对于图形的绘制问题。在Canvas
中我们绘制路径的话,我们可以通过fill
来填充路径,也可以通过stroke
来描边路径,而在我们描边的时候,如果不注意的话可能会陷入一些绘制的问题。假如此时我们要绘制一条线,我们可以分别来看下使用stroke
和fill
的绘制方法实现,此时如果在高清ctx.scale(devicePixel, devicePixel)
情况下,则能明显地看出来绘制位置差0.5px
,而如果基准为1px
的话则会出现1px
的差值以及色值偏差。
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.lineWidth = 1;
ctx.moveTo(5, 5);
ctx.lineTo(100, 5);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "red";
ctx.beginPath();
ctx.moveTo(100, 5);
ctx.lineTo(200, 5);
ctx.lineTo(200, 6);
ctx.lineTo(100, 6);
ctx.closePath();
ctx.fill();
在先前的选中图形frame
中,我们都是用stroke
来实现的,然后最近我想将其真正作为外边框来绘制,然后就发现想绘制inside stroke
确实不是一件容易的事。从MDN
上阅读stroke
的文档可以得到其是以路径的中心线为基准的,也就是说stroke
是由基准分别向内外扩展的,那么问题就来了,假如我们绘制了一条线,而这条线本身是存在1px
宽度的,那么初步理解按照文档所说其本身结构应该是以这1px
本身的中心点也就是0.5px
的位置为中心点向外发散,然而其实际效果是以1px
的外边缘为基准发散,那么就会导致1px
的线在stroke
之后会多出0.5px
的宽度,这个效果可以通过lineTo(0, 100)
外加lineWith=1
来测试,可以发现其可见宽度只有0.5px
,这点可以通过再画一个1px
的Path
来对比。
ctx.beginPath();
ctx.lineWidth = 6;
ctx.strokeStyle = "blue";
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "red";
ctx.moveTo(100, 3);
ctx.lineTo(200, 3);
ctx.closePath();
ctx.stroke();
那么这里的Strokes are aligned to the center of a path
可能与我理解的center of a path
并不相同,或许其只是想表达stroke
是分别向两侧绘制描边的,而并不是解释其基准位置。关于这个问题我咨询了一下,这里主要是理解有偏差,在我们使用API
绘制路径时,本身并没有设置宽度的信息,而坐标信息定义的是路径的轮廓或边界,因此我们在最开始定义的路径结构1px
是不成立的。在图形学的上下文中,路径path
通常是指一个几何形状的轮廓或线条,路径本身是数学上的抽象概念,没有宽度,只是一个由点和线段构成的轨迹,因此当我们提到描边stroke
时,指的是一个可视化过程,即在路径的周围绘制有宽度的线条。
实际上这里如果仅仅是处理frame
的问题的话,可能并没有太大的问题,然而在处理节点的时候,发现由于是使用stroke
绘制的操作节点,那么实际上其总是会超出原始宽度的,也就是上边说的描边问题,而因为超出的这0.5px
的边缘节点,使得我一直认为绘制节点的边缘与填充是没问题的,然而今天才发现这里的顺序反了,描边的内部会被填充覆盖掉,也就是说实现的border
宽度总是会被除以2
的,因此要先填充再描边才是正确的绘制方式。此外,无论是frame
节点的绘制还是类似border
的绘制,在Firefox
中inside stroke
总是会出现兼容性问题,仅有组合fill
以及使用fill
配合Path2D + clip
才能绘制正常的inside stroke
。
ctx.save();
ctx.beginPath();
ctx.arc(70, 75, 50, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "white";
ctx.fill();
ctx.closePath();
ctx.restore();
ctx.save();
ctx.beginPath();
ctx.arc(200, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.closePath();
ctx.restore();
那么我们就可以利用三种方式绘制inside stroke
,当然还有借助lineTo/fillRect
分别绘制4
条边的方式我们没有列举,因为这种方式自然不会出现什么问题,其本身就是使用fill
的方式绘制的,而我们这里主要是讨论stroke
的绘制问题,只是借助Path2D
同样也是fill
的方式绘制的,但是这里需要讨论一下clip
的fillRule-nonzero/evenodd
的问题。那么借助stroke
的特性,方式1
是我们绘制两倍的lineWidth
,然后裁剪掉外部的描边部分,这样就能够正确保留内部的描边了,方式2
则是我们主动校准了描边的位置,将其向内缩小0.5px
的位置,由此来绘制完整的描边,方式3
是借助evenodd
的填充规则,通过clip
来生成规则保留内部的描边,再来实际填充即可实现。
<canvas id="canvas" width="800" height="800"></canvas>
<script>
// https://stackoverflow.com/questions/36615592/canvas-inner-stroke
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const devicePixelRatio = Math.ceil(window.devicePixelRatio || 1);
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
canvas.style.width = width + "px";
canvas.style.height = height + "px";
ctx.scale(devicePixelRatio, devicePixelRatio);
ctx.save();
ctx.beginPath();
ctx.rect(10, 10, 150, 100);
ctx.clip();
ctx.closePath();
ctx.lineWidth = 2;
ctx.strokeStyle