大家好,我是前端西瓜哥。
本文将探究图形编辑器中钢笔工具的功能,算是一篇简单说明书。
只有理解了需求,尤其是复杂的需求,才能更好地进行功能开发,写出诗一样的高鲁棒性代码。
三阶贝塞尔曲线组成的路径
钢笔绘制的是曲线,通常使用 三阶贝塞尔曲线 进行表达。
三阶贝塞尔曲线使用 4 个点:锚点 1、控制点 1、控制点2、锚点 2 来表达一条最小单位的曲线,如下图:
锚点(Anchor Points)是路径上的点,控制点(Control Points)和锚点配对出现,控制曲线的方向和弯曲程度。
原理简单来说,就是这 4 个点依次连接为 3 条线,然后每条线各自从起点向终点,按相同百分比取点位置,然后这些点继续重复连线和取点的操作,最后得到单点。
从起点不断移动到终点,这个点所经过的路径为这个贝塞尔曲线的形状。
一条三阶贝塞尔能表达的曲线还是太简单了。
所以为了表达更复杂的曲线,我们选择 将多个三阶贝塞尔曲线依次首尾相连,表达为 "路径"(Path)。
另外,如果保持上一条曲线的控制点 2 和下一条曲线的控制线 基于公共锚点对称,就能有平滑的效果。
路径可以表达任何形状,比如矩形、椭圆、多边形。甚至你可以在设计图形类的时候,仅仅用一个 Path 类,完全足够的。
至于铅笔工具,其实就是将连续的多段直线线段通过算法进行平滑化处理,转换为三阶贝塞尔曲线组成的路径。
虽然看起来很方便,但通常会产生大量冗余点,不如用钢笔工具清爽。不过倒是适合配合触控笔使用。
路径数据结构设计
三阶贝塞尔曲线的数据结构有两种设计思路。
(1)curve 表达
一种是参考 SVG 的 Path 元素中对三阶贝塞尔曲线的表达。
css
{
curves: [
// 起点
{ point: { x: 0, y: 0 } },
// 锚点 2 和两个控制点。
// 配合上一个锚点,组成一个最小单位的三阶贝塞尔曲线
{
point: { x: 100, y: 10 },
handle1: { x: 10, y: -40 },
handle2: { x: 90, y: 30 },
},
// ...
],
closed: false, // 是否闭合
};
因为前一段三阶贝塞尔曲线的锚点 2 和后一段的锚点 1 是相同的,我们会忽略掉重复的锚点 1 数据。
handle1 和 handle2 是可选的,没有就是和锚点重合了。handle1 和 handle2 如果都没有,则表达为一条直线。
这种表达更贴近三阶贝塞尔曲线的原始意图。
(2)segment 表达
我们还有另一种表达:分成多个同样的片段。每个片段由 1 个锚点,以及 2 个控制点组成,见下图。
控制点为该锚点在相邻两段三阶贝塞尔曲线的两个控制点:入控制点(handleIn)和出控制点(handleOut)。
handleIn 和 handleOut 可以用绝对坐标,也可以用相对锚带你的坐标。
数据结构:
css
{
segments: [
{
point: { x: 0, y: 0 },
handleOut: { x: 10, y: -40 },
},
{
point: { x: 100, y: 10 },
handleIn: { x: 90, y: 30 },
handleOut: { x: 110, y: 30 },
},
{
point: { x: 200, y: 0 },
handleIn: { x: 190, y: -20 },
// 这里可保留路径终点趋势
// handleOut: { x: 220, y: 40 },
},
// ...
],
closed: true,
}
我个人更推荐这种表达,它更优雅,且更匹配钢笔的绘制行为的抽象:绘制路径的每次拖拽其实就是创建了一个 segment,这个用 curve 就不好表达,比较碎片。
此外 segment 表达最后一个锚点时,可以用 handleOut 属性自然地保留下一条贝塞尔曲线的趋势,之后从末尾续一条曲线时,就不需要再进行额外操作,去设置控制点 1。路径的起点同理。
如果要用 curve 的表达,也是可以的,比较别扭,需要额外两个属性。
绘制路径
使用钢笔工具绘制路径,其交互为:
-
鼠标按下,确定新曲线锚点 1 位置,以及上一个曲线;
-
鼠标按下不放,然后移动进行拖拽,确定控制点 1 位置。此时可以使用对称策略更新上一条曲线的控制点 2;
-
鼠标释放,此时移动光标,会有一个 预测曲线,表示如果在当前位置按下鼠标,所产生的新曲线形状。
-
鼠标如果点在起点附近,会将路径进行闭合。
编辑路径
路径曲线画好了,可能有瑕疵,需要微调。
对于绘制好的路径,需支持的常用编辑操作有如下几种。
1、修改锚点位置,对应的控制点也会移动,需要一起修改。如果控制点使用相对位置,甚至不用改。
2、修改锚点,修改曲线的弯曲程度。
因为线条大多情况下要求平滑,所以默认会使用 "锚点对称+长度相等" 效果,此外还有 "锚点对称" 和 "不对称"。
3、修改某段曲线的位置,等价于移动曲线的两个锚点。
4、 添加锚点,在一段曲线的中间某个位置加一个锚点,并保存操作前后形状不变。
4、减少锚点,该锚点会丢弃,然后它的前后两个锚点连接,因为信息变少了,通常无法保持原来的形状。
5、删除锚点或曲线,选中后按下删除键。它会将一条路径从中间断开,如果没有闭合会断开为两个路径,如果闭合就会变成一条不闭合的路径;
6、弯曲(Bend)效果:可以在一段曲线上的某个点拖动,光标所在点会保持在新的曲线形状上。
Figma 的加强版钢笔工具
这里再简单介绍下 Figma 的钢笔工具。
Figma 的钢笔工具,和一般的钢笔工具有点不一样,它做了一些 遥遥领先的创新。
它也是使用了三阶贝塞尔曲线,但画的不再是路径,而是网格了。
Figma 称这种特殊的曲线为 Vector Network(矢量网格)。
路径是一条线,由多个小的曲线依次连接而成,从起点出发,会经过所有的锚点,最后到达终点,所形成的这么一条线。
Figma 的矢量网格是图(graph),它在路径的基础上做了增强,可以有分岔,如下图。
矢量网格对设计师来说是友好的,它让绘制 UI 变得更得心应手,不习惯也能使用原来的绘制路径的方式,矢量网格完全兼容路径。
但对开发来说,表达矢量网格的数据结构就复杂得多,需要使用一种类似邻接表的表达,要考虑的场景更多了。
(开发:你这个,不太好实现。。产品你是不是有。。额。。。诶,原)
首先是用数组记录好所有顶点的数据,数组的索引值为顶点的唯一标识。
css
vertices: [
/* 点 0 */ { x: 0, y: 0 },
/* 点 1 */ { x: 30, y: 5 },
// ...
]
然后是找点与点之间的邻接关系,假设对于点 0,它连接到了点 1、点 2、点 3,产生了 3 条贝塞尔曲线,那它的表达如下。
css
segments: [
{
// 点 1 和 3 相连,并设置它们的控制点,并使用相对位置
start: { vertex: 0, dx: 38, dy: -48 },
end: { vertex: 1, dx: -37, dy: -22 }
},
{
start: { vertex: 1, dx: 47, dy: 28 },
end: { vertex: 2, dx: 38, dy: -17 }
},
{
start: { vertex: 0, dx: -23, dy: 37 },
end: { vertex: 3, dx: -47, dy: -40 }
}
//...
]
最后是子区域的划分,从这些线中找一些线组成成的闭合子区域,如果设置了填充色,就会往这些区域填充颜色。
css
regions: [
{
windingRule: 'NONZERO', // 绕数非零填充规则
loops: [
{
// 组成子区域的 4 个顶点
segments: [0, 4, 5, 1],
},
],
},
];
结尾
以上就是钢笔工具的功能说明书,看完就应该知道怎么实现钢笔工具了吧,赶紧开写吧。
我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。
相关阅读,